package profilemanager import ( "context" "errors" "os" "os/user" "path/filepath" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/util" ) // withTestSM wires up patched globals + a clean config dir and returns a // fully initialized ServiceManager plus the username we are scoped to. func withTestSM(t *testing.T, fn func(sm *ServiceManager, username string)) { t.Helper() withTempConfigDir(t, func(configDir string) { withPatchedGlobals(t, configDir, func() { u, err := user.Current() require.NoError(t, err) sm := &ServiceManager{} require.NoError(t, sm.CreateDefaultProfile()) fn(sm, u.Username) }) }) } func TestServiceProfile_ExactID(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { created, err := sm.AddProfile("work", username) require.NoError(t, err) got, err := sm.ResolveProfile(created.ID.String(), username) require.NoError(t, err) assert.Equal(t, created.ID, got.ID) assert.Equal(t, "work", got.Name) }) } func TestServiceProfile_IDPrefix(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { created, err := sm.AddProfile("work", username) require.NoError(t, err) prefix := created.ID[:4] got, err := sm.ResolveProfile(prefix.String(), username) require.NoError(t, err) assert.Equal(t, created.ID, got.ID) }) } func TestServiceProfile_AmbiguousPrefix(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { // Plant two profiles whose IDs share a known prefix by writing // the files directly, since generated IDs are random. configDir, err := sm.getConfigDir(username) require.NoError(t, err) for _, id := range []string{"abcd1111aaaa", "abcd2222bbbb"} { path := filepath.Join(configDir, id+".json") require.NoError(t, util.WriteJson(context.Background(), path, &Config{Name: id})) } _, err = sm.ResolveProfile("abcd", username) var amb *ErrAmbiguousHandle require.ErrorAs(t, err, &amb) assert.Equal(t, AmbiguityKindIDPrefix, amb.Kind) assert.Len(t, amb.Candidates, 2) }) } func TestServiceProfile_ExactNameUnique(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { _, err := sm.AddProfile("work", username) require.NoError(t, err) got, err := sm.ResolveProfile("work", username) require.NoError(t, err) assert.Equal(t, "work", got.Name) }) } func TestServiceProfile_AmbiguousName(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { _, err := sm.AddProfile("work", username) require.NoError(t, err) _, err = sm.AddProfile("work", username) require.NoError(t, err) _, err = sm.ResolveProfile("work", username) var amb *ErrAmbiguousHandle require.ErrorAs(t, err, &amb) assert.Equal(t, AmbiguityKindName, amb.Kind) assert.Len(t, amb.Candidates, 2) }) } func TestServiceProfile_NotFound(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { _, err := sm.ResolveProfile("nope", username) assert.ErrorIs(t, err, ErrProfileNotFound) }) } func TestServiceProfile_DefaultByExactID(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { got, err := sm.ResolveProfile(defaultProfileName, username) require.NoError(t, err) assert.Equal(t, defaultProfileName, got.ID.String()) }) } func TestServiceProfile_LegacyFilenameCoexists(t *testing.T) { // Legacy profiles stored as .json with no "name" JSON field // should still be discoverable by name and removable by name. withTestSM(t, func(sm *ServiceManager, username string) { configDir, err := sm.getConfigDir(username) require.NoError(t, err) path := filepath.Join(configDir, "legacy.json") require.NoError(t, util.WriteJson(context.Background(), path, &Config{})) got, err := sm.ResolveProfile("legacy", username) require.NoError(t, err) assert.Equal(t, "legacy", got.ID.String()) // Name falls back to the filename stem when JSON omits it. assert.Equal(t, "legacy", got.Name) }) } func TestAddProfile_AllowsDuplicateWithFlag(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { first, err := sm.AddProfile("work", username) require.NoError(t, err) second, err := sm.AddProfile("work", username) require.NoError(t, err) assert.NotEqual(t, first.ID, second.ID) assert.Equal(t, "work", second.Name) }) } func TestAddProfile_RejectsInvalidNames(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { cases := []string{ "", // empty "\x00\x01", // only control chars (becomes empty) strings.Repeat("a", maxProfileNameLen+1), // too long } for _, name := range cases { _, err := sm.AddProfile(name, username) assert.Error(t, err, "expected error for %q", name) } }) } func TestRemoveProfile_RejectsInvalidID(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { err := sm.RemoveProfile("../escape", username) assert.Error(t, err) }) } func TestSanitizeDisplayName(t *testing.T) { cases := []struct { in string want string wantErr bool }{ {"work", "work", false}, {"My Work Account", "My Work Account", false}, {"emoji 🚀 ok", "emoji 🚀 ok", false}, {"漢字テスト", "漢字テスト", false}, {"with\x00null", "withnull", false}, {"\x01\x02\x03", "", true}, {"", "", true}, } for _, tc := range cases { got, err := sanitizeDisplayName(tc.in) if tc.wantErr { assert.Error(t, err, "case %q", tc.in) continue } assert.NoError(t, err, "case %q", tc.in) assert.Equal(t, tc.want, got, "case %q", tc.in) } } func TestIsValidProfileFilenameStem(t *testing.T) { cases := []struct { in string want bool }{ {"default", true}, {"abc123def456", true}, {"legacy-name", true}, {"legacy_name", true}, {"", false}, {"..", false}, {"../etc", false}, {"foo/bar", false}, {`foo\bar`, false}, {"with space", false}, {"with.dot", false}, {strings.Repeat("a", maxProfileIDLen+1), false}, } for _, tc := range cases { got := IsValidProfileFilenameStem(ID(tc.in)) assert.Equal(t, tc.want, got, "case %q", tc.in) } } func TestRemoveProfile_DeletesStateFile(t *testing.T) { withTestSM(t, func(sm *ServiceManager, username string) { created, err := sm.AddProfile("work", username) require.NoError(t, err) configDir, err := sm.getConfigDir(username) require.NoError(t, err) statePath := filepath.Join(configDir, created.ID.String()+".state.json") require.NoError(t, os.WriteFile(statePath, []byte(`{"email":"a@b"}`), 0600)) require.NoError(t, sm.RemoveProfile(created.ID, username)) _, err = os.Stat(statePath) assert.True(t, errors.Is(err, os.ErrNotExist), "state file should be removed") }) }