WitNote / internal /profiles /profiles_test.go
AUXteam's picture
Upload folder using huggingface_hub
6a7089a verified
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)
}
}
// === Security Validation Tests ===
func TestValidateProfileName(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
errMsg string
}{
// Valid names
{"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 name
{"empty", "", true, "cannot be empty"},
// Path traversal attempts
{"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 '..'"},
// Path separator attempts
{"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 '/'"},
// Combined attacks
{"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)
// Create first profile
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())
}
// Try to create duplicate
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())
// Create a valid source directory
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)
}
}