package alerts import ( "testing" alertconfig "github.com/rcourtman/pulse-go-rewrite/internal/alerts/config" ) func TestNormalizeSnapshotDefaults(t *testing.T) { tests := []struct { name string in SnapshotAlertConfig want SnapshotAlertConfig }{ { name: "negative values clamp to zero", in: SnapshotAlertConfig{ WarningDays: -1, CriticalDays: -2, WarningSizeGiB: -3, CriticalSizeGiB: -4, }, want: SnapshotAlertConfig{}, }, { name: "warning values clamp down to critical", in: SnapshotAlertConfig{ WarningDays: 10, CriticalDays: 3, WarningSizeGiB: 40, CriticalSizeGiB: 8, }, want: SnapshotAlertConfig{ WarningDays: 3, CriticalDays: 3, WarningSizeGiB: 8, CriticalSizeGiB: 8, }, }, { name: "critical inherits warning when critical unset", in: SnapshotAlertConfig{ WarningDays: 7, CriticalDays: 0, WarningSizeGiB: 12, CriticalSizeGiB: 0, }, want: SnapshotAlertConfig{ WarningDays: 7, CriticalDays: 7, WarningSizeGiB: 12, CriticalSizeGiB: 12, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { cfg := &AlertConfig{SnapshotDefaults: tc.in} normalizeSnapshotDefaults(cfg) got := cfg.SnapshotDefaults if got != tc.want { t.Fatalf("normalizeSnapshotDefaults() = %+v, want %+v", got, tc.want) } }) } } func TestNormalizeBackupDefaults(t *testing.T) { falseValue := false cfg := &AlertConfig{ BackupDefaults: BackupAlertConfig{ WarningDays: 10, CriticalDays: 3, AlertOrphaned: &falseValue, IgnoreVMIDs: []string{ " 101 ", "", "101", " 10* ", " ", "10*", }, }, } normalizeBackupDefaults(cfg) if cfg.BackupDefaults.WarningDays != 3 { t.Fatalf("WarningDays = %d, want 3", cfg.BackupDefaults.WarningDays) } if cfg.BackupDefaults.CriticalDays != 3 { t.Fatalf("CriticalDays = %d, want 3", cfg.BackupDefaults.CriticalDays) } if cfg.BackupDefaults.AlertOrphaned == nil { t.Fatal("AlertOrphaned should not be nil") } if *cfg.BackupDefaults.AlertOrphaned != false { t.Fatalf("AlertOrphaned = %v, want false", *cfg.BackupDefaults.AlertOrphaned) } wantIgnore := []string{"101", "10*"} if len(cfg.BackupDefaults.IgnoreVMIDs) != len(wantIgnore) { t.Fatalf("IgnoreVMIDs length = %d, want %d (%v)", len(cfg.BackupDefaults.IgnoreVMIDs), len(wantIgnore), cfg.BackupDefaults.IgnoreVMIDs) } for i := range wantIgnore { if cfg.BackupDefaults.IgnoreVMIDs[i] != wantIgnore[i] { t.Fatalf("IgnoreVMIDs[%d] = %q, want %q", i, cfg.BackupDefaults.IgnoreVMIDs[i], wantIgnore[i]) } } } func TestNormalizeBackupDefaultsSetsAlertOrphanedDefaultAndClampsNegativeDays(t *testing.T) { cfg := &AlertConfig{ BackupDefaults: BackupAlertConfig{ WarningDays: -2, CriticalDays: -1, }, } normalizeBackupDefaults(cfg) if cfg.BackupDefaults.WarningDays != 0 { t.Fatalf("WarningDays = %d, want 0", cfg.BackupDefaults.WarningDays) } if cfg.BackupDefaults.CriticalDays != 0 { t.Fatalf("CriticalDays = %d, want 0", cfg.BackupDefaults.CriticalDays) } if cfg.BackupDefaults.AlertOrphaned == nil { t.Fatal("AlertOrphaned should be defaulted to true") } if !*cfg.BackupDefaults.AlertOrphaned { t.Fatalf("AlertOrphaned = %v, want true", *cfg.BackupDefaults.AlertOrphaned) } } func TestBackupIgnoreVMID(t *testing.T) { tests := []struct { name string vmID string ignoreList []string want bool }{ { name: "empty vmid returns false", vmID: "", ignoreList: []string{"101"}, want: false, }, { name: "exact match", vmID: "101", ignoreList: []string{"101"}, want: true, }, { name: "prefix wildcard match", vmID: "10123", ignoreList: []string{"101*"}, want: true, }, { name: "trim spaces before matching", vmID: "200", ignoreList: []string{" 200 "}, want: true, }, { name: "star only pattern does not match everything", vmID: "101", ignoreList: []string{"*"}, want: false, }, { name: "blank entries ignored", vmID: "101", ignoreList: []string{" ", ""}, want: false, }, { name: "no match", vmID: "999", ignoreList: []string{"101", "10*"}, want: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if got := backupIgnoreVMID(tc.vmID, tc.ignoreList); got != tc.want { t.Fatalf("backupIgnoreVMID(%q, %v) = %v, want %v", tc.vmID, tc.ignoreList, got, tc.want) } }) } } func TestNormalizeDiskFillByType(t *testing.T) { t.Run("nil map seeds nvme/sata/hdd defaults", func(t *testing.T) { cfg := &AlertConfig{} alertconfig.NormalizeDiskFillByType(cfg) if cfg.DiskFillByType == nil { t.Fatal("expected DiskFillByType to be seeded, got nil") } nvme, ok := cfg.DiskFillByType["nvme"] if !ok { t.Fatal("nvme key not seeded") } if nvme.Trigger != 92 || nvme.Clear != 87 { t.Fatalf("nvme = %+v, want {Trigger:92 Clear:87}", nvme) } sata, ok := cfg.DiskFillByType["sata"] if !ok { t.Fatal("sata key not seeded") } if sata.Trigger != 90 || sata.Clear != 85 { t.Fatalf("sata = %+v, want {Trigger:90 Clear:85}", sata) } hdd, ok := cfg.DiskFillByType["hdd"] if !ok { t.Fatal("hdd key not seeded") } if hdd.Trigger != 85 || hdd.Clear != 80 { t.Fatalf("hdd = %+v, want {Trigger:85 Clear:80}", hdd) } }) t.Run("operator customized values survive", func(t *testing.T) { cfg := &AlertConfig{ DiskFillByType: map[string]HysteresisThreshold{ "nvme": {Trigger: 95, Clear: 90}, }, } alertconfig.NormalizeDiskFillByType(cfg) nvme := cfg.DiskFillByType["nvme"] if nvme.Trigger != 95 || nvme.Clear != 90 { t.Fatalf("nvme = %+v, want operator value {Trigger:95 Clear:90}", nvme) } // Missing keys still seeded. if sata, ok := cfg.DiskFillByType["sata"]; !ok || sata.Trigger != 90 || sata.Clear != 85 { t.Fatalf("sata = %+v ok=%v, want default {Trigger:90 Clear:85}", sata, ok) } if hdd, ok := cfg.DiskFillByType["hdd"]; !ok || hdd.Trigger != 85 || hdd.Clear != 80 { t.Fatalf("hdd = %+v ok=%v, want default {Trigger:85 Clear:80}", hdd, ok) } }) t.Run("negative trigger resets to default", func(t *testing.T) { cfg := &AlertConfig{ DiskFillByType: map[string]HysteresisThreshold{ "nvme": {Trigger: -1, Clear: 10}, "sata": {Trigger: 88, Clear: -5}, }, } alertconfig.NormalizeDiskFillByType(cfg) nvme := cfg.DiskFillByType["nvme"] if nvme.Trigger != 92 || nvme.Clear != 87 { t.Fatalf("nvme = %+v, want default reset {Trigger:92 Clear:87}", nvme) } sata := cfg.DiskFillByType["sata"] if sata.Trigger != 90 || sata.Clear != 85 { t.Fatalf("sata = %+v, want default reset {Trigger:90 Clear:85}", sata) } }) t.Run("mixed case keys lowercase to canonical", func(t *testing.T) { cfg := &AlertConfig{ DiskFillByType: map[string]HysteresisThreshold{ "NVMe": {Trigger: 93, Clear: 88}, }, } alertconfig.NormalizeDiskFillByType(cfg) if _, exists := cfg.DiskFillByType["NVMe"]; exists { t.Fatalf("expected mixed-case key NVMe to be removed, map=%+v", cfg.DiskFillByType) } nvme, ok := cfg.DiskFillByType["nvme"] if !ok { t.Fatalf("expected lowercase nvme key, map=%+v", cfg.DiskFillByType) } if nvme.Trigger != 93 || nvme.Clear != 88 { t.Fatalf("nvme = %+v, want preserved operator value {Trigger:93 Clear:88}", nvme) } }) } func TestDefaultAlertConfigSeedsDiskFillByType(t *testing.T) { cfg := defaultAlertConfig() if cfg.DiskFillByType == nil { t.Fatal("expected defaultAlertConfig to seed DiskFillByType, got nil") } if nvme, ok := cfg.DiskFillByType["nvme"]; !ok || nvme.Trigger != 92 || nvme.Clear != 87 { t.Fatalf("nvme = %+v ok=%v, want {Trigger:92 Clear:87}", nvme, ok) } if sata, ok := cfg.DiskFillByType["sata"]; !ok || sata.Trigger != 90 || sata.Clear != 85 { t.Fatalf("sata = %+v ok=%v, want {Trigger:90 Clear:85}", sata, ok) } if hdd, ok := cfg.DiskFillByType["hdd"]; !ok || hdd.Trigger != 85 || hdd.Clear != 80 { t.Fatalf("hdd = %+v ok=%v, want {Trigger:85 Clear:80}", hdd, ok) } } func TestNormalizeDiskTempByType(t *testing.T) { t.Run("nil map seeds nvme/sas/sata defaults", func(t *testing.T) { cfg := &AlertConfig{} alertconfig.NormalizeDiskTempByType(cfg) if cfg.DiskTempByType == nil { t.Fatal("expected DiskTempByType to be seeded, got nil") } nvme, ok := cfg.DiskTempByType["nvme"] if !ok { t.Fatal("nvme key not seeded") } if nvme.Trigger != 70 || nvme.Clear != 65 { t.Fatalf("nvme = %+v, want {Trigger:70 Clear:65}", nvme) } sas, ok := cfg.DiskTempByType["sas"] if !ok { t.Fatal("sas key not seeded") } if sas.Trigger != 65 || sas.Clear != 60 { t.Fatalf("sas = %+v, want {Trigger:65 Clear:60}", sas) } sata, ok := cfg.DiskTempByType["sata"] if !ok { t.Fatal("sata key not seeded") } if sata.Trigger != 55 || sata.Clear != 50 { t.Fatalf("sata = %+v, want {Trigger:55 Clear:50}", sata) } if _, ok := cfg.DiskTempByType["hdd"]; ok { t.Fatalf("hdd key should not be seeded for SMART temperature, map=%+v", cfg.DiskTempByType) } }) t.Run("operator customized values survive", func(t *testing.T) { cfg := &AlertConfig{ DiskTempByType: map[string]HysteresisThreshold{ "nvme": {Trigger: 75, Clear: 68}, }, } alertconfig.NormalizeDiskTempByType(cfg) nvme := cfg.DiskTempByType["nvme"] if nvme.Trigger != 75 || nvme.Clear != 68 { t.Fatalf("nvme = %+v, want operator value {Trigger:75 Clear:68}", nvme) } if sas, ok := cfg.DiskTempByType["sas"]; !ok || sas.Trigger != 65 || sas.Clear != 60 { t.Fatalf("sas = %+v ok=%v, want default {Trigger:65 Clear:60}", sas, ok) } if sata, ok := cfg.DiskTempByType["sata"]; !ok || sata.Trigger != 55 || sata.Clear != 50 { t.Fatalf("sata = %+v ok=%v, want default {Trigger:55 Clear:50}", sata, ok) } }) t.Run("negative trigger resets to default", func(t *testing.T) { cfg := &AlertConfig{ DiskTempByType: map[string]HysteresisThreshold{ "nvme": {Trigger: -1, Clear: 10}, "sas": {Trigger: 64, Clear: -5}, }, } alertconfig.NormalizeDiskTempByType(cfg) nvme := cfg.DiskTempByType["nvme"] if nvme.Trigger != 70 || nvme.Clear != 65 { t.Fatalf("nvme = %+v, want default reset {Trigger:70 Clear:65}", nvme) } sas := cfg.DiskTempByType["sas"] if sas.Trigger != 65 || sas.Clear != 60 { t.Fatalf("sas = %+v, want default reset {Trigger:65 Clear:60}", sas) } }) t.Run("mixed case keys lowercase to canonical", func(t *testing.T) { cfg := &AlertConfig{ DiskTempByType: map[string]HysteresisThreshold{ "NVMe": {Trigger: 72, Clear: 67}, }, } alertconfig.NormalizeDiskTempByType(cfg) if _, exists := cfg.DiskTempByType["NVMe"]; exists { t.Fatalf("expected mixed-case key NVMe to be removed, map=%+v", cfg.DiskTempByType) } nvme, ok := cfg.DiskTempByType["nvme"] if !ok { t.Fatalf("expected lowercase nvme key, map=%+v", cfg.DiskTempByType) } if nvme.Trigger != 72 || nvme.Clear != 67 { t.Fatalf("nvme = %+v, want preserved operator value {Trigger:72 Clear:67}", nvme) } }) } func TestDefaultAlertConfigSeedsDiskTempByType(t *testing.T) { cfg := defaultAlertConfig() if cfg.DiskTempByType == nil { t.Fatal("expected defaultAlertConfig to seed DiskTempByType, got nil") } if nvme, ok := cfg.DiskTempByType["nvme"]; !ok || nvme.Trigger != 70 || nvme.Clear != 65 { t.Fatalf("nvme = %+v ok=%v, want {Trigger:70 Clear:65}", nvme, ok) } if sas, ok := cfg.DiskTempByType["sas"]; !ok || sas.Trigger != 65 || sas.Clear != 60 { t.Fatalf("sas = %+v ok=%v, want {Trigger:65 Clear:60}", sas, ok) } if sata, ok := cfg.DiskTempByType["sata"]; !ok || sata.Trigger != 55 || sata.Clear != 50 { t.Fatalf("sata = %+v ok=%v, want {Trigger:55 Clear:50}", sata, ok) } if _, ok := cfg.DiskTempByType["hdd"]; ok { t.Fatalf("hdd key should not be seeded for SMART temperature, map=%+v", cfg.DiskTempByType) } }