Files
Agent-System/internal/server/freeform_doc_api_test.go
T
WeiCHEN 1fc7d0d01f feat(export): 实现单次智能体回复导出、私密文档保存与隐私防泄露全套闭环 (Phase 0 - Phase 2)
- Phase 0: 实现服务端回复导出策略矩阵引擎 (replyexport.EvaluatePolicy)、数据库迁移 (2026-09-11-message-export-policy.sql)、SSE done 事件挂载与能力探测 API
- Phase 1: 构建前端 ReplyDocumentIR 语法树解析与渲染管道,支持 Markdown / DOCX 文件下载及带格式富文本剪贴板复制
- Phase 2: 实现私密自由文档 (freeform_documents) 领域服务、PostgreSQL 乐观锁版本管理、ACL 隐私隔离、一键共享与右侧栏编辑联动
- 专项修复:
  1. 修复后台 API 路由注册并加入双向拉取同步,确保私密保存实时进入我的私密列表
  2. 实现轻量回复专属 DOCX 生成与静默下载逻辑 (downloadDocxReply)
  3. 将 LaTeX 公式转为标准 MathML 矢量标记,支持粘贴到 Microsoft Word / WPS 自动识别为原生公式
2026-09-13 02:50:21 +00:00

183 lines
7.0 KiB
Go

package server_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/config"
"chat-agent-system/internal/server"
)
func setupTestServerForFreeformDoc(t *testing.T) (*server.Server, http.Handler, int64, int64) {
cfg := &config.Config{
DataDir: t.TempDir(),
}
svc := bizdev.NewService(cfg)
srv := server.NewServer(cfg, svc)
handler := srv.Routes()
ctx := context.Background()
ctxMgr := svc.ContextManager()
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)
// 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))
reqCard.Header.Set("X-User-ID", "1")
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))
reqValid.Header.Set("X-User-ID", "1")
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))
reqValid2.Header.Set("X-User-ID", "1")
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)
reqGetOwner.Header.Set("X-User-ID", "1")
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)
reqGetOther.Header.Set("X-User-ID", "999")
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)
reqList.Header.Set("X-User-ID", "1")
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))
reqUpdateConflict.Header.Set("X-User-ID", "1")
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))
reqUpdateOK.Header.Set("X-User-ID", "1")
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)
reqShare.Header.Set("X-User-ID", "1")
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)
reqGetShared.Header.Set("X-User-ID", "999")
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)
reqUnshare.Header.Set("X-User-ID", "1")
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)
reqGetUnshared.Header.Set("X-User-ID", "999")
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)
reqDel.Header.Set("X-User-ID", "1")
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)
reqGetAfterDel.Header.Set("X-User-ID", "1")
recGetAfterDel := httptest.NewRecorder()
handler.ServeHTTP(recGetAfterDel, reqGetAfterDel)
assert.Equal(t, http.StatusNotFound, recGetAfterDel.Code)
}