test: Add preserveAlertState, checkPMGQuarantineBacklog, LoadActiveAlerts tests
Add comprehensive tests for three low-coverage functions: - preserveAlertState: nil handling, state preservation from existing alerts, ackState fallback, new alert handling - checkPMGQuarantineBacklog: nil quarantine handling, warning/critical thresholds, growth rate alerts, alert updates, virus quarantine - LoadActiveAlerts: missing file, valid file loading, old alert filtering, old acknowledged alert filtering, ack state restoration, invalid JSON, duplicate alert handling Coverage improvements: - preserveAlertState: 63.6% → 100% - checkPMGQuarantineBacklog: 12.9% → 100% - checkQuarantineMetric: 0% → 93.1% - LoadActiveAlerts: 26.2% → 80.0% - Alerts package: 83.5% → 86.6%
This commit is contained in:
parent
2133b25bb7
commit
a4e3fe65b8
1 changed files with 663 additions and 0 deletions
|
|
@ -1,9 +1,11 @@
|
||||||
package alerts
|
package alerts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
@ -11,6 +13,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
|
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -14276,3 +14279,663 @@ func TestDispatchAlert(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPreserveAlertState(t *testing.T) {
|
||||||
|
t.Run("nil updated alert is handled", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
// Should not panic
|
||||||
|
m.preserveAlertState("test-id", nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("preserves state from existing alert", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
ackTime := time.Now().Add(-30 * time.Minute)
|
||||||
|
existing := &Alert{
|
||||||
|
ID: "test-alert",
|
||||||
|
Type: "cpu",
|
||||||
|
StartTime: time.Now().Add(-1 * time.Hour),
|
||||||
|
Acknowledged: true,
|
||||||
|
AckUser: "testuser",
|
||||||
|
AckTime: &ackTime,
|
||||||
|
LastEscalation: 2,
|
||||||
|
EscalationTimes: []time.Time{time.Now().Add(-25 * time.Minute)},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.activeAlerts["test-alert"] = existing
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
updated := &Alert{
|
||||||
|
ID: "test-alert",
|
||||||
|
Type: "cpu",
|
||||||
|
StartTime: time.Now(), // Different start time
|
||||||
|
}
|
||||||
|
|
||||||
|
m.preserveAlertState("test-alert", updated)
|
||||||
|
|
||||||
|
if !updated.StartTime.Equal(existing.StartTime) {
|
||||||
|
t.Error("StartTime should be preserved from existing alert")
|
||||||
|
}
|
||||||
|
if !updated.Acknowledged {
|
||||||
|
t.Error("Acknowledged should be preserved")
|
||||||
|
}
|
||||||
|
if updated.AckUser != "testuser" {
|
||||||
|
t.Errorf("AckUser should be preserved, got %s", updated.AckUser)
|
||||||
|
}
|
||||||
|
if updated.AckTime == nil || !updated.AckTime.Equal(ackTime) {
|
||||||
|
t.Error("AckTime should be preserved")
|
||||||
|
}
|
||||||
|
if updated.LastEscalation != 2 {
|
||||||
|
t.Error("LastEscalation should be preserved")
|
||||||
|
}
|
||||||
|
if len(updated.EscalationTimes) != 1 {
|
||||||
|
t.Error("EscalationTimes should be preserved")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("falls back to ackState for new alert", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
ackTime := time.Now().Add(-15 * time.Minute)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.ackState["test-alert"] = ackRecord{
|
||||||
|
acknowledged: true,
|
||||||
|
user: "fallbackuser",
|
||||||
|
time: ackTime,
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
updated := &Alert{
|
||||||
|
ID: "test-alert",
|
||||||
|
Type: "cpu",
|
||||||
|
StartTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
m.preserveAlertState("test-alert", updated)
|
||||||
|
|
||||||
|
if !updated.Acknowledged {
|
||||||
|
t.Error("Acknowledged should be set from ackState")
|
||||||
|
}
|
||||||
|
if updated.AckUser != "fallbackuser" {
|
||||||
|
t.Errorf("AckUser should be from ackState, got %s", updated.AckUser)
|
||||||
|
}
|
||||||
|
if updated.AckTime == nil || !updated.AckTime.Equal(ackTime) {
|
||||||
|
t.Error("AckTime should be from ackState")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no state to preserve for new alert", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
updated := &Alert{
|
||||||
|
ID: "new-alert",
|
||||||
|
Type: "cpu",
|
||||||
|
StartTime: startTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.preserveAlertState("new-alert", updated)
|
||||||
|
|
||||||
|
if !updated.StartTime.Equal(startTime) {
|
||||||
|
t.Error("StartTime should remain unchanged for new alert")
|
||||||
|
}
|
||||||
|
if updated.Acknowledged {
|
||||||
|
t.Error("Acknowledged should remain false for new alert")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckPMGQuarantineBacklog(t *testing.T) {
|
||||||
|
t.Run("nil quarantine clears alerts", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
|
||||||
|
// Create an existing quarantine alert
|
||||||
|
m.mu.Lock()
|
||||||
|
m.activeAlerts["pmg1-quarantine-spam"] = &Alert{
|
||||||
|
ID: "pmg1-quarantine-spam",
|
||||||
|
Type: "quarantine-spam",
|
||||||
|
}
|
||||||
|
m.activeAlerts["pmg1-quarantine-virus"] = &Alert{
|
||||||
|
ID: "pmg1-quarantine-virus",
|
||||||
|
Type: "quarantine-virus",
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
pmg := models.PMGInstance{
|
||||||
|
ID: "pmg1",
|
||||||
|
Name: "pmg-server",
|
||||||
|
Host: "pmg.example.com",
|
||||||
|
Quarantine: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.checkPMGQuarantineBacklog(pmg, PMGThresholdConfig{})
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, spamExists := m.activeAlerts["pmg1-quarantine-spam"]
|
||||||
|
_, virusExists := m.activeAlerts["pmg1-quarantine-virus"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if spamExists {
|
||||||
|
t.Error("spam alert should be cleared when quarantine is nil")
|
||||||
|
}
|
||||||
|
if virusExists {
|
||||||
|
t.Error("virus alert should be cleared when quarantine is nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("warning threshold triggers alert", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.pmgQuarantineHistory = make(map[string][]pmgQuarantineSnapshot)
|
||||||
|
m.config.ActivationState = ActivationActive
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
pmg := models.PMGInstance{
|
||||||
|
ID: "pmg1",
|
||||||
|
Name: "pmg-server",
|
||||||
|
Host: "pmg.example.com",
|
||||||
|
Quarantine: &models.PMGQuarantineTotals{
|
||||||
|
Spam: 2500, // Above warning threshold
|
||||||
|
Virus: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
thresholds := PMGThresholdConfig{
|
||||||
|
QuarantineSpamWarn: 2000,
|
||||||
|
QuarantineSpamCritical: 5000,
|
||||||
|
QuarantineVirusWarn: 2000,
|
||||||
|
QuarantineVirusCritical: 5000,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.checkPMGQuarantineBacklog(pmg, thresholds)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alert, exists := m.activeAlerts["pmg1-quarantine-spam"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
t.Fatal("spam quarantine warning alert should be created")
|
||||||
|
}
|
||||||
|
if alert.Level != AlertLevelWarning {
|
||||||
|
t.Errorf("alert level should be warning, got %s", alert.Level)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("critical threshold triggers alert", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.pmgQuarantineHistory = make(map[string][]pmgQuarantineSnapshot)
|
||||||
|
m.config.ActivationState = ActivationActive
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
pmg := models.PMGInstance{
|
||||||
|
ID: "pmg1",
|
||||||
|
Name: "pmg-server",
|
||||||
|
Host: "pmg.example.com",
|
||||||
|
Quarantine: &models.PMGQuarantineTotals{
|
||||||
|
Spam: 6000, // Above critical threshold
|
||||||
|
Virus: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
thresholds := PMGThresholdConfig{
|
||||||
|
QuarantineSpamWarn: 2000,
|
||||||
|
QuarantineSpamCritical: 5000,
|
||||||
|
QuarantineVirusWarn: 2000,
|
||||||
|
QuarantineVirusCritical: 5000,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.checkPMGQuarantineBacklog(pmg, thresholds)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alert, exists := m.activeAlerts["pmg1-quarantine-spam"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
t.Fatal("spam quarantine critical alert should be created")
|
||||||
|
}
|
||||||
|
if alert.Level != AlertLevelCritical {
|
||||||
|
t.Errorf("alert level should be critical, got %s", alert.Level)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("below threshold clears alert", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.pmgQuarantineHistory = make(map[string][]pmgQuarantineSnapshot)
|
||||||
|
m.activeAlerts["pmg1-quarantine-spam"] = &Alert{
|
||||||
|
ID: "pmg1-quarantine-spam",
|
||||||
|
Type: "quarantine-spam",
|
||||||
|
Level: AlertLevelWarning,
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
pmg := models.PMGInstance{
|
||||||
|
ID: "pmg1",
|
||||||
|
Name: "pmg-server",
|
||||||
|
Host: "pmg.example.com",
|
||||||
|
Quarantine: &models.PMGQuarantineTotals{
|
||||||
|
Spam: 500, // Below warning threshold
|
||||||
|
Virus: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
thresholds := PMGThresholdConfig{
|
||||||
|
QuarantineSpamWarn: 2000,
|
||||||
|
QuarantineSpamCritical: 5000,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.checkPMGQuarantineBacklog(pmg, thresholds)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, exists := m.activeAlerts["pmg1-quarantine-spam"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
t.Error("spam quarantine alert should be cleared when below threshold")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("growth rate triggers warning alert", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.config.ActivationState = ActivationActive
|
||||||
|
// Set up history from ~2 hours ago
|
||||||
|
m.pmgQuarantineHistory = map[string][]pmgQuarantineSnapshot{
|
||||||
|
"pmg1": {
|
||||||
|
{
|
||||||
|
Spam: 1000,
|
||||||
|
Virus: 100,
|
||||||
|
Timestamp: time.Now().Add(-2 * time.Hour),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
pmg := models.PMGInstance{
|
||||||
|
ID: "pmg1",
|
||||||
|
Name: "pmg-server",
|
||||||
|
Host: "pmg.example.com",
|
||||||
|
Quarantine: &models.PMGQuarantineTotals{
|
||||||
|
Spam: 1500, // 50% growth (500 messages)
|
||||||
|
Virus: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
thresholds := PMGThresholdConfig{
|
||||||
|
QuarantineSpamWarn: 10000, // High absolute threshold (won't trigger)
|
||||||
|
QuarantineSpamCritical: 20000,
|
||||||
|
QuarantineGrowthWarnPct: 25, // 25% growth warning
|
||||||
|
QuarantineGrowthWarnMin: 250, // Minimum 250 messages
|
||||||
|
QuarantineGrowthCritPct: 50, // 50% growth critical
|
||||||
|
QuarantineGrowthCritMin: 500, // Minimum 500 messages
|
||||||
|
}
|
||||||
|
|
||||||
|
m.checkPMGQuarantineBacklog(pmg, thresholds)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alert, exists := m.activeAlerts["pmg1-quarantine-spam"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
t.Fatal("spam quarantine growth alert should be created")
|
||||||
|
}
|
||||||
|
if alert.Level != AlertLevelCritical {
|
||||||
|
t.Errorf("alert level should be critical due to 50%% growth + 500 messages, got %s", alert.Level)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates existing alert", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.pmgQuarantineHistory = make(map[string][]pmgQuarantineSnapshot)
|
||||||
|
m.config.ActivationState = ActivationActive
|
||||||
|
m.activeAlerts["pmg1-quarantine-spam"] = &Alert{
|
||||||
|
ID: "pmg1-quarantine-spam",
|
||||||
|
Type: "quarantine-spam",
|
||||||
|
Level: AlertLevelWarning,
|
||||||
|
Value: 2500,
|
||||||
|
Threshold: 2000,
|
||||||
|
LastSeen: time.Now().Add(-5 * time.Minute),
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
pmg := models.PMGInstance{
|
||||||
|
ID: "pmg1",
|
||||||
|
Name: "pmg-server",
|
||||||
|
Host: "pmg.example.com",
|
||||||
|
Quarantine: &models.PMGQuarantineTotals{
|
||||||
|
Spam: 3000, // Higher spam count
|
||||||
|
Virus: 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
thresholds := PMGThresholdConfig{
|
||||||
|
QuarantineSpamWarn: 2000,
|
||||||
|
QuarantineSpamCritical: 5000,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.checkPMGQuarantineBacklog(pmg, thresholds)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alert, exists := m.activeAlerts["pmg1-quarantine-spam"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
t.Fatal("spam quarantine alert should still exist")
|
||||||
|
}
|
||||||
|
if alert.Value != 3000 {
|
||||||
|
t.Errorf("alert value should be updated to 3000, got %.0f", alert.Value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("virus quarantine alert", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.pmgQuarantineHistory = make(map[string][]pmgQuarantineSnapshot)
|
||||||
|
m.config.ActivationState = ActivationActive
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
pmg := models.PMGInstance{
|
||||||
|
ID: "pmg1",
|
||||||
|
Name: "pmg-server",
|
||||||
|
Host: "pmg.example.com",
|
||||||
|
Quarantine: &models.PMGQuarantineTotals{
|
||||||
|
Spam: 100,
|
||||||
|
Virus: 3000, // Above virus warning threshold
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
thresholds := PMGThresholdConfig{
|
||||||
|
QuarantineSpamWarn: 2000,
|
||||||
|
QuarantineSpamCritical: 5000,
|
||||||
|
QuarantineVirusWarn: 2000,
|
||||||
|
QuarantineVirusCritical: 5000,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.checkPMGQuarantineBacklog(pmg, thresholds)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alert, exists := m.activeAlerts["pmg1-quarantine-virus"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
t.Fatal("virus quarantine warning alert should be created")
|
||||||
|
}
|
||||||
|
if alert.Level != AlertLevelWarning {
|
||||||
|
t.Errorf("alert level should be warning, got %s", alert.Level)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadActiveAlerts(t *testing.T) {
|
||||||
|
t.Run("no file returns nil error", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
|
||||||
|
err := m.LoadActiveAlerts()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error when file doesn't exist, got %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("loads alerts from valid file", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
|
||||||
|
// Create an alert and save it
|
||||||
|
startTime := time.Now().Add(-30 * time.Minute)
|
||||||
|
alert := &Alert{
|
||||||
|
ID: "test-load-alert",
|
||||||
|
Type: "cpu",
|
||||||
|
Level: AlertLevelWarning,
|
||||||
|
ResourceID: "test-resource",
|
||||||
|
ResourceName: "test-vm",
|
||||||
|
Node: "node1",
|
||||||
|
Instance: "pve1",
|
||||||
|
Message: "Test alert",
|
||||||
|
Value: 85.0,
|
||||||
|
Threshold: 80.0,
|
||||||
|
StartTime: startTime,
|
||||||
|
LastSeen: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.activeAlerts[alert.ID] = alert
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
// Save to disk
|
||||||
|
_ = m.SaveActiveAlerts()
|
||||||
|
|
||||||
|
// Clear and reload
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
|
||||||
|
err := m.LoadActiveAlerts()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to load alerts: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
loaded, exists := m.activeAlerts["test-load-alert"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
t.Fatal("alert should be loaded from file")
|
||||||
|
}
|
||||||
|
if loaded.Type != "cpu" {
|
||||||
|
t.Errorf("loaded alert type should be cpu, got %s", loaded.Type)
|
||||||
|
}
|
||||||
|
if loaded.Value != 85.0 {
|
||||||
|
t.Errorf("loaded alert value should be 85.0, got %.1f", loaded.Value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("skips old alerts", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
|
||||||
|
// Create an old alert (>24 hours)
|
||||||
|
startTime := time.Now().Add(-25 * time.Hour)
|
||||||
|
alert := &Alert{
|
||||||
|
ID: "old-alert",
|
||||||
|
Type: "cpu",
|
||||||
|
Level: AlertLevelWarning,
|
||||||
|
ResourceID: "test-resource",
|
||||||
|
ResourceName: "test-vm",
|
||||||
|
StartTime: startTime,
|
||||||
|
LastSeen: startTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.activeAlerts[alert.ID] = alert
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
// Save to disk
|
||||||
|
_ = m.SaveActiveAlerts()
|
||||||
|
|
||||||
|
// Clear and reload
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
|
||||||
|
err := m.LoadActiveAlerts()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to load alerts: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, exists := m.activeAlerts["old-alert"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
t.Error("old alert (>24h) should be skipped during load")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("skips old acknowledged alerts", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
|
||||||
|
// Create an alert acknowledged >1 hour ago
|
||||||
|
startTime := time.Now().Add(-30 * time.Minute)
|
||||||
|
ackTime := time.Now().Add(-2 * time.Hour)
|
||||||
|
alert := &Alert{
|
||||||
|
ID: "old-ack-alert",
|
||||||
|
Type: "cpu",
|
||||||
|
Level: AlertLevelWarning,
|
||||||
|
ResourceID: "test-resource",
|
||||||
|
ResourceName: "test-vm",
|
||||||
|
StartTime: startTime,
|
||||||
|
LastSeen: time.Now(),
|
||||||
|
Acknowledged: true,
|
||||||
|
AckTime: &ackTime,
|
||||||
|
AckUser: "testuser",
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.activeAlerts[alert.ID] = alert
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
// Save to disk
|
||||||
|
_ = m.SaveActiveAlerts()
|
||||||
|
|
||||||
|
// Clear and reload
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
|
||||||
|
err := m.LoadActiveAlerts()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to load alerts: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, exists := m.activeAlerts["old-ack-alert"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
t.Error("old acknowledged alert (>1h) should be skipped during load")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("restores acknowledgment state", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
|
||||||
|
// Create an acknowledged alert
|
||||||
|
startTime := time.Now().Add(-10 * time.Minute)
|
||||||
|
ackTime := time.Now().Add(-5 * time.Minute)
|
||||||
|
alert := &Alert{
|
||||||
|
ID: "ack-alert",
|
||||||
|
Type: "cpu",
|
||||||
|
Level: AlertLevelWarning,
|
||||||
|
ResourceID: "test-resource",
|
||||||
|
ResourceName: "test-vm",
|
||||||
|
StartTime: startTime,
|
||||||
|
LastSeen: time.Now(),
|
||||||
|
Acknowledged: true,
|
||||||
|
AckTime: &ackTime,
|
||||||
|
AckUser: "testuser",
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.activeAlerts[alert.ID] = alert
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
// Save to disk
|
||||||
|
_ = m.SaveActiveAlerts()
|
||||||
|
|
||||||
|
// Clear and reload
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
|
||||||
|
err := m.LoadActiveAlerts()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to load alerts: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
loaded, exists := m.activeAlerts["ack-alert"]
|
||||||
|
ackRecord, hasAckRecord := m.ackState["ack-alert"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
t.Fatal("acknowledged alert should be loaded")
|
||||||
|
}
|
||||||
|
if !loaded.Acknowledged {
|
||||||
|
t.Error("loaded alert should be acknowledged")
|
||||||
|
}
|
||||||
|
if loaded.AckUser != "testuser" {
|
||||||
|
t.Errorf("loaded alert AckUser should be testuser, got %s", loaded.AckUser)
|
||||||
|
}
|
||||||
|
if !hasAckRecord {
|
||||||
|
t.Error("ackState should be restored for acknowledged alert")
|
||||||
|
}
|
||||||
|
if !ackRecord.acknowledged {
|
||||||
|
t.Error("ackState should show acknowledged=true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid JSON returns error", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
|
||||||
|
// Write invalid JSON to the alerts file
|
||||||
|
alertsDir := filepath.Join(utils.GetDataDir(), "alerts")
|
||||||
|
if err := os.MkdirAll(alertsDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create alerts dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
alertsFile := filepath.Join(alertsDir, "active-alerts.json")
|
||||||
|
if err := os.WriteFile(alertsFile, []byte("invalid json"), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write invalid json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := m.LoadActiveAlerts()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for invalid JSON")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("skips duplicate alerts", func(t *testing.T) {
|
||||||
|
m := newTestManager(t)
|
||||||
|
m.ClearActiveAlerts()
|
||||||
|
|
||||||
|
// Write JSON with duplicate alert IDs
|
||||||
|
alertsDir := filepath.Join(utils.GetDataDir(), "alerts")
|
||||||
|
if err := os.MkdirAll(alertsDir, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create alerts dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime := time.Now().Add(-10 * time.Minute)
|
||||||
|
alerts := []Alert{
|
||||||
|
{ID: "dup-alert", Type: "cpu", StartTime: startTime, LastSeen: time.Now()},
|
||||||
|
{ID: "dup-alert", Type: "memory", StartTime: startTime, LastSeen: time.Now()},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := json.Marshal(alerts)
|
||||||
|
alertsFile := filepath.Join(alertsDir, "active-alerts.json")
|
||||||
|
if err := os.WriteFile(alertsFile, data, 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write alerts json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := m.LoadActiveAlerts()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to load alerts: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
alert, exists := m.activeAlerts["dup-alert"]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
t.Fatal("alert should exist after load")
|
||||||
|
}
|
||||||
|
// First one wins
|
||||||
|
if alert.Type != "cpu" {
|
||||||
|
t.Errorf("first alert should win, got type %s", alert.Type)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue