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 setupInfraHandlerWithCI(ci port.CIProvider) chi.Router { h := NewInfrastructureHandler(nil, nil, nil, nil, ci, nil, 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) } }) }