161 lines
6.3 KiB
Go
161 lines
6.3 KiB
Go
package apierrors_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
|
)
|
|
|
|
func templateCatalog() apierrors.Catalog {
|
|
return apierrors.Catalog{DefaultStatus: 200, Definitions: map[string]apierrors.Definition{
|
|
"TEXT_TOO_LONG": {Code: 400002, Message: "{label}不能超过{max}个字符", Params: []apierrors.Parameter{{Name: "label", Type: "string", Example: "企业名称"}, {Name: "max", Type: "int", Example: 100}}},
|
|
"FIXED": {Code: 400003, Message: "literal {unchanged}"},
|
|
}}
|
|
}
|
|
func TestMessageParamsIsolationAndResolution(t *testing.T) {
|
|
catalog := templateCatalog()
|
|
r := apierrors.MustRegistry(catalog)
|
|
catalog.Definitions["TEXT_TOO_LONG"].Params[0].Type = "bool"
|
|
r.Catalog().Definitions["TEXT_TOO_LONG"].Params[0].Name = "changed"
|
|
private := errors.New("private cause")
|
|
values := map[string]any{"label": "企业名称", "max": 100}
|
|
reference := apierrors.ByCode("TEXT_TOO_LONG").WithParams(values).WithCause(private)
|
|
values["label"] = "changed"
|
|
first := r.Resolve(fmt.Errorf("wrapped: %w", reference))
|
|
for n := 0; n < 3; n++ {
|
|
first = r.Resolve(first)
|
|
if first.Message != "企业名称不能超过100个字符" || first.Status != 200 || first.BusinessCode != int64(400002) || !errors.Is(first, private) {
|
|
t.Fatal(first)
|
|
}
|
|
}
|
|
second := r.Resolve(first.WithParams(map[string]any{"label": "备注", "max": 500}))
|
|
if first.Message != "企业名称不能超过100个字符" || second.Message != "备注不能超过500个字符" {
|
|
t.Fatal(first, second)
|
|
}
|
|
if r.Example("TEXT_TOO_LONG").Message != first.Message {
|
|
t.Fatal("example and runtime differ")
|
|
}
|
|
if r.Resolve(apierrors.ByCode("FIXED")).Message != "literal {unchanged}" {
|
|
t.Fatal("legacy braces changed")
|
|
}
|
|
var wg sync.WaitGroup
|
|
for n := 0; n < 80; n++ {
|
|
wg.Add(1)
|
|
go func(n int) {
|
|
defer wg.Done()
|
|
label, max := "企业名称", 100
|
|
if n%2 == 1 {
|
|
label, max = "备注", 500
|
|
}
|
|
got := r.Resolve(apierrors.ByCode("TEXT_TOO_LONG").WithParams(map[string]any{"label": label, "max": max}).WithCause(private))
|
|
if got.Message != fmt.Sprintf("%s不能超过%d个字符", label, max) || !errors.Is(got, private) {
|
|
t.Error(got)
|
|
}
|
|
}(n)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
func TestInvalidTemplateParamsFailClosed(t *testing.T) {
|
|
r := apierrors.MustRegistry(templateCatalog())
|
|
cause := errors.New("private")
|
|
for _, values := range []map[string]any{
|
|
nil, {"label": "x"}, {"label": "x", "max": "100"}, {"label": 100, "max": 100},
|
|
{"label": "x", "max": 100.0}, {"label": "x", "max": 100, "extra": "private-value"},
|
|
{"label": "x", "max": nil}, {"label": map[string]any{"secret": "private-value"}, "max": 100},
|
|
} {
|
|
result := r.Resolve(apierrors.ByCode("TEXT_TOO_LONG").WithParams(values).WithCause(cause))
|
|
var diagnostic *apierrors.ParamError
|
|
if result.Code != "INTERNAL_ERROR" || result.Status != 500 || strings.Contains(result.Message, "{") || !errors.As(result, &diagnostic) || !errors.Is(result, cause) {
|
|
t.Fatal(result)
|
|
}
|
|
if strings.Contains(diagnostic.Error(), "private-value") {
|
|
t.Fatal("diagnostic leaked value")
|
|
}
|
|
}
|
|
// A parameterized fallback cannot recurse or expose its template.
|
|
c := templateCatalog()
|
|
c.Definitions["INTERNAL_ERROR"] = c.Definitions["TEXT_TOO_LONG"]
|
|
if result := apierrors.MustRegistry(c).Resolve(errors.New("private")); result.Message != "internal server error" || result.Status != 500 {
|
|
t.Fatal(result)
|
|
}
|
|
for _, change := range []func(*apierrors.Definition){
|
|
func(d *apierrors.Definition) { d.Message = "{label}{missing}" },
|
|
func(d *apierrors.Definition) { d.Message = "{label}" },
|
|
func(d *apierrors.Definition) { d.Message = "{label}{max" },
|
|
func(d *apierrors.Definition) { d.Message = "{label}{max}}" },
|
|
func(d *apierrors.Definition) { d.Params[1].Name = "label" },
|
|
func(d *apierrors.Definition) { d.Params[1].Name = "type" },
|
|
func(d *apierrors.Definition) { d.Params[1].Type = "interface{}" },
|
|
func(d *apierrors.Definition) { d.Params[1].Example = "100" },
|
|
func(d *apierrors.Definition) { d.Params = []apierrors.Parameter{} },
|
|
} {
|
|
c := templateCatalog()
|
|
d := c.Definitions["TEXT_TOO_LONG"]
|
|
change(&d)
|
|
c.Definitions["TEXT_TOO_LONG"] = d
|
|
if _, err := apierrors.NewRegistry(c); err == nil {
|
|
t.Fatal("accepted invalid template", d)
|
|
}
|
|
}
|
|
}
|
|
func TestParamTypesEscapingAndSnapshot(t *testing.T) {
|
|
c := apierrors.Catalog{Definitions: map[string]apierrors.Definition{"T": {Code: "t", Message: "{{literal}} {value} {value}", Params: []apierrors.Parameter{{Name: "value", Type: "string"}}}}}
|
|
r := apierrors.MustRegistry(c)
|
|
got := r.Resolve(apierrors.ByCode("T").WithParams(map[string]any{"value": "{other}"}))
|
|
if got.Message != "{literal} {other} {other}" {
|
|
t.Fatal("recursive substitution", got)
|
|
}
|
|
for _, test := range []struct {
|
|
kind string
|
|
good, bad any
|
|
want string
|
|
}{
|
|
{"int32", int64(math.MaxInt32), int64(math.MaxInt32) + 1, "2147483647"},
|
|
{"int64", json.Number("9223372036854775807"), json.Number("9223372036854775808"), "9223372036854775807"},
|
|
{"bool", true, "true", "true"}, {"float64", float64(1.25), math.Inf(1), "1.25"},
|
|
} {
|
|
d := c.Definitions["T"]
|
|
d.Params = []apierrors.Parameter{{Name: "value", Type: test.kind}}
|
|
c.Definitions["T"] = d
|
|
r = apierrors.MustRegistry(c)
|
|
if x := r.Resolve(apierrors.ByCode("T").WithParams(map[string]any{"value": test.good})); x.Message != "{literal} "+test.want+" "+test.want {
|
|
t.Fatal(test.kind, x)
|
|
}
|
|
if x := r.Resolve(apierrors.ByCode("T").WithParams(map[string]any{"value": test.bad})); x.Status != 500 {
|
|
t.Fatal(test.kind, x)
|
|
}
|
|
}
|
|
original := apierrors.New(200, "T", "complete").WithCause(errors.New("private"))
|
|
original.BusinessCode = int64(math.MaxInt64)
|
|
raw, err := json.Marshal(original.Snapshot())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(raw), "private") || strings.Contains(string(raw), "params") {
|
|
t.Fatal(string(raw))
|
|
}
|
|
// Simulate a store rewriting its envelope with ordinary JSON numbers.
|
|
var storage map[string]any
|
|
if err = json.Unmarshal(raw, &storage); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err = json.Marshal(storage)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var snapshot apierrors.Snapshot
|
|
if err = json.Unmarshal(raw, &snapshot); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
restored := snapshot.ErrorValue()
|
|
if restored.Cause != nil || restored.Message != "complete" || restored.BusinessCode != int64(math.MaxInt64) {
|
|
t.Fatal(restored)
|
|
}
|
|
}
|