- Add auth.RequireScope() to all handler routes for proper authorization - Add SDLC OpenAPI endpoint documentation (state, features, tasks, branches, merge, archive, orchestrator) - Add SDLC documentation guides (getting-started, cli-reference, api-reference, command-catalog) - Add artifact_test.go for SDLC artifact coverage - Add CLAUDE.md rules: auth scopes requirement, error wrapping with %w - Fix error wrapping to use %w instead of %v throughout codebase - Improve CLI merge command with conflict detection and resolution - Fix handler tests to include auth middleware for RequireScope - Add cookbook tree runner scripts for automated testing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
229 lines
6.6 KiB
Go
229 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()
|
|
r.Use(testAdminAuth)
|
|
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()
|
|
r.Use(testAdminAuth)
|
|
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()
|
|
r.Use(testAdminAuth)
|
|
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)
|
|
}
|
|
})
|
|
}
|