rdev/internal/handlers/infrastructure_test.go
jordan 8282d60c69 feat: implement composable monorepo template system with component architecture
Adds the composable monorepo template system that generates project skeletons
with pluggable components (service, worker, app-react, app-astro, cli).

Key changes:
- Monorepo skeleton templates with shared pkg/, scripts/, and git hooks
- Component templates (service, worker, app-react, app-astro, cli) with
  Dockerfiles, CI steps, and component.yaml manifests
- Component domain model with validation and dependency resolution
- Component handler endpoints for CRUD and composition
- Template provider extended with BuildComposableProject and component assembly
- Deployer extended with composable project deployment support
- Handler timeout constants (TimeoutFastLookup through TimeoutLongRunning)
- envutil package for centralized env var reads with defaults
- api.DecodeJSON helper for standardized request body decoding
- Standardized response helpers (WriteBadRequest, WriteNotFound, etc.)
- Replaced fullstack-app cookbook with composable-app cookbook
- Hardened handler timeouts, logging, and error responses across all handlers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:11:42 -07:00

226 lines
6.6 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 setupInfraHandler() (*InfrastructureHandler, *mockGitRepository, *mockDNSProvider, *mockDeployer, chi.Router) {
git := newMockGitRepository()
dns := newMockDNSProvider()
deployer := newMockDeployer()
h := NewInfrastructureHandler(git, dns, deployer, nil, nil, nil, InfrastructureConfig{
DefaultGitOwner: "threesix",
DefaultDomain: "threesix.ai",
ClusterIP: "208.122.204.172",
})
r := chi.NewRouter()
h.Mount(r)
return h, git, dns, deployer, r
}
func TestInfrastructureHandler_CreateRepo(t *testing.T) {
t.Run("success", func(t *testing.T) {
_, git, _, _, router := setupInfraHandler()
body, _ := json.Marshal(CreateRepoRequest{Description: "Test repo", Private: true})
req := httptest.NewRequest("POST", "/projects/myapp/repo", bytes.NewReader(body))
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Errorf("status = %d, want %d", rec.Code, http.StatusCreated)
}
if _, ok := git.repos["myapp"]; !ok {
t.Error("repo not created")
}
})
t.Run("invalid project id", func(t *testing.T) {
_, _, _, _, router := setupInfraHandler()
req := httptest.NewRequest("POST", "/projects/INVALID_NAME!/repo", bytes.NewReader([]byte("{}")))
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest)
}
})
t.Run("empty body allowed", func(t *testing.T) {
_, _, _, _, router := setupInfraHandler()
req := httptest.NewRequest("POST", "/projects/myapp/repo", bytes.NewReader([]byte("")))
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
// Should succeed with empty body (EOF is allowed)
if rec.Code != http.StatusCreated {
t.Errorf("status = %d, want %d", rec.Code, http.StatusCreated)
}
})
t.Run("git not configured", func(t *testing.T) {
h := NewInfrastructureHandler(nil, nil, nil, nil, nil, nil, InfrastructureConfig{})
r := chi.NewRouter()
h.Mount(r)
req := httptest.NewRequest("POST", "/projects/myapp/repo", bytes.NewReader([]byte("{}")))
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != http.StatusInternalServerError {
t.Errorf("status = %d, want %d", rec.Code, http.StatusInternalServerError)
}
})
}
func TestInfrastructureHandler_GetRepo(t *testing.T) {
t.Run("found", func(t *testing.T) {
_, git, _, _, router := setupInfraHandler()
git.repos["myapp"] = &domain.Repo{
ID: 1, Owner: "threesix", Name: "myapp", FullName: "threesix/myapp",
CloneSSH: "git@git.threesix.ai:threesix/myapp.git",
}
req := httptest.NewRequest("GET", "/projects/myapp/repo", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
}
})
t.Run("not found", func(t *testing.T) {
_, _, _, _, router := setupInfraHandler()
req := httptest.NewRequest("GET", "/projects/missing/repo", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Errorf("status = %d, want %d", rec.Code, http.StatusNotFound)
}
})
}
func TestInfrastructureHandler_DeleteRepo(t *testing.T) {
t.Run("success", func(t *testing.T) {
_, git, _, _, router := setupInfraHandler()
git.repos["myapp"] = &domain.Repo{ID: 1, Name: "myapp"}
req := httptest.NewRequest("DELETE", "/projects/myapp/repo", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
}
})
}
func TestInfrastructureHandler_Deploy(t *testing.T) {
t.Run("success", func(t *testing.T) {
_, _, _, deployer, router := setupInfraHandler()
body, _ := json.Marshal(DeployRequest{
Image: "registry.threesix.ai/myapp:latest",
Port: 8080,
Replicas: 2,
})
req := httptest.NewRequest("POST", "/projects/myapp/deploy", bytes.NewReader(body))
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Errorf("status = %d, want %d", rec.Code, http.StatusCreated)
}
if _, ok := deployer.deployments["myapp"]; !ok {
t.Error("deployment not created")
}
})
t.Run("missing image", func(t *testing.T) {
_, _, _, _, router := setupInfraHandler()
body, _ := json.Marshal(DeployRequest{Port: 8080})
req := httptest.NewRequest("POST", "/projects/myapp/deploy", 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("deployer not configured", func(t *testing.T) {
h := NewInfrastructureHandler(nil, nil, nil, nil, nil, nil, InfrastructureConfig{})
r := chi.NewRouter()
h.Mount(r)
body, _ := json.Marshal(DeployRequest{Image: "myimage:latest"})
req := httptest.NewRequest("POST", "/projects/myapp/deploy", 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_GetDeployStatus(t *testing.T) {
t.Run("found", func(t *testing.T) {
_, _, _, deployer, router := setupInfraHandler()
deployer.deployments["myapp"] = &domain.DeployStatus{
ProjectName: "myapp",
Image: "myimage:latest",
Status: domain.DeploymentStatusRunning,
Replicas: 2,
}
req := httptest.NewRequest("GET", "/projects/myapp/deploy/status", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
}
})
t.Run("not found", func(t *testing.T) {
_, _, _, _, router := setupInfraHandler()
req := httptest.NewRequest("GET", "/projects/missing/deploy/status", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Errorf("status = %d, want %d", rec.Code, http.StatusNotFound)
}
})
}
func TestInfrastructureHandler_Undeploy(t *testing.T) {
t.Run("success", func(t *testing.T) {
_, _, _, deployer, router := setupInfraHandler()
deployer.deployments["myapp"] = &domain.DeployStatus{ProjectName: "myapp"}
req := httptest.NewRequest("DELETE", "/projects/myapp/deploy", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
}
})
}