package api import ( "errors" "os" "strings" "testing" "time" "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources" ) func TestIsFallbackMemorySource(t *testing.T) { tests := []struct { source string expected bool }{ // Fallback sources {"", true}, {"unknown", true}, {"Unknown", true}, {"UNKNOWN", true}, {"nodes-endpoint", true}, {"Nodes-Endpoint", true}, {"node-status-used", true}, {"previous-snapshot", true}, {"rrd-available", true}, {"rrd-data", true}, {"listing", true}, // Non-fallback sources {"cgroup", false}, {"qemu-agent", false}, {"proxmox-api", false}, {"available-field", false}, {"avail-field", false}, {"meminfo-available", false}, {"calculated", false}, {"rrddata", false}, {"powered-off", false}, {"some-other-source", false}, } for _, tt := range tests { t.Run(tt.source, func(t *testing.T) { result := isFallbackMemorySource(tt.source) if result != tt.expected { t.Errorf("isFallbackMemorySource(%q) = %v, want %v", tt.source, result, tt.expected) } }) } } func TestCopyStringSlice(t *testing.T) { tests := []struct { name string input []string expected []string }{ { name: "empty slice", input: []string{}, expected: []string{}, }, { name: "nil slice", input: nil, expected: []string{}, }, { name: "single element", input: []string{"one"}, expected: []string{"one"}, }, { name: "multiple elements", input: []string{"one", "two", "three"}, expected: []string{"one", "two", "three"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := copyStringSlice(tt.input) if len(result) != len(tt.expected) { t.Errorf("copyStringSlice() length = %d, want %d", len(result), len(tt.expected)) return } for i, v := range result { if v != tt.expected[i] { t.Errorf("copyStringSlice()[%d] = %q, want %q", i, v, tt.expected[i]) } } // Verify it's a copy, not the same slice if len(tt.input) > 0 && len(result) > 0 { result[0] = "modified" if tt.input[0] == "modified" { t.Error("copyStringSlice() returned reference to original, not a copy") } } }) } } func TestNormalizeVersionLabel(t *testing.T) { tests := []struct { input string expected string }{ // Empty/whitespace {"", ""}, {" ", ""}, // Already has v prefix {"v1.0.0", "v1.0.0"}, {"v4.35.0", "v4.35.0"}, {" v1.0.0 ", "v1.0.0"}, // Needs v prefix {"1.0.0", "v1.0.0"}, {"4.35.0", "v4.35.0"}, {" 4.35.0 ", "v4.35.0"}, // Non-numeric prefix (no v added) {"dev", "dev"}, {"alpha-1.0", "alpha-1.0"}, {"beta", "beta"}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { result := normalizeVersionLabel(tt.input) if result != tt.expected { t.Errorf("normalizeVersionLabel(%q) = %q, want %q", tt.input, result, tt.expected) } }) } } func TestContains(t *testing.T) { tests := []struct { name string slice []string str string expected bool }{ {"empty slice", []string{}, "test", false}, {"nil slice", nil, "test", false}, {"found at start", []string{"test", "foo", "bar"}, "test", true}, {"found at end", []string{"foo", "bar", "test"}, "test", true}, {"found in middle", []string{"foo", "test", "bar"}, "test", true}, {"not found", []string{"foo", "bar", "baz"}, "test", false}, {"case sensitive", []string{"Test", "TEST"}, "test", false}, {"empty string search", []string{"foo", "", "bar"}, "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := contains(tt.slice, tt.str) if result != tt.expected { t.Errorf("contains(%v, %q) = %v, want %v", tt.slice, tt.str, result, tt.expected) } }) } } func TestContainsFold(t *testing.T) { tests := []struct { name string slice []string candidate string expected bool }{ {"empty slice", []string{}, "test", false}, {"nil slice", nil, "test", false}, {"empty candidate", []string{"foo", "bar"}, "", false}, {"whitespace candidate", []string{"foo", "bar"}, " ", false}, {"exact match", []string{"test", "foo"}, "test", true}, {"case insensitive match", []string{"Test", "foo"}, "test", true}, {"uppercase match", []string{"test", "foo"}, "TEST", true}, {"mixed case match", []string{"TeSt", "foo"}, "tEsT", true}, {"with whitespace in slice", []string{" test ", "foo"}, "test", true}, {"with whitespace in candidate", []string{"test", "foo"}, " test ", true}, {"not found", []string{"foo", "bar"}, "test", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := containsFold(tt.slice, tt.candidate) if result != tt.expected { t.Errorf("containsFold(%v, %q) = %v, want %v", tt.slice, tt.candidate, result, tt.expected) } }) } } func TestInterfaceToStringSlice(t *testing.T) { tests := []struct { name string input interface{} expected []string }{ { name: "nil", input: nil, expected: nil, }, { name: "string slice", input: []string{"one", "two", "three"}, expected: []string{"one", "two", "three"}, }, { name: "empty string slice", input: []string{}, expected: []string{}, }, { name: "interface slice with strings", input: []interface{}{"one", "two", "three"}, expected: []string{"one", "two", "three"}, }, { name: "empty interface slice", input: []interface{}{}, expected: []string{}, }, { name: "interface slice with mixed types", input: []interface{}{"one", 2, "three", true}, expected: []string{"one", "three"}, }, { name: "interface slice with only non-strings", input: []interface{}{1, 2, 3}, expected: []string{}, }, { name: "unsupported type", input: "not a slice", expected: nil, }, { name: "int slice (unsupported)", input: []int{1, 2, 3}, expected: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := interfaceToStringSlice(tt.input) if tt.expected == nil { if result != nil { t.Errorf("interfaceToStringSlice() = %v, want nil", result) } return } if len(result) != len(tt.expected) { t.Errorf("interfaceToStringSlice() length = %d, want %d", len(result), len(tt.expected)) return } for i, v := range result { if v != tt.expected[i] { t.Errorf("interfaceToStringSlice()[%d] = %q, want %q", i, v, tt.expected[i]) } } }) } } func TestPreferredDockerHostName(t *testing.T) { tests := []struct { name string host *unifiedresources.DockerHostView expected string }{ { name: "display name preferred", host: dockerHostViewForDiagnostics("resource-1", "id-123", "My Docker Host", "docker-host.local", "agent-456"), expected: "My Docker Host", }, { name: "hostname when no display name", host: dockerHostViewForDiagnostics("resource-1", "id-123", "", "docker-host.local", "agent-456"), expected: "docker-host.local", }, { name: "agent ID when no display name or hostname", host: dockerHostViewForDiagnostics("resource-1", "id-123", "", "", "agent-456"), expected: "agent-456", }, { name: "ID as fallback", host: dockerHostViewForDiagnostics("resource-1", "id-123", "", "", ""), expected: "id-123", }, { name: "whitespace-only display name ignored", host: dockerHostViewForDiagnostics("resource-1", "id-123", " ", "docker-host.local", "agent-456"), expected: "docker-host.local", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := preferredDockerHostName(tt.host) if result != tt.expected { t.Errorf("preferredDockerHostName() = %q, want %q", result, tt.expected) } }) } } func dockerHostViewForDiagnostics(resourceID, hostSourceID, name, hostname, agentID string) *unifiedresources.DockerHostView { r := &unifiedresources.Resource{ ID: resourceID, Type: unifiedresources.ResourceTypeAgent, Name: name, Docker: &unifiedresources.DockerData{ HostSourceID: hostSourceID, Hostname: hostname, AgentID: agentID, }, } view := unifiedresources.NewDockerHostView(r) return &view } func TestFormatTimeMaybe(t *testing.T) { tests := []struct { name string input time.Time expected string }{ { name: "zero time", input: time.Time{}, expected: "", }, { name: "specific time", input: time.Date(2025, 11, 30, 12, 30, 45, 0, time.UTC), expected: "2025-11-30T12:30:45Z", }, { name: "non-UTC time converted to UTC", input: time.Date(2025, 11, 30, 12, 30, 45, 0, time.FixedZone("EST", -5*60*60)), expected: "2025-11-30T17:30:45Z", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := formatTimeMaybe(tt.input) if result != tt.expected { t.Errorf("formatTimeMaybe() = %q, want %q", result, tt.expected) } }) } } func TestFingerprintPublicKey(t *testing.T) { t.Parallel() // Valid SSH keys for testing (these are public keys, safe to include) // ED25519 key from openssh-portable test suite validED25519Key := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl test@example.com" tests := []struct { name string input string wantErr bool errContains string }{ // Error cases - empty/whitespace { name: "empty string returns error", input: "", wantErr: true, errContains: "empty public key", }, { name: "whitespace only returns error", input: " ", wantErr: true, errContains: "empty public key", }, { name: "tab only returns error", input: "\t", wantErr: true, errContains: "empty public key", }, { name: "newline only returns error", input: "\n", wantErr: true, errContains: "empty public key", }, // Error cases - invalid key format { name: "random text returns error", input: "this is not an ssh key", wantErr: true, errContains: "", // ssh library error message varies }, { name: "partial key returns error", input: "ssh-ed25519", wantErr: true, errContains: "", }, { name: "wrong key type prefix returns error", input: "ssh-fake AAAAC3NzaC1lZDI1NTE5AAAAIOMQ==", wantErr: true, errContains: "", }, { name: "base64 with wrong algorithm returns error", input: "ssh-ed25519 notvalidbase64!!!", wantErr: true, errContains: "", }, { name: "truncated base64 returns error", input: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA", wantErr: true, errContains: "", }, { name: "malformed key returns error", input: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJ", wantErr: true, errContains: "", }, // Success cases { name: "valid ED25519 key returns fingerprint", input: validED25519Key, wantErr: false, }, { name: "valid ED25519 key with leading whitespace", input: " " + validED25519Key, wantErr: false, }, { name: "valid ED25519 key with trailing whitespace", input: validED25519Key + " ", wantErr: false, }, { name: "valid ED25519 key without comment", input: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() result, err := fingerprintPublicKey(tt.input) if tt.wantErr { if err == nil { t.Errorf("fingerprintPublicKey() expected error, got nil with result %q", result) return } if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) { t.Errorf("fingerprintPublicKey() error = %q, want error containing %q", err.Error(), tt.errContains) } return } if err != nil { t.Errorf("fingerprintPublicKey() unexpected error: %v", err) return } // Verify fingerprint format (SHA256:base64) if !strings.HasPrefix(result, "SHA256:") { t.Errorf("fingerprintPublicKey() = %q, expected SHA256: prefix", result) } }) } } func TestCountLegacySSHKeys(t *testing.T) { t.Parallel() tests := []struct { name string setup func(t *testing.T) string // returns directory path wantCount int wantErr bool }{ { name: "non-existent directory returns 0 with no error", setup: func(t *testing.T) string { // Use path under /tmp to reliably get "not exists" vs "permission denied" return "/tmp/nonexistent_ssh_keys_test_xyz123" }, wantCount: 0, wantErr: false, }, { name: "empty directory returns 0", setup: func(t *testing.T) string { return t.TempDir() }, wantCount: 0, wantErr: false, }, { name: "directory with no matching files returns 0", setup: func(t *testing.T) string { dir := t.TempDir() _ = os.WriteFile(dir+"/known_hosts", []byte("test"), 0600) _ = os.WriteFile(dir+"/authorized_keys", []byte("test"), 0600) _ = os.WriteFile(dir+"/config", []byte("test"), 0600) return dir }, wantCount: 0, wantErr: false, }, { name: "directory with id_rsa counts as 1", setup: func(t *testing.T) string { dir := t.TempDir() _ = os.WriteFile(dir+"/id_rsa", []byte("test"), 0600) return dir }, wantCount: 1, wantErr: false, }, { name: "directory with id_rsa and id_rsa.pub counts as 2", setup: func(t *testing.T) string { dir := t.TempDir() _ = os.WriteFile(dir+"/id_rsa", []byte("test"), 0600) _ = os.WriteFile(dir+"/id_rsa.pub", []byte("test"), 0600) return dir }, wantCount: 2, wantErr: false, }, { name: "directory with multiple key types", setup: func(t *testing.T) string { dir := t.TempDir() _ = os.WriteFile(dir+"/id_rsa", []byte("test"), 0600) _ = os.WriteFile(dir+"/id_rsa.pub", []byte("test"), 0600) _ = os.WriteFile(dir+"/id_ed25519", []byte("test"), 0600) _ = os.WriteFile(dir+"/id_ed25519.pub", []byte("test"), 0600) _ = os.WriteFile(dir+"/id_ecdsa", []byte("test"), 0600) return dir }, wantCount: 5, wantErr: false, }, { name: "subdirectories named id_* are not counted", setup: func(t *testing.T) string { dir := t.TempDir() _ = os.Mkdir(dir+"/id_subdirectory", 0755) _ = os.WriteFile(dir+"/id_rsa", []byte("test"), 0600) return dir }, wantCount: 1, wantErr: false, }, { name: "mixed files and directories", setup: func(t *testing.T) string { dir := t.TempDir() _ = os.Mkdir(dir+"/id_subdir", 0755) _ = os.WriteFile(dir+"/id_rsa", []byte("test"), 0600) _ = os.WriteFile(dir+"/id_ed25519", []byte("test"), 0600) _ = os.WriteFile(dir+"/known_hosts", []byte("test"), 0600) _ = os.WriteFile(dir+"/authorized_keys", []byte("test"), 0600) return dir }, wantCount: 2, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() dir := tt.setup(t) count, err := countLegacySSHKeys(dir) if tt.wantErr { if err == nil { t.Errorf("countLegacySSHKeys() expected error, got nil") } return } if err != nil { t.Errorf("countLegacySSHKeys() unexpected error: %v", err) return } if count != tt.wantCount { t.Errorf("countLegacySSHKeys() = %d, want %d", count, tt.wantCount) } }) } } func TestResolveUserName(t *testing.T) { t.Parallel() // Get current user to have a valid UID to test currentUID := uint32(os.Getuid()) tests := []struct { name string uid uint32 validate func(t *testing.T, result string) }{ { name: "UID 0 returns root", uid: 0, validate: func(t *testing.T, result string) { // On most systems, UID 0 is "root" if result != "root" && !strings.HasPrefix(result, "uid:") { t.Errorf("resolveUserName(0) = %q, want 'root' or 'uid:0'", result) } }, }, { name: "current user UID returns valid username", uid: currentUID, validate: func(t *testing.T, result string) { // Should return a username or uid:X fallback if result == "" { t.Error("resolveUserName() returned empty string") } if strings.HasPrefix(result, "uid:") { // Fallback is acceptable expected := "uid:" + strings.TrimPrefix(result, "uid:") if result != expected { t.Errorf("resolveUserName() fallback format invalid: %q", result) } } }, }, { name: "non-existent UID returns uid:X format", uid: 999999999, validate: func(t *testing.T, result string) { expected := "uid:999999999" if result != expected { t.Errorf("resolveUserName(999999999) = %q, want %q", result, expected) } }, }, { name: "max uint32 UID returns uid:X format", uid: ^uint32(0), // 4294967295 validate: func(t *testing.T, result string) { expected := "uid:4294967295" if result != expected { t.Errorf("resolveUserName(max) = %q, want %q", result, expected) } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() result := resolveUserName(tt.uid) tt.validate(t, result) }) } } func TestResolveGroupName(t *testing.T) { t.Parallel() // Get current group to have a valid GID to test currentGID := uint32(os.Getgid()) tests := []struct { name string gid uint32 validate func(t *testing.T, result string) }{ { name: "GID 0 returns root or wheel", gid: 0, validate: func(t *testing.T, result string) { // On most systems, GID 0 is "root" or "wheel" if result != "root" && result != "wheel" && !strings.HasPrefix(result, "gid:") { t.Errorf("resolveGroupName(0) = %q, want 'root', 'wheel', or 'gid:0'", result) } }, }, { name: "current group GID returns valid group name", gid: currentGID, validate: func(t *testing.T, result string) { // Should return a group name or gid:X fallback if result == "" { t.Error("resolveGroupName() returned empty string") } if strings.HasPrefix(result, "gid:") { // Fallback is acceptable expected := "gid:" + strings.TrimPrefix(result, "gid:") if result != expected { t.Errorf("resolveGroupName() fallback format invalid: %q", result) } } }, }, { name: "non-existent GID returns gid:X format", gid: 999999999, validate: func(t *testing.T, result string) { expected := "gid:999999999" if result != expected { t.Errorf("resolveGroupName(999999999) = %q, want %q", result, expected) } }, }, { name: "max uint32 GID returns gid:X format", gid: ^uint32(0), // 4294967295 validate: func(t *testing.T, result string) { expected := "gid:4294967295" if result == expected { return } if result == "" { t.Errorf("resolveGroupName(max) = %q, want %q or system group name", result, expected) return } if strings.HasPrefix(result, "gid:") { t.Errorf("resolveGroupName(max) = %q, want %q or system group name", result, expected) } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() result := resolveGroupName(tt.gid) tt.validate(t, result) }) } } func TestMergeDiagnosticsConnection(t *testing.T) { cases := []struct { name string probeConnected bool probeError string monitorConnected bool hasMonitorStatus bool wantConnected bool wantErrContains string }{ {"probe connected stays connected", true, "", false, false, true, ""}, {"probe failed but monitor connected", false, "timeout", true, true, true, "monitor still reports"}, {"probe failed and no monitor status", false, "timeout", false, false, false, "timeout"}, {"probe failed and monitor disconnected", false, "timeout", false, true, false, "timeout"}, {"probe failed empty error, monitor connected", false, "", true, true, true, ""}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { gotConnected, gotErr := mergeDiagnosticsConnection(tc.probeConnected, tc.probeError, tc.monitorConnected, tc.hasMonitorStatus) if gotConnected != tc.wantConnected { t.Fatalf("connected = %v, want %v", gotConnected, tc.wantConnected) } if tc.wantErrContains == "" { if gotErr != "" { t.Fatalf("expected empty error, got %q", gotErr) } } else if !strings.Contains(gotErr, tc.wantErrContains) { t.Fatalf("error %q does not contain %q", gotErr, tc.wantErrContains) } }) } } func TestClassifyDiagnosticsConnectionFailure(t *testing.T) { cases := []struct { name string err error fallback string wantKind string wantMessage string }{ { name: "auth rejected", err: errors.New("API error 401 (Unauthorized): Invalid credentials or token"), fallback: "Failed to connect to Proxmox API", wantKind: "proxmox_auth_rejected", wantMessage: "Proxmox rejected the stored API token or credentials", }, { name: "tls certificate mismatch", err: errors.New("x509: certificate is valid for pve.local, not 192.0.2.10"), fallback: "Failed to connect to Proxmox API", wantKind: "tls_certificate_mismatch", wantMessage: "TLS certificate mismatch or untrusted certificate", }, { name: "connection refused", err: errors.New("dial tcp 192.0.2.10:8006: connect: connection refused"), fallback: "Failed to connect to Proxmox API", wantKind: "connection_refused", wantMessage: "TCP connection refused by host", }, { name: "network unreachable", err: errors.New("dial tcp 192.0.2.10:8006: connect: no route to host"), fallback: "Failed to connect to Proxmox API", wantKind: "network_unreachable", wantMessage: "Network route to host is unavailable", }, { name: "timeout", err: errors.New("context deadline exceeded"), fallback: "Failed to connect to Proxmox API", wantKind: "connection_timeout", wantMessage: "Connection timed out", }, { name: "fallback", err: errors.New("unexpected EOF"), fallback: "Failed to connect to Proxmox API", wantKind: "connection_failed", wantMessage: "Failed to connect to Proxmox API", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := classifyDiagnosticsConnectionFailure(tc.err, tc.fallback) if got.kind != tc.wantKind { t.Fatalf("kind = %q, want %q", got.kind, tc.wantKind) } if got.message != tc.wantMessage { t.Fatalf("message = %q, want %q", got.message, tc.wantMessage) } if strings.TrimSpace(got.troubleshooting) == "" { t.Fatal("expected troubleshooting guidance") } }) } }