package monitoring import ( "testing" "time" "github.com/rcourtman/pulse-go-rewrite/internal/alerts" "github.com/rcourtman/pulse-go-rewrite/internal/config" "github.com/rcourtman/pulse-go-rewrite/internal/models" agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host" ) func TestEvaluateHostAgentsTriggersOfflineAlert(t *testing.T) { t.Helper() monitor := &Monitor{ state: models.NewState(), alertManager: alerts.NewManager(), hostTokenBindings: make(map[string]string), config: &config.Config{}, } t.Cleanup(func() { monitor.alertManager.Stop() }) hostID := "host-offline" monitor.state.UpsertHost(models.Host{ ID: hostID, Hostname: "offline.local", DisplayName: "Offline Host", Status: "online", IntervalSeconds: 30, LastSeen: time.Now().Add(-10 * time.Minute), }) now := time.Now() for i := 0; i < 3; i++ { monitor.evaluateHostAgents(now.Add(time.Duration(i) * time.Second)) } snapshot := monitor.state.GetSnapshot() statusUpdated := false for _, host := range snapshot.Hosts { if host.ID == hostID { statusUpdated = true if got := host.Status; got != "offline" { t.Fatalf("expected host status offline, got %q", got) } } } if !statusUpdated { t.Fatalf("host %q not found in state snapshot", hostID) } connKey := hostConnectionPrefix + hostID if healthy, ok := snapshot.ConnectionHealth[connKey]; !ok || healthy { t.Fatalf("expected connection health false, got %v (exists=%v)", healthy, ok) } alerts := monitor.alertManager.GetActiveAlerts() found := false for _, alert := range alerts { if alert.ID == "host-offline-"+hostID { found = true break } } if !found { t.Fatalf("expected host offline alert to remain active") } } func TestEvaluateHostAgentsClearsAlertWhenHostReturns(t *testing.T) { t.Helper() monitor := &Monitor{ state: models.NewState(), alertManager: alerts.NewManager(), hostTokenBindings: make(map[string]string), config: &config.Config{}, } t.Cleanup(func() { monitor.alertManager.Stop() }) hostID := "host-recover" monitor.state.UpsertHost(models.Host{ ID: hostID, Hostname: "recover.local", DisplayName: "Recover Host", Status: "online", IntervalSeconds: 30, LastSeen: time.Now().Add(-10 * time.Minute), }) for i := 0; i < 3; i++ { monitor.evaluateHostAgents(time.Now().Add(time.Duration(i) * time.Second)) } monitor.state.UpsertHost(models.Host{ ID: hostID, Hostname: "recover.local", DisplayName: "Recover Host", Status: "online", IntervalSeconds: 30, LastSeen: time.Now(), }) monitor.evaluateHostAgents(time.Now()) snapshot := monitor.state.GetSnapshot() connKey := hostConnectionPrefix + hostID if healthy, ok := snapshot.ConnectionHealth[connKey]; !ok || !healthy { t.Fatalf("expected connection health true after recovery, got %v (exists=%v)", healthy, ok) } for _, alert := range monitor.alertManager.GetActiveAlerts() { if alert.ID == "host-offline-"+hostID { t.Fatalf("offline alert still active after recovery") } } } func TestApplyHostReportRejectsTokenReuseAcrossAgents(t *testing.T) { t.Helper() monitor := &Monitor{ state: models.NewState(), alertManager: alerts.NewManager(), hostTokenBindings: make(map[string]string), config: &config.Config{}, } t.Cleanup(func() { monitor.alertManager.Stop() }) now := time.Now().UTC() baseReport := agentshost.Report{ Agent: agentshost.AgentInfo{ ID: "agent-one", Version: "1.0.0", IntervalSeconds: 30, }, Host: agentshost.HostInfo{ ID: "machine-one", Hostname: "host-one", Platform: "linux", OSName: "debian", OSVersion: "12", }, Timestamp: now, Metrics: agentshost.Metrics{ CPUUsagePercent: 1.0, }, } token := &config.APITokenRecord{ID: "token-one", Name: "Token One"} hostOne, err := monitor.ApplyHostReport(baseReport, token) if err != nil { t.Fatalf("ApplyHostReport hostOne: %v", err) } if hostOne.ID == "" { t.Fatalf("expected hostOne to have an identifier") } secondReport := baseReport secondReport.Agent.ID = "agent-two" secondReport.Host.ID = "machine-two" secondReport.Host.Hostname = "host-two" secondReport.Timestamp = now.Add(30 * time.Second) if _, err := monitor.ApplyHostReport(secondReport, token); err == nil { t.Fatalf("expected token reuse across agents to be rejected") } } func TestRemoveHostAgentUnbindsToken(t *testing.T) { t.Helper() monitor := &Monitor{ state: models.NewState(), alertManager: alerts.NewManager(), hostTokenBindings: make(map[string]string), config: &config.Config{}, } t.Cleanup(func() { monitor.alertManager.Stop() }) hostID := "host-to-remove" tokenID := "token-remove" monitor.state.UpsertHost(models.Host{ ID: hostID, Hostname: "remove.me", TokenID: tokenID, }) monitor.hostTokenBindings[tokenID] = "agent-remove" if _, err := monitor.RemoveHostAgent(hostID); err != nil { t.Fatalf("RemoveHostAgent: %v", err) } if _, exists := monitor.hostTokenBindings[tokenID]; exists { t.Fatalf("expected token binding to be cleared after host removal") } }