package changerequest import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/yan1h/agent-coding-workflow/internal/infra/errs" "github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware" ) // fakeSvc implements Service for handler tests. type fakeSvc struct { mergeErr error cr *ChangeRequest } func (f *fakeSvc) Create(_ context.Context, c Caller, wsID uuid.UUID, _ CreateInput) (*ChangeRequest, error) { return &ChangeRequest{ID: uuid.New(), WorkspaceID: wsID, Number: 1, State: StateOpen, ReviewVerdict: VerdictPending, CreatedBy: c.UserID}, nil } func (f *fakeSvc) Get(_ context.Context, _ Caller, id uuid.UUID) (*ChangeRequest, error) { return &ChangeRequest{ID: id, State: StateOpen}, nil } func (f *fakeSvc) GetByNumber(_ context.Context, _ Caller, _ string, n int) (*ChangeRequest, error) { return &ChangeRequest{ID: uuid.New(), Number: n}, nil } func (f *fakeSvc) ListByWorkspace(_ context.Context, _ Caller, wsID uuid.UUID) ([]*ChangeRequest, error) { return []*ChangeRequest{{ID: uuid.New(), WorkspaceID: wsID, Number: 1}}, nil } func (f *fakeSvc) Review(_ context.Context, _ Caller, id uuid.UUID, v Verdict, _ string) (*ChangeRequest, error) { return &ChangeRequest{ID: id, ReviewVerdict: v}, nil } func (f *fakeSvc) Merge(_ context.Context, _ Caller, id uuid.UUID, _ string) (*ChangeRequest, error) { if f.mergeErr != nil { return nil, f.mergeErr } return &ChangeRequest{ID: id, State: StateMerged}, nil } func (f *fakeSvc) SyncStatus(_ context.Context, _ Caller, id uuid.UUID) (*ChangeRequest, error) { return &ChangeRequest{ID: id, CIState: CISuccess}, nil } type fakeResolver struct{ valid map[string]uuid.UUID } func (f *fakeResolver) ResolveSession(_ context.Context, token string) (uuid.UUID, bool, error) { uid, ok := f.valid[token] return uid, ok, nil } type fakeAdmin struct{} func (fakeAdmin) IsAdmin(_ context.Context, _ uuid.UUID) (bool, error) { return false, nil } func newTestHandler(svc Service, tok string, uid uuid.UUID) chi.Router { resolver := &fakeResolver{valid: map[string]uuid.UUID{tok: uid}} h := NewHandler(svc, resolver, fakeAdmin{}) r := chi.NewRouter() r.Use(middleware.RequestID) h.Mount(r) return r } func TestHandler_Unauthenticated(t *testing.T) { t.Parallel() r := newTestHandler(&fakeSvc{}, "tok", uuid.New()) req := httptest.NewRequest("GET", "/api/v1/workspaces/"+uuid.NewString()+"/change-requests", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusUnauthorized, w.Code) } func TestHandler_CreateAndList(t *testing.T) { t.Parallel() uid := uuid.New() r := newTestHandler(&fakeSvc{}, "tok", uid) wsID := uuid.New() body, _ := json.Marshal(createReq{SourceBranch: "feat", Title: "T"}) req := httptest.NewRequest("POST", "/api/v1/workspaces/"+wsID.String()+"/change-requests", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer tok") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code) req = httptest.NewRequest("GET", "/api/v1/workspaces/"+wsID.String()+"/change-requests", nil) req.Header.Set("Authorization", "Bearer tok") w = httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) } func TestHandler_MergeGateReturns412(t *testing.T) { t.Parallel() svc := &fakeSvc{mergeErr: errs.New(errs.CodeFailedPrecondition, "merge blocked: review not approved")} r := newTestHandler(svc, "tok", uuid.New()) req := httptest.NewRequest("POST", "/api/v1/change-requests/"+uuid.NewString()+"/merge", nil) req.Header.Set("Authorization", "Bearer tok") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusPreconditionFailed, w.Code) } func TestHandler_MergeHappyPath(t *testing.T) { t.Parallel() r := newTestHandler(&fakeSvc{}, "tok", uuid.New()) req := httptest.NewRequest("POST", "/api/v1/change-requests/"+uuid.NewString()+"/merge", bytes.NewReader([]byte(`{"method":"squash"}`))) req.Header.Set("Authorization", "Bearer tok") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var resp changeRequestResp require.NoError(t, json.NewDecoder(w.Body).Decode(&resp)) require.Equal(t, string(StateMerged), resp.State) } func TestHandler_Review(t *testing.T) { t.Parallel() r := newTestHandler(&fakeSvc{}, "tok", uuid.New()) body, _ := json.Marshal(reviewReq{Verdict: "approved", Note: "lgtm"}) req := httptest.NewRequest("POST", "/api/v1/change-requests/"+uuid.NewString()+"/review", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer tok") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var resp changeRequestResp require.NoError(t, json.NewDecoder(w.Body).Decode(&resp)) require.Equal(t, "approved", resp.ReviewVerdict) }