// Package keyring provides secure password storage for sudo authentication package keyring import ( "runtime" "strings" "testing" "time" ) func TestDetectStatus(t *testing.T) { status := DetectStatus() // Platform should always be set if status.Platform == "" { t.Error("Platform should not be empty") } // Platform should match runtime if status.Platform != runtime.GOOS { t.Errorf("Platform = %q, want %q", status.Platform, runtime.GOOS) } // Non-Linux should have daemon running if runtime.GOOS != "linux" { if !status.DaemonRunning { t.Error("Non-Linux platforms should have daemonRunning = true") } if status.Distro != "n/a" { t.Errorf("Non-Linux distro = %q, want 'n/a'", status.Distro) } if status.DesktopEnv != "n/a" { t.Errorf("Non-Linux desktopEnv = %q, want 'n/a'", status.DesktopEnv) } } } func TestCheckDependencies(t *testing.T) { err := CheckDependencies() // Non-Linux should always pass if runtime.GOOS != "linux" { if err != nil { t.Errorf("Non-Linux CheckDependencies() should return nil, got: %v", err) } } // Linux check is environment-dependent, so we just ensure it doesn't panic } func TestNeedsInstallPrompt(t *testing.T) { needs := NeedsInstallPrompt() // Non-Linux should never need prompt if runtime.GOOS != "linux" { if needs { t.Error("Non-Linux platforms should not need install prompt") } } // Linux check is environment-dependent, so we just ensure it doesn't panic } func TestCanUseKeyring(t *testing.T) { canUse := CanUseKeyring() // Non-Linux should always be able to use keyring if runtime.GOOS != "linux" { if !canUse { t.Error("Non-Linux platforms should be able to use keyring") } } } func TestOpen(t *testing.T) { if runtime.GOOS == "linux" && testing.Short() { t.Skip("Skipping on Linux in short mode - may require keyring interaction") } // Use a timeout to prevent hanging done := make(chan struct{}) var kr *Keyring var err error go func() { kr, err = Open() close(done) }() select { case <-done: // Good, completed case <-time.After(5 * time.Second): t.Fatal("Open() timed out - likely waiting for keyring interaction") } if err != nil { t.Fatalf("Open() failed: %v", err) } if kr == nil { t.Fatal("Open() returned nil keyring") } // Backend should be set if kr.Backend() == BackendNone { t.Error("Backend should not be BackendNone after successful Open()") } } func TestStoreAndGetPassword(t *testing.T) { if runtime.GOOS == "linux" { t.Skip("Skipping on Linux - may require keyring interaction") } kr, err := Open() if err != nil { t.Fatalf("Open() failed: %v", err) } // Clean up any existing password _ = kr.DeletePassword() // Initially should have no password if kr.HasPassword() { t.Error("HasPassword() should be false initially") } // Store password testPassword := "test-sudo-password-123" err = kr.StorePassword(testPassword) if err != nil { t.Fatalf("StorePassword() failed: %v", err) } // Should have password now if !kr.HasPassword() { t.Error("HasPassword() should be true after StorePassword()") } // Retrieve password retrieved, err := kr.GetPassword() if err != nil { t.Fatalf("GetPassword() failed: %v", err) } if retrieved != testPassword { t.Errorf("GetPassword() = %q, want %q", retrieved, testPassword) } // Delete password err = kr.DeletePassword() if err != nil { t.Fatalf("DeletePassword() failed: %v", err) } // Should not have password after delete if kr.HasPassword() { t.Error("HasPassword() should be false after DeletePassword()") } // GetPassword should fail _, err = kr.GetPassword() if err == nil { t.Error("GetPassword() should fail after DeletePassword()") } } func TestStoreEmptyPassword(t *testing.T) { kr, err := Open() if err != nil { t.Fatalf("Open() failed: %v", err) } // Empty password should fail err = kr.StorePassword("") if err == nil { t.Error("StorePassword('') should fail") } } func TestGetSetupInstructions(t *testing.T) { instructions := GetSetupInstructions() // Non-Linux should have empty instructions if runtime.GOOS != "linux" { if instructions != "" { t.Errorf("Non-Linux GetSetupInstructions() = %q, want empty", instructions) } } } func TestValidateSetup(t *testing.T) { if runtime.GOOS == "linux" { // On Linux, this test may require keyring interaction which hangs in CI // Skip if no keyring daemon is available status := DetectStatus() if !status.DaemonRunning && len(status.AvailableBackends) == 0 { t.Skip("Skipping on Linux without keyring daemon - may require interaction") } } // Use a timeout to prevent hanging done := make(chan struct{}) var valid bool var msg string go func() { valid, msg = ValidateSetup() close(done) }() select { case <-done: // Good, completed case <-time.After(5 * time.Second): t.Skip("ValidateSetup() timed out - likely waiting for keyring interaction") } // Non-Linux should always be valid if runtime.GOOS != "linux" { if !valid { t.Errorf("Non-Linux ValidateSetup() should be valid, got: %s", msg) } } // Message should not be empty if msg == "" { t.Error("ValidateSetup() message should not be empty") } } func TestNormalizeDesktopEnv(t *testing.T) { tests := []struct { input string expected string }{ {"GNOME", "GNOME"}, {"ubuntu:GNOME", "GNOME"}, {"KDE", "KDE"}, {"plasma", "KDE"}, {"XFCE", "XFCE"}, {"xfce4", "XFCE"}, {"i3", "i3"}, {"sway", "Sway"}, {"i3wm", "i3"}, {"Hyprland", "Hyprland"}, {"hyprland", "Hyprland"}, {"COSMIC", "COSMIC"}, {"cosmic", "COSMIC"}, {"Cinnamon", "Cinnamon"}, {"Pantheon", "Pantheon"}, {"river", "River"}, {"qtile", "qtile"}, {"unknown-de", "unknown-de"}, } for _, tt := range tests { result := normalizeDesktopEnv(tt.input) if result != tt.expected { t.Errorf("normalizeDesktopEnv(%q) = %q, want %q", tt.input, result, tt.expected) } } } func TestDetectDistro(t *testing.T) { if runtime.GOOS != "linux" { // Non-Linux should return "n/a" distro := detectDistro() if distro != "n/a" { t.Errorf("Non-Linux detectDistro() = %q, want 'n/a'", distro) } return } // On Linux, just ensure it doesn't panic and returns something distro := detectDistro() if distro == "" { t.Error("detectDistro() should not return empty string") } } func TestDetectDesktopEnvironment(t *testing.T) { if runtime.GOOS != "linux" { // Non-Linux should return "n/a" de := detectDesktopEnvironment() if de != "n/a" { t.Errorf("Non-Linux detectDesktopEnvironment() = %q, want 'n/a'", de) } return } // On Linux, just ensure it doesn't panic and returns something de := detectDesktopEnvironment() if de == "" { t.Error("detectDesktopEnvironment() should not return empty string") } } func TestGenerateInstallHint(t *testing.T) { tests := []struct { distro string de string want string }{ {"debian", "GNOME", "apt"}, {"fedora", "GNOME", "dnf"}, {"arch", "KDE", "pacman"}, {"suse", "GNOME", "zypper"}, {"unknown", "KDE", "kwallet"}, {"unknown", "GNOME", "gnome-keyring"}, {"unknown", "Cinnamon", "gnome-keyring"}, {"unknown", "XFCE", "gnome-keyring"}, {"unknown", "COSMIC", "gnome-keyring"}, {"unknown", "unknown", "keyring"}, // Tiling WMs need special setup hint {"arch", "i3", "gnome-keyring-daemon"}, {"arch", "Hyprland", "gnome-keyring-daemon"}, {"arch", "Sway", "gnome-keyring-daemon"}, {"arch", "bspwm", "gnome-keyring-daemon"}, } for _, tt := range tests { result := generateInstallHint(tt.distro, tt.de) if !strings.Contains(result, tt.want) { t.Errorf("generateInstallHint(%q, %q) = %q, should contain %q", tt.distro, tt.de, result, tt.want) } } } func TestBackendTypeConstants(t *testing.T) { // Ensure backend type constants are defined correctly tests := []struct { backend BackendType want string }{ {BackendSecretService, "secret-service"}, {BackendKWallet, "kwallet"}, {BackendPass, "pass"}, {BackendFile, "file"}, {BackendNone, "none"}, } for _, tt := range tests { if string(tt.backend) != tt.want { t.Errorf("BackendType constant = %q, want %q", tt.backend, tt.want) } } } func TestStatusJSONTags(t *testing.T) { // Verify Status struct has proper JSON tags status := Status{ AvailableBackends: []BackendType{BackendFile}, ActiveBackend: BackendFile, DaemonRunning: true, InstallHint: "test", Distro: "test", DesktopEnv: "test", HasPassword: false, Platform: "test", } // Just verify fields are accessible if status.ActiveBackend != BackendFile { t.Error("Status struct fields not accessible") } }