调整封装

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
-37
View File
@@ -1,37 +0,0 @@
package responsex
import (
"github.com/gin-gonic/gin"
)
func GinError(ctx *gin.Context, err error) {
Error(ctx, ctx.Writer, err)
}
func GinErrorData(ctx *gin.Context, err error, data any) {
ErrorWithData(ctx, ctx.Writer, err, data)
}
func GinErrorStr(ctx *gin.Context, msg string) {
ErrorStr(ctx, ctx.Writer, msg)
}
func GinSuccess(ctx *gin.Context, data any) {
Success(ctx, ctx.Writer, data)
}
func GinSuccessWithPage(ctx *gin.Context, data any, page pagination) {
SuccessWithPage(ctx, ctx.Writer, data, &page)
}
func GinOptions(ctx *gin.Context, ops ...Option) {
ResponseCtx(ctx, ctx.Writer, ops...)
}
func GinMsgMapData(ctx *gin.Context, msgKey string, format map[string]string, data any) {
FormatMessage(ctx, ctx.Writer, msgKey, format, data)
}
func GinMsgKV(ctx *gin.Context, msgKey string, k string, v string) {
FormatMessage(ctx, ctx.Writer, msgKey, map[string]string{k: v}, nil)
}
+38
View File
@@ -0,0 +1,38 @@
package gin
import (
"code.yun.ink/pkg/responsex"
"github.com/gin-gonic/gin"
)
func Error(ctx *gin.Context, err error, ops ...responsex.Option) {
responsex.Error(ctx, ctx.Writer, err, ops...)
}
func ErrorWithData(ctx *gin.Context, err error, data any, ops ...responsex.Option) {
responsex.ErrorWithData(ctx, ctx.Writer, err, data, ops...)
}
func ErrorStr(ctx *gin.Context, msg string, ops ...responsex.Option) {
responsex.ErrorStr(ctx, ctx.Writer, msg, ops...)
}
func Success(ctx *gin.Context, data any, ops ...responsex.Option) {
responsex.Success(ctx, ctx.Writer, data, ops...)
}
func SuccessWithPage(ctx *gin.Context, data any, page responsex.Pagination, ops ...responsex.Option) {
responsex.SuccessWithPage(ctx, ctx.Writer, data, &page, ops...)
}
func Custom(ctx *gin.Context, ops ...responsex.Option) {
responsex.ResponseCtx(ctx, ctx.Writer, ops...)
}
func MsgMapData(ctx *gin.Context, msgKey string, format map[string]string, data any, ops ...responsex.Option) {
responsex.FormatMessage(ctx, ctx.Writer, msgKey, format, data, ops...)
}
func MsgKV(ctx *gin.Context, msgKey string, k string, v string, ops ...responsex.Option) {
responsex.FormatMessage(ctx, ctx.Writer, msgKey, map[string]string{k: v}, nil, ops...)
}
+277
View File
@@ -0,0 +1,277 @@
package gin_test
import (
"encoding/json"
"errors"
"net/http/httptest"
"testing"
"code.yun.ink/pkg/responsex"
respgin "code.yun.ink/pkg/responsex/gin"
"github.com/gin-gonic/gin"
)
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)
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)
if body["message"] == "" {
t.Error("message should not be empty")
}
}
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"])
}
}
+12 -3
View File
@@ -4,25 +4,29 @@ go 1.22.4
require (
github.com/gin-gonic/gin v1.9.1
github.com/yuninks/errorx v0.0.1
github.com/glebarez/sqlite v1.11.0
github.com/yuninks/errorx v1.0.0
github.com/yuninks/langx v0.0.7
github.com/zeromicro/go-zero v1.6.4
gorm.io/gorm v1.25.9
gorm.io/gorm v1.30.0
)
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
@@ -35,6 +39,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/openzipkin/zipkin-go v0.4.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
@@ -54,10 +59,14 @@ require (
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/text v0.20.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/grpc v1.63.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
)
+27 -6
View File
@@ -9,6 +9,8 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583j
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
@@ -17,6 +19,10 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -37,6 +43,10 @@ github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwm
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -72,6 +82,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
@@ -94,8 +107,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuninks/errorx v0.0.1 h1:QFSSHnlCfRPPefzspgksGU+eYW5cSr/jkSG1IalM6yM=
github.com/yuninks/errorx v0.0.1/go.mod h1:N9e4ShfCTkLjHnGShAG0x9n2SYtCDKhKDMxRmyr05hU=
github.com/yuninks/errorx v1.0.0 h1:/AS4dSEVPkfvs3hJFYgqYBFLKyVvxFV32XXbMXoHwvk=
github.com/yuninks/errorx v1.0.0/go.mod h1:N9e4ShfCTkLjHnGShAG0x9n2SYtCDKhKDMxRmyr05hU=
github.com/yuninks/langx v0.0.7 h1:QUt27h/FjnT7//759GoM/+p4ew+HBr10ZN/fkqeQfyA=
github.com/yuninks/langx v0.0.7/go.mod h1:nAylzjNIjCThhEQSJsIKP8Vhja0aJlcyig+NsNPZLSk=
github.com/zeromicro/go-zero v1.6.4 h1:GvZXxxwl1Lby/gIHxHwN/ZNmXl1WFJa1DvoVgqgttUs=
@@ -138,8 +151,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0=
@@ -156,6 +169,14 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+1 -1
View File
@@ -1,4 +1,4 @@
package responsex
package gormx
import "gorm.io/gorm"
+115
View File
@@ -0,0 +1,115 @@
package gormx_test
import (
"testing"
"code.yun.ink/pkg/responsex/gormx"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
// newDB 创建内存 SQLite 数据库用于测试
func newDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite failed: %v", err)
}
return db
}
func TestGetDbOffset(t *testing.T) {
tests := []struct {
name string
page, size int64
wantOffset int
wantLimit int
}{
{"page1 size10", 1, 10, 0, 10},
{"page2 size10", 2, 10, 10, 10},
{"page3 size20", 3, 20, 40, 20},
{"page0 size10 default page to 1", 0, 10, 0, 10},
{"page1 size0 default size to 10", 1, 0, 0, 10},
{"both negative or zero max 1000", 0, 0, 0, 1000},
{"negative page default to 1", -1, 10, 0, 10},
{"negative size default to 10", 1, -1, 0, 10},
{"both negative", -1, -1, 0, 1000},
{"page5 size100", 5, 100, 400, 100},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db := newDB(t)
result := gormx.GetDbOffset(db, tt.page, tt.size)
stmt := result.Statement
// 验证 Limit 被正确设置
if stmt == nil {
t.Fatal("statement is nil")
}
})
}
}
func TestGetDbOffset_Chaining(t *testing.T) {
db := newDB(t)
db.AutoMigrate(&testModel{})
// 插入测试数据
for i := 1; i <= 50; i++ {
db.Create(&testModel{Value: i})
}
// 分页查询:page=2, size=10 → offset=10, limit=10
var results []testModel
db2 := gormx.GetDbOffset(db, 2, 10)
db2.Find(&results)
if len(results) != 10 {
t.Errorf("results length = %d, want 10", len(results))
}
// 第一页是 1-10,第二页 offset=10 应该是 11-20
if results[0].Value < 11 || results[0].Value > 20 {
t.Errorf("first result Value = %d, should be in [11,20]", results[0].Value)
}
}
func TestGetDbOffset_FirstPage(t *testing.T) {
db := newDB(t)
db.AutoMigrate(&testModel{})
for i := 1; i <= 15; i++ {
db.Create(&testModel{Value: i})
}
var results []testModel
gormx.GetDbOffset(db, 1, 10).Find(&results)
if len(results) != 10 {
t.Errorf("results length = %d, want 10", len(results))
}
if results[0].Value != 1 {
t.Errorf("first value = %d, want 1", results[0].Value)
}
}
func TestGetDbOffset_NoLimit(t *testing.T) {
db := newDB(t)
db.AutoMigrate(&testModel{})
for i := 1; i <= 50; i++ {
db.Create(&testModel{Value: i})
}
// page <= 0 && size <= 0 → limit = 1000
var results []testModel
gormx.GetDbOffset(db, 0, 0).Find(&results)
if len(results) != 50 {
t.Errorf("results length = %d, want 50 (all records)", len(results))
}
}
type testModel struct {
ID uint `gorm:"primaryKey"`
Value int
}
+30 -6
View File
@@ -12,10 +12,12 @@ type options struct {
defaultSuccessCode int
defaultErrorCode int
code int
data any
message string
pagination *pagination
code int
data any
message string
pagination *Pagination
httpStatusCode int
headers map[string]string
}
var op *options = nil
@@ -62,7 +64,7 @@ func SetMessage(message string) Option {
}
}
func SetPage(page *pagination) Option {
func SetPage(page *Pagination) Option {
return func(o *options) {
o.pagination = page
}
@@ -103,7 +105,29 @@ func SetTraceId(traceId string) Option {
}
}
// trace_id如何获取
// 设置HTTP状态码
func SetHttpStatusCode(code int) Option {
return func(o *options) {
o.httpStatusCode = code
}
}
// 设置单个响应头
func SetHeader(key, value string) Option {
return func(o *options) {
if o.headers == nil {
o.headers = make(map[string]string)
}
o.headers[key] = value
}
}
// 批量设置响应头
func SetHeaders(headers map[string]string) Option {
return func(o *options) {
o.headers = headers
}
}
type Logger interface {
Info(ctx context.Context, args ...interface{})
+43 -20
View File
@@ -18,11 +18,11 @@ type response struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
Pagination *pagination `json:"pagination,omitempty"`
Pagination *Pagination `json:"pagination,omitempty"`
TraceId string `json:"trace_id,omitempty"`
}
type pagination struct {
type Pagination struct {
Page int64 `json:"page"`
Size int64 `json:"size"`
TotalPage int64 `json:"total_page"`
@@ -44,8 +44,8 @@ func GetOffset(page, size int) (offset int, limit int) {
}
// 计算响应分页
func GetPage(page, size, totalCount int64) pagination {
pa := pagination{}
func GetPage(page, size, totalCount int64) Pagination {
pa := Pagination{}
totalPage := 0
if size != 0 {
totalPage = int(math.Ceil(float64(totalCount) / float64(size)))
@@ -58,47 +58,58 @@ func GetPage(page, size, totalCount int64) pagination {
}
// Msg格式化响应
func FormatMessage(ctx context.Context, w http.ResponseWriter, msgKey string, format map[string]string, data interface{}) {
func FormatMessage(ctx context.Context, w http.ResponseWriter, msgKey string, format map[string]string, data interface{}, ops ...Option) {
code, msg := langx.GetTransFormatCtx(ctx, msgKey, format)
ResponseCtx(ctx, w, SetCode(code), SetMessage(msg), SetData(data))
baseOps := []Option{SetCode(code), SetMessage(msg), SetData(data)}
baseOps = append(baseOps, ops...)
ResponseCtx(ctx, w, baseOps...)
}
// 成功的响应
func Success(ctx context.Context, w http.ResponseWriter, data any) {
ResponseCtx(ctx, w, SetCode(op.defaultSuccessCode), SetMessage("请求成功"), SetData(data))
func Success(ctx context.Context, w http.ResponseWriter, data any, ops ...Option) {
baseOps := []Option{SetCode(op.defaultSuccessCode), SetMessage("请求成功"), SetData(data)}
baseOps = append(baseOps, ops...)
ResponseCtx(ctx, w, baseOps...)
}
func SuccessWithPage(ctx context.Context, w http.ResponseWriter, data any, page *pagination) {
ResponseCtx(ctx, w, SetCode(op.defaultSuccessCode), SetMessage("请求成功"), SetData(data), SetPage(page))
func SuccessWithPage(ctx context.Context, w http.ResponseWriter, data any, page *Pagination, ops ...Option) {
baseOps := []Option{SetCode(op.defaultSuccessCode), SetMessage("请求成功"), SetData(data), SetPage(page)}
baseOps = append(baseOps, ops...)
ResponseCtx(ctx, w, baseOps...)
}
// 失败的响应
func Error(ctx context.Context, w http.ResponseWriter, err error) {
ErrorWithData(ctx, w, err, "")
func Error(ctx context.Context, w http.ResponseWriter, err error, ops ...Option) {
ErrorWithData(ctx, w, err, "", ops...)
}
func ErrorWithData(ctx context.Context, w http.ResponseWriter, err error, data interface{}) {
func ErrorWithData(ctx context.Context, w http.ResponseWriter, err error, data interface{}, ops ...Option) {
code := op.defaultErrorCode
msg := "请求失败"
if err != nil {
val, ok := err.(errorx.ErrorInterface)
val,ok := errorx.As(err)
if ok {
if langx.GetDefaultCode() != val.GetCode() {
code = val.GetCode()
if langx.GetDefaultCode() != val.Code() {
code = val.Code()
}
}
msg = err.Error()
}
ResponseCtx(ctx, w, SetCode(code), SetMessage(msg), SetData(data))
baseOps := []Option{SetCode(code), SetMessage(msg), SetData(data)}
baseOps = append(baseOps, ops...)
ResponseCtx(ctx, w, baseOps...)
}
func ErrorStr(ctx context.Context, w http.ResponseWriter, msg string) {
func ErrorStr(ctx context.Context, w http.ResponseWriter, msg string, ops ...Option) {
msg = langx.GetMsgCtx(ctx, msg)
code := langx.GetCode(msg)
if code == langx.GetDefaultCode() {
code = op.defaultErrorCode
}
ResponseCtx(ctx, w, SetCode(code), SetMessage(msg))
baseOps := []Option{SetCode(code), SetMessage(msg)}
baseOps = append(baseOps, ops...)
ResponseCtx(ctx, w, baseOps...)
}
// 基础的响应
@@ -136,8 +147,20 @@ func ResponseCtx(ctx context.Context, w http.ResponseWriter, ops ...Option) {
def.logger.Info(ctx, "response:", string(bs))
}
// 写入自定义响应头
for k, v := range def.headers {
w.Header().Set(k, v)
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(200)
// 确定HTTP状态码
httpStatus := def.httpStatusCode
if httpStatus == 0 {
httpStatus = 200
}
w.WriteHeader(httpStatus)
n, err := w.Write(bs)
if err != nil || n != len(bs) {
op.logger.Error(ctx, "write response failed err:%+v", err)
+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))
}