package handlers import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "github.com/orchard9/rdev/internal/domain" ) // mockDomainService implements DomainService for testing. type mockDomainService struct { domains map[string][]*domain.ProjectDomain // projectID -> domains err error } func newMockDomainService() *mockDomainService { return &mockDomainService{ domains: make(map[string][]*domain.ProjectDomain), } } func (m *mockDomainService) ListDomains(ctx context.Context, projectID string) ([]*domain.ProjectDomain, error) { if m.err != nil { return nil, m.err } return m.domains[projectID], nil } func (m *mockDomainService) AddDomain(ctx context.Context, req DomainAddRequest) (*domain.ProjectDomain, error) { if m.err != nil { return nil, m.err } pd := &domain.ProjectDomain{ ID: int64(len(m.domains[req.ProjectID]) + 1), ProjectID: req.ProjectID, Domain: req.Domain, Type: req.Type, DNSRecordType: req.RecordType, Verified: true, } m.domains[req.ProjectID] = append(m.domains[req.ProjectID], pd) return pd, nil } func (m *mockDomainService) RemoveDomain(ctx context.Context, projectID, fqdn string) error { if m.err != nil { return m.err } domains := m.domains[projectID] for i, d := range domains { if d.Domain == fqdn { // Check if it's the primary auto domain if d.Type == domain.DomainTypePrimaryAuto { return domain.ErrDomainNotFound // Mimic primary domain protection } m.domains[projectID] = append(domains[:i], domains[i+1:]...) return nil } } return domain.ErrDomainNotFound } func setupInfraDomainHandler() (*InfrastructureHandler, *mockDomainService, chi.Router) { domainSvc := newMockDomainService() h := NewInfrastructureHandler(nil, nil, nil, nil, nil, domainSvc, InfrastructureConfig{ DefaultGitOwner: "threesix", DefaultDomain: "threesix.ai", ClusterIP: "208.122.204.172", }) r := chi.NewRouter() r.Use(testAdminAuth) h.Mount(r) return h, domainSvc, r } func TestInfrastructureHandler_ListDomains(t *testing.T) { t.Run("returns domains from database", func(t *testing.T) { _, domainSvc, router := setupInfraDomainHandler() // Add domains to mock service domainSvc.domains["landing"] = []*domain.ProjectDomain{ {ID: 1, ProjectID: "landing", Domain: "abc12345.threesix.ai", Type: domain.DomainTypePrimaryAuto, DNSRecordType: "A", Verified: true}, {ID: 2, ProjectID: "landing", Domain: "landing.threesix.ai", Type: domain.DomainTypePrimaryCustom, DNSRecordType: "A", Verified: true}, } 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 != 2 { t.Errorf("total = %d, want 2", total) } }) t.Run("domain service not configured", func(t *testing.T) { h := NewInfrastructureHandler(nil, nil, nil, nil, nil, nil, InfrastructureConfig{ DefaultGitOwner: "threesix", DefaultDomain: "threesix.ai", ClusterIP: "208.122.204.172", }) r := chi.NewRouter() r.Use(testAdminAuth) 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 domain alias", func(t *testing.T) { _, domainSvc, router := setupInfraDomainHandler() 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(domainSvc.domains["landing"]) != 1 { t.Errorf("domains = %d, want 1", len(domainSvc.domains["landing"])) } }) t.Run("add primary custom domain", func(t *testing.T) { _, domainSvc, router := setupInfraDomainHandler() body, _ := json.Marshal(DomainAliasRequest{ Domain: "mysite.threesix.ai", DomainType: "primary_custom", }) 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(domainSvc.domains["landing"]) != 1 { t.Errorf("domains = %d, want 1", len(domainSvc.domains["landing"])) } if domainSvc.domains["landing"][0].Type != domain.DomainTypePrimaryCustom { t.Errorf("type = %s, want primary_custom", domainSvc.domains["landing"][0].Type) } }) t.Run("invalid type", func(t *testing.T) { _, _, router := setupInfraDomainHandler() 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 := setupInfraDomainHandler() 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("domain service not configured", func(t *testing.T) { h := NewInfrastructureHandler(nil, nil, nil, nil, nil, nil, InfrastructureConfig{ DefaultGitOwner: "threesix", DefaultDomain: "threesix.ai", ClusterIP: "208.122.204.172", }) r := chi.NewRouter() r.Use(testAdminAuth) 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) { _, domainSvc, router := setupInfraDomainHandler() domainSvc.domains["landing"] = []*domain.ProjectDomain{ {ID: 1, ProjectID: "landing", Domain: "www.threesix.ai", Type: domain.DomainTypeAlias, DNSRecordType: "A"}, } 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()) } if len(domainSvc.domains["landing"]) != 0 { t.Errorf("domains = %d, want 0", len(domainSvc.domains["landing"])) } }) t.Run("not found", func(t *testing.T) { _, _, router := setupInfraDomainHandler() 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()) } }) }