Root causes of the remaining 5 CI PG failures (and the hidden flaky set):
1. All PG-integration packages shared ONE database under parallel package
execution - migrations/seeds/truncates raced (nondeterministic failure
sets, locally and in CI).
Fix: RequireTestDatabaseURL/SkipIfNoTestDatabaseURL now derive a
per-package database (<base>_<pkgHash>, advisory-lock-guarded
CREATE); 9 raw-env straggler tests funnelled through the helpers;
db-package tests call it unqualified (no import cycle).
2. Approval-chain fixtures were not production-shaped: freezeReviewSnapshot
only honors head_snapshot_id, fake content hashes never froze a
snapshot, so ApproverApprove rejected with 'approved requires frozen
review_snapshot_id'. Errors were swallowed (_, _ =) hiding this.
Fix (ownership-respecting, arch-enforced):
- docrevision.SeedContentSnapshotForTest seeds the ledger tables it owns
- docgov GovernanceEngine.FreezeHeadSnapshotForTest freezes head/review
pointers on document_records (docgov-owned) and clears engine cache
- all four integration fixtures use the pair; swallowed errors checked
3. Fixed-name fixtures collide once a package shares one DB (projects
uq_projects_name_global): uniquified in projectinstance + server tests.
4. CAS/ctxmgr tests never bootstrapped migrations (relied on other packages
having migrated the shared DB): explicit RunMigrationsOnceForTest.
Verified: full parallel short suite = 75 packages, 0 failures against an
isolated throwaway PG. CI skip list removed - short tests fully required.
124 lines
4.5 KiB
Go
124 lines
4.5 KiB
Go
package server_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/projectworkflow"
|
|
)
|
|
|
|
func TestTaskRunAuthz_OutsiderForbidden_PG(t *testing.T) {
|
|
pool := openPGPoolWithMigrations(t)
|
|
if pool == nil {
|
|
return
|
|
}
|
|
defer pool.Close()
|
|
|
|
ctx := context.Background()
|
|
handler, _ := setupPGGovernanceServer(t, pool)
|
|
creatorToken := loginTestUser(t, handler, "zhanggong", "123456")
|
|
outsiderToken := loginTestUser(t, handler, "lizhuguan", "123456")
|
|
|
|
createBody, _ := json.Marshal(projectworkflow.CreateProjectInput{Name: fmt.Sprintf("Task Run Authz Test %d", time.Now().UnixNano())})
|
|
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 %s", recCreate.Code, recCreate.Body.String())
|
|
}
|
|
var proj projectworkflow.Project
|
|
_ = json.Unmarshal(recCreate.Body.Bytes(), &proj)
|
|
t.Cleanup(func() { cleanupProject(t, pool, proj.ID) })
|
|
|
|
var wfRunsBefore, taskRunsBefore int
|
|
_ = pool.QueryRow(ctx, `SELECT COUNT(*) FROM workflow_runs WHERE project_id = $1`, proj.ID).Scan(&wfRunsBefore)
|
|
_ = pool.QueryRow(ctx, `SELECT COUNT(*) FROM task_runs WHERE project_id = $1`, proj.ID).Scan(&taskRunsBefore)
|
|
|
|
syncBody, _ := json.Marshal(map[string]interface{}{
|
|
"task_key": "intake",
|
|
"project_id": proj.ID,
|
|
"backend": "stub",
|
|
})
|
|
reqSync := httptest.NewRequest(http.MethodPost, "/api/v1/tasks/run", bytes.NewReader(syncBody))
|
|
withBearerAuth(reqSync, outsiderToken)
|
|
recSync := httptest.NewRecorder()
|
|
handler.ServeHTTP(recSync, reqSync)
|
|
if recSync.Code != http.StatusForbidden {
|
|
t.Fatalf("outsider sync tasks/run expected 403, got %d: %s", recSync.Code, recSync.Body.String())
|
|
}
|
|
|
|
streamBody, _ := json.Marshal(map[string]interface{}{
|
|
"task_key": "intake",
|
|
"project_id": proj.ID,
|
|
"backend": "stub",
|
|
})
|
|
reqStream := httptest.NewRequest(http.MethodPost, "/api/v1/tasks/run/stream", bytes.NewReader(streamBody))
|
|
withBearerAuth(reqStream, outsiderToken)
|
|
recStream := httptest.NewRecorder()
|
|
handler.ServeHTTP(recStream, reqStream)
|
|
if recStream.Code != http.StatusForbidden {
|
|
t.Fatalf("outsider stream tasks/run/stream expected 403, got %d: %s", recStream.Code, recStream.Body.String())
|
|
}
|
|
|
|
var wfRunsAfter, taskRunsAfter int
|
|
_ = pool.QueryRow(ctx, `SELECT COUNT(*) FROM workflow_runs WHERE project_id = $1`, proj.ID).Scan(&wfRunsAfter)
|
|
_ = pool.QueryRow(ctx, `SELECT COUNT(*) FROM task_runs WHERE project_id = $1`, proj.ID).Scan(&taskRunsAfter)
|
|
if wfRunsAfter != wfRunsBefore {
|
|
t.Fatalf("workflow_runs changed for forbidden sync: before=%d after=%d", wfRunsBefore, wfRunsAfter)
|
|
}
|
|
if taskRunsAfter != taskRunsBefore {
|
|
t.Fatalf("task_runs changed for forbidden stream: before=%d after=%d", taskRunsBefore, taskRunsAfter)
|
|
}
|
|
|
|
// 匿名请求
|
|
reqAnon := httptest.NewRequest(http.MethodPost, "/api/v1/tasks/run", bytes.NewReader(syncBody))
|
|
recAnon := httptest.NewRecorder()
|
|
handler.ServeHTTP(recAnon, reqAnon)
|
|
if recAnon.Code != http.StatusUnauthorized {
|
|
t.Fatalf("anonymous tasks/run expected 401, got %d", recAnon.Code)
|
|
}
|
|
}
|
|
|
|
func TestTaskRunAuthz_MemberAllowed_PG(t *testing.T) {
|
|
pool := openPGPoolWithMigrations(t)
|
|
if pool == nil {
|
|
return
|
|
}
|
|
defer pool.Close()
|
|
|
|
handler, _ := setupPGGovernanceServer(t, pool)
|
|
creatorToken := loginTestUser(t, handler, "zhanggong", "123456")
|
|
|
|
createBody, _ := json.Marshal(projectworkflow.CreateProjectInput{Name: fmt.Sprintf("Task Run Member %d", time.Now().UnixNano())})
|
|
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) })
|
|
|
|
syncBody, _ := json.Marshal(map[string]interface{}{
|
|
"task_key": "intake",
|
|
"project_id": proj.ID,
|
|
"backend": "stub",
|
|
})
|
|
reqSync := httptest.NewRequest(http.MethodPost, "/api/v1/tasks/run", bytes.NewReader(syncBody))
|
|
withBearerAuth(reqSync, creatorToken)
|
|
recSync := httptest.NewRecorder()
|
|
handler.ServeHTTP(recSync, reqSync)
|
|
if recSync.Code != http.StatusOK {
|
|
t.Fatalf("member sync tasks/run expected 200, got %d: %s", recSync.Code, recSync.Body.String())
|
|
}
|
|
}
|