package redis import ( "context" "errors" "fmt" "math" "net/http/httptest" "os" "strings" "testing" "time" omni "git.yuntuo.hk/k2safe/go-omni" apierrors "git.yuntuo.hk/k2safe/go-omni/errors" "git.yuntuo.hk/k2safe/go-omni/httpx" "git.yuntuo.hk/k2safe/go-omni/middleware" "git.yuntuo.hk/k2safe/go-omni/response" goredis "github.com/redis/go-redis/v9" ) func TestTemplateErrorRedisReplay(t *testing.T) { address := os.Getenv("GO_OMNI_REDIS_TEST_ADDR") if address == "" { t.Skip("requires isolated Redis") } client := goredis.NewClient(&goredis.Options{Addr: address, PoolSize: 2, MaxRetries: -1}) defer client.Close() backend, err := NewIdempotencyStore(client, fmt.Sprint("template:", time.Now().UnixNano()), IdempotencyConfig{MaxEntries: 4}) if err != nil { t.Fatal(err) } defer client.Del(context.Background(), backend.keys...) var observed []*apierrors.APIError calls := 0 var bodies []string for _, template := range []string{"{label}不能超过{max}个字符", "changed: {label}/{max}"} { cat := apierrors.Catalog{DefaultStatus: 200, Definitions: map[string]apierrors.Definition{"LIMIT": {Code: int64(math.MaxInt64), Message: template, Params: []apierrors.Parameter{{Name: "label", Type: "string"}, {Name: "max", Type: "int64"}}}}} app := omni.New(omni.WithErrors(cat), omni.WithResponse(response.Config{CodeType: "int64", SuccessCode: 0, ErrorCode: 9999, MessageField: "msg", DataMode: "always"})) idem, err := middleware.NewIdempotency(middleware.IdempotencyConfig{Store: backend, Scope: func(*httpx.Context) (string, error) { return "fixture", nil }}) if err != nil { t.Fatal(err) } app.Use(func(next httpx.Handler) httpx.Handler { return func(c *httpx.Context) error { err := next(c); observed = append(observed, c.Failure()); return err } }) app.POST("/", func(c *httpx.Context) error { calls++ return apierrors.ByCode("LIMIT").WithParams(map[string]any{"label": "备注", "max": int64(math.MaxInt64)}).WithCause(errors.New("privateCause")) }, idem.Middleware()) w := httptest.NewRecorder() r := httptest.NewRequest("POST", "/", nil) r.Header.Set("Idempotency-Key", "same") app.ServeHTTP(w, r) app.Close() if w.Code != 200 || !strings.Contains(w.Body.String(), "备注不能超过9223372036854775807个字符") || !strings.Contains(w.Body.String(), `"code":9223372036854775807`) || !strings.Contains(w.Body.String(), `"data":null`) || strings.Contains(w.Body.String(), "privateCause") { t.Fatal(w.Code, w.Body.String()) } bodies = append(bodies, w.Body.String()) } if calls != 1 || bodies[0] != bodies[1] || len(observed) != 2 { t.Fatal(calls, bodies, observed) } for _, e := range observed { if e == nil || e.BusinessCode != int64(math.MaxInt64) || e.Message != "备注不能超过9223372036854775807个字符" { t.Fatal(e) } } if observed[1].Cause != nil { t.Fatal("persisted internal cause") } records, err := client.HVals(context.Background(), backend.keys[0]).Result() if err != nil { t.Fatal(err) } for _, raw := range records { if strings.Contains(raw, "privateCause") || strings.Contains(raw, `"params"`) { t.Fatal("private replay metadata") } } }