Files
zip/urlrecord_test.go
zeekayandHanzo Dev 5f71d2496b The router is three nouns, and every one of them is concrete
App, Group, Operation. A group is a prefix and what holds under it, an operation
is one typed route declared there, and the verb methods are generic methods —
which Go 1.27 allows on a concrete type and still refuses on an interface. So In
and Out come from the handler and the caller writes no type arguments:

	users := app.Group("/v1/users")
	users.Post("/", api.Create)

	mint := users.Group("/mint").With(middleware.Mint(gate))
	mint.Post("/deposit", api.Deposit)

A Group is a CLOSED HANDLE: two unexported fields, and no method that hands back
the app, the parent or the fiber underneath. A subsystem given the group at its
own prefix cannot reach above it, so confinement is a property of the type
instead of a rule something has to police — which is what a host needs in order
to hand out a bounded surface and know what comes back.

That is bought by giving up the router interface, deliberately. Router and
OpTarget are gone, and with them the ability to implement a route table outside
this package: a decorator is no longer a router, a gate is middleware on a group,
and a bound is a path. The five package-level verb functions go the same way —
a registration is a method on the thing it registers into.

Raw carries what is not a typed operation: a stream, an upgrade, a probe, a
foreign subtree. It names its method, and it is the longer spelling because
opting out of the typed model is the exception and should read like one.



Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 01:39:17 -07:00

105 lines
3.2 KiB
Go

package zip_test
// A record's leaves ride a URL, named through the record. A chain's UTXO read
// takes a pagination cursor that way — an address and a utxo id inside one
// field — and with no URL spelling for it the read had no safe method it could
// be served on at all.
import (
"context"
"encoding/json"
"io"
"net/http"
"strings"
"testing"
"github.com/zap-proto/zip"
)
type page struct {
Address string `json:"address"`
UTXO string `json:"utxo"`
}
type fetchIn struct {
Addresses []string `json:"addresses"`
Limit uint32 `json:"limit"`
Start page `json:"start"`
Resume *page `json:"resume"`
}
func fetch(_ context.Context, in *fetchIn) (*fetchIn, error) { return in, nil }
func askFetch(t *testing.T, path string) fetchIn {
t.Helper()
a := zip.New(zip.Config{AppName: "t", DisableStartupMessage: true})
a.Get("/v1/t/utxos", fetch)
res, err := a.Fiber().Test(httpGet(t, path))
if err != nil {
t.Fatal(err)
}
body, err := io.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
if res.StatusCode != http.StatusOK {
t.Fatalf("GET %s: %d %s", path, res.StatusCode, body)
}
var out fetchIn
if err := json.Unmarshal(body, &out); err != nil {
t.Fatalf("GET %s: %v over %s", path, err, body)
}
return out
}
// The record binds beside the list, because a real cursor arrives with the
// query it continues.
func TestURLCarriesARecordBesideItsList(t *testing.T) {
got := askFetch(t, "/v1/t/utxos?addresses=X-one,X-two&limit=7&start.address=X-one&start.utxo=abc")
if len(got.Addresses) != 2 || got.Addresses[0] != "X-one" || got.Addresses[1] != "X-two" {
t.Errorf("addresses: got %q, want [X-one X-two]", got.Addresses)
}
if got.Limit != 7 {
t.Errorf("limit: got %d, want 7", got.Limit)
}
if got.Start != (page{Address: "X-one", UTXO: "abc"}) {
t.Errorf("start: got %+v, want the cursor the caller wrote", got.Start)
}
if got.Resume != nil {
t.Errorf("resume: got %+v, want nil: a record nobody named is absent, not empty", got.Resume)
}
}
func TestABareNameNeverReachesInsideARecord(t *testing.T) {
got := askFetch(t, "/v1/t/utxos?address=X-sneak")
if got.Start.Address != "" {
t.Errorf("start.address: got %q, want empty: a leaf is named through its record", got.Start.Address)
}
}
func TestAPointerRecordIsAllocatedOnlyWhenNamed(t *testing.T) {
got := askFetch(t, "/v1/t/utxos?resume.utxo=def")
if got.Resume == nil {
t.Fatal("resume: nil, want the record the caller named")
}
if got.Resume.UTXO != "def" {
t.Errorf("resume.utxo: got %q, want def", got.Resume.UTXO)
}
}
// The document publishes exactly what the binder fills — one predicate, two
// readers — so a caller reading the spec writes names that work.
func TestTheDocumentPublishesEveryNameTheURLCanCarry(t *testing.T) {
a := zip.New(zip.Config{AppName: "t", DisableStartupMessage: true})
a.Get("/v1/t/utxos", fetch)
spec, err := json.Marshal(a.OpenAPISpec())
if err != nil {
t.Fatal(err)
}
for _, name := range []string{`"addresses"`, `"limit"`, `"start.address"`, `"start.utxo"`, `"resume.address"`, `"resume.utxo"`} {
if !strings.Contains(string(spec), name) {
t.Errorf("the document does not publish the parameter %s the binder fills", name)
}
}
}