package upstream import ( "testing" ) // TestParseProxyTargetsWithConsulResolver tests parsing of nginx config with consul DNS resolver func TestParseProxyTargetsWithConsulResolver(t *testing.T) { config := `upstream redacted-net { zone upstream_web 128k; resolver 127.0.0.1:8600 valid=5s; resolver_timeout 2s; server service.consul service=redacted-net resolve; } server { listen 80; listen [::]:80; server_name redacted.net; location / { proxy_pass http://redacted-net; } }` targets := ParseProxyTargetsFromRawContent(config) // Print actual results for debugging t.Logf("Found %d targets:", len(targets)) for i, target := range targets { t.Logf("Target %d: Host=%s, Port=%s, Type=%s, Resolver=%s, IsConsul=%v", i+1, target.Host, target.Port, target.Type, target.Resolver, target.IsConsul) } // Expected behavior: // - Should parse "service.consul" as host with dynamic port // - Should identify this as an upstream target with consul service discovery // - Should capture resolver information // - proxy_pass http://redacted-net should be ignored since it references upstream expectedTargets := []ProxyTarget{ { Host: "service.consul", Port: "dynamic", Type: "upstream", Resolver: "127.0.0.1:8600", IsConsul: true, ServiceURL: "service.consul service=redacted-net resolve", }, } if len(targets) != len(expectedTargets) { t.Errorf("Expected %d targets, got %d", len(expectedTargets), len(targets)) return } // Create a map for easier comparison targetMap := make(map[string]ProxyTarget) for _, target := range targets { key := target.Host + ":" + target.Port + ":" + target.Type + ":" + target.Resolver if target.IsConsul { key += ":consul:" + target.ServiceURL } targetMap[key] = target } for _, expected := range expectedTargets { key := expected.Host + ":" + expected.Port + ":" + expected.Type + ":" + expected.Resolver if expected.IsConsul { key += ":consul:" + expected.ServiceURL } if _, found := targetMap[key]; !found { t.Errorf("Expected target not found: %+v", expected) } } } // TestConsulResolverExtractServiceName tests service name extraction func TestConsulResolverExtractServiceName(t *testing.T) { resolver := NewConsulResolver("127.0.0.1:8600") tests := []struct { serviceURL string expectedName string }{ { serviceURL: "service.consul service=redacted-net resolve", expectedName: "redacted-net", }, { serviceURL: "service.consul service=web-service resolve", expectedName: "web-service", }, { serviceURL: "service.consul service=api-backend resolve", expectedName: "api-backend", }, { serviceURL: "my-service.service.consul", expectedName: "my-service", }, { serviceURL: "invalid-format", expectedName: "", }, } for _, test := range tests { result := resolver.extractServiceName(test.serviceURL) if result != test.expectedName { t.Errorf("extractServiceName(%q) = %q, expected %q", test.serviceURL, result, test.expectedName) } } } // TestConsulResolverResolveService tests the actual resolution functionality func TestConsulResolverResolveService(t *testing.T) { // Test with a mock resolver that should fail (127.0.0.1:8600 is consul default but likely not running) t.Run("ResolutionWithMockConsul", func(t *testing.T) { resolver := NewConsulResolver("127.0.0.1:8600") addresses, err := resolver.ResolveService("service.consul service=test-service resolve") // We expect this to fail since there's no real consul server if err == nil { t.Logf("Unexpected success: resolved addresses %v (maybe there's a real consul server?)", addresses) } else { t.Logf("Expected failure: %v", err) } }) // Test with invalid service URL t.Run("InvalidServiceURL", func(t *testing.T) { resolver := NewConsulResolver("127.0.0.1:8600") addresses, err := resolver.ResolveService("invalid-service-url") if err == nil { t.Errorf("Expected error for invalid service URL, got addresses: %v", addresses) } if len(addresses) != 0 { t.Errorf("Expected no addresses for invalid service URL, got %v", addresses) } }) // Test with invalid resolver address t.Run("InvalidResolverAddress", func(t *testing.T) { resolver := NewConsulResolver("192.168.254.254:8600") // Unreachable IP addresses, err := resolver.ResolveService("service.consul service=test-service resolve") // Should fail due to unreachable resolver if err == nil { t.Errorf("Expected error for unreachable resolver, got addresses: %v", addresses) } }) // Test service name extraction edge cases t.Run("ServiceNameExtractionEdgeCases", func(t *testing.T) { resolver := NewConsulResolver("127.0.0.1:8600") testCases := []struct { serviceURL string expectedName string }{ {"", ""}, {"service.consul", ""}, {"service.consul resolve", ""}, {"service.consul service= resolve", ""}, {"service.consul service= resolve", ""}, // Empty service name {"my-service.service.consul", "my-service"}, {"complex-service-name.service.consul", "complex-service-name"}, } for _, tc := range testCases { result := resolver.extractServiceName(tc.serviceURL) if result != tc.expectedName { t.Errorf("extractServiceName(%q) = %q, expected %q", tc.serviceURL, result, tc.expectedName) } } }) } // TestTestConsulTargets tests the new dedicated consul testing function func TestTestConsulTargets(t *testing.T) { // Test 1: Valid consul targets with resolver t.Run("ValidConsulTargets", func(t *testing.T) { consulTargets := []ProxyTarget{ { Host: "service.consul", Port: "dynamic", Type: "upstream", Resolver: "127.0.0.1:8600", IsConsul: true, ServiceURL: "service.consul service=test-service resolve", }, } results := TestConsulTargets(consulTargets) // Should have exactly 1 result if len(results) != 1 { t.Errorf("Expected 1 result, got %d", len(results)) } // Check the result exists with correct key key := "service.consul:dynamic" if status, found := results[key]; found { // The status doesn't matter much (likely offline without real consul) // What matters is that the function processes the target correctly t.Logf("Consul target %s processed: Online=%v, Latency=%.2f", key, status.Online, status.Latency) } else { t.Errorf("Expected result for key %s not found", key) } }) // Test 2: Consul target without resolver should be marked offline t.Run("ConsulTargetWithoutResolver", func(t *testing.T) { consulTargets := []ProxyTarget{ { Host: "service.consul", Port: "dynamic", Type: "upstream", // No resolver - should be marked offline immediately IsConsul: true, ServiceURL: "service.consul service=no-resolver resolve", }, } results := TestConsulTargets(consulTargets) if len(results) != 1 { t.Errorf("Expected 1 result, got %d", len(results)) } key := "service.consul:dynamic" if status, found := results[key]; found { if status.Online { t.Errorf("Expected consul target without resolver to be offline, but it's online") } if status.Latency != 0 { t.Errorf("Expected latency to be 0 for offline target, got %.2f", status.Latency) } } else { t.Errorf("Expected result for key %s not found", key) } }) // Test 3: Multiple consul targets with different resolvers t.Run("MultipleConsulTargetsWithDifferentResolvers", func(t *testing.T) { consulTargets := []ProxyTarget{ { Host: "web-service.consul", Port: "dynamic", Type: "upstream", Resolver: "127.0.0.1:8600", IsConsul: true, ServiceURL: "service.consul service=web-service resolve", }, { Host: "api-service.consul", Port: "dynamic", Type: "upstream", Resolver: "127.0.0.1:8500", // Different resolver IsConsul: true, ServiceURL: "service.consul service=api-service resolve", }, } results := TestConsulTargets(consulTargets) // Should have 2 results if len(results) != 2 { t.Errorf("Expected 2 results, got %d", len(results)) } // Check both results exist expectedKeys := []string{ "web-service.consul:dynamic", "api-service.consul:dynamic", } for _, key := range expectedKeys { if _, found := results[key]; !found { t.Errorf("Expected result for key %s not found", key) } } }) // Test 4: Empty consul targets should return empty results t.Run("EmptyConsulTargets", func(t *testing.T) { consulTargets := []ProxyTarget{} results := TestConsulTargets(consulTargets) if len(results) != 0 { t.Errorf("Expected 0 results for empty targets, got %d", len(results)) } }) } // TestSimplifiedArchitecture tests the new simplified architecture func TestSimplifiedArchitecture(t *testing.T) { service := GetUpstreamService() service.ClearTargets() // Mix of traditional and consul targets mixedTargets := []ProxyTarget{ // Traditional targets {Host: "127.0.0.1", Port: "80", Type: "upstream"}, {Host: "192.168.1.100", Port: "8080", Type: "upstream"}, // Consul targets { Host: "service.consul", Port: "dynamic", Type: "upstream", Resolver: "127.0.0.1:8600", IsConsul: true, ServiceURL: "service.consul service=my-service resolve", }, } service.updateTargetsFromConfig("test-config.conf", mixedTargets) // Verify targets are correctly stored service.targetsMutex.RLock() traditionalCount := 0 consulCount := 0 for _, targetInfo := range service.targets { if targetInfo.ProxyTarget.IsConsul { consulCount++ } else { traditionalCount++ } } service.targetsMutex.RUnlock() if traditionalCount != 2 { t.Errorf("Expected 2 traditional targets, got %d", traditionalCount) } if consulCount != 1 { t.Errorf("Expected 1 consul target, got %d", consulCount) } t.Logf("Architecture correctly separated %d traditional and %d consul targets", traditionalCount, consulCount) // Clean up service.ClearTargets() } // TestEnhancedAvailabilityTest tests the enhanced availability testing with mixed targets func TestEnhancedAvailabilityTest(t *testing.T) { targets := []ProxyTarget{ // Regular target { Host: "127.0.0.1", Port: "22", // SSH port might be available Type: "upstream", }, // Consul target (will fail since no real consul) { Host: "service.consul", Port: "dynamic", Type: "upstream", Resolver: "127.0.0.1:8600", IsConsul: true, ServiceURL: "service.consul service=test-service resolve", }, // Invalid regular target { Host: "192.168.254.254", Port: "9999", Type: "upstream", }, } results := EnhancedAvailabilityTest(targets) t.Logf("Found %d test results:", len(results)) for key, status := range results { t.Logf("Target %s: Online=%v, Latency=%.2f", key, status.Online, status.Latency) } // Verify we have results for all targets expectedKeys := []string{ "127.0.0.1:22", "service.consul:dynamic", "192.168.254.254:9999", } for _, key := range expectedKeys { if _, found := results[key]; !found { t.Errorf("Expected result for key %s not found", key) } } // Test that consul target is processed (result doesn't matter, we just verify the flow works) if status, found := results["service.consul:dynamic"]; found { t.Logf("Consul target processed: Online=%v, Latency=%.2f", status.Online, status.Latency) } // Test that unreachable target is properly handled if status, found := results["192.168.254.254:9999"]; found { // This should be offline due to unreachable IP, but we verify the function handles it t.Logf("Unreachable target processed: Online=%v, Latency=%.2f", status.Online, status.Latency) // Most likely offline, but we don't assume - we just verify it was processed } } // TestTraditionalAvailabilityTestUsage tests that traditional AvailabilityTest is used when no consul targets func TestTraditionalAvailabilityTestUsage(t *testing.T) { // Test with only traditional targets targets := []ProxyTarget{ { Host: "127.0.0.1", Port: "80", Type: "upstream", }, { Host: "192.168.254.254", Port: "9999", Type: "upstream", }, } // Test enhanced version with traditional targets only enhancedResults := EnhancedAvailabilityTest(targets) // Test traditional version directly traditionalKeys := []string{"127.0.0.1:80", "192.168.254.254:9999"} traditionalResults := AvailabilityTest(traditionalKeys) t.Logf("Enhanced results: %d items", len(enhancedResults)) t.Logf("Traditional results: %d items", len(traditionalResults)) // Both should have same number of results if len(enhancedResults) != len(traditionalResults) { t.Errorf("Expected same number of results, enhanced=%d, traditional=%d", len(enhancedResults), len(traditionalResults)) return } // Results should be consistent for key, traditionalStatus := range traditionalResults { if enhancedStatus, found := enhancedResults[key]; found { if traditionalStatus.Online != enhancedStatus.Online { t.Errorf("Inconsistent online status for %s: traditional=%v, enhanced=%v", key, traditionalStatus.Online, enhancedStatus.Online) } } else { t.Errorf("Key %s missing in enhanced results", key) } } t.Logf("Enhanced test correctly delegated to traditional test for non-consul targets") } // TestUpstreamServiceSimplifiedFlow tests the new simplified flow in UpstreamService func TestUpstreamServiceSimplifiedFlow(t *testing.T) { service := GetUpstreamService() service.ClearTargets() // Add mixed targets mixedTargets := []ProxyTarget{ {Host: "127.0.0.1", Port: "80", Type: "upstream"}, { Host: "service.consul", Port: "dynamic", Type: "upstream", Resolver: "127.0.0.1:8600", IsConsul: true, ServiceURL: "service.consul service=test resolve", }, } service.updateTargetsFromConfig("test-config.conf", mixedTargets) // This would trigger the simplified flow service.PerformAvailabilityTest() results := service.GetAvailabilityMap() t.Logf("Simplified flow generated %d results:", len(results)) for key, status := range results { t.Logf("Result %s: Online=%v, Latency=%.2f", key, status.Online, status.Latency) } // Should have results for both targets expectedKeys := []string{"127.0.0.1:80", "service.consul:dynamic"} for _, key := range expectedKeys { if _, found := results[key]; !found { t.Errorf("Expected result for key %s not found", key) } } t.Logf("Simplified architecture correctly processed mixed targets") // Clean up service.ClearTargets() }