package main import ( "net/http" "net/http/httptest" "testing" "time" ) // TestHTTPClientTimeout verifies that the HTTP client has a timeout configured func TestHTTPClientTimeout(t *testing.T) { if httpClient.Timeout == 0 { t.Error("HTTP client timeout is not configured") } expectedTimeout := 30 * time.Second if httpClient.Timeout != expectedTimeout { t.Errorf("HTTP client timeout = %v, want %v", httpClient.Timeout, expectedTimeout) } } // TestHTTPClientTimeoutActuallyWorks verifies the timeout actually prevents indefinite hangs func TestHTTPClientTimeoutActuallyWorks(t *testing.T) { // Create a server that delays response longer than timeout server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(35 * time.Second) // Sleep longer than our 30s timeout w.WriteHeader(http.StatusOK) })) defer server.Close() start := time.Now() _, err := httpClient.Get(server.URL) duration := time.Since(start) if err == nil { t.Error("Expected timeout error, got nil") } // Should timeout in ~30 seconds, give 3s buffer for slow systems if duration < 28*time.Second || duration > 33*time.Second { t.Logf("Request took %v (expected ~30s)", duration) } } // TestCooldownCacheBasic verifies basic cooldown functionality func TestCooldownCacheBasic(t *testing.T) { cacheMux.Lock() cooldownCache = make(map[string]time.Time) // Reset cacheMux.Unlock() ip := "192.0.2.1" // First check - should be allowed if isInCooldown(ip, 10) { t.Error("IP should not be in cooldown on first check") } // Add to cache cacheMux.Lock() cooldownCache[ip] = time.Now() cacheMux.Unlock() // Second check - should be in cooldown if !isInCooldown(ip, 10) { t.Error("IP should be in cooldown after being added") } // Wait for cooldown to expire cacheMux.Lock() cooldownCache[ip] = time.Now().Add(-11 * time.Minute) cacheMux.Unlock() // Third check - should be allowed again if isInCooldown(ip, 10) { t.Error("IP should not be in cooldown after expiry") } } // TestCooldownCacheConcurrency verifies thread-safe cache access func TestCooldownCacheConcurrency(t *testing.T) { cacheMux.Lock() cooldownCache = make(map[string]time.Time) cacheMux.Unlock() done := make(chan bool) // Spawn multiple goroutines accessing cache concurrently for i := 0; i < 10; i++ { go func(id int) { for j := 0; j < 100; j++ { ip := "192.0.2." + string(rune(id)) isInCooldown(ip, 10) cacheMux.Lock() cooldownCache[ip] = time.Now() cacheMux.Unlock() } done <- true }(i) } // Wait for all goroutines for i := 0; i < 10; i++ { <-done } // If we got here without a race condition, test passes } // Helper function from ping_service.go func isInCooldown(ip string, cooldownMinutes int) bool { cacheMux.Lock() defer cacheMux.Unlock() lastPing, exists := cooldownCache[ip] if !exists { return false } elapsed := time.Since(lastPing) cooldownDuration := time.Duration(cooldownMinutes) * time.Minute return elapsed < cooldownDuration } // TestConfigParsing verifies config file parsing works correctly func TestConfigDefaults(t *testing.T) { config := Config{ IntervalSeconds: 30, CooldownMinutes: 10, EnableTraceroute: true, TracerouteMaxHops: 30, HealthCheckPort: 8090, } if config.IntervalSeconds <= 0 { t.Error("IntervalSeconds should be positive") } if config.CooldownMinutes <= 0 { t.Error("CooldownMinutes should be positive") } if config.TracerouteMaxHops <= 0 || config.TracerouteMaxHops > 255 { t.Error("TracerouteMaxHops should be between 1 and 255") } if config.HealthCheckPort <= 0 || config.HealthCheckPort > 65535 { t.Error("HealthCheckPort should be between 1 and 65535") } }