feat: add typed error message templates and stable replay snapshots
This commit is contained in:
149
cmd/go-omni/apigen/testdata/errorParams/consumer_test.go
vendored
Normal file
149
cmd/go-omni/apigen/testdata/errorParams/consumer_test.go
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
package consumer_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"example.com/errorCatalog/internal/apierrors"
|
||||
"example.com/errorCatalog/internal/handler"
|
||||
"example.com/errorCatalog/internal/servicecontext"
|
||||
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||
omnierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||
)
|
||||
|
||||
func TestGeneratedTemplateErrors(t *testing.T) {
|
||||
var document map[string]any
|
||||
if err := json.Unmarshal([]byte(handler.OpenAPIDocument), &document); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
operation := document["paths"].(map[string]any)["/token"].(map[string]any)["post"].(map[string]any)
|
||||
media := operation["responses"].(map[string]any)["200"].(map[string]any)["content"].(map[string]any)["application/json"].(map[string]any)
|
||||
example := media["examples"].(map[string]any)["TextTooLong"].(map[string]any)["value"].(map[string]any)
|
||||
if example["msg"] != "企业名称不能超过100个字符" || example["code"] != float64(400002) {
|
||||
t.Fatal(example)
|
||||
}
|
||||
if len(media["schema"].(map[string]any)["anyOf"].([]any)) != 2 {
|
||||
t.Fatal("missing success/failure schema")
|
||||
}
|
||||
found := false
|
||||
for _, raw := range document["x-omni-errors"].([]any) {
|
||||
entry := raw.(map[string]any)
|
||||
if entry["name"] == "TextTooLong" {
|
||||
found = true
|
||||
params := entry["params"].([]any)
|
||||
if len(params) != 2 || params[0].(map[string]any)["type"] != "string" || params[1].(map[string]any)["type"] != "int" {
|
||||
t.Fatal(params)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("missing template metadata")
|
||||
}
|
||||
// Assigning to a typed function also verifies declaration order and Go types.
|
||||
var typed func(string, int) *omnierrors.APIError = apierrors.TextTooLong
|
||||
private := errors.New("private cause")
|
||||
first := typed("企业名称", 100).WithCause(private)
|
||||
second := typed("备注", 500)
|
||||
if first.Message != "企业名称不能超过100个字符" || second.Message != "备注不能超过500个字符" || second.Cause != nil || !errors.Is(first, private) {
|
||||
t.Fatal(first, second)
|
||||
}
|
||||
if apierrors.UsernameInvalid().Message != "用户名格式不正确" {
|
||||
t.Fatal("fixed compatibility")
|
||||
}
|
||||
cat := apierrors.Catalog()
|
||||
cat.Definitions["TEXT_TOO_LONG"].Params[0].Type = "bool"
|
||||
if typed("企业名称", 100).Message != first.Message {
|
||||
t.Fatal("mutable catalog")
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for n := 0; n < 40; n++ {
|
||||
wg.Add(1)
|
||||
go func(n int) {
|
||||
defer wg.Done()
|
||||
if n%2 == 0 {
|
||||
if typed("企业名称", 100).Message != first.Message {
|
||||
t.Error("isolation")
|
||||
}
|
||||
} else {
|
||||
if typed("备注", 500).Message != second.Message {
|
||||
t.Error("isolation")
|
||||
}
|
||||
}
|
||||
}(n)
|
||||
}
|
||||
wg.Wait()
|
||||
cfg := logx.Default()
|
||||
cfg.Writer = io.Discard
|
||||
logger, err := logx.New(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
deps, err := servicecontext.New(context.Background(), logger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer deps.Close()
|
||||
app := omni.New()
|
||||
defer app.Close()
|
||||
app.Use(func(next httpx.Handler) httpx.Handler {
|
||||
return func(c *httpx.Context) error {
|
||||
switch c.Header("X-Fixture") {
|
||||
case "company":
|
||||
return typed("企业名称", 100).WithCause(private)
|
||||
case "remark":
|
||||
return omnierrors.ByCode("TEXT_TOO_LONG").WithParams(map[string]any{"label": "备注", "max": 500})
|
||||
case "missing":
|
||||
return omnierrors.ByCode("TEXT_TOO_LONG").WithParams(map[string]any{"label": "企业名称"})
|
||||
case "type":
|
||||
return omnierrors.ByCode("TEXT_TOO_LONG").WithParams(map[string]any{"label": "企业名称", "max": "100"})
|
||||
case "fixed":
|
||||
return apierrors.UsernameInvalid()
|
||||
case "denied":
|
||||
return apierrors.ResourceDenied("资源")
|
||||
default:
|
||||
return c.OK(map[string]string{"token": "ok"})
|
||||
}
|
||||
}
|
||||
})
|
||||
if err := handler.RegisterRoutes(app, deps); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, path := range []string{"/token", "/raw"} {
|
||||
for _, test := range []struct {
|
||||
kind string
|
||||
status int
|
||||
want string
|
||||
}{
|
||||
{"company", 200, first.Message}, {"remark", 200, second.Message}, {"missing", 500, "internal server error"}, {"type", 500, "internal server error"}, {"fixed", 200, "用户名格式不正确"}, {"denied", 403, "资源不可用"}, {"", 200, `"token":"ok"`},
|
||||
} {
|
||||
method := "POST"
|
||||
if path == "/raw" {
|
||||
method = "GET"
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(method, path, nil)
|
||||
r.Header.Set("X-Fixture", test.kind)
|
||||
app.ServeHTTP(w, r)
|
||||
if w.Code != test.status || !strings.Contains(w.Body.String(), test.want) || strings.Contains(w.Body.String(), "private") || strings.Contains(w.Body.String(), "{label}") {
|
||||
t.Fatal(test.kind, w.Code, w.Body.String())
|
||||
}
|
||||
if test.kind == "company" || test.kind == "remark" {
|
||||
if !strings.Contains(w.Body.String(), `"code":400002`) || !strings.Contains(w.Body.String(), `"data":null`) {
|
||||
t.Fatal(w.Body.String())
|
||||
}
|
||||
}
|
||||
if path == "/raw" && test.kind == "" && strings.Contains(w.Body.String(), "code") {
|
||||
t.Fatal("raw response wrapped")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user