Files
gateway/mount_test.go
zeekayandGitHub ca31860513 feat(cloud-mount): port gateway to HIP-0106 Mount(*zip.App, cloud.Deps) (#14)
* feat(cloud-mount): port gateway to HIP-0106 Mount(*zip.App, cloud.Deps)

Add pkg/gateway/mount.go (build tag: cloud) exposing the canonical
HIP-0106 contract so the unified hanzoai/cloud binary can co-resident
the gateway trust boundary without standing a second JWT validator up
in-process.

The mount does three things:
  1. Installs the existing NewAuthMiddleware as a zip middleware via a
     gin -> net/http -> zip adaptor. Same strip-list, same JWKS cache,
     same X-* mint list as the standalone cmd/gateway binary — one
     trust boundary, one implementation.
  2. Registers a native zip /_/gateway/healthz endpoint independent of
     the auth chain so probes work even with a misconfigured JWKS.
  3. Best-effort loads the routes table from GATEWAY_ROUTES_FILE; a
     missing file is non-fatal in cloud-mode (co-resident subsystems
     own their own routes), unlike the standalone path which fails
     closed.

Build tag isolation: the standalone gateway binary (KrakenD/Lura,
vendor mode, ghcr.io/hanzoai/gateway) builds with `-mod=vendor` and
must NOT pull hanzoai/cloud + hanzoai/zip into its vendor tree. The
`//go:build cloud` constraint on mount.go + mount_test.go keeps the
default build path clean. Build the cloud variant with `-tags=cloud`.

Wiring:
  - init() registers ("gateway", order=80) in cloud.Registry — runs
    before commerce(100) so identity headers are minted upstream of
    every subsystem.
  - go.mod gains the hanzoai/cloud + hanzoai/zip requires (gated by
    tag at the import sites), pinned via local ../ replace.

Tests:
  - TestMount_RegistersHealthAndRegistry boots a fresh zip.App, mounts
    gateway with AUTH_ENABLED=false, asserts /_/gateway/healthz answers.
  - cmd/gateway still builds clean with `-mod=vendor` (the standalone
    KrakenD path).

Schema: schema/gateway.zap describes Health service for zapc codegen.

* refactor: collapse pkg/gateway/ → root for clean import path

Subsystem code lives in package gateway at repo root. Importers now use
`github.com/hanzoai/gateway` (no /pkg/gateway/ nesting). Standalone shim moved
to cmd/gateway/ where applicable.

Per Hanzo Go-stdlib pattern: repo IS the package.

* refactor: flatten import path — github.com/hanzoai/cloud (drop /pkg/cloud)
2026-05-18 23:37:53 -07:00

82 lines
2.0 KiB
Go

// Copyright © 2026 Hanzo AI. MIT License.
//go:build cloud
// +build cloud
package gateway
import (
"io"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/hanzoai/cloud"
luxlog "github.com/luxfi/log"
"github.com/hanzoai/zip"
)
// TestMount_RegistersHealthAndRegistry asserts the gateway subsystem
// (1) registers itself in cloud.Registry via init(), (2) mounts a native
// zip /_/gateway/healthz endpoint that answers 200 + JSON, and (3) does
// not refuse to start when no GATEWAY_ROUTES_FILE is configured —
// co-resident subsystems own their own routes in cloud-mode.
func TestMount_RegistersHealthAndRegistry(t *testing.T) {
// Force AUTH_ENABLED off in the test: validating JWTs against a real
// JWKS URL would require network. The strip-list is exercised by
// auth_middleware_security_test.go at the gateway root.
t.Setenv("AUTH_ENABLED", "false")
// Ensure no routes file is referenced.
_ = os.Unsetenv("GATEWAY_ROUTES_FILE")
if !registryContains("gateway") {
t.Fatalf("cloud.Registry missing 'gateway'; Names=%v", registryNames())
}
app := zip.New(zip.Config{Logger: luxlog.New("test")})
deps := cloud.Deps{
Logger: luxlog.New("test"),
Brand: "hanzo",
Domain: "api.hanzo.ai",
DataDir: t.TempDir(),
}
if err := Mount(app, deps); err != nil {
t.Fatalf("Mount: %v", err)
}
req := httptest.NewRequest("GET", "/_/gateway/healthz", nil)
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatalf("Fiber Test: %v", err)
}
if resp.StatusCode != 200 {
t.Fatalf("status: got %d want 200", resp.StatusCode)
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("read body: %v", err)
}
if !strings.Contains(string(raw), `"service":"gateway"`) {
t.Fatalf("body: got %q, want service=gateway", string(raw))
}
}
func registryContains(name string) bool {
for _, s := range cloud.Registry {
if s.Name == name {
return true
}
}
return false
}
func registryNames() []string {
out := make([]string, 0, len(cloud.Registry))
for _, s := range cloud.Registry {
out = append(out, s.Name)
}
return out
}