You've already forked agentic-coding-workflow
117 lines
4.4 KiB
Go
117 lines
4.4 KiB
Go
package project
|
|
|
|
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/transport/http/middleware"
|
|
)
|
|
|
|
// fakeResolver 把固定 token "tok-<uid>" 解码为 uuid。
|
|
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
|
|
}
|
|
|
|
// fakeAdmin 始终返回 false(owner 路径足够覆盖大多数测试)。
|
|
type fakeAdmin struct{}
|
|
|
|
func (fakeAdmin) IsAdmin(_ context.Context, _ uuid.UUID) (bool, error) { return false, nil }
|
|
|
|
func mountFullHandler(t *testing.T, ownerToken string, ownerID uuid.UUID) (*fakeRepo, http.Handler) {
|
|
t.Helper()
|
|
repo := newFakeRepo()
|
|
pSvc := NewProjectService(repo, nil)
|
|
rSvc := NewRequirementService(repo, nil)
|
|
iSvc := NewIssueService(repo, nil)
|
|
resolver := &fakeResolver{valid: map[string]uuid.UUID{ownerToken: ownerID}}
|
|
h := NewHandler(pSvc, rSvc, iSvc, resolver, fakeAdmin{})
|
|
r := chi.NewRouter()
|
|
r.Use(middleware.RequestID)
|
|
h.Mount(r)
|
|
return repo, r
|
|
}
|
|
|
|
func doJSON(t *testing.T, h http.Handler, method, path, token string, body any) *http.Response {
|
|
t.Helper()
|
|
var b []byte
|
|
if body != nil {
|
|
b, _ = json.Marshal(body)
|
|
}
|
|
req := httptest.NewRequest(method, path, bytes.NewReader(b))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
return rec.Result()
|
|
}
|
|
|
|
// checkStatus calls doJSON, asserts the HTTP status, and closes the response body.
|
|
func checkStatus(t *testing.T, h http.Handler, method, path, token string, body any, wantStatus int) {
|
|
t.Helper()
|
|
resp := doJSON(t, h, method, path, token, body)
|
|
t.Cleanup(func() { _ = resp.Body.Close() })
|
|
require.Equal(t, wantStatus, resp.StatusCode)
|
|
}
|
|
|
|
func TestHandler_FullClosedLoop(t *testing.T) {
|
|
owner := uuid.New()
|
|
tok := "tok-1"
|
|
_, h := mountFullHandler(t, tok, owner)
|
|
|
|
// 1. create project
|
|
checkStatus(t, h, "POST", "/api/v1/projects/", tok, map[string]any{"slug": "demo", "name": "Demo"}, http.StatusCreated)
|
|
|
|
// 2. create 2 requirements
|
|
checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements", tok, map[string]any{"title": "R1"}, http.StatusCreated)
|
|
checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements", tok, map[string]any{"title": "R2"}, http.StatusCreated)
|
|
|
|
// 3. phase change
|
|
checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements/1/phase", tok, map[string]any{"phase": "auditing"}, http.StatusOK)
|
|
|
|
// 4. create issue attached + freelance
|
|
checkStatus(t, h, "POST", "/api/v1/projects/demo/issues", tok, map[string]any{"title": "I1", "requirement_number": 1}, http.StatusCreated)
|
|
checkStatus(t, h, "POST", "/api/v1/projects/demo/issues", tok, map[string]any{"title": "Free"}, http.StatusCreated)
|
|
|
|
// 5. close requirement → phase change must fail with 409 / requirement_closed
|
|
checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements/1/close", tok, nil, http.StatusNoContent)
|
|
checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements/1/phase", tok, map[string]any{"phase": "done"}, http.StatusConflict)
|
|
|
|
// 6. archive project → all writes blocked
|
|
checkStatus(t, h, "POST", "/api/v1/projects/demo/archive", tok, nil, http.StatusNoContent)
|
|
checkStatus(t, h, "POST", "/api/v1/projects/demo/requirements", tok, map[string]any{"title": "X"}, http.StatusConflict)
|
|
}
|
|
|
|
func TestHandler_Unauthenticated_Returns401(t *testing.T) {
|
|
owner := uuid.New()
|
|
_, h := mountFullHandler(t, "tok", owner)
|
|
resp := doJSON(t, h, "GET", "/api/v1/projects/", "", nil)
|
|
t.Cleanup(func() { _ = resp.Body.Close() })
|
|
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
}
|
|
|
|
func TestHandler_SlugTaken_Returns409(t *testing.T) {
|
|
owner := uuid.New()
|
|
tok := "tok"
|
|
_, h := mountFullHandler(t, tok, owner)
|
|
checkStatus(t, h, "POST", "/api/v1/projects/", tok, map[string]any{"slug": "demo", "name": "A"}, http.StatusCreated)
|
|
resp := doJSON(t, h, "POST", "/api/v1/projects/", tok, map[string]any{"slug": "demo", "name": "B"})
|
|
t.Cleanup(func() { _ = resp.Body.Close() })
|
|
require.Equal(t, http.StatusConflict, resp.StatusCode)
|
|
var body map[string]any
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
|
|
require.Equal(t, "project_slug_taken", body["code"])
|
|
}
|