@fuzdev/fuz_app 0.29.0 → 0.31.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 +630 -0
- package/dist/actions/action_rpc.d.ts +29 -0
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +42 -6
- package/dist/actions/action_types.d.ts +2 -2
- package/dist/actions/cancel.d.ts +12 -13
- package/dist/actions/cancel.d.ts.map +1 -1
- package/dist/actions/cancel.js +10 -13
- package/dist/actions/heartbeat.d.ts +8 -13
- package/dist/actions/heartbeat.d.ts.map +1 -1
- package/dist/actions/heartbeat.js +5 -8
- package/dist/actions/register_action_ws.d.ts +3 -3
- package/dist/actions/register_action_ws.js +2 -2
- package/dist/actions/register_ws_endpoint.d.ts +4 -4
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +3 -3
- package/dist/actions/socket.svelte.d.ts +16 -16
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +15 -15
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.d.ts +15 -0
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +17 -0
- package/dist/auth/CLAUDE.md +923 -0
- package/dist/auth/account_action_specs.d.ts +216 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -0
- package/dist/auth/account_action_specs.js +159 -0
- package/dist/auth/account_actions.d.ts +51 -0
- package/dist/auth/account_actions.d.ts.map +1 -0
- package/dist/auth/account_actions.js +119 -0
- package/dist/auth/account_queries.d.ts +6 -2
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +40 -4
- package/dist/auth/account_routes.d.ts +94 -16
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +108 -180
- package/dist/auth/account_schema.d.ts +85 -30
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +40 -8
- package/dist/auth/admin_action_specs.d.ts +674 -0
- package/dist/auth/admin_action_specs.d.ts.map +1 -0
- package/dist/auth/admin_action_specs.js +287 -0
- package/dist/auth/admin_actions.d.ts +69 -0
- package/dist/auth/admin_actions.d.ts.map +1 -0
- package/dist/auth/admin_actions.js +256 -0
- package/dist/auth/api_token.d.ts +10 -0
- package/dist/auth/api_token.d.ts.map +1 -1
- package/dist/auth/api_token.js +9 -0
- package/dist/auth/api_token_queries.d.ts +3 -3
- package/dist/auth/api_token_queries.js +3 -3
- package/dist/auth/app_settings_schema.d.ts +4 -3
- package/dist/auth/app_settings_schema.d.ts.map +1 -1
- package/dist/auth/app_settings_schema.js +2 -1
- package/dist/auth/audit_log_routes.d.ts +14 -6
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +22 -79
- package/dist/auth/audit_log_schema.d.ts +100 -29
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +83 -11
- package/dist/auth/bootstrap_routes.d.ts +14 -0
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +10 -3
- package/dist/auth/cleanup.d.ts +63 -0
- package/dist/auth/cleanup.d.ts.map +1 -0
- package/dist/auth/cleanup.js +80 -0
- package/dist/auth/invite_schema.d.ts +11 -10
- package/dist/auth/invite_schema.d.ts.map +1 -1
- package/dist/auth/invite_schema.js +4 -3
- package/dist/auth/migrations.d.ts +6 -0
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +28 -0
- package/dist/auth/permit_offer_action_specs.d.ts +364 -0
- package/dist/auth/permit_offer_action_specs.d.ts.map +1 -0
- package/dist/auth/permit_offer_action_specs.js +216 -0
- package/dist/auth/permit_offer_actions.d.ts +96 -0
- package/dist/auth/permit_offer_actions.d.ts.map +1 -0
- package/dist/auth/permit_offer_actions.js +428 -0
- package/dist/auth/permit_offer_notifications.d.ts +361 -0
- package/dist/auth/permit_offer_notifications.d.ts.map +1 -0
- package/dist/auth/permit_offer_notifications.js +179 -0
- package/dist/auth/permit_offer_queries.d.ts +165 -0
- package/dist/auth/permit_offer_queries.d.ts.map +1 -0
- package/dist/auth/permit_offer_queries.js +390 -0
- package/dist/auth/permit_offer_schema.d.ts +103 -0
- package/dist/auth/permit_offer_schema.d.ts.map +1 -0
- package/dist/auth/permit_offer_schema.js +142 -0
- package/dist/auth/permit_queries.d.ts +77 -14
- package/dist/auth/permit_queries.d.ts.map +1 -1
- package/dist/auth/permit_queries.js +119 -24
- package/dist/auth/session_queries.d.ts +4 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +4 -2
- package/dist/auth/signup_routes.d.ts +13 -0
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +14 -7
- package/dist/http/CLAUDE.md +584 -0
- package/dist/http/pending_effects.d.ts +29 -0
- package/dist/http/pending_effects.d.ts.map +1 -0
- package/dist/http/pending_effects.js +31 -0
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/route_spec.js +4 -3
- package/dist/rate_limiter.d.ts +30 -0
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +25 -2
- package/dist/realtime/sse_auth_guard.d.ts +2 -0
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +5 -3
- package/dist/testing/CLAUDE.md +668 -1
- package/dist/testing/admin_integration.d.ts +10 -7
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +382 -482
- package/dist/testing/app_server.d.ts +7 -6
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/attack_surface.d.ts +9 -3
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +4 -4
- package/dist/testing/audit_completeness.d.ts +6 -0
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +158 -134
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +4 -33
- package/dist/testing/db.d.ts +1 -1
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +2 -0
- package/dist/testing/entities.d.ts +35 -13
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +17 -0
- package/dist/testing/integration.d.ts +10 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +352 -340
- package/dist/testing/integration_helpers.d.ts +16 -5
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +24 -4
- package/dist/testing/rate_limiting.d.ts +7 -0
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +41 -10
- package/dist/testing/rpc_helpers.d.ts +153 -1
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +184 -8
- package/dist/testing/sse_round_trip.d.ts +8 -0
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +10 -3
- package/dist/testing/standard.d.ts +9 -1
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +6 -2
- package/dist/testing/surface_invariants.d.ts +7 -3
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +5 -4
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +9 -38
- package/dist/ui/AccountSessions.svelte +8 -4
- package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
- package/dist/ui/AdminAccounts.svelte +61 -33
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminAuditLog.svelte +3 -2
- package/dist/ui/AdminAuditLog.svelte.d.ts.map +1 -1
- package/dist/ui/AdminInvites.svelte +3 -2
- package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +14 -9
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/AdminPermitHistory.svelte +3 -2
- package/dist/ui/AdminPermitHistory.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSessions.svelte +29 -25
- package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +351 -0
- package/dist/ui/OpenSignupToggle.svelte +6 -3
- package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -1
- package/dist/ui/PermitOfferForm.svelte +141 -0
- package/dist/ui/PermitOfferForm.svelte.d.ts +14 -0
- package/dist/ui/PermitOfferForm.svelte.d.ts.map +1 -0
- package/dist/ui/PermitOfferHistory.svelte +109 -0
- package/dist/ui/PermitOfferHistory.svelte.d.ts +11 -0
- package/dist/ui/PermitOfferHistory.svelte.d.ts.map +1 -0
- package/dist/ui/PermitOfferInbox.svelte +121 -0
- package/dist/ui/PermitOfferInbox.svelte.d.ts +12 -0
- package/dist/ui/PermitOfferInbox.svelte.d.ts.map +1 -0
- package/dist/ui/account_sessions_state.svelte.d.ts +53 -3
- package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.js +39 -16
- package/dist/ui/admin_accounts_state.svelte.d.ts +118 -2
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +99 -23
- package/dist/ui/admin_invites_state.svelte.d.ts +47 -1
- package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_invites_state.svelte.js +38 -26
- package/dist/ui/admin_sessions_state.svelte.d.ts +26 -0
- package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_sessions_state.svelte.js +35 -21
- package/dist/ui/app_settings_state.svelte.d.ts +39 -0
- package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
- package/dist/ui/app_settings_state.svelte.js +34 -18
- package/dist/ui/audit_log_state.svelte.d.ts +40 -3
- package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
- package/dist/ui/audit_log_state.svelte.js +36 -42
- package/dist/ui/auth_state.svelte.d.ts +4 -3
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +4 -1
- package/dist/ui/permit_offers_state.svelte.d.ts +125 -0
- package/dist/ui/permit_offers_state.svelte.d.ts.map +1 -0
- package/dist/ui/permit_offers_state.svelte.js +197 -0
- package/package.json +3 -3
- package/dist/auth/admin_routes.d.ts +0 -29
- package/dist/auth/admin_routes.d.ts.map +0 -1
- package/dist/auth/admin_routes.js +0 -226
- package/dist/auth/app_settings_routes.d.ts +0 -27
- package/dist/auth/app_settings_routes.d.ts.map +0 -1
- package/dist/auth/app_settings_routes.js +0 -66
- package/dist/auth/invite_routes.d.ts +0 -18
- package/dist/auth/invite_routes.d.ts.map +0 -1
- package/dist/auth/invite_routes.js +0 -129
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -1,3 +1,670 @@
|
|
|
1
1
|
# testing/
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Composable test utilities exported to consumer projects. Stubs, factories,
|
|
4
|
+
attack-surface generators, middleware mocks, integration suites, and RPC/SSE/WS
|
|
5
|
+
round-trip harnesses. Consumers import these to assemble their own test suites
|
|
6
|
+
against a fuz_app-derived server.
|
|
7
|
+
|
|
8
|
+
For narrative wiring examples (how to call these from a consumer's vitest
|
|
9
|
+
setup), see `../../../docs/testing.md`. For fuz_app's own test suite
|
|
10
|
+
conventions (`.db.test.ts` suffix, the `db` vitest project, `assert_rejects`),
|
|
11
|
+
see `../../test/CLAUDE.md`. This file is a reference index for the helpers
|
|
12
|
+
themselves.
|
|
13
|
+
|
|
14
|
+
## Production guard — always the first import
|
|
15
|
+
|
|
16
|
+
Every module in this directory starts with `import './assert_dev_env.js';`
|
|
17
|
+
as its first line. The side-effect import reads `DEV` from `esm-env` and
|
|
18
|
+
throws if it is false — preventing accidental inclusion in production
|
|
19
|
+
bundles. SvelteKit and Vite set `DEV` correctly for dev + tests; the
|
|
20
|
+
production code path explodes at the first testing-module import.
|
|
21
|
+
|
|
22
|
+
When adding a new module to this directory, make this import the first
|
|
23
|
+
line. The convention is enforced by grep, not by a linter — break it and
|
|
24
|
+
the production bundle still builds, then crashes at runtime on first
|
|
25
|
+
module load.
|
|
26
|
+
|
|
27
|
+
## Stubs, factories, mocks
|
|
28
|
+
|
|
29
|
+
### `stubs.ts` — `AppDeps` + `AppServerContext` stubs
|
|
30
|
+
|
|
31
|
+
| Helper | Role |
|
|
32
|
+
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| `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 `{}`. |
|
|
34
|
+
| `create_noop_stub<T>(label, overrides?)` | Proxy whose every method returns `async () => undefined`; `overrides` lets callers pin specific props. |
|
|
35
|
+
| `stub` | Pre-built throwing stub labelled `'stub'`. |
|
|
36
|
+
| `create_stub_db()` | Returns a real `Db` whose `client.query` yields `{rows: []}` and whose `transaction(fn)` synchronously calls `fn(inner_stub_db)`. Safe for `apply_route_specs`'s declarative transaction wrapper. |
|
|
37
|
+
| `stub_handler()` | Returns a fresh `Response('stub')`. |
|
|
38
|
+
| `stub_mw` | Pass-through middleware handler (`async (_c, next) => next()`). |
|
|
39
|
+
| `stub_app_deps` | Frozen `AppDeps` — every capability is a throwing stub, `on_audit_event` is a noop. |
|
|
40
|
+
| `create_stub_app_deps()` | Factory returning fresh `AppDeps` with no-op FS/keyring/password, a `create_noop_stub` DB, silent `Logger`. |
|
|
41
|
+
| `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. |
|
|
42
|
+
| `create_stub_app_server_context(session_options)` | Stub `AppServerContext` — rate limiters null, `bootstrap_status.available: false`, `app_settings.open_signup: false`. |
|
|
43
|
+
| `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?`, `transform_middleware?`, `bootstrap_route_prefix?`. Single source of truth for attack-surface tests — track `create_app_server` wiring changes here. |
|
|
44
|
+
|
|
45
|
+
Throwing stubs surface mock escape: a test that accidentally reaches into
|
|
46
|
+
stub territory breaks immediately with a label-scoped error rather than
|
|
47
|
+
silently returning `undefined` or `{}`. Use throwing stubs by default;
|
|
48
|
+
use no-op stubs only when a dep is known to be reached with a don't-care
|
|
49
|
+
result.
|
|
50
|
+
|
|
51
|
+
### `entities.ts` — test entity factories
|
|
52
|
+
|
|
53
|
+
Plain `(overrides?) => Entity` constructors with sensible defaults —
|
|
54
|
+
callers set only the fields the test cares about. Names prefix with
|
|
55
|
+
`create_test_*` to avoid collisions with real `account_queries.ts`
|
|
56
|
+
factories.
|
|
57
|
+
|
|
58
|
+
Override types widen branded `Uuid` fields to `string` so tests pass
|
|
59
|
+
literal ids without per-site casts — the factory brands internally.
|
|
60
|
+
Exported as `TestAccountOverrides` / `TestActorOverrides` /
|
|
61
|
+
`TestPermitOverrides` / `TestAuditEventOverrides`.
|
|
62
|
+
|
|
63
|
+
| Factory | Default id / role |
|
|
64
|
+
| ------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
65
|
+
| `create_test_account(overrides?)` | `{id: 'acct-test', username: 'test_user', …}` |
|
|
66
|
+
| `create_test_actor(overrides?)` | `{id: 'actor-test', account_id: 'acct-test', …}` |
|
|
67
|
+
| `create_test_permit(overrides?)` | `{id: 'permit-test', actor_id: 'actor-test', role: 'admin', scope_id: null, …}` |
|
|
68
|
+
| `create_test_context(permits?)` | `{account, actor, permits}` — pass `[{role: 'keeper'}, {role: 'admin'}]` for multi-role. |
|
|
69
|
+
| `create_test_audit_event(overrides?)` | `{id: 'evt-test', event_type: 'login', outcome: 'success', …}` — for SSE guard / audit tests. |
|
|
70
|
+
|
|
71
|
+
### `mock_fs.ts` — in-memory filesystem
|
|
72
|
+
|
|
73
|
+
`create_mock_fs(initial_files?) => {read_file, write_file, get_file}`.
|
|
74
|
+
Missing-path reads throw an `Error` with `.code = 'ENOENT'` so callers
|
|
75
|
+
exercise the same branches as `node:fs`. Use for DI-based filesystem
|
|
76
|
+
tests; never replaces `node:fs` globally.
|
|
77
|
+
|
|
78
|
+
## Database — `db.ts`
|
|
79
|
+
|
|
80
|
+
Factory builders for parameterized DB tests. Consumer projects pass their
|
|
81
|
+
`init_schema` callback (which calls `run_migrations(db, [AUTH_MIGRATION_NS, ...app_migrations])`);
|
|
82
|
+
factories accept any migration namespace set.
|
|
83
|
+
|
|
84
|
+
| Helper | Role |
|
|
85
|
+
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
86
|
+
| `IS_CI` | `process.env.CI === 'true'` — CI detection. |
|
|
87
|
+
| `DbFactory` interface | `{name, create, close, skip, skip_reason?}`. |
|
|
88
|
+
| `reset_pglite(db)` | `DROP SCHEMA public CASCADE` + recreate. Reuses a live PGlite instance. |
|
|
89
|
+
| `create_pglite_factory(init_schema)` | In-memory; no external deps; `skip: false`. See WASM caching below. |
|
|
90
|
+
| `create_pg_factory(init_schema, test_url?)` | PostgreSQL; `skip: true` when `test_url` is missing; drops `schema_version` before `init_schema` so migrations re-evaluate against actual tables; pool is reused + cleaned up across `create()` calls. |
|
|
91
|
+
| `AUTH_TRUNCATE_TABLES` | `['invite', 'api_token', 'auth_session', 'permit', 'permit_offer', 'actor', 'account']` in FK-safe order. Excludes `audit_log` — unit DB tests don't need to truncate it. |
|
|
92
|
+
| `AUTH_INTEGRATION_TRUNCATE_TABLES` | `AUTH_TRUNCATE_TABLES + ['audit_log']` — for integration suites that exercise the audit path. |
|
|
93
|
+
| `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. |
|
|
94
|
+
| `drop_auth_schema(db)` | `DROP TABLE IF EXISTS <table> CASCADE` for every entry in `AUTH_DROP_TABLES` plus `schema_version`. Safe on fresh DBs. |
|
|
95
|
+
| `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`. |
|
|
96
|
+
| `log_db_factory_status(factories)` | Console summary of enabled / skipped factories. |
|
|
97
|
+
|
|
98
|
+
**PGlite WASM caching.** `create_pglite_factory` shares a single PGlite
|
|
99
|
+
instance in a module-level ref (`module_db`) across all factories in the
|
|
100
|
+
same vitest worker thread. Subsequent `create()` calls
|
|
101
|
+
`DROP SCHEMA public CASCADE` instead of paying the ~500–700ms WASM
|
|
102
|
+
cold-start cost again. Since each vitest file runs in its own worker,
|
|
103
|
+
there is no cross-file contamination — but inside a file, suites share
|
|
104
|
+
state until the schema is reset. The `db` vitest project (opted into by
|
|
105
|
+
the `.db.test.ts` suffix) runs with `isolate: false` +
|
|
106
|
+
`fileParallelism: false` to amortize the WASM boot across every DB test
|
|
107
|
+
file in the run.
|
|
108
|
+
|
|
109
|
+
## Test app assembly
|
|
110
|
+
|
|
111
|
+
### `app_server.ts`
|
|
112
|
+
|
|
113
|
+
`create_test_app_server(options)` bootstraps a minimal `AppBackend` with a
|
|
114
|
+
keeper account, API token, session cookie, and signed `Keyring`.
|
|
115
|
+
`create_test_app(options)` layers `create_app_server` on top, returning a
|
|
116
|
+
fully assembled Hono app + the backend + helpers.
|
|
117
|
+
|
|
118
|
+
Key module-scope values:
|
|
119
|
+
|
|
120
|
+
- `stub_password_deps` — `PasswordHashDeps` that hashes via
|
|
121
|
+
`stub_hash_${password}` and verifies by equality. Deterministic, no
|
|
122
|
+
Argon2 cost — use for every test that isn't specifically exercising
|
|
123
|
+
password hashing.
|
|
124
|
+
- `TEST_COOKIE_SECRET` — 64-hex-char deterministic cookie secret.
|
|
125
|
+
Produces a valid `Keyring` via `create_validated_keyring`. Never used
|
|
126
|
+
in production — the stub guard plus fixed value is the contract.
|
|
127
|
+
- `fallback_pglite_factory` — module-level PGlite factory that
|
|
128
|
+
`create_test_app_server` uses when no `db` is passed. Reuses the WASM
|
|
129
|
+
cache via `create_pglite_factory`.
|
|
130
|
+
|
|
131
|
+
`bootstrap_test_account(options)` is extracted because both
|
|
132
|
+
`create_test_app_server` and `TestApp.create_account` reuse the same
|
|
133
|
+
"insert account + actor + roles + API token + session + cookie" flow.
|
|
134
|
+
Takes `{db, keyring, session_options, password, username?, password_value?, roles?}`.
|
|
135
|
+
|
|
136
|
+
| Type | Shape |
|
|
137
|
+
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
138
|
+
| `TestAppServer extends AppBackend` | Adds `account`, `actor`, `api_token`, `session_cookie`, `keyring`, `cleanup()`. |
|
|
139
|
+
| `TestAppServerOptions` | `session_options` (required), optional `db`, `db_type`, `password`, `username`, `password_value`, `roles`, `on_audit_event`. |
|
|
140
|
+
| `CreateTestAppOptions extends TestAppServerOptions` | Adds `create_route_specs` (required) + `app_options` (narrow `Partial<AppServerOptions>` excluding the three the helper manages). |
|
|
141
|
+
| `TestAccount` | `{account, actor, session_cookie, api_token, create_session_headers, create_bearer_headers}`. |
|
|
142
|
+
| `TestApp` | `{app, backend, surface_spec, surface, route_specs, create_session_headers, create_bearer_headers, create_daemon_token_headers, create_account, cleanup}`. |
|
|
143
|
+
|
|
144
|
+
`create_test_app` hard-codes the test-friendly `AppServerOptions`:
|
|
145
|
+
`allowed_origins: [/^http:\/\/localhost/]`, stub proxy pinned to
|
|
146
|
+
`127.0.0.1`, `env_schema: z.object({})`, every rate limiter `null`,
|
|
147
|
+
static daemon token state (no rotation, keeper already set),
|
|
148
|
+
**`await_pending_effects: true`** (fire-and-forget effects complete
|
|
149
|
+
before the response returns so tests can assert on side effects inline),
|
|
150
|
+
and silent logger. Override via `app_options`.
|
|
151
|
+
|
|
152
|
+
A fresh Hono app is created on every call because middleware closures
|
|
153
|
+
bind to the server's deps (db, keyring). Hono assembly is cheap
|
|
154
|
+
(~10–50ms); PGlite WASM caching in `db.ts` is where the real savings are.
|
|
155
|
+
|
|
156
|
+
### `auth_apps.ts` — adversarial-auth app factories
|
|
157
|
+
|
|
158
|
+
Pre-built Hono apps at each auth level (public / authed / keeper / per-role)
|
|
159
|
+
for attack-surface testing. No middleware stack — a single `/*` middleware
|
|
160
|
+
injects the `REQUEST_CONTEXT_KEY` + `CREDENTIAL_TYPE_KEY` (default
|
|
161
|
+
`'session'`) and hands off to `apply_route_specs` with
|
|
162
|
+
`fuz_auth_guard_resolver`.
|
|
163
|
+
|
|
164
|
+
| Helper | Role |
|
|
165
|
+
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
166
|
+
| `create_test_request_context(role?)` | Minimal `RequestContext` — one account, one actor, one permit for `role` (or none). |
|
|
167
|
+
| `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. |
|
|
168
|
+
| `AuthTestApps` | `{public, authed, keeper, by_role: Map<string, Hono>}`. |
|
|
169
|
+
| `create_auth_test_apps(specs, roles)` | Builds one app per auth level. Keeper app uses `credential_type: 'daemon_token'` so `require_keeper` passes. |
|
|
170
|
+
| `select_auth_app(apps, auth)` | Map `RouteAuth` → matching Hono app. Throws for missing `role:*` entries. |
|
|
171
|
+
| `resolve_test_path(path)` | `:foo` → `test_foo` — adequate for routes without format-constrained params. |
|
|
172
|
+
|
|
173
|
+
## Assertions, coverage, helpers
|
|
174
|
+
|
|
175
|
+
### `assertions.ts` — surface + error-schema assertions
|
|
176
|
+
|
|
177
|
+
| Helper | Role |
|
|
178
|
+
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
179
|
+
| `resolve_fixture_path(filename, import_meta_url)` | Absolute path relative to the caller's module (use `import.meta.url`). |
|
|
180
|
+
| `assert_surface_matches_snapshot(surface, path)` | Compares live `AppSurface` against a committed JSON snapshot; failure message instructs `gro gen`. |
|
|
181
|
+
| `assert_surface_deterministic(build_surface)` | Build twice, `deepStrictEqual` the two results — catches nondeterminism in surface generation. |
|
|
182
|
+
| `assert_only_expected_public_routes(surface, list)` | Bidirectional: no unexpected public routes, no missing expected ones. Format: `['GET /health', 'POST /api/account/login']`. |
|
|
183
|
+
| `assert_full_middleware_stack(surface, prefix, mws)` | Every route under `prefix` has exactly `mws` as its middleware chain. |
|
|
184
|
+
| `get_route_error_schema(lookup, route, status)` | Read out of a pre-built merged-error-schema map. |
|
|
185
|
+
| `assert_error_schema_valid(lookup, route, status, body)` | Assert a schema exists + parses the body. |
|
|
186
|
+
|
|
187
|
+
### `surface_invariants.ts` — structural + policy invariants
|
|
188
|
+
|
|
189
|
+
Structural invariants (options-free, apply universally):
|
|
190
|
+
|
|
191
|
+
| Assertion | Checks |
|
|
192
|
+
| ----------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
193
|
+
| `assert_protected_routes_declare_401` | Every protected route has 401 in `error_schemas`. |
|
|
194
|
+
| `assert_role_routes_declare_403` | Every role/keeper route has 403. |
|
|
195
|
+
| `assert_input_routes_declare_400` | Every route with input has 400. |
|
|
196
|
+
| `assert_params_routes_declare_400` | Every route with params has 400. |
|
|
197
|
+
| `assert_query_routes_declare_400` | Every route with query has 400. |
|
|
198
|
+
| `assert_descriptions_present` | Every route has a non-empty description. |
|
|
199
|
+
| `assert_no_duplicate_routes` | No duplicate method+path pairs. |
|
|
200
|
+
| `assert_middleware_errors_propagated` | Every middleware-declared error status appears on every applicable route. |
|
|
201
|
+
| `assert_error_schemas_structurally_valid` | Every declared error schema has an `error` property at the top level (matches `ApiError`). |
|
|
202
|
+
| `assert_error_code_status_consistency` | The same `z.literal()` error code never appears at two different HTTP statuses. |
|
|
203
|
+
| `assert_404_schemas_use_specific_errors` | Routes with params declaring 404 must use `z.literal()` or `z.enum()`, not generic `z.string()`. |
|
|
204
|
+
|
|
205
|
+
Policy invariants (configurable, sensible defaults):
|
|
206
|
+
|
|
207
|
+
| Assertion | Checks |
|
|
208
|
+
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
209
|
+
| `assert_sensitive_routes_rate_limited` | Routes matching `sensitive_route_patterns` (default: `/login`, `/password`, `/bootstrap`, `/tokens/create`) declare rate limiting or a 429 schema. |
|
|
210
|
+
| `assert_no_unexpected_public_mutations` | Public mutation routes must be in `public_mutation_allowlist`. |
|
|
211
|
+
| `assert_mutation_routes_use_post` | Routes with input schemas must not be GET (bypasses browser GET idempotency assumptions). |
|
|
212
|
+
| `assert_keeper_routes_under_prefix` | Keeper routes must be under `keeper_route_prefixes` (default `['/api/']`). |
|
|
213
|
+
|
|
214
|
+
Tightness audit:
|
|
215
|
+
|
|
216
|
+
- `audit_error_schema_tightness(surface) => Array<ErrorSchemaAuditEntry>` —
|
|
217
|
+
classifies every route × status combination as `'literal' | 'enum' | 'generic'`.
|
|
218
|
+
- `assert_error_schema_tightness(surface, options?)` — fails routes below a
|
|
219
|
+
threshold (`min_specificity`, default `'enum'`) with `allowlist` + `ignore_statuses` escape hatches.
|
|
220
|
+
- `DEFAULT_ERROR_SCHEMA_TIGHTNESS` — `{ignore_statuses: [401, 403, 429]}`
|
|
221
|
+
(middleware-injected codes that commonly use generic schemas). Applied
|
|
222
|
+
by `describe_standard_attack_surface_tests` when `error_schema_tightness`
|
|
223
|
+
is omitted; pass an override config or `null` to opt out.
|
|
224
|
+
|
|
225
|
+
Aggregate runners (called by the standard attack-surface suite):
|
|
226
|
+
|
|
227
|
+
- `assert_surface_invariants(surface)` — runs all structural assertions.
|
|
228
|
+
- `assert_surface_security_policy(surface, options?)` — runs all policy assertions.
|
|
229
|
+
|
|
230
|
+
### `error_coverage.ts` — reachability tracking
|
|
231
|
+
|
|
232
|
+
`ErrorCoverageCollector` tracks which declared error paths get exercised.
|
|
233
|
+
Observations live in a `Set<string>` keyed by `"METHOD /spec-path:STATUS"` or
|
|
234
|
+
`"METHOD /spec-path:STATUS:CODE"` — the two shapes coexist and a
|
|
235
|
+
status-only observation satisfies the "any-code" coverage rule for all
|
|
236
|
+
declared codes at that status.
|
|
237
|
+
|
|
238
|
+
Methods:
|
|
239
|
+
|
|
240
|
+
- `record(specs, method, path, status, code?)` — resolves concrete paths
|
|
241
|
+
back to spec templates (e.g. `/api/accounts/abc` → `/api/accounts/:id`).
|
|
242
|
+
- `assert_and_record(specs, method, path, response, code?)` — wraps
|
|
243
|
+
`assert_response_matches_spec` and auto-extracts `body.error` from the
|
|
244
|
+
JSON body via `response.clone()`. Pass an explicit `code` when the
|
|
245
|
+
body was already consumed.
|
|
246
|
+
- `uncovered(specs, options?)` — per-status rows for generic schemas,
|
|
247
|
+
per-code rows for `z.literal` / `z.enum` schemas.
|
|
248
|
+
|
|
249
|
+
Support functions:
|
|
250
|
+
|
|
251
|
+
- `extract_declared_error_codes(schema)` — reads `schema.shape.error`;
|
|
252
|
+
returns the literal value(s) for `z.literal` / `z.enum`, `null`
|
|
253
|
+
otherwise.
|
|
254
|
+
- `assert_error_coverage(collector, specs, options?)` — logs
|
|
255
|
+
`[error coverage] covered/total (N.M%)` with uncovered list; fails
|
|
256
|
+
when `min_coverage > 0` and the ratio falls below.
|
|
257
|
+
- `DEFAULT_INTEGRATION_ERROR_COVERAGE = 0.2` — conservative baseline
|
|
258
|
+
for the standard integration/admin suites; consumers tighten as
|
|
259
|
+
their own test coverage matures.
|
|
260
|
+
|
|
261
|
+
### `schema_generators.ts` — valid-value generation
|
|
262
|
+
|
|
263
|
+
Walks Zod schemas to generate valid values for adversarial/round-trip tests.
|
|
264
|
+
|
|
265
|
+
- `detect_format(field_schema)` — reads `format` / `pattern` from the
|
|
266
|
+
JSON Schema representation.
|
|
267
|
+
- `generate_valid_value(field, field_schema)` — base-type switch
|
|
268
|
+
producing a valid sample (UUIDs → nil UUID, strings → `'xxxxxxxxxx'`,
|
|
269
|
+
numbers → `1`, objects → recurse, enums → first entry, etc.).
|
|
270
|
+
Falls back through `/` + URL prefixes if a branded-string refinement
|
|
271
|
+
rejects the plain base.
|
|
272
|
+
- `resolve_valid_path(path, params_schema?)` — swaps `:param` for
|
|
273
|
+
valid-format values (nil UUID for UUID params, `test_param` otherwise).
|
|
274
|
+
- `generate_valid_body(input_schema) => Record<string, unknown> | undefined` —
|
|
275
|
+
builds a body that satisfies the input schema. Throws with Zod
|
|
276
|
+
`issues` if the generated body fails validation — surfaces broken
|
|
277
|
+
generation logic with a descriptive error rather than a confusing 400
|
|
278
|
+
downstream.
|
|
279
|
+
|
|
280
|
+
### `integration_helpers.ts` — route lookup + body checks
|
|
281
|
+
|
|
282
|
+
| Helper | Role |
|
|
283
|
+
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
284
|
+
| `find_route_spec(specs, method, path)` | Exact match then parameterized match (`:foo` matches any segment). |
|
|
285
|
+
| `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 (post-RPC-migration, only login/logout/password/verify/signup/bootstrap remain). |
|
|
286
|
+
| `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. |
|
|
287
|
+
| `create_expired_test_cookie(keyring, session_options)` | Validly signed cookie with `expires_at` in 1970. |
|
|
288
|
+
| `check_error_response_fields(body)` | Returns the list of fields outside `KNOWN_SAFE_ERROR_FIELDS` (`error`, `issues`, `required_role`, `retry_after`, `credential_type`, `has_references`, `ok`). |
|
|
289
|
+
| `assert_no_error_info_leakage(body, context)` | Rejects field-name patterns (`stack`, `trace`, `sql`, …) + value patterns (`node_modules`, stack-like `at …`, `.ts:NN`). |
|
|
290
|
+
| `assert_rate_limit_retry_after_header(response, body)` | `Retry-After` numeric header equals `Math.ceil(body.retry_after)`. |
|
|
291
|
+
| `SENSITIVE_FIELD_BLOCKLIST` | `['password_hash', 'token_hash']` — never in any response body. |
|
|
292
|
+
| `ADMIN_ONLY_FIELD_BLOCKLIST` | `['updated_by', 'created_by']` — never in non-admin response bodies. |
|
|
293
|
+
| `collect_json_keys_recursive(value)` | Deep walk; returns `Set<string>` of every key at every nesting depth. |
|
|
294
|
+
| `assert_no_sensitive_fields_in_json(body, blocklist, context)` | Rejects any key in the blocklist at any depth. |
|
|
295
|
+
| `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. |
|
|
296
|
+
|
|
297
|
+
## Attack surface suites
|
|
298
|
+
|
|
299
|
+
### `attack_surface.ts` — `describe_standard_attack_surface_tests`
|
|
300
|
+
|
|
301
|
+
Single-call bundle of 5 top-level groups (10 named tests + every
|
|
302
|
+
adversarial case per route):
|
|
303
|
+
|
|
304
|
+
1. **attack surface snapshot** — `matches committed snapshot`, `is deterministic`.
|
|
305
|
+
2. **attack surface structure** — `only expected public routes`, `full middleware stack on API routes`, `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`).
|
|
306
|
+
3. **adversarial HTTP auth enforcement** — `unauthenticated → 401`, `wrong role → 403` × roles, `authenticated without role → 403`, `keeper routes reject session credential → 403`, `correct auth passes guard`.
|
|
307
|
+
4. **adversarial input validation** — delegated to `describe_adversarial_input`.
|
|
308
|
+
5. **adversarial 404 response validation** — delegated to `describe_adversarial_404`.
|
|
309
|
+
|
|
310
|
+
Options: `{build: () => AppSurfaceSpec, snapshot_path, expected_public_routes, expected_api_middleware, roles, api_path_prefix?, security_policy?, error_schema_tightness?}`.
|
|
311
|
+
|
|
312
|
+
Also exported: `describe_adversarial_auth(options)` (groups 3 on its own)
|
|
313
|
+
and `build_error_schema_lookup(specs, middleware_specs?)` (pre-built
|
|
314
|
+
`Map<string, RouteErrorSchemas>` for per-response validation).
|
|
315
|
+
|
|
316
|
+
### `adversarial_input.ts` — schema-walk payload generation
|
|
317
|
+
|
|
318
|
+
`describe_adversarial_input({build, roles})` — fires input body / params /
|
|
319
|
+
query validation failures at every route with correct-auth credentials
|
|
320
|
+
so validation middleware is actually exercised (not short-circuited by
|
|
321
|
+
401). All cases expect 400 with one of `ERROR_INVALID_REQUEST_BODY` /
|
|
322
|
+
`_INVALID_JSON_BODY` / `_INVALID_ROUTE_PARAMS` / `_INVALID_QUERY_PARAMS`.
|
|
323
|
+
|
|
324
|
+
Exported generators:
|
|
325
|
+
|
|
326
|
+
- `generate_input_test_cases(input_schema)` — whole-body structural
|
|
327
|
+
(non-object, extra key when `strictObject`), missing required fields,
|
|
328
|
+
one wrong-type per field, null for required non-nullable, one format
|
|
329
|
+
violation per constrained field, numeric/array/string boundary cases
|
|
330
|
+
via JSON Schema introspection.
|
|
331
|
+
- `generate_params_test_cases(params_schema)` — format violations only
|
|
332
|
+
(unconstrained string params accept anything).
|
|
333
|
+
- `generate_query_test_cases(query_schema)` — missing required +
|
|
334
|
+
format violations.
|
|
335
|
+
|
|
336
|
+
GET-with-input routes hit the RPC `?params=` query convention; invalid-
|
|
337
|
+
JSON arrays there collapse to `ERROR_INVALID_REQUEST_BODY` (schema
|
|
338
|
+
failure) rather than `ERROR_INVALID_JSON_BODY`.
|
|
339
|
+
|
|
340
|
+
### `adversarial_404.ts` — 404 schema conformance
|
|
341
|
+
|
|
342
|
+
`describe_adversarial_404({build, roles})` — for every route with
|
|
343
|
+
`params` + 404 in `error_schemas` + an extractable error code
|
|
344
|
+
(`z.literal` or first `z.enum`), replaces the handler with a stub
|
|
345
|
+
returning `{error: <code>}`, fires with nil-UUID params, asserts 404 +
|
|
346
|
+
body matches the declared 404 Zod schema. No DB needed.
|
|
347
|
+
|
|
348
|
+
### `adversarial_headers.ts` — header injection suite
|
|
349
|
+
|
|
350
|
+
`describe_standard_adversarial_headers(suite_name, options, allowed_origin, extra_cases?)`
|
|
351
|
+
— 7 standard cases:
|
|
352
|
+
|
|
353
|
+
1. bearer + rogue Origin → 403 `ERROR_FORBIDDEN_ORIGIN`
|
|
354
|
+
2. bearer + allowed Origin → bearer silently discarded (browser context)
|
|
355
|
+
3. no auth headers → passes through
|
|
356
|
+
4. bearer + empty Origin → 403 `ERROR_FORBIDDEN_ORIGIN` (defense-in-depth)
|
|
357
|
+
5. lowercase `bearer` scheme → RFC 7235 §2.1 soft-fail
|
|
358
|
+
6. bearer + rogue Referer → 403 `ERROR_FORBIDDEN_REFERER`
|
|
359
|
+
7. bearer + allowed Referer → bearer silently discarded
|
|
360
|
+
|
|
361
|
+
Each case declares `validate_expectation: 'called' | 'not_called'` so the
|
|
362
|
+
suite asserts that short-circuit middleware actually fires before token
|
|
363
|
+
validation. Extra cases append to the standard list.
|
|
364
|
+
|
|
365
|
+
## Middleware stack — `middleware.ts`
|
|
366
|
+
|
|
367
|
+
Module-level `vi.mock()` for the four query modules bearer auth touches:
|
|
368
|
+
`api_token_queries`, `account_queries`, `permit_queries`. Because
|
|
369
|
+
`vi.mock()` is hoisted, these run before any imports resolve — so any
|
|
370
|
+
test file that imports from `middleware.ts` gets these mocks globally.
|
|
371
|
+
Pair with `vi.restoreAllMocks()` in `afterEach` when mixing into
|
|
372
|
+
`.db.test.ts` files (see DB test caveat below).
|
|
373
|
+
|
|
374
|
+
| Helper | Role |
|
|
375
|
+
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
376
|
+
| `BearerAuthTestOptions`, `BearerAuthTestCase` | Test-case table shape for the bearer auth runner. |
|
|
377
|
+
| `create_bearer_auth_mocks(tc)` | Configures the module-level mocks per test case; returns spy references. |
|
|
378
|
+
| `TEST_CLIENT_IP = '127.0.0.1'` | IP set by the proxy stub in `create_bearer_auth_test_app`. |
|
|
379
|
+
| `create_bearer_auth_test_app(tc, ip_rate_limiter?)` | Hono app with bearer middleware + echo route at `/api/test` returning `{ok, has_context, credential_type, account_id, actor_id, permit_count, api_token_id}`. |
|
|
380
|
+
| `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. |
|
|
381
|
+
| `TEST_MIDDLEWARE_PATH = '/api/test'` | Path used by the echo route in the stack factory. |
|
|
382
|
+
| `create_test_middleware_stack_app(options?)` | Real proxy + origin + bearer middleware for integration-shape testing. Echo route returns `{ok, client_ip, has_context}`. |
|
|
383
|
+
|
|
384
|
+
The echo route under `create_bearer_auth_test_app` deliberately surfaces
|
|
385
|
+
every middleware-written context variable (`REQUEST_CONTEXT_KEY`,
|
|
386
|
+
`CREDENTIAL_TYPE_KEY`, `AUTH_API_TOKEN_ID_KEY`). When public auth surface
|
|
387
|
+
gains a new context variable, header, or field, update this echo
|
|
388
|
+
alongside the assertions in `src/test/auth/*.test.ts` — the two move
|
|
389
|
+
together.
|
|
390
|
+
|
|
391
|
+
## Round-trip suites
|
|
392
|
+
|
|
393
|
+
### `round_trip.ts` — `describe_round_trip_validation`
|
|
394
|
+
|
|
395
|
+
For every route spec, fires a valid request with matching auth and
|
|
396
|
+
validates the response against declared schemas. DB-backed via
|
|
397
|
+
`create_test_app`. Per-route test (`test.each`) — one line per route
|
|
398
|
+
in the vitest output.
|
|
399
|
+
|
|
400
|
+
Options: `{session_options, create_route_specs, app_options?, db_factories?, skip_routes?, input_overrides?}`.
|
|
401
|
+
`input_overrides` is a `Map<"METHOD /path", body>` — override generated
|
|
402
|
+
bodies for routes whose input schema can't round-trip cleanly (e.g.
|
|
403
|
+
fields that must reference DB state).
|
|
404
|
+
|
|
405
|
+
SSE routes are skipped by Content-Type sniff; `describe_sse_route_tests`
|
|
406
|
+
picks them up separately.
|
|
407
|
+
|
|
408
|
+
### `rpc_round_trip.ts` — `describe_rpc_round_trip_tests`
|
|
409
|
+
|
|
410
|
+
DB-backed round-trip for RPC: one POST test for all methods, one GET
|
|
411
|
+
test for `side_effects: false` methods. Successful responses validate
|
|
412
|
+
against `action.spec.output`; error responses validate as well-formed
|
|
413
|
+
JSON-RPC error envelopes. Required: `{session_options, create_route_specs, rpc_endpoints, ...}`.
|
|
414
|
+
The admin RPC auth test picks a session-based identity (`authed` /
|
|
415
|
+
`admin` / bootstrapped keeper) based on `method.auth`; keeper uses the
|
|
416
|
+
daemon token.
|
|
417
|
+
|
|
418
|
+
### `sse_round_trip.ts` — `describe_sse_route_tests`
|
|
419
|
+
|
|
420
|
+
Per SSE route: open stream with matching auth, assert the
|
|
421
|
+
`SSE_CONNECTED_COMMENT` comment, fire a consumer-supplied `trigger()`,
|
|
422
|
+
validate the next `data:` frame as `{method, params}` against declared
|
|
423
|
+
`EventSpec`s, then (by default) fire `POST /api/account/sessions/revoke-all`
|
|
424
|
+
and assert the stream closes within 2s.
|
|
425
|
+
|
|
426
|
+
`SseRouteTestSpec` per route: `{path, trigger, event_specs?, assert_closes_on_revoke?}`.
|
|
427
|
+
Pass `on_audit_event` on the suite options to wire a close-on-revoke
|
|
428
|
+
guard (e.g. via `create_sse_auth_guard`) for consumer SSE registries —
|
|
429
|
+
without it, the revoke assertion hangs because the guard never fires.
|
|
430
|
+
|
|
431
|
+
Frame reader (`create_sse_frame_reader`) is internal but handles
|
|
432
|
+
`\n\n` framing, a 2s per-read timeout (prevents vitest hangs), and
|
|
433
|
+
`wait_for_close` for the revocation check.
|
|
434
|
+
|
|
435
|
+
### `ws_round_trip.ts` — WebSocket harness (non-HTTP)
|
|
436
|
+
|
|
437
|
+
In-process test driver for `register_action_ws`. Consumers pass specs +
|
|
438
|
+
handlers, receive `{transport, connect()}` back. The full dispatch path
|
|
439
|
+
is exercised (per-action auth, input validation, `ctx.notify`,
|
|
440
|
+
broadcast via `BackendWebsocketTransport`, close-on-revoke), but Hono's
|
|
441
|
+
wire upgrade is skipped (the Node test runtime has no
|
|
442
|
+
`@hono/node-ws` adapter).
|
|
443
|
+
|
|
444
|
+
Three layers:
|
|
445
|
+
|
|
446
|
+
1. **Primitives** — `create_fake_ws()`, `create_fake_hono_context(opts)`,
|
|
447
|
+
`create_stub_upgrade()`, `MinimalActionEnvironment`,
|
|
448
|
+
`dispatch_ws_message(on_message, event, ws)`.
|
|
449
|
+
2. **Harness** — `create_ws_test_harness<TCtx>({actions, extend_context?, 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.
|
|
450
|
+
3. **Round-trip helpers** — `is_notification(method)`,
|
|
451
|
+
`is_notification_with<P>(method, match)` (type-guard combinator —
|
|
452
|
+
narrows `wait_for` return type), `is_response_for(id)`.
|
|
453
|
+
`JsonrpcNotificationFrame<P>` / `JsonrpcSuccessResponseFrame<R>` /
|
|
454
|
+
`JsonrpcErrorResponseFrame<D>` — typed wire-frame shapes distinct
|
|
455
|
+
from the runtime Zod schemas in `http/jsonrpc.ts` (generic over
|
|
456
|
+
`params` / `result` / `data` so tests narrow without casts).
|
|
457
|
+
`build_broadcast_api<TApi>({harness, specs})` — wires a typed
|
|
458
|
+
broadcast API against the harness transport.
|
|
459
|
+
|
|
460
|
+
`MockWsClient`: `{send, request<R>, close, messages, wait_for}`.
|
|
461
|
+
`request` throws with code + message + data on error frames (so
|
|
462
|
+
asserting `result.foo` on a failed request surfaces the real cause,
|
|
463
|
+
not a `Cannot read property 'foo' of undefined`). `wait_for(predicate,
|
|
464
|
+
timeout_ms?)` checks already-received messages first, then waits for
|
|
465
|
+
new arrivals (default 1000ms); drops the waiter on timeout so the
|
|
466
|
+
`waiters` array doesn't grow.
|
|
467
|
+
|
|
468
|
+
`keeper_identity()` — convenience for `{credential_type: 'daemon_token', roles: [ROLE_KEEPER]}`.
|
|
469
|
+
|
|
470
|
+
## Data exposure + rate limiting
|
|
471
|
+
|
|
472
|
+
### `data_exposure.ts` — `describe_data_exposure_tests`
|
|
473
|
+
|
|
474
|
+
Six tests in two top-level groups:
|
|
475
|
+
|
|
476
|
+
1. **schema-level** (3 tests, no DB) — walks JSON Schema representations:
|
|
477
|
+
- `no sensitive fields in any output schema` — `SENSITIVE_FIELD_BLOCKLIST`
|
|
478
|
+
- `no admin-only fields in non-admin output schemas` — `ADMIN_ONLY_FIELD_BLOCKLIST`
|
|
479
|
+
- `no sensitive fields in any error schema`
|
|
480
|
+
2. **runtime** (3 tests, DB-backed via `create_test_app`):
|
|
481
|
+
- `unauthenticated error responses contain no sensitive fields`
|
|
482
|
+
- `admin routes return 403 for non-admin user` — cross-privilege check
|
|
483
|
+
- `all 2xx responses pass field blocklists` — GETs sorted before POSTs so data-returning routes fire before destructive ones (logout, revoke-all) invalidate sessions
|
|
484
|
+
|
|
485
|
+
Support functions: `collect_json_schema_property_names(schema)` (walks
|
|
486
|
+
`properties`/`items`/`allOf`/`anyOf`/`oneOf`/`additionalProperties`),
|
|
487
|
+
`assert_output_schemas_no_sensitive_fields(surface, fields?)`,
|
|
488
|
+
`assert_non_admin_schemas_no_admin_fields(surface, fields?)`.
|
|
489
|
+
|
|
490
|
+
Options: `{build, session_options, create_route_specs, sensitive_fields?, admin_only_fields?, app_options?, db_factories?, skip_routes?}`.
|
|
491
|
+
|
|
492
|
+
### `rate_limiting.ts` — `describe_rate_limiting_tests`
|
|
493
|
+
|
|
494
|
+
Three test groups:
|
|
495
|
+
|
|
496
|
+
1. IP rate limiting on login — fires `max_attempts + 1` requests; last one should be 429 with `RateLimitError` body + valid `Retry-After` header.
|
|
497
|
+
2. Per-account rate limiting on login — same username exhausts the bucket; a different username is not blocked.
|
|
498
|
+
3. Bearer auth IP rate limiting — invalid bearer tokens exhaust the IP bucket via the `account_verify` RPC method.
|
|
499
|
+
|
|
500
|
+
Each group asserts its required route exists with a descriptive
|
|
501
|
+
message. Creates a tight rate limiter (default `max_attempts: 2`,
|
|
502
|
+
`window_ms: 60_000`) per test and disposes it in `finally`.
|
|
503
|
+
|
|
504
|
+
Options: `{session_options, create_route_specs, app_options?, db_factories?, max_attempts?}`.
|
|
505
|
+
|
|
506
|
+
## Integration suites
|
|
507
|
+
|
|
508
|
+
### `integration.ts` — `describe_standard_integration_tests`
|
|
509
|
+
|
|
510
|
+
Exercises the full stack against real PGlite + auth middleware + session
|
|
511
|
+
cookies + bearer tokens. The suite has ~19 `describe` blocks grouped under
|
|
512
|
+
these thematic areas:
|
|
513
|
+
|
|
514
|
+
1. Login/logout lifecycle
|
|
515
|
+
2. Login response body (strict schema)
|
|
516
|
+
3. Cookie attributes (HttpOnly, Secure-in-prod, SameSite)
|
|
517
|
+
4. Session security (tampering, forgery)
|
|
518
|
+
5. Session revocation (self + revoke-all)
|
|
519
|
+
6. Password change (revokes all sessions + API tokens)
|
|
520
|
+
7. Origin verification
|
|
521
|
+
8. Bearer auth + browser-context discard on mutations
|
|
522
|
+
9. Token revocation + cross-account isolation
|
|
523
|
+
10. Response body schema validation + error-response information leakage
|
|
524
|
+
11. Signup invite edge cases + rate-limiting smoke + expired credential rejection + error-coverage breadth
|
|
525
|
+
|
|
526
|
+
An `ErrorCoverageCollector` runs across groups; `afterAll` filters to
|
|
527
|
+
auth-related routes (login/logout/verify/sessions/tokens/password/
|
|
528
|
+
signup/bootstrap) and asserts `DEFAULT_INTEGRATION_ERROR_COVERAGE`
|
|
529
|
+
(20%). Consumer-specific routes aren't exercised here — they don't
|
|
530
|
+
count against the baseline.
|
|
531
|
+
|
|
532
|
+
Options: `{session_options, create_route_specs, app_options?, db_factories?}`.
|
|
533
|
+
|
|
534
|
+
### `admin_integration.ts` — `describe_standard_admin_integration_tests`
|
|
535
|
+
|
|
536
|
+
7 test groups covering admin surface: account listing, permit grant
|
|
537
|
+
lifecycle (via `permit_offer_create` + `permit_revoke` RPC flows —
|
|
538
|
+
**not** REST; see `../auth/CLAUDE.md` for `permit_offer_action_specs.ts` + `permit_offer_actions.ts`), session / token management, audit log reads (RPC),
|
|
539
|
+
admin-to-admin isolation, error coverage, response schema validation.
|
|
540
|
+
|
|
541
|
+
Required options: `{session_options, create_route_specs, roles: RoleSchemaResult, rpc_endpoints: Array<RpcEndpointSpec>, admin_prefix?, app_options?, db_factories?}`.
|
|
542
|
+
|
|
543
|
+
**Hard-fails via `require_rpc_endpoint_path(options.rpc_endpoints)`** at
|
|
544
|
+
setup time when `rpc_endpoints` is empty — admin permit grant/revoke
|
|
545
|
+
plus session/token revoke-all plus audit-log list/history are all
|
|
546
|
+
RPC-only since the 2026-04-22 migration. A confusing test failure
|
|
547
|
+
mid-suite is worse than a clear setup error.
|
|
548
|
+
|
|
549
|
+
Error-coverage scope is narrowed to the REST suffixes still on the
|
|
550
|
+
admin surface (`/sessions`, `/audit-log/stream`); the RPC surface is
|
|
551
|
+
covered by `describe_rpc_round_trip_tests`.
|
|
552
|
+
|
|
553
|
+
### `audit_completeness.ts` — `describe_audit_completeness_tests`
|
|
554
|
+
|
|
555
|
+
Verifies every auth mutation produces the expected `audit_log` row by
|
|
556
|
+
querying the table after each request. Uses the real middleware stack.
|
|
557
|
+
Same `rpc_endpoints` hard-fail as the admin suite — the mutation-audit
|
|
558
|
+
tests drive permit flow, session/token revoke-all, and invite
|
|
559
|
+
create/delete through `permit_offer_create_action_spec` /
|
|
560
|
+
`permit_revoke_action_spec` / `admin_session_revoke_all_action_spec` /
|
|
561
|
+
`admin_token_revoke_all_action_spec` / `app_settings_update_action_spec` /
|
|
562
|
+
`invite_create_action_spec` / `invite_delete_action_spec`.
|
|
563
|
+
|
|
564
|
+
Bootstrap audit logging is excluded because `create_test_app` doesn't
|
|
565
|
+
provide the filesystem token state; covered separately in
|
|
566
|
+
`bootstrap_account.db.test.ts`.
|
|
567
|
+
|
|
568
|
+
### `standard.ts` — `describe_standard_tests`
|
|
569
|
+
|
|
570
|
+
Convenience wrapper: always runs `describe_standard_integration_tests`;
|
|
571
|
+
runs `describe_standard_admin_integration_tests` only when `roles` is
|
|
572
|
+
provided. `rpc_endpoints` is a required field on `StandardTestOptions`
|
|
573
|
+
— the admin suite's requirement is enforced at the type level, so a
|
|
574
|
+
missing `rpc_endpoints` is a compile error rather than a runtime throw.
|
|
575
|
+
|
|
576
|
+
## RPC helpers
|
|
577
|
+
|
|
578
|
+
### `rpc_helpers.ts` — envelope construction + response assertions
|
|
579
|
+
|
|
580
|
+
Shared by `rpc_attack_surface.ts`, `rpc_round_trip.ts`, the admin and
|
|
581
|
+
audit integration suites, and consumer tests that hit RPC methods
|
|
582
|
+
directly.
|
|
583
|
+
|
|
584
|
+
Request builders:
|
|
585
|
+
|
|
586
|
+
- `create_rpc_post_init(method, params?, id?)` — `RequestInit` with
|
|
587
|
+
JSON-RPC envelope body. `params === undefined || params === null` →
|
|
588
|
+
envelope has no `params` field (JSON-RPC doesn't accept
|
|
589
|
+
`"params": null`).
|
|
590
|
+
- `create_rpc_get_url(endpoint_path, method, params?, id?)` — GET URL
|
|
591
|
+
with `?method=&id=¶ms=<JSON>`.
|
|
592
|
+
|
|
593
|
+
Response assertions:
|
|
594
|
+
|
|
595
|
+
- `assert_jsonrpc_error_response(body, expected_code?)` — validates
|
|
596
|
+
`JsonrpcErrorResponse`; optional code check.
|
|
597
|
+
- `assert_jsonrpc_success_response(body, output_schema?)` — validates
|
|
598
|
+
`JsonrpcResponse`; optional `output_schema.safeParse(result)`.
|
|
599
|
+
|
|
600
|
+
One-shot transport:
|
|
601
|
+
|
|
602
|
+
- `RpcTestTransport = (url, init) => Promise<Response>` — duck type
|
|
603
|
+
`Hono.request` already satisfies.
|
|
604
|
+
- `http_transport(app)` — adapter for anything with a `request()` method.
|
|
605
|
+
- `RpcCallResult` — discriminated `{ok: true, status, result}` / `{ok: false, status, error: {code, message, data?}}`.
|
|
606
|
+
- `RpcCallArgs` — `{app, path, method, params?, headers?, id?, verb?}`. `verb` defaults to `'POST'`; use `'GET'` for `side_effects: false` methods.
|
|
607
|
+
- `rpc_call(args)` — merges `RPC_CALL_DEFAULT_HEADERS` (`host: 'localhost'`, `origin: 'http://localhost:5173'`, `Content-Type: 'application/json'`) under caller headers. Envelope-shape violations throw; JSON-RPC errors return `{ok: false, error}` so callers assert on `error.code` / `error.data.reason`.
|
|
608
|
+
- `rpc_call_typed<T>(args, output_schema)` — parses the success `result` through the schema; throws on envelope failure, error response, or schema mismatch. Use `rpc_call` when the test needs to assert on error shapes.
|
|
609
|
+
- `rpc_call_for_spec<TSpec>(args)` — spec-bound variant: takes `{..., spec, params}` in place of `{..., method, params}`. `params` is typed from `spec.input` and the success `result` is typed from `spec.output` (runtime-validated, same contract as `rpc_call_typed`). Error branch stays untyped (JSON-RPC `error.data` shapes vary per call site). Use at happy-path + denial-path call sites; fall back to `rpc_call` for adversarial tests that send deliberately-malformed params.
|
|
610
|
+
|
|
611
|
+
Registry lookups:
|
|
612
|
+
|
|
613
|
+
- `find_rpc_action(rpc_endpoints, method)` — endpoint path + `RpcAction` source.
|
|
614
|
+
- `find_rpc_method(rpc_endpoints, method)` — surface-shape lookup over `AppSurfaceRpcEndpoint[]` (generated by `generate_app_surface`).
|
|
615
|
+
- `require_rpc_endpoint_path(rpc_endpoints)` — returns the single endpoint path; throws descriptively on zero or multiple endpoints. Used by the admin/audit suites to hard-fail at setup.
|
|
616
|
+
|
|
617
|
+
### `rpc_attack_surface.ts` — `describe_rpc_attack_surface_tests`
|
|
618
|
+
|
|
619
|
+
3 test groups for JSON-RPC endpoints:
|
|
620
|
+
|
|
621
|
+
1. **RPC auth enforcement** — per-endpoint, per-method:
|
|
622
|
+
- unauthenticated → `unauthenticated` (code -32001)
|
|
623
|
+
- wrong role → `forbidden` (-32002)
|
|
624
|
+
- authenticated without role → `forbidden`
|
|
625
|
+
- **keeper rejects non-daemon credentials** — session and api_token credentials are rejected even when the account has the keeper role (only `daemon_token` passes). Mirrors `require_keeper`'s two-part guard (see `../auth/CLAUDE.md` for `require_keeper.ts`).
|
|
626
|
+
- correct auth passes (not 401/403)
|
|
627
|
+
- GET unauthenticated for `side_effects: false` reads
|
|
628
|
+
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`.
|
|
629
|
+
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`.
|
|
630
|
+
|
|
631
|
+
Skips silently when `surface.rpc_endpoints` is empty. Uses stub
|
|
632
|
+
deps — no DB needed.
|
|
633
|
+
|
|
634
|
+
Options: `{build: () => AppSurfaceSpec, roles: Array<string>}`.
|
|
635
|
+
|
|
636
|
+
## Cross-cutting conventions
|
|
637
|
+
|
|
638
|
+
- **`assert` from vitest, not `expect`.** Project-wide convention
|
|
639
|
+
(mirrored in `src/test/CLAUDE.md`). Use `assert_rejects` from
|
|
640
|
+
`@fuzdev/fuz_util/testing.js` for async rejection assertions.
|
|
641
|
+
- **`.db.test.ts` suffix** for any test file that instantiates a `Db`
|
|
642
|
+
(directly or via `create_test_app`, `create_describe_db`,
|
|
643
|
+
`create_pglite_factory`). The suffix opts the file into the `db`
|
|
644
|
+
vitest project (`isolate: false`, `fileParallelism: false`) so the
|
|
645
|
+
PGlite WASM cache is shared across every DB test file.
|
|
646
|
+
- **`await_pending_effects: true`** is set by `create_test_app`.
|
|
647
|
+
Fire-and-forget effects (audit logs, session touches, WS fan-out via
|
|
648
|
+
`emit_after_commit`) resolve before the response returns, so tests
|
|
649
|
+
can assert on side effects inline without manual flushing.
|
|
650
|
+
- **Avoid `vi.mock()` inside `.db.test.ts`.** With `isolate: false`,
|
|
651
|
+
module-level mocks leak across files. When a mock is unavoidable
|
|
652
|
+
(e.g. `middleware.ts` uses them module-level for bearer auth tests),
|
|
653
|
+
always pair with `vi.restoreAllMocks()` in `afterEach` to contain
|
|
654
|
+
the blast radius.
|
|
655
|
+
- **Deep-path imports only.** `testing/` follows the package
|
|
656
|
+
convention — import from the canonical module (`./db.js`,
|
|
657
|
+
`./rpc_helpers.js`, etc.), never a barrel. fuz_app's `dist/` doesn't
|
|
658
|
+
ship one.
|
|
659
|
+
- **DI via small `*Deps` interfaces.** Stub factories here accept the
|
|
660
|
+
same narrow `*Deps` contracts production code uses — never
|
|
661
|
+
`Pick<GodType, ...>`. New helpers that need env/fs/logger access
|
|
662
|
+
should take `EnvDeps` / `FsReadDeps` / `Logger` from
|
|
663
|
+
`runtime/deps.ts` or `@fuzdev/fuz_util/log.js`.
|
|
664
|
+
- **Keep the shared echo routes in sync with public surface.** When
|
|
665
|
+
middleware or public API gains a new context variable, header, or
|
|
666
|
+
field, update the echo in `middleware.ts`
|
|
667
|
+
(`create_bearer_auth_test_app`, `create_test_middleware_stack_app`)
|
|
668
|
+
alongside the assertions in `src/test/auth/*.test.ts`. The two move
|
|
669
|
+
together — drift between them shows up as a missed assertion, not a
|
|
670
|
+
test failure.
|