2026-07-22 01:19:46 +08:00
|
|
|
|
package ginx_test
|
2026-07-22 01:17:07 +08:00
|
|
|
|
|
|
|
|
|
|
import (
|
2026-09-19 01:31:34 +08:00
|
|
|
|
"context"
|
2026-07-22 01:17:07 +08:00
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
|
|
"code.yun.ink/pkg/responsex"
|
2026-09-19 21:41:50 +08:00
|
|
|
|
respgin "code.yun.ink/pkg/responsex/ginx"
|
2026-07-22 01:17:07 +08:00
|
|
|
|
"github.com/gin-gonic/gin"
|
2026-09-19 01:31:34 +08:00
|
|
|
|
"github.com/yuninks/langx"
|
2026-07-22 01:17:07 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func decodeBody(t *testing.T, w *httptest.ResponseRecorder) map[string]any {
|
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
var body map[string]any
|
|
|
|
|
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
|
|
|
|
t.Fatalf("decode body failed: %v, raw: %s", err, w.Body.String())
|
|
|
|
|
|
}
|
|
|
|
|
|
return body
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func newGinCtx(w *httptest.ResponseRecorder) *gin.Context {
|
|
|
|
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
|
|
c.Request = httptest.NewRequest("GET", "/", nil)
|
|
|
|
|
|
return c
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- Success ----------
|
|
|
|
|
|
|
|
|
|
|
|
func TestSuccess(t *testing.T) {
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
c := newGinCtx(w)
|
2026-09-19 01:31:34 +08:00
|
|
|
|
// c.Set("trace_id","adada")
|
|
|
|
|
|
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), "trace_id", "adada"))
|
2026-07-22 01:17:07 +08:00
|
|
|
|
|
|
|
|
|
|
respgin.Success(c, "hello world")
|
|
|
|
|
|
|
|
|
|
|
|
body := decodeBody(t, w)
|
|
|
|
|
|
if body["code"] != float64(200) {
|
|
|
|
|
|
t.Errorf("code = %v, want 200", body["code"])
|
|
|
|
|
|
}
|
|
|
|
|
|
if body["data"] != "hello world" {
|
|
|
|
|
|
t.Errorf("data = %v, want hello world", body["data"])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestSuccessWithOptions(t *testing.T) {
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
c := newGinCtx(w)
|
|
|
|
|
|
|
|
|
|
|
|
respgin.Success(c, "created",
|
|
|
|
|
|
responsex.SetHttpStatusCode(201),
|
|
|
|
|
|
responsex.SetHeader("X-Custom", "gin"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if w.Code != 201 {
|
|
|
|
|
|
t.Errorf("http status = %d, want 201", w.Code)
|
|
|
|
|
|
}
|
|
|
|
|
|
if v := w.Header().Get("X-Custom"); v != "gin" {
|
|
|
|
|
|
t.Errorf("X-Custom = %q, want gin", v)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestSuccessNilData(t *testing.T) {
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
c := newGinCtx(w)
|
|
|
|
|
|
|
|
|
|
|
|
respgin.Success(c, nil)
|
|
|
|
|
|
|
|
|
|
|
|
body := decodeBody(t, w)
|
|
|
|
|
|
if _, ok := body["data"]; ok {
|
|
|
|
|
|
t.Errorf("data should be omitted for nil")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- Error ----------
|
|
|
|
|
|
|
|
|
|
|
|
func TestError(t *testing.T) {
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
c := newGinCtx(w)
|
|
|
|
|
|
|
|
|
|
|
|
respgin.Error(c, errors.New("gin error"))
|
|
|
|
|
|
|
|
|
|
|
|
body := decodeBody(t, w)
|
|
|
|
|
|
if body["code"] != float64(400) {
|
|
|
|
|
|
t.Errorf("code = %v, want 400", body["code"])
|
|
|
|
|
|
}
|
|
|
|
|
|
if body["message"] != "gin error" {
|
|
|
|
|
|
t.Errorf("message = %v, want 'gin error'", body["message"])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestErrorWithHttpStatus(t *testing.T) {
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
c := newGinCtx(w)
|
|
|
|
|
|
|
|
|
|
|
|
respgin.Error(c, errors.New("server error"),
|
|
|
|
|
|
responsex.SetHttpStatusCode(500),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if w.Code != 500 {
|
|
|
|
|
|
t.Errorf("http status = %d, want 500", w.Code)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestErrorWithHeader(t *testing.T) {
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
c := newGinCtx(w)
|
|
|
|
|
|
|
|
|
|
|
|
respgin.Error(c, errors.New("bad request"),
|
|
|
|
|
|
responsex.SetHttpStatusCode(400),
|
|
|
|
|
|
responsex.SetHeaders(map[string]string{
|
|
|
|
|
|
"X-Error-Code": "GIN_001",
|
|
|
|
|
|
"X-Request-Id": "abc",
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if v := w.Header().Get("X-Error-Code"); v != "GIN_001" {
|
|
|
|
|
|
t.Errorf("X-Error-Code = %q, want GIN_001", v)
|
|
|
|
|
|
}
|
|
|
|
|
|
if v := w.Header().Get("X-Request-Id"); v != "abc" {
|
|
|
|
|
|
t.Errorf("X-Request-Id = %q, want abc", v)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- ErrorWithData ----------
|
|
|
|
|
|
|
|
|
|
|
|
func TestErrorWithData(t *testing.T) {
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
c := newGinCtx(w)
|
|
|
|
|
|
|
|
|
|
|
|
respgin.ErrorWithData(c, errors.New("validation error"), map[string]any{
|
|
|
|
|
|
"fields": []string{"email", "name"},
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
body := decodeBody(t, w)
|
|
|
|
|
|
data, ok := body["data"].(map[string]any)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
t.Fatalf("data should be map, got %T", body["data"])
|
|
|
|
|
|
}
|
|
|
|
|
|
fields, _ := data["fields"].([]any)
|
|
|
|
|
|
if len(fields) != 2 {
|
|
|
|
|
|
t.Errorf("fields length = %d, want 2", len(fields))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestErrorWithDataAndOptions(t *testing.T) {
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
c := newGinCtx(w)
|
|
|
|
|
|
|
|
|
|
|
|
respgin.ErrorWithData(c, errors.New("gone"), "resource",
|
|
|
|
|
|
responsex.SetHttpStatusCode(410),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if w.Code != 410 {
|
|
|
|
|
|
t.Errorf("http status = %d, want 410", w.Code)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- ErrorStr ----------
|
|
|
|
|
|
|
|
|
|
|
|
func TestErrorStr(t *testing.T) {
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
c := newGinCtx(w)
|
|
|
|
|
|
|
|
|
|
|
|
respgin.ErrorStr(c, "string error")
|
|
|
|
|
|
|
|
|
|
|
|
body := decodeBody(t, w)
|
2026-09-19 01:31:34 +08:00
|
|
|
|
if body["message"] != "string error" {
|
|
|
|
|
|
t.Errorf("message = %v, want 'string error'", body["message"])
|
2026-07-22 01:17:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestErrorStrWithOptions(t *testing.T) {
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
c := newGinCtx(w)
|
|
|
|
|
|
|
|
|
|
|
|
respgin.ErrorStr(c, "forbidden",
|
|
|
|
|
|
responsex.SetHttpStatusCode(403),
|
|
|
|
|
|
responsex.SetHeader("X-Reason", "no-access"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if w.Code != 403 {
|
|
|
|
|
|
t.Errorf("http status = %d, want 403", w.Code)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- SuccessWithPage ----------
|
|
|
|
|
|
|
|
|
|
|
|
func TestSuccessWithPage(t *testing.T) {
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
c := newGinCtx(w)
|
|
|
|
|
|
|
|
|
|
|
|
page := responsex.Pagination{
|
|
|
|
|
|
Page: 1,
|
|
|
|
|
|
Size: 10,
|
|
|
|
|
|
TotalPage: 3,
|
|
|
|
|
|
TotalCount: 30,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
respgin.SuccessWithPage(c, []string{"a", "b"}, page)
|
|
|
|
|
|
|
|
|
|
|
|
body := decodeBody(t, w)
|
|
|
|
|
|
p, ok := body["pagination"].(map[string]any)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
t.Fatalf("pagination should be map, got %T", body["pagination"])
|
|
|
|
|
|
}
|
|
|
|
|
|
if p["total_count"] != float64(30) {
|
|
|
|
|
|
t.Errorf("total_count = %v, want 30", p["total_count"])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestSuccessWithPageAndOptions(t *testing.T) {
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
c := newGinCtx(w)
|
|
|
|
|
|
|
|
|
|
|
|
page := responsex.Pagination{Page: 1, Size: 20, TotalPage: 1, TotalCount: 20}
|
|
|
|
|
|
|
|
|
|
|
|
respgin.SuccessWithPage(c, []int{1}, page,
|
|
|
|
|
|
responsex.SetHeader("X-Total", "20"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if v := w.Header().Get("X-Total"); v != "20" {
|
|
|
|
|
|
t.Errorf("X-Total = %q, want 20", v)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- MsgMapData / MsgKV ----------
|
|
|
|
|
|
|
|
|
|
|
|
func TestMsgKV(t *testing.T) {
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
c := newGinCtx(w)
|
|
|
|
|
|
|
|
|
|
|
|
respgin.MsgKV(c, "welcome", "name", "Alice")
|
|
|
|
|
|
|
|
|
|
|
|
// 仅验证 HTTP 200 正常返回,具体内容由 langx 决定
|
|
|
|
|
|
if w.Code != 200 {
|
|
|
|
|
|
t.Errorf("http status = %d, want 200", w.Code)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestMsgKVWithOptions(t *testing.T) {
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
c := newGinCtx(w)
|
|
|
|
|
|
|
|
|
|
|
|
respgin.MsgKV(c, "welcome", "name", "Alice",
|
|
|
|
|
|
responsex.SetHeader("X-Lang", "zh"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if v := w.Header().Get("X-Lang"); v != "zh" {
|
|
|
|
|
|
t.Errorf("X-Lang = %q, want zh", v)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- Custom ----------
|
|
|
|
|
|
|
|
|
|
|
|
func TestCustom(t *testing.T) {
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
c := newGinCtx(w)
|
|
|
|
|
|
|
|
|
|
|
|
respgin.Custom(c,
|
|
|
|
|
|
responsex.SetCode(100),
|
|
|
|
|
|
responsex.SetMessage("custom gin"),
|
|
|
|
|
|
responsex.SetHttpStatusCode(202),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if w.Code != 202 {
|
|
|
|
|
|
t.Errorf("http status = %d, want 202", w.Code)
|
|
|
|
|
|
}
|
|
|
|
|
|
body := decodeBody(t, w)
|
|
|
|
|
|
if body["code"] != float64(100) {
|
|
|
|
|
|
t.Errorf("code = %v, want 100", body["code"])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-19 01:31:34 +08:00
|
|
|
|
|
|
|
|
|
|
// ---------- Middleware:trace_id 桥接 ----------
|
|
|
|
|
|
|
|
|
|
|
|
func newEngine() *gin.Engine {
|
|
|
|
|
|
r := gin.New()
|
|
|
|
|
|
r.Use(respgin.Middleware())
|
|
|
|
|
|
return r
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestMiddlewareBridgesTraceID(t *testing.T) {
|
|
|
|
|
|
r := newEngine()
|
|
|
|
|
|
r.GET("/t", func(c *gin.Context) {
|
|
|
|
|
|
c.Set("trace_id", "trace-from-gin")
|
|
|
|
|
|
respgin.Success(c, "ok")
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
r.ServeHTTP(w, httptest.NewRequest("GET", "/t", nil))
|
|
|
|
|
|
|
|
|
|
|
|
body := decodeBody(t, w)
|
|
|
|
|
|
if body["trace_id"] != "trace-from-gin" {
|
|
|
|
|
|
t.Errorf("trace_id = %v, want trace-from-gin (需要 Middleware 桥接)", body["trace_id"])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestMiddlewareTraceIDFromHeader(t *testing.T) {
|
|
|
|
|
|
r := newEngine()
|
|
|
|
|
|
r.GET("/t", func(c *gin.Context) {
|
|
|
|
|
|
respgin.Success(c, "ok")
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest("GET", "/t", nil)
|
|
|
|
|
|
req.Header.Set("X-Request-Id", "req-header-id")
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
r.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
|
|
body := decodeBody(t, w)
|
|
|
|
|
|
if body["trace_id"] != "req-header-id" {
|
|
|
|
|
|
t.Errorf("trace_id = %v, want req-header-id", body["trace_id"])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- Middleware:按 Accept-Language 翻译 ----------
|
|
|
|
|
|
|
|
|
|
|
|
func TestMiddlewareBridgesLanguage(t *testing.T) {
|
|
|
|
|
|
langx.AppendTrans("en", map[string]string{
|
|
|
|
|
|
"responsex_success": "success",
|
|
|
|
|
|
"not_found": "resource not found",
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
r := newEngine()
|
|
|
|
|
|
r.GET("/t", func(c *gin.Context) {
|
|
|
|
|
|
respgin.Success(c, "ok")
|
|
|
|
|
|
})
|
|
|
|
|
|
r.GET("/e", func(c *gin.Context) {
|
|
|
|
|
|
respgin.ErrorStr(c, "not_found")
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest("GET", "/t", nil)
|
|
|
|
|
|
req.Header.Set("Accept-Language", "en")
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
r.ServeHTTP(w, req)
|
|
|
|
|
|
if body := decodeBody(t, w); body["message"] != "success" {
|
|
|
|
|
|
t.Errorf("message = %v, want success", body["message"])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
req = httptest.NewRequest("GET", "/e", nil)
|
|
|
|
|
|
req.Header.Set("Accept-Language", "en")
|
|
|
|
|
|
w = httptest.NewRecorder()
|
|
|
|
|
|
r.ServeHTTP(w, req)
|
|
|
|
|
|
if body := decodeBody(t, w); body["message"] != "resource not found" {
|
|
|
|
|
|
t.Errorf("message = %v, want 'resource not found'", body["message"])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|