242 lines
8.4 KiB
Go
242 lines
8.4 KiB
Go
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())
|
|
}
|
|
}
|