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

@@ -1,7 +1,6 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
@@ -9,6 +8,7 @@ import (
"time"
"github.com/alicebob/miniredis/v2"
"github.com/ez-api/ez-api/internal/middleware"
"github.com/ez-api/ez-api/internal/model"
"github.com/ez-api/ez-api/internal/service"
"github.com/ez-api/foundation/tokenhash"
@@ -58,6 +58,7 @@ func TestAuthHandler_Whoami_InvalidIssuedAtEpoch_Returns401(t *testing.T) {
mr.HSet("auth:master:1", "status", "active")
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.GET("/auth/whoami", handler.Whoami)
req := httptest.NewRequest(http.MethodGet, "/auth/whoami", nil)
@@ -83,6 +84,7 @@ func TestAuthHandler_Whoami_InvalidMasterEpoch_Returns401(t *testing.T) {
mr.HSet("auth:master:1", "status", "active")
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.GET("/auth/whoami", handler.Whoami)
req := httptest.NewRequest(http.MethodGet, "/auth/whoami", nil)
@@ -128,6 +130,7 @@ func TestAuthHandler_Whoami_KeyResponseIncludesIPRules(t *testing.T) {
}
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.GET("/auth/whoami", handler.Whoami)
req := httptest.NewRequest(http.MethodGet, "/auth/whoami", nil)
@@ -140,8 +143,12 @@ func TestAuthHandler_Whoami_KeyResponseIncludesIPRules(t *testing.T) {
}
var resp map[string]any
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
env := decodeEnvelope(t, rr, &resp)
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 resp["allow_ips"] != "1.2.3.4" {
t.Fatalf("expected allow_ips, got %v", resp["allow_ips"])
@@ -187,6 +194,7 @@ func TestAuthHandler_Whoami_ExpiredKey_Returns401(t *testing.T) {
}
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.GET("/auth/whoami", handler.Whoami)
req := httptest.NewRequest(http.MethodGet, "/auth/whoami", nil)
@@ -199,8 +207,12 @@ func TestAuthHandler_Whoami_ExpiredKey_Returns401(t *testing.T) {
}
var resp map[string]any
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
env := decodeEnvelope(t, rr, &resp)
if env.Code != "unauthorized" {
t.Fatalf("expected code=unauthorized, got %q", env.Code)
}
if env.Message != "token has expired" {
t.Fatalf("expected message 'token has expired', got %q", env.Message)
}
if resp["error"] != "token has expired" {
t.Fatalf("expected 'token has expired' error, got %v", resp["error"])