Files
ez-api/internal/api/feature_handler_test.go
zenfun 33838b1e2c 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.
2026-01-10 00:15:08 +08:00

246 lines
7.9 KiB
Go

package api
import (
"bytes"
"context"
"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"
)
func TestFeatureHandler_UpdateFeatures_LogOverrides(t *testing.T) {
gin.SetMode(gin.TestMode)
mr := miniredis.RunT(t)
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
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}`)
req := httptest.NewRequest(http.MethodPut, "/admin/features", bytes.NewReader(body))
rr := httptest.NewRecorder()
r.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rr.Code, rr.Body.String())
}
type resp struct {
Updated map[string]string `json:"updated"`
}
var got resp
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"])
}
if got.Updated["log_max_records"] != "123" {
t.Fatalf("expected log_max_records=123, got %q", got.Updated["log_max_records"])
}
if v, _ := mr.Get("meta:log:retention_days"); v != "7" {
t.Fatalf("expected redis retention_days=7, got %q", v)
}
if v, _ := mr.Get("meta:log:max_records"); v != "123" {
t.Fatalf("expected redis max_records=123, got %q", v)
}
if v := mr.HGet("meta:features", "log_retention_days"); v != "" {
t.Fatalf("expected no log_retention_days in meta:features, got %q", v)
}
if v := mr.HGet("meta:features", "log_max_records"); v != "" {
t.Fatalf("expected no log_max_records in meta:features, got %q", v)
}
}
func TestFeatureHandler_UpdateFeatures_LogOverridesClear(t *testing.T) {
gin.SetMode(gin.TestMode)
mr := miniredis.RunT(t)
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
if err := rdb.Set(context.Background(), "meta:log:retention_days", "9", 0).Err(); err != nil {
t.Fatalf("seed retention: %v", err)
}
if err := rdb.Set(context.Background(), "meta:log:max_records", "999", 0).Err(); err != nil {
t.Fatalf("seed max_records: %v", err)
}
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}`)
req := httptest.NewRequest(http.MethodPut, "/admin/features", bytes.NewReader(body))
rr := httptest.NewRecorder()
r.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rr.Code, rr.Body.String())
}
if mr.Exists("meta:log:retention_days") {
t.Fatalf("expected retention_days to be cleared")
}
if mr.Exists("meta:log:max_records") {
t.Fatalf("expected max_records to be cleared")
}
}
func TestFeatureHandler_UpdateFeatures_RegularKeys(t *testing.T) {
gin.SetMode(gin.TestMode)
mr := miniredis.RunT(t)
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
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}`)
req := httptest.NewRequest(http.MethodPut, "/admin/features", bytes.NewReader(body))
rr := httptest.NewRecorder()
r.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rr.Code, rr.Body.String())
}
var got struct {
Updated map[string]string `json:"updated"`
}
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"])
}
if got.Updated["dp_state_store_backend"] != "redis" {
t.Fatalf("expected dp_state_store_backend=redis, got %q", got.Updated["dp_state_store_backend"])
}
if got.Updated["dp_claude_cross_upstream"] != "false" {
t.Fatalf("expected dp_claude_cross_upstream=false, got %q", got.Updated["dp_claude_cross_upstream"])
}
if got.Updated["custom_number"] != "12" {
t.Fatalf("expected custom_number=12, got %q", got.Updated["custom_number"])
}
if v := mr.HGet("meta:features", "dp_context_preflight_enabled"); v != "false" {
t.Fatalf("expected redis dp_context_preflight_enabled=false, got %q", v)
}
if v := mr.HGet("meta:features", "dp_state_store_backend"); v != "redis" {
t.Fatalf("expected redis dp_state_store_backend=redis, got %q", v)
}
if v := mr.HGet("meta:features", "dp_claude_cross_upstream"); v != "false" {
t.Fatalf("expected redis dp_claude_cross_upstream=false, got %q", v)
}
if v := mr.HGet("meta:features", "custom_number"); v != "12" {
t.Fatalf("expected redis custom_number=12, got %q", v)
}
}
func TestFeatureHandler_UpdateFeatures_MixedKeys(t *testing.T) {
gin.SetMode(gin.TestMode)
mr := miniredis.RunT(t)
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
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}`)
req := httptest.NewRequest(http.MethodPut, "/admin/features", bytes.NewReader(body))
rr := httptest.NewRecorder()
r.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rr.Code, rr.Body.String())
}
var got struct {
Updated map[string]string `json:"updated"`
}
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"])
}
if got.Updated["log_retention_days"] != "5" {
t.Fatalf("expected log_retention_days=5, got %q", got.Updated["log_retention_days"])
}
if v := mr.HGet("meta:features", "dp_context_preflight_enabled"); v != "true" {
t.Fatalf("expected redis dp_context_preflight_enabled=true, got %q", v)
}
if v, _ := mr.Get("meta:log:retention_days"); v != "5" {
t.Fatalf("expected redis retention_days=5, got %q", v)
}
}
func TestFeatureHandler_ListFeatures_IncludesLogOverrides(t *testing.T) {
gin.SetMode(gin.TestMode)
mr := miniredis.RunT(t)
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
if err := rdb.HSet(context.Background(), "meta:features", map[string]any{
"registration_code_enabled": "true",
}).Err(); err != nil {
t.Fatalf("seed features: %v", err)
}
if err := rdb.Set(context.Background(), "meta:log:retention_days", "14", 0).Err(); err != nil {
t.Fatalf("seed retention: %v", err)
}
if err := rdb.Set(context.Background(), "meta:log:max_records", "2048", 0).Err(); err != nil {
t.Fatalf("seed max_records: %v", err)
}
h := NewFeatureHandler(rdb)
r := gin.New()
r.Use(middleware.ResponseEnvelope())
r.GET("/admin/features", h.ListFeatures)
req := httptest.NewRequest(http.MethodGet, "/admin/features", nil)
rr := httptest.NewRecorder()
r.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", rr.Code, rr.Body.String())
}
var got struct {
Features map[string]string `json:"features"`
}
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"])
}
if got.Features["log_retention_days"] != "14" {
t.Fatalf("expected log_retention_days=14, got %q", got.Features["log_retention_days"])
}
if got.Features["log_max_records"] != "2048" {
t.Fatalf("expected log_max_records=2048, got %q", got.Features["log_max_records"])
}
}