- 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>
328 lines
8.8 KiB
Go
328 lines
8.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
"github.com/orchard9/rdev/internal/port"
|
|
)
|
|
|
|
// mockCIProvider implements port.CIProvider for testing.
|
|
type mockCIProvider struct {
|
|
pipelines map[string][]*domain.CIPipeline
|
|
err error
|
|
}
|
|
|
|
func newMockCIProvider() *mockCIProvider {
|
|
return &mockCIProvider{pipelines: make(map[string][]*domain.CIPipeline)}
|
|
}
|
|
|
|
func (m *mockCIProvider) ActivateRepo(context.Context, string, string, string) (*domain.CIRepo, error) {
|
|
return nil, m.err
|
|
}
|
|
func (m *mockCIProvider) DeactivateRepo(context.Context, string, string) error {
|
|
return m.err
|
|
}
|
|
func (m *mockCIProvider) GetRepo(context.Context, string, string) (*domain.CIRepo, error) {
|
|
return nil, m.err
|
|
}
|
|
func (m *mockCIProvider) ListRepos(context.Context) ([]*domain.CIRepo, error) {
|
|
return nil, m.err
|
|
}
|
|
func (m *mockCIProvider) AddSecret(context.Context, string, string, domain.CISecret) error {
|
|
return m.err
|
|
}
|
|
func (m *mockCIProvider) DeleteSecret(context.Context, string, string, string) error {
|
|
return m.err
|
|
}
|
|
|
|
func (m *mockCIProvider) ListPipelines(_ context.Context, owner, repo string) ([]*domain.CIPipeline, error) {
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
key := owner + "/" + repo
|
|
p, ok := m.pipelines[key]
|
|
if !ok {
|
|
return nil, fmt.Errorf("repo not found: %s", key)
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func (m *mockCIProvider) GetPipeline(_ context.Context, owner, repo string, number int64) (*domain.CIPipeline, error) {
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
key := owner + "/" + repo
|
|
for _, p := range m.pipelines[key] {
|
|
if p.Number == number {
|
|
return p, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("pipeline %d not found", number)
|
|
}
|
|
|
|
func (m *mockCIProvider) TriggerBuild(_ context.Context, owner, repo, branch string) (int64, error) {
|
|
if m.err != nil {
|
|
return 0, m.err
|
|
}
|
|
return 1, nil
|
|
}
|
|
|
|
func (m *mockCIProvider) GetPipelineSteps(_ context.Context, owner, repo string, number int64) (*domain.CIPipelineSteps, error) {
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
key := owner + "/" + repo
|
|
// Check if pipeline exists
|
|
for _, p := range m.pipelines[key] {
|
|
if p.Number == number {
|
|
return &domain.CIPipelineSteps{
|
|
PipelineNumber: number,
|
|
URL: fmt.Sprintf("https://ci.example.com/%s/%d", key, number),
|
|
Steps: []domain.CIPipelineStep{
|
|
{ID: 1, Name: "clone", Status: "success", Duration: 5},
|
|
{ID: 2, Name: "build", Status: "success", Duration: 30},
|
|
{ID: 3, Name: "test", Status: "success", Duration: 15},
|
|
},
|
|
}, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("pipeline %d not found", number)
|
|
}
|
|
|
|
func setupInfraHandlerWithCI(ci port.CIProvider) chi.Router {
|
|
h := NewInfrastructureHandler(nil, nil, nil, nil, ci, nil, InfrastructureConfig{
|
|
DefaultGitOwner: "threesix",
|
|
DefaultDomain: "threesix.ai",
|
|
})
|
|
r := chi.NewRouter()
|
|
r.Use(testAdminAuth)
|
|
h.Mount(r)
|
|
return r
|
|
}
|
|
|
|
func TestInfrastructureHandler_ListPipelines(t *testing.T) {
|
|
t.Run("success", func(t *testing.T) {
|
|
ci := newMockCIProvider()
|
|
ci.pipelines["threesix/myapp"] = []*domain.CIPipeline{
|
|
{
|
|
ID: 100,
|
|
Number: 1,
|
|
Status: "success",
|
|
Event: "push",
|
|
Branch: "main",
|
|
Commit: "abc123",
|
|
Message: "initial commit",
|
|
Author: "dev",
|
|
Started: time.Date(2025, 1, 15, 10, 0, 0, 0, time.UTC),
|
|
},
|
|
{
|
|
ID: 101,
|
|
Number: 2,
|
|
Status: "running",
|
|
Event: "push",
|
|
Branch: "feature",
|
|
Commit: "def456",
|
|
Author: "dev",
|
|
},
|
|
}
|
|
|
|
router := setupInfraHandlerWithCI(ci)
|
|
req := httptest.NewRequest("GET", "/projects/myapp/pipelines", 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("ci not configured", func(t *testing.T) {
|
|
router := setupInfraHandlerWithCI(nil)
|
|
req := httptest.NewRequest("GET", "/projects/myapp/pipelines", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusInternalServerError {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusInternalServerError)
|
|
}
|
|
})
|
|
|
|
t.Run("repo not found", func(t *testing.T) {
|
|
ci := newMockCIProvider()
|
|
router := setupInfraHandlerWithCI(ci)
|
|
|
|
req := httptest.NewRequest("GET", "/projects/missing/pipelines", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid project id", func(t *testing.T) {
|
|
ci := newMockCIProvider()
|
|
router := setupInfraHandlerWithCI(ci)
|
|
|
|
req := httptest.NewRequest("GET", "/projects/INVALID!/pipelines", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestInfrastructureHandler_GetPipeline(t *testing.T) {
|
|
t.Run("success", func(t *testing.T) {
|
|
ci := newMockCIProvider()
|
|
ci.pipelines["threesix/myapp"] = []*domain.CIPipeline{
|
|
{
|
|
ID: 100,
|
|
Number: 5,
|
|
Status: "success",
|
|
Event: "push",
|
|
Branch: "main",
|
|
Commit: "abc123",
|
|
Message: "fix bug",
|
|
Author: "dev",
|
|
Started: time.Date(2025, 1, 15, 10, 0, 0, 0, time.UTC),
|
|
Finished: time.Date(2025, 1, 15, 10, 5, 0, 0, time.UTC),
|
|
},
|
|
}
|
|
|
|
router := setupInfraHandlerWithCI(ci)
|
|
req := httptest.NewRequest("GET", "/projects/myapp/pipelines/5", 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("ci not configured", func(t *testing.T) {
|
|
router := setupInfraHandlerWithCI(nil)
|
|
req := httptest.NewRequest("GET", "/projects/myapp/pipelines/1", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusInternalServerError {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusInternalServerError)
|
|
}
|
|
})
|
|
|
|
t.Run("pipeline not found", func(t *testing.T) {
|
|
ci := newMockCIProvider()
|
|
ci.pipelines["threesix/myapp"] = []*domain.CIPipeline{}
|
|
router := setupInfraHandlerWithCI(ci)
|
|
|
|
req := httptest.NewRequest("GET", "/projects/myapp/pipelines/999", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid pipeline number", func(t *testing.T) {
|
|
ci := newMockCIProvider()
|
|
router := setupInfraHandlerWithCI(ci)
|
|
|
|
req := httptest.NewRequest("GET", "/projects/myapp/pipelines/notanumber", 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("invalid project id", func(t *testing.T) {
|
|
ci := newMockCIProvider()
|
|
router := setupInfraHandlerWithCI(ci)
|
|
|
|
req := httptest.NewRequest("GET", "/projects/INVALID!/pipelines/1", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestInfrastructureHandler_GetPipelineSteps(t *testing.T) {
|
|
t.Run("success", func(t *testing.T) {
|
|
ci := newMockCIProvider()
|
|
ci.pipelines["threesix/myapp"] = []*domain.CIPipeline{
|
|
{
|
|
ID: 100,
|
|
Number: 5,
|
|
Status: "failure",
|
|
},
|
|
}
|
|
|
|
router := setupInfraHandlerWithCI(ci)
|
|
req := httptest.NewRequest("GET", "/projects/myapp/pipelines/5/steps", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d, body: %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("ci not configured", func(t *testing.T) {
|
|
router := setupInfraHandlerWithCI(nil)
|
|
req := httptest.NewRequest("GET", "/projects/myapp/pipelines/1/steps", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusInternalServerError {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusInternalServerError)
|
|
}
|
|
})
|
|
|
|
t.Run("pipeline not found", func(t *testing.T) {
|
|
ci := newMockCIProvider()
|
|
ci.pipelines["threesix/myapp"] = []*domain.CIPipeline{}
|
|
router := setupInfraHandlerWithCI(ci)
|
|
|
|
req := httptest.NewRequest("GET", "/projects/myapp/pipelines/999/steps", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFormatTime(t *testing.T) {
|
|
t.Run("non-zero time", func(t *testing.T) {
|
|
ts := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)
|
|
got := formatTime(ts)
|
|
want := "2025-01-15T10:30:00Z"
|
|
if got != want {
|
|
t.Errorf("formatTime() = %q, want %q", got, want)
|
|
}
|
|
})
|
|
|
|
t.Run("zero time", func(t *testing.T) {
|
|
got := formatTime(time.Time{})
|
|
if got != "" {
|
|
t.Errorf("formatTime(zero) = %q, want empty string", got)
|
|
}
|
|
})
|
|
}
|