Files
Agent-System/internal/server/docgov_comment_audit_nonmember_pg_test.go
weichen a9c9a9182e refactor(server): remove 24 mirrored Runtime fields via nil-safe accessors; fix Shutdown-vs-background-loop DATA RACE and goose 00071 duplicate version
- Server no longer mirrors app.Runtime services (gov/svc/pwSvc/...): 574 call sites now use accessors in runtime_accessors.go
- backgroundLoop (mutex+sync.Once) stops sweeper/publisher/reminder/wsGC goroutines race-free (baseline 44161d2a reproduced DATA RACE; now green under -race)
- goose 00071 duplicate (dialogue merge 1da17a28 brought two 00071): context-conflict-adjudications renumbered to 00072; dev DB applied only through 00070 so zero data migration
- task_finalize tests build minimal app.Runtime; test injection via serverTestOverrides + setOverride* (explicit-nil markers for fail-closed cases)
2026-09-25 13:17:22 +00:00

109 lines
3.8 KiB
Go

package server
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"chat-agent-system/internal/auth"
"chat-agent-system/internal/authz"
"chat-agent-system/internal/bizdev"
)
// 批注与审计的项目成员检查必须走 Postgres 授权器。
// 内存授权器在无库时把任意正用户 id 视为成员,不能用它证明 HTTP 负例。
func TestDocGovCommentsAndAudit_NonMemberIs403_PG(t *testing.T) {
cfg, pool := pgConfigFromTestDSN(t)
defer pool.Close()
cfg.AuthRegistrationEnabled = true
cfg.AuthRequireApproval = false
svc := bizdev.MustNewService(t, cfg)
srv := NewServer(cfg, svc)
srv.setOverrideAuthorizer(authz.NewAuthorizer(pool))
ctx := context.Background()
var projectID int64
if err := pool.QueryRow(ctx, `SELECT id FROM projects ORDER BY id ASC LIMIT 1`).Scan(&projectID); err != nil || projectID <= 0 {
t.Skip("no projects for comment/audit negative test")
}
docID := fmt.Sprintf("doc_h2_nonmember_%d", time.Now().UnixNano())
if _, err := pool.Exec(ctx, `
INSERT INTO document_records (doc_id, project_id, task_key, title)
VALUES ($1, $2, 'intake', 'H2 非成员负例')
`, docID, projectID); err != nil {
t.Fatalf("insert document: %v", err)
}
t.Cleanup(func() {
_, _ = pool.Exec(context.Background(), `DELETE FROM document_records WHERE doc_id = $1`, docID)
})
uname := fmt.Sprintf("outsider_h2_%d", time.Now().UnixNano())
pass := "OutsiderPass123!"
regBody, _ := json.Marshal(map[string]string{
"username": uname,
"password": pass,
"name": "局外人",
})
regReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewReader(regBody))
regW := httptest.NewRecorder()
srv.Routes().ServeHTTP(regW, regReq)
if regW.Code != http.StatusOK && regW.Code != http.StatusCreated {
// 同库多包串行时,前序包可能污染准入策略读取(GetConfig 500)。
// 该负例只在自己的库上可判定;污染证据下 skip,不冒充 PASS。
if regW.Code == http.StatusInternalServerError {
t.Skipf("register outsider infra 500 (likely cross-package pollution on shared test db): %s", regW.Body.String())
}
t.Fatalf("register outsider: %d %s", regW.Code, regW.Body.String())
}
loginBody, _ := json.Marshal(map[string]string{"username": uname, "password": pass})
loginReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(loginBody))
loginW := httptest.NewRecorder()
srv.Routes().ServeHTTP(loginW, loginReq)
if loginW.Code != http.StatusOK {
t.Fatalf("outsider login: %d %s", loginW.Code, loginW.Body.String())
}
var session auth.SessionInfo
if err := json.NewDecoder(loginW.Body).Decode(&session); err != nil || session.Token == "" || session.User == nil {
t.Fatalf("decode outsider session: %v body=%s", err, loginW.Body.String())
}
if err := srv.authorizer().RequireProjectAccess(ctx, projectID, session.User.ID); err != authz.ErrForbidden {
t.Fatalf("outsider must not be a member of project %d: %v", projectID, err)
}
cases := []struct {
name string
method string
target string
body []byte
}{
{name: "list comments", method: http.MethodGet, target: "/api/v1/docgov/comments?doc_id=" + url.QueryEscape(docID)},
{name: "list audit", method: http.MethodGet, target: "/api/v1/docgov/audit-logs?doc_id=" + url.QueryEscape(docID)},
{
name: "add comment",
method: http.MethodPost,
target: "/api/v1/docgov/comments",
body: []byte(`{"doc_id":"` + docID + `","content":"should-fail"}`),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(tc.method, tc.target, bytes.NewReader(tc.body))
withBearerAuth(req, session.Token)
w := httptest.NewRecorder()
srv.Routes().ServeHTTP(w, req)
if w.Code != http.StatusForbidden {
t.Fatalf("%s want 403 got %d %s", tc.name, w.Code, w.Body.String())
}
})
}
}