调整封装

This commit is contained in:
Yun
2026-07-22 01:17:07 +08:00
parent d3786198df
commit 69f90115dc
10 changed files with 1024 additions and 107 deletions
+481 -34
View File
@@ -1,44 +1,491 @@
package responsex_test
import (
"io"
"net/http"
"context"
"encoding/json"
"errors"
"net/http/httptest"
"testing"
"time"
"code.yun.ink/pkg/responsex"
"github.com/gin-gonic/gin"
)
func TestResponse(t *testing.T) {
go func() {
g := gin.Default()
g.GET("/", func(ctx *gin.Context) {
responsex.GinSuccess(ctx, "hello world")
})
// 启动服务
// http://localhost:8080
g.Run(":8080")
}()
time.Sleep(1 * time.Second)
// 测试响应
// 发送请求
// curl http://localhost:8080
// 期望响应: {"code":200,"msg":"请求成功","data":"hello world"}
resp, err := http.Get("http://localhost:8080")
if err != nil {
t.Fatal(err)
// decodeBody 解码响应体为 map,失败则 Fatal
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
}
// ---------- Success ----------
func TestSuccess(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.Success(ctx, w, "hello")
body := decodeBody(t, w)
if body["code"] != float64(200) {
t.Errorf("code = %v, want 200", body["code"])
}
if body["message"] != "请求成功" {
t.Errorf("message = %v, want 请求成功", body["message"])
}
if body["data"] != "hello" {
t.Errorf("data = %v, want hello", body["data"])
}
if w.Code != 200 {
t.Errorf("http status = %d, want 200", w.Code)
}
}
func TestSuccessNilData(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.Success(ctx, w, nil)
body := decodeBody(t, w)
if _, ok := body["data"]; ok {
t.Errorf("data should be omitted for nil, got %v", body["data"])
}
}
func TestSuccessWithHttpStatusCode(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.Success(ctx, w, "created",
responsex.SetHttpStatusCode(201),
)
body := decodeBody(t, w)
if body["code"] != float64(200) {
t.Errorf("code = %v, want 200", body["code"])
}
if w.Code != 201 {
t.Errorf("http status = %d, want 201", w.Code)
}
}
func TestSuccessWithHeader(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.Success(ctx, w, "ok",
responsex.SetHeader("X-Request-Id", "req-123"),
responsex.SetHeader("X-Custom", "custom-val"),
)
if v := w.Header().Get("X-Request-Id"); v != "req-123" {
t.Errorf("X-Request-Id = %q, want req-123", v)
}
if v := w.Header().Get("X-Custom"); v != "custom-val" {
t.Errorf("X-Custom = %q, want custom-val", v)
}
}
func TestSuccessWithHeaders(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.Success(ctx, w, "ok",
responsex.SetHeaders(map[string]string{
"X-One": "1",
"X-Two": "2",
}),
)
if v := w.Header().Get("X-One"); v != "1" {
t.Errorf("X-One = %q, want 1", v)
}
if v := w.Header().Get("X-Two"); v != "2" {
t.Errorf("X-Two = %q, want 2", v)
}
}
func TestSuccessMultipleSetHeader(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.Success(ctx, w, "ok",
responsex.SetHeader("X-A", "a1"),
responsex.SetHeader("X-A", "a2"), // 同 key 多次 SetHeader 覆盖
)
if v := w.Header().Get("X-A"); v != "a2" {
t.Errorf("X-A = %q, want a2", v)
}
}
// ---------- Error ----------
func TestError(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.Error(ctx, w, errors.New("something went wrong"))
body := decodeBody(t, w)
if body["code"] != float64(400) {
t.Errorf("code = %v, want 400", body["code"])
}
if body["message"] != "something went wrong" {
t.Errorf("message = %v, want 'something went wrong'", body["message"])
}
if w.Code != 200 {
t.Errorf("http status = %d, want 200 (default)", w.Code)
}
}
func TestErrorWithHttpStatusCode(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.Error(ctx, w, errors.New("internal 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()
ctx := context.Background()
responsex.Error(ctx, w, errors.New("bad request"),
responsex.SetHttpStatusCode(400),
responsex.SetHeader("X-Error-Code", "VALIDATION_001"),
)
if w.Code != 400 {
t.Errorf("http status = %d, want 400", w.Code)
}
if v := w.Header().Get("X-Error-Code"); v != "VALIDATION_001" {
t.Errorf("X-Error-Code = %q, want VALIDATION_001", v)
}
}
func TestErrorWithData(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.ErrorWithData(ctx, w, errors.New("validation failed"), map[string]any{
"field": "email",
"msg": "invalid format",
})
body := decodeBody(t, w)
if body["code"] != float64(400) {
t.Errorf("code = %v, want 400", body["code"])
}
data, ok := body["data"].(map[string]any)
if !ok {
t.Fatalf("data should be a map, got %T", body["data"])
}
if data["field"] != "email" {
t.Errorf("data.field = %v, want email", data["field"])
}
}
func TestErrorWithDataAndOptions(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.ErrorWithData(ctx, w, errors.New("gone"), "resource deleted",
responsex.SetHttpStatusCode(410),
responsex.SetHeader("X-Deleted-At", "2024-01-01"),
)
if w.Code != 410 {
t.Errorf("http status = %d, want 410", w.Code)
}
body := decodeBody(t, w)
if body["data"] != "resource deleted" {
t.Errorf("data = %v, want 'resource deleted'", body["data"])
}
}
func TestErrorNilError(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.Error(ctx, w, nil)
body := decodeBody(t, w)
if body["code"] != float64(400) {
t.Errorf("code = %v, want 400", body["code"])
}
if body["message"] != "请求失败" {
t.Errorf("message = %v, want 请求失败", body["message"])
}
}
// ---------- ErrorStr ----------
func TestErrorStr(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.ErrorStr(ctx, w, "unauthorized")
body := decodeBody(t, w)
if body["message"] == "" {
t.Error("message should not be empty")
}
}
func TestErrorStrWithOptions(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.ErrorStr(ctx, w, "forbidden",
responsex.SetHttpStatusCode(403),
responsex.SetHeader("X-Reason", "no permission"),
)
if w.Code != 403 {
t.Errorf("http status = %d, want 403", w.Code)
}
}
// ---------- SuccessWithPage ----------
func TestSuccessWithPage(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
page := &responsex.Pagination{
Page: 2,
Size: 10,
TotalPage: 5,
TotalCount: 50,
}
responsex.SuccessWithPage(ctx, w, []string{"a", "b"}, page)
body := decodeBody(t, w)
pagination, ok := body["pagination"].(map[string]any)
if !ok {
t.Fatalf("pagination should be a map, got %T", body["pagination"])
}
if pagination["page"] != float64(2) {
t.Errorf("page = %v, want 2", pagination["page"])
}
if pagination["total_count"] != float64(50) {
t.Errorf("total_count = %v, want 50", pagination["total_count"])
}
}
func TestSuccessWithPageAndOptions(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
page := &responsex.Pagination{Page: 1, Size: 20, TotalPage: 3, TotalCount: 50}
responsex.SuccessWithPage(ctx, w, []int{1, 2}, page,
responsex.SetHeader("X-Total-Count", "50"),
responsex.SetHeader("X-Page", "1"),
)
if v := w.Header().Get("X-Total-Count"); v != "50" {
t.Errorf("X-Total-Count = %q, want 50", v)
}
}
// ---------- ResponseCtx ----------
func TestResponseCtx_Custom(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.ResponseCtx(ctx, w,
responsex.SetCode(0),
responsex.SetMessage("custom msg"),
responsex.SetHttpStatusCode(418),
responsex.SetHeaders(map[string]string{"X-Teapot": "true"}),
)
if w.Code != 418 {
t.Errorf("http status = %d, want 418", w.Code)
}
if v := w.Header().Get("X-Teapot"); v != "true" {
t.Errorf("X-Teapot = %q, want true", v)
}
body := decodeBody(t, w)
if body["code"] != float64(0) {
t.Errorf("code = %v, want 0", body["code"])
}
}
func TestResponseCtx_HttpStatusDefault(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
// 不设置 SetHttpStatusCode,默认应为 200
responsex.ResponseCtx(ctx, w,
responsex.SetCode(500),
responsex.SetMessage("error"),
)
if w.Code != 200 {
t.Errorf("http status = %d, want 200 (default)", w.Code)
}
}
// ---------- FormatMessage ----------
func TestFormatMessage(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.FormatMessage(ctx, w, "test_key", map[string]string{
"name": "world",
}, "some_data")
body := decodeBody(t, w)
// FormatMessage 内部调用 langx,此处仅验证结构完整性和 HTTP 200
if w.Code != 200 {
t.Errorf("http status = %d, want 200", w.Code)
}
_ = body
}
func TestFormatMessageWithOptions(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.FormatMessage(ctx, w, "key", map[string]string{"k": "v"}, nil,
responsex.SetHeader("X-Lang", "zh-CN"),
)
if v := w.Header().Get("X-Lang"); v != "zh-CN" {
t.Errorf("X-Lang = %q, want zh-CN", v)
}
}
// ---------- GetOffset ----------
func TestGetOffset(t *testing.T) {
tests := []struct {
name string
page, size int
wantOffset int
wantLimit int
}{
{"page 1 size 10", 1, 10, 0, 10},
{"page 2 size 10", 2, 10, 10, 10},
{"page 3 size 20", 3, 20, 40, 20},
{"page 0 defaults to 1", 0, 10, 0, 10},
{"size 0 defaults to 10", 1, 0, 0, 10},
{"negative page defaults to 1", -1, 10, 0, 10},
{"both zero", 0, 0, 0, 10},
{"page 5 size 100", 5, 100, 400, 100},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
offset, limit := responsex.GetOffset(tt.page, tt.size)
if offset != tt.wantOffset || limit != tt.wantLimit {
t.Errorf("GetOffset(%d,%d) = (%d,%d), want (%d,%d)",
tt.page, tt.size, offset, limit, tt.wantOffset, tt.wantLimit)
}
})
}
}
// ---------- GetPage ----------
func TestGetPage(t *testing.T) {
tests := []struct {
name string
page, size, total int64
wantPage, wantSize int64
wantTotalPage int64
wantTotalCount int64
}{
{"normal", 1, 10, 100, 1, 10, 10, 100},
{"partial last page", 3, 10, 25, 3, 10, 3, 25},
{"size zero", 1, 0, 100, 1, 0, 0, 100},
{"total zero", 1, 10, 0, 1, 10, 0, 0},
{"exact division", 1, 25, 100, 1, 25, 4, 100},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := responsex.GetPage(tt.page, tt.size, tt.total)
if p.Page != tt.wantPage {
t.Errorf("Page = %d, want %d", p.Page, tt.wantPage)
}
if p.Size != tt.wantSize {
t.Errorf("Size = %d, want %d", p.Size, tt.wantSize)
}
if p.TotalPage != tt.wantTotalPage {
t.Errorf("TotalPage = %d, want %d", p.TotalPage, tt.wantTotalPage)
}
if p.TotalCount != tt.wantTotalCount {
t.Errorf("TotalCount = %d, want %d", p.TotalCount, tt.wantTotalCount)
}
})
}
}
// ---------- SetCode ----------
func TestSetCodeOption(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.ResponseCtx(ctx, w,
responsex.SetCode(1001),
responsex.SetMessage("custom code"),
)
body := decodeBody(t, w)
if body["code"] != float64(1001) {
t.Errorf("code = %v, want 1001", body["code"])
}
}
// ---------- Content-Type ----------
func TestContentTypeHeader(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.Success(ctx, w, "ok")
ct := w.Header().Get("Content-Type")
if ct != "application/json; charset=utf-8" {
t.Errorf("Content-Type = %q, want application/json; charset=utf-8", ct)
}
}
// ---------- JSON Structure ----------
func TestErrorResponseJsonStructure(t *testing.T) {
w := httptest.NewRecorder()
ctx := context.Background()
responsex.Error(ctx, w, errors.New("not found"))
var resp struct {
Code int `json:"code"`
Message string `json:"message"`
}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("json unmarshal failed: %v", err)
}
if resp.Code != 400 {
t.Errorf("Code = %d, want 400", resp.Code)
}
if resp.Message != "not found" {
t.Errorf("Message = %q, want 'not found'", resp.Message)
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
t.Log(string(b))
}