187 lines
6.5 KiB
Go
187 lines
6.5 KiB
Go
package app_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"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/logx"
|
|
"git.yuntuo.hk/k2safe/go-omni/metrics"
|
|
"git.yuntuo.hk/k2safe/go-omni/middleware"
|
|
"git.yuntuo.hk/k2safe/go-omni/response"
|
|
)
|
|
|
|
func TestTemplateErrorsAcrossPoliciesAndReplay(t *testing.T) {
|
|
var logs bytes.Buffer
|
|
lc := logx.Default()
|
|
lc.Writer = &logs
|
|
logger, err := logx.New(lc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
logx.SetDefault(logger)
|
|
defer logx.SetDefault(nil)
|
|
defer logger.Close()
|
|
cat := apierrors.Catalog{DefaultStatus: 200, Definitions: map[string]apierrors.Definition{"TOO_LONG": {
|
|
Code: 400002, Message: "{label}不能超过{max}个字符", Params: []apierrors.Parameter{{Name: "label", Type: "string"}, {Name: "max", Type: "int"}},
|
|
}}}
|
|
registry := apierrors.MustRegistry(cat)
|
|
encoder := response.MustWithErrors(cat, response.MustNew(response.Config{CodeType: "int32", SuccessCode: 0, ErrorCode: 9999, MessageField: "msg", DataMode: "always"}))
|
|
app := omni.New(omni.WithLogger(true))
|
|
defer app.Close()
|
|
meter := metrics.New()
|
|
app.Use(meter.Middleware())
|
|
// The clone uses the route policy even though the App has a different default.
|
|
app.Use(func(next httpx.Handler) httpx.Handler {
|
|
return func(c *httpx.Context) error {
|
|
err := next(c)
|
|
if f := c.Failure(); f != nil && strings.Contains(f.Message, "{label}") {
|
|
t.Error("unrendered failure", f)
|
|
}
|
|
return err
|
|
}
|
|
})
|
|
bridge := httpx.WrapMiddleware(func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, r) })
|
|
})
|
|
calls := 0
|
|
handler := func(c *httpx.Context) error {
|
|
calls++
|
|
values := map[string]any{"label": "企业名称", "max": 100}
|
|
if c.Header("X-Label") == "remark" {
|
|
values = map[string]any{"label": "备注", "max": 500}
|
|
}
|
|
switch c.Header("X-Failure") {
|
|
case "missing":
|
|
delete(values, "max")
|
|
case "type":
|
|
values["max"] = "private-value"
|
|
}
|
|
e := apierrors.ByCode("TOO_LONG").WithParams(values).WithCause(errors.New("private cause"))
|
|
if c.Header("X-Resolved") == "true" {
|
|
return registry.Resolve(e)
|
|
}
|
|
return e
|
|
}
|
|
app.HandleWithResponse("POST", "/direct", encoder, handler, bridge)
|
|
for _, shared := range []bool{false, true} {
|
|
cfg := middleware.IdempotencyConfig{Scope: func(*httpx.Context) (string, error) { return "fixture", nil }}
|
|
path := "/local"
|
|
if shared {
|
|
path = "/shared"
|
|
cfg.Store = &catalogStore{records: map[string]middleware.IdempotencyRecord{}}
|
|
}
|
|
idem, err := middleware.NewIdempotency(cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
app.HandleWithResponse("POST", path, encoder, handler, idem.Middleware(), bridge)
|
|
}
|
|
check := func(path, label, key, kind string, status int, want string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
w := httptest.NewRecorder()
|
|
r := httptest.NewRequest("POST", path, nil)
|
|
r.Header.Set("Idempotency-Key", key)
|
|
r.Header.Set("X-Label", label)
|
|
r.Header.Set("X-Failure", kind)
|
|
r.Header.Set("X-Resolved", "true")
|
|
app.ServeHTTP(w, r)
|
|
if w.Code != status || !strings.Contains(w.Body.String(), want) || strings.Contains(w.Body.String(), "private") || strings.Contains(w.Body.String(), "{label}") {
|
|
t.Fatal(path, w.Code, w.Body.String())
|
|
}
|
|
if status == 200 && (!strings.Contains(w.Body.String(), `"data":null`) || !strings.Contains(w.Body.String(), `"code":400002`)) {
|
|
t.Fatal(w.Body.String())
|
|
}
|
|
return w
|
|
}
|
|
for _, path := range []string{"/direct", "/local", "/shared"} {
|
|
for _, label := range []string{"company", "remark"} {
|
|
want := "企业名称不能超过100个字符"
|
|
if label == "remark" {
|
|
want = "备注不能超过500个字符"
|
|
}
|
|
first := check(path, label, label, "", 200, want)
|
|
if path != "/direct" {
|
|
before := calls
|
|
second := check(path, label, label, "", 200, want)
|
|
if calls != before || first.Body.String() != second.Body.String() || second.Header().Get("Idempotency-Replayed") != "true" {
|
|
t.Fatal("bad replay")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
check("/direct", "company", "", "missing", 500, "internal server error")
|
|
check("/direct", "company", "", "type", 500, "internal server error")
|
|
stats := meter.Snapshot()
|
|
if stats.Errors != 12 || stats.StatusCounts["200"] != 10 || stats.StatusCounts["500"] != 2 {
|
|
t.Fatal(stats)
|
|
}
|
|
seen, diagnostics := 0, 0
|
|
for _, line := range strings.Split(strings.TrimSpace(logs.String()), "\n") {
|
|
var entry map[string]any
|
|
if err := json.Unmarshal([]byte(line), &entry); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if entry["msg"] != "http.request" {
|
|
continue
|
|
}
|
|
seen++
|
|
if entry["failed"] != true {
|
|
t.Fatal(entry)
|
|
}
|
|
if entry["status"] == float64(200) && (entry["errorKey"] != "TOO_LONG" || entry["businessCode"] != float64(400002)) {
|
|
t.Fatal(entry)
|
|
}
|
|
if entry["errorParameter"] != nil {
|
|
diagnostics++
|
|
if strings.Contains(entry["errorParameter"].(string), "private-value") {
|
|
t.Fatal(entry)
|
|
}
|
|
}
|
|
}
|
|
if seen != 12 || diagnostics != 2 {
|
|
t.Fatal(seen, diagnostics)
|
|
}
|
|
}
|
|
|
|
func TestResolvedFailureReplaySurvivesCatalogChanges(t *testing.T) {
|
|
store := &catalogStore{records: map[string]middleware.IdempotencyRecord{}}
|
|
var observed []*apierrors.APIError
|
|
makeApp := func(message string) *omni.App {
|
|
cat := apierrors.Catalog{DefaultStatus: 200, Definitions: map[string]apierrors.Definition{"T": {Code: "code", Message: message, Params: []apierrors.Parameter{{Name: "label", Type: "string"}}}}}
|
|
app := omni.New(omni.WithErrors(cat))
|
|
app.Use(func(next httpx.Handler) httpx.Handler {
|
|
return func(c *httpx.Context) error { err := next(c); observed = append(observed, c.Failure()); return err }
|
|
})
|
|
idem, err := middleware.NewIdempotency(middleware.IdempotencyConfig{Store: store, Scope: func(*httpx.Context) (string, error) { return "fixture", nil }})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
app.POST("/", func(c *httpx.Context) error {
|
|
return apierrors.ByCode("T").WithParams(map[string]any{"label": "企业"})
|
|
}, idem.Middleware())
|
|
return app
|
|
}
|
|
for _, message := range []string{"{label}原文案", "{label}新文案"} {
|
|
app := makeApp(message)
|
|
w := httptest.NewRecorder()
|
|
r := httptest.NewRequest("POST", "/", nil)
|
|
r.Header.Set("Idempotency-Key", "same")
|
|
app.ServeHTTP(w, r)
|
|
app.Close()
|
|
if !strings.Contains(w.Body.String(), "企业原文案") {
|
|
t.Fatal(w.Body.String())
|
|
}
|
|
}
|
|
if len(observed) != 2 || observed[0].Message != "企业原文案" || observed[1].Message != "企业原文案" || observed[1].Code != "T" {
|
|
t.Fatal(observed)
|
|
}
|
|
}
|