diff --git a/internal/monitoring/helpers_test.go b/internal/monitoring/helpers_test.go index 95c8859..0ae09d6 100644 --- a/internal/monitoring/helpers_test.go +++ b/internal/monitoring/helpers_test.go @@ -17,20 +17,49 @@ func TestNormalizeEndpointHost(t *testing.T) { t.Parallel() cases := []struct { + name string input string want string }{ - {"", ""}, - {" node.local ", "node.local"}, - {"https://node.local:8006/", "node.local"}, - {"node.local:8006", "node.local"}, - {"node.local/path", "node.local"}, - {"https://[2001:db8::1]:8006", "2001:db8::1"}, + // Empty and whitespace + {"empty string", "", ""}, + {"whitespace only", " ", ""}, + {"whitespace with tabs", " \t ", ""}, + + // Full URLs with scheme + {"https URL with port and path", "https://example.com:8006/api", "example.com"}, + {"http URL with path", "http://host/path", "host"}, + {"https URL with trailing slash", "https://node.local:8006/", "node.local"}, + + // URLs without scheme + {"host with port", "example.com:8006", "example.com"}, + {"host with port no scheme", "node.local:8006", "node.local"}, + + // Hostname only + {"hostname only", "example.com", "example.com"}, + {"simple hostname", "node.local", "node.local"}, + + // IP addresses + {"IPv4 with port", "192.168.1.1:8006", "192.168.1.1"}, + {"IPv4 only", "192.168.1.100", "192.168.1.100"}, + {"IPv6 bracketed with port", "https://[2001:db8::1]:8006", "2001:db8::1"}, + + // Host with path (no scheme) + {"host with path no scheme", "node.local/path", "node.local"}, + {"host with deep path", "server.example.com/api/v1/resource", "server.example.com"}, + + // Edge cases with just scheme prefix + {"just https prefix", "https://", ""}, + {"just http prefix", "http://", ""}, + + // Whitespace trimming + {"whitespace around hostname", " node.local ", "node.local"}, + {"whitespace around URL", " https://example.com:8006 ", "example.com"}, } for _, tc := range cases { tc := tc - t.Run(tc.input, func(t *testing.T) { + t.Run(tc.name, func(t *testing.T) { t.Parallel() if got := normalizeEndpointHost(tc.input); got != tc.want { t.Fatalf("normalizeEndpointHost(%q) = %q, want %q", tc.input, got, tc.want) diff --git a/internal/monitoring/metrics_test.go b/internal/monitoring/metrics_test.go index 6ef319b..fc15bb9 100644 --- a/internal/monitoring/metrics_test.go +++ b/internal/monitoring/metrics_test.go @@ -1094,3 +1094,104 @@ func TestDecInFlight_DecrementsGauge(t *testing.T) { t.Errorf("inflight{pve} = %v, want 0 after full decrement", got) } } + +func TestDecrementPending_NilPollMetrics(t *testing.T) { + t.Parallel() + + var pm *PollMetrics + // Should not panic + pm.decrementPending() +} + +func TestDecrementPending_DecrementsWhenPositive(t *testing.T) { + t.Parallel() + + pm := newFullTestPollMetrics(t) + + // Set initial pending count + pm.ResetQueueDepth(5) + + pm.decrementPending() + + pm.mu.RLock() + gotPending := pm.pending + pm.mu.RUnlock() + + if gotPending != 4 { + t.Errorf("pending = %v, want 4 after decrement from 5", gotPending) + } +} + +func TestDecrementPending_DoesNotGoBelowZero(t *testing.T) { + t.Parallel() + + pm := newFullTestPollMetrics(t) + + // Start at 0 (default) + pm.decrementPending() + + pm.mu.RLock() + gotPending := pm.pending + pm.mu.RUnlock() + + if gotPending != 0 { + t.Errorf("pending = %v, want 0 (should not go negative)", gotPending) + } + + // Also verify the gauge is 0 + gotQueueDepth := getGaugeValue(pm.queueDepth) + if gotQueueDepth != 0 { + t.Errorf("queueDepth gauge = %v, want 0", gotQueueDepth) + } +} + +func TestDecrementPending_UpdatesQueueDepthGauge(t *testing.T) { + t.Parallel() + + pm := newFullTestPollMetrics(t) + + pm.ResetQueueDepth(10) + pm.decrementPending() + + gotQueueDepth := getGaugeValue(pm.queueDepth) + if gotQueueDepth != 9 { + t.Errorf("queueDepth gauge = %v, want 9", gotQueueDepth) + } +} + +func TestDecrementPending_MultipleDecrements(t *testing.T) { + t.Parallel() + + pm := newFullTestPollMetrics(t) + + pm.ResetQueueDepth(5) + + // Decrement 5 times + for i := 0; i < 5; i++ { + pm.decrementPending() + } + + pm.mu.RLock() + gotPending := pm.pending + pm.mu.RUnlock() + + if gotPending != 0 { + t.Errorf("pending = %v, want 0 after 5 decrements from 5", gotPending) + } + + gotQueueDepth := getGaugeValue(pm.queueDepth) + if gotQueueDepth != 0 { + t.Errorf("queueDepth gauge = %v, want 0", gotQueueDepth) + } + + // Decrement one more time - should stay at 0 + pm.decrementPending() + + pm.mu.RLock() + gotPending = pm.pending + pm.mu.RUnlock() + + if gotPending != 0 { + t.Errorf("pending = %v, want 0 after extra decrement", gotPending) + } +} diff --git a/internal/monitoring/monitor_timeout_test.go b/internal/monitoring/monitor_timeout_test.go index bae5a63..c516925 100644 --- a/internal/monitoring/monitor_timeout_test.go +++ b/internal/monitoring/monitor_timeout_test.go @@ -65,4 +65,51 @@ func TestTaskExecutionTimeout(t *testing.T) { t.Fatalf("taskExecutionTimeout() = %v, want %v", got, defaultTaskTimeout) } }) + + t.Run("nil Monitor returns defaultTaskTimeout", func(t *testing.T) { + var m *Monitor + got := m.taskExecutionTimeout(InstanceTypePVE) + if got != defaultTaskTimeout { + t.Fatalf("taskExecutionTimeout() = %v, want %v", got, defaultTaskTimeout) + } + }) + + t.Run("zero pollTimeout returns defaultTaskTimeout", func(t *testing.T) { + m := &Monitor{pollTimeout: 0} + got := m.taskExecutionTimeout(InstanceTypePVE) + if got != defaultTaskTimeout { + t.Fatalf("taskExecutionTimeout() = %v, want %v", got, defaultTaskTimeout) + } + }) + + t.Run("negative pollTimeout returns defaultTaskTimeout", func(t *testing.T) { + m := &Monitor{pollTimeout: -5 * time.Second} + got := m.taskExecutionTimeout(InstanceTypePVE) + if got != defaultTaskTimeout { + t.Fatalf("taskExecutionTimeout() = %v, want %v", got, defaultTaskTimeout) + } + }) + + t.Run("InstanceType parameter is ignored", func(t *testing.T) { + m := &Monitor{pollTimeout: 60 * time.Second} + // All instance types should return the same value + gotPVE := m.taskExecutionTimeout(InstanceTypePVE) + gotPBS := m.taskExecutionTimeout(InstanceTypePBS) + gotPMG := m.taskExecutionTimeout(InstanceTypePMG) + gotUnknown := m.taskExecutionTimeout(InstanceType("unknown")) + + expected := 60 * time.Second + if gotPVE != expected { + t.Errorf("taskExecutionTimeout(PVE) = %v, want %v", gotPVE, expected) + } + if gotPBS != expected { + t.Errorf("taskExecutionTimeout(PBS) = %v, want %v", gotPBS, expected) + } + if gotPMG != expected { + t.Errorf("taskExecutionTimeout(PMG) = %v, want %v", gotPMG, expected) + } + if gotUnknown != expected { + t.Errorf("taskExecutionTimeout(unknown) = %v, want %v", gotUnknown, expected) + } + }) }