mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-25 00:51:28 +02:00
The Android binding never recorded which account a profile belongs to, so every interactive login and every session extend went to the IdP with no login_hint. With nothing to go on the IdP picks an account itself, which on a session extend means re-authenticating an account the profile is already signed in with. Store the email the PKCE flow already parses out of the ID token, and pass it back as the hint on later flows. An empty hint stays meaningful: a fresh profile, or one that was logged out, deliberately leaves the choice to the IdP, which is how a profile changes accounts. Logout clears the stored email for that reason — while it is on disk it would steer the next login straight back into the account just logged out of. The email is keyed off the profile's config path rather than the active profile: Auth.login runs in a goroutine, so the active profile can change under a flow already in flight. It lands in <profile>.account.json, not the <profile>.state.json desktop uses for the same data — there the email and the engine's state manager sit in different directories, but on Android both resolve under files/, and the state manager rewrites the whole file from its own keys.
162 lines
4.4 KiB
Go
162 lines
4.4 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: "/data/data/io.netbird.client/files/netbird.account.json",
|
|
},
|
|
{
|
|
name: "id profile",
|
|
configPath: "/data/data/io.netbird.client/files/profiles/4c5f5c8198c3989cffb5b5394f5a7ae0.json",
|
|
want: "/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: "/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 logout, got %q", got)
|
|
}
|
|
|
|
// Logout 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)
|
|
}
|