test: Add tests for polling interval, proxy success handlers, and failed instance removal
- effectivePVEPollingInterval: 80%→100% (6 cases for nil/clamping) - handleProxySuccess: 80%→100% (3 cases for nil client, reset) - handleProxyHostSuccess: →100% (6 cases for empty/whitespace/removal) - removeFailedPBSNode: 75%→100% (5 cases for removal, backups, health) - removeFailedPMGInstance: 75%→100% (5 cases for removal, backups, health)
This commit is contained in:
parent
acb7f4cf3a
commit
2d775e9409
2 changed files with 423 additions and 0 deletions
|
|
@ -1067,6 +1067,70 @@ func TestExtractSnapshotName(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEffectivePVEPollingInterval(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
monitor *Monitor
|
||||||
|
expected time.Duration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil monitor returns minInterval",
|
||||||
|
monitor: nil,
|
||||||
|
expected: 10 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil config returns minInterval",
|
||||||
|
monitor: &Monitor{config: nil},
|
||||||
|
expected: 10 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero PVEPollingInterval returns minInterval",
|
||||||
|
monitor: &Monitor{
|
||||||
|
config: &config.Config{
|
||||||
|
PVEPollingInterval: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: 10 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid interval within range",
|
||||||
|
monitor: &Monitor{
|
||||||
|
config: &config.Config{
|
||||||
|
PVEPollingInterval: 30 * time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: 30 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "interval below minInterval clamped to 10s",
|
||||||
|
monitor: &Monitor{
|
||||||
|
config: &config.Config{
|
||||||
|
PVEPollingInterval: 5 * time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: 10 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "interval above maxInterval clamped to 1h",
|
||||||
|
monitor: &Monitor{
|
||||||
|
config: &config.Config{
|
||||||
|
PVEPollingInterval: 2 * time.Hour,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: time.Hour,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := tt.monitor.effectivePVEPollingInterval()
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("expected %v, got %v", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestClampUint64ToInt64(t *testing.T) {
|
func TestClampUint64ToInt64(t *testing.T) {
|
||||||
t.Run("zero value returns 0", func(t *testing.T) {
|
t.Run("zero value returns 0", func(t *testing.T) {
|
||||||
result := clampUint64ToInt64(0)
|
result := clampUint64ToInt64(0)
|
||||||
|
|
@ -1103,3 +1167,213 @@ func TestClampUint64ToInt64(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRemoveFailedPBSNode(t *testing.T) {
|
||||||
|
t.Run("removes correct instance from PBSInstances", func(t *testing.T) {
|
||||||
|
state := models.NewState()
|
||||||
|
state.UpdatePBSInstances([]models.PBSInstance{
|
||||||
|
{Name: "pbs1", Host: "192.168.1.1"},
|
||||||
|
{Name: "pbs2", Host: "192.168.1.2"},
|
||||||
|
{Name: "pbs3", Host: "192.168.1.3"},
|
||||||
|
})
|
||||||
|
|
||||||
|
m := &Monitor{state: state}
|
||||||
|
m.removeFailedPBSNode("pbs2")
|
||||||
|
|
||||||
|
if len(state.PBSInstances) != 2 {
|
||||||
|
t.Fatalf("expected 2 instances, got %d", len(state.PBSInstances))
|
||||||
|
}
|
||||||
|
for _, inst := range state.PBSInstances {
|
||||||
|
if inst.Name == "pbs2" {
|
||||||
|
t.Error("expected pbs2 to be removed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Verify other instances remain
|
||||||
|
names := make(map[string]bool)
|
||||||
|
for _, inst := range state.PBSInstances {
|
||||||
|
names[inst.Name] = true
|
||||||
|
}
|
||||||
|
if !names["pbs1"] || !names["pbs3"] {
|
||||||
|
t.Errorf("expected pbs1 and pbs3 to remain, got %v", names)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("clears PBS backups for that instance", func(t *testing.T) {
|
||||||
|
state := models.NewState()
|
||||||
|
state.UpdatePBSBackups("pbs1", []models.PBSBackup{
|
||||||
|
{ID: "backup1", Instance: "pbs1"},
|
||||||
|
{ID: "backup2", Instance: "pbs1"},
|
||||||
|
})
|
||||||
|
state.UpdatePBSBackups("pbs2", []models.PBSBackup{
|
||||||
|
{ID: "backup3", Instance: "pbs2"},
|
||||||
|
})
|
||||||
|
|
||||||
|
m := &Monitor{state: state}
|
||||||
|
m.removeFailedPBSNode("pbs1")
|
||||||
|
|
||||||
|
// Verify pbs1 backups are gone
|
||||||
|
for _, backup := range state.PBSBackups {
|
||||||
|
if backup.Instance == "pbs1" {
|
||||||
|
t.Errorf("expected pbs1 backups to be cleared, found %s", backup.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Verify pbs2 backups remain
|
||||||
|
found := false
|
||||||
|
for _, backup := range state.PBSBackups {
|
||||||
|
if backup.Instance == "pbs2" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("expected pbs2 backups to remain")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sets connection health to false", func(t *testing.T) {
|
||||||
|
state := models.NewState()
|
||||||
|
state.SetConnectionHealth("pbs-pbs1", true)
|
||||||
|
state.SetConnectionHealth("pbs-pbs2", true)
|
||||||
|
|
||||||
|
m := &Monitor{state: state}
|
||||||
|
m.removeFailedPBSNode("pbs1")
|
||||||
|
|
||||||
|
if state.ConnectionHealth["pbs-pbs1"] != false {
|
||||||
|
t.Error("expected pbs-pbs1 connection health to be false")
|
||||||
|
}
|
||||||
|
if state.ConnectionHealth["pbs-pbs2"] != true {
|
||||||
|
t.Error("expected pbs-pbs2 connection health to remain true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles empty instances list", func(t *testing.T) {
|
||||||
|
state := models.NewState()
|
||||||
|
m := &Monitor{state: state}
|
||||||
|
|
||||||
|
// Should not panic
|
||||||
|
m.removeFailedPBSNode("nonexistent")
|
||||||
|
|
||||||
|
if len(state.PBSInstances) != 0 {
|
||||||
|
t.Errorf("expected empty instances, got %d", len(state.PBSInstances))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles instance not found", func(t *testing.T) {
|
||||||
|
state := models.NewState()
|
||||||
|
state.UpdatePBSInstances([]models.PBSInstance{
|
||||||
|
{Name: "pbs1", Host: "192.168.1.1"},
|
||||||
|
})
|
||||||
|
|
||||||
|
m := &Monitor{state: state}
|
||||||
|
m.removeFailedPBSNode("nonexistent")
|
||||||
|
|
||||||
|
if len(state.PBSInstances) != 1 {
|
||||||
|
t.Errorf("expected 1 instance to remain, got %d", len(state.PBSInstances))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveFailedPMGInstance(t *testing.T) {
|
||||||
|
t.Run("removes correct instance from PMGInstances", func(t *testing.T) {
|
||||||
|
state := models.NewState()
|
||||||
|
state.UpdatePMGInstances([]models.PMGInstance{
|
||||||
|
{Name: "pmg1", Host: "192.168.1.1"},
|
||||||
|
{Name: "pmg2", Host: "192.168.1.2"},
|
||||||
|
{Name: "pmg3", Host: "192.168.1.3"},
|
||||||
|
})
|
||||||
|
|
||||||
|
m := &Monitor{state: state}
|
||||||
|
m.removeFailedPMGInstance("pmg2")
|
||||||
|
|
||||||
|
if len(state.PMGInstances) != 2 {
|
||||||
|
t.Fatalf("expected 2 instances, got %d", len(state.PMGInstances))
|
||||||
|
}
|
||||||
|
for _, inst := range state.PMGInstances {
|
||||||
|
if inst.Name == "pmg2" {
|
||||||
|
t.Error("expected pmg2 to be removed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Verify other instances remain
|
||||||
|
names := make(map[string]bool)
|
||||||
|
for _, inst := range state.PMGInstances {
|
||||||
|
names[inst.Name] = true
|
||||||
|
}
|
||||||
|
if !names["pmg1"] || !names["pmg3"] {
|
||||||
|
t.Errorf("expected pmg1 and pmg3 to remain, got %v", names)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("clears PMG backups for that instance", func(t *testing.T) {
|
||||||
|
state := models.NewState()
|
||||||
|
state.UpdatePMGBackups("pmg1", []models.PMGBackup{
|
||||||
|
{ID: "backup1", Instance: "pmg1"},
|
||||||
|
{ID: "backup2", Instance: "pmg1"},
|
||||||
|
})
|
||||||
|
state.UpdatePMGBackups("pmg2", []models.PMGBackup{
|
||||||
|
{ID: "backup3", Instance: "pmg2"},
|
||||||
|
})
|
||||||
|
|
||||||
|
m := &Monitor{state: state}
|
||||||
|
m.removeFailedPMGInstance("pmg1")
|
||||||
|
|
||||||
|
// Verify pmg1 backups are gone
|
||||||
|
for _, backup := range state.PMGBackups {
|
||||||
|
if backup.Instance == "pmg1" {
|
||||||
|
t.Errorf("expected pmg1 backups to be cleared, found %s", backup.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Verify pmg2 backups remain
|
||||||
|
found := false
|
||||||
|
for _, backup := range state.PMGBackups {
|
||||||
|
if backup.Instance == "pmg2" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("expected pmg2 backups to remain")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sets connection health to false", func(t *testing.T) {
|
||||||
|
state := models.NewState()
|
||||||
|
state.SetConnectionHealth("pmg-pmg1", true)
|
||||||
|
state.SetConnectionHealth("pmg-pmg2", true)
|
||||||
|
|
||||||
|
m := &Monitor{state: state}
|
||||||
|
m.removeFailedPMGInstance("pmg1")
|
||||||
|
|
||||||
|
if state.ConnectionHealth["pmg-pmg1"] != false {
|
||||||
|
t.Error("expected pmg-pmg1 connection health to be false")
|
||||||
|
}
|
||||||
|
if state.ConnectionHealth["pmg-pmg2"] != true {
|
||||||
|
t.Error("expected pmg-pmg2 connection health to remain true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles empty instances list", func(t *testing.T) {
|
||||||
|
state := models.NewState()
|
||||||
|
m := &Monitor{state: state}
|
||||||
|
|
||||||
|
// Should not panic
|
||||||
|
m.removeFailedPMGInstance("nonexistent")
|
||||||
|
|
||||||
|
if len(state.PMGInstances) != 0 {
|
||||||
|
t.Errorf("expected empty instances, got %d", len(state.PMGInstances))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles instance not found", func(t *testing.T) {
|
||||||
|
state := models.NewState()
|
||||||
|
state.UpdatePMGInstances([]models.PMGInstance{
|
||||||
|
{Name: "pmg1", Host: "192.168.1.1"},
|
||||||
|
})
|
||||||
|
|
||||||
|
m := &Monitor{state: state}
|
||||||
|
m.removeFailedPMGInstance("nonexistent")
|
||||||
|
|
||||||
|
if len(state.PMGInstances) != 1 {
|
||||||
|
t.Errorf("expected 1 instance to remain, got %d", len(state.PMGInstances))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1486,6 +1486,155 @@ func TestShouldSkipProxyHost_TrimsWhitespace(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests for handleProxySuccess
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
func TestHandleProxySuccess_NilProxyClient(t *testing.T) {
|
||||||
|
tc := &TemperatureCollector{
|
||||||
|
proxyClient: nil,
|
||||||
|
proxyFailures: 5, // should remain unchanged
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.handleProxySuccess()
|
||||||
|
|
||||||
|
if tc.proxyFailures != 5 {
|
||||||
|
t.Errorf("expected proxyFailures to remain 5 when proxyClient is nil, got %d", tc.proxyFailures)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleProxySuccess_ResetsFailures(t *testing.T) {
|
||||||
|
stub := &stubTemperatureProxy{}
|
||||||
|
|
||||||
|
tc := &TemperatureCollector{
|
||||||
|
proxyClient: stub,
|
||||||
|
proxyFailures: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.handleProxySuccess()
|
||||||
|
|
||||||
|
if tc.proxyFailures != 0 {
|
||||||
|
t.Errorf("expected proxyFailures to be reset to 0, got %d", tc.proxyFailures)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleProxySuccess_AlreadyZeroFailures(t *testing.T) {
|
||||||
|
stub := &stubTemperatureProxy{}
|
||||||
|
|
||||||
|
tc := &TemperatureCollector{
|
||||||
|
proxyClient: stub,
|
||||||
|
proxyFailures: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.handleProxySuccess()
|
||||||
|
|
||||||
|
if tc.proxyFailures != 0 {
|
||||||
|
t.Errorf("expected proxyFailures to remain 0, got %d", tc.proxyFailures)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests for handleProxyHostSuccess
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
func TestHandleProxyHostSuccess_EmptyHost(t *testing.T) {
|
||||||
|
tc := &TemperatureCollector{
|
||||||
|
proxyHostStates: map[string]*proxyHostState{
|
||||||
|
"192.168.1.100": {failures: 2, cooldownUntil: time.Now().Add(time.Minute)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.handleProxyHostSuccess("")
|
||||||
|
|
||||||
|
// Map should be unchanged
|
||||||
|
tc.proxyMu.Lock()
|
||||||
|
if len(tc.proxyHostStates) != 1 {
|
||||||
|
t.Errorf("expected map to have 1 entry, got %d", len(tc.proxyHostStates))
|
||||||
|
}
|
||||||
|
tc.proxyMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleProxyHostSuccess_WhitespaceOnlyHost(t *testing.T) {
|
||||||
|
tc := &TemperatureCollector{
|
||||||
|
proxyHostStates: map[string]*proxyHostState{
|
||||||
|
"192.168.1.100": {failures: 2, cooldownUntil: time.Now().Add(time.Minute)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.handleProxyHostSuccess(" ")
|
||||||
|
|
||||||
|
// Map should be unchanged
|
||||||
|
tc.proxyMu.Lock()
|
||||||
|
if len(tc.proxyHostStates) != 1 {
|
||||||
|
t.Errorf("expected map to have 1 entry, got %d", len(tc.proxyHostStates))
|
||||||
|
}
|
||||||
|
tc.proxyMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleProxyHostSuccess_RemovesHostFromMap(t *testing.T) {
|
||||||
|
tc := &TemperatureCollector{
|
||||||
|
proxyHostStates: map[string]*proxyHostState{
|
||||||
|
"192.168.1.100": {failures: 5, cooldownUntil: time.Now().Add(time.Minute)},
|
||||||
|
"192.168.1.101": {failures: 3, cooldownUntil: time.Now().Add(time.Minute)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.handleProxyHostSuccess("192.168.1.100")
|
||||||
|
|
||||||
|
tc.proxyMu.Lock()
|
||||||
|
defer tc.proxyMu.Unlock()
|
||||||
|
|
||||||
|
if _, exists := tc.proxyHostStates["192.168.1.100"]; exists {
|
||||||
|
t.Error("expected host 192.168.1.100 to be removed from map")
|
||||||
|
}
|
||||||
|
if _, exists := tc.proxyHostStates["192.168.1.101"]; !exists {
|
||||||
|
t.Error("expected host 192.168.1.101 to remain in map")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleProxyHostSuccess_HostNotInMap(t *testing.T) {
|
||||||
|
tc := &TemperatureCollector{
|
||||||
|
proxyHostStates: map[string]*proxyHostState{
|
||||||
|
"192.168.1.100": {failures: 2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not panic when host doesn't exist
|
||||||
|
tc.handleProxyHostSuccess("192.168.1.200")
|
||||||
|
|
||||||
|
tc.proxyMu.Lock()
|
||||||
|
if len(tc.proxyHostStates) != 1 {
|
||||||
|
t.Errorf("expected map to have 1 entry, got %d", len(tc.proxyHostStates))
|
||||||
|
}
|
||||||
|
tc.proxyMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleProxyHostSuccess_TrimsWhitespace(t *testing.T) {
|
||||||
|
tc := &TemperatureCollector{
|
||||||
|
proxyHostStates: map[string]*proxyHostState{
|
||||||
|
"192.168.1.100": {failures: 5, cooldownUntil: time.Now().Add(time.Minute)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.handleProxyHostSuccess(" 192.168.1.100 ")
|
||||||
|
|
||||||
|
tc.proxyMu.Lock()
|
||||||
|
defer tc.proxyMu.Unlock()
|
||||||
|
|
||||||
|
if _, exists := tc.proxyHostStates["192.168.1.100"]; exists {
|
||||||
|
t.Error("expected host to be removed after trimming whitespace from input")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleProxyHostSuccess_NilMap(t *testing.T) {
|
||||||
|
tc := &TemperatureCollector{
|
||||||
|
proxyHostStates: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not panic with nil map
|
||||||
|
tc.handleProxyHostSuccess("192.168.1.100")
|
||||||
|
}
|
||||||
|
|
||||||
// Helper functions for test setup
|
// Helper functions for test setup
|
||||||
|
|
||||||
func intPtr(i int) *int {
|
func intPtr(i int) *int {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue