mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
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]>
103 lines
3.7 KiB
Go
103 lines
3.7 KiB
Go
package zip
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The trap the fleet migration found: iam mounts on a SCOPE WITH 5 PREFIXES.
|
|
//
|
|
// A real identity provider owns several disjoint subtrees — /v1/iam,
|
|
// /login/oauth, /.well-known and friends — and a host that hands it a
|
|
// multi-prefix scope must not re-address the whole definition under every one
|
|
// of them. Composing an App once per prefix is composing it FIVE times, which
|
|
// is five occurrences of every route it owns.
|
|
//
|
|
// This pins what actually happens, so the migration has an answer rather than a
|
|
// guess.
|
|
func TestMultiPrefixScope_OneDefinitionPerPrefixIsFiveOccurrences(t *testing.T) {
|
|
prefixes := []string{"/v1/iam", "/login/oauth", "/.well-known", "/oidc", "/scim"}
|
|
|
|
iam := quiet("iam")
|
|
iam.Raw("GET", "/keys", func(c *Ctx) error { return c.String(200, "keys") })
|
|
|
|
host := quiet("host")
|
|
for _, p := range prefixes {
|
|
host.Group(p).Use(iam)
|
|
}
|
|
if err := host.Build(); err != nil {
|
|
t.Fatalf("build: %v", err)
|
|
}
|
|
|
|
// Five prefixes, five distinct addresses. No collision, because each
|
|
// occurrence answers under its own prefix.
|
|
for _, p := range prefixes {
|
|
if code, body := wireGET(t, host, p+"/keys"); code != 200 || body != "keys" {
|
|
t.Errorf("%s/keys answered %d %q", p, code, body)
|
|
}
|
|
}
|
|
// And the definition is still ONE definition.
|
|
n := 0
|
|
for _, def := range host.hostSet() {
|
|
if def == iam {
|
|
n++
|
|
}
|
|
}
|
|
if n != 1 {
|
|
t.Errorf("the shared definition appears %d times in the host set, want 1", n)
|
|
}
|
|
}
|
|
|
|
// The failure mode the trap actually is: a definition whose routes are ALREADY
|
|
// absolute cannot be re-addressed under a prefix without moving them. If a host
|
|
// composes an App under a prefix, the routes move — so a host that wants iam at
|
|
// its own absolute paths composes it at the ROOT, once, not once per prefix.
|
|
func TestMultiPrefixScope_ComposingAtRootKeepsAbsolutePaths(t *testing.T) {
|
|
iam := quiet("iam")
|
|
for _, p := range []string{"/v1/iam/keys", "/login/oauth/token", "/.well-known/jwks"} {
|
|
iam.Raw("GET", p, func(c *Ctx) error { return c.String(200, "ok") })
|
|
}
|
|
|
|
host := quiet("host")
|
|
host.Use(iam) // ONCE, at the root: the definition's paths are already absolute
|
|
if err := host.Build(); err != nil {
|
|
t.Fatalf("build: %v", err)
|
|
}
|
|
for _, p := range []string{"/v1/iam/keys", "/login/oauth/token", "/.well-known/jwks"} {
|
|
if code, _ := wireGET(t, host, p); code != 200 {
|
|
t.Errorf("%s answered %d — composing at the root must not move absolute paths", p, code)
|
|
}
|
|
}
|
|
}
|
|
|
|
// And the mistake, refused loudly: composing an absolute-path definition under
|
|
// five prefixes does NOT serve its absolute paths, it serves five prefixed
|
|
// copies. A migration that does this silently loses every original address.
|
|
func TestMultiPrefixScope_PrefixingAnAbsoluteDefinitionMovesIt(t *testing.T) {
|
|
iam := quiet("iam")
|
|
iam.Raw("GET", "/v1/iam/keys", func(c *Ctx) error { return c.String(200, "keys") })
|
|
|
|
host := quiet("host")
|
|
host.Group("/v1/iam").Use(iam) // the mistake: prefix applied to absolute paths
|
|
if err := host.Build(); err != nil {
|
|
t.Fatalf("build: %v", err)
|
|
}
|
|
if code, _ := wireGET(t, host, "/v1/iam/keys"); code != 404 {
|
|
t.Errorf("/v1/iam/keys answered %d — expected the prefix to have MOVED it", code)
|
|
}
|
|
if code, _ := wireGET(t, host, "/v1/iam/v1/iam/keys"); code != 200 {
|
|
t.Errorf("the doubled path does not answer either; the prefix did something else")
|
|
}
|
|
// The doubling is visible in the registry, which is where a migration should
|
|
// notice it rather than in production.
|
|
var paths []string
|
|
for _, o := range host.plan() {
|
|
if r, ok := o.route(); ok {
|
|
paths = append(paths, o.abs(r.path))
|
|
}
|
|
}
|
|
if !strings.Contains(strings.Join(paths, " "), "/v1/iam/v1/iam/keys") {
|
|
t.Errorf("registry does not show the doubled path: %v", paths)
|
|
}
|
|
}
|