Files
zip/zip_test.go
zeekayandGitHub 879ef2d2bb zip on Fiber v3 — canonical Hanzo Go framework (#1)
* wipe upstream zeekay/zip — rebuilding on Fiber v3

* core: zip on Fiber v3 — App, Ctx, Router, typed handlers, OpenAPI

zip wraps *fiber.App. Sinatra/Express idiom (app.Get/Post/...).
Generic zip.Get[I,O], zip.Post[I,O] etc. reflection-build OpenAPI 3.1
spec served at /.well-known/openapi.json + Swagger UI at /docs.

Ctx exposes Hanzo identity (Org/User/UserEmail/IsAdmin) from gateway
X-* headers per HIP-0026. luxfi/log per-request logger via c.Log().

Module(method+path, runtime, modulePath) mounts a HIP-0105 extension
as a Fiber route. Loader is duck-typed via zip/runtime.Loader so zip
does not pull hanzoai/base as a transitive dep — service binary
constructs *extruntime.Loader and injects via zip.Config.Loader.

go.mod: go 1.26.3 (forced by luxfi/log v1.4.3 floor; brief said
1.23.6 but luxfi canonical requires 1.26.x). Fiber v3.2.0, no major
bumps planned.

* middleware: Recover, RequestID, Logger, Timeout, MaxBody, CORS, Auth, RateLimit, Telemetry

Recover catches handler panics → luxfi/log + 500 JSON.
RequestID forwards X-Request-Id (or mints 16-byte hex).
Logger wraps each request with method/path/status/dur, enriches the
per-request luxfi/log Logger via c.SetLog().
Auth trusts gateway-minted X-* headers first; falls back to bearer
verification via injected AuthVerifier.
StripIdentityHeaders for non-gateway deployments.
RateLimit is per-org (or per-IP fallback) in-memory token bucket.
Telemetry plumbs to any O11ySink.

* adapters + wsx + zaprpc

AdaptNetHTTP/AdaptNetHTTPFunc/AdaptNetHTTPMiddleware via Fiber's
adaptor pkg. app.Mount(prefix, http.Handler) for legacy chi/gin/beego
migration. Each adapter doc'd as MIGRATION TOOL — costs ~5% vs native
Fiber dispatch.

wsx is fasthttp/websocket wrapped — wsx.Upgrade(fn) returns a
zip.Handler. Conn is *websocket.Conn from fasthttp/websocket; text/
binary/close/ping/pong message constants re-exported.

zaprpc ships Service + Registry + Dispatch. app.ZAPRegistry() returns
the per-App registry. app.ZAPListen(addr) reserves the port and logs
but the on-the-wire dispatcher is stubbed — full integration in a
follow-up PR pending zapc server type stabilization.

* examples + core tests

examples/
  hello/             — minimal Sinatra-style API
  zap-typed/         — generic typed handler + auto-OpenAPI + Swagger
  subsystem-mount/   — HIP-0106 Mount(*App, deps) idiom
  module-routes/     — app.Module() over a stub zip/runtime.Loader
  websocket/         — wsx.Upgrade echo server
  sse-streaming/     — c.SendStreamWriter Server-Sent Events
  migrate-from-gin/  — gin→zip mechanical port
  migrate-from-chi/  — chi→zip via AdaptNetHTTP
  migrate-from-beego/— beego→zip via AdaptNetHTTP

Verified live: hello binary serves /hello + /health 200s; zap-typed
serves /v1/validate + /.well-known/openapi.json with reflection-
derived schemas. Tests pass: 5 cases (basic routing, HTTPError, typed
validation, groups, identity headers).

* docs: README, HANZO_CHANGES, MIGRATION, MIDDLEWARE, MODULE_HANDLERS

README pitches zip as canonical Hanzo Go framework on Fiber v3.
HANZO_CHANGES documents the diff from upstream zeekay/zip — every
file replaced; only the name kept.
MIGRATION covers gin/chi/beego/net-http with per-route recipes.
MIDDLEWARE covers Recover/RequestID/Logger/Auth/etc.
MODULE_HANDLERS covers app.Module() over HIP-0105 with the wire
envelope and per-runtime guidance.

* ctx.Bind validates via reflection tags; tighten AuthVerifier interface

c.Bind/BindQuery/BindURI now run validate() after parse and return
*HTTPError(400) directly — handler code is one line:

	if err := c.Bind(&v); err != nil { return err }

AuthVerifier.Verify now takes context.Context (was an empty
interface{}). middleware import adjusted.

* module-routes: default Content-Type to application/json; add tests

Modules that don't set a Content-Type in their response envelope get
application/json; charset=utf-8 by default — the wire format is JSON
both directions.

Tests cover: happy path (POST /v1/eval echoed back), runtime mismatch
between caller and manifest, AllowedRuntimes refusal, nil Loader.
2026-05-18 23:36:43 -07:00

140 lines
3.7 KiB
Go

package zip_test
import (
"context"
"encoding/json"
"io"
"net/http"
"strings"
"testing"
"github.com/hanzoai/zip"
)
// TestBasicRouting hits the hello-world path through fiber.Test to
// confirm the Sinatra idiom + JSON response work end-to-end.
func TestBasicRouting(t *testing.T) {
app := zip.New(zip.Config{AppName: "test", DisableStartupMessage: true})
app.Get("/hello", func(c *zip.Ctx) error {
return c.JSON(200, map[string]string{"message": "hi"})
})
req, _ := http.NewRequest("GET", "/hello", nil)
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatalf("Test(): %v", err)
}
if resp.StatusCode != 200 {
t.Fatalf("status %d, want 200", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
var got map[string]string
if err := json.Unmarshal(body, &got); err != nil {
t.Fatalf("json: %v / body=%s", err, body)
}
if got["message"] != "hi" {
t.Fatalf("body=%s", body)
}
}
// TestHTTPError checks zip.HTTPError → JSON error response.
func TestHTTPError(t *testing.T) {
app := zip.New(zip.Config{DisableStartupMessage: true})
app.Get("/boom", func(c *zip.Ctx) error {
return zip.ErrNotFound("nope")
})
req, _ := http.NewRequest("GET", "/boom", nil)
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatalf("Test(): %v", err)
}
if resp.StatusCode != 404 {
t.Fatalf("status %d, want 404", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
if !strings.Contains(string(body), "nope") {
t.Fatalf("body=%s", body)
}
}
// TestTyped exercises the generic typed handler + reflection-based
// validation + OpenAPI route installation.
func TestTyped(t *testing.T) {
type In struct {
Email string `json:"email" validate:"required,minlen=3"`
}
type Out struct {
OK bool `json:"ok"`
}
app := zip.New(zip.Config{DisableStartupMessage: true})
zip.Post(app, "/v1/test", func(ctx context.Context, in *In) (*Out, error) {
return &Out{OK: true}, nil
})
// Valid call.
req, _ := http.NewRequest("POST", "/v1/test",
strings.NewReader(`{"email":"[email protected]"}`))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatalf("Test(): %v", err)
}
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
t.Fatalf("status %d body=%s", resp.StatusCode, body)
}
// Invalid call — missing email.
req2, _ := http.NewRequest("POST", "/v1/test",
strings.NewReader(`{}`))
req2.Header.Set("Content-Type", "application/json")
resp2, err := app.Fiber().Test(req2)
if err != nil {
t.Fatalf("Test(): %v", err)
}
if resp2.StatusCode != 400 {
body, _ := io.ReadAll(resp2.Body)
t.Fatalf("status %d body=%s, want 400", resp2.StatusCode, body)
}
}
// TestGroup verifies app.Group prefixing.
func TestGroup(t *testing.T) {
app := zip.New(zip.Config{DisableStartupMessage: true})
v1 := app.Group("/v1")
v1.Get("/ping", func(c *zip.Ctx) error {
return c.String(200, "pong")
})
req, _ := http.NewRequest("GET", "/v1/ping", nil)
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatalf("Test(): %v", err)
}
if resp.StatusCode != 200 {
t.Fatalf("status %d", resp.StatusCode)
}
}
// TestIdentityHeaders confirms c.Org/User/Email map to X-* headers.
func TestIdentityHeaders(t *testing.T) {
app := zip.New(zip.Config{DisableStartupMessage: true})
app.Get("/who", func(c *zip.Ctx) error {
return c.JSON(200, map[string]string{
"org": c.Org(),
"user": c.User(),
})
})
req, _ := http.NewRequest("GET", "/who", nil)
req.Header.Set("X-Org-Id", "hanzo")
req.Header.Set("X-User-Id", "z")
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatalf("Test(): %v", err)
}
body, _ := io.ReadAll(resp.Body)
if !strings.Contains(string(body), `"org":"hanzo"`) ||
!strings.Contains(string(body), `"user":"z"`) {
t.Fatalf("body=%s", body)
}
}