Pulse/internal/notifications/notifications_test.go
2025-10-11 23:29:47 +00:00

154 lines
3.7 KiB
Go

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")
}
}