Files
Agent-System/internal/server/phase5_acceptance_pg_test.go
T
weichenandCursor c767a99807 fix(authz): close project-context ABAC gap and repair CI PG fixtures
enforceChatProjectAccess before allowAnyStageAccess on GET /context/project
so activated projects are no longer readable by arbitrary authenticated users.

Align failing gate fixtures with official workflow keys, isolated TEST_DATABASE_URL,
unique project names (00057), duty membership, and in-memory bizdev-cli envs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 18:12:26 +08:00

192 lines
7.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package server_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"chat-agent-system/internal/db"
"chat-agent-system/internal/projectinstance"
"chat-agent-system/internal/projectworkflow"
"chat-agent-system/internal/workflowdef"
)
func TestPhase5_MigrationVersionGate_PG(t *testing.T) {
pool := openPGPoolWithMigrations(t)
if pool == nil {
return
}
defer pool.Close()
ctx := context.Background()
migDir := db.ResolveMigrationsDir()
expected, err := db.ExpectedMigrationCount(migDir)
if err != nil {
t.Fatalf("count migrations: %v", err)
}
current, err := db.CurrentVersion(ctx, pool)
if err != nil {
t.Fatalf("current version: %v", err)
}
if int(current) < expected {
t.Fatalf("schema version %d behind expected %d migration files", current, expected)
}
// 重复迁移应幂等(模拟备份恢复后再 Up)
if err := db.RunMigrations(ctx, pool, migDir); err != nil {
t.Fatalf("re-run migrations: %v", err)
}
current2, _ := db.CurrentVersion(ctx, pool)
if current2 != current {
t.Fatalf("migration not idempotent: %d -> %d", current, current2)
}
}
func TestPhase5_AuthzNegativeMatrix_PG(t *testing.T) {
pool := openPGPoolWithMigrations(t)
if pool == nil {
return
}
defer pool.Close()
handler, _ := setupPGGovernanceServer(t, pool)
creatorToken := loginTestUser(t, handler, "zhanggong", "123456")
outsiderToken := loginTestUser(t, handler, "lizhuguan", "123456")
createBody, _ := json.Marshal(projectworkflow.CreateProjectInput{Name: "Phase5 Authz Matrix Test"})
reqCreate := httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(createBody))
withBearerAuth(reqCreate, creatorToken)
recCreate := httptest.NewRecorder()
handler.ServeHTTP(recCreate, reqCreate)
if recCreate.Code != http.StatusCreated {
t.Fatalf("create project: %d", recCreate.Code)
}
var proj projectworkflow.Project
_ = json.Unmarshal(recCreate.Body.Bytes(), &proj)
t.Cleanup(func() { cleanupProject(t, pool, proj.ID) })
// 访客无权读项目设置(requireProjectAccess)
reqSettings := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/settings", proj.ID), nil)
withBearerAuth(reqSettings, outsiderToken)
recSettings := httptest.NewRecorder()
handler.ServeHTTP(recSettings, reqSettings)
if recSettings.Code != http.StatusForbidden {
t.Fatalf("outsider settings expected 403, got %d", recSettings.Code)
}
// 非成员无权保存工作流规划
saveBody, _ := json.Marshal(projectinstance.SavePlanInput{
Plan: projectinstance.WorkflowPlan{WorkflowDefinitionKey: "medical_device_development", WorkflowVersion: 1},
})
reqSave := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/projects/%d/workflow-plan", proj.ID), bytes.NewReader(saveBody))
withBearerAuth(reqSave, outsiderToken)
recSave := httptest.NewRecorder()
handler.ServeHTTP(recSave, reqSave)
if recSave.Code != http.StatusForbidden {
t.Fatalf("outsider save plan expected 403, got %d", recSave.Code)
}
// 未认证请求
reqAnon := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/settings", proj.ID), nil)
recAnon := httptest.NewRecorder()
handler.ServeHTTP(recAnon, reqAnon)
if recAnon.Code != http.StatusUnauthorized {
t.Fatalf("anonymous settings expected 401, got %d", recAnon.Code)
}
}
func TestPhase5_ActivateIdempotencyConflict_PG(t *testing.T) {
pool := openPGPoolWithMigrations(t)
if pool == nil {
return
}
defer pool.Close()
handler, _ := setupPGGovernanceServer(t, pool)
creatorToken := loginTestUser(t, handler, "zhanggong", "123456")
wfKey := workflowdef.OfficialKeyMain
seedOfficialWorkflowManifest(t, pool, wfKey)
uniqueName := "Phase5 Idempotency Test " + time.Now().Format("150405.000000")
createBody, _ := json.Marshal(projectworkflow.CreateProjectInput{Name: uniqueName})
reqCreate := httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(createBody))
withBearerAuth(reqCreate, creatorToken)
recCreate := httptest.NewRecorder()
handler.ServeHTTP(recCreate, reqCreate)
if recCreate.Code != http.StatusOK && recCreate.Code != http.StatusCreated {
t.Fatalf("create project: %d %s", recCreate.Code, recCreate.Body.String())
}
var proj projectworkflow.Project
_ = json.Unmarshal(recCreate.Body.Bytes(), &proj)
t.Cleanup(func() { cleanupProject(t, pool, proj.ID) })
creatorID := fetchAuthUserID(t, handler, creatorToken)
reviewerID := fetchAuthUserID(t, handler, loginTestUser(t, handler, "lizhuguan", "123456"))
approverID := fetchAuthUserID(t, handler, loginTestUser(t, handler, "wangzong", "123456"))
ensureProjectMembers(t, pool, proj.ID, map[int64]string{
creatorID: "owner",
reviewerID: "member",
approverID: "member",
})
reqDefault := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/workflow-plan/default?workflow_key=%s&version=0", proj.ID, wfKey), nil)
withBearerAuth(reqDefault, creatorToken)
recDefault := httptest.NewRecorder()
handler.ServeHTTP(recDefault, reqDefault)
if recDefault.Code != http.StatusOK {
t.Fatalf("default plan: %d %s", recDefault.Code, recDefault.Body.String())
}
var plan projectinstance.WorkflowPlan
_ = json.Unmarshal(recDefault.Body.Bytes(), &plan)
for i := range plan.Stages {
for j := range plan.Stages[i].DocTasks {
plan.Stages[i].DocTasks[j].Duties = projectinstance.DocDuty{
AuthorUserID: creatorID, ReviewerUserID: reviewerID, ApproverUserID: approverID,
}
}
}
saveBody, _ := json.Marshal(projectinstance.SavePlanInput{Plan: plan, ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1})
reqSave := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/projects/%d/workflow-plan", proj.ID), bytes.NewReader(saveBody))
withBearerAuth(reqSave, creatorToken)
recSave := httptest.NewRecorder()
handler.ServeHTTP(recSave, reqSave)
if recSave.Code != http.StatusOK {
t.Fatalf("save plan: %d %s", recSave.Code, recSave.Body.String())
}
reqPreview := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/projects/%d/activation-preview", proj.ID), nil)
withBearerAuth(reqPreview, creatorToken)
recPreview := httptest.NewRecorder()
handler.ServeHTTP(recPreview, reqPreview)
if recPreview.Code != http.StatusOK {
t.Fatalf("activation preview: %d %s", recPreview.Code, recPreview.Body.String())
}
var preview projectinstance.ActivationPreview
_ = json.Unmarshal(recPreview.Body.Bytes(), &preview)
activateBody, _ := json.Marshal(projectinstance.ActivateInput{
ConfirmationToken: preview.ConfirmationToken, ExpectedProjectVersion: preview.ExpectedProjectVersion,
ExpectedPlanRevision: preview.ExpectedPlanRevision,
})
reqActivate := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/projects/%d/workflow-instances/activate", proj.ID), bytes.NewReader(activateBody))
withBearerAuth(reqActivate, creatorToken)
recActivate := httptest.NewRecorder()
handler.ServeHTTP(recActivate, reqActivate)
if recActivate.Code != http.StatusOK {
t.Fatalf("first activate: %d %s", recActivate.Code, recActivate.Body.String())
}
// 二次激活应冲突
reqActivate2 := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/projects/%d/workflow-instances/activate", proj.ID), bytes.NewReader(activateBody))
withBearerAuth(reqActivate2, creatorToken)
recActivate2 := httptest.NewRecorder()
handler.ServeHTTP(recActivate2, reqActivate2)
if recActivate2.Code != http.StatusConflict {
t.Fatalf("second activate expected 409, got %d %s", recActivate2.Code, recActivate2.Body.String())
}
}