mirror of
https://github.com/hanzoai/gateway.git
synced 2026-10-30 15:04:44 +00:00
The trust boundary's strip did not survive its own bridge. gateway.Mount — the HIP-0106 boundary the unified cloud binary runs, and the one piece of code whose entire job is to delete client-supplied identity — reached the gin middleware through zip.AdaptNetHTTPMiddleware. fiber's adaptor copies the middleware's headers back onto the fasthttp request with Set and never Del, so every header the strip DELETED was restored from the original request. A client-supplied X-User-IsAdmin: true reached the downstream subsystem intact. Reproduced against the old bridge before deleting it; TestZipAuth_StripSurvives is the guard. The cause was braiding: the ALLOW/DENY ladder was written out inside a gin closure, so the zip edge could not call it and bridged to it instead. The ladder is now a value — authGate.admit in authpolicy.go, which knows about no framework — and each transport is a dozen lines over it: gin for the legacy Lura edge, native zip for the mount. TestBothTransportsAdmitAlike drives one request through both and asserts they agree, across refuse / tokenless / public path / public host / ingest / API key / valid token. Every gate is unchanged; the 90KB security suite passes untouched, which is the proof. The probes are TYPED ops. Five anonymous closures across three apps each wrote their own map literal, so one answer had five spellings and none was in any document — an untyped route appends nothing to the op registry, so no OpenAPI operation, no MCP tool, no CLI, no SDK method. One declaration in probes.go now serves all three mount points, and zipdoc_gen.go carries the prose. Route table: BuildApp 2/2 typed, health app 3 routes/2 typed (/metrics is the one hatch — Prometheus exposition is text, and the reason is written next to it), Mount 1 route/0 typed → 2/2. Counts are asserted, not described: TestBuildApp_IsProbesAndNothingElse, TestMount_ProbesAreTypedOps and TestHealthApp_TableIsInTheBinary, which boots the app main boots rather than mounting the table directly. internal/lura and internal/plugin are untouched: they are vendored KrakenD/Lura, they are what the shipping `legacy` image runs, and rewriting a fork's router buys permanent merge pain. They go at Phase C with the rest of the legacy set. What left is the gin RUNTIME on the default build — no first-party path constructs a gin engine any more; the three files that still import it export transports consumed solely by legacy_engine.go. Co-authored-by: Hanzo Dev <[email protected]>
87 lines
2.8 KiB
Go
87 lines
2.8 KiB
Go
// Copyright © 2026 Hanzo AI. Apache-2.0 License.
|
|
|
|
package gateway
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
luxlog "github.com/luxfi/log"
|
|
zaplib "github.com/luxfi/zap"
|
|
)
|
|
|
|
// TestBuildApp_IsProbesAndNothingElse pins the HIP-0110 relay's HTTP surface.
|
|
//
|
|
// The relay's real work is on the ZAP node; the :8080 listener exists so the
|
|
// process has a liveness door. That makes the ROUTE COUNT a contract: the day
|
|
// somebody adds an HTTP route here, the gateway has quietly stopped being a
|
|
// pure relay and grown a second, unreviewed edge. This fails when that happens.
|
|
//
|
|
// It also pins the other half — every route this app carries is a TYPED op, so
|
|
// there is nothing on the public listener that is in no document.
|
|
func TestBuildApp_IsProbesAndNothingElse(t *testing.T) {
|
|
node := zaplib.NewNode(zaplib.NodeConfig{NodeID: "probes-test", Port: 0, NoDiscovery: true})
|
|
if err := node.Start(); err != nil {
|
|
t.Fatalf("zap node start: %v", err)
|
|
}
|
|
t.Cleanup(node.Stop)
|
|
|
|
app, err := BuildApp(RouterDeps{Logger: luxlog.New("test"), ZAPNode: node})
|
|
if err != nil {
|
|
t.Fatalf("BuildApp: %v", err)
|
|
}
|
|
|
|
d := app.Declaration()
|
|
const wantRoutes, wantOps = 2, 2
|
|
if len(d.Routes) != wantRoutes {
|
|
t.Errorf("relay HTTP routes: got %d want %d — %+v", len(d.Routes), wantRoutes, d.Routes)
|
|
}
|
|
if len(d.Ops) != wantOps {
|
|
t.Errorf("typed ops: got %d want %d — %+v", len(d.Ops), wantOps, d.Ops)
|
|
}
|
|
if len(d.Routes) != len(d.Ops) {
|
|
t.Errorf("the relay surface carries %d untyped route(s); it should carry none",
|
|
len(d.Routes)-len(d.Ops))
|
|
}
|
|
|
|
for path, want := range map[string]string{"/healthz": "ok", "/readyz": "ready"} {
|
|
resp, err := app.Fiber().Test(httptest.NewRequest("GET", path, nil))
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", path, err)
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("%s: status %d want 200", path, resp.StatusCode)
|
|
}
|
|
var out ProbeOut
|
|
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
|
t.Fatalf("%s decode: %v", path, err)
|
|
}
|
|
if out.Status != want || out.Service != ServiceName {
|
|
t.Fatalf("%s: got %+v want status=%q service=%q", path, out, want, ServiceName)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestProbes_OneAnswerEverywhere is the DRY assertion behind probes.go: the
|
|
// three places the gateway becomes an HTTP server answer the same probe with
|
|
// the same body, because there is one declaration rather than three literals.
|
|
//
|
|
// Asserted on the ops themselves, so it holds however they are mounted.
|
|
func TestProbes_OneAnswerEverywhere(t *testing.T) {
|
|
live, err := healthz(t.Context(), &ProbeIn{})
|
|
if err != nil {
|
|
t.Fatalf("healthz: %v", err)
|
|
}
|
|
ready, err := readyz(t.Context(), &ProbeIn{})
|
|
if err != nil {
|
|
t.Fatalf("readyz: %v", err)
|
|
}
|
|
if live.Status != "ok" || live.Service != ServiceName {
|
|
t.Errorf("healthz: got %+v", live)
|
|
}
|
|
if ready.Status != "ready" || ready.Service != ServiceName {
|
|
t.Errorf("readyz: got %+v", ready)
|
|
}
|
|
}
|