package keyring import ( "context" "encoding/json" "errors" "runtime" "strings" "sync" "sync/atomic" "testing" "time" systemkeyring "github.com/zalando/go-keyring" ) type fakeSecretStore struct { set func(service, user, password string) error get func(service, user string) (string, error) delete func(service, user string) error } func (f *fakeSecretStore) Set(service, user, password string) error { if f.set != nil { return f.set(service, user, password) } return nil } func (f *fakeSecretStore) Get(service, user string) (string, error) { if f.get != nil { return f.get(service, user) } return "", systemkeyring.ErrNotFound } func (f *fakeSecretStore) Delete(service, user string) error { if f.delete != nil { return f.delete(service, user) } return nil } func useFakeStore(t *testing.T, store secretStore) { t.Helper() oldStore := defaultSecretStore oldGate := operationGate defaultSecretStore = store operationGate = make(chan struct{}, 1) t.Cleanup(func() { defaultSecretStore = oldStore operationGate = oldGate }) } func testKeyring(t *testing.T, store secretStore) *Keyring { t.Helper() backend := nativeBackend() if backend == BackendNone { backend = BackendSecretService } return &Keyring{store: store, backend: backend} } func TestNativeBackendMapping(t *testing.T) { tests := map[string]BackendType{ "linux": BackendSecretService, "darwin": BackendKeychain, "windows": BackendWinCred, "plan9": BackendNone, } for goos, want := range tests { if got := nativeBackendFor(goos); got != want { t.Errorf("nativeBackendFor(%q) = %q, want %q", goos, got, want) } } } func TestOpenUsesOnlyNativeBackend(t *testing.T) { kr, err := Open() if nativeBackend() == BackendNone { if !errors.Is(err, ErrNoBackend) { t.Fatalf("Open() error = %v, want ErrNoBackend", err) } return } if err != nil { t.Fatalf("Open() failed: %v", err) } if got, want := kr.Backend(), nativeBackend(); got != want { t.Errorf("Backend() = %q, want %q", got, want) } if got := kr.Backend(); got == BackendFile || got == BackendPass || got == BackendKWallet { t.Fatalf("Open() selected legacy fallback backend %q", got) } } func TestPasswordRoundTripUsesStableServiceAndAccount(t *testing.T) { var mu sync.Mutex var stored string store := &fakeSecretStore{ set: func(service, user, password string) error { if service != ServiceName || user != PasswordKey { t.Fatalf("Set(%q, %q, ...), want (%q, %q, ...)", service, user, ServiceName, PasswordKey) } mu.Lock() stored = password mu.Unlock() return nil }, get: func(service, user string) (string, error) { if service != ServiceName || user != PasswordKey { t.Fatalf("Get(%q, %q), want (%q, %q)", service, user, ServiceName, PasswordKey) } mu.Lock() defer mu.Unlock() if stored == "" { return "", systemkeyring.ErrNotFound } return stored, nil }, delete: func(service, user string) error { mu.Lock() stored = "" mu.Unlock() return nil }, } kr := testKeyring(t, store) if err := kr.StorePassword(""); err == nil { t.Fatal("StorePassword(\"\") succeeded") } if err := kr.StorePassword("test-secret"); err != nil { t.Fatalf("StorePassword() failed: %v", err) } if !kr.HasPassword() { t.Fatal("HasPassword() = false after storing a password") } password, err := kr.GetPassword() if err != nil || password != "test-secret" { t.Fatalf("GetPassword() = %q, %v; want test-secret, nil", password, err) } if err := kr.DeletePassword(); err != nil { t.Fatalf("DeletePassword() failed: %v", err) } if _, err := kr.GetPassword(); !errors.Is(err, ErrNoPassword) { t.Fatalf("GetPassword() error = %v, want ErrNoPassword", err) } } func TestDeleteMissingPasswordIsIdempotent(t *testing.T) { kr := testKeyring(t, &fakeSecretStore{ delete: func(string, string) error { return systemkeyring.ErrNotFound }, }) if err := kr.DeletePassword(); err != nil { t.Fatalf("DeletePassword() = %v, want nil", err) } } func TestBoundedOperationDoesNotAccumulateBlockedCalls(t *testing.T) { oldTimeout := operationTimeout operationTimeout = 40 * time.Millisecond t.Cleanup(func() { operationTimeout = oldTimeout }) release := make(chan struct{}) var calls atomic.Int32 store := &fakeSecretStore{get: func(string, string) (string, error) { calls.Add(1) <-release return "secret", nil }} useFakeStore(t, store) kr := testKeyring(t, store) start := time.Now() if _, err := kr.GetPassword(); !errors.Is(err, ErrOperationTimeout) { t.Fatalf("first GetPassword() error = %v, want ErrOperationTimeout", err) } if _, err := kr.GetPassword(); !errors.Is(err, ErrOperationTimeout) { t.Fatalf("second GetPassword() error = %v, want ErrOperationTimeout", err) } if elapsed := time.Since(start); elapsed > 500*time.Millisecond { t.Fatalf("bounded reads took %v", elapsed) } if got := calls.Load(); got != 1 { t.Fatalf("native Get calls = %d, want 1 while first call is stuck", got) } close(release) deadline := time.Now().Add(time.Second) for len(operationGate) != 0 && time.Now().Before(deadline) { time.Sleep(time.Millisecond) } if len(operationGate) != 0 { t.Fatal("operation gate was not released after native call returned") } } func TestCallerCancellationBoundsKeyringRead(t *testing.T) { release := make(chan struct{}) store := &fakeSecretStore{get: func(string, string) (string, error) { <-release return "", nil }} useFakeStore(t, store) kr := testKeyring(t, store) ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := kr.GetPasswordContext(ctx); !errors.Is(err, context.Canceled) { t.Fatalf("GetPasswordContext() error = %v, want context.Canceled", err) } close(release) } func TestDetectStatusUsesOneBoundedRead(t *testing.T) { var calls atomic.Int32 store := &fakeSecretStore{get: func(string, string) (string, error) { calls.Add(1) return "", systemkeyring.ErrNotFound }} useFakeStore(t, store) status := DetectStatus() if got := calls.Load(); got != 1 { t.Fatalf("DetectStatus native reads = %d, want 1", got) } if status.ActiveBackend != nativeBackend() { t.Errorf("ActiveBackend = %q, want %q", status.ActiveBackend, nativeBackend()) } if status.HasPassword { t.Error("HasPassword = true for a missing item") } if !status.DaemonRunning || len(status.AvailableBackends) != 1 { t.Errorf("responsive native backend reported unavailable: %+v", status) } if status.Error != "" { t.Errorf("Error = %q for a responsive store", status.Error) } } func TestDetectStatusReportsUnavailableProvider(t *testing.T) { store := &fakeSecretStore{get: func(string, string) (string, error) { return "", errors.New("session bus unavailable") }} useFakeStore(t, store) status := DetectStatus() if status.ActiveBackend != BackendNone || status.DaemonRunning { t.Errorf("unavailable store reported active: %+v", status) } if !strings.Contains(status.Error, "session bus unavailable") { t.Errorf("Error = %q, want session bus failure", status.Error) } } func TestDetectStatusRespectsCallerCancellation(t *testing.T) { var calls atomic.Int32 store := &fakeSecretStore{get: func(string, string) (string, error) { calls.Add(1) return "", nil }} useFakeStore(t, store) ctx, cancel := context.WithCancel(context.Background()) cancel() status := DetectStatusContext(ctx) if status.ActiveBackend != BackendNone { t.Errorf("cancelled status ActiveBackend = %q, want none", status.ActiveBackend) } if !strings.Contains(status.Error, "cancelled") { t.Errorf("cancelled status Error = %q", status.Error) } if got := calls.Load(); got != 0 { t.Errorf("native Get calls after pre-cancellation = %d, want 0", got) } } func TestCheckHelpersUseNativeStatus(t *testing.T) { useFakeStore(t, &fakeSecretStore{}) if err := CheckDependencies(); err != nil { t.Fatalf("CheckDependencies() = %v", err) } if NeedsInstallPrompt() { t.Error("NeedsInstallPrompt() = true for responsive native store") } if !CanUseKeyring() { t.Error("CanUseKeyring() = false for responsive native store") } if got := GetSetupInstructions(); got != "" { t.Errorf("GetSetupInstructions() = %q, want empty", got) } valid, message := ValidateSetup() if !valid || !strings.Contains(message, "responding") { t.Errorf("ValidateSetup() = %v, %q", valid, message) } } func TestNormalizeDesktopEnv(t *testing.T) { tests := map[string]string{ "ubuntu:GNOME": "GNOME", "plasma": "KDE", "xfce4": "XFCE", "i3wm": "i3", "Hyprland": "Hyprland", "COSMIC": "COSMIC", "unknown-de": "unknown-de", } for input, want := range tests { if got := normalizeDesktopEnv(input); got != want { t.Errorf("normalizeDesktopEnv(%q) = %q, want %q", input, got, want) } } } func TestGenerateInstallHint(t *testing.T) { tests := []struct { distro string desktop string want string }{ {"debian", "GNOME", "apt"}, {"fedora", "GNOME", "dnf"}, {"arch", "GNOME", "pacman"}, {"suse", "GNOME", "zypper"}, {"unknown", "KDE", "KDE Wallet"}, {"unknown", "i3", "Secret Service"}, } for _, test := range tests { if got := generateInstallHint(test.distro, test.desktop); !strings.Contains(got, test.want) { t.Errorf("generateInstallHint(%q, %q) = %q, want substring %q", test.distro, test.desktop, got, test.want) } } } func TestBackendTypeConstantsRemainCompatible(t *testing.T) { tests := map[BackendType]string{ BackendSecretService: "secret-service", BackendKWallet: "kwallet", BackendPass: "pass", BackendFile: "file", BackendKeychain: "keychain", BackendWinCred: "wincred", BackendNone: "none", } for backend, want := range tests { if got := string(backend); got != want { t.Errorf("backend constant = %q, want %q", got, want) } } } func TestStatusJSONContract(t *testing.T) { encoded, err := json.Marshal(Status{Error: "locked"}) if err != nil { t.Fatal(err) } for _, field := range []string{ "availableBackends", "activeBackend", "daemonRunning", "installHint", "distro", "desktopEnv", "hasPassword", "platform", "error", } { if !strings.Contains(string(encoded), `"`+field+`"`) { t.Errorf("Status JSON %s missing field %q", encoded, field) } } } func TestDetectedEnvironmentIsNonEmpty(t *testing.T) { if got := detectDistro(); got == "" { t.Error("detectDistro() returned empty string") } if got := detectDesktopEnvironment(); got == "" { t.Error("detectDesktopEnvironment() returned empty string") } if got := nativeBackend(); got != nativeBackendFor(runtime.GOOS) { t.Errorf("nativeBackend() = %q, want mapping for %s", got, runtime.GOOS) } }