Files
zip/collectionroot_test.go
zeekayandHanzo Dev 4de3b5fbf0 An address has one spelling, and one place that spells it
A pattern never ends in "/" now, the root aside, because the router serves "/x"
and "/x/" alike — so two spellings were two keys for one address, and a group
could not name its own root at all: declaring "" on it produced the prefix with
a trailing slash. That is why a collection root had to be declared at an
absolute path on the parent instead, which put the route outside the middleware
of the group it belonged to.

The rule was also written twice. The router composed a prefix with a leaf and
cmd/zipdoc composed the same two things to find that route's prose, so trimming
in the router alone would have filed every collection root's sentence under an
address the document does not contain — complete document, missing description,
no error anywhere. Both copies are gone; internal/addr is the one that spells it.

A test registers a trailing-slash pattern — the shape the Sentry-compatible
ingest addresses use on purpose — and asserts the route table, the registry, the
document's template, and the operation and concrete address a handler reads are
all the same string, because a service that resolves its runtime handler by name
from that pattern refuses every call if two of them disagree.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 12:31:19 -07:00

141 lines
4.5 KiB
Go

package zip_test
import (
"context"
"net/http/httptest"
"sort"
"testing"
"github.com/zap-proto/zip"
)
type rootIn struct{}
type rootOut struct {
OK bool `json:"ok"`
}
type coll struct{}
// List returns the collection.
func (coll) List(context.Context, *rootIn) (*rootOut, error) { return &rootOut{OK: true}, nil }
// Add adds to it.
func (coll) Add(context.Context, *rootIn) (*rootOut, error) { return &rootOut{OK: true}, nil }
// AN EMPTY LEAF IS THE PREFIX ITSELF. `g.Get("", h)` declares the collection
// root at the group's own address; only an explicit "/" asks for the trailing
// slash.
//
// Serving was never the problem — both spellings answer either way. The
// DECLARED pattern is what the document, the op key and cmd/zipdoc publish, and
// a group that could not spell its own root is why a service declared that route
// at an absolute path on the subsystem root instead, which put it outside the
// middleware of the group it belonged to.
func TestGroup_AnEmptyLeafIsTheCollectionRoot(t *testing.T) {
app := zip.New(zip.Config{AppName: "coll", DisableStartupMessage: true})
g := app.Group("/v1/function")
g.Get("", coll{}.List)
g.Post("/", coll{}.Add)
var declared []string
for _, r := range app.Routes() {
declared = append(declared, r.Method+" "+r.Pattern)
}
sort.Strings(declared)
// Both spellings canonicalise to the same address; they differ only by verb.
want := []string{"GET /v1/function", "POST /v1/function"}
if len(declared) != 2 || declared[0] != want[0] || declared[1] != want[1] {
t.Errorf("declared %v, want %v", declared, want)
}
// And it still answers, at the address it now declares.
for _, addr := range []string{"/v1/function", "/v1/function/"} {
resp, err := app.Test(httptest.NewRequest("GET", addr, nil))
if err != nil {
t.Fatalf("%s: %v", addr, err)
}
_ = resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("GET %s = %d, want 200", addr, resp.StatusCode)
}
}
}
// The root app is unaffected: with no prefix there is nothing for an empty leaf
// to be, so it stays the root.
func TestGroup_AnEmptyLeafAtTheRootIsStillTheRoot(t *testing.T) {
app := zip.New(zip.Config{AppName: "coll", DisableStartupMessage: true})
app.Get("", coll{}.List)
for _, r := range app.Routes() {
if r.Pattern != "/" {
t.Errorf("declared %q, want /", r.Pattern)
}
}
}
type ingestIn struct {
Project string `json:"project"`
}
type ingest struct{}
// Envelope accepts a Sentry envelope for one project.
func (ingest) Envelope(ctx context.Context, _ *ingestIn) (*rootOut, error) {
seenPattern, _ := zip.OpOf(ctx)
seenAddr, _ := zip.AddressOf(ctx)
ingestPattern, ingestAddr = seenPattern.Path, seenAddr
return &rootOut{OK: true}, nil
}
var ingestPattern, ingestAddr string
// EVERY READER OF AN ADDRESS AGREES ON HOW IT IS SPELLED.
//
// A pattern written with a trailing slash — which the Sentry-compatible ingest
// addresses are, deliberately — is canonicalised once, so the route table, the
// document, the operation a handler reads and the concrete address it forwards
// to are all the same string. A service that resolves its runtime handler BY
// NAME from Template(pattern) is what makes this load-bearing: if registration
// trimmed and the lookup did not, every ingest call would refuse.
func TestGroup_EveryReaderSpellsTheAddressTheSameWay(t *testing.T) {
app := zip.New(zip.Config{AppName: "ingest", DisableStartupMessage: true})
app.Group("/v1/event/:project").Post("/envelope/", ingest{}.Envelope)
const want = "/v1/event/:project/envelope"
routes := app.Routes()
if len(routes) != 1 || routes[0].Pattern != want {
t.Fatalf("Routes() = %+v, want one at %q", routes, want)
}
ops := app.Manifest().Ops
if len(ops) != 1 || ops[0].Path != want {
t.Fatalf("registry = %+v, want one at %q", ops, want)
}
if got := zip.Template(want); got != "/v1/event/{project}/envelope" {
t.Errorf("Template = %q; the document's spelling of the same address", got)
}
// And the request serves under BOTH spellings, reporting one.
for _, addr := range []string{"/v1/event/abc/envelope", "/v1/event/abc/envelope/"} {
ingestPattern, ingestAddr = "", ""
resp, err := app.Test(httptest.NewRequest("POST", addr, nil))
if err != nil {
t.Fatalf("%s: %v", addr, err)
}
_ = resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("POST %s = %d, want 200", addr, resp.StatusCode)
continue
}
if ingestPattern != want {
t.Errorf("POST %s: OpOf reported %q, want %q", addr, ingestPattern, want)
}
if ingestAddr != "/v1/event/abc/envelope" {
t.Errorf("POST %s: AddressOf reported %q", addr, ingestAddr)
}
}
}