- Add ListPipelines/GetPipeline to CIProvider port with Woodpecker adapter
- Add DNS alias endpoints: GET/POST/DELETE /projects/{id}/domains
- Implement worker executor daemon, build executor, and git operations
- Add build service, worker service, and build audit tracking
- Add worker registry with PostgreSQL adapter and migration
- Add multi-provider code agent interface (Claude Code + OpenCode)
- Add create-and-build combo endpoint
- Update landing-page cookbook to reflect all gaps closed
- Fix tech debt: unified validation, auth scopes, error wrapping, slog patterns
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
247 lines
7.4 KiB
Go
247 lines
7.4 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|