@fuzdev/fuz_app 0.64.0 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/CLAUDE.md +510 -946
- package/dist/actions/action_codegen.d.ts +1 -1
- package/dist/actions/action_codegen.js +1 -1
- package/dist/actions/action_event_data.d.ts +1 -1
- package/dist/actions/broadcast_api.d.ts +1 -1
- package/dist/actions/broadcast_api.js +1 -1
- package/dist/actions/cancel.d.ts +2 -2
- package/dist/actions/cancel.js +3 -3
- package/dist/actions/connection_closer.d.ts +1 -4
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +1 -4
- package/dist/actions/register_action_ws.d.ts +2 -2
- package/dist/actions/register_ws_endpoint.d.ts +1 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +1 -2
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +1 -2
- package/dist/auth/CLAUDE.md +570 -1871
- package/dist/auth/account_schema.d.ts +1 -1
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +1 -1
- package/dist/auth/audit_log_ddl.d.ts +1 -1
- package/dist/auth/audit_log_ddl.d.ts.map +1 -1
- package/dist/auth/audit_log_ddl.js +1 -1
- package/dist/auth/audit_log_schema.js +2 -2
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +1 -5
- package/dist/auth/bootstrap_routes.d.ts +7 -1
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +15 -11
- package/dist/auth/daemon_token_middleware.d.ts +15 -5
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +24 -15
- package/dist/auth/invite_queries.d.ts +17 -7
- package/dist/auth/invite_queries.d.ts.map +1 -1
- package/dist/auth/invite_queries.js +19 -8
- package/dist/auth/keyring.d.ts +6 -6
- package/dist/auth/keyring.js +8 -8
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_actions.js +4 -2
- package/dist/auth/signup_routes.d.ts +47 -1
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +103 -52
- package/dist/db/create_db.d.ts.map +1 -1
- package/dist/db/create_db.js +13 -0
- package/dist/dev/setup.d.ts +2 -2
- package/dist/dev/setup.js +3 -3
- package/dist/env/resolve.d.ts +44 -7
- package/dist/env/resolve.d.ts.map +1 -1
- package/dist/env/resolve.js +94 -27
- package/dist/http/CLAUDE.md +243 -522
- package/dist/http/error_schemas.d.ts +0 -4
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +0 -4
- package/dist/http/ip_canonical.d.ts +5 -4
- package/dist/http/ip_canonical.d.ts.map +1 -1
- package/dist/http/ip_canonical.js +8 -4
- package/dist/http/jsonrpc.d.ts +23 -7
- package/dist/http/jsonrpc.d.ts.map +1 -1
- package/dist/http/jsonrpc.js +19 -3
- package/dist/http/origin.d.ts +1 -1
- package/dist/http/origin.js +1 -1
- package/dist/http/surface.d.ts +9 -2
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/runtime/mock.d.ts +1 -1
- package/dist/runtime/mock.js +2 -2
- package/dist/server/app_server.d.ts +41 -10
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +10 -4
- package/dist/server/env.d.ts +7 -7
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +14 -14
- package/dist/server/static.d.ts +4 -4
- package/dist/server/static.js +7 -7
- package/dist/testing/CLAUDE.md +740 -418
- package/dist/testing/admin_integration.d.ts +18 -23
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +230 -216
- package/dist/testing/app_server.d.ts +141 -39
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +157 -44
- package/dist/testing/audit_completeness.d.ts +25 -22
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +198 -159
- package/dist/testing/bootstrap_success.d.ts +28 -0
- package/dist/testing/bootstrap_success.d.ts.map +1 -0
- package/dist/testing/bootstrap_success.js +144 -0
- package/dist/testing/cross_backend/backend_config.d.ts +113 -0
- package/dist/testing/cross_backend/backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/backend_config.js +1 -0
- package/dist/testing/cross_backend/bench/bench_report.d.ts +46 -0
- package/dist/testing/cross_backend/bench/bench_report.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/bench_report.js +83 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +44 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +38 -0
- package/dist/testing/cross_backend/bench/scenario.d.ts +57 -0
- package/dist/testing/cross_backend/bench/scenario.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/scenario.js +28 -0
- package/dist/testing/cross_backend/bootstrap_backend.d.ts +41 -0
- package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -0
- package/dist/testing/cross_backend/bootstrap_backend.js +34 -0
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts +24 -0
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -0
- package/dist/testing/cross_backend/build_test_backend_paths.js +33 -0
- package/dist/testing/cross_backend/capabilities.d.ts +65 -0
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
- package/dist/testing/cross_backend/capabilities.js +47 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts +122 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_backend_configs.js +111 -0
- package/dist/testing/cross_backend/default_secrets.d.ts +40 -0
- package/dist/testing/cross_backend/default_secrets.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_secrets.js +39 -0
- package/dist/testing/cross_backend/default_spine_surface.d.ts +64 -0
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_spine_surface.js +121 -0
- package/dist/testing/cross_backend/setup.d.ts +451 -0
- package/dist/testing/cross_backend/setup.d.ts.map +1 -0
- package/dist/testing/cross_backend/setup.js +581 -0
- package/dist/testing/cross_backend/spawn_backend.d.ts +58 -0
- package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -0
- package/dist/testing/cross_backend/spawn_backend.js +229 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.d.ts +66 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.js +49 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts +37 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -0
- package/dist/testing/cross_backend/sse_round_trip.js +137 -0
- package/dist/testing/cross_backend/standard.d.ts +96 -0
- package/dist/testing/cross_backend/standard.d.ts.map +1 -0
- package/dist/testing/cross_backend/standard.js +49 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +171 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_reset_actions.js +213 -0
- package/dist/testing/cross_backend/testing_server_bun.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_bun.js +59 -0
- package/dist/testing/cross_backend/testing_server_core.d.ts +140 -0
- package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_core.js +68 -0
- package/dist/testing/cross_backend/testing_server_deno.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_deno.js +37 -0
- package/dist/testing/cross_backend/testing_server_node.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_node.js +50 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +72 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.js +112 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +35 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_round_trip.js +113 -0
- package/dist/testing/data_exposure.d.ts +11 -14
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +123 -146
- package/dist/testing/db_entities.d.ts +22 -1
- package/dist/testing/db_entities.d.ts.map +1 -1
- package/dist/testing/db_entities.js +24 -1
- package/dist/testing/integration.d.ts +56 -21
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +294 -319
- package/dist/testing/integration_helpers.d.ts +16 -6
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +7 -7
- package/dist/testing/mock_fs.d.ts.map +1 -1
- package/dist/testing/mock_fs.js +0 -2
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +9 -0
- package/dist/testing/role_grant_helpers.d.ts +31 -0
- package/dist/testing/role_grant_helpers.d.ts.map +1 -0
- package/dist/testing/role_grant_helpers.js +46 -0
- package/dist/testing/round_trip.d.ts +20 -16
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +61 -86
- package/dist/testing/rpc_helpers.d.ts +10 -4
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +1 -1
- package/dist/testing/rpc_round_trip.d.ts +24 -21
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +87 -104
- package/dist/testing/schema_introspect.d.ts +106 -0
- package/dist/testing/schema_introspect.d.ts.map +1 -0
- package/dist/testing/schema_introspect.js +123 -0
- package/dist/testing/schema_parity.d.ts +144 -0
- package/dist/testing/schema_parity.d.ts.map +1 -0
- package/dist/testing/schema_parity.js +233 -0
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +1 -68
- package/dist/testing/standard.d.ts +56 -25
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +62 -5
- package/dist/testing/stubs.d.ts +21 -6
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +33 -23
- package/dist/testing/testing_rate_limiter.d.ts +59 -0
- package/dist/testing/testing_rate_limiter.d.ts.map +1 -0
- package/dist/testing/testing_rate_limiter.js +74 -0
- package/dist/testing/transports/bootstrap.d.ts +52 -0
- package/dist/testing/transports/bootstrap.d.ts.map +1 -0
- package/dist/testing/transports/bootstrap.js +70 -0
- package/dist/testing/transports/fetch_transport.d.ts +81 -0
- package/dist/testing/transports/fetch_transport.d.ts.map +1 -0
- package/dist/testing/transports/fetch_transport.js +74 -0
- package/dist/testing/transports/sse_frame_reader.d.ts +41 -0
- package/dist/testing/transports/sse_frame_reader.d.ts.map +1 -0
- package/dist/testing/transports/sse_frame_reader.js +84 -0
- package/dist/testing/transports/sse_transport.d.ts +54 -0
- package/dist/testing/transports/sse_transport.d.ts.map +1 -0
- package/dist/testing/transports/sse_transport.js +51 -0
- package/dist/testing/transports/ws_client.d.ts +108 -0
- package/dist/testing/transports/ws_client.d.ts.map +1 -0
- package/dist/testing/transports/ws_client.js +56 -0
- package/dist/testing/transports/ws_transport.d.ts +43 -0
- package/dist/testing/transports/ws_transport.d.ts.map +1 -0
- package/dist/testing/transports/ws_transport.js +169 -0
- package/dist/testing/ws_round_trip.d.ts +21 -103
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +42 -40
- package/dist/ui/CLAUDE.md +5 -3
- package/dist/ui/MenuLink.svelte +16 -16
- package/dist/ui/MenuLink.svelte.d.ts +13 -4
- package/dist/ui/MenuLink.svelte.d.ts.map +1 -1
- package/package.json +10 -4
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -5,12 +5,11 @@ attack-surface generators, middleware mocks, integration suites, and RPC/SSE/WS
|
|
|
5
5
|
round-trip harnesses. Consumers import these to assemble their own test suites
|
|
6
6
|
against a fuz_app-derived server.
|
|
7
7
|
|
|
8
|
-
For narrative wiring examples (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
the helpers themselves.
|
|
8
|
+
For narrative wiring examples (consumer vitest setup), see
|
|
9
|
+
../../../docs/testing.md. For fuz_app's own suite conventions, see
|
|
10
|
+
../../test/CLAUDE.md. For shared testing conventions (`.db.test.ts`, `assert`
|
|
11
|
+
from vitest, `assert_rejects`, `vi.mock` caveats), see Skill(fuz-stack)
|
|
12
|
+
testing-patterns. This file is a reference index for the helpers themselves.
|
|
14
13
|
|
|
15
14
|
## Production guard — always the first import
|
|
16
15
|
|
|
@@ -22,84 +21,92 @@ Enforced by grep, not a linter; make this the first line in new modules.
|
|
|
22
21
|
|
|
23
22
|
### `stubs.ts` — `AppDeps` + `AppServerContext` stubs
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
| `create_stub_app_server_context(session_options)` | Stub `AppServerContext` — rate limiters null, `bootstrap_status.available: false`, `app_settings.open_signup: false`. |
|
|
39
|
-
| `create_test_app_surface_spec(options)` | Builds an `AppSurfaceSpec` that mirrors `create_app_server`'s route assembly: consumer routes + factory-managed bootstrap routes (prefixed via `bootstrap_route_prefix`, default `'/api/account'`) + stub middleware + surface generation. `CreateTestAppSurfaceSpecOptions` accepts `session_options`, `create_route_specs`, `env_schema?`, `event_specs?`, `rpc_endpoints?`, `ws_endpoints?`, `transform_middleware?`, `bootstrap_route_prefix?`. Single source of truth for attack-surface tests — track `create_app_server` wiring changes here. |
|
|
24
|
+
- `create_throwing_stub<T>(label)` — Proxy whose every property access throws `Throwing stub 'label' — unexpected access to 'prop'`. JS-internal probes return `undefined`; `toJSON` returns `"[throwing_stub:label]"` so accidental serialization is visible rather than `{}`.
|
|
25
|
+
- `create_noop_stub<T>(label, overrides?)` — Proxy whose every method returns `async () => undefined`; `overrides` pins specific props.
|
|
26
|
+
- `stub` — pre-built throwing stub labelled `'stub'`.
|
|
27
|
+
- `create_stub_db()` — real `Db` whose `client.query` yields `{rows: []}` and `transaction(fn)` synchronously calls `fn(inner_stub_db)`. Safe for `apply_route_specs`'s declarative transaction wrapper.
|
|
28
|
+
- `stub_handler()` — fresh `Response('stub')`.
|
|
29
|
+
- `stub_mw` — pass-through middleware (`async (_c, next) => next()`).
|
|
30
|
+
- `stub_app_deps` — frozen `AppDeps`, every capability throwing, `audit` a no-op `AuditEmitter` from `create_test_audit_emitter`.
|
|
31
|
+
- `create_stub_app_deps()` — factory: fresh `AppDeps` with no-op FS/keyring/password, a `create_noop_stub` DB, silent `Logger`, no-op `audit`.
|
|
32
|
+
- `create_test_audit_emitter()` — no-op `AuditEmitter`; `emit` / `emit_role_grant_target` no-op, `emit_pool` resolves immediately, `notify` no-op, `on_event_chain` empty.
|
|
33
|
+
- `create_stub_audit_sse()` — no-op `AuditLogSse` for surface-test wiring without booting real SSE. `subscribe` returns a no-op cleanup; `on_audit_event` no-op; `registry` is a fresh `SubscriberRegistry` (live `.size` / `.close_*` for registry-state tests, isolated per call). For real SSE plumbing build via `create_audit_log_sse` against `create_test_app`.
|
|
34
|
+
- `create_stub_api_middleware({include_daemon_token?})` — stub `MiddlewareSpec[]` matching `create_auth_middleware_specs`'s output (origin/session/request_context/bearer_auth, optional daemon_token) for surface generation without booting real auth. See `auth/CLAUDE.md` §Middleware for the real stack.
|
|
35
|
+
- `create_stub_app_server_context(session_options)` — stub `AppServerContext`; rate limiters null, `bootstrap_status.available: false`, `app_settings.open_signup: false`.
|
|
36
|
+
- `create_test_app_surface_spec(options)` — builds an `AppSurfaceSpec` mirroring `create_app_server`'s route assembly (consumer routes + stub middleware + surface generation). `CreateTestAppSurfaceSpecOptions`: `session_options`, `create_route_specs`, `env_schema?`, `event_specs?`, `rpc_endpoints?`, `ws_endpoints?`, `transform_middleware?`, `bootstrap?`. Bootstrap is opt-in (symmetric with `create_app_server` — omit to skip; pass the same value as prod to mount routes at `bootstrap.route_prefix ?? '/api/account'`). Single source of truth for attack-surface tests — track `create_app_server` wiring changes here.
|
|
40
37
|
|
|
41
38
|
Throwing stubs surface mock escape: a test that accidentally reaches into
|
|
42
39
|
stub territory breaks immediately with a label-scoped error rather than
|
|
43
40
|
silently returning `undefined` or `{}`. Use throwing stubs by default;
|
|
44
|
-
|
|
45
|
-
result.
|
|
41
|
+
no-op stubs only when a dep is known to be reached with a don't-care result.
|
|
46
42
|
|
|
47
43
|
### `entities.ts` — test entity factories
|
|
48
44
|
|
|
49
|
-
Plain `(overrides?) => Entity` constructors with sensible defaults —
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
Plain `(overrides?) => Entity` constructors with sensible defaults — callers
|
|
46
|
+
set only the fields the test cares about. `create_test_*` prefix avoids
|
|
47
|
+
collisions with real `account_queries.ts` factories. Override types widen
|
|
48
|
+
branded `Uuid` fields to `string` so tests pass literal ids without per-site
|
|
49
|
+
casts — the factory brands internally. Exported as `TestAccountOverrides` /
|
|
50
|
+
`TestActorOverrides` / `TestRoleGrantOverrides` / `TestAuditEventOverrides`.
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`
|
|
58
|
-
|
|
59
|
-
| Factory | Default id / role |
|
|
60
|
-
| ------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
61
|
-
| `create_test_account(overrides?)` | `{id: 'acct-test', username: 'test_user', …}` |
|
|
62
|
-
| `create_test_actor(overrides?)` | `{id: 'actor-test', account_id: 'acct-test', …}` |
|
|
63
|
-
| `create_test_role_grant(overrides?)` | `{id: 'role-grant-test', actor_id: 'actor-test', role: 'admin', scope_id: null, …}` |
|
|
64
|
-
| `create_test_context(role_grants?)` | `{account, actor, role_grants}` — pass `[{role: 'keeper'}, {role: 'admin'}]` for multi-role. |
|
|
65
|
-
| `create_test_audit_event(overrides?)` | `{id: 'evt-test', event_type: 'login', outcome: 'success', …}` — for SSE guard / audit tests. |
|
|
52
|
+
- `create_test_account(overrides?)` — `{id: 'acct-test', username: 'test_user', …}`
|
|
53
|
+
- `create_test_actor(overrides?)` — `{id: 'actor-test', account_id: 'acct-test', …}`
|
|
54
|
+
- `create_test_role_grant(overrides?)` — `{id: 'role-grant-test', actor_id: 'actor-test', role: 'admin', scope_id: null, …}`
|
|
55
|
+
- `create_test_context(role_grants?)` — `{account, actor, role_grants}`; pass `[{role: 'keeper'}, {role: 'admin'}]` for multi-role.
|
|
56
|
+
- `create_test_audit_event(overrides?)` — `{id: 'evt-test', event_type: 'login', outcome: 'success', …}`, for SSE guard / audit tests.
|
|
66
57
|
|
|
67
58
|
### `mock_fs.ts` — in-memory filesystem
|
|
68
59
|
|
|
69
60
|
`create_mock_fs(initial_files?) => {read_file, write_file, get_file}`.
|
|
70
61
|
Missing-path reads throw an `Error` with `.code = 'ENOENT'` so callers
|
|
71
|
-
exercise the same branches as `node:fs`.
|
|
72
|
-
|
|
62
|
+
exercise the same branches as `node:fs`. DI-based filesystem tests only;
|
|
63
|
+
never replaces `node:fs` globally.
|
|
73
64
|
|
|
74
65
|
### `db_entities.ts` — DB-backed entity factories
|
|
75
66
|
|
|
76
67
|
`create_test_account_with_actor(db, {username, password_hash?})` wraps
|
|
77
|
-
`query_create_account_with_actor` with
|
|
68
|
+
`query_create_account_with_actor` with default `password_hash` (`'hash'`).
|
|
78
69
|
Returns `{account, actor}`. Replaces the per-file `create_user` /
|
|
79
70
|
`create_test_actor` / `create_test_account` helpers that had accumulated
|
|
80
|
-
across the auth test suite. Use for query-level tests
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
71
|
+
across the auth test suite. Use for query-level tests needing real DB rows
|
|
72
|
+
but not a full session/token bundle. For tests also needing an API token +
|
|
73
|
+
session cookie + role_grants, use `bootstrap_test_keeper` from `app_server.ts`.
|
|
74
|
+
|
|
75
|
+
`create_test_role_grant_direct(db, input)` wraps `query_create_role_grant`
|
|
76
|
+
for tests needing an active role_grant seeded directly, bypassing the
|
|
77
|
+
production offer/accept consent flow. Use only when the test focuses on
|
|
78
|
+
revoke or isolation semantics rather than the consent path — the schema
|
|
79
|
+
permits null `source_offer_id` for exactly this case. For tests exercising
|
|
80
|
+
the production grant flow, drive `role_grant_offer_and_accept` from
|
|
81
|
+
`role_grant_helpers.ts` instead.
|
|
82
|
+
|
|
83
|
+
### `role_grant_helpers.ts` — RPC-flow role_grant helpers
|
|
84
|
+
|
|
85
|
+
`role_grant_offer_and_accept({app, rpc_path, grantor, recipient, role})`
|
|
86
|
+
drives the full consent flow (grantor `role_grant_offer_create` → recipient
|
|
87
|
+
`role_grant_offer_accept`) over the production RPC surface and returns
|
|
88
|
+
`{offer_id, role_grant_id}`. Sibling to `create_test_role_grant_direct` —
|
|
89
|
+
that one bypasses the consent flow; this one exercises it end-to-end so the
|
|
90
|
+
suite picks up post-commit fan-out (audit, SSE broadcasts, `_supersede`
|
|
91
|
+
notifications) a direct DB seed would miss. `grantor` and `recipient` accept
|
|
92
|
+
`TestApp | TestAccount` / `TestAccount` so the call site passes the same
|
|
93
|
+
object that already owns the headers + account id, ruling out caller-side
|
|
94
|
+
mismatch.
|
|
84
95
|
|
|
85
96
|
### `audit_drift_guard.ts` — audit-emission validation
|
|
86
97
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
| `create_recording_audit_emitter(calls_ref?)` | Build a no-op `AuditEmitter` that pushes every `emit` and `emit_pool` call into `calls`. Pass `calls_ref` to write into a caller-owned array; omit to let the helper allocate one. Returns `{emitter, calls}` — destructure `emitter` as the `audit` dep and read `calls` to assert on captured metadata. Replaces per-file capturing emitters previously duplicated across `password_change.test.ts`, `audit_log.test.ts`, etc. |
|
|
93
|
-
| `RecordingAuditEmitter` | `{emitter: AuditEmitter; calls: Array<AuditLogInput>}` — return shape of `create_recording_audit_emitter`. |
|
|
98
|
+
- `install_audit_drift_guard()` — `beforeEach` resets + `afterEach` zero-checks `audit_metadata_validation_failures` + `audit_unknown_event_type_failures` counters from `auth/audit_log_queries.ts`. Call once at the top of any `describe_db` block firing audit emits — production validation is fail-open, so without this any regression shipping a typo'd `event_type` or undeclared metadata field is silent. Pair with `await_pending_effects: true` (the `create_test_app` default) so fire-and-forget audit writes complete by response time.
|
|
99
|
+
- `create_emit_ordering_audit_factory<E>(seq_ref, events_ref, build_inner)` — returns an `AuditFactory` wrapping `build_inner({db, log})` so every `emit` pushes `{kind: 'emit', at: seq.value++}` into a shared sequence + events array. Pass through `create_test_app({audit_factory: …})` — the test backend invokes it with its `{db, log}` and lands the wrapped emitter on `deps.audit`. Generic `E extends {kind: string; at: number}` so the events array typechecks against the caller's own `close` / custom marker shape. Pair with `create_recording_closer(seq_ref)` for close-vs-emit ordering tests. Scope is `emit` only — `emit_role_grant_target`, `emit_pool`, `notify` forward to the inner emitter unwrapped.
|
|
100
|
+
- `AuditEmitMarker` — `{kind: 'emit'; at: number}`, the marker type pushed.
|
|
101
|
+
- `create_recording_audit_emitter(calls_ref?)` — no-op `AuditEmitter` pushing every `emit` and `emit_pool` call into `calls`. Pass `calls_ref` to write into a caller-owned array; omit to let the helper allocate. Returns `{emitter, calls}` — destructure `emitter` as the `audit` dep and read `calls` to assert. Replaces per-file capturing emitters previously duplicated across `password_change.test.ts`, `audit_log.test.ts`, etc.
|
|
102
|
+
- `RecordingAuditEmitter` — `{emitter: AuditEmitter; calls: Array<AuditLogInput>}`.
|
|
94
103
|
|
|
95
104
|
### `connection_closer_helpers.ts` — `ConnectionCloser` test doubles
|
|
96
105
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
| `RecordedClose` | `{method: 'session' \| 'token' \| 'account', id, at}` — recorded shape pushed by the closer. |
|
|
102
|
-
| `RecordingCloser` | `{closer, calls}` — return shape of `create_recording_closer`. |
|
|
106
|
+
- `create_recording_closer(seq_ref?)` — `{closer, calls}`; every method on `closer` records `{method, id, at}` into `calls`. Pass `seq_ref` to share the sequence counter with `create_emit_ordering_audit_factory` so close + emit markers compose for ordering tests.
|
|
107
|
+
- `assert_close_call(call, method, id)` — pins `{method, id}` on a recorded close call without baking in the `at: N` sequence number. Use at every "did the closer fire?" site; reserve `at: N` assertions for the dedicated ordering test paired with the capture helper.
|
|
108
|
+
- `RecordedClose` — `{method: 'session' | 'token' | 'account', id, at}`.
|
|
109
|
+
- `RecordingCloser` — `{closer, calls}`.
|
|
103
110
|
|
|
104
111
|
## Database — `db.ts`
|
|
105
112
|
|
|
@@ -107,30 +114,27 @@ Factory builders for parameterized DB tests. Consumer projects pass their
|
|
|
107
114
|
`init_schema` callback (which calls `run_migrations(db, [auth_migration_ns, ...app_migrations])`);
|
|
108
115
|
factories accept any migration namespace set.
|
|
109
116
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
| `create_describe_db(factories, truncate_tables)` | Returns `describe_db(name, fn)` that runs `fn(get_db)` once per factory, inside a `describe` block with shared `beforeAll(create)` + `beforeEach(TRUNCATE)` + `afterAll(close)`. Skipped factories use `describe.skip`. |
|
|
122
|
-
| `log_db_factory_status(factories)` | Console summary of enabled / skipped factories. |
|
|
117
|
+
- `IS_CI` — `process.env.CI === 'true'`.
|
|
118
|
+
- `DbFactory` — `{name, create, close, skip, skip_reason?}`.
|
|
119
|
+
- `reset_pglite(db)` — `DROP SCHEMA public CASCADE` + recreate. Reuses a live PGlite instance.
|
|
120
|
+
- `create_pglite_factory(init_schema)` — in-memory; no external deps; `skip: false`. See WASM caching below.
|
|
121
|
+
- `create_pg_factory(init_schema, test_url?)` — PostgreSQL; `skip: true` when `test_url` missing. Drops `schema_version` before `init_schema` so migrations re-evaluate against actual tables (prevents stale tracker rows from skipping migrations when DDL changes between sessions). Pool reused + cleaned up across `create()` calls.
|
|
122
|
+
- `auth_truncate_tables` — `['invite', 'api_token', 'auth_session', 'role_grant', 'role_grant_offer', 'actor', 'account']` in FK-safe order. Excludes `audit_log` — unit DB tests don't need to truncate it.
|
|
123
|
+
- `auth_integration_truncate_tables` — `auth_truncate_tables + ['audit_log']` for integration suites that exercise the audit path.
|
|
124
|
+
- `auth_drop_tables` — full set from `auth_migrations` in drop order; call `drop_auth_schema(db)` at the top of `init_schema` on persistent pg databases that may hold stale DDL from previous fuz_app versions.
|
|
125
|
+
- `drop_auth_schema(db)` — `DROP TABLE IF EXISTS <table> CASCADE` for every entry in `auth_drop_tables` plus `schema_version`. Safe on fresh DBs.
|
|
126
|
+
- `create_describe_db(factories, truncate_tables)` — returns `describe_db(name, fn)` running `fn(get_db)` once per factory inside a `describe` with shared `beforeAll(create)` + `beforeEach(TRUNCATE)` + `afterAll(close)`. Skipped factories use `describe.skip`.
|
|
127
|
+
- `log_db_factory_status(factories)` — console summary of enabled / skipped factories.
|
|
123
128
|
|
|
124
129
|
**PGlite WASM caching.** `create_pglite_factory` shares a single PGlite
|
|
125
130
|
instance in a module-level ref (`module_db`) across all factories in the
|
|
126
131
|
same vitest worker thread. Subsequent `create()` calls
|
|
127
|
-
`DROP SCHEMA public CASCADE` instead of paying the ~500–700ms WASM
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
file in the run.
|
|
132
|
+
`DROP SCHEMA public CASCADE` instead of paying the ~500–700ms WASM cold-start
|
|
133
|
+
cost again. Each vitest file runs in its own worker, so no cross-file
|
|
134
|
+
contamination — but inside a file, suites share state until the schema is
|
|
135
|
+
reset. The `db` vitest project (opted into by the `.db.test.ts` suffix) runs
|
|
136
|
+
with `isolate: false` + `fileParallelism: false` to amortize WASM boot across
|
|
137
|
+
every DB test file.
|
|
134
138
|
|
|
135
139
|
## Test app assembly
|
|
136
140
|
|
|
@@ -143,41 +147,44 @@ fully assembled Hono app + the backend + helpers.
|
|
|
143
147
|
|
|
144
148
|
Key module-scope values:
|
|
145
149
|
|
|
146
|
-
- `stub_password_deps` — `PasswordHashDeps`
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
- `
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
150
|
+
- `stub_password_deps` — `PasswordHashDeps` hashing via `stub_hash_${password}` and verifying by equality. Deterministic, no Argon2 cost — use for every test not specifically exercising password hashing.
|
|
151
|
+
- `TEST_COOKIE_SECRET` — 64-hex-char deterministic cookie secret. Produces a valid `Keyring` via `create_validated_keyring`. Never used in production — the stub guard plus fixed value is the contract.
|
|
152
|
+
- `fallback_pglite_factory` — module-level PGlite factory `create_test_app_server` uses when no `db` is passed. Reuses the WASM cache via `create_pglite_factory`.
|
|
153
|
+
|
|
154
|
+
Two helpers share the "insert account + actor + roles + API token + session +
|
|
155
|
+
cookie" flow, split by intent:
|
|
156
|
+
|
|
157
|
+
- `bootstrap_test_keeper(options)` — keeper path used by `create_test_app_server`. Same body as the general helper plus a lock flip (`UPDATE bootstrap_lock SET bootstrapped = true ...`) so test DB state matches a real bootstrap completion, letting production code trust the lock as the single signal.
|
|
158
|
+
- `create_test_account_with_credentials(options)` — general path used by `TestApp.create_account` for additional non-keeper accounts. Same body, no lock interaction (additional accounts aren't bootstraps).
|
|
159
|
+
|
|
160
|
+
Both take `{db, keyring, session_options, password, username?, password_value?, roles?}`
|
|
161
|
+
(shared `CreateTestAccountWithCredentialsOptions` / `BootstrapTestKeeperOptions`).
|
|
162
|
+
|
|
163
|
+
For exercising the bootstrap success path end-to-end against an empty DB (no
|
|
164
|
+
pre-keeper, lock unflipped), use `create_test_app_for_bootstrap` — pair with
|
|
165
|
+
`describe_bootstrap_success_tests` for the consumer-runnable suite.
|
|
166
|
+
|
|
167
|
+
Types:
|
|
168
|
+
|
|
169
|
+
- `TestAppServer extends AppBackend` — adds `account`, `actor`, `api_token`, `session_cookie`, `keyring`, `cleanup()`.
|
|
170
|
+
- `TestAppServerOptions` — `session_options` (required), optional `db`, `db_type`, `password`, `username`, `password_value`, `roles`, `audit_factory`. The optional `audit_factory` defaults to `default_audit_factory` (no-listener `create_audit_emitter` over the test backend's `{db, log}`); pass a custom factory to compose `on_audit_event` / `audit_log_config`, wrap with `emit_decorator` (via `create_emit_ordering_audit_factory`), or otherwise replace the emitter. Mirrors `CreateAppBackendOptions` end-to-end — the previous `on_audit_event` / `audit_log_config` sugar was removed alongside the production rename.
|
|
171
|
+
- `CreateTestAppOptions extends TestAppServerOptions` — adds `create_route_specs` (required), `rpc_endpoints?: RpcEndpointsSuiteOption` (top-level only — single source of truth, symmetric with the suite-level option), `bootstrap?: BootstrapServerOptions` (top-level only — same precedent as `rpc_endpoints`), and `app_options?: SuiteAppOptions` (`Partial<AppServerOptions>` excluding the five fields the helper manages: `backend`, `session_options`, `create_route_specs`, `rpc_endpoints`, `bootstrap`).
|
|
172
|
+
- `TestAccount` — `{account, actor, session_cookie, api_token, create_session_headers, create_bearer_headers}`.
|
|
173
|
+
- `TestApp` — `{app, backend, surface_spec, surface, route_specs, create_session_headers, create_bearer_headers, create_daemon_token_headers, create_account, cleanup}`.
|
|
174
|
+
- `CreateTestAppForBootstrapOptions` — `{session_options, create_route_specs, rpc_endpoints?, bootstrap: BootstrapLiveOptions, bootstrap_token, app_options?, db?, db_type?, password?, audit_factory?}`. `bootstrap` is required + narrowed to `live` mode (the helper exists for the success-path test).
|
|
175
|
+
- `TestAppForBootstrap` — `{app, backend, surface_spec, surface, route_specs, create_request_headers, cleanup}`. No keeper credentials (test drives bootstrap itself).
|
|
169
176
|
|
|
170
177
|
`create_test_app` hard-codes the test-friendly `AppServerOptions`:
|
|
171
|
-
`allowed_origins: [/^http:\/\/localhost/]`, stub proxy pinned to
|
|
172
|
-
`
|
|
173
|
-
|
|
174
|
-
**`await_pending_effects: true`** (fire-and-forget effects complete
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
`allowed_origins: [/^http:\/\/localhost/]`, stub proxy pinned to `127.0.0.1`,
|
|
179
|
+
`env_schema: z.object({})`, every rate limiter `null`, static daemon token
|
|
180
|
+
state (no rotation, keeper already set),
|
|
181
|
+
**`await_pending_effects: true`** (fire-and-forget effects complete before
|
|
182
|
+
the response returns so tests can assert on side effects inline), and silent
|
|
183
|
+
logger. Override via `app_options`.
|
|
177
184
|
|
|
178
|
-
A fresh Hono app is created on every call because middleware closures
|
|
179
|
-
|
|
180
|
-
|
|
185
|
+
A fresh Hono app is created on every call because middleware closures bind
|
|
186
|
+
to the server's deps (db, keyring). Hono assembly is cheap (~10–50ms);
|
|
187
|
+
PGlite WASM caching in `db.ts` is where the real savings are.
|
|
181
188
|
|
|
182
189
|
### `auth_apps.ts` — adversarial-auth app factories
|
|
183
190
|
|
|
@@ -185,62 +192,86 @@ Pre-built Hono apps at each auth level (public / authed / keeper / per-role)
|
|
|
185
192
|
for attack-surface testing. No middleware stack — a single `/*` middleware
|
|
186
193
|
injects `ACCOUNT_ID_KEY` + `REQUEST_CONTEXT_KEY` + `CREDENTIAL_TYPE_KEY`
|
|
187
194
|
(default `'session'`) plus the `TEST_CONTEXT_PRESET_KEY` flag (so the
|
|
188
|
-
dispatcher's authorization phase trusts the pre-baked context and skips
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
dispatcher's authorization phase trusts the pre-baked context and skips its
|
|
196
|
+
DB-backed actor resolution), then hands off to `apply_route_specs` with
|
|
197
|
+
`fuz_auth_guard_resolver` + `create_fuz_authorization_handler`. Production
|
|
198
|
+
middleware never sets `TEST_CONTEXT_PRESET_KEY`, so the escape hatch is
|
|
199
|
+
test-only by construction.
|
|
200
|
+
|
|
201
|
+
- `create_test_request_context(role?)` — minimal `RequestContext`: one account, one actor, one role_grant for `role` (or none).
|
|
202
|
+
- `create_test_app_from_specs(specs, auth_ctx?, credential_type?)` — Hono app with pre-set context + `apply_route_specs`. `credential_type` defaults to `'session'` when an auth context is supplied — override for `'daemon_token'` / `'api_token'` tests.
|
|
203
|
+
- `AuthTestApps` — `{public, authed, keeper, by_role: Map<string, Hono>}`.
|
|
204
|
+
- `create_auth_test_apps(specs, roles)` — builds one app per auth level. Keeper app uses `credential_type: 'daemon_token'` so `require_credential_types(['daemon_token'])` passes.
|
|
205
|
+
- `select_auth_app(apps, auth)` — map `RouteAuth` → matching Hono app. Throws for missing `role:*` entries.
|
|
206
|
+
- `resolve_test_path(path)` — `:foo` → `test_foo`; adequate for routes without format-constrained params.
|
|
207
|
+
|
|
208
|
+
## Cross-impl schema parity
|
|
209
|
+
|
|
210
|
+
### `schema_introspect.ts` — `query_schema_snapshot`
|
|
211
|
+
|
|
212
|
+
- `query_schema_snapshot(db, options?)` — introspects a live DB into a deterministic `SchemaSnapshot` via `pg_catalog` + `information_schema`. Captures tables, columns (with `udt_name` to distinguish int4/int8), indexes (`indexdef`), constraints (`pg_get_constraintdef`), sequences, and `schema_version` rows.
|
|
213
|
+
- `SchemaSnapshot` — fully JSON-serializable; every collection deterministically sorted on capture so structural equality is stable across runs. `applied_at` is excluded from `schema_version` rows so timestamps don't drift the snapshot.
|
|
214
|
+
|
|
215
|
+
### `schema_parity.ts` — `assert_schema_snapshots_equal`
|
|
216
|
+
|
|
217
|
+
- `diff_schema_snapshots(a, b)` — structured `Array<SchemaDiff>` between two snapshots; empty array means parity holds.
|
|
218
|
+
- `format_schema_diffs(diffs, labels?)` — human-readable multi-line rendering; labels name the impl on each side (e.g., `{a: 'deno', b: 'rust'}`).
|
|
219
|
+
- `assert_schema_snapshots_equal(a, b, labels?)` — throws on drift with a fully-formatted diff message.
|
|
220
|
+
- `SchemaDiff` — tagged-union per drift kind: `schema_version_only_in`, `schema_version_sequence_differs`, `table_only_in`, `column_only_in`, `column_field_differs`, `index_only_in`, `index_definition_differs`, `constraint_only_in`, `constraint_differs`, `sequence_only_in`, `sequence_data_type_differs`.
|
|
221
|
+
|
|
222
|
+
**Cross-impl gate pattern** — consumers running two backends against a
|
|
223
|
+
shared schema (zzz's `--backend=both`, fuz_app's cross-backend suite)
|
|
224
|
+
bootstrap each impl against an isolated DB, snapshot, then compare:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
await drop_recreate_db('zzz_test');
|
|
228
|
+
await spawn_backend(deno_config);
|
|
229
|
+
const snapshot_deno = await query_schema_snapshot(db);
|
|
230
|
+
await drop_recreate_db('zzz_test');
|
|
231
|
+
await spawn_backend(rust_config);
|
|
232
|
+
const snapshot_rust = await query_schema_snapshot(db);
|
|
233
|
+
assert_schema_snapshots_equal(snapshot_deno, snapshot_rust, {a: 'deno', b: 'rust'});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Each impl's _own_ tests still gate its DDL correctness independently — this
|
|
237
|
+
pair is purely the cross-impl drift check.
|
|
202
238
|
|
|
203
239
|
## Assertions, coverage, helpers
|
|
204
240
|
|
|
205
241
|
### `assertions.ts` — surface + error-schema assertions
|
|
206
242
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
| `get_route_error_schema(lookup, route, status)` | Read out of a pre-built merged-error-schema map. |
|
|
215
|
-
| `assert_error_schema_valid(lookup, route, status, body)` | Assert a schema exists + parses the body. |
|
|
243
|
+
- `resolve_fixture_path(filename, import_meta_url)` — absolute path relative to the caller's module (use `import.meta.url`).
|
|
244
|
+
- `assert_surface_matches_snapshot(surface, path)` — compares live `AppSurface` against a committed JSON snapshot; failure message instructs `gro gen`.
|
|
245
|
+
- `assert_surface_deterministic(build_surface)` — build twice, `deepStrictEqual` results; catches nondeterminism in surface generation.
|
|
246
|
+
- `assert_only_expected_public_routes(surface, list)` — bidirectional: no unexpected public routes, no missing expected ones. Format: `['GET /health', 'POST /api/account/login']`.
|
|
247
|
+
- `assert_full_middleware_stack(surface, prefix, mws)` — every route under `prefix` has exactly `mws` as its middleware chain.
|
|
248
|
+
- `get_route_error_schema(lookup, route, status)` — reads from a pre-built merged-error-schema map.
|
|
249
|
+
- `assert_error_schema_valid(lookup, route, status, body)` — assert a schema exists + parses the body.
|
|
216
250
|
|
|
217
251
|
### `surface_invariants.ts` — structural + policy invariants
|
|
218
252
|
|
|
219
|
-
Structural invariants (options-free,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
| `assert_ws_method_descriptions_present` | Every WS method on every endpoint has a non-empty `description`. |
|
|
242
|
-
| `assert_ws_endpoints_include_protocol_actions` | Every WS endpoint includes `heartbeat` + `cancel` (the `protocol_actions` spread from `actions/protocol.js`). |
|
|
243
|
-
| `assert_ws_notifications_have_null_auth` | WS method `kind === 'remote_notification' ⟺ auth === null` — guards against drift between spec union and surface emitter. |
|
|
253
|
+
Structural invariants (options-free, universal):
|
|
254
|
+
|
|
255
|
+
- `assert_protected_routes_declare_401` — every protected route has 401 in `error_schemas`.
|
|
256
|
+
- `assert_role_routes_declare_403` — every role/keeper route has 403.
|
|
257
|
+
- `assert_input_routes_declare_400` — every route with input has 400.
|
|
258
|
+
- `assert_params_routes_declare_400` — every route with params has 400.
|
|
259
|
+
- `assert_query_routes_declare_400` — every route with query has 400.
|
|
260
|
+
- `assert_descriptions_present` — every route has a non-empty description.
|
|
261
|
+
- `assert_no_duplicate_routes` — no duplicate method+path pairs.
|
|
262
|
+
- `assert_middleware_errors_propagated` — every middleware-declared error status appears on every applicable route.
|
|
263
|
+
- `assert_error_schemas_structurally_valid` — every declared error schema has an `error` property at the top level (matches `ApiError`).
|
|
264
|
+
- `assert_error_code_status_consistency` — the same `z.literal()` error code never appears at two different HTTP statuses.
|
|
265
|
+
- `assert_404_schemas_use_specific_errors` — routes with params declaring 404 must use `z.literal()` or `z.enum()`, not generic `z.string()`.
|
|
266
|
+
|
|
267
|
+
RPC / WS structural invariants (options-free, apply over `surface.rpc_endpoints`
|
|
268
|
+
|
|
269
|
+
- `surface.ws_endpoints`):
|
|
270
|
+
|
|
271
|
+
* `assert_rpc_method_descriptions_present` — every RPC method on every endpoint has a non-empty `description`.
|
|
272
|
+
* `assert_ws_method_descriptions_present` — every WS method on every endpoint has a non-empty `description`.
|
|
273
|
+
* `assert_ws_endpoints_include_protocol_actions` — every WS endpoint includes `heartbeat` + `cancel` (the `protocol_actions` spread from `actions/protocol.js`).
|
|
274
|
+
* `assert_ws_notifications_have_null_auth` — WS method `kind === 'remote_notification' ⟺ auth === null`; guards against drift between spec union and surface emitter.
|
|
244
275
|
|
|
245
276
|
Per-endpoint duplicate method names and the auth-shape biconditional are
|
|
246
277
|
already enforced at startup by `compile_action_registry` (see
|
|
@@ -249,36 +280,18 @@ contract-surface concerns a runtime registration check cannot reach.
|
|
|
249
280
|
|
|
250
281
|
Policy invariants (configurable, sensible defaults):
|
|
251
282
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
| `assert_mutation_routes_use_post` | Routes with input schemas must not be GET (bypasses browser GET idempotency assumptions). |
|
|
257
|
-
| `assert_keeper_routes_under_prefix` | Keeper routes must be under `keeper_route_prefixes` (default `['/api/']`). |
|
|
283
|
+
- `assert_sensitive_routes_rate_limited` — routes matching `sensitive_route_patterns` (default: `/login`, `/password`, `/bootstrap`, `/tokens/create`) declare rate limiting or a 429 schema.
|
|
284
|
+
- `assert_no_unexpected_public_mutations` — public mutation routes must be in `public_mutation_allowlist`.
|
|
285
|
+
- `assert_mutation_routes_use_post` — routes with input schemas must not be GET (bypasses browser GET idempotency assumptions).
|
|
286
|
+
- `assert_keeper_routes_under_prefix` — keeper routes must be under `keeper_route_prefixes` (default `['/api/']`).
|
|
258
287
|
|
|
259
288
|
Tightness audit:
|
|
260
289
|
|
|
261
|
-
- `audit_error_schema_tightness(surface) => Array<ErrorSchemaAuditEntry>` —
|
|
262
|
-
|
|
263
|
-
- `
|
|
264
|
-
|
|
265
|
-
- `
|
|
266
|
-
fuz_app-shipped route (account login/password/bootstrap/signup, db
|
|
267
|
-
health/tables/:name/tables/:name/rows/:id) has been tightened in place to
|
|
268
|
-
`z.enum([...])` / `z.literal(...)` against every emit-site code. Kept as a
|
|
269
|
-
forward-compatibility hook for future stock routes that need an interim
|
|
270
|
-
exemption; paths assume the standard `/api/account` + `/api/db` prefixes.
|
|
271
|
-
- `default_error_schema_tightness` — `{ignore_statuses: [401, 403, 429], allowlist: fuz_app_stock_route_tightness_allowlist}`.
|
|
272
|
-
Applied by `describe_standard_attack_surface_tests` when
|
|
273
|
-
`error_schema_tightness` is omitted; pass an override config or `null` to
|
|
274
|
-
opt out.
|
|
275
|
-
- **Merge semantics in `describe_standard_attack_surface_tests`**:
|
|
276
|
-
consumer-supplied `allowlist` and `ignore_statuses` are concatenated
|
|
277
|
-
underneath the defaults (stock entries first, consumer entries last),
|
|
278
|
-
so consumer allowlists are additive rather than replacing. Scalar fields
|
|
279
|
-
like `min_specificity` are overwritten by the consumer. Exported as
|
|
280
|
-
`resolve_standard_error_schema_tightness(consumer_options)` for consumers
|
|
281
|
-
calling `assert_error_schema_tightness` directly outside the suite.
|
|
290
|
+
- `audit_error_schema_tightness(surface) => Array<ErrorSchemaAuditEntry>` — classifies every route × status combination as `'literal' | 'enum' | 'generic'`.
|
|
291
|
+
- `assert_error_schema_tightness(surface, options?)` — fails routes below a threshold (`min_specificity`, default `'enum'`) with `allowlist` + `ignore_statuses` escape hatches.
|
|
292
|
+
- `fuz_app_stock_route_tightness_allowlist` — currently empty. Every fuz_app-shipped route (account login/password/bootstrap/signup, db health/tables/:name/tables/:name/rows/:id) has been tightened in place to `z.enum([...])` / `z.literal(...)` against every emit-site code. Kept as a forward-compatibility hook for future stock routes that need an interim exemption; paths assume the standard `/api/account` + `/api/db` prefixes.
|
|
293
|
+
- `default_error_schema_tightness` — `{ignore_statuses: [401, 403, 429], allowlist: fuz_app_stock_route_tightness_allowlist}`. Applied by `describe_standard_attack_surface_tests` when `error_schema_tightness` is omitted; pass an override config or `null` to opt out.
|
|
294
|
+
- **Merge semantics in `describe_standard_attack_surface_tests`**: consumer-supplied `allowlist` and `ignore_statuses` are concatenated underneath the defaults (stock entries first, consumer entries last), so consumer allowlists are additive rather than replacing. Scalar fields like `min_specificity` are overwritten by the consumer. Exported as `resolve_standard_error_schema_tightness(consumer_options)` for consumers calling `assert_error_schema_tightness` directly outside the suite.
|
|
282
295
|
|
|
283
296
|
Aggregate runners (called by the standard attack-surface suite):
|
|
284
297
|
|
|
@@ -290,80 +303,52 @@ Aggregate runners (called by the standard attack-surface suite):
|
|
|
290
303
|
|
|
291
304
|
`ErrorCoverageCollector` tracks which declared error paths get exercised.
|
|
292
305
|
Observations live in a `Set<string>` keyed by `"METHOD /spec-path:STATUS"` or
|
|
293
|
-
`"METHOD /spec-path:STATUS:CODE"` — the two shapes coexist and a
|
|
294
|
-
|
|
295
|
-
|
|
306
|
+
`"METHOD /spec-path:STATUS:CODE"` — the two shapes coexist and a status-only
|
|
307
|
+
observation satisfies the "any-code" coverage rule for all declared codes at
|
|
308
|
+
that status.
|
|
296
309
|
|
|
297
310
|
Methods:
|
|
298
311
|
|
|
299
|
-
- `record(specs, method, path, status, code?)` — resolves concrete paths
|
|
300
|
-
|
|
301
|
-
- `
|
|
302
|
-
`assert_response_matches_spec` and auto-extracts `body.error` from the
|
|
303
|
-
JSON body via `response.clone()`. Pass an explicit `code` when the
|
|
304
|
-
body was already consumed.
|
|
305
|
-
- `uncovered(specs, options?)` — per-status rows for generic schemas,
|
|
306
|
-
per-code rows for `z.literal` / `z.enum` schemas.
|
|
312
|
+
- `record(specs, method, path, status, code?)` — resolves concrete paths back to spec templates (e.g. `/api/accounts/abc` → `/api/accounts/:id`).
|
|
313
|
+
- `assert_and_record(specs, method, path, response, code?)` — wraps `assert_response_matches_spec` and auto-extracts `body.error` from the JSON body via `response.clone()`. Pass an explicit `code` when the body was already consumed.
|
|
314
|
+
- `uncovered(specs, options?)` — per-status rows for generic schemas, per-code rows for `z.literal` / `z.enum` schemas.
|
|
307
315
|
|
|
308
316
|
Support functions:
|
|
309
317
|
|
|
310
|
-
- `extract_declared_error_codes(schema)` — reads `schema.shape.error`;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
- `assert_error_coverage(collector, specs, options?)` — logs
|
|
314
|
-
`[error coverage] covered/total (N.M%)` with uncovered list; fails
|
|
315
|
-
when `min_coverage > 0` and the ratio falls below.
|
|
316
|
-
- `DEFAULT_INTEGRATION_ERROR_COVERAGE = 0.2` — conservative baseline
|
|
317
|
-
for the standard integration/admin suites; consumers tighten as
|
|
318
|
-
their own test coverage matures.
|
|
318
|
+
- `extract_declared_error_codes(schema)` — reads `schema.shape.error`; returns the literal value(s) for `z.literal` / `z.enum`, `null` otherwise.
|
|
319
|
+
- `assert_error_coverage(collector, specs, options?)` — logs `[error coverage] covered/total (N.M%)` with uncovered list; fails when `min_coverage > 0` and the ratio falls below.
|
|
320
|
+
- `DEFAULT_INTEGRATION_ERROR_COVERAGE = 0.2` — conservative baseline for the standard integration/admin suites; consumers tighten as their own test coverage matures.
|
|
319
321
|
|
|
320
322
|
### `schema_generators.ts` — valid-value generation
|
|
321
323
|
|
|
322
324
|
Walks Zod schemas to generate valid values for adversarial/round-trip tests.
|
|
323
325
|
|
|
324
|
-
- `detect_format(field_schema)` — reads `format` / `pattern` from the
|
|
325
|
-
|
|
326
|
-
- `
|
|
327
|
-
|
|
328
|
-
numbers → `1`, objects → recurse, enums → first entry, etc.).
|
|
329
|
-
For branded-string refinements, walks a fallback chain synthesized
|
|
330
|
-
from the `pattern` string the JSON Schema representation exposes:
|
|
331
|
-
fixed-length hex (`^[0-9a-f]{N}$` — blake3 / sha256 / md5 digests;
|
|
332
|
-
`0`.repeat(N)), prefix-lengthed slug (`^<prefix>_[A-Za-z0-9_-]{N}$`
|
|
333
|
-
— `ApiTokenId`-style ids; `<prefix>_` + `x`.repeat(N)), absolute
|
|
334
|
-
path prefix, URL prefix. First candidate that `safeParse` accepts
|
|
335
|
-
is used.
|
|
336
|
-
- `resolve_valid_path(path, params_schema?)` — swaps `:param` for
|
|
337
|
-
valid-format values (nil UUID for UUID params, `test_param` otherwise).
|
|
338
|
-
- `generate_valid_body(input_schema) => Record<string, unknown> | undefined` —
|
|
339
|
-
builds a body that satisfies the input schema. Throws with Zod
|
|
340
|
-
`issues` if the generated body fails validation — surfaces broken
|
|
341
|
-
generation logic with a descriptive error rather than a confusing 400
|
|
342
|
-
downstream.
|
|
326
|
+
- `detect_format(field_schema)` — reads `format` / `pattern` from the JSON Schema representation.
|
|
327
|
+
- `generate_valid_value(field, field_schema)` — base-type switch producing a valid sample (UUIDs → nil UUID, strings → `'xxxxxxxxxx'`, numbers → `1`, objects → recurse, enums → first entry, etc.). For branded-string refinements, walks a fallback chain synthesized from the `pattern` string the JSON Schema representation exposes: fixed-length hex (`^[0-9a-f]{N}$` — blake3 / sha256 / md5 digests; `0`.repeat(N)), prefix-lengthed slug (`^<prefix>_[A-Za-z0-9_-]{N}$` — `ApiTokenId`-style ids; `<prefix>_` + `x`.repeat(N)), absolute path prefix, URL prefix. First candidate that `safeParse` accepts is used.
|
|
328
|
+
- `resolve_valid_path(path, params_schema?)` — swaps `:param` for valid-format values (nil UUID for UUID params, `test_param` otherwise).
|
|
329
|
+
- `generate_valid_body(input_schema) => Record<string, unknown> | undefined` — builds a body satisfying the input schema. Throws with Zod `issues` if the generated body fails validation — surfaces broken generation logic with a descriptive error rather than a confusing 400 downstream.
|
|
343
330
|
|
|
344
331
|
### `integration_helpers.ts` — route lookup + body checks
|
|
345
332
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
| `assert_no_sensitive_fields_in_json(body, blocklist, context)` | Rejects any key in the blocklist at any depth. |
|
|
359
|
-
| `pick_auth_headers(spec, test_app, authed_account, admin_account)` | `RouteAuth` → appropriate test credentials; role `admin` uses `admin_account`, other roles use bootstrapped keeper, `keeper` uses daemon token. |
|
|
333
|
+
- `find_route_spec(specs, method, path)` — exact match then parameterized match (`:foo` matches any segment).
|
|
334
|
+
- `find_auth_route(specs, suffix, method)` — suffix-ending match for REST auth routes; decouples tests from consumer prefix. `suffix` is typed as `RestAuthRouteSuffix` and throws at runtime on unknown values (only login/logout/password/verify/signup/bootstrap remain on REST).
|
|
335
|
+
- `assert_response_matches_spec(specs, method, path, response)` — 2xx → validates against `spec.output`; non-2xx → validates against merged error schemas for that status. Non-JSON responses allowed only when no schema applies.
|
|
336
|
+
- `create_expired_test_cookie(keyring, session_options)` — validly signed cookie with `expires_at` in 1970.
|
|
337
|
+
- `check_error_response_fields(body)` — returns the list of fields outside `KNOWN_SAFE_ERROR_FIELDS` (`error`, `issues`, `required_roles`, `required_credential_types`, `retry_after`, `has_references`, `ok`).
|
|
338
|
+
- `assert_no_error_info_leakage(body, context)` — rejects field-name patterns (`stack`, `trace`, `sql`, …) + value patterns (`node_modules`, stack-like `at …`, `.ts:NN`).
|
|
339
|
+
- `assert_rate_limit_retry_after_header(response, body)` — `Retry-After` numeric header equals `Math.ceil(body.retry_after)`.
|
|
340
|
+
- `sensitive_field_blocklist` — `['password_hash', 'token_hash']`; never in any response body.
|
|
341
|
+
- `admin_only_field_blocklist` — `['updated_by', 'created_by']`; never in non-admin response bodies.
|
|
342
|
+
- `collect_json_keys_recursive(value)` — deep walk; returns `Set<string>` of every key at every nesting depth.
|
|
343
|
+
- `assert_no_sensitive_fields_in_json(body, blocklist, context)` — rejects any key in the blocklist at any depth.
|
|
344
|
+
- `pick_auth_headers(spec, test_app, authed_account, admin_account)` — `RouteAuth` → appropriate test credentials; role `admin` uses `admin_account`, other roles use bootstrapped keeper, `keeper` uses daemon token.
|
|
360
345
|
|
|
361
346
|
## Attack surface suites
|
|
362
347
|
|
|
363
348
|
### `attack_surface.ts` — `describe_standard_attack_surface_tests`
|
|
364
349
|
|
|
365
|
-
Single-call bundle of 5 top-level groups (10 named tests + every
|
|
366
|
-
|
|
350
|
+
Single-call bundle of 5 top-level groups (10 named tests + every adversarial
|
|
351
|
+
case per route):
|
|
367
352
|
|
|
368
353
|
1. **attack surface snapshot** — `matches committed snapshot`, `is deterministic`.
|
|
369
354
|
2. **attack surface structure** — `only expected public routes`, `full middleware stack on API routes`, `surface invariants`, `rpc/ws surface invariants`, `security policy`, `error schema tightness` (logs counts and asserts against `default_error_schema_tightness` by default; pass an override config or `null` via `error_schema_tightness`).
|
|
@@ -373,41 +358,35 @@ adversarial case per route):
|
|
|
373
358
|
|
|
374
359
|
Options: `{build: () => AppSurfaceSpec, snapshot_path, expected_public_routes, expected_api_middleware, roles, api_path_prefix?, security_policy?, error_schema_tightness?}`.
|
|
375
360
|
|
|
376
|
-
Also exported: `describe_adversarial_auth(options)` (
|
|
377
|
-
|
|
361
|
+
Also exported: `describe_adversarial_auth(options)` (group 3 on its own) and
|
|
362
|
+
`build_error_schema_lookup(specs, middleware_specs?)` (pre-built
|
|
378
363
|
`Map<string, RouteErrorSchemas>` for per-response validation).
|
|
379
364
|
|
|
380
365
|
### `adversarial_input.ts` — schema-walk payload generation
|
|
381
366
|
|
|
382
367
|
`describe_adversarial_input({build, roles})` — fires input body / params /
|
|
383
|
-
query validation failures at every route with correct-auth credentials
|
|
384
|
-
|
|
385
|
-
|
|
368
|
+
query validation failures at every route with correct-auth credentials so
|
|
369
|
+
validation middleware is actually exercised (not short-circuited by 401).
|
|
370
|
+
All cases expect 400 with one of `ERROR_INVALID_REQUEST_BODY` /
|
|
386
371
|
`_INVALID_JSON_BODY` / `_INVALID_ROUTE_PARAMS` / `_INVALID_QUERY_PARAMS`.
|
|
387
372
|
|
|
388
373
|
Exported generators:
|
|
389
374
|
|
|
390
|
-
- `generate_input_test_cases(input_schema)` — whole-body structural
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
violation per constrained field, numeric/array/string boundary cases
|
|
394
|
-
via JSON Schema introspection.
|
|
395
|
-
- `generate_params_test_cases(params_schema)` — format violations only
|
|
396
|
-
(unconstrained string params accept anything).
|
|
397
|
-
- `generate_query_test_cases(query_schema)` — missing required +
|
|
398
|
-
format violations.
|
|
375
|
+
- `generate_input_test_cases(input_schema)` — whole-body structural (non-object, extra key when `strictObject`), missing required fields, one wrong-type per field, null for required non-nullable, one format violation per constrained field, numeric/array/string boundary cases via JSON Schema introspection.
|
|
376
|
+
- `generate_params_test_cases(params_schema)` — format violations only (unconstrained string params accept anything).
|
|
377
|
+
- `generate_query_test_cases(query_schema)` — missing required + format violations.
|
|
399
378
|
|
|
400
|
-
GET-with-input routes hit the RPC `?params=` query convention; invalid-
|
|
401
|
-
|
|
402
|
-
|
|
379
|
+
GET-with-input routes hit the RPC `?params=` query convention; invalid-JSON
|
|
380
|
+
arrays there collapse to `ERROR_INVALID_REQUEST_BODY` (schema failure)
|
|
381
|
+
rather than `ERROR_INVALID_JSON_BODY`.
|
|
403
382
|
|
|
404
383
|
### `adversarial_404.ts` — 404 schema conformance
|
|
405
384
|
|
|
406
|
-
`describe_adversarial_404({build, roles})` — for every route with
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
385
|
+
`describe_adversarial_404({build, roles})` — for every route with `params` +
|
|
386
|
+
404 in `error_schemas` + an extractable error code (`z.literal` or first
|
|
387
|
+
`z.enum`), replaces the handler with a stub returning `{error: <code>}`,
|
|
388
|
+
fires with nil-UUID params, asserts 404 + body matches the declared 404 Zod
|
|
389
|
+
schema. No DB needed.
|
|
411
390
|
|
|
412
391
|
### `adversarial_headers.ts` — header injection suite
|
|
413
392
|
|
|
@@ -430,56 +409,51 @@ validation. Extra cases append to the standard list.
|
|
|
430
409
|
|
|
431
410
|
Module-level `vi.mock()` for the four query modules bearer auth touches:
|
|
432
411
|
`api_token_queries`, `account_queries`, `role_grant_queries`. Because
|
|
433
|
-
`vi.mock()` is hoisted, these run before any imports resolve — so any
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
| `describe_bearer_auth_cases(suite_name, cases, ip_rate_limiter?)` | Table-driven runner — one `test()` per case; asserts status, error, body fields, `api_token_id`, context preservation. |
|
|
445
|
-
| `TEST_MIDDLEWARE_PATH = '/api/test'` | Path used by the echo route in the stack factory. |
|
|
446
|
-
| `create_test_middleware_stack_app(options?)` | Real proxy + origin + bearer middleware for integration-shape testing. Echo route returns `{ok, client_ip, has_context}`. |
|
|
412
|
+
`vi.mock()` is hoisted, these run before any imports resolve — so any test
|
|
413
|
+
file that imports from `middleware.ts` gets these mocks globally. Pair with
|
|
414
|
+
`vi.restoreAllMocks()` in `afterEach` when mixing into `.db.test.ts` files.
|
|
415
|
+
|
|
416
|
+
- `BearerAuthTestOptions`, `BearerAuthTestCase` — test-case table shape for the bearer auth runner.
|
|
417
|
+
- `create_bearer_auth_mocks(tc)` — configures the module-level mocks per test case; returns spy references.
|
|
418
|
+
- `TEST_CLIENT_IP = '127.0.0.1'` — IP set by the proxy stub in `create_bearer_auth_test_app`.
|
|
419
|
+
- `create_bearer_auth_test_app(tc, ip_rate_limiter?)` — Hono app with bearer middleware + echo route at `/api/test` returning `{ok, account_id, credential_type, api_token_id, request_context_set}` — the account-grain identity bearer auth writes, plus a flag for tests that pre-populate `REQUEST_CONTEXT_KEY` via `pre_context`.
|
|
420
|
+
- `describe_bearer_auth_cases(suite_name, cases, ip_rate_limiter?)` — table-driven runner; one `test()` per case; asserts status, error, body fields, `api_token_id`, context preservation.
|
|
421
|
+
- `TEST_MIDDLEWARE_PATH = '/api/test'` — path used by the echo route in the stack factory.
|
|
422
|
+
- `create_test_middleware_stack_app(options?)` — real proxy + origin + bearer middleware for integration-shape testing. Echo route returns `{ok, client_ip, has_context}`.
|
|
447
423
|
|
|
448
424
|
The echo route under `create_bearer_auth_test_app` deliberately surfaces
|
|
449
425
|
every middleware-written context variable (`ACCOUNT_ID_KEY`,
|
|
450
|
-
`CREDENTIAL_TYPE_KEY`, `AUTH_API_TOKEN_ID_KEY`) — bearer middleware
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
426
|
+
`CREDENTIAL_TYPE_KEY`, `AUTH_API_TOKEN_ID_KEY`) — bearer middleware writes
|
|
427
|
+
account-grain identity only; the dispatcher's authorization phase owns
|
|
428
|
+
`REQUEST_CONTEXT_KEY`. The `request_context_set` flag covers the test-only
|
|
429
|
+
`pre_context` injection path. When public auth surface gains a new context
|
|
430
|
+
variable, header, or field, update this echo alongside the assertions in
|
|
431
|
+
`src/test/auth/*.test.ts` — the two move together.
|
|
456
432
|
|
|
457
433
|
## Round-trip suites
|
|
458
434
|
|
|
459
435
|
### `round_trip.ts` — `describe_round_trip_validation`
|
|
460
436
|
|
|
461
|
-
For every route spec, fires a valid request with matching auth and
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
in the vitest output.
|
|
437
|
+
For every route spec, fires a valid request with matching auth and validates
|
|
438
|
+
the response against declared schemas. DB-backed via `create_test_app`.
|
|
439
|
+
Per-route test (`test.each`) — one line per route in the vitest output.
|
|
465
440
|
|
|
466
|
-
Options: `{
|
|
441
|
+
Options: `{setup_test, surface_source, capabilities, skip_routes?, input_overrides?}`.
|
|
467
442
|
`input_overrides` is a `Map<"METHOD /path", body>` — override generated
|
|
468
|
-
bodies for routes whose input schema can't round-trip cleanly (e.g.
|
|
469
|
-
|
|
443
|
+
bodies for routes whose input schema can't round-trip cleanly (e.g. fields
|
|
444
|
+
that must reference DB state).
|
|
470
445
|
|
|
471
446
|
SSE routes are skipped by Content-Type sniff; `describe_sse_route_tests`
|
|
472
447
|
picks them up separately.
|
|
473
448
|
|
|
474
449
|
### `rpc_round_trip.ts` — `describe_rpc_round_trip_tests`
|
|
475
450
|
|
|
476
|
-
DB-backed round-trip for RPC: one POST test for all methods, one GET
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
The admin RPC auth test picks a session-based identity (`authed` /
|
|
481
|
-
|
|
482
|
-
daemon token.
|
|
451
|
+
DB-backed round-trip for RPC: one POST test for all methods, one GET test
|
|
452
|
+
for `side_effects: false` methods. Successful responses validate against
|
|
453
|
+
`action.spec.output`; error responses validate as well-formed JSON-RPC error
|
|
454
|
+
envelopes. Options: `{setup_test, surface_source, capabilities, session_options, rpc_endpoints, skip_methods?, input_overrides?}`.
|
|
455
|
+
The admin RPC auth test picks a session-based identity (`authed` / `admin` /
|
|
456
|
+
bootstrapped keeper) based on `method.auth`; keeper uses the daemon token.
|
|
483
457
|
|
|
484
458
|
### `sse_round_trip.ts` — `describe_sse_route_tests`
|
|
485
459
|
|
|
@@ -490,46 +464,45 @@ validate the next `data:` frame as `{method, params}` against declared
|
|
|
490
464
|
and assert the stream closes within 2s.
|
|
491
465
|
|
|
492
466
|
`SseRouteTestSpec` per route: `{path, trigger, event_specs?, assert_closes_on_revoke?}`.
|
|
493
|
-
Pass `on_audit_event` on the suite options to wire a close-on-revoke
|
|
494
|
-
|
|
495
|
-
|
|
467
|
+
Pass `on_audit_event` on the suite options to wire a close-on-revoke guard
|
|
468
|
+
(e.g. via `create_sse_auth_guard`) for consumer SSE registries — without it,
|
|
469
|
+
the revoke assertion hangs because the guard never fires.
|
|
496
470
|
|
|
497
|
-
Frame
|
|
498
|
-
`\n\n` framing, a 2s per-read timeout
|
|
499
|
-
`wait_for_close` for the revocation check.
|
|
471
|
+
Frame reading is delegated to the shared `create_sse_frame_reader`
|
|
472
|
+
(`transports/sse_frame_reader.ts`) — `\n\n` framing, a 2s per-read timeout
|
|
473
|
+
(prevents vitest hangs), and `wait_for_close` for the revocation check. The
|
|
474
|
+
cross-process `transports/sse_transport.ts` reuses the same reader over a
|
|
475
|
+
streaming `fetch` body.
|
|
500
476
|
|
|
501
477
|
### `ws_round_trip.ts` — WebSocket harness (non-HTTP)
|
|
502
478
|
|
|
503
479
|
In-process test driver for `register_action_ws`. Consumers pass specs +
|
|
504
|
-
handlers, receive `{transport, connect()}` back. The full dispatch path
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
`@hono/node-ws` adapter).
|
|
480
|
+
handlers, receive `{transport, connect()}` back. The full dispatch path is
|
|
481
|
+
exercised (per-action auth, input validation, `ctx.notify`, broadcast via
|
|
482
|
+
`BackendWebsocketTransport`, close-on-revoke), but Hono's wire upgrade is
|
|
483
|
+
skipped (the Node test runtime has no `@hono/node-ws` adapter).
|
|
509
484
|
|
|
510
485
|
Three layers:
|
|
511
486
|
|
|
512
|
-
1. **Primitives** — `create_fake_ws()`, `create_fake_hono_context(opts)`,
|
|
513
|
-
`create_stub_upgrade()`, `MinimalActionEnvironment`,
|
|
514
|
-
`dispatch_ws_message(on_message, event, ws)`.
|
|
487
|
+
1. **Primitives** — `create_fake_ws()`, `create_fake_hono_context(opts)`, `create_stub_upgrade()`, `MinimalActionEnvironment`, `dispatch_ws_message(on_message, event, ws)`.
|
|
515
488
|
2. **Harness** — `create_ws_test_harness({actions, transport?, heartbeat?, log?, on_socket_open?, on_socket_close?})` → `WsTestHarness`. `connect(identity?)` is async and resolves after `on_socket_open` completes, so broadcasts sent immediately after `await harness.connect()` reach the client. The harness threads its own `create_stub_db()` into the dispatcher's `db` slot so handlers declaring `side_effects: true` execute under the same transaction wrap they would in production (the stub's `transaction(fn)` synchronously calls `fn(stub_db)`); domain deps reach handlers via factory closures, the same way HTTP RPC factories already wire them. Audit fan-out runs through whatever `audit` emitter the consumer supplied to its action factory closure (typically `create_test_audit_emitter()` for unit harnesses).
|
|
516
|
-
3. **Round-trip helpers** — `is_notification(method)`,
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
`request` throws with
|
|
528
|
-
asserting `result.foo` on a
|
|
529
|
-
not a `Cannot read property 'foo'
|
|
530
|
-
timeout_ms?)` checks already-received
|
|
531
|
-
new arrivals (default 1000ms); drops the
|
|
532
|
-
`waiters` array doesn't grow.
|
|
489
|
+
3. **Round-trip helpers** — predicates + wire-frame types live in `transports/ws_client.ts` (shared with the cross-process `ws_transport.ts` impl): `is_notification(method)`, `is_notification_with<P>(method, match)` (type-guard combinator — narrows `wait_for` return type), `is_response_for(id)`, `JsonrpcNotificationFrame<P>` / `JsonrpcSuccessResponseFrame<R>` / `JsonrpcErrorResponseFrame<D>` (typed wire-frame shapes distinct from the runtime Zod schemas in `http/jsonrpc.ts` — generic over `params` / `result` / `data` so tests narrow without casts). `build_broadcast_api<TApi>({harness, specs})` (in `ws_round_trip.ts`) wires a typed broadcast API against the harness transport.
|
|
490
|
+
|
|
491
|
+
`WsClient` (in `transports/ws_client.ts`):
|
|
492
|
+
`{send, request<R>, close, messages, wait_for, wait_for_close}`. The
|
|
493
|
+
harness's `connect()` returns this shape; the cross-process
|
|
494
|
+
`create_ws_transport` in `transports/ws_transport.ts` implements the same
|
|
495
|
+
interface so assertion helpers and suite bodies work against either impl.
|
|
496
|
+
`wait_for_close(timeout_ms?)` resolves `true` if the server closes the
|
|
497
|
+
socket within the timeout, `false` on timeout (and `true` immediately when
|
|
498
|
+
already closed) — the signal for server-initiated close (e.g. an auth-guard
|
|
499
|
+
revocation), distinct from client-initiated `close()`. Mirrors the SSE frame
|
|
500
|
+
reader's `wait_for_close`. `request` throws with
|
|
501
|
+
code + message + data on error frames (so asserting `result.foo` on a
|
|
502
|
+
failed request surfaces the real cause, not a `Cannot read property 'foo'
|
|
503
|
+
of undefined`). `wait_for(predicate, timeout_ms?)` checks already-received
|
|
504
|
+
messages first, then waits for new arrivals (default 1000ms); drops the
|
|
505
|
+
waiter on timeout so the `waiters` array doesn't grow.
|
|
533
506
|
|
|
534
507
|
`keeper_identity()` — convenience for `{credential_type: 'daemon_token', roles: [ROLE_KEEPER]}`.
|
|
535
508
|
|
|
@@ -553,21 +526,27 @@ Support functions: `collect_json_schema_property_names(schema)` (walks
|
|
|
553
526
|
`assert_output_schemas_no_sensitive_fields(surface, fields?)`,
|
|
554
527
|
`assert_non_admin_schemas_no_admin_fields(surface, fields?)`.
|
|
555
528
|
|
|
556
|
-
Options: `{
|
|
529
|
+
Options: `{setup_test, surface_source, capabilities, sensitive_fields?, admin_only_fields?, skip_routes?}`.
|
|
557
530
|
|
|
558
531
|
### `rate_limiting.ts` — `describe_rate_limiting_tests`
|
|
559
532
|
|
|
560
533
|
Three test groups:
|
|
561
534
|
|
|
562
|
-
1. IP rate limiting on login — fires `max_attempts + 1` requests; last
|
|
535
|
+
1. IP rate limiting on login — fires `max_attempts + 1` requests; last should be 429 with `RateLimitError` body + valid `Retry-After` header.
|
|
563
536
|
2. Per-account rate limiting on login — same username exhausts the bucket; a different username is not blocked.
|
|
564
537
|
3. Bearer auth IP rate limiting — invalid bearer tokens exhaust the IP bucket via the `account_verify` RPC method.
|
|
565
538
|
|
|
566
|
-
Each group asserts its required route exists with a descriptive
|
|
567
|
-
|
|
568
|
-
|
|
539
|
+
Each group asserts its required route exists with a descriptive message.
|
|
540
|
+
Creates a tight rate limiter (default `max_attempts: 2`, `window_ms: 60_000`)
|
|
541
|
+
per test and disposes it in `finally`.
|
|
569
542
|
|
|
570
|
-
Options: `{session_options, create_route_specs, app_options?, db_factories?, max_attempts?}`.
|
|
543
|
+
Options: `{session_options, create_route_specs, rpc_endpoints, app_options?, db_factories?, max_attempts?}`.
|
|
544
|
+
Reads inputs directly from the options bag instead of going through the
|
|
545
|
+
`setup_test` fixture protocol — the per-test rate-limiter overrides need a
|
|
546
|
+
fresh `TestApp` per test that the single-fixture model can't carry.
|
|
547
|
+
Consumers still pass `default_in_process_suite_options(...)` for shape
|
|
548
|
+
uniformity; the extra `{setup_test, surface_source, capabilities}` fields on
|
|
549
|
+
the spread are ignored by the suite.
|
|
571
550
|
|
|
572
551
|
## Integration suites
|
|
573
552
|
|
|
@@ -587,24 +566,39 @@ these thematic areas:
|
|
|
587
566
|
8. Bearer auth + browser-context discard on mutations
|
|
588
567
|
9. Token revocation + cross-account isolation
|
|
589
568
|
10. Response body schema validation + error-response information leakage
|
|
590
|
-
11. Signup invite edge cases +
|
|
569
|
+
11. Signup invite edge cases + expired credential rejection + error-coverage breadth
|
|
591
570
|
|
|
592
571
|
An `ErrorCoverageCollector` runs across groups; `afterAll` filters to
|
|
593
|
-
auth-related routes (login/logout/verify/sessions/tokens/password/
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
572
|
+
auth-related routes (login/logout/verify/sessions/tokens/password/signup)
|
|
573
|
+
and asserts `DEFAULT_INTEGRATION_ERROR_COVERAGE` (20%). Bootstrap is
|
|
574
|
+
excluded because no describe block in this suite drives it — its declared
|
|
575
|
+
codes would always be uncovered. Consumer-specific routes aren't exercised
|
|
576
|
+
here either — they don't count against the baseline. Override with
|
|
577
|
+
`error_coverage_min?: number` (set to `0` to skip the assertion — useful for
|
|
578
|
+
minimal route sets whose declared error codes outpace the suite's
|
|
579
|
+
denial-path drivers).
|
|
597
580
|
|
|
598
|
-
Options: `{session_options,
|
|
581
|
+
Options: `{setup_test, surface_source, capabilities, session_options, rpc_endpoints, error_coverage_min?}`.
|
|
599
582
|
|
|
600
583
|
### `admin_integration.ts` — `describe_standard_admin_integration_tests`
|
|
601
584
|
|
|
602
585
|
7 test groups covering admin surface: account listing, role_grant grant
|
|
603
|
-
lifecycle (via `role_grant_offer_create` + `
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
586
|
+
lifecycle (via `role_grant_offer_create` + `role_grant_offer_accept` +
|
|
587
|
+
`role_grant_revoke` RPC flows — **not** REST, **not** direct
|
|
588
|
+
`query_accept_offer`; see `auth/CLAUDE.md` for
|
|
589
|
+
`role_grant_offer_action_specs.ts` + `role_grant_offer_actions.ts`),
|
|
590
|
+
session / token management, audit log reads (RPC), admin-to-admin
|
|
591
|
+
isolation, error coverage, response schema validation.
|
|
592
|
+
|
|
593
|
+
The shared `role_grant_offer_and_accept` helper (`role_grant_helpers.ts`)
|
|
594
|
+
composes both RPCs end-to-end and takes
|
|
595
|
+
`{grantor: TestApp | TestAccount, recipient: TestAccount}` — closing the
|
|
596
|
+
headers/account loop on a single object per party rules out caller-side
|
|
597
|
+
header/account mismatch. Direct-grant fixtures (test focuses on revoke or
|
|
598
|
+
isolation, not the consent path) go through `create_test_role_grant_direct`
|
|
599
|
+
from `db_entities.ts`.
|
|
600
|
+
|
|
601
|
+
Required options: `{setup_test, surface_source, capabilities, session_options, rpc_endpoints: RpcEndpointsSuiteOption, roles: RoleSchemaResult, admin_prefix?}`.
|
|
608
602
|
|
|
609
603
|
`rpc_endpoints` is `Array<RpcEndpointSpec> | ((ctx: AppServerContext) => Array<RpcEndpointSpec>)` —
|
|
610
604
|
the same `RpcEndpointsSuiteOption` union every DB-backed suite accepts
|
|
@@ -615,86 +609,125 @@ raw to the top-level `rpc_endpoints` slot on `CreateTestAppOptions` so
|
|
|
615
609
|
action handlers can close over
|
|
616
610
|
`ctx.deps` / `ctx.app_settings` (e.g. `create_standard_rpc_actions(ctx.deps,
|
|
617
611
|
{app_settings: ctx.app_settings})`). Factory must return the same endpoint
|
|
618
|
-
`path` regardless of ctx — `resolve_rpc_endpoints_for_setup` invokes it
|
|
619
|
-
|
|
620
|
-
|
|
612
|
+
`path` regardless of ctx — `resolve_rpc_endpoints_for_setup` invokes it once
|
|
613
|
+
with a stub ctx for path lookup and `create_app_server` invokes it again
|
|
614
|
+
per-test for live dispatch.
|
|
621
615
|
|
|
622
616
|
**Hard-fails via `require_rpc_endpoint_path`** at setup time when
|
|
623
617
|
`rpc_endpoints` is empty — admin role_grant grant/revoke plus session/token
|
|
624
618
|
revoke-all plus audit-log list/history are RPC-only. A confusing test
|
|
625
619
|
failure mid-suite is worse than a clear setup error.
|
|
626
620
|
|
|
627
|
-
The suite also exercises `account_token_create` (and
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
collector to the RPC round-trip suite entirely and delete this skip
|
|
643
|
-
branch.
|
|
621
|
+
The suite also exercises `account_token_create` (and `account_token_revoke`)
|
|
622
|
+
for the cross-admin isolation + audit-trail scenarios. Wire the account
|
|
623
|
+
actions alongside admin / role-grant-offer — easiest is
|
|
624
|
+
`create_standard_rpc_actions`, which bundles all three. Consumers that only
|
|
625
|
+
wire admin will hit `method not found: account_token_create` on first run.
|
|
626
|
+
|
|
627
|
+
Error-coverage scope is narrowed to the REST suffixes still on the admin
|
|
628
|
+
surface (`/audit/stream`); the RPC surface is covered by
|
|
629
|
+
`describe_rpc_round_trip_tests`. The scoped REST surface is 0–1 routes —
|
|
630
|
+
when the scoped count is ≤1, the `afterAll` hook logs
|
|
631
|
+
`[error coverage] skipped admin REST coverage assertion — …` and does not
|
|
632
|
+
fail. The 20% `DEFAULT_INTEGRATION_ERROR_COVERAGE` baseline is a REST-era
|
|
633
|
+
threshold; the RPC surface has its own coverage via
|
|
634
|
+
`describe_rpc_round_trip_tests`. TODO: move this error-coverage collector
|
|
635
|
+
to the RPC round-trip suite entirely and delete this skip branch.
|
|
644
636
|
|
|
645
637
|
### `audit_completeness.ts` — `describe_audit_completeness_tests`
|
|
646
638
|
|
|
647
|
-
Verifies every auth mutation produces the expected `audit_log` row
|
|
648
|
-
|
|
639
|
+
Verifies every auth mutation produces the expected `audit_log` row.
|
|
640
|
+
Mutations fire over the real middleware stack; reads go back through the
|
|
641
|
+
`audit_log_list` RPC (the same path the admin UI consumes) — intentional
|
|
642
|
+
end-to-end coverage of emit → persist → query → wire response. For
|
|
643
|
+
unit-level "did the handler emit?" assertions without the persistence path,
|
|
644
|
+
use `create_recording_audit_emitter` from `audit_drift_guard.ts`.
|
|
645
|
+
|
|
649
646
|
Same `rpc_endpoints` hard-fail as the admin suite — the mutation-audit
|
|
650
647
|
tests drive role_grant flow, session/token revoke-all, and invite
|
|
651
648
|
create/delete through `role_grant_offer_create_action_spec` /
|
|
652
|
-
`
|
|
649
|
+
`role_grant_offer_accept_action_spec` / `role_grant_revoke_action_spec` /
|
|
650
|
+
`admin_session_revoke_all_action_spec` /
|
|
653
651
|
`admin_token_revoke_all_action_spec` / `app_settings_update_action_spec` /
|
|
654
652
|
`invite_create_action_spec` / `invite_delete_action_spec`.
|
|
655
653
|
|
|
654
|
+
**Observer-account pattern.** Each audit-touching test mints a dedicated
|
|
655
|
+
admin account (`create_admin_observer`) whose sole job is reading the audit
|
|
656
|
+
log via RPC. Decoupling the observer from the subject keeps the helper
|
|
657
|
+
shape uniform across every test — even mutations that revoke the
|
|
658
|
+
bootstrapped admin's credentials (logout, session_revoke, password_change).
|
|
659
|
+
|
|
656
660
|
Bootstrap audit logging is excluded because `create_test_app` doesn't
|
|
657
661
|
provide the filesystem token state; covered separately in
|
|
658
662
|
`bootstrap_account.db.test.ts`.
|
|
659
663
|
|
|
660
664
|
### `standard.ts` — `describe_standard_tests`
|
|
661
665
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
`
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
666
|
+
Bundles every DB-backed suite carrying the standard option shape, each
|
|
667
|
+
gated on its relevant config — silent-skip when the gate isn't met:
|
|
668
|
+
|
|
669
|
+
- `integration` — always
|
|
670
|
+
- `admin` — `roles` provided
|
|
671
|
+
- `audit_completeness` — `roles` provided (proxy for consumer admin wiring; `rpc_endpoints` is bundle-required)
|
|
672
|
+
- `bootstrap_success` — `bootstrap.mode === 'live'`
|
|
673
|
+
- `round_trip` — always
|
|
674
|
+
- `rpc_round_trip` — `rpc_endpoints` provided
|
|
675
|
+
- `data_exposure` — always
|
|
676
|
+
- `rate_limiting` — always (owns its own per-test setup, bypasses the fixture protocol — needs `create_route_specs` directly)
|
|
677
|
+
|
|
678
|
+
Realization that lifted the bundle from 2 suites to 8: fold-in cost between
|
|
679
|
+
suites is zero because each `describe_*` block owns its own setup via the
|
|
680
|
+
`{setup_test, surface_source, capabilities}` protocol, so suites whose
|
|
681
|
+
tests need opposite-shaped default DB state (e.g. the bootstrap-success
|
|
682
|
+
suite needs an empty DB while the integration suite needs the
|
|
683
|
+
pre-bootstrapped keeper) coexist in one bundle without cost. Each test
|
|
684
|
+
invokes the right per-test fixture. Consumers wiring the standard surface
|
|
685
|
+
call once instead of seven times; forgetting a suite no longer silently
|
|
686
|
+
loses coverage.
|
|
687
|
+
|
|
688
|
+
`StandardTestOptions` requires `create_route_specs` (for rate_limiting) and
|
|
689
|
+
`rpc_endpoints` (for admin/audit_completeness/rpc_round_trip); the admin
|
|
690
|
+
suite's requirement is enforced at the type level so a missing
|
|
691
|
+
`rpc_endpoints` is a compile error rather than a runtime throw. Optional
|
|
692
|
+
`bootstrap` (top-level, same precedent as `rpc_endpoints`) feeds both the
|
|
693
|
+
disabled/surface_only/live wire-shape gating and the bootstrap-success
|
|
694
|
+
suite gate.
|
|
695
|
+
|
|
696
|
+
Attack surface suites stay separate — their option shape is
|
|
697
|
+
`{build, snapshot_path, expected_public_routes, ...}` rather than the
|
|
698
|
+
shared `{setup_test, surface_source, capabilities}`. A peer
|
|
699
|
+
`describe_standard_surface_tests` bundler lives for that side if/when
|
|
700
|
+
needed.
|
|
701
|
+
|
|
702
|
+
Cross-process counterpart: `cross_backend/standard.ts` —
|
|
703
|
+
`describe_standard_cross_process_tests`. Different bundle because three of
|
|
704
|
+
the eight in-process suites don't survive a process boundary
|
|
705
|
+
(`rate_limiting` needs a fresh per-test `TestApp`, `audit_completeness`
|
|
706
|
+
reads FK structure, `bootstrap_success` is one-shot per backend lifecycle);
|
|
707
|
+
the cross-process bundle documents the omissions once upstream so
|
|
708
|
+
per-consumer files don't repeat the bookkeeping. See the Cross-backend
|
|
709
|
+
integration layer §`cross_backend/standard.ts` below.
|
|
669
710
|
|
|
670
711
|
## RPC helpers
|
|
671
712
|
|
|
672
713
|
### `rpc_helpers.ts` — envelope construction + response assertions
|
|
673
714
|
|
|
674
|
-
Shared by `rpc_attack_surface.ts`, `rpc_round_trip.ts`, the admin and
|
|
675
|
-
|
|
676
|
-
directly.
|
|
715
|
+
Shared by `rpc_attack_surface.ts`, `rpc_round_trip.ts`, the admin and audit
|
|
716
|
+
integration suites, and consumer tests that hit RPC methods directly.
|
|
677
717
|
|
|
678
718
|
Request builders:
|
|
679
719
|
|
|
680
|
-
- `create_rpc_post_init(method, params?, id?)` — `RequestInit` with
|
|
681
|
-
|
|
682
|
-
envelope has no `params` field (JSON-RPC doesn't accept
|
|
683
|
-
`"params": null`).
|
|
684
|
-
- `create_rpc_get_url(endpoint_path, method, params?, id?)` — GET URL
|
|
685
|
-
with `?method=&id=¶ms=<JSON>`.
|
|
720
|
+
- `create_rpc_post_init(method, params?, id?)` — `RequestInit` with JSON-RPC envelope body. `params === undefined || params === null` → envelope has no `params` field (JSON-RPC doesn't accept `"params": null`).
|
|
721
|
+
- `create_rpc_get_url(endpoint_path, method, params?, id?)` — GET URL with `?method=&id=¶ms=<JSON>`.
|
|
686
722
|
|
|
687
723
|
Response assertions:
|
|
688
724
|
|
|
689
|
-
- `assert_jsonrpc_error_response(body, expected_code?)` — validates
|
|
690
|
-
|
|
691
|
-
- `assert_jsonrpc_success_response(body, output_schema?)` — validates
|
|
692
|
-
`JsonrpcResponse`; optional `output_schema.safeParse(result)`.
|
|
725
|
+
- `assert_jsonrpc_error_response(body, expected_code?)` — validates `JsonrpcErrorResponse`; optional code check.
|
|
726
|
+
- `assert_jsonrpc_success_response(body, output_schema?)` — validates `JsonrpcResponse`; optional `output_schema.safeParse(result)`.
|
|
693
727
|
|
|
694
728
|
One-shot transport:
|
|
695
729
|
|
|
696
|
-
- `RpcTestTransport = (url, init) => Promise<Response>` — duck type
|
|
697
|
-
`Hono.request` already satisfies.
|
|
730
|
+
- `RpcTestTransport = (url, init) => Promise<Response>` — duck type `Hono.request` already satisfies.
|
|
698
731
|
- `http_transport(app)` — adapter for anything with a `request()` method.
|
|
699
732
|
- `RpcCallResult` — discriminated `{ok: true, status, result}` / `{ok: false, status, error: {code, message, data?}}`.
|
|
700
733
|
- `RpcCallArgs` — `{app, path, method, params?, headers?, id?, verb?}`. `verb` defaults to `'POST'`; use `'GET'` for `side_effects: false` methods.
|
|
@@ -718,21 +751,21 @@ Registry lookups:
|
|
|
718
751
|
- unauthenticated → `unauthenticated` (code -32001)
|
|
719
752
|
- wrong role → `forbidden` (-32002)
|
|
720
753
|
- authenticated without role → `forbidden`
|
|
721
|
-
- **keeper rejects non-daemon credentials** — session and api_token credentials are rejected even when the account has the keeper role (only `daemon_token` passes). The credential-type gate fires before the role gate (see `auth/CLAUDE.md`
|
|
754
|
+
- **keeper rejects non-daemon credentials** — session and api_token credentials are rejected even when the account has the keeper role (only `daemon_token` passes). The credential-type gate fires before the role gate (see `auth/CLAUDE.md` §Keeper auth shape).
|
|
722
755
|
- correct auth passes (not 401/403)
|
|
723
756
|
- GET unauthenticated for `side_effects: false` reads
|
|
724
757
|
2. **RPC adversarial envelopes** — fixed set exercising dispatcher steps 1–2: non-JSON body, wrong `jsonrpc` version, missing `jsonrpc` / `method` / `id`, batch array, unknown method, GET missing `method`/`id`, GET invalid JSON params, GET non-object params, GET mutation method → `invalid_request`.
|
|
725
758
|
3. **RPC adversarial params** — reuses `generate_input_test_cases` but filters out structural cases (those hit envelope validation at step 1, not params validation at step 5). Every case expects 400 `invalid_params`.
|
|
726
759
|
|
|
727
|
-
Skips silently when `surface.rpc_endpoints` is empty. Uses stub
|
|
728
|
-
|
|
760
|
+
Skips silently when `surface.rpc_endpoints` is empty. Uses stub deps — no
|
|
761
|
+
DB needed.
|
|
729
762
|
|
|
730
763
|
Options: `{build: () => AppSurfaceSpec, roles: Array<string>}`.
|
|
731
764
|
|
|
732
|
-
**Opt-in bundles need their own per-bundle suite file.** Action bundles
|
|
733
|
-
|
|
734
|
-
`actor_lookup_actions`, and `actor_search_actions`) get zero adversarial
|
|
735
|
-
|
|
765
|
+
**Opt-in bundles need their own per-bundle suite file.** Action bundles not
|
|
766
|
+
folded into `create_standard_rpc_actions` (today `self_service_role_actions`,
|
|
767
|
+
`actor_lookup_actions`, and `actor_search_actions`) get zero adversarial /
|
|
768
|
+
round-trip coverage from `describe_rpc_attack_surface_tests` +
|
|
736
769
|
`describe_rpc_round_trip_tests` unless the consumer ships a
|
|
737
770
|
`<module>.rpc_suites.db.test.ts` mounting the opt-in factory on the RPC
|
|
738
771
|
endpoint and calling both suites. See ../../test/CLAUDE.md §Composable
|
|
@@ -743,24 +776,313 @@ Test Suites for the obligation note; existing
|
|
|
743
776
|
|
|
744
777
|
Shared conventions (`.db.test.ts` suffix, `isolate: false` semantics,
|
|
745
778
|
`assert` from vitest, `assert_rejects`, `vi.mock` avoidance under
|
|
746
|
-
`isolate: false`) live in Skill(fuz-stack) testing-patterns.
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
- **`await_pending_effects: true`** is set by `create_test_app`.
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
779
|
+
`isolate: false`) live in Skill(fuz-stack) testing-patterns. fuz_app-specific
|
|
780
|
+
points:
|
|
781
|
+
|
|
782
|
+
- **`await_pending_effects: true`** is set by `create_test_app`. Fire-and-forget effects (audit logs, session touches, WS fan-out via `emit_after_commit`) resolve before the response returns, so tests can assert on side effects inline without manual flushing.
|
|
783
|
+
- **Deep-path imports only.** Import from the canonical module (`testing/db.js`, `testing/rpc_helpers.js`, etc.); fuz_app's `dist/` ships no barrel.
|
|
784
|
+
- **DI via small `*Deps` interfaces.** Stub factories accept the same narrow `*Deps` contracts production code uses — never `Pick<GodType, ...>`. New helpers needing env/fs/logger take `EnvDeps` / `FsReadDeps` / `Logger` from `runtime/deps.ts` or `@fuzdev/fuz_util/log.js`.
|
|
785
|
+
- **Keep the shared echo routes in sync with public surface.** When middleware or public API gains a new context variable, header, or field, update the echo in `middleware.ts` (`create_bearer_auth_test_app`, `create_test_middleware_stack_app`) alongside the assertions in `src/test/auth/*.test.ts`. Drift surfaces as a missed assertion, not a test failure.
|
|
786
|
+
|
|
787
|
+
## Cross-backend integration layer
|
|
788
|
+
|
|
789
|
+
The standard test suites take a unified
|
|
790
|
+
`{setup_test, surface_source, capabilities}` shape so the same suite bodies
|
|
791
|
+
run against an in-process Hono harness today and against a spawned backend
|
|
792
|
+
over real HTTP — either the Rust spine (`zzz_server`, `fuz_forge_server`, or
|
|
793
|
+
the non-domain `testing_spine_stub`) or a **TS** spine binary built on the
|
|
794
|
+
test-server core below (fuz_app's own domain-free `testing_spine_server`, run
|
|
795
|
+
on Node + Deno + Bun). In-process is the fast feedback path; cross-process is the
|
|
796
|
+
source of truth for wire-shape conformance.
|
|
797
|
+
|
|
798
|
+
### Fixture protocol + capabilities
|
|
799
|
+
|
|
800
|
+
- `testing/cross_backend/setup.ts` — `SetupTest` / `TestFixture` /
|
|
801
|
+
`TestAccountFixture` / `CreateTestAccountOptions` types,
|
|
802
|
+
`default_in_process_setup(options)` (wraps `create_test_app`), and
|
|
803
|
+
`default_in_process_suite_options(options)` (emits the full Tier 1 suite
|
|
804
|
+
options bag: the `{setup_test, surface_source, capabilities}` triple plus
|
|
805
|
+
`session_options` / `create_route_specs` / `rpc_endpoints` pass-through;
|
|
806
|
+
call sites pass the output directly or spread it alongside
|
|
807
|
+
suite-specific extras like `roles`, `skip_routes`, `input_overrides`,
|
|
808
|
+
`db_factories`). Also exports `BootstrappedBackendHandle` (a
|
|
809
|
+
`BackendHandle` enriched with the keeper's captured credentials) and
|
|
810
|
+
`default_cross_process_setup(handle, options?)` — full runtime body.
|
|
811
|
+
Every per-test invocation unconditionally fires `_testing_reset` over the
|
|
812
|
+
keeper's daemon-token channel: wipes every auth-namespace row (no
|
|
813
|
+
keeper-preserve filter), resets `app_settings` + `bootstrap_lock`, and
|
|
814
|
+
inline-seeds a fresh keeper (`[ROLE_KEEPER, ROLE_ADMIN, ...extra_keeper_roles]`)
|
|
815
|
+
plus any declared `extra_accounts`. The fixture's `account` / `actor` /
|
|
816
|
+
cookies refresh to the new keeper on every call — in-process and
|
|
817
|
+
cross-process both run against a freshly bootstrapped keeper per test.
|
|
818
|
+
`fixture.create_account()` keeps a separate path: keeper-driven
|
|
819
|
+
`invite_create` (username-scoped) → signup → login → `account_token_create`
|
|
820
|
+
over the production RPC surface, so the invite-gated mint keeps
|
|
821
|
+
`open_signup` at its production default (`false`) and the per-test
|
|
822
|
+
secondary holds real session + bearer credentials.
|
|
823
|
+
`create_account({roles: [...]})` then drives `role_grant_offer_create`
|
|
824
|
+
(keeper) + `role_grant_offer_accept` (per-test) for each role — roles
|
|
825
|
+
whose `RoleSpec.grant_paths` don't include `'admin'` reject loudly at
|
|
826
|
+
offer-create time (`ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE`); those
|
|
827
|
+
roles must be seeded via `extra_accounts` at bootstrap-equivalent time
|
|
828
|
+
instead. Caller-supplied `username`s pass through _as-is_ now that the DB
|
|
829
|
+
wipes between tests — hardcoded names like `'user_two'` work and the
|
|
830
|
+
earlier uniquification prefix is gone. Every `TestFixture` also exposes
|
|
831
|
+
`fresh_transport({origin?: string | null})` — cookie-jar-free probe;
|
|
832
|
+
pass `{origin: null}` for bearer-only paths.
|
|
833
|
+
|
|
834
|
+
**Keeper ≠ admin.** `fixture.account` is the keeper account holding
|
|
835
|
+
`[ROLE_KEEPER, ROLE_ADMIN]` — the role split mirrors production
|
|
836
|
+
`bootstrap_account`. `ROLE_KEEPER` does not grant admin reach; bootstrap
|
|
837
|
+
just happens to land both as separate grants. Probing the separation (a
|
|
838
|
+
keeper-only account must 403 on admin RPCs) requires declaring
|
|
839
|
+
`extra_accounts: [{username, roles: [ROLE_KEEPER]}]` — `ROLE_KEEPER`'s
|
|
840
|
+
`grant_paths` is bootstrap-only, so a post-bootstrap offer/accept can't
|
|
841
|
+
deliver it.
|
|
842
|
+
|
|
843
|
+
- `testing/cross_backend/capabilities.ts` — `BackendCapabilities` vocabulary
|
|
844
|
+
(`bearer_auth` / `trusted_proxy` / `login_rate_limit` / `ws` / `sse` /
|
|
845
|
+
`in_process_only`), `test_if(cond, name, fn)` for capability-gated cases,
|
|
846
|
+
and `in_process_capabilities` preset.
|
|
847
|
+
|
|
848
|
+
### `cross_backend/standard.ts` — `describe_standard_cross_process_tests`
|
|
849
|
+
|
|
850
|
+
Cross-process counterpart to `describe_standard_tests`. Wires the
|
|
851
|
+
cross-process-safe subset in one call:
|
|
852
|
+
|
|
853
|
+
- `integration` — always
|
|
854
|
+
- `admin` — `roles` provided
|
|
855
|
+
- `round_trip` — always
|
|
856
|
+
- `rpc_round_trip` — always
|
|
857
|
+
- `data_exposure` — always
|
|
858
|
+
|
|
859
|
+
Three suites from the in-process bundle are omitted by design:
|
|
860
|
+
`rate_limiting` (needs a fresh per-test `TestApp` for tight rate-limiter
|
|
861
|
+
overrides; the spawned binary has no restart-per-test budget),
|
|
862
|
+
`audit_completeness` (reads FK structure that only the in-process backend
|
|
863
|
+
exposes; wire-level audit observability lives in the consumer's own audit
|
|
864
|
+
`.cross.test.ts`), `bootstrap_success` (bootstrap is one-shot per backend
|
|
865
|
+
lifecycle, already consumed by the consumer's `globalSetup`). The omission
|
|
866
|
+
rationale lives in the module doc once instead of repeating in each
|
|
867
|
+
consumer's `*.cross.test.ts`.
|
|
868
|
+
|
|
869
|
+
Hard-codes the cross-process-safe set with no `skip` knob; if a future
|
|
870
|
+
consumer needs partial opt-out, add the knob then.
|
|
871
|
+
`StandardCrossProcessTestOptions` is shape-aligned with
|
|
872
|
+
`StandardTestOptions` minus the in-process-only knobs (`create_route_specs`,
|
|
873
|
+
`bootstrap`, `rate_limiting_app_options`, `bootstrap_token`) — those drive
|
|
874
|
+
the omitted suites.
|
|
875
|
+
|
|
876
|
+
### `cross_backend/ws_round_trip.ts` — `describe_cross_process_ws_tests`
|
|
877
|
+
|
|
878
|
+
Real-upgrade WebSocket coverage of a spawned backend — the cross-process
|
|
879
|
+
counterpart to the in-process `ws_round_trip.ts` harness, kept a separate
|
|
880
|
+
call (not folded into `describe_standard_cross_process_tests`) because it
|
|
881
|
+
needs raw `base_url` / `ws_path` the standard bundle doesn't carry, mirroring
|
|
882
|
+
how `describe_ws_round_trip_tests` sits beside `describe_standard_tests`
|
|
883
|
+
in-process. `describe_cross_process_ws_tests({setup_test, capabilities,
|
|
884
|
+
base_url, ws_path, origin?, rpc_path?})` opens a live `WebSocket` via
|
|
885
|
+
`create_ws_transport` (the `ws` npm package) and asserts up to four cases
|
|
886
|
+
against the upgrade stack `register_ws_endpoint` wires (origin →
|
|
887
|
+
`require_auth` → dispatch): authed upgrade round-trips `heartbeat`,
|
|
888
|
+
anonymous upgrade refused, disallowed-origin upgrade refused, and — gated on
|
|
889
|
+
`rpc_path` — a live socket drops when the account's sessions are revoked
|
|
890
|
+
mid-connection (`account_session_revoke_all` over the keeper session channel
|
|
891
|
+
emits `session_revoke_all`, which `create_ws_auth_guard` closes on; asserted
|
|
892
|
+
via `WsClient.wait_for_close`). Per-connection auth is enforced **at upgrade
|
|
893
|
+
time**, so the negative upgrade cases assert the upgrade itself rejects, not
|
|
894
|
+
a per-message error; the close-on-revoke case proves the audit-fed guard is
|
|
895
|
+
the revocation seam for an already-open socket, since per-message dispatch
|
|
896
|
+
never re-checks credential validity. Omit `rpc_path` to skip the close case
|
|
897
|
+
(consumers without the standard account actions on their RPC endpoint).
|
|
898
|
+
**Consumer-agnostic** — it drives only the `heartbeat` protocol action
|
|
899
|
+
(guaranteed on every WS endpoint by `assert_ws_endpoints_include_protocol_actions`),
|
|
900
|
+
so it validates the transport without touching domain WS methods. Gated on
|
|
901
|
+
`capabilities.ws`; cross-process only (needs a real bound socket — wire from
|
|
902
|
+
a `*.cross.test.ts`, never an in-process setup). Authed cookies come from the
|
|
903
|
+
fresh-per-test keeper via `fixture.transport.cookies()`, not the stale
|
|
904
|
+
globalSetup handle. fuz_app's own wiring is `src/test/cross_backend/ws.cross.test.ts`.
|
|
905
|
+
|
|
906
|
+
### `cross_backend/sse_round_trip.ts` — `describe_cross_process_sse_tests`
|
|
907
|
+
|
|
908
|
+
Cross-process counterpart to the in-process `sse_round_trip.ts` harness —
|
|
909
|
+
opens a **real** streaming `fetch` against a spawned backend's audit-log SSE
|
|
910
|
+
endpoint via `create_sse_transport` (built-in `fetch` + `TextDecoder`, no
|
|
911
|
+
dep), threading the fresh-per-test keeper's session cookie. Kept a separate
|
|
912
|
+
call (not folded into `describe_standard_cross_process_tests`) for the same
|
|
913
|
+
reason the WS suite is — it needs raw `base_url` / `sse_path` the standard
|
|
914
|
+
bundle doesn't carry. Up to three cases, mirroring the in-process audit-log
|
|
915
|
+
self-test: the stream emits the `: connected` comment; a minted secondary's
|
|
916
|
+
sessions are revoked over the keeper's admin channel (`admin_session_revoke_all`),
|
|
917
|
+
broadcasting one `session_revoke_all` audit `data:` frame **without** closing
|
|
918
|
+
the keeper's stream (target ≠ subscriber — secondary minted before the stream
|
|
919
|
+
opens so its `create_account` audit events stay off it); and the subscriber's
|
|
920
|
+
_own_ sessions are revoked (`account_session_revoke_all`) so the audit guard
|
|
921
|
+
drops the live stream (asserted via `SseTransport.wait_for_close`). The
|
|
922
|
+
data-frame + close cases gate on `rpc_path` (they drive the standard
|
|
923
|
+
account/admin actions); all cases gate on `capabilities.sse`. Cross-process
|
|
924
|
+
only — wire from a `*.cross.test.ts`. fuz_app's own wiring is
|
|
925
|
+
`src/test/cross_backend/sse.cross.test.ts`; only the TS spines advertise
|
|
926
|
+
`sse` (they wire `audit_log_sse`), so the Rust `spine_stub` cases `.skip`.
|
|
927
|
+
|
|
928
|
+
### Cross-process plumbing (consumed by `*.cross.test.ts` suites)
|
|
929
|
+
|
|
930
|
+
- `testing/cross_backend/backend_config.ts` — `BackendConfig` +
|
|
931
|
+
`BackendBootstrapConfig` interfaces. Consumer factories
|
|
932
|
+
(`deno_backend_config()`, `rust_backend_config()`,
|
|
933
|
+
`spine_stub_backend_config()`) produce these; fuz_app ships
|
|
934
|
+
`spine_stub_backend_config()` as a convenience preset for the non-domain
|
|
935
|
+
third spine consumer, but otherwise backend-specific paths and env are a
|
|
936
|
+
consumer concern.
|
|
937
|
+
- `testing/cross_backend/spawn_backend.ts` — `spawn_backend(config) => BackendHandle`.
|
|
938
|
+
Writes the bootstrap token, spawns `detached: true` in its own process
|
|
939
|
+
group (so SIGTERM to the negative pid tears down descendants), polls
|
|
940
|
+
health, reads the deterministic daemon token from the binary-written
|
|
941
|
+
file. Registers exit-time + signal cleanup so vitest worker death or
|
|
942
|
+
Ctrl+C kills children before they strand ports.
|
|
943
|
+
- `testing/transports/fetch_transport.ts` — cookie-threading HTTP transport
|
|
944
|
+
satisfying `RpcTestTransport`. Carries a name-keyed cookie jar that
|
|
945
|
+
updates on every response's `Set-Cookie` and re-sends on every request;
|
|
946
|
+
`Origin` defaults to `base_url` (`origin: null` disables for bearer-only
|
|
947
|
+
paths). Exposes `cookies()` so `ws_transport` can thread the session
|
|
948
|
+
cookie onto the WS upgrade.
|
|
949
|
+
- `testing/transports/bootstrap.ts` — stateless `bootstrap({transport, config})`
|
|
950
|
+
POSTs `/api/account/bootstrap` against the running binary, parses the
|
|
951
|
+
`{ok, account, actor}` envelope, returns the keeper credentials. The
|
|
952
|
+
transport carries the keeper session cookie in its jar after this call
|
|
953
|
+
resolves.
|
|
954
|
+
- `testing/transports/ws_client.ts` — shared `WsClient` interface (`send` /
|
|
955
|
+
`request` / `close` / `messages` / `wait_for` / `wait_for_close`),
|
|
956
|
+
wire-frame types, and
|
|
957
|
+
predicates (`is_notification`, `is_response_for`, ...). Both in-process
|
|
958
|
+
(`ws_round_trip.ts`) and cross-process (`ws_transport.ts`) impls satisfy
|
|
959
|
+
this interface.
|
|
960
|
+
- `testing/transports/ws_transport.ts` — `create_ws_transport({base_url, ws_path, cookies, origin?})`
|
|
961
|
+
builds a real-upgrade WS client using the `ws` npm package (optional
|
|
962
|
+
peerDep; consumers wiring cross-process tests `npm install --save-dev ws`).
|
|
963
|
+
Threads the keeper cookie onto the upgrade so per-action auth succeeds on
|
|
964
|
+
the first message.
|
|
965
|
+
- `testing/transports/sse_frame_reader.ts` — `create_sse_frame_reader(reader, default_timeout_ms?)`,
|
|
966
|
+
the transport-agnostic SSE framing core over a
|
|
967
|
+
`ReadableStreamDefaultReader<Uint8Array>`: `\n\n` framing, per-read timeout,
|
|
968
|
+
`read_frame` / `wait_for_close` / `cancel`. Shared by the in-process route
|
|
969
|
+
suite (`sse_round_trip.ts`, over a Hono `Response.body`) and the
|
|
970
|
+
cross-process transport below (over a streaming `fetch` body).
|
|
971
|
+
- `testing/transports/sse_transport.ts` — `create_sse_transport({base_url, sse_path, cookies, origin?})`
|
|
972
|
+
opens a real streaming `fetch` (threading the keeper cookie), validates the
|
|
973
|
+
`text/event-stream` connect, then delegates frame reading to
|
|
974
|
+
`create_sse_frame_reader`. Built-in `fetch` + `TextDecoder` — no dep.
|
|
975
|
+
- `surface_source: AppSurfaceSpec` — the same shape both in-process and
|
|
976
|
+
cross-process tests pass. Constructed in TS via
|
|
977
|
+
`create_test_app_surface_spec` (or a consumer's equivalent like
|
|
978
|
+
`create_zzz_app_surface_spec`) — same builder both modes use. The
|
|
979
|
+
cross-process-ness lives in `setup_test: default_cross_process_setup(handle)`
|
|
980
|
+
— the `FetchTransport`, not the schema source. The on-disk
|
|
981
|
+
`*_attack_surface.json` snapshot is an observability artifact for human
|
|
982
|
+
inspection + gen-time drift detection
|
|
983
|
+
(`assert_surface_matches_snapshot`); it is not consumed at test runtime.
|
|
984
|
+
- `testing/cross_backend/testing_reset_actions.ts` —
|
|
985
|
+
`create_testing_actions(deps, options)` factory returning the
|
|
986
|
+
`_testing_reset` RPC action. Test binaries register it on their RPC
|
|
987
|
+
endpoint; `default_cross_process_setup` fires it unconditionally per
|
|
988
|
+
test. Handler DELETEs every auth-namespace row (no keeper-preserve
|
|
989
|
+
filter), resets `app_settings` to production defaults, flips
|
|
990
|
+
`bootstrap_lock.bootstrapped = true`, inline-seeds a fresh keeper via
|
|
991
|
+
`create_test_account_with_credentials` (same primitive in-process uses,
|
|
992
|
+
keeping write semantics in parity), seeds any `extra_accounts` at the
|
|
993
|
+
same bootstrap-equivalent step (the only path for roles like
|
|
994
|
+
`ROLE_KEEPER` whose `grant_paths` is bootstrap-only), refreshes
|
|
995
|
+
`DaemonTokenState.keeper_account_id` to the new row, then fires the
|
|
996
|
+
consumer-supplied `reset_state` callback for domain-state reset. Auth
|
|
997
|
+
gates on `credential_types: ['daemon_token']` — effectively keeper-only
|
|
998
|
+
without forcing the `actor: 'required'` ⟺ `acting?: ActingActor`
|
|
999
|
+
biconditional. No free-form runtime grant action exists — see the
|
|
1000
|
+
`testing_reset_actions.ts` TSDoc for the audit + WS fan-out rationale
|
|
1001
|
+
that rejected a `_testing_seed_role_grant` shape.
|
|
1002
|
+
|
|
1003
|
+
### Building a TS test-server binary — `testing_server_core.ts` + adapters
|
|
1004
|
+
|
|
1005
|
+
The reusable shape for standing up a **spawnable TS** cross-process test
|
|
1006
|
+
binary (the TS analog of the Rust `testing_spine_stub`), so consumers don't
|
|
1007
|
+
re-roll the serve / daemon-info / WS-attach / drain boilerplate:
|
|
1008
|
+
|
|
1009
|
+
- `testing/cross_backend/testing_server_core.ts` — `start_testing_server({adapter, daemon_name, host, port, app_version?, build_app})`. Owns the runtime-neutral orchestration: open-host refusal, stale-daemon check, daemon-info write, `serve`, post-serve WS attach, graceful drain. Domain-free — the app is the caller's `build_app(): Promise<BuiltTestingApp>` seam (`{app, close, mount_websocket?}`). `mount_websocket(upgrade)` is invoked after the app exists + the adapter prepared WS (the mount-after-app order Node's `@hono/node-ws` forces — `create_app_server`'s `ws_endpoints` auto-mount can't be used on Node). Exports the `TestingServerAdapter` / `ServeHandle` / `PreparedWebsocket` interfaces.
|
|
1010
|
+
- `testing/cross_backend/testing_server_node.ts` — `create_node_testing_adapter()` (`@hono/node-server` + `@hono/node-ws`). Optional peer deps (like `ws`); only test binaries import them.
|
|
1011
|
+
- `testing/cross_backend/testing_server_deno.ts` — `create_deno_testing_adapter()` (`Deno.serve` + `hono/deno`; `Deno` declared locally so it typechecks under the Node toolchain). Spawn the entry with `--sloppy-imports` (Deno doesn't do `.js`→`.ts`; Gro's loader does, so the Node path needs no flag).
|
|
1012
|
+
- `testing/cross_backend/testing_server_bun.ts` — `create_bun_testing_adapter()` (`Bun.serve` + `hono/bun`'s module-level `upgradeWebSocket` + `websocket`; `Bun.serve` declared locally so it typechecks under the Node toolchain). **No extra deps** (`hono/bun` ships with `hono`; `Bun.serve` is built in, unlike Node's `@hono/node-server` + `@hono/node-ws`), and Bun resolves `.js`→`.ts` natively (no flag, unlike Deno). Reuses `create_node_runtime` (Bun implements the `node:fs`/`node:process` surface). WS is module-level + stateless (like Deno) — the `websocket` handler is threaded into `serve`, where `Bun.serve` wants it, so no post-serve attach.
|
|
1013
|
+
- `testing/cross_backend/default_spine_surface.ts` — the canonical no-domain spine surface (account/admin/audit/signup + bootstrap): `spine_session_options`, `spine_roles`, `create_spine_route_specs`, `spine_rpc_endpoints`, `create_spine_surface_spec`. `$lib`-free (it's reached by the spawned binary under Gro's loader, which doesn't resolve `$lib`), so keep it on relative imports. Shared by the spine_stub cross test, the TS cross tests, and the binary.
|
|
1014
|
+
- `testing/cross_backend/ts_spine_backend_config.ts` — `ts_spine_node_backend_config()` / `ts_spine_deno_backend_config()` / `ts_spine_bun_backend_config()` presets (in-memory PGlite, no external infra), the TS analog of `spine_stub_backend_config()`.
|
|
1015
|
+
|
|
1016
|
+
fuz_app's own binary wiring (`src/test/cross_backend/testing_spine_server{,_node,_deno,_bun}.ts`) is the worked example: ~one `build_app` over `create_app_backend` + `create_app_server` + `_testing_reset` + a WS mount, reusing `default_spine_surface`. The `_node`/`_deno`/`_bun` entries differ only in which adapter they wire — `build_spine_app` is runtime-agnostic.
|
|
1017
|
+
|
|
1018
|
+
The in-process `ws_round_trip` harness stays (it drives the dispatcher
|
|
1019
|
+
against a fake upgrade, no wire), but the real-upgrade coverage now lives in
|
|
1020
|
+
the cross-process `cross_backend/ws_round_trip.ts` suite below — including
|
|
1021
|
+
close-on-revoke (`WsClient.wait_for_close` asserts the audit-guard drops a
|
|
1022
|
+
live socket on `session_revoke_all`).
|
|
1023
|
+
|
|
1024
|
+
`audit_completeness` is in-process by design (FK-structural introspection
|
|
1025
|
+
beyond the `audit_log_list` RPC reads — structurally in-process).
|
|
1026
|
+
|
|
1027
|
+
**Cross-process SSE** is wired (see §`cross_backend/sse_round_trip.ts`
|
|
1028
|
+
above). The TS spine binary serves `GET /api/admin/audit/stream` —
|
|
1029
|
+
`build_spine_app` passes `audit_log_sse: true` and `create_spine_route_specs`
|
|
1030
|
+
mounts `create_audit_log_route_specs({stream: ctx.audit_sse})` when
|
|
1031
|
+
`ctx.audit_sse` is set (keeps `default_spine_surface.ts` `$lib`-free and the
|
|
1032
|
+
shared surface snapshot SSE-free, since the surface stub ctx has
|
|
1033
|
+
`audit_sse: null`). `capabilities.sse` is scoped to the TS spine configs
|
|
1034
|
+
(`ts_spine_backend_config.ts`), not the shared `ts_default_capabilities`,
|
|
1035
|
+
which stays honest for consumers who don't wire `audit_log_sse`. The
|
|
1036
|
+
real-HTTP `transports/sse_transport.ts` feeds `describe_cross_process_sse_tests`.
|
|
1037
|
+
|
|
1038
|
+
The auth-cost handling for cross-process testing is consumer-side: each
|
|
1039
|
+
consumer ships a separate test binary wiring a fast-params
|
|
1040
|
+
`TestingArgon2idHasher` from a sibling Rust testing crate. Cross-process
|
|
1041
|
+
`bootstrap` + `create_account` are then plain RPC calls against the test
|
|
1042
|
+
binary — no DB-direct surgery in fuz_app's testing library, no runtime
|
|
1043
|
+
knobs in production code, no shared cookie key with the backend.
|
|
1044
|
+
|
|
1045
|
+
### cross_backend/bench/ — cross-impl measurement
|
|
1046
|
+
|
|
1047
|
+
Generic primitive for cross-impl **measurement**: drive identical wire
|
|
1048
|
+
scenarios across several spawned backends and time each round trip so a
|
|
1049
|
+
TS impl and a Rust impl compare apples-to-apples (both cross-process over
|
|
1050
|
+
real HTTP). A thin
|
|
1051
|
+
scenario→task→report adapter over `@fuzdev/fuz_util`'s benchmark library
|
|
1052
|
+
(`Benchmark`, `benchmark_stats_compare`, `benchmark_format_markdown`) —
|
|
1053
|
+
no stats engine reinvented. fuz_app ships the primitive; consumers wire
|
|
1054
|
+
scenarios + the run (zzz's `npm run benchmark:cross-impl` was the first).
|
|
1055
|
+
fuz_app also ships its **own** `npm run benchmark:cross-impl`
|
|
1056
|
+
(`src/benchmarks/cross_impl.bench.ts`) on the back of its TS spine binary —
|
|
1057
|
+
ts-node + ts-deno + ts-bun (+ the Rust `spine_stub` when `FUZ_TESTING_SPINE_STUB_BIN`
|
|
1058
|
+
is set). The three TS runtimes are apples-to-apples with each other (same
|
|
1059
|
+
PGlite driver); TS-vs-Rust carries the PGlite-vs-Postgres DB-layer caveat
|
|
1060
|
+
(documented in the run). The artifact (`*.latest.json`) is gitignored.
|
|
1061
|
+
|
|
1062
|
+
- `bench/scenario.ts` — `BenchScenario` (`{name, requires?, run}`) +
|
|
1063
|
+
`BenchScenarioContext` (pre-authed `transport`, `rpc_path`,
|
|
1064
|
+
`capabilities`). The `run` body is the timed task; it `throw`s on a
|
|
1065
|
+
non-success envelope so the benchmark records a failed iteration.
|
|
1066
|
+
`default_bench_scenarios` are read-only spine-surface calls
|
|
1067
|
+
(`account_verify` dispatch floor, `account_session_list`,
|
|
1068
|
+
`audit_log_list`) — idempotent, so safe to repeat against one
|
|
1069
|
+
bootstrapped keeper without a per-iteration `_testing_reset`. `login`
|
|
1070
|
+
is omitted on purpose (test binaries use a fast hasher, so it'd measure
|
|
1071
|
+
dispatch not real Argon2).
|
|
1072
|
+
- `bench/run_cross_impl_bench.ts` — `run_cross_impl_bench({handles, scenarios, config?})`
|
|
1073
|
+
bootstraps each backend once (uses `handle.keeper_transport`; **no reset
|
|
1074
|
+
in the hot loop**), runs each scenario as a one-task `Benchmark` named by
|
|
1075
|
+
the backend, returns `CrossImplBenchResult` (`{backends, scenarios, entries}`).
|
|
1076
|
+
Network-tuned defaults override fuz_util's micro defaults
|
|
1077
|
+
(warmup 20 / min 100 / duration 3000ms).
|
|
1078
|
+
- `bench/bench_report.ts` — `format_cross_impl_markdown` (per-scenario
|
|
1079
|
+
table, backend rows), `compare_cross_impl` (Welch verdict per scenario
|
|
1080
|
+
vs a reference backend) + `format_cross_impl_comparison`, and
|
|
1081
|
+
`format_cross_impl_json` (self-describing artifact: per backend×scenario
|
|
1082
|
+
percentiles off the raw-sample tail, budget, iteration count).
|
|
1083
|
+
|
|
1084
|
+
Tail honesty depends on a fuz_util change: `BenchmarkStats` computes order
|
|
1085
|
+
statistics (min/max/p50–p99) on raw samples while central-tendency stats
|
|
1086
|
+
stay MAD-cleaned — so p99 reflects real tail events. Deeper tiers (resource
|
|
1087
|
+
sampling, workload corpora, load/soak) and the static-docs dashboard with
|
|
1088
|
+
committed historical fixtures stay deferred until CI automation exists.
|