- 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>
251 lines
6.6 KiB
Go
251 lines
6.6 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 setupInfraHandlerWithCI(ci port.CIProvider) chi.Router {
|
|
h := NewInfrastructureHandler(nil, nil, nil, nil, ci, InfrastructureConfig{
|
|
DefaultGitOwner: "threesix",
|
|
DefaultDomain: "threesix.ai",
|
|
})
|
|
r := chi.NewRouter()
|
|
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 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)
|
|
}
|
|
})
|
|
}
|