package handlers import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "github.com/orchard9/rdev/internal/domain" ) func TestInfrastructureHandler_ListDomains(t *testing.T) { t.Run("returns matching A records", func(t *testing.T) { _, _, dns, _, router := setupInfraHandler() // Add records — one matching the project, one unrelated dns.records["landing.threesix.ai"] = &domain.DNSRecord{ ID: "rec-1", Type: "A", Name: "landing.threesix.ai", Content: "208.122.204.172", TTL: 1, } dns.records["other.threesix.ai"] = &domain.DNSRecord{ ID: "rec-2", Type: "A", Name: "other.threesix.ai", Content: "208.122.204.172", TTL: 1, } req := httptest.NewRequest("GET", "/projects/landing/domains", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String()) } var resp map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal: %v", err) } data := resp["data"].(map[string]any) total := int(data["total"].(float64)) if total != 1 { t.Errorf("total = %d, want 1 (only landing.threesix.ai)", total) } }) t.Run("returns CNAME aliases", func(t *testing.T) { _, _, dns, _, router := setupInfraHandler() dns.records["landing.threesix.ai"] = &domain.DNSRecord{ ID: "rec-1", Type: "A", Name: "landing.threesix.ai", Content: "208.122.204.172", TTL: 1, } dns.records["www.threesix.ai"] = &domain.DNSRecord{ ID: "rec-2", Type: "CNAME", Name: "www.threesix.ai", Content: "landing.threesix.ai", TTL: 1, } req := httptest.NewRequest("GET", "/projects/landing/domains", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) } var resp map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal: %v", err) } data := resp["data"].(map[string]any) total := int(data["total"].(float64)) if total != 2 { t.Errorf("total = %d, want 2 (A + CNAME)", total) } }) t.Run("DNS not configured", func(t *testing.T) { h := NewInfrastructureHandler(nil, nil, nil, nil, nil, InfrastructureConfig{ DefaultGitOwner: "threesix", DefaultDomain: "threesix.ai", ClusterIP: "208.122.204.172", }) r := chi.NewRouter() h.Mount(r) req := httptest.NewRequest("GET", "/projects/myapp/domains", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusInternalServerError { t.Errorf("status = %d, want %d", rec.Code, http.StatusInternalServerError) } }) } func TestInfrastructureHandler_AddDomainAlias(t *testing.T) { t.Run("add A record alias", func(t *testing.T) { _, _, dns, _, router := setupInfraHandler() body, _ := json.Marshal(DomainAliasRequest{Domain: "www.threesix.ai"}) req := httptest.NewRequest("POST", "/projects/landing/domains", bytes.NewReader(body)) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusCreated { t.Fatalf("status = %d, want %d; body: %s", rec.Code, http.StatusCreated, rec.Body.String()) } if len(dns.records) != 1 { t.Errorf("DNS records = %d, want 1", len(dns.records)) } }) t.Run("add CNAME alias", func(t *testing.T) { _, _, dns, _, router := setupInfraHandler() body, _ := json.Marshal(DomainAliasRequest{ Domain: "www.threesix.ai", Type: "CNAME", }) req := httptest.NewRequest("POST", "/projects/landing/domains", bytes.NewReader(body)) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusCreated { t.Fatalf("status = %d, want %d; body: %s", rec.Code, http.StatusCreated, rec.Body.String()) } // CNAME should target landing.threesix.ai for _, r := range dns.records { if r.Type != "CNAME" { t.Errorf("type = %s, want CNAME", r.Type) } if r.Content != "landing.threesix.ai" { t.Errorf("content = %s, want landing.threesix.ai", r.Content) } } }) t.Run("invalid type", func(t *testing.T) { _, _, _, _, router := setupInfraHandler() body, _ := json.Marshal(DomainAliasRequest{Domain: "www.threesix.ai", Type: "MX"}) req := httptest.NewRequest("POST", "/projects/landing/domains", bytes.NewReader(body)) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest) } }) t.Run("missing domain", func(t *testing.T) { _, _, _, _, router := setupInfraHandler() body, _ := json.Marshal(DomainAliasRequest{}) req := httptest.NewRequest("POST", "/projects/landing/domains", bytes.NewReader(body)) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest) } }) t.Run("DNS not configured", func(t *testing.T) { h := NewInfrastructureHandler(nil, nil, nil, nil, nil, InfrastructureConfig{ DefaultGitOwner: "threesix", DefaultDomain: "threesix.ai", ClusterIP: "208.122.204.172", }) r := chi.NewRouter() h.Mount(r) body, _ := json.Marshal(DomainAliasRequest{Domain: "www.threesix.ai"}) req := httptest.NewRequest("POST", "/projects/landing/domains", bytes.NewReader(body)) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusInternalServerError { t.Errorf("status = %d, want %d", rec.Code, http.StatusInternalServerError) } }) } func TestInfrastructureHandler_RemoveDomainAlias(t *testing.T) { t.Run("removes alias", func(t *testing.T) { _, _, dns, _, router := setupInfraHandler() dns.records["www"] = &domain.DNSRecord{ ID: "rec-www", Type: "A", Name: "www", Content: "208.122.204.172", } req := httptest.NewRequest("DELETE", "/projects/landing/domains/www.threesix.ai", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String()) } }) t.Run("prevents removing primary domain", func(t *testing.T) { _, _, _, _, router := setupInfraHandler() req := httptest.NewRequest("DELETE", "/projects/landing/domains/landing.threesix.ai", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest) } }) t.Run("not found", func(t *testing.T) { _, _, dns, _, router := setupInfraHandler() dns.err = nil // No records stored req := httptest.NewRequest("DELETE", "/projects/landing/domains/nonexistent.threesix.ai", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Errorf("status = %d, want %d; body: %s", rec.Code, http.StatusNotFound, rec.Body.String()) } }) } func TestIsProjectDomain(t *testing.T) { tests := []struct { name, projectID, baseDomain string want bool }{ {"landing.threesix.ai", "landing", "threesix.ai", true}, {"other.threesix.ai", "landing", "threesix.ai", false}, {"landing.example.com", "landing", "threesix.ai", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := isProjectDomain(tt.name, tt.projectID, tt.baseDomain) if got != tt.want { t.Errorf("isProjectDomain(%q, %q, %q) = %v, want %v", tt.name, tt.projectID, tt.baseDomain, got, tt.want) } }) } }