- Add pass/fail/needs-fix CLI commands to cmd/sdlc/cmd_artifact.go
- Add 3 new methods to SDLCExecutor interface in internal/port
- Implement methods in kubernetes adapter
- Add service methods to SDLCService
- Add HTTP handlers for POST .../artifacts/{type}/pass|fail|needs-fix
- Update 6 skeleton commands to evaluate and set artifact status
- Update test mocks
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
704 lines
23 KiB
Go
704 lines
23 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
"github.com/orchard9/rdev/internal/port"
|
|
"github.com/orchard9/rdev/internal/sdlc"
|
|
"github.com/orchard9/rdev/internal/service"
|
|
)
|
|
|
|
// testSDLCExecutor implements port.SDLCExecutor for handler tests.
|
|
type testSDLCExecutor struct {
|
|
state *sdlc.State
|
|
classification *sdlc.Classification
|
|
features []*sdlc.Feature
|
|
feature *sdlc.Feature
|
|
artifacts map[sdlc.ArtifactType]*sdlc.Artifact
|
|
tasks []sdlc.Task
|
|
task *sdlc.Task
|
|
blocked []port.BlockedInfo
|
|
ready []port.ReadyInfo
|
|
approval []port.ApprovalInfo
|
|
err error
|
|
}
|
|
|
|
func (m *testSDLCExecutor) GetState(_ context.Context, _ string) (*sdlc.State, error) {
|
|
return m.state, m.err
|
|
}
|
|
func (m *testSDLCExecutor) GetNext(_ context.Context, _, _ string) (*sdlc.Classification, error) {
|
|
return m.classification, m.err
|
|
}
|
|
func (m *testSDLCExecutor) ListFeatures(_ context.Context, _ string) ([]*sdlc.Feature, error) {
|
|
return m.features, m.err
|
|
}
|
|
func (m *testSDLCExecutor) GetFeature(_ context.Context, _, _ string) (*sdlc.Feature, error) {
|
|
return m.feature, m.err
|
|
}
|
|
func (m *testSDLCExecutor) CreateFeature(_ context.Context, _, slug, title string) (*sdlc.Feature, error) {
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
return &sdlc.Feature{Slug: slug, Title: title, Phase: sdlc.PhaseDraft}, nil
|
|
}
|
|
func (m *testSDLCExecutor) TransitionFeature(_ context.Context, _, _ string, _ sdlc.FeaturePhase) error {
|
|
return m.err
|
|
}
|
|
func (m *testSDLCExecutor) BlockFeature(_ context.Context, _, _, _ string) error { return m.err }
|
|
func (m *testSDLCExecutor) UnblockFeature(_ context.Context, _, _ string) error { return m.err }
|
|
func (m *testSDLCExecutor) DeleteFeature(_ context.Context, _, _ string) error { return m.err }
|
|
func (m *testSDLCExecutor) GetArtifactStatus(_ context.Context, _, _ string) (map[sdlc.ArtifactType]*sdlc.Artifact, error) {
|
|
return m.artifacts, m.err
|
|
}
|
|
func (m *testSDLCExecutor) ApproveArtifact(_ context.Context, _, _ string, _ sdlc.ArtifactType) error {
|
|
return m.err
|
|
}
|
|
func (m *testSDLCExecutor) RejectArtifact(_ context.Context, _, _ string, _ sdlc.ArtifactType) error {
|
|
return m.err
|
|
}
|
|
func (m *testSDLCExecutor) PassArtifact(_ context.Context, _, _ string, _ sdlc.ArtifactType) error {
|
|
return m.err
|
|
}
|
|
func (m *testSDLCExecutor) FailArtifact(_ context.Context, _, _ string, _ sdlc.ArtifactType) error {
|
|
return m.err
|
|
}
|
|
func (m *testSDLCExecutor) NeedsFixArtifact(_ context.Context, _, _ string, _ sdlc.ArtifactType) error {
|
|
return m.err
|
|
}
|
|
func (m *testSDLCExecutor) ListTasks(_ context.Context, _, _ string) ([]sdlc.Task, error) {
|
|
return m.tasks, m.err
|
|
}
|
|
func (m *testSDLCExecutor) AddTask(_ context.Context, _, _, title string) (*sdlc.Task, error) {
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
if m.task != nil {
|
|
return m.task, nil
|
|
}
|
|
return &sdlc.Task{ID: "task-001", Title: title, Status: sdlc.TaskPending}, nil
|
|
}
|
|
func (m *testSDLCExecutor) StartTask(_ context.Context, _, _, _ string) error { return m.err }
|
|
func (m *testSDLCExecutor) CompleteTask(_ context.Context, _, _, _ string) error { return m.err }
|
|
func (m *testSDLCExecutor) BlockTask(_ context.Context, _, _, _ string) error { return m.err }
|
|
func (m *testSDLCExecutor) QueryBlocked(_ context.Context, _ string) ([]port.BlockedInfo, error) {
|
|
return m.blocked, m.err
|
|
}
|
|
func (m *testSDLCExecutor) QueryReady(_ context.Context, _ string) ([]port.ReadyInfo, error) {
|
|
return m.ready, m.err
|
|
}
|
|
func (m *testSDLCExecutor) QueryNeedsApproval(_ context.Context, _ string) ([]port.ApprovalInfo, error) {
|
|
return m.approval, m.err
|
|
}
|
|
func (m *testSDLCExecutor) CreateBranch(_ context.Context, _, slug string) (*sdlc.BranchManifest, error) {
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
return &sdlc.BranchManifest{Name: "feature/" + slug, Feature: slug}, nil
|
|
}
|
|
func (m *testSDLCExecutor) GetBranchStatus(_ context.Context, _, slug string) (*port.BranchStatus, error) {
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
return &port.BranchStatus{
|
|
Branch: &sdlc.BranchManifest{Name: "feature/" + slug, Feature: slug},
|
|
Checklist: nil,
|
|
Ready: true,
|
|
}, nil
|
|
}
|
|
func (m *testSDLCExecutor) SyncBranch(_ context.Context, _, _ string) error { return m.err }
|
|
func (m *testSDLCExecutor) MergeFeature(_ context.Context, _, _, _ string) error { return m.err }
|
|
func (m *testSDLCExecutor) ArchiveFeature(_ context.Context, _, _ string) error { return m.err }
|
|
|
|
// testSDLCProjectRepo implements port.ProjectRepository for handler tests.
|
|
type testSDLCProjectRepo struct {
|
|
project *domain.Project
|
|
}
|
|
|
|
func (m *testSDLCProjectRepo) Get(_ context.Context, _ domain.ProjectID) (*domain.Project, error) {
|
|
if m.project == nil {
|
|
return nil, domain.ErrProjectNotFound
|
|
}
|
|
return m.project, nil
|
|
}
|
|
func (m *testSDLCProjectRepo) List(_ context.Context) ([]domain.Project, error) { return nil, nil }
|
|
func (m *testSDLCProjectRepo) Exists(_ context.Context, _ domain.ProjectID) (bool, error) {
|
|
return m.project != nil, nil
|
|
}
|
|
func (m *testSDLCProjectRepo) Register(_ context.Context, _ *domain.Project) error { return nil }
|
|
func (m *testSDLCProjectRepo) Unregister(_ context.Context, _ domain.ProjectID) error { return nil }
|
|
func (m *testSDLCProjectRepo) RefreshStatus(_ context.Context) error { return nil }
|
|
|
|
func setupSDLCHandler(exec *testSDLCExecutor) (*SDLCHandler, *chi.Mux) {
|
|
repo := &testSDLCProjectRepo{
|
|
project: &domain.Project{ID: "test-project", PodName: "test-pod"},
|
|
}
|
|
svc := service.NewSDLCService(exec, repo, service.SDLCServiceConfig{})
|
|
handler := NewSDLCHandler(svc, nil)
|
|
r := chi.NewRouter()
|
|
r.Use(testAdminAuth)
|
|
handler.Mount(r)
|
|
return handler, r
|
|
}
|
|
|
|
func TestSDLCHandler_GetState(t *testing.T) {
|
|
exec := &testSDLCExecutor{
|
|
state: &sdlc.State{Version: 1},
|
|
}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/state", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_GetState_NotInitialized(t *testing.T) {
|
|
exec := &testSDLCExecutor{
|
|
err: sdlc.ErrNotInitialized,
|
|
}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/state", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_GetState_ProjectNotFound(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
repo := &testSDLCProjectRepo{project: nil}
|
|
svc := service.NewSDLCService(exec, repo, service.SDLCServiceConfig{})
|
|
handler := NewSDLCHandler(svc, nil)
|
|
r := chi.NewRouter()
|
|
r.Use(testAdminAuth)
|
|
handler.Mount(r)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/nonexistent/sdlc/state", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_CreateFeature(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
body, _ := json.Marshal(CreateFeatureRequest{Slug: "auth-flow", Title: "Auth Flow"})
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Errorf("expected status 201, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_CreateFeature_MissingFields(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
body, _ := json.Marshal(CreateFeatureRequest{})
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_CreateFeature_AlreadyExists(t *testing.T) {
|
|
exec := &testSDLCExecutor{err: sdlc.ErrFeatureExists}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
body, _ := json.Marshal(CreateFeatureRequest{Slug: "auth-flow", Title: "Auth Flow"})
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_TransitionFeature(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
body, _ := json.Marshal(TransitionFeatureRequest{Phase: "specified"})
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/transition", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_TransitionFeature_InvalidPhase(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
body, _ := json.Marshal(TransitionFeatureRequest{Phase: "not-a-phase"})
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/transition", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_TransitionFeature_InvalidTransition(t *testing.T) {
|
|
exec := &testSDLCExecutor{err: sdlc.ErrInvalidTransition}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
body, _ := json.Marshal(TransitionFeatureRequest{Phase: "review"})
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/transition", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_BlockFeature(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
body, _ := json.Marshal(BlockFeatureRequest{Reason: "needs API key"})
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/block", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_BlockFeature_MissingReason(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
body, _ := json.Marshal(BlockFeatureRequest{})
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/block", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_DeleteFeature(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/projects/test-project/sdlc/features/auth-flow", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNoContent {
|
|
t.Errorf("expected status 204, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_DeleteFeature_NotFound(t *testing.T) {
|
|
exec := &testSDLCExecutor{err: sdlc.ErrFeatureNotFound}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/projects/test-project/sdlc/features/nonexistent", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_ApproveArtifact(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/artifacts/spec/approve", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_ApproveArtifact_InvalidType(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/artifacts/invalid/approve", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_AddTask(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
body, _ := json.Marshal(AddTaskRequest{Title: "Add login form"})
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/tasks", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Errorf("expected status 201, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_AddTask_MissingTitle(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
body, _ := json.Marshal(AddTaskRequest{})
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/tasks", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_QueryBlocked(t *testing.T) {
|
|
exec := &testSDLCExecutor{
|
|
blocked: []port.BlockedInfo{
|
|
{Slug: "auth", Phase: "implementation", Blockers: []string{"needs API key"}},
|
|
},
|
|
}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/query/blocked", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_InternalError(t *testing.T) {
|
|
exec := &testSDLCExecutor{err: errors.New("something unexpected")}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/state", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Errorf("expected status 500, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_GetNext(t *testing.T) {
|
|
exec := &testSDLCExecutor{
|
|
classification: &sdlc.Classification{
|
|
Feature: "auth-flow",
|
|
CurrentPhase: sdlc.PhaseDraft,
|
|
RuleMatched: "needs-spec",
|
|
Action: sdlc.ActionCreateSpec,
|
|
},
|
|
}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/next?feature=auth-flow", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_GetNext_FeatureNotFound(t *testing.T) {
|
|
exec := &testSDLCExecutor{err: sdlc.ErrFeatureNotFound}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/next?feature=nonexistent", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_ListFeatures(t *testing.T) {
|
|
exec := &testSDLCExecutor{
|
|
features: []*sdlc.Feature{
|
|
{Slug: "auth-flow", Title: "Auth Flow", Phase: sdlc.PhaseDraft},
|
|
{Slug: "payments", Title: "Payments", Phase: sdlc.PhaseImplementation},
|
|
},
|
|
}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_ListFeatures_Empty(t *testing.T) {
|
|
exec := &testSDLCExecutor{features: nil}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
// Should return empty array, not null
|
|
if w.Body.String() == "null" {
|
|
t.Error("expected empty array, not null")
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_GetFeature(t *testing.T) {
|
|
exec := &testSDLCExecutor{
|
|
feature: &sdlc.Feature{
|
|
Slug: "auth-flow",
|
|
Title: "Auth Flow",
|
|
Phase: sdlc.PhaseDraft,
|
|
},
|
|
}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features/auth-flow", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_GetFeature_NotFound(t *testing.T) {
|
|
exec := &testSDLCExecutor{err: sdlc.ErrFeatureNotFound}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features/nonexistent", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_UnblockFeature(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/unblock", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_UnblockFeature_NotFound(t *testing.T) {
|
|
exec := &testSDLCExecutor{err: sdlc.ErrFeatureNotFound}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/nonexistent/unblock", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_RejectArtifact(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/artifacts/spec/reject", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_RejectArtifact_InvalidType(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/artifacts/invalid/reject", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_GetArtifactStatus(t *testing.T) {
|
|
exec := &testSDLCExecutor{
|
|
artifacts: map[sdlc.ArtifactType]*sdlc.Artifact{
|
|
sdlc.ArtifactSpec: sdlc.NewArtifact(sdlc.ArtifactSpec),
|
|
sdlc.ArtifactDesign: sdlc.NewArtifact(sdlc.ArtifactDesign),
|
|
},
|
|
}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features/auth-flow/artifacts", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_ListTasks(t *testing.T) {
|
|
exec := &testSDLCExecutor{
|
|
tasks: []sdlc.Task{
|
|
{ID: "task-001", Title: "Setup auth", Status: sdlc.TaskPending},
|
|
{ID: "task-002", Title: "Add login", Status: sdlc.TaskInProgress},
|
|
},
|
|
}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features/auth-flow/tasks", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_StartTask(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/tasks/task-001/start", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_StartTask_NotFound(t *testing.T) {
|
|
exec := &testSDLCExecutor{err: sdlc.ErrTaskNotFound}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/tasks/nonexistent/start", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_CompleteTask(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/tasks/task-001/complete", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_BlockTask(t *testing.T) {
|
|
exec := &testSDLCExecutor{}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/tasks/task-001/block", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_QueryReady(t *testing.T) {
|
|
exec := &testSDLCExecutor{
|
|
ready: []port.ReadyInfo{
|
|
{Slug: "auth", Phase: "ready", Action: "IMPLEMENT_TASK"},
|
|
},
|
|
}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/query/ready", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSDLCHandler_QueryNeedsApproval(t *testing.T) {
|
|
exec := &testSDLCExecutor{
|
|
approval: []port.ApprovalInfo{
|
|
{Slug: "auth", Phase: "draft", Message: "spec requires approval"},
|
|
},
|
|
}
|
|
_, router := setupSDLCHandler(exec)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/query/needs-approval", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|