Files
Agent-System/internal/server/chat_unread_and_export_api_test.go
T
weichen 3145550a59 feat(security): complete P0 release blocking remediation
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.
2026-09-13 02:50:22 +00:00

184 lines
6.8 KiB
Go

package server_test
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/config"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/server"
)
func TestServer_ChatUnreadAndExportAPIs(t *testing.T) {
cfg := &config.Config{
Env: "dev",
HTTPHost: "127.0.0.1",
HTTPPort: 8891,
DataDir: t.TempDir(),
}
svc := bizdev.MustNewService(t, cfg)
srv := server.NewServer(cfg, svc)
handler := srv.Routes()
sessID := "sess_api_unread_export_001"
dayKey1 := "2026-09-08"
dayKey2 := "2026-09-09"
ctx := t.Context()
ctxMgr := svc.ContextManager()
token := loginTestAdmin(t, handler, cfg)
if _, err := ctxMgr.EnsureChatSession(ctx, sessID, 1, 0, "未读导出测试"); err != nil {
t.Fatalf("EnsureChatSession failed: %v", err)
}
// 注入种子消息并获取真实自增 ID
// 消息 1: user, day 1, category=task
_, _ = ctxMgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "消息1:探讨立项需求", "TRIGGER_TASK", "", map[string]any{
"day_key": dayKey1,
})
// 消息 2: assistant, day 1, category=casual
id2, _ := ctxMgr.RecordUserMessageWithID(ctx, sessID, 0, "assistant", "消息2:收到需求,开始规划。以下为初步里程碑拆解与责任分工建议,供项目组评审。", "", "", map[string]any{
"day_key": dayKey1,
"category": "casual",
})
// 消息 3: user, day 2, category=review (DOCGOV)
_, _ = ctxMgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "消息3:关于型检机构认证进度", "DOCGOV_REVIEW", "", map[string]any{
"day_key": dayKey2,
"category": "review",
})
// 消息 4: assistant, day 2, category=review — 可导出正文
_, _ = ctxMgr.RecordUserMessageWithID(ctx, sessID, 0, "assistant", "消息4:关于型检机构认证进度,已整理成以下要点:1) 受理号已确认;2) 补正材料清单如下;3) 预计复核周期 15 个工作日。", "DOCGOV_REVIEW", "", map[string]any{
"day_key": dayKey2,
"category": "review",
})
// 消息 5: user, day 2
_, _ = ctxMgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "消息5:另外请补充EMC整改报告", "DOCGOV_AUDIT", "", map[string]any{
"day_key": dayKey2,
"category": "review",
})
t.Run("GET /api/v1/chat/unread 统计 since_message_id 之后的未读", func(t *testing.T) {
// since_message_id=id2, 应统计 id3 与 id4
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/unread?session_id=%s&since_message_id=%d", sessID, id2), nil)
withBearerAuth(req, token)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200 OK, got %d: %s", w.Code, w.Body.String())
}
var summary ctxmgr.ChatUnreadSummary
if err := json.NewDecoder(w.Body).Decode(&summary); err != nil {
t.Fatalf("decode unread summary failed: %v", err)
}
if summary.Total != 3 {
t.Errorf("expected total 3, got %d", summary.Total)
}
if len(summary.Items) != 1 {
t.Fatalf("expected 1 unread day bucket, got %d", len(summary.Items))
}
if summary.Items[0].Date != dayKey2 {
t.Errorf("expected date %s, got %s", dayKey2, summary.Items[0].Date)
}
if summary.Items[0].Count != 3 {
t.Errorf("expected count 3, got %d", summary.Items[0].Count)
}
if summary.NextMessageID == 0 {
t.Errorf("expected NextMessageID > 0, got %d", summary.NextMessageID)
}
// 消费最新一条未读 (POST /api/v1/chat/read)
readBody := fmt.Sprintf(`{"session_id":"%s","user_id":0,"read_message_ids":[%d]}`, sessID, summary.NextMessageID)
readReq := httptest.NewRequest(http.MethodPost, "/api/v1/chat/read", strings.NewReader(readBody))
readReq.Header.Set("Content-Type", "application/json")
withBearerAuth(readReq, token)
readW := httptest.NewRecorder()
handler.ServeHTTP(readW, readReq)
if readW.Code != http.StatusOK {
t.Fatalf("expected 200 OK on /api/v1/chat/read, got %d: %s", readW.Code, readW.Body.String())
}
// 再次获取未读,数量应减为 1
req2 := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/unread?session_id=%s&since_message_id=%d", sessID, id2), nil)
withBearerAuth(req2, token)
w2 := httptest.NewRecorder()
handler.ServeHTTP(w2, req2)
var summary2 ctxmgr.ChatUnreadSummary
_ = json.NewDecoder(w2.Body).Decode(&summary2)
if summary2.Total != 2 {
t.Errorf("expected total 2 after consuming one unread, got %d", summary2.Total)
}
})
t.Run("GET /api/v1/chat/export/messages 导出全量并支持分类过滤", func(t *testing.T) {
// 1. 无过滤条件导出
req := httptest.NewRequest(http.MethodGet, "/api/v1/chat/export/messages?session_id="+sessID, nil)
withBearerAuth(req, token)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200 OK, got %d: %s", w.Code, w.Body.String())
}
var msgs []ctxmgr.ChatMessageRecord
if err := json.NewDecoder(w.Body).Decode(&msgs); err != nil {
t.Fatalf("decode export messages failed: %v", err)
}
if len(msgs) != 2 {
t.Fatalf("expected 2 exportable assistant messages, got %d", len(msgs))
}
// 2. 按分类 review 过滤
reqReview := httptest.NewRequest(http.MethodGet, "/api/v1/chat/export/messages?session_id="+sessID+"&category=review", nil)
withBearerAuth(reqReview, token)
wReview := httptest.NewRecorder()
handler.ServeHTTP(wReview, reqReview)
if wReview.Code != http.StatusOK {
t.Fatalf("expected 200 OK, got %d: %s", wReview.Code, wReview.Body.String())
}
var reviewMsgs []ctxmgr.ChatMessageRecord
if err := json.NewDecoder(wReview.Body).Decode(&reviewMsgs); err != nil {
t.Fatalf("decode filtered export failed: %v", err)
}
if len(reviewMsgs) != 1 {
t.Fatalf("expected 1 exportable review assistant message, got %d", len(reviewMsgs))
}
// 3. 按日期过滤
reqDate := httptest.NewRequest(http.MethodGet, "/api/v1/chat/export/messages?session_id="+sessID+"&date_from=2026-09-09&date_to=2026-09-09", nil)
withBearerAuth(reqDate, token)
wDate := httptest.NewRecorder()
handler.ServeHTTP(wDate, reqDate)
if wDate.Code != http.StatusOK {
t.Fatalf("expected 200 OK, got %d: %s", wDate.Code, wDate.Body.String())
}
var dateMsgs []ctxmgr.ChatMessageRecord
if err := json.NewDecoder(wDate.Body).Decode(&dateMsgs); err != nil {
t.Fatalf("decode date-filtered export failed: %v", err)
}
if len(dateMsgs) != 1 {
t.Fatalf("expected 1 exportable message on 2026-09-09, got %d", len(dateMsgs))
}
// 4. 跨用户导出应被拒绝
reqIDOR := httptest.NewRequest(http.MethodGet, "/api/v1/chat/export/messages?session_id="+sessID, nil)
withUserIDAuth(reqIDOR, loginTestUser(t, handler, "zhanggong", "123456"))
wIDOR := httptest.NewRecorder()
handler.ServeHTTP(wIDOR, reqIDOR)
if wIDOR.Code != http.StatusForbidden {
t.Fatalf("expected 403 for cross-user session export, got %d: %s", wIDOR.Code, wIDOR.Body.String())
}
})
}