test: cover asset raw round-trip and harden cross-user session upsert

Deny foreign user_id upsert without manage rights (403); unique project
names in ctxmgr PG fixture for global name index; add test-env data repair SQL.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-20 09:18:36 +00:00
co-authored by Cursor
parent 21782751ab
commit 18a323c4dc
4 changed files with 126 additions and 5 deletions
+3 -2
View File
@@ -153,11 +153,12 @@ func TestContextManager_DatabaseIntegration(t *testing.T) {
// 准备真实项目与用户
var projID int64
projCode := "PROJ_MGR_" + time.Now().Format("20060102150405")
projName := "智能微创内窥镜-" + time.Now().Format("150405.000")
err = pool.QueryRow(ctx, `
INSERT INTO projects (code, name, customer_name, device_type, current_stage)
VALUES ($1, '智能微创内窥镜', '水木创新中心', '内窥镜', 'V0.5_feasibility')
VALUES ($1, $2, '水木创新中心', '内窥镜', 'V0.5_feasibility')
RETURNING id
`, projCode).Scan(&projID)
`, projCode, projName).Scan(&projID)
if err != nil {
t.Fatalf("insert project failed: %v", err)
}
+72
View File
@@ -3,9 +3,11 @@ package server
import (
"bytes"
"encoding/json"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
@@ -96,3 +98,73 @@ func TestUploadProjectAsset_RejectsOversizedFilename(t *testing.T) {
t.Fatalf("expected 415 for long filename, got %d", resp.StatusCode)
}
}
func TestGetProjectAssetRaw_RoundTrip(t *testing.T) {
cfg := &config.Config{Env: "test", DataDir: t.TempDir()}
svc := bizdev.MustNewService(t, cfg)
srv := NewServer(cfg, svc)
ts := httptest.NewServer(srv.Routes())
defer ts.Close()
payload := []byte("raw asset preview payload")
var body bytes.Buffer
w := multipart.NewWriter(&body)
part, _ := w.CreateFormFile("file", "preview.txt")
_, _ = part.Write(payload)
_ = w.Close()
token := loginTestAdmin(t, srv)
upReq, _ := http.NewRequest(http.MethodPost, ts.URL+"/api/v1/projects/7/assets", &body)
upReq.Header.Set("Content-Type", w.FormDataContentType())
withBearerAuth(upReq, token)
upResp, err := http.DefaultClient.Do(upReq)
if err != nil {
t.Fatalf("upload: %v", err)
}
defer upResp.Body.Close()
if upResp.StatusCode != http.StatusOK {
t.Fatalf("upload status=%d", upResp.StatusCode)
}
var uploaded projectAssetResponse
if err := json.NewDecoder(upResp.Body).Decode(&uploaded); err != nil {
t.Fatalf("decode upload: %v", err)
}
rawReq, _ := http.NewRequest(
http.MethodGet,
ts.URL+"/api/v1/projects/7/assets/"+strconv.FormatInt(uploaded.AssetID, 10)+"/raw",
nil,
)
withBearerAuth(rawReq, token)
rawResp, err := http.DefaultClient.Do(rawReq)
if err != nil {
t.Fatalf("raw: %v", err)
}
defer rawResp.Body.Close()
if rawResp.StatusCode != http.StatusOK {
t.Fatalf("raw status=%d", rawResp.StatusCode)
}
got, _ := io.ReadAll(rawResp.Body)
if !bytes.Equal(got, payload) {
t.Fatalf("raw body mismatch: got %q want %q", got, payload)
}
if ct := rawResp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/plain") {
t.Fatalf("unexpected content-type: %s", ct)
}
// Wrong project id must not leak the blob.
wrongReq, _ := http.NewRequest(
http.MethodGet,
ts.URL+"/api/v1/projects/999/assets/"+strconv.FormatInt(uploaded.AssetID, 10)+"/raw",
nil,
)
withBearerAuth(wrongReq, token)
wrongResp, err := http.DefaultClient.Do(wrongReq)
if err != nil {
t.Fatalf("wrong project raw: %v", err)
}
defer wrongResp.Body.Close()
if wrongResp.StatusCode == http.StatusOK {
t.Fatalf("expected non-200 for mismatched project, got %d", wrongResp.StatusCode)
}
}
+3 -3
View File
@@ -1091,9 +1091,9 @@ func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request)
ownerID := user.ID
if req.UserID > 0 && req.UserID != user.ID {
if req.ProjectID <= 0 {
writeJSONResponse(w, http.StatusBadRequest, map[string]string{
"error_code": "ERR_INVALID_PARAM",
"message": "代注册他人会话时必须指定 project_id",
writeJSONResponse(w, http.StatusForbidden, map[string]string{
"error_code": "ERR_FORBIDDEN",
"message": "无权为其他用户注册项目会话",
})
return
}
+48
View File
@@ -0,0 +1,48 @@
-- One-shot data repair for test env triage (project 4/5 dup + member sessions).
-- Usage:
-- docker exec -i shuimu-system-pg-test psql -U app -d agent_system_test < scripts/fix-test-env-triage-data.sql
-- Safe to re-run (idempotent where possible).
BEGIN;
-- 1) Retire duplicate project 4 (wanjiahao); keep project 5 (admin + members + assets).
UPDATE projects
SET status = 'demo_retired',
updated_at = now()
WHERE id = 4
AND name = '全自动冲击波治疗仪'
AND COALESCE(status, 'active') <> 'demo_retired';
-- 2) Fix mis-owned member sessions created by creator upsert (owner was forced to admin).
UPDATE chat_sessions
SET user_id = 32,
updated_at = now()
WHERE session_id = 'sess-p5-u32'
AND project_id = 5
AND user_id = 4;
UPDATE chat_sessions
SET user_id = 33,
updated_at = now()
WHERE session_id = 'sess-p5-u33'
AND project_id = 5
AND user_id = 4;
-- 3) Ensure chenwei (35) private session on project 5.
INSERT INTO chat_sessions (session_id, user_id, project_id, title, created_at, updated_at)
SELECT 'sess-p5-u35', 35, 5, '全自动冲击波治疗仪', now(), now()
WHERE EXISTS (
SELECT 1 FROM project_members
WHERE project_id = 5 AND user_id = 35 AND status = 'active'
)
ON CONFLICT (session_id) DO UPDATE SET
user_id = EXCLUDED.user_id,
project_id = EXCLUDED.project_id,
title = COALESCE(NULLIF(EXCLUDED.title, ''), chat_sessions.title),
updated_at = now();
COMMIT;
-- Verification queries (run after):
-- SELECT id, name, creator_user_id, status FROM projects WHERE name LIKE '%冲击波%' OR id IN (4,5);
-- SELECT session_id, user_id, project_id FROM chat_sessions WHERE project_id = 5 ORDER BY session_id;