package server_test import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "chat-agent-system/internal/bizdev" "chat-agent-system/internal/config" "chat-agent-system/internal/server" ) func TestServer_ReplyExportAPI(t *testing.T) { cfg := &config.Config{ Env: "dev", HTTPHost: "127.0.0.1", HTTPPort: 8893, DataDir: t.TempDir(), } svc := bizdev.MustNewService(t, cfg) srv := server.NewServer(cfg, svc) handler := srv.Routes() ctx := t.Context() ctxMgr := bizdev.Snapshot(svc).Ctx sessID := "sess_export_api_test" token := loginTestAdmin(t, handler, cfg) if _, err := ctxMgr.EnsureChatSession(ctx, sessID, 1, 0, "导出测试会话"); err != nil { t.Fatalf("EnsureChatSession failed: %v", err) } // 1. 注入一条完整 assistant 消息 fullContent := "# 临床风险评估报告\n\n根据系统审查,该产品存在以下风险点:\n1. 电气隔离距离不足\n2. 软件未定义看门狗" asstID, err := ctxMgr.RecordUserMessageWithID(ctx, sessID, 0, "assistant", fullContent, "", "", nil) if err != nil { t.Fatalf("RecordUserMessageWithID failed: %v", err) } // 2. 注入一条纯卡片依赖引导消息 cardOnlyContent := "请确认以下操作卡片:" cardMeta := map[string]any{ "interactive_card": map[string]any{ "card_id": "c1", "title": "驳回门禁", }, } cardOnlyID, err := ctxMgr.RecordUserMessageWithID(ctx, sessID, 0, "assistant", cardOnlyContent, "", "", cardMeta) if err != nil { t.Fatalf("RecordUserMessageWithID cardOnly failed: %v", err) } // ------------------------------------------------------------- // 测试 A: 获取 full 消息能力 // ------------------------------------------------------------- req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/messages/%d/export-capability", asstID), nil) withBearerAuth(req, token) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String()) } var capRes map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &capRes); err != nil { t.Fatalf("json unmarshal failed: %v", err) } if capRes["policy"] != "full" { t.Errorf("expected policy full, got %v", capRes["policy"]) } formats, ok := capRes["formats"].([]any) if !ok || len(formats) == 0 { t.Errorf("expected non-empty formats list") } // ------------------------------------------------------------- // 测试 B: 获取 cardOnly 消息能力 (policy = none, reason = CARD_ONLY) // ------------------------------------------------------------- reqCard := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/messages/%d/export-capability", cardOnlyID), nil) withBearerAuth(reqCard, token) recCard := httptest.NewRecorder() handler.ServeHTTP(recCard, reqCard) if recCard.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", recCard.Code, recCard.Body.String()) } var cardCapRes map[string]any _ = json.Unmarshal(recCard.Body.Bytes(), &cardCapRes) if cardCapRes["policy"] != "none" { t.Errorf("expected cardOnly policy none, got %v", cardCapRes["policy"]) } if cardCapRes["reason_code"] != "CARD_ONLY" { t.Errorf("expected CARD_ONLY, got %v", cardCapRes["reason_code"]) } // ------------------------------------------------------------- // 测试 C: 获取 full 消息正文快照 // ------------------------------------------------------------- reqSrc := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/messages/%d/export-source", asstID), nil) withBearerAuth(reqSrc, token) recSrc := httptest.NewRecorder() handler.ServeHTTP(recSrc, reqSrc) if recSrc.Code != http.StatusOK { t.Fatalf("expected status 200, got %d: %s", recSrc.Code, recSrc.Body.String()) } var srcRes map[string]any if err := json.Unmarshal(recSrc.Body.Bytes(), &srcRes); err != nil { t.Fatalf("json unmarshal failed: %v", err) } if srcRes["title"] != "临床风险评估报告" { t.Errorf("expected title '临床风险评估报告', got %v", srcRes["title"]) } if srcRes["markdown"] != fullContent { t.Errorf("expected markdown match, got %v", srcRes["markdown"]) } if recSrc.Header().Get("Cache-Control") != "private, no-store" { t.Errorf("expected Cache-Control: private, no-store, got %v", recSrc.Header().Get("Cache-Control")) } // ------------------------------------------------------------- // 测试 D: cardOnly 请求正文快照应返回 400 MESSAGE_NOT_EXPORTABLE // ------------------------------------------------------------- reqCardSrc := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/messages/%d/export-source", cardOnlyID), nil) withBearerAuth(reqCardSrc, token) recCardSrc := httptest.NewRecorder() handler.ServeHTTP(recCardSrc, reqCardSrc) if recCardSrc.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", recCardSrc.Code) } // ------------------------------------------------------------- // 测试 E: 不存在的消息 ID 返回 404 // ------------------------------------------------------------- reqNotFound := httptest.NewRequest(http.MethodGet, "/api/v1/chat/messages/999999/export-source", nil) withBearerAuth(reqNotFound, token) recNotFound := httptest.NewRecorder() handler.ServeHTTP(recNotFound, reqNotFound) if recNotFound.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", recNotFound.Code) } // ------------------------------------------------------------- // 测试 F: 冲突的 content_revision 返回 409 // ------------------------------------------------------------- reqConflict := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/messages/%d/export-source?content_revision=999", asstID), nil) withBearerAuth(reqConflict, token) recConflict := httptest.NewRecorder() handler.ServeHTTP(recConflict, reqConflict) if recConflict.Code != http.StatusConflict { t.Fatalf("expected 409, got %d", recConflict.Code) } // 测试 G: 未登录返回 401(严格模式,禁用测试回退) strictCfg := &config.Config{DataDir: t.TempDir(), AllowInMemory: false} strictSvc, err := bizdev.NewService(strictCfg) if err != nil { t.Fatalf("strict NewService: %v", err) } strictHandler := server.NewServer(strictCfg, strictSvc).Routes() reqUnauth := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/messages/%d/export-source", asstID), nil) recUnauth := httptest.NewRecorder() strictHandler.ServeHTTP(recUnauth, reqUnauth) if recUnauth.Code != http.StatusUnauthorized { t.Fatalf("expected 401 without auth, got %d", recUnauth.Code) } // 测试 H: 其他用户无权导出 (IDOR) reqIDOR := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/messages/%d/export-source", asstID), nil) withUserIDAuth(reqIDOR, loginTestUser(t, handler, "zhanggong", "123456")) recIDOR := httptest.NewRecorder() handler.ServeHTTP(recIDOR, reqIDOR) if recIDOR.Code != http.StatusForbidden { t.Fatalf("expected 403 for cross-user export, got %d: %s", recIDOR.Code, recIDOR.Body.String()) } }