Files
Agent-System/internal/server/freeform_doc_api_test.go
T
weichenandCursor f6813cc272 feat(api): add additive HTTP/SSE error envelopes
Public errors now emit code, safe message, and request_id while keeping the legacy error field. Raw SQL, paths, tokens, and XSS payloads are stripped; the frontend branches on code instead of substring matching.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 14:40:56 +00:00

185 lines
7.1 KiB
Go

package server_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/config"
"chat-agent-system/internal/server"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupTestServerForFreeformDoc(t *testing.T) (*server.Server, http.Handler, int64, int64) {
cfg := &config.Config{
DataDir: t.TempDir(),
}
svc := bizdev.MustNewService(t, cfg)
srv := server.NewServer(cfg, svc)
handler := srv.Routes()
ctx := context.Background()
ctxMgr := bizdev.Snapshot(svc).Ctx
validMsgID, err := ctxMgr.RecordUserMessageWithID(ctx, "sess-test", 1, "assistant", "# 临床安全评估\n\n评估合格。", "consult", "main", nil)
require.NoError(t, err)
cardMsgID, err := ctxMgr.RecordUserMessageWithID(ctx, "sess-test", 1, "assistant", "请确认操作", "consult", "main", map[string]any{
"interactive_card": map[string]any{"card_id": "card-1"},
})
require.NoError(t, err)
return srv, handler, validMsgID, cardMsgID
}
func TestFreeformDocAPI_EndToEnd(t *testing.T) {
_, handler, validMsgID, cardMsgID := setupTestServerForFreeformDoc(t)
ownerToken := loginTestUser(t, handler, "admin", "admin-secret-password-123")
otherToken := loginTestUser(t, handler, "guest", "123456")
// 1. 不可导出消息保存测试 -> 403
bodyCard, _ := json.Marshal(map[string]any{
"message_id": cardMsgID,
"project_id": 1,
})
reqCard := httptest.NewRequest(http.MethodPost, "/api/v1/freeform-documents/from-message", bytes.NewReader(bodyCard))
withUserIDAuth(reqCard, ownerToken)
recCard := httptest.NewRecorder()
handler.ServeHTTP(recCard, reqCard)
assert.Equal(t, http.StatusForbidden, recCard.Code)
// 2. 有效消息保存测试 -> 201 Created
bodyValid, _ := json.Marshal(map[string]any{
"message_id": validMsgID,
"project_id": 1,
})
reqValid := httptest.NewRequest(http.MethodPost, "/api/v1/freeform-documents/from-message", bytes.NewReader(bodyValid))
withUserIDAuth(reqValid, ownerToken)
recValid := httptest.NewRecorder()
handler.ServeHTTP(recValid, reqValid)
require.Equal(t, http.StatusCreated, recValid.Code)
var createResp struct {
Code int `json:"code"`
IsExisting bool `json:"is_existing"`
Data struct {
Document struct {
DocID string `json:"doc_id"`
Title string `json:"title"`
Space string `json:"space"`
CurrentVersionID int `json:"current_version_id"`
} `json:"document"`
Content string `json:"content_markdown"`
} `json:"data"`
}
err := json.Unmarshal(recValid.Body.Bytes(), &createResp)
require.NoError(t, err)
assert.False(t, createResp.IsExisting)
assert.Equal(t, "临床安全评估", createResp.Data.Document.Title)
assert.Equal(t, "my_private", createResp.Data.Document.Space)
docID := createResp.Data.Document.DocID
// 3. 幂等性测试 -> 200 OK + is_existing: true
reqValid2 := httptest.NewRequest(http.MethodPost, "/api/v1/freeform-documents/from-message", bytes.NewReader(bodyValid))
withUserIDAuth(reqValid2, ownerToken)
recValid2 := httptest.NewRecorder()
handler.ServeHTTP(recValid2, reqValid2)
assert.Equal(t, http.StatusOK, recValid2.Code)
var createResp2 struct {
IsExisting bool `json:"is_existing"`
}
_ = json.Unmarshal(recValid2.Body.Bytes(), &createResp2)
assert.True(t, createResp2.IsExisting)
// 4. 读详情与权限隔离测试
// Owner 读 -> 200
reqGetOwner := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/freeform-documents/%s", docID), nil)
withUserIDAuth(reqGetOwner, ownerToken)
recGetOwner := httptest.NewRecorder()
handler.ServeHTTP(recGetOwner, reqGetOwner)
assert.Equal(t, http.StatusOK, recGetOwner.Code)
// 非本人读私密文档 -> 403 Forbidden
reqGetOther := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/freeform-documents/%s", docID), nil)
withUserIDAuth(reqGetOther, otherToken)
recGetOther := httptest.NewRecorder()
handler.ServeHTTP(recGetOther, reqGetOther)
assert.Equal(t, http.StatusForbidden, recGetOther.Code)
// 5. 列表查询
reqList := httptest.NewRequest(http.MethodGet, "/api/v1/freeform-documents?space=my_private", nil)
withUserIDAuth(reqList, ownerToken)
recList := httptest.NewRecorder()
handler.ServeHTTP(recList, reqList)
assert.Equal(t, http.StatusOK, recList.Code)
// 6. 编辑与 If-Match 乐观并发冲突
newContent := "更新后的第二版内容"
bodyUpdate, _ := json.Marshal(map[string]any{
"content_markdown": newContent,
})
// 错误 If-Match 99 -> 409 Conflict
reqUpdateConflict := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/freeform-documents/%s", docID), bytes.NewReader(bodyUpdate))
withUserIDAuth(reqUpdateConflict, ownerToken)
reqUpdateConflict.Header.Set("If-Match", "\"99\"")
recUpdateConflict := httptest.NewRecorder()
handler.ServeHTTP(recUpdateConflict, reqUpdateConflict)
assert.Equal(t, http.StatusConflict, recUpdateConflict.Code)
// 正确 If-Match "1" -> 200 OK
reqUpdateOK := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/freeform-documents/%s", docID), bytes.NewReader(bodyUpdate))
withUserIDAuth(reqUpdateOK, ownerToken)
reqUpdateOK.Header.Set("If-Match", "\"1\"")
recUpdateOK := httptest.NewRecorder()
handler.ServeHTTP(recUpdateOK, reqUpdateOK)
assert.Equal(t, http.StatusOK, recUpdateOK.Code)
// 7. 共享与撤回
reqShare := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/freeform-documents/%s/share", docID), nil)
withUserIDAuth(reqShare, ownerToken)
recShare := httptest.NewRecorder()
handler.ServeHTTP(recShare, reqShare)
assert.Equal(t, http.StatusOK, recShare.Code)
// 共享后,用户 999 可以读取
reqGetShared := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/freeform-documents/%s", docID), nil)
withUserIDAuth(reqGetShared, otherToken)
recGetShared := httptest.NewRecorder()
handler.ServeHTTP(recGetShared, reqGetShared)
assert.Equal(t, http.StatusOK, recGetShared.Code)
// 撤回共享
reqUnshare := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/freeform-documents/%s/unshare", docID), nil)
withUserIDAuth(reqUnshare, ownerToken)
recUnshare := httptest.NewRecorder()
handler.ServeHTTP(recUnshare, reqUnshare)
assert.Equal(t, http.StatusOK, recUnshare.Code)
// 撤回后用户 999 再次读取 -> 403 Forbidden
reqGetUnshared := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/freeform-documents/%s", docID), nil)
withUserIDAuth(reqGetUnshared, otherToken)
recGetUnshared := httptest.NewRecorder()
handler.ServeHTTP(recGetUnshared, reqGetUnshared)
assert.Equal(t, http.StatusForbidden, recGetUnshared.Code)
// 8. 软删除
reqDel := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/v1/freeform-documents/%s", docID), nil)
withUserIDAuth(reqDel, ownerToken)
recDel := httptest.NewRecorder()
handler.ServeHTTP(recDel, reqDel)
assert.Equal(t, http.StatusOK, recDel.Code)
// 删除后再读取 -> 404 Not Found
reqGetAfterDel := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/freeform-documents/%s", docID), nil)
withUserIDAuth(reqGetAfterDel, ownerToken)
recGetAfterDel := httptest.NewRecorder()
handler.ServeHTTP(recGetAfterDel, reqGetAfterDel)
assert.Equal(t, http.StatusNotFound, recGetAfterDel.Code)
}