mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-25 00:51:28 +02:00
Align Android logout semantics with the desktop UI and CLI: logging out no longer deletes the stored account email, so the next login passes it as the OIDC login_hint and the IdP preselects the account. Removing the profile is now the operation that deletes the email; previously RemoveProfile left the account file behind, which the fixed-name default profile would have inherited on recreation.
162 lines
4.5 KiB
Go
162 lines
4.5 KiB
Go
package android
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestProfileAccountPathFor(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
configPath string
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "default profile",
|
|
configPath: "/data/data/io.netbird.client/files/netbird.cfg",
|
|
want: filepath.FromSlash("/data/data/io.netbird.client/files/netbird.account.json"),
|
|
},
|
|
{
|
|
name: "id profile",
|
|
configPath: "/data/data/io.netbird.client/files/profiles/4c5f5c8198c3989cffb5b5394f5a7ae0.json",
|
|
want: filepath.FromSlash("/data/data/io.netbird.client/files/profiles/4c5f5c8198c3989cffb5b5394f5a7ae0.account.json"),
|
|
},
|
|
{
|
|
name: "legacy name-keyed profile is handled the same way",
|
|
configPath: "/data/data/io.netbird.client/files/profiles/work.json",
|
|
want: filepath.FromSlash("/data/data/io.netbird.client/files/profiles/work.account.json"),
|
|
},
|
|
{
|
|
name: "empty path is rejected",
|
|
configPath: "",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := profileAccountPathFor(tt.configPath)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("expected an error, got path %q", got)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("got %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProfileAccountPathForDefaultDoesNotCollide(t *testing.T) {
|
|
root := "/data/data/io.netbird.client/files"
|
|
|
|
defaultAccount, err := profileAccountPathFor(filepath.Join(root, defaultConfigFilename))
|
|
if err != nil {
|
|
t.Fatalf("default profile: %v", err)
|
|
}
|
|
|
|
idAccount, err := profileAccountPathFor(filepath.Join(root, profilesSubdir, "abc123.json"))
|
|
if err != nil {
|
|
t.Fatalf("id profile: %v", err)
|
|
}
|
|
|
|
if defaultAccount == idAccount {
|
|
t.Fatalf("default and id profile share an account file: %q", defaultAccount)
|
|
}
|
|
}
|
|
|
|
// The account file must never land on the engine state file: on Android both
|
|
// resolve under files/, and the state manager rewrites the whole file from its
|
|
// own keys, so sharing a path would have the two overwrite each other. The
|
|
// expected names here mirror ProfileManager.GetStateFilePath.
|
|
func TestProfileAccountPathAvoidsEngineStateFile(t *testing.T) {
|
|
root := "/data/data/io.netbird.client/files"
|
|
|
|
cases := []struct {
|
|
configPath string
|
|
engineState string
|
|
}{
|
|
{
|
|
configPath: filepath.Join(root, defaultConfigFilename),
|
|
engineState: filepath.Join(root, "state.json"),
|
|
},
|
|
{
|
|
configPath: filepath.Join(root, profilesSubdir, "abc123.json"),
|
|
engineState: filepath.Join(root, profilesSubdir, "abc123.state.json"),
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
account, err := profileAccountPathFor(c.configPath)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", c.configPath, err)
|
|
}
|
|
if account == c.engineState {
|
|
t.Errorf("account file collides with the engine state file: %q", account)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWriteThenReadProfileEmail(t *testing.T) {
|
|
configPath := filepath.Join(t.TempDir(), "profiles", "abc123.json")
|
|
if err := ensureDirFor(t, configPath); err != nil {
|
|
t.Fatalf("prepare dir: %v", err)
|
|
}
|
|
|
|
if got := readProfileEmail(configPath); got != "" {
|
|
t.Errorf("expected no email before a login, got %q", got)
|
|
}
|
|
|
|
const email = "user@example.com"
|
|
if err := writeProfileEmail(configPath, email); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
if got := readProfileEmail(configPath); got != email {
|
|
t.Errorf("got %q, want %q", got, email)
|
|
}
|
|
|
|
if err := removeProfileEmail(configPath); err != nil {
|
|
t.Fatalf("remove: %v", err)
|
|
}
|
|
if got := readProfileEmail(configPath); got != "" {
|
|
t.Errorf("expected no email after removal, got %q", got)
|
|
}
|
|
|
|
// Removal may run on a never-logged-in profile, so a second remove must pass.
|
|
if err := removeProfileEmail(configPath); err != nil {
|
|
t.Fatalf("second remove should be a no-op: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWriteProfileEmailIgnoresEmpty(t *testing.T) {
|
|
configPath := filepath.Join(t.TempDir(), "profiles", "abc123.json")
|
|
if err := ensureDirFor(t, configPath); err != nil {
|
|
t.Fatalf("prepare dir: %v", err)
|
|
}
|
|
|
|
const email = "user@example.com"
|
|
if err := writeProfileEmail(configPath, email); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
if err := writeProfileEmail(configPath, ""); err != nil {
|
|
t.Fatalf("write empty: %v", err)
|
|
}
|
|
|
|
if got := readProfileEmail(configPath); got != email {
|
|
t.Errorf("empty write clobbered the stored email: got %q, want %q", got, email)
|
|
}
|
|
}
|
|
|
|
func ensureDirFor(t *testing.T, path string) error {
|
|
t.Helper()
|
|
return os.MkdirAll(filepath.Dir(path), 0o700)
|
|
}
|