mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
A refusal now leaves by one door and is written in the vocabulary of the address it happened at. HTTPError loses its struct tags and becomes a plain value; MarshalJSON is the single place that says what it looks like. Its three members already mapped onto both standards exactly, so no field was added: Status is `status`, Msg is `detail`, Code is an extension member (RFC 9457), and the same value reads as `error` / `error_description` under RFC 6749 §5.2. With() members still merge at the top level per §3.2, with the document's own written last. zip.OAuth(r) returns a Router whose routes answer the OAuth vocabulary. The fact rides the route entry rather than a table of addresses, so it survives composition, and it is a property of the address rather than of the returned value — which is what makes it reach the refusals no handler wrote (Bind, validation, panics). A verb the endpoint does not serve is the router refusing, and answers a problem document with Allow. middleware.Recover wrote its own 500 body with c.JSON, a second error surface that spoke only the default vocabulary; it now returns the error so the one handler renders it. group() copies oauth beside wrap, so a nested scope keeps the vocabulary. Allow on 405 comes from the fiber fork's router and was already correct; TestAWrongVerbSaysWhichVerbsWork now pins it in both directions so an error handler edit cannot drop it. Consumers reading `body.error` read `body.detail` now. Each repo takes the change when it moves its own pin.
71 lines
2.4 KiB
Go
71 lines
2.4 KiB
Go
package zip
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
// TestARefusalCarriesItsDetail is the capability six subsystems were missing: a
|
|
// typed op's only way to refuse is to return an error, so a non-2xx with a shape
|
|
// could not be typed at all.
|
|
func TestARefusalCarriesItsDetail(t *testing.T) {
|
|
err := ErrPaymentRequired("spend cap exceeded").
|
|
With(map[string]any{"cap": 5000, "spent": 5127, "ledger": "acme"})
|
|
|
|
raw, mErr := json.Marshal(err)
|
|
if mErr != nil {
|
|
t.Fatal(mErr)
|
|
}
|
|
var got map[string]any
|
|
if err := json.Unmarshal(raw, &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got["status"] != float64(402) || got["detail"] != "spend cap exceeded" {
|
|
t.Fatalf("problem document did not survive: %v", got)
|
|
}
|
|
// MERGED, not nested: a reader sees one object rather than a body filed
|
|
// under a key it has to know to look in — RFC 9457 §3.2.
|
|
for k, want := range map[string]any{"cap": float64(5000), "spent": float64(5127), "ledger": "acme"} {
|
|
if got[k] != want {
|
|
t.Errorf("detail %s = %v, want %v", k, got[k], want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDetailCannotDisplaceTheDocument is the half that matters more than the
|
|
// feature. A domain field named `status` silently overwriting the refusal's
|
|
// status is how a 402 reads as a 200 — so the document's own members are
|
|
// written last and win.
|
|
func TestDetailCannotDisplaceTheDocument(t *testing.T) {
|
|
err := ErrUnprocessable("build failed").
|
|
With(map[string]any{"status": 200, "detail": "everything is fine", "code": "ok", "type": "https://evil"})
|
|
err.Code = "build_failed"
|
|
|
|
raw, _ := json.Marshal(err)
|
|
var got map[string]any
|
|
_ = json.Unmarshal(raw, &got)
|
|
|
|
for member, want := range map[string]any{
|
|
"status": float64(422),
|
|
"detail": "build failed",
|
|
"code": "build_failed",
|
|
"type": "about:blank",
|
|
} {
|
|
if got[member] != want {
|
|
t.Errorf("%s = %v, want %v — an extension member displaced the refusal", member, got[member], want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestWithMerges pins that two callers adding one fact each both land — a gate
|
|
// naming the cap and a meter naming the ledger are two facts about one refusal.
|
|
func TestWithMerges(t *testing.T) {
|
|
e := ErrPaymentRequired("no").With(map[string]any{"cap": 1}).With(map[string]any{"ledger": "acme"})
|
|
if e.Detail["cap"] != 1 || e.Detail["ledger"] != "acme" {
|
|
t.Errorf("second With replaced the first: %v", e.Detail)
|
|
}
|
|
if ErrNotFound("x").With(nil).Detail != nil {
|
|
t.Error("With(nil) allocated")
|
|
}
|
|
}
|