Files
t 18c9354095 refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。

- 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。

- 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。

- 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
2026-08-28 22:23:13 +08:00

127 lines
3.4 KiB
Go

package httpx
import (
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/mlogclub/simple/sqls"
"github.com/mlogclub/simple/web"
)
func TestWriteJSONWrapsCommonResultTypes(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
name string
input any
wantStatus int
want web.JsonResult
}{
{
name: "nil becomes success",
input: nil,
wantStatus: http.StatusOK,
want: *web.JsonSuccess(),
},
{
name: "plain value becomes data",
input: map[string]any{"id": float64(1)},
wantStatus: http.StatusOK,
want: *web.JsonData(map[string]any{"id": float64(1)}),
},
{
name: "error becomes json error",
input: errors.New("boom"),
wantStatus: http.StatusOK,
want: *web.JsonError(errors.New("boom")),
},
{
name: "page data becomes page result",
input: PageData([]any{"a"}, &sqls.Paging{Page: 1, Limit: 20, Total: 1}),
wantStatus: http.StatusOK,
want: *web.JsonPageData([]any{"a"}, &sqls.Paging{Page: 1, Limit: 20, Total: 1}),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, recorder := testContext()
WriteJSON(ctx, tt.input)
if recorder.Code != tt.wantStatus {
t.Fatalf("status = %d, want %d", recorder.Code, tt.wantStatus)
}
var got web.JsonResult
if err := json.Unmarshal(recorder.Body.Bytes(), &got); err != nil {
t.Fatalf("decode response: %v", err)
}
if got.Success != tt.want.Success || got.ErrorCode != tt.want.ErrorCode || got.Message != tt.want.Message {
t.Fatalf("result = %+v, want %+v", got, tt.want)
}
})
}
}
func TestWriteJSONLocalizesI18nErrors(t *testing.T) {
gin.SetMode(gin.TestMode)
ctx, recorder := testContext()
i18nx.SetLocale(ctx, i18nx.LocaleEnUS)
WriteJSON(ctx, errorsx.InvalidParamI18n("error.e0116"))
var got web.JsonResult
if err := json.Unmarshal(recorder.Body.Bytes(), &got); err != nil {
t.Fatalf("decode response: %v", err)
}
if got.Message != "Conversation not found." {
t.Fatalf("message = %q, want %q", got.Message, "Conversation not found.")
}
}
func TestWriteJSONUsesSnakeCaseCursorFields(t *testing.T) {
gin.SetMode(gin.TestMode)
ctx, recorder := testContext()
WriteJSON(ctx, CursorData([]string{"a"}, "next", true))
var body map[string]any
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
t.Fatalf("decode response: %v", err)
}
data, ok := body["data"].(map[string]any)
if !ok {
t.Fatalf("data = %#v, want object", body["data"])
}
if got := data["has_more"]; got != true {
t.Fatalf("has_more = %#v, want true", got)
}
if _, exists := data["hasMore"]; exists {
t.Fatalf("camelCase hasMore should not be exposed: %s", recorder.Body.String())
}
}
func TestWriteHttpStatusJSONUsesProvidedStatus(t *testing.T) {
gin.SetMode(gin.TestMode)
ctx, recorder := testContext()
WriteHttpStatusJSON(ctx, http.StatusUnauthorized, web.JsonErrorMsg("unauthorized"))
if recorder.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want %d", recorder.Code, http.StatusUnauthorized)
}
}
func testContext() (*gin.Context, *httptest.ResponseRecorder) {
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodGet, "/", nil)
return ctx, recorder
}