feat: release configurable API response contracts
Define reusable response bodies in .api with typed code mappings, per-route overrides and matching OpenAPI schemas. Add application and callback encoders, early response policy selection, and isolated remote-consumer release verification. Release v0.8.0-preview after full race, vet, build and local MySQL workflow acceptance.
This commit is contained in:
72
response/template_test.go
Normal file
72
response/template_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package response_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/response"
|
||||
)
|
||||
|
||||
func TestResponseBodyTemplates(t *testing.T) {
|
||||
config := response.Config{
|
||||
Body: map[string]any{"success": "${ok}", "payload": "${data}", "meta": map[string]any{"trace": "${requestId}", "status": "${status}"}, "literal": "$${data}", "version": 1},
|
||||
ErrorBody: map[string]any{"success": "${ok}", "error": map[string]any{"id": "${code}", "reason": "${message}"}},
|
||||
CodeType: "integer", SuccessCode: 0, ErrorCode: 9999, Codes: map[string]any{"DENIED": 403001},
|
||||
}
|
||||
f := response.MustNew(config)
|
||||
config.Body["meta"].(map[string]any)["status"] = "mutated"
|
||||
f.Config().ErrorBody["error"].(map[string]any)["reason"] = "mutated"
|
||||
good := f.Value(response.Result{Status: 201, Code: "CREATED", Data: []string{"a"}})
|
||||
want := map[string]any{"success": true, "payload": []string{"a"}, "meta": map[string]any{"status": 201}, "literal": "${data}", "version": 1}
|
||||
if !reflect.DeepEqual(good, want) {
|
||||
t.Fatalf("%#v", good)
|
||||
}
|
||||
bad := f.Value(response.Result{Status: 403, Code: "DENIED", Message: "denied", Err: errors.New("PRIVATE")})
|
||||
want = map[string]any{"success": false, "error": map[string]any{"id": int64(403001), "reason": "denied"}}
|
||||
if !reflect.DeepEqual(bad, want) {
|
||||
t.Fatalf("%#v", bad)
|
||||
}
|
||||
// Returning or serializing configuration preserves reusable templates, even {}.
|
||||
for _, cfg := range []response.Config{f.Config(), {Body: map[string]any{}, ErrorBody: map[string]any{}}, {Body: map[string]any{"values": []any{"${data}", true, nil}}}} {
|
||||
before := response.MustNew(cfg).Value(response.Result{Status: 200})
|
||||
data, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var copy response.Config
|
||||
if err = json.Unmarshal(data, ©); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a, _ := json.Marshal(before)
|
||||
b, _ := json.Marshal(response.MustNew(copy).Value(response.Result{Status: 200}))
|
||||
if string(a) != string(b) {
|
||||
t.Fatalf("round trip: %s / %s", a, b)
|
||||
}
|
||||
}
|
||||
cyclic := map[string]any{}
|
||||
cyclic["cycle"] = cyclic
|
||||
for _, cfg := range []response.Config{
|
||||
{Body: map[string]any{"value": "${missing}"}}, {Body: map[string]any{"value": "${data}-suffix"}},
|
||||
{Body: cyclic}, {Body: map[string]any{"value": make(chan int)}}, {Body: map[string]any{}, DataField: "data"},
|
||||
{ErrorBody: map[string]any{}},
|
||||
} {
|
||||
if _, err := response.New(cfg.Clone()); err == nil {
|
||||
t.Fatal("invalid template accepted")
|
||||
}
|
||||
}
|
||||
if strings.Contains(string(mustJSON(t, bad)), "PRIVATE") {
|
||||
t.Fatal("private cause leaked")
|
||||
}
|
||||
}
|
||||
|
||||
func mustJSON(t *testing.T, value any) []byte {
|
||||
t.Helper()
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
Reference in New Issue
Block a user