package notifications import ( "testing" "time" "github.com/rcourtman/pulse-go-rewrite/internal/alerts" ) func flushPending(n *NotificationManager) { n.mu.Lock() if n.groupTimer != nil { n.groupTimer.Stop() n.groupTimer = nil } n.mu.Unlock() n.sendGroupedAlerts() } func TestNotificationCooldownAllowsNewAlertInstance(t *testing.T) { nm := NewNotificationManager("") nm.SetCooldown(1) // 1 minute cooldown nm.SetGroupingWindow(3600) // keep timer from firing immediately alertStart := time.Now().Add(-time.Minute) alertA := &alerts.Alert{ ID: "vm-100-memory", Type: "memory", Level: alerts.AlertLevelWarning, StartTime: alertStart, } nm.SendAlert(alertA) flushPending(nm) nm.mu.RLock() firstRecord, ok := nm.lastNotified[alertA.ID] nm.mu.RUnlock() if !ok { t.Fatalf("first notification not recorded") } nm.SendAlert(alertA) nm.mu.RLock() pendingAfter := len(nm.pendingAlerts) nm.mu.RUnlock() if pendingAfter != 0 { t.Fatalf("cooldown alert should not be queued, found %d pending", pendingAfter) } alertRestart := &alerts.Alert{ ID: "vm-100-memory", Type: "memory", Level: alerts.AlertLevelWarning, StartTime: alertStart.Add(time.Minute), } nm.SendAlert(alertRestart) flushPending(nm) nm.mu.RLock() recordAfter := nm.lastNotified[alertRestart.ID] nm.mu.RUnlock() if !recordAfter.alertStart.Equal(alertRestart.StartTime) { t.Fatalf("expected alertStart %v, got %v", alertRestart.StartTime, recordAfter.alertStart) } if !recordAfter.lastSent.After(firstRecord.lastSent) { t.Fatalf("lastSent was not updated for new alert instance") } } func TestCancelAlertRemovesPending(t *testing.T) { nm := NewNotificationManager("") nm.SetGroupingWindow(120) alertA := &alerts.Alert{ ID: "vm-100-disk", Type: "disk", Level: alerts.AlertLevelWarning, StartTime: time.Now(), } alertB := &alerts.Alert{ ID: "vm-101-disk", Type: "disk", Level: alerts.AlertLevelWarning, StartTime: time.Now(), } nm.SendAlert(alertA) nm.SendAlert(alertB) nm.CancelAlert(alertA.ID) nm.mu.RLock() remaining := make([]string, 0, len(nm.pendingAlerts)) for _, pending := range nm.pendingAlerts { if pending != nil { remaining = append(remaining, pending.ID) } } groupTimerActive := nm.groupTimer != nil nm.mu.RUnlock() if len(remaining) != 1 || remaining[0] != alertB.ID { t.Fatalf("expected only %s to remain pending, got %v", alertB.ID, remaining) } if !groupTimerActive { t.Fatalf("expected grouping timer to remain active while other alerts pending") } nm.CancelAlert(alertB.ID) nm.mu.RLock() if len(nm.pendingAlerts) != 0 { nm.mu.RUnlock() t.Fatalf("expected no pending alerts after cancelling all, found %d", len(nm.pendingAlerts)) } timerStopped := nm.groupTimer == nil nm.mu.RUnlock() if !timerStopped { t.Fatalf("expected grouping timer to be cleared when no alerts remain") } } func TestConvertWebhookCustomFields(t *testing.T) { if result := convertWebhookCustomFields(nil); result != nil { t.Fatalf("expected nil for empty input, got %#v", result) } original := map[string]string{ "app_token": "abc123", "user_token": "user456", } converted := convertWebhookCustomFields(original) if len(converted) != len(original) { t.Fatalf("expected %d keys, got %d", len(original), len(converted)) } for key, value := range original { if got, ok := converted[key]; !ok || got != value { t.Fatalf("expected %s=%s, got %v (present=%v)", key, value, got, ok) } } // Mutate original map and ensure converted copy remains unchanged original["extra"] = "new-value" if _, ok := converted["extra"]; ok { t.Fatalf("expected converted map to be independent of original mutations") } } func TestRenderWebhookURL_PathEncoding(t *testing.T) { data := WebhookPayloadData{ Message: "CPU spike detected", } result, err := renderWebhookURL("https://example.com/alerts/{{.Message}}", data) if err != nil { t.Fatalf("expected no error rendering URL template, got %v", err) } expected := "https://example.com/alerts/CPU%20spike%20detected" if result != expected { t.Fatalf("expected %s, got %s", expected, result) } } func TestRenderWebhookURL_QueryEncoding(t *testing.T) { data := WebhookPayloadData{ Message: "CPU & Memory > 90%", } result, err := renderWebhookURL("https://hooks.example.com?msg={{urlquery .Message}}", data) if err != nil { t.Fatalf("expected no error rendering URL template, got %v", err) } expected := "https://hooks.example.com?msg=CPU+%26+Memory+%3E+90%25" if result != expected { t.Fatalf("expected %s, got %s", expected, result) } } func TestRenderWebhookURL_InvalidTemplate(t *testing.T) { _, err := renderWebhookURL("https://example.com/{{.Missing", WebhookPayloadData{}) if err == nil { t.Fatalf("expected error for invalid URL template, got nil") } }