| package profiles |
|
|
| import ( |
| "encoding/json" |
| "fmt" |
| "net/http" |
| "net/http/httptest" |
| "os" |
| "path/filepath" |
| "strings" |
| "testing" |
| "time" |
|
|
| "github.com/pinchtab/pinchtab/internal/bridge" |
| ) |
|
|
| func TestProfileManagerCreateAndList(t *testing.T) { |
| dir := t.TempDir() |
| pm := NewProfileManager(dir) |
|
|
| if err := pm.Create("test-profile"); err != nil { |
| t.Fatal(err) |
| } |
|
|
| profileDir := filepath.Join(dir, profileID("test-profile")) |
| if _, err := os.Stat(profileDir); err != nil { |
| t.Fatalf("profile directory not created: %s", profileDir) |
| } |
|
|
| defaultDir := filepath.Join(profileDir, "Default") |
| if _, err := os.Stat(defaultDir); err != nil { |
| t.Fatalf("Default directory not created: %s", defaultDir) |
| } |
|
|
| profiles, err := pm.List() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(profiles) != 1 { |
| t.Fatalf("expected 1 profile, got %d", len(profiles)) |
| } |
| if profiles[0].Name != "test-profile" { |
| t.Errorf("expected name test-profile, got %s", profiles[0].Name) |
| } |
| if profiles[0].Source != "created" { |
| t.Errorf("expected source created, got %s", profiles[0].Source) |
| } |
| if !profiles[0].PathExists { |
| t.Errorf("profile path should exist") |
| } |
| if profiles[0].Path != profileDir { |
| t.Errorf("expected path %s, got %s", profileDir, profiles[0].Path) |
| } |
| } |
|
|
| func TestProfileManagerCreateDuplicate(t *testing.T) { |
| dir := t.TempDir() |
| pm := NewProfileManager(dir) |
|
|
| _ = pm.Create("dup") |
| err := pm.Create("dup") |
| if err == nil { |
| t.Fatal("expected error on duplicate create") |
| } |
| } |
|
|
| func TestProfileManagerImport(t *testing.T) { |
| dir := t.TempDir() |
| pm := NewProfileManager(dir) |
|
|
| src := filepath.Join(t.TempDir(), "chrome-src") |
| _ = os.MkdirAll(filepath.Join(src, "Default"), 0755) |
| _ = os.WriteFile(filepath.Join(src, "Default", "Preferences"), []byte(`{}`), 0644) |
|
|
| if err := pm.Import("imported-profile", src); err != nil { |
| t.Fatal(err) |
| } |
|
|
| profiles, _ := pm.List() |
| if len(profiles) != 1 { |
| t.Fatalf("expected 1 profile, got %d", len(profiles)) |
| } |
| if profiles[0].Source != "imported" { |
| t.Errorf("expected source imported, got %s", profiles[0].Source) |
| } |
| } |
|
|
| func TestProfileManagerImportBadSource(t *testing.T) { |
| dir := t.TempDir() |
| pm := NewProfileManager(dir) |
|
|
| err := pm.Import("bad", "/nonexistent/path") |
| if err == nil { |
| t.Fatal("expected error on bad source") |
| } |
| } |
|
|
| func TestProfileManagerListReadsAccountFromPreferences(t *testing.T) { |
| dir := t.TempDir() |
| pm := NewProfileManager(dir) |
| if err := pm.Create("acc-pref"); err != nil { |
| t.Fatal(err) |
| } |
|
|
| prefsPath := filepath.Join(dir, profileID("acc-pref"), "Default", "Preferences") |
| prefs := `{"account_info":[{"email":"alice@pinchtab.com","full_name":"Alice"}]}` |
| if err := os.WriteFile(prefsPath, []byte(prefs), 0644); err != nil { |
| t.Fatal(err) |
| } |
|
|
| profiles, err := pm.List() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(profiles) != 1 { |
| t.Fatalf("expected 1 profile, got %d", len(profiles)) |
| } |
| if profiles[0].AccountEmail != "alice@pinchtab.com" { |
| t.Fatalf("expected account email alice@pinchtab.com, got %q", profiles[0].AccountEmail) |
| } |
| if profiles[0].AccountName != "Alice" { |
| t.Fatalf("expected account name Alice, got %q", profiles[0].AccountName) |
| } |
| if !profiles[0].HasAccount { |
| t.Fatal("expected hasAccount=true") |
| } |
| } |
|
|
| func TestProfileManagerListReadsLocalStateIdentity(t *testing.T) { |
| dir := t.TempDir() |
| pm := NewProfileManager(dir) |
| if err := pm.Create("acc-local"); err != nil { |
| t.Fatal(err) |
| } |
|
|
| localStatePath := filepath.Join(dir, profileID("acc-local"), "Local State") |
| localState := `{"profile":{"info_cache":{"Default":{"name":"Work","user_name":"bob@pinchtab.com","gaia_name":"Bob","gaia_id":"123"}}}}` |
| if err := os.WriteFile(localStatePath, []byte(localState), 0644); err != nil { |
| t.Fatal(err) |
| } |
|
|
| profiles, err := pm.List() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(profiles) != 1 { |
| t.Fatalf("expected 1 profile, got %d", len(profiles)) |
| } |
| if profiles[0].ChromeProfileName != "Work" { |
| t.Fatalf("expected chrome profile name Work, got %q", profiles[0].ChromeProfileName) |
| } |
| if profiles[0].AccountEmail != "bob@pinchtab.com" { |
| t.Fatalf("expected account email bob@pinchtab.com, got %q", profiles[0].AccountEmail) |
| } |
| if profiles[0].AccountName != "Bob" { |
| t.Fatalf("expected account name Bob, got %q", profiles[0].AccountName) |
| } |
| if !profiles[0].HasAccount { |
| t.Fatal("expected hasAccount=true") |
| } |
| } |
|
|
| func TestProfileManagerReset(t *testing.T) { |
| dir := t.TempDir() |
| pm := NewProfileManager(dir) |
| _ = pm.Create("reset-me") |
|
|
| sessDir := filepath.Join(dir, profileID("reset-me"), "Default", "Sessions") |
| _ = os.MkdirAll(sessDir, 0755) |
| _ = os.WriteFile(filepath.Join(sessDir, "session1"), []byte("data"), 0644) |
|
|
| cacheDir := filepath.Join(dir, profileID("reset-me"), "Default", "Cache") |
| _ = os.MkdirAll(cacheDir, 0755) |
|
|
| if err := pm.Reset("reset-me"); err != nil { |
| t.Fatal(err) |
| } |
|
|
| if _, err := os.Stat(sessDir); !os.IsNotExist(err) { |
| t.Error("Sessions dir should be removed after reset") |
| } |
| if _, err := os.Stat(cacheDir); !os.IsNotExist(err) { |
| t.Error("Cache dir should be removed after reset") |
| } |
|
|
| if _, err := os.Stat(filepath.Join(dir, profileID("reset-me"))); err != nil { |
| t.Error("Profile dir should still exist after reset") |
| } |
| } |
|
|
| func TestProfileManagerResetNotFound(t *testing.T) { |
| pm := NewProfileManager(t.TempDir()) |
| err := pm.Reset("nope") |
| if err == nil { |
| t.Fatal("expected error") |
| } |
| } |
|
|
| func TestProfileManagerDelete(t *testing.T) { |
| dir := t.TempDir() |
| pm := NewProfileManager(dir) |
| _ = pm.Create("delete-me") |
|
|
| if err := pm.Delete("delete-me"); err != nil { |
| t.Fatal(err) |
| } |
|
|
| profiles, _ := pm.List() |
| if len(profiles) != 0 { |
| t.Errorf("expected 0 profiles after delete, got %d", len(profiles)) |
| } |
| } |
|
|
| func TestActionTracker(t *testing.T) { |
| at := NewActionTracker() |
| profName := fmt.Sprintf("prof-%d", time.Now().UnixNano()) |
|
|
| for i := 0; i < 5; i++ { |
| at.Record(profName, bridge.ActionRecord{ |
| Timestamp: time.Now().Add(time.Duration(i) * time.Second), |
| Method: "GET", |
| Endpoint: "/snapshot", |
| URL: "https://pinchtab.com", |
| DurationMs: 100, |
| Status: 200, |
| }) |
| } |
|
|
| logs := at.GetLogs(profName, 3) |
| if len(logs) != 3 { |
| t.Errorf("expected 3 logs, got %d", len(logs)) |
| } |
|
|
| report := at.Analyze(profName) |
| if report.TotalActions != 5 { |
| t.Errorf("expected 5 total actions, got %d", report.TotalActions) |
| } |
| } |
|
|
| func TestProfileHandlerList(t *testing.T) { |
| pm := NewProfileManager(t.TempDir()) |
| _ = pm.Create("a") |
| _ = pm.Create("b") |
|
|
| mux := http.NewServeMux() |
| pm.RegisterHandlers(mux) |
|
|
| req := httptest.NewRequest("GET", "/profiles", nil) |
| w := httptest.NewRecorder() |
| mux.ServeHTTP(w, req) |
|
|
| if w.Code != 200 { |
| t.Fatalf("expected 200, got %d", w.Code) |
| } |
|
|
| var profiles []bridge.ProfileInfo |
| _ = json.NewDecoder(w.Body).Decode(&profiles) |
| if len(profiles) != 2 { |
| t.Errorf("expected 2 profiles, got %d", len(profiles)) |
| } |
| for _, p := range profiles { |
| if p.Path == "" { |
| t.Fatalf("expected path to be present for profile %q", p.Name) |
| } |
| if !p.PathExists { |
| t.Fatalf("expected pathExists=true for profile %q", p.Name) |
| } |
| } |
| } |
|
|
| func TestProfileHandlerCreate(t *testing.T) { |
| baseDir := t.TempDir() |
| pm := NewProfileManager(baseDir) |
| mux := http.NewServeMux() |
| pm.RegisterHandlers(mux) |
|
|
| body := `{"name": "new-profile"}` |
| req := httptest.NewRequest("POST", "/profiles/create", strings.NewReader(body)) |
| w := httptest.NewRecorder() |
| mux.ServeHTTP(w, req) |
|
|
| if w.Code != 200 { |
| t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) |
| } |
|
|
| idDir := filepath.Join(baseDir, profileID("new-profile")) |
| if _, err := os.Stat(idDir); err != nil { |
| t.Fatalf("expected id-based directory to exist: %s", idDir) |
| } |
| nameDir := filepath.Join(baseDir, "new-profile") |
| if _, err := os.Stat(nameDir); !os.IsNotExist(err) { |
| t.Fatalf("expected name-based directory not to exist: %s", nameDir) |
| } |
| } |
|
|
| func TestProfileHandlerReset(t *testing.T) { |
| pm := NewProfileManager(t.TempDir()) |
| _ = pm.Create("resettable") |
| id := profileID("resettable") |
| mux := http.NewServeMux() |
| pm.RegisterHandlers(mux) |
|
|
| req := httptest.NewRequest("POST", "/profiles/"+id+"/reset", nil) |
| w := httptest.NewRecorder() |
| mux.ServeHTTP(w, req) |
|
|
| if w.Code != 200 { |
| t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) |
| } |
| } |
|
|
| func TestProfileHandlerDelete(t *testing.T) { |
| pm := NewProfileManager(t.TempDir()) |
| _ = pm.Create("deletable") |
| id := profileID("deletable") |
| mux := http.NewServeMux() |
| pm.RegisterHandlers(mux) |
|
|
| req := httptest.NewRequest("DELETE", "/profiles/"+id, nil) |
| w := httptest.NewRecorder() |
| mux.ServeHTTP(w, req) |
|
|
| if w.Code != 200 { |
| t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) |
| } |
| } |
|
|
| func TestProfileMetaReadWrite(t *testing.T) { |
| dir := t.TempDir() |
| pm := NewProfileManager(dir) |
|
|
| meta := ProfileMeta{ |
| UseWhen: "I need to access work email", |
| Description: "Work profile for corporate tasks", |
| } |
| if err := pm.CreateWithMeta("work-profile", meta); err != nil { |
| t.Fatal(err) |
| } |
|
|
| readMeta := readProfileMeta(filepath.Join(dir, profileID("work-profile"))) |
| if readMeta.UseWhen != "I need to access work email" { |
| t.Errorf("expected useWhen 'I need to access work email', got %q", readMeta.UseWhen) |
| } |
| if readMeta.Description != "Work profile for corporate tasks" { |
| t.Errorf("expected description 'Work profile for corporate tasks', got %q", readMeta.Description) |
| } |
| } |
|
|
| func TestProfileUpdateMeta(t *testing.T) { |
| pm := NewProfileManager(t.TempDir()) |
| mux := http.NewServeMux() |
| pm.RegisterHandlers(mux) |
|
|
| _ = pm.Create("updatable") |
|
|
| body := `{"name":"updatable","useWhen":"Updated use case","description":"Updated description"}` |
| req := httptest.NewRequest("PATCH", "/profiles/meta", strings.NewReader(body)) |
| req.Header.Set("Content-Type", "application/json") |
| w := httptest.NewRecorder() |
| mux.ServeHTTP(w, req) |
|
|
| if w.Code != 200 { |
| t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) |
| } |
| } |
|
|
| func TestProfileUpdateByIDCanClearMetadata(t *testing.T) { |
| pm := NewProfileManager(t.TempDir()) |
| mux := http.NewServeMux() |
| pm.RegisterHandlers(mux) |
|
|
| if err := pm.CreateWithMeta("clearable", ProfileMeta{ |
| UseWhen: "Used for work", |
| Description: "Has metadata", |
| }); err != nil { |
| t.Fatal(err) |
| } |
|
|
| body := `{"useWhen":"","description":""}` |
| req := httptest.NewRequest("PATCH", "/profiles/"+profileID("clearable"), strings.NewReader(body)) |
| req.Header.Set("Content-Type", "application/json") |
| w := httptest.NewRecorder() |
| mux.ServeHTTP(w, req) |
|
|
| if w.Code != 200 { |
| t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) |
| } |
|
|
| profiles, err := pm.List() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(profiles) != 1 { |
| t.Fatalf("expected 1 profile, got %d", len(profiles)) |
| } |
| if profiles[0].UseWhen != "" { |
| t.Errorf("expected empty useWhen after clear, got %q", profiles[0].UseWhen) |
| } |
| if profiles[0].Description != "" { |
| t.Errorf("expected empty description after clear, got %q", profiles[0].Description) |
| } |
| } |
|
|
| func TestProfileCreateWithUseWhen(t *testing.T) { |
| pm := NewProfileManager(t.TempDir()) |
| mux := http.NewServeMux() |
| pm.RegisterHandlers(mux) |
|
|
| body := `{"name":"test-usewhen","useWhen":"For testing purposes"}` |
| req := httptest.NewRequest("POST", "/profiles/create", strings.NewReader(body)) |
| req.Header.Set("Content-Type", "application/json") |
| w := httptest.NewRecorder() |
| mux.ServeHTTP(w, req) |
|
|
| if w.Code != 200 { |
| t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) |
| } |
|
|
| profiles, err := pm.List() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(profiles) != 1 { |
| t.Fatalf("expected 1 profile, got %d", len(profiles)) |
| } |
| if profiles[0].UseWhen != "For testing purposes" { |
| t.Errorf("expected useWhen 'For testing purposes', got %q", profiles[0].UseWhen) |
| } |
| } |
|
|
| func TestProfileListIncludesUseWhen(t *testing.T) { |
| pm := NewProfileManager(t.TempDir()) |
|
|
| meta := ProfileMeta{UseWhen: "Personal browsing"} |
| _ = pm.CreateWithMeta("personal", meta) |
|
|
| profiles, err := pm.List() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(profiles) != 1 { |
| t.Fatalf("expected 1 profile, got %d", len(profiles)) |
| } |
| if profiles[0].UseWhen != "Personal browsing" { |
| t.Errorf("expected useWhen 'Personal browsing', got %q", profiles[0].UseWhen) |
| } |
| } |
|
|
| |
|
|
| func TestValidateProfileName(t *testing.T) { |
| tests := []struct { |
| name string |
| input string |
| wantErr bool |
| errMsg string |
| }{ |
| |
| {"valid simple", "my-profile", false, ""}, |
| {"valid with numbers", "profile123", false, ""}, |
| {"valid with underscore", "my_profile", false, ""}, |
| {"valid with dots", "my.profile", false, ""}, |
| {"valid single char", "a", false, ""}, |
|
|
| |
| {"empty", "", true, "cannot be empty"}, |
|
|
| |
| {"double dot", "..", true, "cannot contain '..'"}, |
| {"double dot prefix", "../test", true, "cannot contain '..'"}, |
| {"double dot suffix", "test/..", true, "cannot contain '..'"}, |
| {"double dot middle", "test/../other", true, "cannot contain '..'"}, |
| {"triple dot", "...", true, "cannot contain '..'"}, |
| {"double dot no slash", "..test", true, "cannot contain '..'"}, |
|
|
| |
| {"forward slash", "test/profile", true, "cannot contain '/'"}, |
| {"forward slash prefix", "/test", true, "cannot contain '/'"}, |
| {"forward slash suffix", "test/", true, "cannot contain '/'"}, |
| {"backslash", "test\\profile", true, "cannot contain '/'"}, |
| {"backslash prefix", "\\test", true, "cannot contain '/'"}, |
|
|
| |
| {"traversal with slash", "../../../etc/passwd", true, "cannot contain"}, |
| {"traversal windows", "..\\..\\system32", true, "cannot contain"}, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| err := ValidateProfileName(tt.input) |
| if tt.wantErr { |
| if err == nil { |
| t.Errorf("ValidateProfileName(%q) = nil, want error containing %q", tt.input, tt.errMsg) |
| } else if !strings.Contains(err.Error(), tt.errMsg) { |
| t.Errorf("ValidateProfileName(%q) = %q, want error containing %q", tt.input, err.Error(), tt.errMsg) |
| } |
| } else { |
| if err != nil { |
| t.Errorf("ValidateProfileName(%q) = %v, want nil", tt.input, err) |
| } |
| } |
| }) |
| } |
| } |
|
|
| func TestProfileCreateRejectsPathTraversal(t *testing.T) { |
| pm := NewProfileManager(t.TempDir()) |
|
|
| badNames := []string{ |
| "../test", |
| "..\\test", |
| "test/../other", |
| "../../etc/passwd", |
| "test/subdir", |
| "/absolute", |
| } |
|
|
| for _, name := range badNames { |
| t.Run(name, func(t *testing.T) { |
| err := pm.Create(name) |
| if err == nil { |
| t.Errorf("Create(%q) should have returned error", name) |
| } |
| }) |
| } |
| } |
|
|
| func TestProfileHandlerCreateRejectsPathTraversal(t *testing.T) { |
| pm := NewProfileManager(t.TempDir()) |
| mux := http.NewServeMux() |
| pm.RegisterHandlers(mux) |
|
|
| tests := []struct { |
| name string |
| body string |
| wantStatus int |
| }{ |
| {"path traversal ..", `{"name":"../malicious"}`, 400}, |
| {"path traversal /", `{"name":"test/nested"}`, 400}, |
| {"path traversal backslash", `{"name":"test\\nested"}`, 400}, |
| {"empty name", `{"name":""}`, 400}, |
| {"valid name", `{"name":"valid-profile"}`, 200}, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| req := httptest.NewRequest("POST", "/profiles/create", strings.NewReader(tt.body)) |
| req.Header.Set("Content-Type", "application/json") |
| w := httptest.NewRecorder() |
| mux.ServeHTTP(w, req) |
|
|
| if w.Code != tt.wantStatus { |
| t.Errorf("POST /profiles/create with %s: got status %d, want %d. Body: %s", |
| tt.body, w.Code, tt.wantStatus, w.Body.String()) |
| } |
| }) |
| } |
| } |
|
|
| func TestProfileHandlerCreateReturns409OnDuplicate(t *testing.T) { |
| pm := NewProfileManager(t.TempDir()) |
| mux := http.NewServeMux() |
| pm.RegisterHandlers(mux) |
|
|
| |
| body := `{"name":"duplicate-test"}` |
| req := httptest.NewRequest("POST", "/profiles/create", strings.NewReader(body)) |
| req.Header.Set("Content-Type", "application/json") |
| w := httptest.NewRecorder() |
| mux.ServeHTTP(w, req) |
|
|
| if w.Code != 200 { |
| t.Fatalf("first create failed: %d %s", w.Code, w.Body.String()) |
| } |
|
|
| |
| req = httptest.NewRequest("POST", "/profiles/create", strings.NewReader(body)) |
| req.Header.Set("Content-Type", "application/json") |
| w = httptest.NewRecorder() |
| mux.ServeHTTP(w, req) |
|
|
| if w.Code != 409 { |
| t.Errorf("duplicate create: got status %d, want 409. Body: %s", w.Code, w.Body.String()) |
| } |
| } |
|
|
| func TestProfileImportRejectsPathTraversal(t *testing.T) { |
| pm := NewProfileManager(t.TempDir()) |
|
|
| |
| src := filepath.Join(t.TempDir(), "chrome-src") |
| _ = os.MkdirAll(filepath.Join(src, "Default"), 0755) |
| _ = os.WriteFile(filepath.Join(src, "Default", "Preferences"), []byte(`{}`), 0644) |
|
|
| badNames := []string{ |
| "../imported", |
| "test/nested", |
| "..\\windows", |
| } |
|
|
| for _, name := range badNames { |
| t.Run(name, func(t *testing.T) { |
| err := pm.Import(name, src) |
| if err == nil { |
| t.Errorf("Import(%q, ...) should have returned error", name) |
| } |
| }) |
| } |
| } |
|
|
| func TestProfileResetRejectsPathTraversal(t *testing.T) { |
| pm := NewProfileManager(t.TempDir()) |
| _ = pm.Create("legit") |
|
|
| badNames := []string{ |
| "../legit", |
| "legit/../other", |
| } |
|
|
| for _, name := range badNames { |
| t.Run(name, func(t *testing.T) { |
| err := pm.Reset(name) |
| if err == nil { |
| t.Errorf("Reset(%q) should have returned error", name) |
| } |
| }) |
| } |
| } |
|
|
| func TestProfileDeleteRejectsPathTraversal(t *testing.T) { |
| pm := NewProfileManager(t.TempDir()) |
| _ = pm.Create("legit") |
|
|
| badNames := []string{ |
| "../legit", |
| "legit/../other", |
| } |
|
|
| for _, name := range badNames { |
| t.Run(name, func(t *testing.T) { |
| err := pm.Delete(name) |
| if err == nil { |
| t.Errorf("Delete(%q) should have returned error", name) |
| } |
| }) |
| } |
| } |
|
|
| func TestProfileRename(t *testing.T) { |
| dir := t.TempDir() |
| pm := NewProfileManager(dir) |
|
|
| if err := pm.Create("old-name"); err != nil { |
| t.Fatal(err) |
| } |
|
|
| if err := pm.Rename("old-name", "new-name"); err != nil { |
| t.Fatalf("Rename failed: %v", err) |
| } |
|
|
| if pm.Exists("old-name") { |
| t.Error("old name should not exist after rename") |
| } |
| if !pm.Exists("new-name") { |
| t.Error("new name should exist after rename") |
| } |
|
|
| profiles, _ := pm.List() |
| if len(profiles) != 1 { |
| t.Fatalf("expected 1 profile, got %d", len(profiles)) |
| } |
| if profiles[0].Name != "new-name" { |
| t.Errorf("expected name new-name, got %s", profiles[0].Name) |
| } |
| if profiles[0].ID != profileID("new-name") { |
| t.Errorf("expected ID %s, got %s", profileID("new-name"), profiles[0].ID) |
| } |
| } |
|
|
| func TestProfileRenameRejectsPathTraversal(t *testing.T) { |
| pm := NewProfileManager(t.TempDir()) |
| _ = pm.Create("legit") |
|
|
| badNames := []string{"../evil", "evil/../other", "..\\windows"} |
| for _, name := range badNames { |
| t.Run("to_"+name, func(t *testing.T) { |
| err := pm.Rename("legit", name) |
| if err == nil { |
| t.Errorf("Rename to %q should have returned error", name) |
| } |
| }) |
| } |
| } |
|
|
| func TestProfileRenameRejectsDuplicate(t *testing.T) { |
| pm := NewProfileManager(t.TempDir()) |
| _ = pm.Create("profile-a") |
| _ = pm.Create("profile-b") |
|
|
| err := pm.Rename("profile-a", "profile-b") |
| if err == nil { |
| t.Error("Rename to existing name should fail") |
| } |
| if !strings.Contains(err.Error(), "already exists") { |
| t.Errorf("expected 'already exists' error, got: %v", err) |
| } |
| } |
|
|