mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
Add unit tests for Alert clone and metadata utility functions (alerts)
Tests for: - normalizePoweredOffSeverity: Verifies severity normalization to warning/critical - cloneMetadata: Deep copy functionality for metadata maps - cloneMetadataValue: Type-specific cloning for various slice and map types - Alert.Clone: Full alert cloning with pointer field independence 46 test cases covering nil handling, deep copy verification, slice/map independence, and all supported metadata value types. Coverage improved from 44.8% to 45.3%.
This commit is contained in:
parent
943c2b5082
commit
f0ead101d3
1 changed files with 490 additions and 0 deletions
490
internal/alerts/clone_test.go
Normal file
490
internal/alerts/clone_test.go
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
package alerts
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNormalizePoweredOffSeverity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input AlertLevel
|
||||
expected AlertLevel
|
||||
}{
|
||||
{
|
||||
name: "critical lowercase",
|
||||
input: AlertLevel("critical"),
|
||||
expected: AlertLevelCritical,
|
||||
},
|
||||
{
|
||||
name: "critical uppercase",
|
||||
input: AlertLevel("CRITICAL"),
|
||||
expected: AlertLevelCritical,
|
||||
},
|
||||
{
|
||||
name: "critical mixed case",
|
||||
input: AlertLevel("Critical"),
|
||||
expected: AlertLevelCritical,
|
||||
},
|
||||
{
|
||||
name: "warning lowercase",
|
||||
input: AlertLevel("warning"),
|
||||
expected: AlertLevelWarning,
|
||||
},
|
||||
{
|
||||
name: "warning uppercase",
|
||||
input: AlertLevel("WARNING"),
|
||||
expected: AlertLevelWarning,
|
||||
},
|
||||
{
|
||||
name: "empty string defaults to warning",
|
||||
input: AlertLevel(""),
|
||||
expected: AlertLevelWarning,
|
||||
},
|
||||
{
|
||||
name: "unknown value defaults to warning",
|
||||
input: AlertLevel("info"),
|
||||
expected: AlertLevelWarning,
|
||||
},
|
||||
{
|
||||
name: "garbage value defaults to warning",
|
||||
input: AlertLevel("xyz123"),
|
||||
expected: AlertLevelWarning,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := normalizePoweredOffSeverity(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("normalizePoweredOffSeverity(%q) = %q, want %q", tc.input, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneMetadata_Nil(t *testing.T) {
|
||||
result := cloneMetadata(nil)
|
||||
if result != nil {
|
||||
t.Errorf("cloneMetadata(nil) = %v, want nil", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneMetadata_Empty(t *testing.T) {
|
||||
src := map[string]interface{}{}
|
||||
result := cloneMetadata(src)
|
||||
if result == nil {
|
||||
t.Fatal("cloneMetadata({}) returned nil, want empty map")
|
||||
}
|
||||
if len(result) != 0 {
|
||||
t.Errorf("cloneMetadata({}) length = %d, want 0", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneMetadata_SimpleValues(t *testing.T) {
|
||||
src := map[string]interface{}{
|
||||
"string": "hello",
|
||||
"int": 42,
|
||||
"float": 3.14,
|
||||
"bool": true,
|
||||
"nil_val": nil,
|
||||
}
|
||||
|
||||
result := cloneMetadata(src)
|
||||
|
||||
if len(result) != len(src) {
|
||||
t.Errorf("cloneMetadata length = %d, want %d", len(result), len(src))
|
||||
}
|
||||
|
||||
for k, v := range src {
|
||||
if result[k] != v {
|
||||
t.Errorf("cloneMetadata[%s] = %v, want %v", k, result[k], v)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify it's a copy, not the same reference
|
||||
src["new_key"] = "new_value"
|
||||
if _, exists := result["new_key"]; exists {
|
||||
t.Error("cloneMetadata should create independent copy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneMetadata_NestedMap(t *testing.T) {
|
||||
src := map[string]interface{}{
|
||||
"outer": map[string]interface{}{
|
||||
"inner": "value",
|
||||
"count": 10,
|
||||
},
|
||||
}
|
||||
|
||||
result := cloneMetadata(src)
|
||||
|
||||
// Verify structure
|
||||
outer, ok := result["outer"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("cloneMetadata nested map not preserved")
|
||||
}
|
||||
if outer["inner"] != "value" {
|
||||
t.Errorf("cloneMetadata nested value = %v, want 'value'", outer["inner"])
|
||||
}
|
||||
|
||||
// Verify it's a deep copy - modify original nested map
|
||||
srcOuter := src["outer"].(map[string]interface{})
|
||||
srcOuter["new"] = "modified"
|
||||
|
||||
// Result should not be affected
|
||||
if _, exists := outer["new"]; exists {
|
||||
t.Error("cloneMetadata should deep copy nested maps")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneMetadataValue_StringSlice(t *testing.T) {
|
||||
src := []string{"a", "b", "c"}
|
||||
result := cloneMetadataValue(src)
|
||||
|
||||
resultSlice, ok := result.([]string)
|
||||
if !ok {
|
||||
t.Fatalf("cloneMetadataValue([]string) type = %T, want []string", result)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(resultSlice, src) {
|
||||
t.Errorf("cloneMetadataValue([]string) = %v, want %v", resultSlice, src)
|
||||
}
|
||||
|
||||
// Verify independence
|
||||
src[0] = "modified"
|
||||
if resultSlice[0] == "modified" {
|
||||
t.Error("cloneMetadataValue should create independent copy of []string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneMetadataValue_IntSlice(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
result := cloneMetadataValue(src)
|
||||
|
||||
resultSlice, ok := result.([]int)
|
||||
if !ok {
|
||||
t.Fatalf("cloneMetadataValue([]int) type = %T, want []int", result)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(resultSlice, src) {
|
||||
t.Errorf("cloneMetadataValue([]int) = %v, want %v", resultSlice, src)
|
||||
}
|
||||
|
||||
// Verify independence
|
||||
src[0] = 999
|
||||
if resultSlice[0] == 999 {
|
||||
t.Error("cloneMetadataValue should create independent copy of []int")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneMetadataValue_FloatSlice(t *testing.T) {
|
||||
src := []float64{1.1, 2.2, 3.3}
|
||||
result := cloneMetadataValue(src)
|
||||
|
||||
resultSlice, ok := result.([]float64)
|
||||
if !ok {
|
||||
t.Fatalf("cloneMetadataValue([]float64) type = %T, want []float64", result)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(resultSlice, src) {
|
||||
t.Errorf("cloneMetadataValue([]float64) = %v, want %v", resultSlice, src)
|
||||
}
|
||||
|
||||
// Verify independence
|
||||
src[0] = 999.9
|
||||
if resultSlice[0] == 999.9 {
|
||||
t.Error("cloneMetadataValue should create independent copy of []float64")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneMetadataValue_InterfaceSlice(t *testing.T) {
|
||||
src := []interface{}{"a", 1, 3.14}
|
||||
result := cloneMetadataValue(src)
|
||||
|
||||
resultSlice, ok := result.([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("cloneMetadataValue([]interface{}) type = %T, want []interface{}", result)
|
||||
}
|
||||
|
||||
if len(resultSlice) != len(src) {
|
||||
t.Errorf("cloneMetadataValue([]interface{}) length = %d, want %d", len(resultSlice), len(src))
|
||||
}
|
||||
|
||||
for i, v := range src {
|
||||
if resultSlice[i] != v {
|
||||
t.Errorf("cloneMetadataValue([]interface{})[%d] = %v, want %v", i, resultSlice[i], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneMetadataValue_MapStringString(t *testing.T) {
|
||||
src := map[string]string{"key": "value", "foo": "bar"}
|
||||
result := cloneMetadataValue(src)
|
||||
|
||||
// The function converts map[string]string to map[string]interface{}
|
||||
resultMap, ok := result.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("cloneMetadataValue(map[string]string) type = %T, want map[string]interface{}", result)
|
||||
}
|
||||
|
||||
if len(resultMap) != len(src) {
|
||||
t.Errorf("cloneMetadataValue length = %d, want %d", len(resultMap), len(src))
|
||||
}
|
||||
|
||||
for k, v := range src {
|
||||
if resultMap[k] != v {
|
||||
t.Errorf("cloneMetadataValue[%s] = %v, want %v", k, resultMap[k], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneMetadataValue_Primitives(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
}{
|
||||
{"string", "hello"},
|
||||
{"int", 42},
|
||||
{"float64", 3.14},
|
||||
{"bool", true},
|
||||
{"nil", nil},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := cloneMetadataValue(tc.input)
|
||||
if result != tc.input {
|
||||
t.Errorf("cloneMetadataValue(%v) = %v, want same value", tc.input, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlert_Clone_Nil(t *testing.T) {
|
||||
var a *Alert
|
||||
result := a.Clone()
|
||||
if result != nil {
|
||||
t.Errorf("nil.Clone() = %v, want nil", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlert_Clone_BasicFields(t *testing.T) {
|
||||
now := time.Now()
|
||||
original := &Alert{
|
||||
ID: "test-alert-1",
|
||||
Type: "cpu",
|
||||
Level: AlertLevelCritical,
|
||||
ResourceID: "vm-100",
|
||||
ResourceName: "test-vm",
|
||||
Node: "node1",
|
||||
Instance: "instance1",
|
||||
Message: "CPU usage high",
|
||||
Value: 95.5,
|
||||
Threshold: 90.0,
|
||||
StartTime: now,
|
||||
LastSeen: now,
|
||||
Acknowledged: true,
|
||||
AckUser: "admin",
|
||||
}
|
||||
|
||||
clone := original.Clone()
|
||||
|
||||
if clone == original {
|
||||
t.Error("Clone() should return a different pointer")
|
||||
}
|
||||
|
||||
if clone.ID != original.ID {
|
||||
t.Errorf("Clone ID = %q, want %q", clone.ID, original.ID)
|
||||
}
|
||||
if clone.Type != original.Type {
|
||||
t.Errorf("Clone Type = %q, want %q", clone.Type, original.Type)
|
||||
}
|
||||
if clone.Level != original.Level {
|
||||
t.Errorf("Clone Level = %q, want %q", clone.Level, original.Level)
|
||||
}
|
||||
if clone.ResourceID != original.ResourceID {
|
||||
t.Errorf("Clone ResourceID = %q, want %q", clone.ResourceID, original.ResourceID)
|
||||
}
|
||||
if clone.ResourceName != original.ResourceName {
|
||||
t.Errorf("Clone ResourceName = %q, want %q", clone.ResourceName, original.ResourceName)
|
||||
}
|
||||
if clone.Node != original.Node {
|
||||
t.Errorf("Clone Node = %q, want %q", clone.Node, original.Node)
|
||||
}
|
||||
if clone.Instance != original.Instance {
|
||||
t.Errorf("Clone Instance = %q, want %q", clone.Instance, original.Instance)
|
||||
}
|
||||
if clone.Message != original.Message {
|
||||
t.Errorf("Clone Message = %q, want %q", clone.Message, original.Message)
|
||||
}
|
||||
if clone.Value != original.Value {
|
||||
t.Errorf("Clone Value = %f, want %f", clone.Value, original.Value)
|
||||
}
|
||||
if clone.Threshold != original.Threshold {
|
||||
t.Errorf("Clone Threshold = %f, want %f", clone.Threshold, original.Threshold)
|
||||
}
|
||||
if !clone.StartTime.Equal(original.StartTime) {
|
||||
t.Errorf("Clone StartTime = %v, want %v", clone.StartTime, original.StartTime)
|
||||
}
|
||||
if !clone.LastSeen.Equal(original.LastSeen) {
|
||||
t.Errorf("Clone LastSeen = %v, want %v", clone.LastSeen, original.LastSeen)
|
||||
}
|
||||
if clone.Acknowledged != original.Acknowledged {
|
||||
t.Errorf("Clone Acknowledged = %v, want %v", clone.Acknowledged, original.Acknowledged)
|
||||
}
|
||||
if clone.AckUser != original.AckUser {
|
||||
t.Errorf("Clone AckUser = %q, want %q", clone.AckUser, original.AckUser)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlert_Clone_AckTime(t *testing.T) {
|
||||
now := time.Now()
|
||||
ackTime := now.Add(-1 * time.Hour)
|
||||
original := &Alert{
|
||||
ID: "test-1",
|
||||
AckTime: &ackTime,
|
||||
}
|
||||
|
||||
clone := original.Clone()
|
||||
|
||||
if clone.AckTime == nil {
|
||||
t.Fatal("Clone AckTime should not be nil")
|
||||
}
|
||||
if clone.AckTime == original.AckTime {
|
||||
t.Error("Clone AckTime should be a different pointer")
|
||||
}
|
||||
if !clone.AckTime.Equal(*original.AckTime) {
|
||||
t.Errorf("Clone AckTime = %v, want %v", *clone.AckTime, *original.AckTime)
|
||||
}
|
||||
|
||||
// Verify independence
|
||||
*original.AckTime = time.Now().Add(24 * time.Hour)
|
||||
if clone.AckTime.Equal(*original.AckTime) {
|
||||
t.Error("Clone AckTime should be independent of original")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlert_Clone_LastNotified(t *testing.T) {
|
||||
now := time.Now()
|
||||
notified := now.Add(-30 * time.Minute)
|
||||
original := &Alert{
|
||||
ID: "test-1",
|
||||
LastNotified: ¬ified,
|
||||
}
|
||||
|
||||
clone := original.Clone()
|
||||
|
||||
if clone.LastNotified == nil {
|
||||
t.Fatal("Clone LastNotified should not be nil")
|
||||
}
|
||||
if clone.LastNotified == original.LastNotified {
|
||||
t.Error("Clone LastNotified should be a different pointer")
|
||||
}
|
||||
if !clone.LastNotified.Equal(*original.LastNotified) {
|
||||
t.Errorf("Clone LastNotified = %v, want %v", *clone.LastNotified, *original.LastNotified)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlert_Clone_EscalationTimes(t *testing.T) {
|
||||
now := time.Now()
|
||||
original := &Alert{
|
||||
ID: "test-1",
|
||||
LastEscalation: 2,
|
||||
EscalationTimes: []time.Time{now.Add(-2 * time.Hour), now.Add(-1 * time.Hour)},
|
||||
}
|
||||
|
||||
clone := original.Clone()
|
||||
|
||||
if clone.LastEscalation != original.LastEscalation {
|
||||
t.Errorf("Clone LastEscalation = %d, want %d", clone.LastEscalation, original.LastEscalation)
|
||||
}
|
||||
if len(clone.EscalationTimes) != len(original.EscalationTimes) {
|
||||
t.Fatalf("Clone EscalationTimes length = %d, want %d", len(clone.EscalationTimes), len(original.EscalationTimes))
|
||||
}
|
||||
for i, et := range original.EscalationTimes {
|
||||
if !clone.EscalationTimes[i].Equal(et) {
|
||||
t.Errorf("Clone EscalationTimes[%d] = %v, want %v", i, clone.EscalationTimes[i], et)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify slice independence
|
||||
original.EscalationTimes[0] = time.Now()
|
||||
if clone.EscalationTimes[0].Equal(original.EscalationTimes[0]) {
|
||||
t.Error("Clone EscalationTimes should be independent of original")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlert_Clone_Metadata(t *testing.T) {
|
||||
original := &Alert{
|
||||
ID: "test-1",
|
||||
Metadata: map[string]interface{}{
|
||||
"key": "value",
|
||||
"nested": map[string]interface{}{"inner": "data"},
|
||||
},
|
||||
}
|
||||
|
||||
clone := original.Clone()
|
||||
|
||||
if clone.Metadata == nil {
|
||||
t.Fatal("Clone Metadata should not be nil")
|
||||
}
|
||||
if clone.Metadata["key"] != "value" {
|
||||
t.Errorf("Clone Metadata[key] = %v, want 'value'", clone.Metadata["key"])
|
||||
}
|
||||
|
||||
// Verify deep copy
|
||||
original.Metadata["key"] = "modified"
|
||||
if clone.Metadata["key"] == "modified" {
|
||||
t.Error("Clone Metadata should be a deep copy")
|
||||
}
|
||||
|
||||
// Verify nested map independence
|
||||
originalNested := original.Metadata["nested"].(map[string]interface{})
|
||||
originalNested["inner"] = "changed"
|
||||
|
||||
cloneNested := clone.Metadata["nested"].(map[string]interface{})
|
||||
if cloneNested["inner"] == "changed" {
|
||||
t.Error("Clone Metadata nested map should be independent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlert_Clone_NilOptionalFields(t *testing.T) {
|
||||
original := &Alert{
|
||||
ID: "test-1",
|
||||
AckTime: nil,
|
||||
LastNotified: nil,
|
||||
EscalationTimes: nil,
|
||||
Metadata: nil,
|
||||
}
|
||||
|
||||
clone := original.Clone()
|
||||
|
||||
if clone.AckTime != nil {
|
||||
t.Error("Clone AckTime should be nil when original is nil")
|
||||
}
|
||||
if clone.LastNotified != nil {
|
||||
t.Error("Clone LastNotified should be nil when original is nil")
|
||||
}
|
||||
if clone.EscalationTimes != nil {
|
||||
t.Error("Clone EscalationTimes should be nil when original is nil")
|
||||
}
|
||||
if clone.Metadata != nil {
|
||||
t.Error("Clone Metadata should be nil when original is nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlert_Clone_EmptyEscalationTimes(t *testing.T) {
|
||||
original := &Alert{
|
||||
ID: "test-1",
|
||||
EscalationTimes: []time.Time{},
|
||||
}
|
||||
|
||||
clone := original.Clone()
|
||||
|
||||
// Empty slice should remain nil after clone (len == 0 check)
|
||||
if len(clone.EscalationTimes) != 0 {
|
||||
t.Errorf("Clone EscalationTimes length = %d, want 0", len(clone.EscalationTimes))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue