mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
feat(api): wrap JSON responses in envelope
Add response envelope middleware to standardize JSON responses as
`{code,data,message}` with consistent business codes across endpoints.
Update Swagger annotations and tests to reflect the new response shape.
BREAKING CHANGE: API responses are now wrapped in a response envelope; clients must read payloads from `data` and handle `code`/`message` fields.
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ez-api/ez-api/internal/middleware"
|
||||
"github.com/ez-api/ez-api/internal/model"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/sqlite"
|
||||
@@ -61,6 +62,7 @@ func TestMaster_ListSelfLogs_FiltersByMaster(t *testing.T) {
|
||||
}
|
||||
|
||||
r := gin.New()
|
||||
r.Use(middleware.ResponseEnvelope())
|
||||
r.GET("/v1/logs", withMaster(mh.ListSelfLogs))
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
@@ -70,9 +72,7 @@ func TestMaster_ListSelfLogs_FiltersByMaster(t *testing.T) {
|
||||
t.Fatalf("expected 200, got %d body=%s", rr.Code, rr.Body.String())
|
||||
}
|
||||
var resp ListMasterLogsResponse
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
env := decodeEnvelope(t, rr, &resp)
|
||||
if resp.Total != 1 || len(resp.Items) != 1 {
|
||||
t.Fatalf("expected 1 item, got total=%d len=%d body=%s", resp.Total, len(resp.Items), rr.Body.String())
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func TestMaster_ListSelfLogs_FiltersByMaster(t *testing.T) {
|
||||
}
|
||||
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &raw); err != nil {
|
||||
if err := json.Unmarshal(env.Data, &raw); err != nil {
|
||||
t.Fatalf("unmarshal raw: %v", err)
|
||||
}
|
||||
items, ok := raw["items"].([]any)
|
||||
@@ -141,6 +141,7 @@ func TestAdmin_DeleteLogs_BeforeFilters(t *testing.T) {
|
||||
|
||||
h := &Handler{db: db, logDB: db}
|
||||
r := gin.New()
|
||||
r.Use(middleware.ResponseEnvelope())
|
||||
r.DELETE("/admin/logs", h.DeleteLogs)
|
||||
|
||||
body := []byte(fmt.Sprintf(`{"before":"%s","key_id":1,"model":"m1"}`, cutoff.Format(time.RFC3339)))
|
||||
@@ -152,9 +153,7 @@ func TestAdmin_DeleteLogs_BeforeFilters(t *testing.T) {
|
||||
}
|
||||
|
||||
var resp DeleteLogsResponse
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
decodeEnvelope(t, rr, &resp)
|
||||
if resp.DeletedCount != 1 {
|
||||
t.Fatalf("expected deleted_count=1, got %d", resp.DeletedCount)
|
||||
}
|
||||
@@ -182,6 +181,7 @@ func TestAdmin_DeleteLogs_RequiresBefore(t *testing.T) {
|
||||
|
||||
h := &Handler{db: db, logDB: db}
|
||||
r := gin.New()
|
||||
r.Use(middleware.ResponseEnvelope())
|
||||
r.DELETE("/admin/logs", h.DeleteLogs)
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/admin/logs", bytes.NewReader([]byte(`{}`)))
|
||||
@@ -217,6 +217,7 @@ func TestAdmin_ListLogs_IncludesRequestBody(t *testing.T) {
|
||||
|
||||
h := &Handler{db: db, logDB: db}
|
||||
r := gin.New()
|
||||
r.Use(middleware.ResponseEnvelope())
|
||||
r.GET("/admin/logs", h.ListLogs)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/admin/logs", nil)
|
||||
@@ -227,9 +228,7 @@ func TestAdmin_ListLogs_IncludesRequestBody(t *testing.T) {
|
||||
}
|
||||
|
||||
var resp ListLogsResponse
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
decodeEnvelope(t, rr, &resp)
|
||||
if resp.Total != 1 || len(resp.Items) != 1 {
|
||||
t.Fatalf("expected 1 item, got total=%d len=%d", resp.Total, len(resp.Items))
|
||||
}
|
||||
@@ -264,6 +263,7 @@ func TestLogStats_GroupByModel(t *testing.T) {
|
||||
|
||||
h := &Handler{db: db, logDB: db}
|
||||
r := gin.New()
|
||||
r.Use(middleware.ResponseEnvelope())
|
||||
r.GET("/admin/logs/stats", h.LogStats)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/admin/logs/stats?group_by=model", nil)
|
||||
@@ -274,9 +274,7 @@ func TestLogStats_GroupByModel(t *testing.T) {
|
||||
}
|
||||
|
||||
var resp GroupedStatsResponse
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
decodeEnvelope(t, rr, &resp)
|
||||
if len(resp.Items) != 2 {
|
||||
t.Fatalf("expected 2 groups, got %d: %+v", len(resp.Items), resp.Items)
|
||||
}
|
||||
@@ -324,6 +322,7 @@ func TestLogStats_DefaultBehavior(t *testing.T) {
|
||||
|
||||
h := &Handler{db: db, logDB: db}
|
||||
r := gin.New()
|
||||
r.Use(middleware.ResponseEnvelope())
|
||||
r.GET("/admin/logs/stats", h.LogStats)
|
||||
|
||||
// Without group_by, should return aggregated stats
|
||||
@@ -335,9 +334,7 @@ func TestLogStats_DefaultBehavior(t *testing.T) {
|
||||
}
|
||||
|
||||
var resp LogStatsResponse
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
decodeEnvelope(t, rr, &resp)
|
||||
if resp.Total != 2 {
|
||||
t.Fatalf("expected total=2, got %d", resp.Total)
|
||||
}
|
||||
@@ -452,6 +449,7 @@ func TestTrafficChart_MinuteGranularityValidation(t *testing.T) {
|
||||
|
||||
h := &Handler{db: db, logDB: db}
|
||||
r := gin.New()
|
||||
r.Use(middleware.ResponseEnvelope())
|
||||
r.GET("/admin/logs/stats/traffic-chart", h.GetTrafficChart)
|
||||
|
||||
tests := []struct {
|
||||
@@ -503,8 +501,9 @@ func TestTrafficChart_MinuteGranularityValidation(t *testing.T) {
|
||||
}
|
||||
|
||||
var resp map[string]any
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
env := decodeEnvelope(t, rr, &resp)
|
||||
if env.Message != tt.wantError {
|
||||
t.Fatalf("expected message=%q, got %q", tt.wantError, env.Message)
|
||||
}
|
||||
if errMsg, ok := resp["error"].(string); !ok || errMsg != tt.wantError {
|
||||
t.Fatalf("expected error=%q, got %v", tt.wantError, resp["error"])
|
||||
|
||||
Reference in New Issue
Block a user