test: one gate, and a ceiling that was never chosen

This repo had no test target at all, so "run the tests" meant whatever each
person typed. `make test` is now the one command, and it is not a bare
`go test ./...`: that reuses cached PASS results, so a stale build reports green
for code you just changed, and it runs without the race detector, which is where
this repo's store and session defects surface. -count=1 defeats the cache.

Adding -race exposed the reason nobody had: 8 failures, all `i/o timeout`, all on
argon2id paths (signup, onboard, registry token, SCIM create), in a different
package each run. fiber's App.Test defaults to TestConfig{Timeout: time.Second}.
The detector costs roughly an order of magnitude, `go test ./...` runs packages
in parallel, and one second is simply not a valid budget for a deliberately
expensive KDF under that load. Each suite had inherited the default separately —
23 call sites, 19 files, none of them having chosen it.

That is the worst failure mode a gate can have. It is red without a defect, so it
teaches everyone to re-run until green, and a real regression hiding in the noise
gets re-run away with it.

internal/testhttp states the ceiling once — generous enough that only a genuine
hang trips it, bounded so a deadlock still fails rather than blocking forever —
and every suite drives the router through it. The KDF cost is untouched: it is
the security property, and lowering it for tests would be testing something else.

Verified: `make test` green end to end, and 0 remaining direct Fiber().Test calls
so no suite can drift back to its own timeout.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
2026-07-28 18:50:06 -07:00
parent 7bf4b04dd7
commit eb83277c7d
21 changed files with 121 additions and 23 deletions
+20
View File
@@ -0,0 +1,20 @@
# The test gate. One command, run identically by a human and by CI.
#
# Not a bare `go test ./...`: that reuses cached PASS results, so a stale build
# can report green for code you just changed, and it runs without the race
# detector, which is where this repo's store and session defects actually show
# up. -count=1 defeats the cache; -race is the point.
.PHONY: test build fmt vet
test: ## Run the full suite — the gate. Everything must be green to ship.
go test ./... -race -count=1
build: ## Build every package.
go build ./...
fmt: ## Format.
go fmt ./...
vet: ## Vet.
go vet ./...
+4 -2
View File
@@ -35,6 +35,8 @@ import (
"github.com/hanzoai/iam/internal/routes"
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/store"
"github.com/hanzoai/iam/internal/testhttp"
)
const signingKid = "cert-hanzo" // the seeded admin signing cert's name = JWKS kid
@@ -187,7 +189,7 @@ func (h *harness) do(t *testing.T, method, path, bearer string, body any) int {
if bearer != "" {
req.Header.Set("Authorization", "Bearer "+bearer)
}
resp, err := h.app.Fiber().Test(req)
resp, err := testhttp.Do(h.app, req)
if err != nil {
t.Fatalf("%s %s: %v", method, path, err)
}
@@ -220,7 +222,7 @@ func (h *harness) mcpToolCall(t *testing.T, bearer, tool string, args any) (stat
if bearer != "" {
req.Header.Set("Authorization", "Bearer "+bearer)
}
resp, err := h.app.Fiber().Test(req)
resp, err := testhttp.Do(h.app, req)
if err != nil {
t.Fatalf("mcp tools/call %s: %v", tool, err)
}
+3 -1
View File
@@ -14,6 +14,8 @@ import (
"net/http/httptest"
"strings"
"testing"
"github.com/hanzoai/iam/internal/testhttp"
)
// doBody is do() plus the response body — the read surface's real contract.
@@ -32,7 +34,7 @@ func (h *harness) doBody(t *testing.T, method, path, bearer string, body any) (i
if bearer != "" {
req.Header.Set("Authorization", "Bearer "+bearer)
}
resp, err := h.app.Fiber().Test(req)
resp, err := testhttp.Do(h.app, req)
if err != nil {
t.Fatalf("%s %s: %v", method, path, err)
}
+3 -1
View File
@@ -36,6 +36,8 @@ import (
"github.com/hanzoai/orm"
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/testhttp"
)
// The two spellings an unauthorized caller must not be able to tell apart: a
@@ -95,7 +97,7 @@ func (h *harness) send(t *testing.T, method, path, auth string, body any) reply
if auth != "" {
req.Header.Set("Authorization", auth)
}
resp, err := h.app.Fiber().Test(req)
resp, err := testhttp.Do(h.app, req)
if err != nil {
t.Fatalf("%s %s: %v", method, path, err)
}
+4 -2
View File
@@ -12,6 +12,8 @@ import (
"github.com/hanzoai/orm"
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/testhttp"
)
// THE REQUEST CLOUD ACTUALLY MAKES.
@@ -59,7 +61,7 @@ func (h *harness) basicGet(t *testing.T, path, clientID, secret string) int {
req.Host = "hanzo.id"
req.Header.Set("Authorization", "Basic "+
base64.StdEncoding.EncodeToString([]byte(clientID+":"+secret)))
resp, err := h.app.Fiber().Test(req)
resp, err := testhttp.Do(h.app, req)
if err != nil {
t.Fatalf("GET %s: %v", path, err)
}
@@ -170,7 +172,7 @@ func TestSelfRead_ReturnsTheRowNotAnEmptyOk(t *testing.T) {
req.Host = "hanzo.id"
req.Header.Set("Authorization", "Basic "+
base64.StdEncoding.EncodeToString([]byte("hanzo-cloud:s3cret")))
resp, err := h.app.Fiber().Test(req)
resp, err := testhttp.Do(h.app, req)
if err != nil {
t.Fatalf("request: %v", err)
}
+3 -1
View File
@@ -18,6 +18,8 @@ import (
"github.com/hanzoai/iam/internal/routes"
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/store"
"github.com/hanzoai/iam/internal/testhttp"
)
const svcToken = "svc-token-secret-value"
@@ -49,7 +51,7 @@ func post(t *testing.T, app *zip.App, path, token, body string) (int, map[string
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := app.Fiber().Test(req)
resp, err := testhttp.Do(app, req)
if err != nil {
t.Fatalf("POST %s: %v", path, err)
}
+3 -1
View File
@@ -32,6 +32,8 @@ import (
"github.com/hanzoai/iam/internal/routes"
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/testhttp"
)
const signingKid = "cert-hanzo"
@@ -112,7 +114,7 @@ func (h *harness) get(t *testing.T, path, bearer string) (int, string) {
if bearer != "" {
req.Header.Set("Authorization", "Bearer "+bearer)
}
resp, err := h.app.Fiber().Test(req)
resp, err := testhttp.Do(h.app, req)
if err != nil {
t.Fatalf("GET %s: %v", path, err)
}
+3 -1
View File
@@ -21,6 +21,8 @@ import (
"github.com/hanzoai/orm"
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/testhttp"
)
const (
@@ -53,7 +55,7 @@ func (h *harness) getBasic(t *testing.T, path, clientID, secret string) (int, st
req := httptest.NewRequest("GET", path, nil)
req.Host = "hanzo.id"
req.SetBasicAuth(clientID, secret)
resp, err := h.app.Fiber().Test(req)
resp, err := testhttp.Do(h.app, req)
if err != nil {
t.Fatalf("GET %s: %v", path, err)
}
+3 -1
View File
@@ -18,6 +18,8 @@ import (
"net/http/httptest"
"strings"
"testing"
"github.com/hanzoai/iam/internal/testhttp"
)
// post issues a JSON POST through the real router and returns (status, rawBody).
@@ -30,7 +32,7 @@ func (h *harness) post(t *testing.T, path, bearer string, body any) (int, string
if bearer != "" {
req.Header.Set("Authorization", "Bearer "+bearer)
}
resp, err := h.app.Fiber().Test(req)
resp, err := testhttp.Do(h.app, req)
if err != nil {
t.Fatalf("POST %s: %v", path, err)
}
+4 -2
View File
@@ -36,6 +36,8 @@ import (
"github.com/hanzoai/iam/internal/oidc"
"github.com/hanzoai/iam/internal/routes"
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/testhttp"
)
const (
@@ -251,7 +253,7 @@ func (e *env) form(t *testing.T, path, clientID, secret string, form url.Values)
req.Host = "hanzo.id"
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(clientID+":"+secret)))
resp, err := e.app.Fiber().Test(req)
resp, err := testhttp.Do(e.app, req)
if err != nil {
t.Fatalf("form %s: %v", path, err)
}
@@ -275,7 +277,7 @@ func (e *env) req(t *testing.T, method, path, bearer, body, contentType string)
if bearer != "" {
req.Header.Set("Authorization", "Bearer "+bearer)
}
resp, err := e.app.Fiber().Test(req)
resp, err := testhttp.Do(e.app, req)
if err != nil {
t.Fatalf("%s %s: %v", method, path, err)
}
+3 -1
View File
@@ -34,6 +34,8 @@ import (
"github.com/hanzoai/iam/internal/routes"
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/store"
"github.com/hanzoai/iam/internal/testhttp"
)
const signingKid = "cert-hanzo"
@@ -126,7 +128,7 @@ func (h *harness) postBasic(t *testing.T, path string, body any, clientID, secre
// caller asserts on the status alone.
func (h *harness) do(t *testing.T, req *http.Request) (int, env) {
t.Helper()
resp, err := h.app.Fiber().Test(req)
resp, err := testhttp.Do(h.app, req)
if err != nil {
t.Fatalf("%s %s: %v", req.Method, req.URL.Path, err)
}
+3 -1
View File
@@ -33,6 +33,8 @@ import (
"github.com/hanzoai/iam/internal/routes"
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/store"
"github.com/hanzoai/iam/internal/testhttp"
)
const signingKid = "cert-hanzo"
@@ -98,7 +100,7 @@ func (h *harness) do(t *testing.T, path, bearer, body string) (int, map[string]a
if bearer != "" {
req.Header.Set("Authorization", "Bearer "+bearer)
}
resp, err := h.app.Fiber().Test(req)
resp, err := testhttp.Do(h.app, req)
if err != nil {
t.Fatalf("POST %s: %v", path, err)
}
+3 -1
View File
@@ -20,6 +20,8 @@ import (
"github.com/zap-proto/zip"
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/testhttp"
)
// HTTP-level test harness: register the whole OIDC surface on a fresh store and
@@ -137,7 +139,7 @@ func jsonReq(method, path string, body any) *http.Request {
func do(t *testing.T, app *zip.App, req *http.Request) (*http.Response, []byte) {
t.Helper()
resp, err := app.Fiber().Test(req)
resp, err := testhttp.Do(app, req)
if err != nil {
t.Fatalf("test request %s %s: %v", req.Method, req.URL.Path, err)
}
+3 -1
View File
@@ -17,6 +17,8 @@ import (
"github.com/hanzoai/iam/internal/routes"
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/testhttp"
)
// bootApp brings up the full IAM app over an embedded SQLite store, with the unified
@@ -48,7 +50,7 @@ func postProvision(t *testing.T, app *zip.App, token, body string) (int, map[str
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := app.Fiber().Test(req)
resp, err := testhttp.Do(app, req)
if err != nil {
t.Fatalf("POST /v1/iam/admin/provision: %v", err)
}
+3 -1
View File
@@ -32,6 +32,8 @@ import (
"github.com/hanzoai/iam/internal/routes"
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/testhttp"
)
const signingKid = "cert-hanzo"
@@ -96,7 +98,7 @@ func (h *harness) do(t *testing.T, method, path, bearer, body string) (int, map[
if bearer != "" {
req.Header.Set("Authorization", "Bearer "+bearer)
}
resp, err := h.app.Fiber().Test(req)
resp, err := testhttp.Do(h.app, req)
if err != nil {
t.Fatalf("%s %s: %v", method, path, err)
}
+3 -1
View File
@@ -28,6 +28,8 @@ import (
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/store"
"github.com/hanzoai/iam/internal/users"
"github.com/hanzoai/iam/internal/testhttp"
)
// HTTP-level harness: every test drives the REAL registered token/jwks routes through
@@ -240,7 +242,7 @@ func tokenPOST(t *testing.T, app *zip.App, id, secret, service string, scopes ..
func do(t *testing.T, app *zip.App, req *http.Request) (int, map[string]any, http.Header) {
t.Helper()
resp, err := app.Fiber().Test(req)
resp, err := testhttp.Do(app, req)
if err != nil {
t.Fatalf("%s %s: %v", req.Method, req.URL.Path, err)
}
+4 -2
View File
@@ -14,6 +14,8 @@ import (
"github.com/hanzoai/iam/internal/routes"
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/testhttp"
)
// IAM EMBEDDED ALONGSIDE A SIBLING SUBSYSTEM.
@@ -50,7 +52,7 @@ func TestGuard_DoesNotGateASiblingSubsystemsRoutes(t *testing.T) {
req := httptest.NewRequest("GET", "/v1/models", nil)
req.Host = "api.hanzo.ai"
resp, err := app.Fiber().Test(req)
resp, err := testhttp.Do(app, req)
if err != nil {
t.Fatalf("GET /v1/models: %v", err)
}
@@ -91,7 +93,7 @@ func TestGuard_StillGatesIamsOwnPaths(t *testing.T) {
} {
req := httptest.NewRequest("GET", path, nil)
req.Host = "hanzo.id"
resp, err := app.Fiber().Test(req)
resp, err := testhttp.Do(app, req)
if err != nil {
t.Fatalf("GET %s: %v", path, err)
}
+3 -1
View File
@@ -32,6 +32,8 @@ import (
"github.com/hanzoai/iam/internal/routes"
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/store"
"github.com/hanzoai/iam/internal/testhttp"
)
const (
@@ -106,7 +108,7 @@ func (h *harness) do(t *testing.T, method, path, bearer, body string) (int, stri
if bearer != "" {
req.Header.Set("Authorization", "Bearer "+bearer)
}
resp, err := h.app.Fiber().Test(req)
resp, err := testhttp.Do(h.app, req)
if err != nil {
t.Fatalf("%s %s: %v", method, path, err)
}
+40
View File
@@ -0,0 +1,40 @@
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// Package testhttp is the ONE way a test drives the registered router.
//
// It exists for a single value: the timeout. fiber's App.Test defaults to
// TestConfig{Timeout: time.Second}, and this repo's hot paths are argon2id —
// deliberately expensive, and under -race (the detector costs roughly an order of
// magnitude) on a box running `go test ./...` with packages in parallel, a signup,
// an onboard, a registry token or a SCIM create routinely exceeds one second. The
// suite then fails with `i/o timeout` on whichever package lost the CPU race, in a
// different place each run.
//
// That is the worst failure a gate can have: it is red without a defect, so it
// teaches everyone to re-run until green, and a real regression hiding among the
// noise gets re-run away with it. The KDF cost is deliberate and must not be
// lowered for tests; the one-second ceiling was simply never chosen.
//
// So the ceiling is stated once, here, generously — long enough that only a
// genuine hang trips it, short enough that a deadlock still fails rather than
// blocking the run forever. Every suite calls Do; none carries its own timeout,
// so none can drift.
package testhttp
import (
"net/http"
"time"
"github.com/zap-proto/fiber/v3"
"github.com/zap-proto/zip"
)
// timeout bounds one in-process test request. It is not a latency budget — no
// assertion depends on it — only a deadlock guard.
const timeout = 2 * time.Minute
// Do issues req against app's registered router in process and returns the
// response. The caller closes the body.
func Do(app *zip.App, req *http.Request) (*http.Response, error) {
return app.Fiber().Test(req, fiber.TestConfig{Timeout: timeout, FailOnTimeout: true})
}
+3 -1
View File
@@ -27,6 +27,8 @@ import (
"github.com/hanzoai/iam/internal/authz"
"github.com/hanzoai/iam/internal/oidc"
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/testhttp"
)
// HTTP-level harness: every test drives the REAL registered router behind the REAL
@@ -121,7 +123,7 @@ func seed(t *testing.T, db orm.DB, o opts) *schema.Application {
// a 200, the envelope proves what the client reads.
func do(t *testing.T, app *zip.App, req *http.Request) (int, map[string]any) {
t.Helper()
resp, err := app.Fiber().Test(req)
resp, err := testhttp.Do(app, req)
if err != nil {
t.Fatalf("%s %s: %v", req.Method, req.URL.Path, err)
}
+3 -1
View File
@@ -35,6 +35,8 @@ import (
"github.com/hanzoai/iam/internal/routes"
"github.com/hanzoai/iam/internal/schema"
"github.com/hanzoai/iam/internal/testhttp"
)
const signingKid = "cert-hanzo"
@@ -99,7 +101,7 @@ func (h *harness) do(t *testing.T, method, path, bearer, body string) (int, map[
if bearer != "" {
req.Header.Set("Authorization", "Bearer "+bearer)
}
resp, err := h.app.Fiber().Test(req)
resp, err := testhttp.Do(h.app, req)
if err != nil {
t.Fatalf("%s %s: %v", method, path, err)
}