Files
MailUI4Agents/server/internal/handler/decode_test.go

139 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handler
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/agentmail/gateway/internal/models"
)
// 400 的信息必须指向具体字段。
//
// 起因:写跨主机验证脚本时把 workspaces 传成了字符串数组,
// 服务端回的是一句固定的 "Invalid JSON" —— 只能靠翻服务端结构体才发现是哪个字段。
// 第三方客户端没有这个条件。
func TestDecodeBodyErrorNamesTheField(t *testing.T) {
type body struct {
Name string `json:"name"`
Workspaces []models.Workspace `json:"workspaces"`
}
cases := []struct {
name string
payload string
wantHas []string
wantMiss []string
}{
{
name: "字段类型不对要说出字段名与期望类型",
payload: `{"name":"bot","workspaces":["/tmp/ws"]}`,
// 期望能看出:是 workspaces要的是 object 数组,给的是 string
wantHas: []string{"workspaces", "object", "string"},
// 不该把 Go 类型名漏出去
wantMiss: []string{"models.Workspace", "[]models"},
},
{
name: "整个体的类型不对",
payload: `["not","an","object"]`,
wantHas: []string{"object"},
},
{
// 截断的 JSON 走的是 io.ErrUnexpectedEOF不是 json.SyntaxError
name: "被截断的体要说明是截断",
payload: `{"name":`,
wantHas: []string{"语法", "结束"},
},
{
name: "非法字符要给出位置",
payload: `{"name":1x}`,
wantHas: []string{"语法", "字节"},
},
{
name: "空体单独说明",
payload: ``,
wantHas: []string{"为空"},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/x", strings.NewReader(c.payload))
w := httptest.NewRecorder()
var v body
if DecodeBody(w, r, &v) {
t.Fatal("这个体应当解析失败")
}
if w.Code != http.StatusBadRequest {
t.Fatalf("状态码应为 400实际 %d", w.Code)
}
var resp map[string]string
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("响应不是 JSON: %v", err)
}
msg := resp["error"]
if msg == "" {
t.Fatal("error 字段为空")
}
for _, want := range c.wantHas {
if !strings.Contains(msg, want) {
t.Errorf("信息里应含 %q实际 %q", want, msg)
}
}
for _, miss := range c.wantMiss {
if strings.Contains(msg, miss) {
t.Errorf("信息里不该含 Go 类型名 %q%q", miss, msg)
}
}
})
}
}
// 合法请求体不该被拦,也不该写任何响应 ——
// 写了的话调用方接着写自己的响应就成了两次 WriteHeader。
func TestDecodeBodyPassesValidPayload(t *testing.T) {
type body struct {
Name string `json:"name"`
Workspaces []models.Workspace `json:"workspaces"`
}
payload := `{"name":"bot","workspaces":[{"name":"demo","path":"/tmp/ws"}]}`
r := httptest.NewRequest(http.MethodPost, "/x", strings.NewReader(payload))
w := httptest.NewRecorder()
var v body
if !DecodeBody(w, r, &v) {
t.Fatalf("合法体被拒:%s", w.Body.String())
}
if w.Body.Len() != 0 {
t.Errorf("成功时不该写响应体,实际写了 %q", w.Body.String())
}
if v.Name != "bot" || len(v.Workspaces) != 1 || v.Workspaces[0].Path != "/tmp/ws" {
t.Errorf("解析结果不对:%+v", v)
}
}
// 空数组是合法的 —— 两个正式插件注册时都传 workspaces: []。
func TestDecodeBodyAcceptsEmptyWorkspaces(t *testing.T) {
type body struct {
Name string `json:"name"`
Workspaces []models.Workspace `json:"workspaces"`
}
r := httptest.NewRequest(http.MethodPost, "/x",
strings.NewReader(`{"name":"opencode","workspaces":[]}`))
w := httptest.NewRecorder()
var v body
if !DecodeBody(w, r, &v) {
t.Fatalf("空 workspaces 被拒:%s", w.Body.String())
}
if len(v.Workspaces) != 0 {
t.Errorf("应为空数组,实际 %+v", v.Workspaces)
}
}