Unify auth middleware and remove X-User-ID trust, add project_members authorization, sandbox workspaces via resolver, and admin-only reconcile by run_id. Introduce goose migrations with startup schema gate, fail closed when DB unavailable outside explicit ALLOW_IN_MEMORY, bundle web/dist in app Dockerfile, and remove frontend pseudo-login fallback.
185 lines
7.1 KiB
Go
185 lines
7.1 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.MustNewService(t, 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)
|
|
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)
|
|
}
|