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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user