调整封装
This commit is contained in:
+481
-34
@@ -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))
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user