test: Add HistoryManager tests for alerts package

New history_test.go with 24 tests covering GetStats, getFileSize,
AddAlert, GetHistory, GetAllHistory, RemoveAlert, ClearAllHistory,
cleanOldEntries, saveHistory, loadHistory. Alerts package 83.6%→84.0%.
This commit is contained in:
rcourtman 2025-12-02 12:06:53 +00:00
parent fc96ba7b69
commit f184174f2b

View file

@ -0,0 +1,492 @@
package alerts
import (
"os"
"path/filepath"
"testing"
"time"
)
// newTestHistoryManager creates a HistoryManager using a temp directory
// that is automatically cleaned up after the test.
func newTestHistoryManager(t *testing.T) *HistoryManager {
t.Helper()
tempDir := t.TempDir()
// Create minimal HistoryManager without starting goroutines
hm := &HistoryManager{
dataDir: tempDir,
historyFile: filepath.Join(tempDir, HistoryFileName),
backupFile: filepath.Join(tempDir, HistoryBackupFileName),
history: make([]HistoryEntry, 0),
saveInterval: 5 * time.Minute,
stopChan: make(chan struct{}),
}
return hm
}
func TestGetStats_EmptyHistory(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
stats := hm.GetStats()
if stats["totalEntries"].(int) != 0 {
t.Errorf("totalEntries = %v, want 0", stats["totalEntries"])
}
if stats["dataDir"].(string) != hm.dataDir {
t.Errorf("dataDir = %v, want %v", stats["dataDir"], hm.dataDir)
}
if stats["fileSize"].(int64) != 0 {
t.Errorf("fileSize = %v, want 0", stats["fileSize"])
}
}
func TestGetStats_WithHistory(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
// Add some history entries
now := time.Now()
hm.history = []HistoryEntry{
{Alert: Alert{ID: "alert1"}, Timestamp: now.Add(-1 * time.Hour)},
{Alert: Alert{ID: "alert2"}, Timestamp: now.Add(-30 * time.Minute)},
{Alert: Alert{ID: "alert3"}, Timestamp: now},
}
stats := hm.GetStats()
if stats["totalEntries"].(int) != 3 {
t.Errorf("totalEntries = %v, want 3", stats["totalEntries"])
}
oldest := stats["oldestEntry"].(time.Time)
if !oldest.Equal(hm.history[0].Timestamp) {
t.Errorf("oldestEntry = %v, want %v", oldest, hm.history[0].Timestamp)
}
newest := stats["newestEntry"].(time.Time)
if !newest.Equal(hm.history[2].Timestamp) {
t.Errorf("newestEntry = %v, want %v", newest, hm.history[2].Timestamp)
}
}
func TestGetFileSize_NonExistent(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
size := hm.getFileSize()
if size != 0 {
t.Errorf("getFileSize for non-existent file = %v, want 0", size)
}
}
func TestGetFileSize_ExistingFile(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
// Create a file with known content
content := []byte(`[{"alert":{"id":"test"},"timestamp":"2024-01-01T00:00:00Z"}]`)
if err := os.WriteFile(hm.historyFile, content, 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
size := hm.getFileSize()
if size != int64(len(content)) {
t.Errorf("getFileSize = %v, want %v", size, len(content))
}
}
func TestAddAlert(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
alert := Alert{
ID: "test-alert-1",
Type: "cpu",
Level: AlertLevelWarning,
ResourceName: "pve1",
Message: "CPU high",
}
hm.AddAlert(alert)
if len(hm.history) != 1 {
t.Fatalf("history length = %d, want 1", len(hm.history))
}
if hm.history[0].Alert.ID != "test-alert-1" {
t.Errorf("alert ID = %s, want test-alert-1", hm.history[0].Alert.ID)
}
}
func TestGetHistory_WithLimit(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
now := time.Now()
hm.history = []HistoryEntry{
{Alert: Alert{ID: "alert1"}, Timestamp: now.Add(-2 * time.Hour)},
{Alert: Alert{ID: "alert2"}, Timestamp: now.Add(-1 * time.Hour)},
{Alert: Alert{ID: "alert3"}, Timestamp: now.Add(-30 * time.Minute)},
{Alert: Alert{ID: "alert4"}, Timestamp: now.Add(-10 * time.Minute)},
}
// Get last 2 entries
results := hm.GetHistory(time.Time{}, 2)
if len(results) != 2 {
t.Fatalf("len(results) = %d, want 2", len(results))
}
// Should be newest first
if results[0].ID != "alert4" {
t.Errorf("first result ID = %s, want alert4", results[0].ID)
}
if results[1].ID != "alert3" {
t.Errorf("second result ID = %s, want alert3", results[1].ID)
}
}
func TestGetHistory_WithSinceFilter(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
now := time.Now()
hm.history = []HistoryEntry{
{Alert: Alert{ID: "alert1"}, Timestamp: now.Add(-2 * time.Hour)},
{Alert: Alert{ID: "alert2"}, Timestamp: now.Add(-1 * time.Hour)},
{Alert: Alert{ID: "alert3"}, Timestamp: now.Add(-30 * time.Minute)},
{Alert: Alert{ID: "alert4"}, Timestamp: now.Add(-10 * time.Minute)},
}
// Get entries from last 45 minutes
since := now.Add(-45 * time.Minute)
results := hm.GetHistory(since, 0)
if len(results) != 2 {
t.Fatalf("len(results) = %d, want 2 (alert3 and alert4)", len(results))
}
}
func TestGetAllHistory(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
now := time.Now()
hm.history = []HistoryEntry{
{Alert: Alert{ID: "alert1"}, Timestamp: now.Add(-2 * time.Hour)},
{Alert: Alert{ID: "alert2"}, Timestamp: now.Add(-1 * time.Hour)},
{Alert: Alert{ID: "alert3"}, Timestamp: now.Add(-30 * time.Minute)},
}
results := hm.GetAllHistory(0)
if len(results) != 3 {
t.Fatalf("len(results) = %d, want 3", len(results))
}
// Should be newest first
if results[0].ID != "alert3" {
t.Errorf("first result ID = %s, want alert3", results[0].ID)
}
}
func TestGetAllHistory_WithLimit(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
now := time.Now()
hm.history = []HistoryEntry{
{Alert: Alert{ID: "alert1"}, Timestamp: now.Add(-2 * time.Hour)},
{Alert: Alert{ID: "alert2"}, Timestamp: now.Add(-1 * time.Hour)},
{Alert: Alert{ID: "alert3"}, Timestamp: now.Add(-30 * time.Minute)},
}
results := hm.GetAllHistory(2)
if len(results) != 2 {
t.Fatalf("len(results) = %d, want 2", len(results))
}
// Should be newest 2, with newest first
if results[0].ID != "alert3" {
t.Errorf("first result ID = %s, want alert3", results[0].ID)
}
if results[1].ID != "alert2" {
t.Errorf("second result ID = %s, want alert2", results[1].ID)
}
}
func TestRemoveAlert(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
now := time.Now()
hm.history = []HistoryEntry{
{Alert: Alert{ID: "alert1"}, Timestamp: now.Add(-2 * time.Hour)},
{Alert: Alert{ID: "alert2"}, Timestamp: now.Add(-1 * time.Hour)},
{Alert: Alert{ID: "alert3"}, Timestamp: now.Add(-30 * time.Minute)},
}
hm.RemoveAlert("alert2")
if len(hm.history) != 2 {
t.Fatalf("history length = %d, want 2", len(hm.history))
}
for _, entry := range hm.history {
if entry.Alert.ID == "alert2" {
t.Error("alert2 should have been removed")
}
}
}
func TestRemoveAlert_NotFound(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
hm.history = []HistoryEntry{
{Alert: Alert{ID: "alert1"}, Timestamp: time.Now()},
}
// Should not panic or error
hm.RemoveAlert("nonexistent")
if len(hm.history) != 1 {
t.Errorf("history length = %d, want 1", len(hm.history))
}
}
func TestClearAllHistory(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
// Add some history
hm.history = []HistoryEntry{
{Alert: Alert{ID: "alert1"}, Timestamp: time.Now()},
{Alert: Alert{ID: "alert2"}, Timestamp: time.Now()},
}
// Create files
_ = os.WriteFile(hm.historyFile, []byte("[]"), 0644)
_ = os.WriteFile(hm.backupFile, []byte("[]"), 0644)
err := hm.ClearAllHistory()
if err != nil {
t.Fatalf("ClearAllHistory error: %v", err)
}
if len(hm.history) != 0 {
t.Errorf("history length = %d, want 0", len(hm.history))
}
// Files should be removed
if _, err := os.Stat(hm.historyFile); !os.IsNotExist(err) {
t.Error("history file should be removed")
}
if _, err := os.Stat(hm.backupFile); !os.IsNotExist(err) {
t.Error("backup file should be removed")
}
}
func TestCleanOldEntries(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
now := time.Now()
hm.history = []HistoryEntry{
{Alert: Alert{ID: "old1"}, Timestamp: now.AddDate(0, 0, -40)}, // 40 days old - should be removed
{Alert: Alert{ID: "old2"}, Timestamp: now.AddDate(0, 0, -35)}, // 35 days old - should be removed
{Alert: Alert{ID: "recent1"}, Timestamp: now.AddDate(0, 0, -25)}, // 25 days old - should stay
{Alert: Alert{ID: "recent2"}, Timestamp: now.AddDate(0, 0, -1)}, // 1 day old - should stay
}
hm.cleanOldEntries()
if len(hm.history) != 2 {
t.Fatalf("history length = %d, want 2", len(hm.history))
}
// Check that only recent entries remain
for _, entry := range hm.history {
if entry.Alert.ID == "old1" || entry.Alert.ID == "old2" {
t.Errorf("old entry %s should have been removed", entry.Alert.ID)
}
}
}
func TestSaveHistory(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
now := time.Now()
hm.history = []HistoryEntry{
{Alert: Alert{ID: "alert1", Type: "cpu"}, Timestamp: now},
{Alert: Alert{ID: "alert2", Type: "memory"}, Timestamp: now},
}
err := hm.saveHistory()
if err != nil {
t.Fatalf("saveHistory error: %v", err)
}
// Verify file was created
if _, err := os.Stat(hm.historyFile); os.IsNotExist(err) {
t.Error("history file should exist after save")
}
// Read file and verify content
data, err := os.ReadFile(hm.historyFile)
if err != nil {
t.Fatalf("Failed to read history file: %v", err)
}
if len(data) == 0 {
t.Error("history file should not be empty")
}
}
func TestLoadHistory_NonExistent(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
err := hm.loadHistory()
if err != nil {
t.Fatalf("loadHistory error: %v", err)
}
if len(hm.history) != 0 {
t.Errorf("history should be empty for non-existent files, got %d entries", len(hm.history))
}
}
func TestLoadHistory_FromMainFile(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
// Create a history file with a recent timestamp (within MaxHistoryDays)
recentTime := time.Now().Add(-24 * time.Hour).Format(time.RFC3339)
content := `[{"alert":{"id":"test-1","type":"cpu"},"timestamp":"` + recentTime + `"}]`
if err := os.WriteFile(hm.historyFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
err := hm.loadHistory()
if err != nil {
t.Fatalf("loadHistory error: %v", err)
}
if len(hm.history) != 1 {
t.Fatalf("history length = %d, want 1", len(hm.history))
}
if hm.history[0].Alert.ID != "test-1" {
t.Errorf("alert ID = %s, want test-1", hm.history[0].Alert.ID)
}
}
func TestLoadHistory_FromBackupFile(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
// Only create backup file with a recent timestamp
recentTime := time.Now().Add(-24 * time.Hour).Format(time.RFC3339)
content := `[{"alert":{"id":"backup-1","type":"memory"},"timestamp":"` + recentTime + `"}]`
if err := os.WriteFile(hm.backupFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to create backup file: %v", err)
}
err := hm.loadHistory()
if err != nil {
t.Fatalf("loadHistory error: %v", err)
}
if len(hm.history) != 1 {
t.Fatalf("history length = %d, want 1", len(hm.history))
}
if hm.history[0].Alert.ID != "backup-1" {
t.Errorf("alert ID = %s, want backup-1", hm.history[0].Alert.ID)
}
}
func TestLoadHistory_InvalidJSON(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
// Create an invalid JSON file
if err := os.WriteFile(hm.historyFile, []byte("not valid json{"), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
err := hm.loadHistory()
if err == nil {
t.Error("loadHistory should return error for invalid JSON")
}
}
func TestSaveHistoryWithRetry_Success(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
hm.history = []HistoryEntry{
{Alert: Alert{ID: "alert1"}, Timestamp: time.Now()},
}
err := hm.saveHistoryWithRetry(3)
if err != nil {
t.Fatalf("saveHistoryWithRetry error: %v", err)
}
// Verify file was created
if _, err := os.Stat(hm.historyFile); os.IsNotExist(err) {
t.Error("history file should exist after save")
}
}
func TestSaveHistory_CreatesBackup(t *testing.T) {
t.Parallel()
hm := newTestHistoryManager(t)
// Create initial history file
if err := os.WriteFile(hm.historyFile, []byte("[]"), 0644); err != nil {
t.Fatalf("Failed to create initial file: %v", err)
}
hm.history = []HistoryEntry{
{Alert: Alert{ID: "new-alert"}, Timestamp: time.Now()},
}
err := hm.saveHistory()
if err != nil {
t.Fatalf("saveHistory error: %v", err)
}
// Backup should exist (created from previous file)
if _, err := os.Stat(hm.backupFile); os.IsNotExist(err) {
t.Error("backup file should exist after save with existing main file")
}
}