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]>
121 lines
4.9 KiB
Go
121 lines
4.9 KiB
Go
package zip_test
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/zap-proto/zip"
|
|
)
|
|
|
|
// These tests pin the one composition path for fronting a foreign
|
|
// net/http.Handler: it mounts as an ordinary wildcard route via
|
|
//
|
|
// app.Group(prefix).All("/*", zip.AdaptNetHTTP(h))
|
|
//
|
|
// The load-bearing property is specificity: a static route registered AFTER
|
|
// the mount still wins, because the mounted subtree is a normal route subject
|
|
// to the zap-proto/fiber fork's most-specific-wins precedence — NOT a separate
|
|
// pre-router prefix dispatch.
|
|
|
|
// call issues an in-memory request through fiber.Test and returns
|
|
// (status, body). Empty body sends no request body.
|
|
// deadline is how long a request may take before the test calls it unanswered.
|
|
//
|
|
// fiber's own default is one second, which is the round trip and nothing else.
|
|
// A request to a lazy plugin waits for a CHILD PROCESS to start, and on a busy
|
|
// machine that alone can outlast a second — so the four warm tests and the two
|
|
// MCP ones failed on a deadline that was never measuring them, in a different
|
|
// subset every run. A generous ceiling weakens no assertion: a deadline request
|
|
// still returns the moment it is answered.
|
|
const deadline = 10 * time.Second
|
|
|
|
func call(t *testing.T, app *zip.App, method, target, body string) (int, string) {
|
|
t.Helper()
|
|
var r io.Reader
|
|
if body != "" {
|
|
r = strings.NewReader(body)
|
|
}
|
|
req, err := http.NewRequest(method, target, r)
|
|
if err != nil {
|
|
t.Fatalf("NewRequest(%s %s): %v", method, target, err)
|
|
}
|
|
resp, err := app.Test(req, zip.TestConfig{Timeout: deadline, FailOnTimeout: true})
|
|
if err != nil {
|
|
t.Fatalf("Test(%s %s): %v", method, target, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
b, _ := io.ReadAll(resp.Body)
|
|
return resp.StatusCode, string(b)
|
|
}
|
|
|
|
// echoHandler is a plain net/http handler that reports the path, query and
|
|
// body it received, so tests can prove the request reached it intact.
|
|
func echoHandler() http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
b, _ := io.ReadAll(r.Body)
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
_, _ = io.WriteString(w,
|
|
"net/http path="+r.URL.Path+" query="+r.URL.RawQuery+" body="+string(b))
|
|
})
|
|
}
|
|
|
|
// TestAdaptNetHTTP_NewForm_Serves proves the recommended form —
|
|
// app.All(prefix+"/*", zip.AdaptNetHTTP(h)) — serves the mounted subtree.
|
|
func TestAdaptNetHTTP_NewForm_Serves(t *testing.T) {
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
app.Raw(zip.MethodAll, "/api/*", zip.AdaptNetHTTP(echoHandler()))
|
|
|
|
status, body := call(t, app, "GET", "/api/foo", "")
|
|
if status != 200 {
|
|
t.Fatalf("GET /api/foo: status %d, want 200 (body=%s)", status, body)
|
|
}
|
|
if !strings.Contains(body, "net/http path=/api/foo") {
|
|
t.Fatalf("GET /api/foo: body=%q, want it deadline by the mounted net/http handler", body)
|
|
}
|
|
}
|
|
|
|
// TestAdaptNetHTTP_StaticRouteWins is the load-bearing proof: a static route
|
|
// registered AFTER the wildcard mount still wins for its exact path, while
|
|
// every other subpath falls through to the mount. This only holds because
|
|
// the mount is an ordinary route participating in specificity precedence —
|
|
// registration order is inverted on purpose.
|
|
func TestAdaptNetHTTP_StaticRouteWins(t *testing.T) {
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
|
|
// Wildcard mount FIRST, static route SECOND. Specificity, not order,
|
|
// must decide.
|
|
app.Raw(zip.MethodAll, "/v1/commerce/*", zip.AdaptNetHTTP(echoHandler()))
|
|
app.Raw("GET", "/v1/commerce/health", func(c *zip.Ctx) error {
|
|
return c.String(200, "static-health")
|
|
})
|
|
|
|
// Exact static path: the later, more-specific static route wins.
|
|
if status, body := call(t, app, "GET", "/v1/commerce/health", ""); status != 200 || body != "static-health" {
|
|
t.Fatalf("GET /v1/commerce/health: status=%d body=%q, want 200 \"static-health\" (the mount must NOT shadow the later static route)", status, body)
|
|
}
|
|
// Any other subpath: the mount serves it.
|
|
if status, body := call(t, app, "GET", "/v1/commerce/orders", ""); status != 200 || !strings.Contains(body, "net/http path=/v1/commerce/orders") {
|
|
t.Fatalf("GET /v1/commerce/orders: status=%d body=%q, want the mount to serve it", status, body)
|
|
}
|
|
}
|
|
|
|
// TestAdaptNetHTTP_BodyAndPathIntact proves the adapted handler receives the request
|
|
// body and the full subpath + query unchanged through the wildcard mount.
|
|
func TestAdaptNetHTTP_BodyAndPathIntact(t *testing.T) {
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
app.Raw(zip.MethodAll, "/api/*", zip.AdaptNetHTTP(echoHandler()))
|
|
|
|
status, body := call(t, app, "POST", "/api/echo/42?q=hi", "payload-bytes")
|
|
if status != 200 {
|
|
t.Fatalf("POST /api/echo/42?q=hi: status %d, want 200 (body=%s)", status, body)
|
|
}
|
|
for _, want := range []string{"path=/api/echo/42", "query=q=hi", "body=payload-bytes"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("POST /api/echo/42?q=hi: response %q missing %q — request did not reach the handler intact", body, want)
|
|
}
|
|
}
|
|
}
|