| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683 | package upstreamimport (	"context"	"fmt"	"net"	"sort"	"strings"	"testing")// MockDNSServer simulates DNS responses for testingtype MockDNSServer struct {	srvRecords map[string][]*net.SRV	aRecords   map[string][]net.IPAddr}// NewMockDNSServer creates a mock DNS server for testingfunc NewMockDNSServer() *MockDNSServer {	return &MockDNSServer{		srvRecords: make(map[string][]*net.SRV),		aRecords:   make(map[string][]net.IPAddr),	}}// AddSRVRecord adds a SRV record to the mock DNS serverfunc (m *MockDNSServer) AddSRVRecord(domain string, priority, weight uint16, port uint16, target string) {	m.srvRecords[domain] = append(m.srvRecords[domain], &net.SRV{		Priority: priority,		Weight:   weight,		Port:     port,		Target:   target,	})}// AddARecord adds an A record to the mock DNS serverfunc (m *MockDNSServer) AddARecord(domain string, ip string) {	m.aRecords[domain] = append(m.aRecords[domain], net.IPAddr{		IP: net.ParseIP(ip),	})}// MockResolver is a custom resolver that uses our mock DNS servertype MockResolver struct {	mockServer *MockDNSServer}// LookupSRV simulates SRV record lookup with proper priority sortingfunc (mr *MockResolver) LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error) {	domain := name	if service != "" || proto != "" {		domain = fmt.Sprintf("_%s._%s.%s", service, proto, name)	}	if records, exists := mr.mockServer.srvRecords[domain]; exists {		// Sort SRV records by priority (lowest first), then by weight (highest first)		// This follows RFC 2782 and nginx behavior		sortedRecords := make([]*net.SRV, len(records))		copy(sortedRecords, records)		sort.Slice(sortedRecords, func(i, j int) bool {			if sortedRecords[i].Priority != sortedRecords[j].Priority {				return sortedRecords[i].Priority < sortedRecords[j].Priority			}			// For same priority, higher weight comes first (but this is simplified for testing)			return sortedRecords[i].Weight > sortedRecords[j].Weight		})		return "", sortedRecords, nil	}	return "", nil, fmt.Errorf("no SRV records for %s", domain)}// LookupIPAddr simulates A record lookupfunc (mr *MockResolver) LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error) {	if records, exists := mr.mockServer.aRecords[host]; exists {		return records, nil	}	return nil, fmt.Errorf("no A records for %s", host)}// TestParseServiceURL tests the parseServiceURL function with nginx compliancefunc TestParseServiceURL(t *testing.T) {	tests := []struct {		name        string		input       string		expectedErr bool		expected    *ServiceInfo	}{		{			name:  "Valid nginx service URL - simple service name",			input: "backend.example.com service=http resolve",			expected: &ServiceInfo{				Hostname:    "backend.example.com",				ServiceName: "http",			},		},		{			name:  "Valid nginx service URL - service name with underscores",			input: "backend.example.com service=_http._tcp resolve",			expected: &ServiceInfo{				Hostname:    "backend.example.com",				ServiceName: "_http._tcp",			},		},		{			name:  "Valid nginx service URL - service name with dots",			input: "example.com service=server1.backend resolve",			expected: &ServiceInfo{				Hostname:    "example.com",				ServiceName: "server1.backend",			},		},		{			name:  "Consul service example",			input: "service.consul service=web-service resolve",			expected: &ServiceInfo{				Hostname:    "service.consul",				ServiceName: "web-service",			},		},		{			name:        "Empty input",			input:       "",			expectedErr: true,		},		{			name:        "Missing resolve parameter",			input:       "backend.example.com service=http",			expectedErr: true,		},		{			name:        "Missing service parameter",			input:       "backend.example.com resolve",			expectedErr: true,		},		{			name:        "Empty service name",			input:       "backend.example.com service= resolve",			expectedErr: true,		},		{			name:        "Only hostname",			input:       "backend.example.com",			expectedErr: true,		},	}	resolver := NewDynamicResolver("127.0.0.1:8600")	for _, tt := range tests {		t.Run(tt.name, func(t *testing.T) {			result, err := resolver.parseServiceURL(tt.input)			if tt.expectedErr {				if err == nil {					t.Errorf("Expected error but got none")				}				return			}			if err != nil {				t.Errorf("Unexpected error: %v", err)				return			}			if result.Hostname != tt.expected.Hostname {				t.Errorf("Expected hostname %s, got %s", tt.expected.Hostname, result.Hostname)			}			if result.ServiceName != tt.expected.ServiceName {				t.Errorf("Expected service name %s, got %s", tt.expected.ServiceName, result.ServiceName)			}		})	}}// TestConstructSRVDomain tests SRV domain construction according to nginx.org rulesfunc TestConstructSRVDomain(t *testing.T) {	tests := []struct {		name     string		input    *ServiceInfo		expected string		rule     string	}{		{			name: "Rule 1: Service name without dots - http",			input: &ServiceInfo{				Hostname:    "backend.example.com",				ServiceName: "http",			},			expected: "_http._tcp.backend.example.com",			rule:     "nginx rule 1: no dots, add TCP protocol",		},		{			name: "Rule 1: Service name without dots - https",			input: &ServiceInfo{				Hostname:    "api.example.com",				ServiceName: "https",			},			expected: "_https._tcp.api.example.com",			rule:     "nginx rule 1: no dots, add TCP protocol",		},		{			name: "Rule 1: Service name without dots - mysql",			input: &ServiceInfo{				Hostname:    "db.example.com",				ServiceName: "mysql",			},			expected: "_mysql._tcp.db.example.com",			rule:     "nginx rule 1: no dots, add TCP protocol",		},		{			name: "Rule 2: Service name with dots - _http._tcp",			input: &ServiceInfo{				Hostname:    "backend.example.com",				ServiceName: "_http._tcp",			},			expected: "_http._tcp.backend.example.com",			rule:     "nginx rule 2: contains dots, join directly",		},		{			name: "Rule 2: Service name with dots - server1.backend",			input: &ServiceInfo{				Hostname:    "example.com",				ServiceName: "server1.backend",			},			expected: "server1.backend.example.com",			rule:     "nginx rule 2: contains dots, join directly",		},		{			name: "Rule 2: Complex service name with underscores and dots",			input: &ServiceInfo{				Hostname:    "dc1.consul",				ServiceName: "_api._tcp.production",			},			expected: "_api._tcp.production.dc1.consul",			rule:     "nginx rule 2: contains dots, join directly",		},		{			name: "Consul example - simple service",			input: &ServiceInfo{				Hostname:    "service.consul",				ServiceName: "web",			},			expected: "_web._tcp.service.consul",			rule:     "nginx rule 1: no dots, add TCP protocol",		},	}	resolver := NewDynamicResolver("127.0.0.1:8600")	for _, tt := range tests {		t.Run(tt.name, func(t *testing.T) {			result := resolver.constructSRVDomain(tt.input)			if result != tt.expected {				t.Errorf("Expected SRV domain %s, got %s (rule: %s)", tt.expected, result, tt.rule)			}		})	}}// TestNginxOfficialExamples tests the exact examples from nginx.org documentationfunc TestNginxOfficialExamples(t *testing.T) {	tests := []struct {		name          string		nginxConfig   string		expectedQuery string		description   string	}{		{			name:          "Official Example 1",			nginxConfig:   "backend.example.com service=http resolve",			expectedQuery: "_http._tcp.backend.example.com",			description:   "To look up _http._tcp.backend.example.com SRV record",		},		{			name:          "Official Example 2",			nginxConfig:   "backend.example.com service=_http._tcp resolve",			expectedQuery: "_http._tcp.backend.example.com",			description:   "Service name already contains dots, join directly",		},		{			name:          "Official Example 3",			nginxConfig:   "example.com service=server1.backend resolve",			expectedQuery: "server1.backend.example.com",			description:   "Service name contains dots, join directly",		},	}	resolver := NewDynamicResolver("127.0.0.1:8600")	for _, tt := range tests {		t.Run(tt.name, func(t *testing.T) {			serviceInfo, err := resolver.parseServiceURL(tt.nginxConfig)			if err != nil {				t.Fatalf("Failed to parse nginx config: %v", err)			}			result := resolver.constructSRVDomain(serviceInfo)			if result != tt.expectedQuery {				t.Errorf("nginx.org example failed: expected %s, got %s (%s)",					tt.expectedQuery, result, tt.description)			}		})	}}// TestSRVRecordResolutionWithMockDNS tests actual SRV record resolution using mock DNSfunc TestSRVRecordResolutionWithMockDNS(t *testing.T) {	// Create mock DNS server	mockDNS := NewMockDNSServer()	// Add SRV records for _http._tcp.backend.example.com	mockDNS.AddSRVRecord("_http._tcp.backend.example.com", 10, 60, 8080, "web1.backend.example.com")	mockDNS.AddSRVRecord("_http._tcp.backend.example.com", 10, 40, 8080, "web2.backend.example.com")	mockDNS.AddSRVRecord("_http._tcp.backend.example.com", 20, 100, 8080, "web3.backend.example.com")	// Add A records for the targets	mockDNS.AddARecord("web1.backend.example.com", "192.168.1.10")	mockDNS.AddARecord("web2.backend.example.com", "192.168.1.11")	mockDNS.AddARecord("web3.backend.example.com", "192.168.1.12")	t.Run("SRV record resolution", func(t *testing.T) {		mockResolver := &MockResolver{mockServer: mockDNS}		// Test SRV lookup		_, srvRecords, err := mockResolver.LookupSRV(context.Background(), "", "", "_http._tcp.backend.example.com")		if err != nil {			t.Fatalf("SRV lookup failed: %v", err)		}		if len(srvRecords) != 3 {			t.Errorf("Expected 3 SRV records, got %d", len(srvRecords))		}		// Verify priority ordering (lowest priority first) and weight ordering (highest weight first within same priority)		expectedPriorities := []uint16{10, 10, 20}		expectedWeights := []uint16{60, 40, 100} // For priorities [10, 10, 20], weights should be [60, 40, 100]		expectedTargets := []string{"web1.backend.example.com", "web2.backend.example.com", "web3.backend.example.com"}		for i, srv := range srvRecords {			if srv.Priority != expectedPriorities[i] {				t.Errorf("Expected priority %d at index %d, got %d", expectedPriorities[i], i, srv.Priority)			}			if srv.Weight != expectedWeights[i] {				t.Errorf("Expected weight %d at index %d, got %d", expectedWeights[i], i, srv.Weight)			}			if srv.Target != expectedTargets[i] {				t.Errorf("Expected target %s at index %d, got %s", expectedTargets[i], i, srv.Target)			}		}		// Test A record resolution for each target		for _, srv := range srvRecords {			ips, err := mockResolver.LookupIPAddr(context.Background(), srv.Target)			if err != nil {				t.Errorf("A record lookup failed for %s: %v", srv.Target, err)				continue			}			if len(ips) != 1 {				t.Errorf("Expected 1 IP for %s, got %d", srv.Target, len(ips))			}		}	})}// TestSRVPriorityHandling tests nginx SRV priority handling as per nginx.org documentationfunc TestSRVPriorityHandling(t *testing.T) {	// Create mock DNS server	mockDNS := NewMockDNSServer()	// Add SRV records with different priorities to test primary/backup server logic	// Priority 5 (highest priority / primary servers)	mockDNS.AddSRVRecord("_http._tcp.app.example.com", 5, 100, 8080, "primary1.app.example.com")	mockDNS.AddSRVRecord("_http._tcp.app.example.com", 5, 50, 8080, "primary2.app.example.com")	// Priority 10 (backup servers)	mockDNS.AddSRVRecord("_http._tcp.app.example.com", 10, 80, 8080, "backup1.app.example.com")	// Priority 15 (lower priority backup servers)	mockDNS.AddSRVRecord("_http._tcp.app.example.com", 15, 200, 8080, "backup2.app.example.com")	// Add A records	mockDNS.AddARecord("primary1.app.example.com", "10.0.1.1")	mockDNS.AddARecord("primary2.app.example.com", "10.0.1.2")	mockDNS.AddARecord("backup1.app.example.com", "10.0.2.1")	mockDNS.AddARecord("backup2.app.example.com", "10.0.3.1")	t.Run("SRV priority handling", func(t *testing.T) {		mockResolver := &MockResolver{mockServer: mockDNS}		// Test SRV lookup		_, srvRecords, err := mockResolver.LookupSRV(context.Background(), "", "", "_http._tcp.app.example.com")		if err != nil {			t.Fatalf("SRV lookup failed: %v", err)		}		if len(srvRecords) != 4 {			t.Errorf("Expected 4 SRV records, got %d", len(srvRecords))		}		// According to nginx.org: "Highest-priority SRV records (records with the same lowest-number priority value)		// are resolved as primary servers, the rest of SRV records are resolved as backup servers"		expectedOrder := []struct {			priority   uint16			weight     uint16			target     string			serverType string		}{			{5, 100, "primary1.app.example.com", "primary"}, // Highest priority (lowest number)			{5, 50, "primary2.app.example.com", "primary"},  // Same priority, lower weight			{10, 80, "backup1.app.example.com", "backup"},   // Lower priority (backup)			{15, 200, "backup2.app.example.com", "backup"},  // Lowest priority (backup)		}		for i, srv := range srvRecords {			expected := expectedOrder[i]			if srv.Priority != expected.priority {				t.Errorf("Record %d: expected priority %d, got %d", i, expected.priority, srv.Priority)			}			if srv.Weight != expected.weight {				t.Errorf("Record %d: expected weight %d, got %d", i, expected.weight, srv.Weight)			}			if srv.Target != expected.target {				t.Errorf("Record %d: expected target %s, got %s", i, expected.target, srv.Target)			}			// Log the server type for documentation			t.Logf("Record %d: Priority %d, Weight %d, Target %s (%s server)",				i, srv.Priority, srv.Weight, srv.Target, expected.serverType)		}		// Verify primary servers come first (lowest priority numbers)		primaryCount := 0		for _, srv := range srvRecords {			if srv.Priority == 5 { // Primary servers have priority 5				primaryCount++			} else {				break // Once we hit a non-primary, all following should be backups			}		}		if primaryCount != 2 {			t.Errorf("Expected 2 primary servers at the beginning, got %d", primaryCount)		}	})}// TestARecordFallback tests A record fallback when SRV lookup failsfunc TestARecordFallback(t *testing.T) {	mockDNS := NewMockDNSServer()	// Only add A record, no SRV record	mockDNS.AddARecord("_http._tcp.backend.example.com", "192.168.1.100")	t.Run("A record fallback", func(t *testing.T) {		mockResolver := &MockResolver{mockServer: mockDNS}		// SRV lookup should fail		_, srvRecords, err := mockResolver.LookupSRV(context.Background(), "", "", "_http._tcp.backend.example.com")		if err == nil {			t.Error("Expected SRV lookup to fail")		}		if len(srvRecords) != 0 {			t.Errorf("Expected 0 SRV records, got %d", len(srvRecords))		}		// A record lookup should succeed		ips, err := mockResolver.LookupIPAddr(context.Background(), "_http._tcp.backend.example.com")		if err != nil {			t.Fatalf("A record lookup failed: %v", err)		}		if len(ips) != 1 {			t.Errorf("Expected 1 IP, got %d", len(ips))		}		expectedIP := "192.168.1.100"		if ips[0].IP.String() != expectedIP {			t.Errorf("Expected IP %s, got %s", expectedIP, ips[0].IP.String())		}	})}// TestComplexNginxScenarios tests more complex real-world nginx scenariosfunc TestComplexNginxScenarios(t *testing.T) {	tests := []struct {		name        string		nginxLine   string		expectedSRV string		scenario    string	}{		{			name:        "Load balancer with HTTP service",			nginxLine:   "api.microservices.local service=http resolve",			expectedSRV: "_http._tcp.api.microservices.local",			scenario:    "Microservices API load balancing",		},		{			name:        "Database connection",			nginxLine:   "db.cluster.local service=mysql resolve",			expectedSRV: "_mysql._tcp.db.cluster.local",			scenario:    "Database cluster connection",		},		{			name:        "WebSocket service",			nginxLine:   "chat.app.local service=ws resolve",			expectedSRV: "_ws._tcp.chat.app.local",			scenario:    "WebSocket service discovery",		},		{			name:        "Custom protocol with dots",			nginxLine:   "service.consul service=_grpc._tcp resolve",			expectedSRV: "_grpc._tcp.service.consul",			scenario:    "gRPC service via Consul",		},		{			name:        "Multi-level service hierarchy",			nginxLine:   "consul.local service=api.v1.production resolve",			expectedSRV: "api.v1.production.consul.local",			scenario:    "Multi-level service naming",		},		{			name:        "Kubernetes style service",			nginxLine:   "cluster.local service=_http._tcp.nginx.default resolve",			expectedSRV: "_http._tcp.nginx.default.cluster.local",			scenario:    "Kubernetes service discovery",		},	}	resolver := NewDynamicResolver("127.0.0.1:8600")	for _, tt := range tests {		t.Run(tt.name, func(t *testing.T) {			serviceInfo, err := resolver.parseServiceURL(tt.nginxLine)			if err != nil {				t.Fatalf("Failed to parse nginx line: %v", err)			}			result := resolver.constructSRVDomain(serviceInfo)			if result != tt.expectedSRV {				t.Errorf("Scenario '%s' failed: expected %s, got %s",					tt.scenario, tt.expectedSRV, result)			}		})	}}// TestBackwardCompatibility tests backward compatibility with old formatfunc TestBackwardCompatibility(t *testing.T) {	tests := []struct {		name     string		input    string		expected string	}{		{			name:     "New nginx format should work",			input:    "backend.example.com service=http resolve",			expected: "http",		},		{			name:     "New nginx format with dots",			input:    "example.com service=_http._tcp resolve",			expected: "_http._tcp",		},		{			name:     "Old consul format should still work as fallback",			input:    "test-service.service.consul",			expected: "test-service",		},		{			name:     "Invalid format should return empty",			input:    "invalid format without proper structure",			expected: "",		},	}	resolver := NewDynamicResolver("127.0.0.1:8600")	for _, tt := range tests {		t.Run(tt.name, func(t *testing.T) {			result := resolver.extractServiceName(tt.input)			if result != tt.expected {				t.Errorf("Expected %s, got %s", tt.expected, result)			}		})	}}// TestDynamicTargetsFunction tests the TestDynamicTargets functionfunc TestDynamicTargetsFunction(t *testing.T) {	t.Run("Valid dynamic targets", func(t *testing.T) {		targets := []ProxyTarget{			{				Host:       "service.consul",				Port:       "dynamic",				Type:       "upstream",				Resolver:   "127.0.0.1:8600",				IsConsul:   true,				ServiceURL: "backend.example.com service=http resolve",			},		}		results := TestDynamicTargets(targets)		if len(results) != 1 {			t.Errorf("Expected 1 result, got %d", len(results))		}		key := "service.consul:dynamic"		if _, found := results[key]; !found {			t.Errorf("Expected result for key %s not found", key)		}	})	t.Run("Target without resolver should be offline", func(t *testing.T) {		targets := []ProxyTarget{			{				Host:       "service.consul",				Port:       "dynamic",				Type:       "upstream",				IsConsul:   true,				ServiceURL: "backend.example.com service=http resolve",				// No resolver specified			},		}		results := TestDynamicTargets(targets)		key := "service.consul:dynamic"		if status, found := results[key]; found {			if status.Online {				t.Error("Expected target without resolver to be offline")			}			if status.Latency != 0 {				t.Errorf("Expected latency 0 for offline target, got %.2f", status.Latency)			}		} else {			t.Errorf("Expected result for key %s", key)		}	})}// TestIntegrationWithProxyParser tests integration with the proxy parserfunc TestIntegrationWithProxyParser(t *testing.T) {	config := `upstream web-backend {    zone upstream_web 128k;    resolver 127.0.0.1:8600 valid=5s;    resolver_timeout 2s;    server backend.example.com service=http resolve;}server {    listen 80;    server_name example.com;    location / {        proxy_pass http://web-backend;    }}`	targets := ParseProxyTargetsFromRawContent(config)	// Should find the dynamic DNS target	found := false	for _, target := range targets {		if target.IsConsul && strings.Contains(target.ServiceURL, "service=http") {			found = true			// Verify the target is correctly parsed			if target.Resolver != "127.0.0.1:8600" {				t.Errorf("Expected resolver 127.0.0.1:8600, got %s", target.Resolver)			}			if target.ServiceURL != "backend.example.com service=http resolve" {				t.Errorf("Expected service URL 'backend.example.com service=http resolve', got %s", target.ServiceURL)			}			break		}	}	if !found {		t.Error("Dynamic DNS target not found in parsed config")	}}
 |