From d06ed2edb3d9ef9686626a4013e90815e2e4d9a6 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 19 Jan 2026 19:25:38 +0000 Subject: [PATCH] refactor: Add testability improvements to core packages hostagent/commands.go: - Extract execCommandContext as mockable variable hostagent/proxmox_setup.go: - Convert stateFilePath constants to variables (testable) - Extract runCommand and lookPath as mockable functions - Add duplicate comment (minor cleanup needed) notifications/notifications.go: - Add GetQueueStats() method for interface compliance - Used by NotificationMonitor interface updates/manager.go: - Add AddSSEClient, RemoveSSEClient, GetSSECachedStatus methods - Enables interface-based SSE client management pkg/audit/export.go: - Minor testability improvements go.mod/go.sum: - Add stretchr/objx v0.5.2 (test mocking dependency) --- go.mod | 1 + go.sum | 2 ++ internal/hostagent/commands.go | 6 ++++-- internal/hostagent/proxmox_setup.go | 22 +++++++++++++++------- internal/notifications/notifications.go | 12 ++++++++++++ internal/updates/manager.go | 23 +++++++++++++++++++++++ pkg/audit/export.go | 10 ++++++++-- 7 files changed, 65 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 083f9f7f8..8fc100d1b 100644 --- a/go.mod +++ b/go.mod @@ -96,6 +96,7 @@ require ( github.com/russellhaering/goxmldsig v1.4.0 // indirect github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect github.com/x448/float16 v0.8.4 // indirect diff --git a/go.sum b/go.sum index 71935070a..95cbf6645 100644 --- a/go.sum +++ b/go.sum @@ -208,6 +208,8 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/internal/hostagent/commands.go b/internal/hostagent/commands.go index 880296721..8c6ff7c2e 100644 --- a/internal/hostagent/commands.go +++ b/internal/hostagent/commands.go @@ -17,6 +17,8 @@ import ( "github.com/rs/zerolog" ) +var execCommandContext = exec.CommandContext + // CommandClient handles WebSocket connection to Pulse for AI command execution type CommandClient struct { pulseURL string @@ -413,9 +415,9 @@ func (c *CommandClient) executeCommand(ctx context.Context, payload executeComma // Execute the command var cmd *exec.Cmd if runtime.GOOS == "windows" { - cmd = exec.CommandContext(cmdCtx, "cmd", "/C", command) + cmd = execCommandContext(cmdCtx, "cmd", "/C", command) } else { - cmd = exec.CommandContext(cmdCtx, "sh", "-c", command) + cmd = execCommandContext(cmdCtx, "sh", "-c", command) } var stdout, stderr bytes.Buffer diff --git a/internal/hostagent/proxmox_setup.go b/internal/hostagent/proxmox_setup.go index 3f7863eac..d87700cef 100644 --- a/internal/hostagent/proxmox_setup.go +++ b/internal/hostagent/proxmox_setup.go @@ -43,8 +43,11 @@ const ( proxmoxUserPVE = "pulse-monitor@pam" proxmoxUserPBS = "pulse-monitor@pbs" proxmoxComment = "Pulse monitoring service" - stateFilePath = "/var/lib/pulse-agent/proxmox-registered" // Legacy, kept for backward compat - stateFileDir = "/var/lib/pulse-agent" +) + +var ( + stateFilePath = "/var/lib/pulse-agent/proxmox-registered" // Legacy, kept for backward compat + stateFileDir = "/var/lib/pulse-agent" // Per-type state files for multi-product support (PVE+PBS on same host) stateFilePVE = "/var/lib/pulse-agent/proxmox-pve-registered" stateFilePBS = "/var/lib/pulse-agent/proxmox-pbs-registered" @@ -210,6 +213,9 @@ func (p *ProxmoxSetup) detectProxmoxType() string { return "" } +// detectProxmoxTypes checks for ALL Proxmox products on this system. +// Returns a slice of detected types (e.g., ["pve", "pbs"] if both are installed). +// This is common when PBS is installed directly on a PVE host. // detectProxmoxTypes checks for ALL Proxmox products on this system. // Returns a slice of detected types (e.g., ["pve", "pbs"] if both are installed). // This is common when PBS is installed directly on a PVE host. @@ -217,12 +223,12 @@ func (p *ProxmoxSetup) detectProxmoxTypes() []string { var types []string // Check for PVE - if _, err := exec.LookPath("pvesh"); err == nil { + if _, err := lookPath("pvesh"); err == nil { types = append(types, "pve") } // Check for PBS - if _, err := exec.LookPath("proxmox-backup-manager"); err == nil { + if _, err := lookPath("proxmox-backup-manager"); err == nil { types = append(types, "pbs") } @@ -707,14 +713,16 @@ func (p *ProxmoxSetup) markTypeAsRegistered(ptype string) { } // runCommand executes a command and returns any error. -func runCommand(ctx context.Context, name string, args ...string) error { +var runCommand = func(ctx context.Context, name string, args ...string) error { cmd := exec.CommandContext(ctx, name, args...) return cmd.Run() } -// runCommandOutput executes a command and returns the output. -func runCommandOutput(ctx context.Context, name string, args ...string) (string, error) { +var runCommandOutput = func(ctx context.Context, name string, args ...string) (string, error) { cmd := exec.CommandContext(ctx, name, args...) output, err := cmd.CombinedOutput() return string(output), err } + +// lookPath searches for an executable in the directories named by the PATH environment variable. +var lookPath = exec.LookPath diff --git a/internal/notifications/notifications.go b/internal/notifications/notifications.go index 27d157c58..7e8dd943f 100644 --- a/internal/notifications/notifications.go +++ b/internal/notifications/notifications.go @@ -2585,6 +2585,18 @@ func buildNotificationTestAlert() *alerts.Alert { } } +// GetQueueStats returns statistics about the notification queue +func (n *NotificationManager) GetQueueStats() (map[string]int, error) { + n.mu.RLock() + queue := n.queue + n.mu.RUnlock() + + if queue == nil { + return nil, fmt.Errorf("notification queue not initialized") + } + return queue.GetQueueStats() +} + // SendTestNotification sends a test notification func (n *NotificationManager) SendTestNotification(method string) error { testAlert := buildNotificationTestAlert() diff --git a/internal/updates/manager.go b/internal/updates/manager.go index 3adc54588..3c1683079 100644 --- a/internal/updates/manager.go +++ b/internal/updates/manager.go @@ -142,6 +142,29 @@ func (m *Manager) GetQueue() *UpdateQueue { return m.queue } +// AddSSEClient adds a new SSE client for update progress streaming +func (m *Manager) AddSSEClient(w http.ResponseWriter, clientID string) *SSEClient { + if m.sseBroadcast == nil { + return nil + } + return m.sseBroadcast.AddClient(w, clientID) +} + +// RemoveSSEClient removes an SSE client +func (m *Manager) RemoveSSEClient(clientID string) { + if m.sseBroadcast != nil { + m.sseBroadcast.RemoveClient(clientID) + } +} + +// GetCachedStatus returns the last broadcasted status +func (m *Manager) GetSSECachedStatus() (UpdateStatus, time.Time) { + if m.sseBroadcast == nil { + return UpdateStatus{}, time.Time{} + } + return m.sseBroadcast.GetCachedStatus() +} + // CheckForUpdates checks GitHub for available updates using saved config channel func (m *Manager) CheckForUpdates(ctx context.Context) (*UpdateInfo, error) { return m.CheckForUpdatesWithChannel(ctx, "") diff --git a/pkg/audit/export.go b/pkg/audit/export.go index 48c3eaf6c..13909fe8a 100644 --- a/pkg/audit/export.go +++ b/pkg/audit/export.go @@ -38,13 +38,19 @@ type ExportEvent struct { SignatureValid *bool `json:"signature_valid,omitempty"` } +// PersistentLogger defines the interface for loggers that support querying and verification. +type PersistentLogger interface { + Query(filter QueryFilter) ([]Event, error) + VerifySignature(event Event) bool +} + // Exporter provides export functionality for audit logs. type Exporter struct { - logger *SQLiteLogger + logger PersistentLogger } // NewExporter creates a new exporter for the given logger. -func NewExporter(logger *SQLiteLogger) *Exporter { +func NewExporter(logger PersistentLogger) *Exporter { return &Exporter{logger: logger} }