Files
gateway/probes_test.go
zeekayandHanzo Dev eadca6f736 gateway: the HTTP edge is native zip — typed probes, one policy, no gin bridge
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]>
2026-08-01 22:04:20 -07:00

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)
}
}