package app_test import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "sync" "testing" 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/i18n" "git.yuntuo.hk/k2safe/go-omni/i18n/errori18n" "git.yuntuo.hk/k2safe/go-omni/i18n/httpi18n" "git.yuntuo.hk/k2safe/go-omni/middleware" "git.yuntuo.hk/k2safe/go-omni/response" ) func localizedCatalog(namespace string, status int) apierrors.Catalog { return errori18n.Bind(apierrors.Catalog{DefaultStatus: status, Definitions: map[string]apierrors.Definition{ "SAME": {Code: int64(9223372036854775807), Message: namespace + ":{value}", Params: []apierrors.Parameter{{Name: "value", Type: "string"}}}, "INTERNAL_ERROR": {Status: 500, Code: int64(500), Message: "internal"}, "FIXED": {Status: 401, Code: int64(401), Message: "fixed {literal}"}, }}, namespace) } func localizedOptions(t *testing.T, catalogs ...apierrors.Catalog) (*httpi18n.Policy, *errori18n.Localizer) { t.Helper() docs := []i18n.Document{} for _, catalog := range catalogs { source, err := errori18n.Source(catalog, "en") if err != nil { t.Fatal(err) } docs = append(docs, source...) for _, d := range source { docs = append(docs, i18n.Document{Version: 1, Namespace: d.Namespace, Locale: "zh-Hans", Messages: map[string]i18n.Message{ "SAME": {Value: i18n.Pattern{Text: d.Namespace + "译文:{value}"}}, "FIXED": {Value: i18n.Pattern{Text: "需要认证"}}, "INTERNAL_ERROR": {Value: i18n.Pattern{Text: "系统错误"}}, }}) } } b, err := i18n.New(i18n.Config{SourceLocale: "en", DefaultLocale: "en", Locales: []string{"en", "zh-Hans"}}, docs...) if err != nil { t.Fatal(err) } l := errori18n.New(b, nil) for _, c := range catalogs { if err := l.Validate(c); err != nil { t.Fatal(err) } } p, err := httpi18n.New(b) if err != nil { t.Fatal(err) } return p, l } func TestI18nRouteScopesEarlyFailuresAndReplay(t *testing.T) { global, local := localizedCatalog("app", 200), localizedCatalog("route", 409) policy, localizer := localizedOptions(t, global, local) format := response.Config{CodeType: "int64", SuccessCode: int64(0), ErrorCode: int64(999), Body: map[string]any{"error": map[string]any{"reason": "${message}", "id": "${code}"}, "ok": "${ok}", "data": "${data}"}, DataMode: "always", Messages: map[string]string{"FIXED": "fixed {literal}"}} app := omni.New(omni.WithErrors(global), omni.WithResponse(format), omni.WithLanguagePolicy(policy), omni.WithErrorLocalizer(localizer)) defer app.Close() private := errors.New("private-value") app.Use(func(next httpx.Handler) httpx.Handler { return func(c *httpx.Context) error { switch c.Header("X-Early") { case "auth": return apierrors.ByCode("FIXED") case "panic": panic(private) } return next(c) } }) handler := func(c *httpx.Context) error { if c.Header("X-Params") == "missing" { return apierrors.ByCode("SAME") } return apierrors.ByCode("SAME").WithParams(map[string]any{"value": "{literal}"}).WithCause(private) } app.GET("/global", handler) bridge := httpx.WrapMiddleware(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, r) }) }) app.HandleWithResponse("GET", "/route", response.RawJSON(response.MustWithErrors(local, response.MustNew(format))), handler, bridge) for _, path := range []string{"/global", "/route"} { for _, locale := range []string{"en", "zh-CN"} { for _, mode := range []string{"", "auth", "panic", "missing"} { r := httptest.NewRequest("GET", path, nil) r.Header.Set("Accept-Language", locale) r.Header.Set("X-Early", mode) r.Header.Set("X-Params", mode) w := httptest.NewRecorder() app.ServeHTTP(w, r) var body struct { Error struct { Reason string `json:"reason"` ID json.Number `json:"id"` } `json:"error"` OK bool `json:"ok"` Data any `json:"data"` } if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { t.Fatal(err, w.Body.String()) } ns, status := "app", 200 if path == "/route" { ns, status = "route", 409 } want := ns + ":{literal}" if locale != "en" { want = ns + "译文:{literal}" } if mode == "auth" { status = 401 want = "fixed {literal}" if locale != "en" { want = "需要认证" } } else if mode == "panic" || mode == "missing" { status = 500 want = "internal" if locale != "en" { want = "系统错误" } } if w.Code != status || body.OK || body.Error.Reason != want || strings.Contains(w.Body.String(), "private-value") { t.Fatal(path, locale, mode, w.Code, w.Body.String()) } if mode == "" && body.Error.ID.String() != "9223372036854775807" { t.Fatal("public code changed", body.Error) } } } } for _, shared := range []bool{false, true} { cfg := middleware.IdempotencyConfig{Scope: func(*httpx.Context) (string, error) { return "test", nil }} if shared { cfg.Store = &catalogStore{records: map[string]middleware.IdempotencyRecord{}} } idem, err := middleware.NewIdempotency(cfg) if err != nil { t.Fatal(err) } path := "/localReplay" if shared { path = "/sharedReplay" } calls := 0 app.POST(path, func(c *httpx.Context) error { calls++; return handler(c) }, idem.Middleware()) var first string for _, locale := range []string{"en", "zh-CN"} { r := httptest.NewRequest("POST", path, strings.NewReader(`{}`)) r.Header.Set("Idempotency-Key", "same") r.Header.Set("Accept-Language", locale) w := httptest.NewRecorder() app.ServeHTTP(w, r) if locale == "en" { first = w.Body.String() } else if w.Body.String() != first || w.Header().Get("Idempotency-Replayed") != "true" { t.Fatal("replay changed", w.Body.String(), first) } } if calls != 1 { t.Fatal("language retriggered business", calls) } } } func TestI18nExplicitProtocolsAndConcurrentApps(t *testing.T) { apps := []*omni.App{} for _, ns := range []string{"one", "two"} { c := localizedCatalog(ns, 200) p, l := localizedOptions(t, c) a := omni.New(omni.WithErrors(c), omni.WithResponse(response.Config{CodeType: "int64", SuccessCode: 0, ErrorCode: 999}), omni.WithLanguagePolicy(p), omni.WithErrorLocalizer(l)) defer a.Close() a.GET("/", func(*httpx.Context) error { return apierrors.ByCode("FIXED") }) a.HandleWithResponse("GET", "/ack", response.EncoderFunc(func(r response.Result) (response.Payload, error) { if !r.IsError() || r.Message != "fixed {literal}" || r.PublicMessage() != "需要认证" { t.Error(r) } return response.Text(200, "ACK"), nil }), func(*httpx.Context) error { return apierrors.ByCode("FIXED") }) a.GET("/direct", func(c *httpx.Context) error { return c.Text(200, "fixed-protocol") }) apps = append(apps, a) } var wg sync.WaitGroup for n := 0; n < 60; n++ { wg.Add(1) go func(n int) { defer wg.Done() locale, want := "en", "fixed {literal}" if n%2 == 0 { locale, want = "zh-CN", "需要认证" } r := httptest.NewRequest("GET", "/", nil) r.Header.Set("Accept-Language", locale) w := httptest.NewRecorder() apps[n%len(apps)].ServeHTTP(w, r) if w.Code != 401 || !strings.Contains(w.Body.String(), want) { t.Error(w.Code, w.Body.String()) } }(n) } wg.Wait() for _, path := range []string{"/ack", "/direct"} { r := httptest.NewRequest("GET", path, nil) r.Header.Set("Accept-Language", "zh-CN") w := httptest.NewRecorder() apps[0].ServeHTTP(w, r) want := "ACK" if path == "/direct" { want = "fixed-protocol" } if w.Body.String() != want { t.Fatal(w.Body.String()) } } } type panickingLocalizer struct{} func (panickingLocalizer) Localize(context.Context, *apierrors.Registry, *apierrors.APIError) (string, bool) { panic("presentation failed") } func TestI18nAdapterFailurePreservesBusiness(t *testing.T) { c := localizedCatalog("test", 200) a := omni.New(omni.WithErrors(c), omni.WithResponse(response.Config{CodeType: "int64", SuccessCode: 0, ErrorCode: 999}), omni.WithErrorLocalizer(panickingLocalizer{})) defer a.Close() a.GET("/", func(*httpx.Context) error { return apierrors.ByCode("FIXED") }) w := httptest.NewRecorder() a.ServeHTTP(w, httptest.NewRequest("GET", "/", nil)) if w.Code != 401 || !strings.Contains(w.Body.String(), "fixed {literal}") { t.Fatal(w.Code, w.Body.String()) } }