package reconcile import ( "os" "path/filepath" "testing" "github.com/wild-cloud/wild-central/internal/config" "github.com/wild-cloud/wild-central/internal/dnsmasq" "github.com/wild-cloud/wild-central/internal/domains" "github.com/wild-cloud/wild-central/internal/haproxy" "github.com/wild-cloud/wild-central/internal/sse" ) // --- Stubs --- type stubDomainManager struct { domains []domains.Domain err error } func (s *stubDomainManager) List() ([]domains.Domain, error) { return s.domains, s.err } type stubHAProxy struct { generateCalls int lastL4Routes []haproxy.L4Route writtenConfig string writeErr error reloadCalled bool } func (s *stubHAProxy) GenerateWithOpts(l4 []haproxy.L4Route, _ []haproxy.CustomRoute, _ haproxy.GenerateOpts) string { s.generateCalls++ s.lastL4Routes = l4 return "generated-config" } func (s *stubHAProxy) SafeApply(c string) error { s.writtenConfig = c; return s.writeErr } func (s *stubHAProxy) WriteConfig(c string) error { s.writtenConfig = c; return s.writeErr } func (s *stubHAProxy) ReloadService() error { s.reloadCalled = true; return nil } type stubDNS struct { entries []dnsmasq.DNSEntry filterPath string applyCalled bool } func (s *stubDNS) Generate(_ *config.State, entries []dnsmasq.DNSEntry) string { s.entries = entries return "generated-dns-config" } func (s *stubDNS) SafeApply(_ string) error { s.applyCalled = true return nil } func (s *stubDNS) SetFilterConfPath(p string) { s.filterPath = p } func (s *stubDNS) GetStatus() (*dnsmasq.Status, error) { return nil, nil } type stubAuth struct { userCount int restartCalled bool } func (s *stubAuth) UserCount() int { return s.userCount } func (s *stubAuth) RestartService() error { s.restartCalled = true; return nil } type stubDDNS struct{ triggerCalled bool } func (s *stubDDNS) Trigger() { s.triggerCalled = true } type stubDNSFilter struct{ hostsPath string } func (s *stubDNSFilter) HostsFilePath() string { return s.hostsPath } type stubSSE struct{ events []*sse.Event } func (s *stubSSE) Broadcast(e *sse.Event) { s.events = append(s.events, e) } func newTestReconciler(t *testing.T, doms []domains.Domain) (*Reconciler, *stubHAProxy, *stubDNS, *stubDDNS) { t.Helper() tmpDir := t.TempDir() statePath := filepath.Join(tmpDir, "state.yaml") // Write minimal state _ = config.SaveState(&config.State{}, statePath) hp := &stubHAProxy{} dns := &stubDNS{} ddns := &stubDDNS{} r := &Reconciler{ Domains: &stubDomainManager{domains: doms}, HAProxy: hp, DNS: dns, Auth: &stubAuth{}, DDNS: ddns, DNSFilter: &stubDNSFilter{}, SSE: &stubSSE{}, StatePath: statePath, } return r, hp, dns, ddns } // --- Pure function tests --- func TestBuildRoutes_MixedBackendTypes(t *testing.T) { doms := []domains.Domain{ {DomainName: "cloud.example.com", Backend: domains.Backend{Address: "192.168.1.10:443", Type: domains.BackendTCPPassthrough}, Subdomains: true}, {DomainName: "api.example.com", Backend: domains.Backend{Address: "192.168.1.20:8080", Type: domains.BackendHTTP}}, {DomainName: "ssh.example.com", Backend: domains.Backend{Address: "192.168.1.30:22", Type: domains.BackendDNSOnly}}, } l4, http := buildRoutes(doms) if len(l4) != 1 { t.Fatalf("expected 1 L4 route, got %d", len(l4)) } if l4[0].Domain != "cloud.example.com" || l4[0].BackendIP != "192.168.1.10" || !l4[0].Subdomains { t.Errorf("L4 route mismatch: %+v", l4[0]) } if len(http) != 1 { t.Fatalf("expected 1 HTTP route, got %d", len(http)) } if http[0].Domain != "api.example.com" { t.Errorf("HTTP route domain = %q, want api.example.com", http[0].Domain) } if len(http[0].Routes) != 1 || http[0].Routes[0].Backend != "192.168.1.20:8080" { t.Errorf("HTTP route backend mismatch: %+v", http[0].Routes) } } func TestBuildRoutes_EmptyDomains(t *testing.T) { l4, http := buildRoutes(nil) if l4 != nil || http != nil { t.Error("expected nil routes for nil domains") } } func TestBuildDNSEntries_IPAssignment(t *testing.T) { centralIP := "10.0.0.1" doms := []domains.Domain{ {DomainName: "central.example.com", Backend: domains.Backend{Address: "127.0.0.1:5055", Type: domains.BackendHTTP}}, {DomainName: "cloud.example.com", Backend: domains.Backend{Address: "192.168.1.10:443", Type: domains.BackendTCPPassthrough}}, {DomainName: "ssh.example.com", Backend: domains.Backend{Address: "192.168.1.30:22", Type: domains.BackendDNSOnly}}, } entries := buildDNSEntries(doms, centralIP) if len(entries) != 3 { t.Fatalf("expected 3 entries, got %d", len(entries)) } // HTTP → centralIP if entries[0].IP != centralIP { t.Errorf("HTTP domain IP = %q, want %q", entries[0].IP, centralIP) } // TCP passthrough → backend IP if entries[1].IP != "192.168.1.10" { t.Errorf("TCP domain IP = %q, want 192.168.1.10", entries[1].IP) } // DNS-only → backend IP if entries[2].IP != "192.168.1.30" { t.Errorf("DNS-only domain IP = %q, want 192.168.1.30", entries[2].IP) } } func TestBuildDNSEntries_SkipsEmptyDomainName(t *testing.T) { entries := buildDNSEntries([]domains.Domain{{DomainName: ""}}, "10.0.0.1") if len(entries) != 0 { t.Error("expected empty domain to be skipped") } } func TestBuildDNSEntries_WildcardFlag(t *testing.T) { doms := []domains.Domain{ {DomainName: "cloud.example.com", Backend: domains.Backend{Address: "192.168.1.10:443", Type: domains.BackendTCPPassthrough}, Subdomains: true}, {DomainName: "exact.example.com", Backend: domains.Backend{Address: "192.168.1.20:443", Type: domains.BackendTCPPassthrough}, Subdomains: false}, } entries := buildDNSEntries(doms, "10.0.0.1") if !entries[0].Wildcard { t.Error("expected first entry to be wildcard") } if entries[1].Wildcard { t.Error("expected second entry to not be wildcard") } } // --- Integration tests with stubs --- func TestReconcile_EmptyDomains(t *testing.T) { r, hp, dns, ddns := newTestReconciler(t, nil) r.Reconcile() if !dns.applyCalled { t.Error("expected dnsmasq update") } if len(dns.entries) != 0 { t.Errorf("expected 0 DNS entries, got %d", len(dns.entries)) } if !hp.reloadCalled { // No routes but config is still generated and written } if !ddns.triggerCalled { t.Error("expected DDNS trigger") } // With no routes, HAProxy should still generate (empty config is valid) if hp.generateCalls != 1 { t.Errorf("expected 1 HAProxy generate call, got %d", hp.generateCalls) } } func TestReconcile_DDNSTriggered(t *testing.T) { doms := []domains.Domain{ {DomainName: "test.example.com", Backend: domains.Backend{Address: "127.0.0.1:80", Type: domains.BackendHTTP}}, } r, _, _, ddns := newTestReconciler(t, doms) r.Reconcile() if !ddns.triggerCalled { t.Error("expected DDNS.Trigger() to be called") } } func TestReconcile_DNSEntriesBuiltFromDomains(t *testing.T) { doms := []domains.Domain{ {DomainName: "app.example.com", Backend: domains.Backend{Address: "127.0.0.1:8080", Type: domains.BackendHTTP}}, {DomainName: "k8s.example.com", Backend: domains.Backend{Address: "192.168.1.50:443", Type: domains.BackendTCPPassthrough}}, } r, _, dns, _ := newTestReconciler(t, doms) r.Reconcile() if len(dns.entries) != 2 { t.Fatalf("expected 2 DNS entries, got %d", len(dns.entries)) } // HTTP domain should use centralIP (127.0.0.1 fallback since state has no IP) if dns.entries[0].Domain != "app.example.com" { t.Errorf("first DNS entry domain = %q", dns.entries[0].Domain) } // TCP passthrough should use backend IP directly if dns.entries[1].IP != "192.168.1.50" { t.Errorf("TCP DNS entry IP = %q, want 192.168.1.50", dns.entries[1].IP) } } // --- Helper tests --- func TestIsValidCertFile_Valid(t *testing.T) { tmp := t.TempDir() path := filepath.Join(tmp, "test.pem") if err := os.WriteFile(path, []byte("--- cert content ---"), 0600); err != nil { t.Fatal(err) } if !isValidCertFile(path) { t.Error("expected valid cert file to return true") } } func TestIsValidCertFile_Empty(t *testing.T) { tmp := t.TempDir() path := filepath.Join(tmp, "empty.pem") if err := os.WriteFile(path, nil, 0600); err != nil { t.Fatal(err) } if isValidCertFile(path) { t.Error("expected 0-byte cert file to return false") } } func TestIsValidCertFile_Missing(t *testing.T) { if isValidCertFile("/nonexistent/path.pem") { t.Error("expected missing file to return false") } } func TestExtractHost(t *testing.T) { tests := []struct { input, want string }{ {"192.168.1.1:8080", "192.168.1.1"}, {"example.com:443", "example.com"}, {"localhost", "localhost"}, {"127.0.0.1", "127.0.0.1"}, } for _, tt := range tests { if got := extractHost(tt.input); got != tt.want { t.Errorf("extractHost(%q) = %q, want %q", tt.input, got, tt.want) } } }