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:
zenfun
2026-01-10 00:15:08 +08:00
parent f400ffde95
commit 33838b1e2c
40 changed files with 771 additions and 371 deletions

View File

@@ -3,12 +3,12 @@ package api
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/alicebob/miniredis/v2"
"github.com/ez-api/ez-api/internal/middleware"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
)
@@ -21,6 +21,7 @@ func TestFeatureHandler_UpdateFeatures_LogOverrides(t *testing.T) {
h := NewFeatureHandler(rdb)
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.PUT("/admin/features", h.UpdateFeatures)
body := []byte(`{"log_retention_days":7,"log_max_records":123}`)
@@ -36,8 +37,12 @@ func TestFeatureHandler_UpdateFeatures_LogOverrides(t *testing.T) {
Updated map[string]string `json:"updated"`
}
var got resp
if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil {
t.Fatalf("decode response: %v", err)
env := decodeEnvelope(t, rr, &got)
if env.Code != "ok" {
t.Fatalf("expected code=ok, got %q", env.Code)
}
if env.Message != "" {
t.Fatalf("expected empty message, got %q", env.Message)
}
if got.Updated["log_retention_days"] != "7" {
t.Fatalf("expected log_retention_days=7, got %q", got.Updated["log_retention_days"])
@@ -75,6 +80,7 @@ func TestFeatureHandler_UpdateFeatures_LogOverridesClear(t *testing.T) {
h := NewFeatureHandler(rdb)
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.PUT("/admin/features", h.UpdateFeatures)
body := []byte(`{"log_retention_days":0,"log_max_records":0}`)
@@ -101,6 +107,7 @@ func TestFeatureHandler_UpdateFeatures_RegularKeys(t *testing.T) {
h := NewFeatureHandler(rdb)
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.PUT("/admin/features", h.UpdateFeatures)
body := []byte(`{"dp_context_preflight_enabled":false,"dp_state_store_backend":"redis","dp_claude_cross_upstream":"false","custom_number":12}`)
@@ -115,8 +122,9 @@ func TestFeatureHandler_UpdateFeatures_RegularKeys(t *testing.T) {
var got struct {
Updated map[string]string `json:"updated"`
}
if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil {
t.Fatalf("decode response: %v", err)
env := decodeEnvelope(t, rr, &got)
if env.Code != "ok" {
t.Fatalf("expected code=ok, got %q", env.Code)
}
if got.Updated["dp_context_preflight_enabled"] != "false" {
t.Fatalf("expected dp_context_preflight_enabled=false, got %q", got.Updated["dp_context_preflight_enabled"])
@@ -153,6 +161,7 @@ func TestFeatureHandler_UpdateFeatures_MixedKeys(t *testing.T) {
h := NewFeatureHandler(rdb)
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.PUT("/admin/features", h.UpdateFeatures)
body := []byte(`{"dp_context_preflight_enabled":true,"log_retention_days":5}`)
@@ -167,8 +176,9 @@ func TestFeatureHandler_UpdateFeatures_MixedKeys(t *testing.T) {
var got struct {
Updated map[string]string `json:"updated"`
}
if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil {
t.Fatalf("decode response: %v", err)
env := decodeEnvelope(t, rr, &got)
if env.Code != "ok" {
t.Fatalf("expected code=ok, got %q", env.Code)
}
if got.Updated["dp_context_preflight_enabled"] != "true" {
t.Fatalf("expected dp_context_preflight_enabled=true, got %q", got.Updated["dp_context_preflight_enabled"])
@@ -205,6 +215,7 @@ func TestFeatureHandler_ListFeatures_IncludesLogOverrides(t *testing.T) {
h := NewFeatureHandler(rdb)
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.GET("/admin/features", h.ListFeatures)
req := httptest.NewRequest(http.MethodGet, "/admin/features", nil)
@@ -218,8 +229,9 @@ func TestFeatureHandler_ListFeatures_IncludesLogOverrides(t *testing.T) {
var got struct {
Features map[string]string `json:"features"`
}
if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil {
t.Fatalf("decode response: %v", err)
env := decodeEnvelope(t, rr, &got)
if env.Code != "ok" {
t.Fatalf("expected code=ok, got %q", env.Code)
}
if got.Features["registration_code_enabled"] != "true" {
t.Fatalf("expected registration_code_enabled=true, got %q", got.Features["registration_code_enabled"])