@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.
Files changed (223) hide show
  1. package/dist/actions/CLAUDE.md +510 -946
  2. package/dist/actions/action_codegen.d.ts +1 -1
  3. package/dist/actions/action_codegen.js +1 -1
  4. package/dist/actions/action_event_data.d.ts +1 -1
  5. package/dist/actions/broadcast_api.d.ts +1 -1
  6. package/dist/actions/broadcast_api.js +1 -1
  7. package/dist/actions/cancel.d.ts +2 -2
  8. package/dist/actions/cancel.js +3 -3
  9. package/dist/actions/connection_closer.d.ts +1 -4
  10. package/dist/actions/connection_closer.d.ts.map +1 -1
  11. package/dist/actions/connection_closer.js +1 -4
  12. package/dist/actions/register_action_ws.d.ts +2 -2
  13. package/dist/actions/register_ws_endpoint.d.ts +1 -1
  14. package/dist/actions/transports_ws_auth_guard.d.ts +1 -2
  15. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  16. package/dist/actions/transports_ws_auth_guard.js +1 -2
  17. package/dist/auth/CLAUDE.md +570 -1871
  18. package/dist/auth/account_schema.d.ts +1 -1
  19. package/dist/auth/account_schema.d.ts.map +1 -1
  20. package/dist/auth/api_token_queries.js +1 -1
  21. package/dist/auth/audit_log_ddl.d.ts +1 -1
  22. package/dist/auth/audit_log_ddl.d.ts.map +1 -1
  23. package/dist/auth/audit_log_ddl.js +1 -1
  24. package/dist/auth/audit_log_schema.js +2 -2
  25. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  26. package/dist/auth/bootstrap_account.js +1 -5
  27. package/dist/auth/bootstrap_routes.d.ts +7 -1
  28. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  29. package/dist/auth/bootstrap_routes.js +15 -11
  30. package/dist/auth/daemon_token_middleware.d.ts +15 -5
  31. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  32. package/dist/auth/daemon_token_middleware.js +24 -15
  33. package/dist/auth/invite_queries.d.ts +17 -7
  34. package/dist/auth/invite_queries.d.ts.map +1 -1
  35. package/dist/auth/invite_queries.js +19 -8
  36. package/dist/auth/keyring.d.ts +6 -6
  37. package/dist/auth/keyring.js +8 -8
  38. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  39. package/dist/auth/role_grant_offer_actions.js +4 -2
  40. package/dist/auth/signup_routes.d.ts +47 -1
  41. package/dist/auth/signup_routes.d.ts.map +1 -1
  42. package/dist/auth/signup_routes.js +103 -52
  43. package/dist/db/create_db.d.ts.map +1 -1
  44. package/dist/db/create_db.js +13 -0
  45. package/dist/dev/setup.d.ts +2 -2
  46. package/dist/dev/setup.js +3 -3
  47. package/dist/env/resolve.d.ts +44 -7
  48. package/dist/env/resolve.d.ts.map +1 -1
  49. package/dist/env/resolve.js +94 -27
  50. package/dist/http/CLAUDE.md +243 -522
  51. package/dist/http/error_schemas.d.ts +0 -4
  52. package/dist/http/error_schemas.d.ts.map +1 -1
  53. package/dist/http/error_schemas.js +0 -4
  54. package/dist/http/ip_canonical.d.ts +5 -4
  55. package/dist/http/ip_canonical.d.ts.map +1 -1
  56. package/dist/http/ip_canonical.js +8 -4
  57. package/dist/http/jsonrpc.d.ts +23 -7
  58. package/dist/http/jsonrpc.d.ts.map +1 -1
  59. package/dist/http/jsonrpc.js +19 -3
  60. package/dist/http/origin.d.ts +1 -1
  61. package/dist/http/origin.js +1 -1
  62. package/dist/http/surface.d.ts +9 -2
  63. package/dist/http/surface.d.ts.map +1 -1
  64. package/dist/runtime/mock.d.ts +1 -1
  65. package/dist/runtime/mock.js +2 -2
  66. package/dist/server/app_server.d.ts +41 -10
  67. package/dist/server/app_server.d.ts.map +1 -1
  68. package/dist/server/app_server.js +10 -4
  69. package/dist/server/env.d.ts +7 -7
  70. package/dist/server/env.d.ts.map +1 -1
  71. package/dist/server/env.js +14 -14
  72. package/dist/server/static.d.ts +4 -4
  73. package/dist/server/static.js +7 -7
  74. package/dist/testing/CLAUDE.md +740 -418
  75. package/dist/testing/admin_integration.d.ts +18 -23
  76. package/dist/testing/admin_integration.d.ts.map +1 -1
  77. package/dist/testing/admin_integration.js +230 -216
  78. package/dist/testing/app_server.d.ts +141 -39
  79. package/dist/testing/app_server.d.ts.map +1 -1
  80. package/dist/testing/app_server.js +157 -44
  81. package/dist/testing/audit_completeness.d.ts +25 -22
  82. package/dist/testing/audit_completeness.d.ts.map +1 -1
  83. package/dist/testing/audit_completeness.js +198 -159
  84. package/dist/testing/bootstrap_success.d.ts +28 -0
  85. package/dist/testing/bootstrap_success.d.ts.map +1 -0
  86. package/dist/testing/bootstrap_success.js +144 -0
  87. package/dist/testing/cross_backend/backend_config.d.ts +113 -0
  88. package/dist/testing/cross_backend/backend_config.d.ts.map +1 -0
  89. package/dist/testing/cross_backend/backend_config.js +1 -0
  90. package/dist/testing/cross_backend/bench/bench_report.d.ts +46 -0
  91. package/dist/testing/cross_backend/bench/bench_report.d.ts.map +1 -0
  92. package/dist/testing/cross_backend/bench/bench_report.js +83 -0
  93. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +44 -0
  94. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts.map +1 -0
  95. package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +38 -0
  96. package/dist/testing/cross_backend/bench/scenario.d.ts +57 -0
  97. package/dist/testing/cross_backend/bench/scenario.d.ts.map +1 -0
  98. package/dist/testing/cross_backend/bench/scenario.js +28 -0
  99. package/dist/testing/cross_backend/bootstrap_backend.d.ts +41 -0
  100. package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -0
  101. package/dist/testing/cross_backend/bootstrap_backend.js +34 -0
  102. package/dist/testing/cross_backend/build_test_backend_paths.d.ts +24 -0
  103. package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -0
  104. package/dist/testing/cross_backend/build_test_backend_paths.js +33 -0
  105. package/dist/testing/cross_backend/capabilities.d.ts +65 -0
  106. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
  107. package/dist/testing/cross_backend/capabilities.js +47 -0
  108. package/dist/testing/cross_backend/default_backend_configs.d.ts +122 -0
  109. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -0
  110. package/dist/testing/cross_backend/default_backend_configs.js +111 -0
  111. package/dist/testing/cross_backend/default_secrets.d.ts +40 -0
  112. package/dist/testing/cross_backend/default_secrets.d.ts.map +1 -0
  113. package/dist/testing/cross_backend/default_secrets.js +39 -0
  114. package/dist/testing/cross_backend/default_spine_surface.d.ts +64 -0
  115. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -0
  116. package/dist/testing/cross_backend/default_spine_surface.js +121 -0
  117. package/dist/testing/cross_backend/setup.d.ts +451 -0
  118. package/dist/testing/cross_backend/setup.d.ts.map +1 -0
  119. package/dist/testing/cross_backend/setup.js +581 -0
  120. package/dist/testing/cross_backend/spawn_backend.d.ts +58 -0
  121. package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -0
  122. package/dist/testing/cross_backend/spawn_backend.js +229 -0
  123. package/dist/testing/cross_backend/spine_stub_backend_config.d.ts +66 -0
  124. package/dist/testing/cross_backend/spine_stub_backend_config.d.ts.map +1 -0
  125. package/dist/testing/cross_backend/spine_stub_backend_config.js +49 -0
  126. package/dist/testing/cross_backend/sse_round_trip.d.ts +37 -0
  127. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -0
  128. package/dist/testing/cross_backend/sse_round_trip.js +137 -0
  129. package/dist/testing/cross_backend/standard.d.ts +96 -0
  130. package/dist/testing/cross_backend/standard.d.ts.map +1 -0
  131. package/dist/testing/cross_backend/standard.js +49 -0
  132. package/dist/testing/cross_backend/testing_reset_actions.d.ts +171 -0
  133. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -0
  134. package/dist/testing/cross_backend/testing_reset_actions.js +213 -0
  135. package/dist/testing/cross_backend/testing_server_bun.d.ts +5 -0
  136. package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -0
  137. package/dist/testing/cross_backend/testing_server_bun.js +59 -0
  138. package/dist/testing/cross_backend/testing_server_core.d.ts +140 -0
  139. package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -0
  140. package/dist/testing/cross_backend/testing_server_core.js +68 -0
  141. package/dist/testing/cross_backend/testing_server_deno.d.ts +5 -0
  142. package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -0
  143. package/dist/testing/cross_backend/testing_server_deno.js +37 -0
  144. package/dist/testing/cross_backend/testing_server_node.d.ts +5 -0
  145. package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -0
  146. package/dist/testing/cross_backend/testing_server_node.js +50 -0
  147. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +72 -0
  148. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -0
  149. package/dist/testing/cross_backend/ts_spine_backend_config.js +112 -0
  150. package/dist/testing/cross_backend/ws_round_trip.d.ts +35 -0
  151. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -0
  152. package/dist/testing/cross_backend/ws_round_trip.js +113 -0
  153. package/dist/testing/data_exposure.d.ts +11 -14
  154. package/dist/testing/data_exposure.d.ts.map +1 -1
  155. package/dist/testing/data_exposure.js +123 -146
  156. package/dist/testing/db_entities.d.ts +22 -1
  157. package/dist/testing/db_entities.d.ts.map +1 -1
  158. package/dist/testing/db_entities.js +24 -1
  159. package/dist/testing/integration.d.ts +56 -21
  160. package/dist/testing/integration.d.ts.map +1 -1
  161. package/dist/testing/integration.js +294 -319
  162. package/dist/testing/integration_helpers.d.ts +16 -6
  163. package/dist/testing/integration_helpers.d.ts.map +1 -1
  164. package/dist/testing/integration_helpers.js +7 -7
  165. package/dist/testing/mock_fs.d.ts.map +1 -1
  166. package/dist/testing/mock_fs.js +0 -2
  167. package/dist/testing/rate_limiting.d.ts.map +1 -1
  168. package/dist/testing/rate_limiting.js +9 -0
  169. package/dist/testing/role_grant_helpers.d.ts +31 -0
  170. package/dist/testing/role_grant_helpers.d.ts.map +1 -0
  171. package/dist/testing/role_grant_helpers.js +46 -0
  172. package/dist/testing/round_trip.d.ts +20 -16
  173. package/dist/testing/round_trip.d.ts.map +1 -1
  174. package/dist/testing/round_trip.js +61 -86
  175. package/dist/testing/rpc_helpers.d.ts +10 -4
  176. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  177. package/dist/testing/rpc_helpers.js +1 -1
  178. package/dist/testing/rpc_round_trip.d.ts +24 -21
  179. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  180. package/dist/testing/rpc_round_trip.js +87 -104
  181. package/dist/testing/schema_introspect.d.ts +106 -0
  182. package/dist/testing/schema_introspect.d.ts.map +1 -0
  183. package/dist/testing/schema_introspect.js +123 -0
  184. package/dist/testing/schema_parity.d.ts +144 -0
  185. package/dist/testing/schema_parity.d.ts.map +1 -0
  186. package/dist/testing/schema_parity.js +233 -0
  187. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  188. package/dist/testing/sse_round_trip.js +1 -68
  189. package/dist/testing/standard.d.ts +56 -25
  190. package/dist/testing/standard.d.ts.map +1 -1
  191. package/dist/testing/standard.js +62 -5
  192. package/dist/testing/stubs.d.ts +21 -6
  193. package/dist/testing/stubs.d.ts.map +1 -1
  194. package/dist/testing/stubs.js +33 -23
  195. package/dist/testing/testing_rate_limiter.d.ts +59 -0
  196. package/dist/testing/testing_rate_limiter.d.ts.map +1 -0
  197. package/dist/testing/testing_rate_limiter.js +74 -0
  198. package/dist/testing/transports/bootstrap.d.ts +52 -0
  199. package/dist/testing/transports/bootstrap.d.ts.map +1 -0
  200. package/dist/testing/transports/bootstrap.js +70 -0
  201. package/dist/testing/transports/fetch_transport.d.ts +81 -0
  202. package/dist/testing/transports/fetch_transport.d.ts.map +1 -0
  203. package/dist/testing/transports/fetch_transport.js +74 -0
  204. package/dist/testing/transports/sse_frame_reader.d.ts +41 -0
  205. package/dist/testing/transports/sse_frame_reader.d.ts.map +1 -0
  206. package/dist/testing/transports/sse_frame_reader.js +84 -0
  207. package/dist/testing/transports/sse_transport.d.ts +54 -0
  208. package/dist/testing/transports/sse_transport.d.ts.map +1 -0
  209. package/dist/testing/transports/sse_transport.js +51 -0
  210. package/dist/testing/transports/ws_client.d.ts +108 -0
  211. package/dist/testing/transports/ws_client.d.ts.map +1 -0
  212. package/dist/testing/transports/ws_client.js +56 -0
  213. package/dist/testing/transports/ws_transport.d.ts +43 -0
  214. package/dist/testing/transports/ws_transport.d.ts.map +1 -0
  215. package/dist/testing/transports/ws_transport.js +169 -0
  216. package/dist/testing/ws_round_trip.d.ts +21 -103
  217. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  218. package/dist/testing/ws_round_trip.js +42 -40
  219. package/dist/ui/CLAUDE.md +5 -3
  220. package/dist/ui/MenuLink.svelte +16 -16
  221. package/dist/ui/MenuLink.svelte.d.ts +13 -4
  222. package/dist/ui/MenuLink.svelte.d.ts.map +1 -1
  223. package/package.json +10 -4
@@ -16,33 +16,16 @@ import './assert_dev_env.js';
16
16
  *
17
17
  * @module
18
18
  */
19
- import { describe, test, assert, beforeAll, afterAll } from 'vitest';
20
- import { auth_migration_ns } from '../auth/migrations.js';
21
- import { create_test_app } from './app_server.js';
22
- import { create_pglite_factory, create_describe_db, auth_integration_truncate_tables, } from './db.js';
19
+ import { describe, test, assert, afterAll } from 'vitest';
23
20
  import { find_auth_route, assert_response_matches_spec, create_expired_test_cookie, assert_no_error_info_leakage, } from './integration_helpers.js';
24
21
  import { find_rpc_action, rpc_call_for_spec, require_rpc_endpoint_path, resolve_rpc_endpoints_for_setup, } from './rpc_helpers.js';
25
- import { RateLimiter } from '../rate_limiter.js';
26
- import { run_migrations } from '../db/migrate.js';
27
22
  import { ErrorCoverageCollector, assert_error_coverage, DEFAULT_INTEGRATION_ERROR_COVERAGE, } from './error_coverage.js';
28
23
  import { ApiError, ERROR_FORBIDDEN_ORIGIN } from '../http/error_schemas.js';
29
24
  import { is_public_auth } from '../http/auth_shape.js';
30
25
  import { account_verify_action_spec, account_session_list_action_spec, account_session_revoke_action_spec, account_session_revoke_all_action_spec, account_token_create_action_spec, account_token_list_action_spec, account_token_revoke_action_spec, } from '../auth/account_action_specs.js';
31
26
  import { invite_create_action_spec } from '../auth/admin_action_specs.js';
32
- /**
33
- * Build `CreateTestAppOptions` from standard options plus a database.
34
- * Forwards `options.rpc_endpoints` to the top-level `rpc_endpoints` slot on
35
- * `CreateTestAppOptions` so `create_app_server` auto-mounts it per-test with
36
- * the real ctx. `SuiteAppOptions` excludes `rpc_endpoints` so there's no way
37
- * for `options.app_options` to collide with the suite-level field.
38
- */
39
- const build_test_app_options = (options, db) => ({
40
- session_options: options.session_options,
41
- create_route_specs: options.create_route_specs,
42
- db,
43
- rpc_endpoints: options.rpc_endpoints,
44
- app_options: options.app_options,
45
- });
27
+ import { test_if } from './cross_backend/capabilities.js';
28
+ import { DEFAULT_TEST_PASSWORD } from './app_server.js';
46
29
  /**
47
30
  * Standard integration test suite for fuz_app auth routes.
48
31
  *
@@ -55,62 +38,69 @@ const build_test_app_options = (options, db) => ({
55
38
  * Each test group asserts that required routes exist, failing with a descriptive
56
39
  * message if the consumer's route specs are misconfigured.
57
40
  *
41
+ * The two signup-invite-edge-case tests call `invite_create_action_spec`
42
+ * (admin-gated) over the fixture's session, so consumers wiring signup +
43
+ * admin actions must thread `extra_keeper_roles: [ROLE_ADMIN]` through
44
+ * either `default_in_process_suite_options` or
45
+ * `default_cross_process_setup(handle, {extra_keeper_roles:
46
+ * [ROLE_ADMIN]})`. In both modes the fixture's `fixture.account` is the
47
+ * fresh keeper, and the extra-keeper-roles list grants the bootstrapped
48
+ * keeper additional roles inline (cross-process via `_testing_reset`'s
49
+ * bootstrap-time seeding; in-process via `bootstrap_test_keeper`) — no
50
+ * per-role RPC cost, no offer/accept round-trip. The tests run against
51
+ * the production `open_signup: false` default — the cross-process
52
+ * per-test secondary mint via `fixture.create_account()` is
53
+ * invite-gated (keeper drives `invite_create` before signup) so the
54
+ * harness doesn't need to flip the setting. Consumers that don't wire
55
+ * signup or `invite_create` silently skip these two tests.
56
+ *
58
57
  * @throws Error at setup time when `options.rpc_endpoints` is empty — the
59
58
  * suite hard-fails via `require_rpc_endpoint_path` rather than running
60
59
  * tests that would crash mid-suite trying to dispatch
61
60
  * `account_verify` / `account_session_*` / `account_token_*`.
62
61
  */
63
62
  export const describe_standard_integration_tests = (options) => {
63
+ const route_specs = options.surface_source.route_specs;
64
64
  // Hard-fail early so consumers see a clear setup error instead of a
65
65
  // confusing test failure when `rpc_endpoints` is missing. Factory-form
66
66
  // callers are resolved with a stub ctx purely to extract the endpoint
67
- // path; real handlers run per-test via the top-level `rpc_endpoints` slot on `CreateTestAppOptions`.
67
+ // path; the running backend handles live dispatch.
68
68
  const rpc_endpoints_for_setup = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
69
69
  const rpc_path = require_rpc_endpoint_path(rpc_endpoints_for_setup);
70
- const init_schema = async (db) => {
71
- await run_migrations(db, [auth_migration_ns]);
72
- };
73
- const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
74
- const describe_db = create_describe_db(factories, auth_integration_truncate_tables);
75
- describe_db('standard_integration', (get_db) => {
70
+ describe('standard_integration', () => {
76
71
  const { cookie_name } = options.session_options;
77
72
  // Error coverage tracking across test groups
78
73
  const error_collector = new ErrorCoverageCollector();
79
- let captured_route_specs = null;
80
- beforeAll(async () => {
81
- // Capture route specs once up front so coverage assertion runs even if
82
- // individual tests are skipped or fail early. Route specs are immutable
83
- // config; the transient test app is discarded.
84
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
85
- captured_route_specs = test_app.route_specs;
86
- });
87
74
  afterAll(() => {
88
- if (captured_route_specs) {
89
- // Scope coverage to auth routes this suite actually exercises:
90
- // the REST auth surface (login/logout/password/signup/bootstrap)
91
- // plus the shared RPC endpoint. Consumer-specific routes would
92
- // dilute the coverage percentage; admin-role routes are scoped
93
- // to the admin suite instead.
94
- const auth_routes = captured_route_specs.filter((s) => {
95
- if (s.auth.roles?.includes('admin') ?? false)
96
- return false;
97
- const rest_suffixes = ['/login', '/logout', '/password', '/signup', '/bootstrap'];
98
- if (rest_suffixes.some((suffix) => s.path.endsWith(suffix)))
99
- return true;
100
- return s.path === rpc_path;
101
- });
102
- assert_error_coverage(error_collector, auth_routes.length > 0 ? auth_routes : captured_route_specs, {
103
- min_coverage: DEFAULT_INTEGRATION_ERROR_COVERAGE,
104
- });
105
- }
75
+ // Scope coverage to auth routes this suite actually exercises:
76
+ // login / logout / password drivers + signup invite edge cases
77
+ // (when the consumer wires signup) + the shared RPC endpoint.
78
+ // Bootstrap (when the consumer wires it via the top-level `bootstrap`
79
+ // option in `mode: 'live'`) is intentionally excluded — this suite has
80
+ // no describe block that drives bootstrap; the dedicated
81
+ // `describe_bootstrap_success_tests` suite picks it up via
82
+ // `create_test_app_for_bootstrap`. Consumer-specific
83
+ // routes would dilute the ratio; admin-role routes are scoped to
84
+ // the admin suite instead.
85
+ const auth_routes = route_specs.filter((s) => {
86
+ if (s.auth.roles?.includes('admin') ?? false)
87
+ return false;
88
+ const rest_suffixes = ['/login', '/logout', '/password', '/signup'];
89
+ if (rest_suffixes.some((suffix) => s.path.endsWith(suffix)))
90
+ return true;
91
+ return s.path === rpc_path;
92
+ });
93
+ assert_error_coverage(error_collector, auth_routes.length > 0 ? auth_routes : route_specs, {
94
+ min_coverage: options.error_coverage_min ?? DEFAULT_INTEGRATION_ERROR_COVERAGE,
95
+ });
106
96
  });
107
97
  // --- 1. Login/logout lifecycle ---
108
98
  describe('login/logout lifecycle', () => {
109
99
  test('login with correct credentials returns 200 with Set-Cookie', async () => {
110
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
111
- const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
100
+ const fixture = await options.setup_test();
101
+ const login_route = find_auth_route(route_specs, '/login', 'POST');
112
102
  assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
113
- const res = await test_app.app.request(login_route.path, {
103
+ const res = await fixture.transport(login_route.path, {
114
104
  method: 'POST',
115
105
  headers: {
116
106
  host: 'localhost',
@@ -118,8 +108,8 @@ export const describe_standard_integration_tests = (options) => {
118
108
  'content-type': 'application/json',
119
109
  },
120
110
  body: JSON.stringify({
121
- username: test_app.backend.account.username,
122
- password: 'test-password-123',
111
+ username: fixture.account.username,
112
+ password: DEFAULT_TEST_PASSWORD,
123
113
  }),
124
114
  });
125
115
  assert.strictEqual(res.status, 200);
@@ -130,10 +120,10 @@ export const describe_standard_integration_tests = (options) => {
130
120
  assert.ok(set_cookie.includes(`${cookie_name}=`), `Expected ${cookie_name} cookie`);
131
121
  });
132
122
  test('login with wrong password returns 401', async () => {
133
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
134
- const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
123
+ const fixture = await options.setup_test();
124
+ const login_route = find_auth_route(route_specs, '/login', 'POST');
135
125
  assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
136
- const res = await test_app.app.request(login_route.path, {
126
+ const res = await fixture.transport(login_route.path, {
137
127
  method: 'POST',
138
128
  headers: {
139
129
  host: 'localhost',
@@ -141,20 +131,20 @@ export const describe_standard_integration_tests = (options) => {
141
131
  'content-type': 'application/json',
142
132
  },
143
133
  body: JSON.stringify({
144
- username: test_app.backend.account.username,
134
+ username: fixture.account.username,
145
135
  password: 'wrong-password',
146
136
  }),
147
137
  });
148
138
  assert.strictEqual(res.status, 401);
149
139
  const body = await res.clone().json();
150
140
  assert.strictEqual(body.error, 'invalid_credentials');
151
- await error_collector.assert_and_record(test_app.route_specs, 'POST', login_route.path, res);
141
+ await error_collector.assert_and_record(route_specs, 'POST', login_route.path, res);
152
142
  });
153
143
  test('login with nonexistent user returns 401', async () => {
154
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
155
- const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
144
+ const fixture = await options.setup_test();
145
+ const login_route = find_auth_route(route_specs, '/login', 'POST');
156
146
  assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
157
- const res = await test_app.app.request(login_route.path, {
147
+ const res = await fixture.transport(login_route.path, {
158
148
  method: 'POST',
159
149
  headers: {
160
150
  host: 'localhost',
@@ -163,19 +153,19 @@ export const describe_standard_integration_tests = (options) => {
163
153
  },
164
154
  body: JSON.stringify({
165
155
  username: 'nonexistent_user',
166
- password: 'test-password-123',
156
+ password: DEFAULT_TEST_PASSWORD,
167
157
  }),
168
158
  });
169
159
  assert.strictEqual(res.status, 401);
170
160
  const body = await res.clone().json();
171
161
  assert.strictEqual(body.error, 'invalid_credentials');
172
- await error_collector.assert_and_record(test_app.route_specs, 'POST', login_route.path, res);
162
+ await error_collector.assert_and_record(route_specs, 'POST', login_route.path, res);
173
163
  });
174
164
  test('login trims whitespace from username', async () => {
175
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
176
- const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
165
+ const fixture = await options.setup_test();
166
+ const login_route = find_auth_route(route_specs, '/login', 'POST');
177
167
  assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
178
- const res = await test_app.app.request(login_route.path, {
168
+ const res = await fixture.transport(login_route.path, {
179
169
  method: 'POST',
180
170
  headers: {
181
171
  host: 'localhost',
@@ -183,8 +173,8 @@ export const describe_standard_integration_tests = (options) => {
183
173
  'content-type': 'application/json',
184
174
  },
185
175
  body: JSON.stringify({
186
- username: ` ${test_app.backend.account.username} `,
187
- password: 'test-password-123',
176
+ username: ` ${fixture.account.username} `,
177
+ password: DEFAULT_TEST_PASSWORD,
188
178
  }),
189
179
  });
190
180
  assert.strictEqual(res.status, 200);
@@ -192,13 +182,13 @@ export const describe_standard_integration_tests = (options) => {
192
182
  assert.strictEqual(body.ok, true);
193
183
  });
194
184
  test('full cycle: login → verify → logout → verify fails', async () => {
195
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
196
- const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
197
- const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
185
+ const fixture = await options.setup_test();
186
+ const login_route = find_auth_route(route_specs, '/login', 'POST');
187
+ const logout_route = find_auth_route(route_specs, '/logout', 'POST');
198
188
  assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
199
189
  assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
200
190
  // Login
201
- const login_res = await test_app.app.request(login_route.path, {
191
+ const login_res = await fixture.transport(login_route.path, {
202
192
  method: 'POST',
203
193
  headers: {
204
194
  host: 'localhost',
@@ -206,8 +196,8 @@ export const describe_standard_integration_tests = (options) => {
206
196
  'content-type': 'application/json',
207
197
  },
208
198
  body: JSON.stringify({
209
- username: test_app.backend.account.username,
210
- password: 'test-password-123',
199
+ username: fixture.account.username,
200
+ password: DEFAULT_TEST_PASSWORD,
211
201
  }),
212
202
  });
213
203
  assert.strictEqual(login_res.status, 200);
@@ -224,7 +214,7 @@ export const describe_standard_integration_tests = (options) => {
224
214
  });
225
215
  // Verify works
226
216
  const verify_res = await rpc_call_for_spec({
227
- app: test_app.app,
217
+ app: { request: fixture.transport },
228
218
  path: rpc_path,
229
219
  spec: account_verify_action_spec,
230
220
  params: undefined,
@@ -232,17 +222,17 @@ export const describe_standard_integration_tests = (options) => {
232
222
  });
233
223
  assert.strictEqual(verify_res.status, 200);
234
224
  // Logout
235
- const logout_res = await test_app.app.request(logout_route.path, {
225
+ const logout_res = await fixture.transport(logout_route.path, {
236
226
  method: 'POST',
237
227
  headers: create_headers(),
238
228
  });
239
229
  assert.strictEqual(logout_res.status, 200);
240
230
  const logout_body = await logout_res.json();
241
231
  assert.strictEqual(logout_body.ok, true);
242
- assert.strictEqual(logout_body.username, test_app.backend.account.username, 'Logout response should include the username');
232
+ assert.strictEqual(logout_body.username, fixture.account.username, 'Logout response should include the username');
243
233
  // Verify fails after logout (session revoked)
244
234
  const verify_after = await rpc_call_for_spec({
245
- app: test_app.app,
235
+ app: { request: fixture.transport },
246
236
  path: rpc_path,
247
237
  spec: account_verify_action_spec,
248
238
  params: undefined,
@@ -254,10 +244,10 @@ export const describe_standard_integration_tests = (options) => {
254
244
  // --- 1b. Login response body identity (account enumeration prevention) ---
255
245
  describe('login response body identity', () => {
256
246
  test('nonexistent user and wrong password responses are structurally identical', async () => {
257
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
258
- const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
247
+ const fixture = await options.setup_test();
248
+ const login_route = find_auth_route(route_specs, '/login', 'POST');
259
249
  assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
260
- const make_login = (username, password) => test_app.app.request(login_route.path, {
250
+ const make_login = (username, password) => fixture.transport(login_route.path, {
261
251
  method: 'POST',
262
252
  headers: {
263
253
  host: 'localhost',
@@ -267,7 +257,7 @@ export const describe_standard_integration_tests = (options) => {
267
257
  body: JSON.stringify({ username, password }),
268
258
  });
269
259
  // wrong password for existing user
270
- const wrong_pw_res = await make_login(test_app.backend.account.username, 'wrong-password-999');
260
+ const wrong_pw_res = await make_login(fixture.account.username, 'wrong-password-999');
271
261
  assert.strictEqual(wrong_pw_res.status, 401);
272
262
  const wrong_pw_body = await wrong_pw_res.json();
273
263
  // nonexistent user
@@ -284,10 +274,10 @@ export const describe_standard_integration_tests = (options) => {
284
274
  // --- 2. Cookie attributes ---
285
275
  describe('cookie attributes', () => {
286
276
  test('session cookie has secure attributes', async () => {
287
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
288
- const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
277
+ const fixture = await options.setup_test();
278
+ const login_route = find_auth_route(route_specs, '/login', 'POST');
289
279
  assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
290
- const res = await test_app.app.request(login_route.path, {
280
+ const res = await fixture.transport(login_route.path, {
291
281
  method: 'POST',
292
282
  headers: {
293
283
  host: 'localhost',
@@ -295,8 +285,8 @@ export const describe_standard_integration_tests = (options) => {
295
285
  'content-type': 'application/json',
296
286
  },
297
287
  body: JSON.stringify({
298
- username: test_app.backend.account.username,
299
- password: 'test-password-123',
288
+ username: fixture.account.username,
289
+ password: DEFAULT_TEST_PASSWORD,
300
290
  }),
301
291
  });
302
292
  assert.strictEqual(res.status, 200);
@@ -312,9 +302,9 @@ export const describe_standard_integration_tests = (options) => {
312
302
  // --- 3. Session security ---
313
303
  describe('session security', () => {
314
304
  test('no cookie on protected route returns 401', async () => {
315
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
305
+ const fixture = await options.setup_test();
316
306
  const res = await rpc_call_for_spec({
317
- app: test_app.app,
307
+ app: { request: fixture.fresh_transport() },
318
308
  path: rpc_path,
319
309
  spec: account_verify_action_spec,
320
310
  params: undefined,
@@ -323,9 +313,9 @@ export const describe_standard_integration_tests = (options) => {
323
313
  assert.strictEqual(res.status, 401);
324
314
  });
325
315
  test('corrupted cookie returns 401', async () => {
326
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
316
+ const fixture = await options.setup_test();
327
317
  const res = await rpc_call_for_spec({
328
- app: test_app.app,
318
+ app: { request: fixture.transport },
329
319
  path: rpc_path,
330
320
  spec: account_verify_action_spec,
331
321
  params: undefined,
@@ -333,11 +323,12 @@ export const describe_standard_integration_tests = (options) => {
333
323
  });
334
324
  assert.strictEqual(res.status, 401);
335
325
  });
336
- test('expired cookie returns 401', async () => {
337
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
338
- const expired_cookie = await create_expired_test_cookie(test_app.backend.keyring, options.session_options);
326
+ test_if(options.capabilities.in_process_only, 'expired cookie returns 401', async () => {
327
+ const fixture = await options.setup_test();
328
+ assert(fixture.in_process);
329
+ const expired_cookie = await create_expired_test_cookie(fixture.keyring, options.session_options);
339
330
  const res = await rpc_call_for_spec({
340
- app: test_app.app,
331
+ app: { request: fixture.transport },
341
332
  path: rpc_path,
342
333
  spec: account_verify_action_spec,
343
334
  params: undefined,
@@ -349,11 +340,11 @@ export const describe_standard_integration_tests = (options) => {
349
340
  // --- 4. Session revocation ---
350
341
  describe('session revocation', () => {
351
342
  test('revoke single session by ID invalidates that session', async () => {
352
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
353
- const headers = test_app.create_session_headers();
343
+ const fixture = await options.setup_test();
344
+ const headers = fixture.create_session_headers();
354
345
  // List own sessions to get the session ID
355
346
  const list_res = await rpc_call_for_spec({
356
- app: test_app.app,
347
+ app: { request: fixture.transport },
357
348
  path: rpc_path,
358
349
  spec: account_session_list_action_spec,
359
350
  params: undefined,
@@ -364,7 +355,7 @@ export const describe_standard_integration_tests = (options) => {
364
355
  const session_id = list_res.result.sessions[0].id;
365
356
  // Revoke that session by ID
366
357
  const revoke_res = await rpc_call_for_spec({
367
- app: test_app.app,
358
+ app: { request: fixture.transport },
368
359
  path: rpc_path,
369
360
  spec: account_session_revoke_action_spec,
370
361
  params: { session_id },
@@ -375,7 +366,7 @@ export const describe_standard_integration_tests = (options) => {
375
366
  assert.strictEqual(revoke_res.result.revoked, true);
376
367
  // Session should no longer work
377
368
  const after = await rpc_call_for_spec({
378
- app: test_app.app,
369
+ app: { request: fixture.transport },
379
370
  path: rpc_path,
380
371
  spec: account_verify_action_spec,
381
372
  params: undefined,
@@ -384,11 +375,11 @@ export const describe_standard_integration_tests = (options) => {
384
375
  assert.strictEqual(after.status, 401);
385
376
  });
386
377
  test('revoke-all invalidates existing session', async () => {
387
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
388
- const headers = test_app.create_session_headers();
378
+ const fixture = await options.setup_test();
379
+ const headers = fixture.create_session_headers();
389
380
  // Verify works
390
381
  const before = await rpc_call_for_spec({
391
- app: test_app.app,
382
+ app: { request: fixture.transport },
392
383
  path: rpc_path,
393
384
  spec: account_verify_action_spec,
394
385
  params: undefined,
@@ -397,7 +388,7 @@ export const describe_standard_integration_tests = (options) => {
397
388
  assert.strictEqual(before.status, 200);
398
389
  // Revoke all sessions
399
390
  const revoke_res = await rpc_call_for_spec({
400
- app: test_app.app,
391
+ app: { request: fixture.transport },
401
392
  path: rpc_path,
402
393
  spec: account_session_revoke_all_action_spec,
403
394
  params: undefined,
@@ -406,7 +397,7 @@ export const describe_standard_integration_tests = (options) => {
406
397
  assert.ok(revoke_res.ok, 'account_session_revoke_all should succeed');
407
398
  // Verify fails after revocation
408
399
  const after = await rpc_call_for_spec({
409
- app: test_app.app,
400
+ app: { request: fixture.transport },
410
401
  path: rpc_path,
411
402
  spec: account_verify_action_spec,
412
403
  params: undefined,
@@ -418,20 +409,20 @@ export const describe_standard_integration_tests = (options) => {
418
409
  // --- 4b. Password change ---
419
410
  describe('password change', () => {
420
411
  test('password change invalidates all sessions and allows login with new password', async () => {
421
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
422
- const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
423
- const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
412
+ const fixture = await options.setup_test();
413
+ const login_route = find_auth_route(route_specs, '/login', 'POST');
414
+ const password_route = find_auth_route(route_specs, '/password', 'POST');
424
415
  assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
425
416
  assert.ok(password_route, 'Expected POST /password route — ensure create_route_specs includes account routes');
426
- const headers = test_app.create_session_headers({
417
+ const headers = fixture.create_session_headers({
427
418
  'content-type': 'application/json',
428
419
  });
429
420
  // Change password
430
- const change_res = await test_app.app.request(password_route.path, {
421
+ const change_res = await fixture.transport(password_route.path, {
431
422
  method: 'POST',
432
423
  headers,
433
424
  body: JSON.stringify({
434
- current_password: 'test-password-123',
425
+ current_password: DEFAULT_TEST_PASSWORD,
435
426
  new_password: 'new-password-456',
436
427
  }),
437
428
  });
@@ -442,15 +433,15 @@ export const describe_standard_integration_tests = (options) => {
442
433
  assert.ok(change_body.sessions_revoked >= 1, 'Expected at least 1 session revoked');
443
434
  // Old session should be invalid
444
435
  const verify_after = await rpc_call_for_spec({
445
- app: test_app.app,
436
+ app: { request: fixture.transport },
446
437
  path: rpc_path,
447
438
  spec: account_verify_action_spec,
448
439
  params: undefined,
449
- headers: test_app.create_session_headers(),
440
+ headers: fixture.create_session_headers(),
450
441
  });
451
442
  assert.strictEqual(verify_after.status, 401);
452
443
  // Login with new password works
453
- const login_res = await test_app.app.request(login_route.path, {
444
+ const login_res = await fixture.transport(login_route.path, {
454
445
  method: 'POST',
455
446
  headers: {
456
447
  host: 'localhost',
@@ -458,7 +449,7 @@ export const describe_standard_integration_tests = (options) => {
458
449
  'content-type': 'application/json',
459
450
  },
460
451
  body: JSON.stringify({
461
- username: test_app.backend.account.username,
452
+ username: fixture.account.username,
462
453
  password: 'new-password-456',
463
454
  }),
464
455
  });
@@ -467,12 +458,12 @@ export const describe_standard_integration_tests = (options) => {
467
458
  assert.strictEqual(login_body.ok, true);
468
459
  });
469
460
  test('password change with wrong current password returns 401', async () => {
470
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
471
- const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
461
+ const fixture = await options.setup_test();
462
+ const password_route = find_auth_route(route_specs, '/password', 'POST');
472
463
  assert.ok(password_route, 'Expected POST /password route — ensure create_route_specs includes account routes');
473
- const res = await test_app.app.request(password_route.path, {
464
+ const res = await fixture.transport(password_route.path, {
474
465
  method: 'POST',
475
- headers: test_app.create_session_headers({
466
+ headers: fixture.create_session_headers({
476
467
  'content-type': 'application/json',
477
468
  }),
478
469
  body: JSON.stringify({
@@ -481,31 +472,32 @@ export const describe_standard_integration_tests = (options) => {
481
472
  }),
482
473
  });
483
474
  assert.strictEqual(res.status, 401);
484
- error_collector.record(test_app.route_specs, 'POST', password_route.path, 401);
475
+ error_collector.record(route_specs, 'POST', password_route.path, 401);
485
476
  // Session should still be valid (password didn't change)
486
477
  const verify_res = await rpc_call_for_spec({
487
- app: test_app.app,
478
+ app: { request: fixture.transport },
488
479
  path: rpc_path,
489
480
  spec: account_verify_action_spec,
490
481
  params: undefined,
491
- headers: test_app.create_session_headers(),
482
+ headers: fixture.create_session_headers(),
492
483
  });
493
484
  assert.strictEqual(verify_res.status, 200);
494
485
  });
495
486
  });
496
487
  // --- 5. Origin verification ---
497
488
  describe('origin verification', () => {
498
- test('evil origin is rejected with 403', async () => {
499
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
489
+ test_if(options.capabilities.in_process_only, 'evil origin is rejected with 403', async () => {
490
+ const fixture = await options.setup_test();
491
+ assert(fixture.in_process);
500
492
  // `verify_request_source` runs before the RPC dispatcher and returns a
501
493
  // plain REST `{error}` body — not a JSON-RPC envelope. Skip `rpc_call`.
502
- const res = await test_app.app.request(rpc_path, {
494
+ const res = await fixture.transport(rpc_path, {
503
495
  method: 'POST',
504
496
  headers: {
505
497
  host: 'localhost',
506
498
  origin: 'http://evil.com',
507
499
  'content-type': 'application/json',
508
- cookie: `${cookie_name}=${test_app.backend.session_cookie}`,
500
+ cookie: `${cookie_name}=${fixture.backend_internals.session_cookie}`,
509
501
  },
510
502
  body: JSON.stringify({
511
503
  jsonrpc: '2.0',
@@ -518,26 +510,27 @@ export const describe_standard_integration_tests = (options) => {
518
510
  assert.strictEqual(body.error, ERROR_FORBIDDEN_ORIGIN);
519
511
  });
520
512
  test('valid origin is accepted', async () => {
521
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
513
+ const fixture = await options.setup_test();
522
514
  const res = await rpc_call_for_spec({
523
- app: test_app.app,
515
+ app: { request: fixture.transport },
524
516
  path: rpc_path,
525
517
  spec: account_verify_action_spec,
526
518
  params: undefined,
527
- headers: test_app.create_session_headers(),
519
+ headers: fixture.create_session_headers(),
528
520
  });
529
521
  assert.strictEqual(res.status, 200);
530
522
  });
531
- test('no origin header is allowed (direct access)', async () => {
532
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
523
+ test_if(options.capabilities.in_process_only, 'no origin header is allowed (direct access)', async () => {
524
+ const fixture = await options.setup_test();
525
+ assert(fixture.in_process);
533
526
  // Probe the "no Origin / no Referer" path; `suppress_default_origin`
534
527
  // skips the default `origin` header.
535
528
  const res = await rpc_call_for_spec({
536
- app: test_app.app,
529
+ app: { request: fixture.transport },
537
530
  path: rpc_path,
538
531
  spec: account_verify_action_spec,
539
532
  params: undefined,
540
- headers: { cookie: `${cookie_name}=${test_app.backend.session_cookie}` },
533
+ headers: { cookie: `${cookie_name}=${fixture.backend_internals.session_cookie}` },
541
534
  suppress_default_origin: true,
542
535
  });
543
536
  assert.notStrictEqual(res.status, 403);
@@ -546,21 +539,24 @@ export const describe_standard_integration_tests = (options) => {
546
539
  // --- 6. Bearer auth ---
547
540
  describe('bearer auth', () => {
548
541
  test('valid bearer token authenticates', async () => {
549
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
542
+ const fixture = await options.setup_test();
550
543
  const res = await rpc_call_for_spec({
551
- app: test_app.app,
544
+ app: { request: fixture.transport },
552
545
  path: rpc_path,
553
546
  spec: account_verify_action_spec,
554
547
  params: undefined,
555
- headers: test_app.create_bearer_headers(),
548
+ headers: fixture.create_bearer_headers(),
556
549
  suppress_default_origin: true,
557
550
  });
558
551
  assert.strictEqual(res.status, 200);
559
552
  });
560
553
  test('invalid bearer token returns 401', async () => {
561
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
554
+ const fixture = await options.setup_test();
555
+ // `origin: null` — bearer auth requires no browser-context
556
+ // indicator; the default `Origin: <base_url>` would silently
557
+ // discard the bearer cross-process.
562
558
  const res = await rpc_call_for_spec({
563
- app: test_app.app,
559
+ app: { request: fixture.fresh_transport({ origin: null }) },
564
560
  path: rpc_path,
565
561
  spec: account_verify_action_spec,
566
562
  params: undefined,
@@ -570,11 +566,13 @@ export const describe_standard_integration_tests = (options) => {
570
566
  assert.strictEqual(res.status, 401);
571
567
  });
572
568
  test('bearer token with Origin header is rejected', async () => {
573
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
574
- const bearer_headers = test_app.create_bearer_headers();
575
- // Without Origin — works.
569
+ const fixture = await options.setup_test();
570
+ const bearer_headers = fixture.create_bearer_headers();
571
+ // Without Origin — works. `origin: null` so the transport
572
+ // doesn't auto-add the default Origin (which would discard
573
+ // the bearer as browser-context cross-process).
576
574
  const ok_res = await rpc_call_for_spec({
577
- app: test_app.app,
575
+ app: { request: fixture.fresh_transport({ origin: null }) },
578
576
  path: rpc_path,
579
577
  spec: account_verify_action_spec,
580
578
  params: undefined,
@@ -584,7 +582,7 @@ export const describe_standard_integration_tests = (options) => {
584
582
  assert.strictEqual(ok_res.status, 200);
585
583
  // With Origin — bearer silently discarded (browser context), falls through to no auth.
586
584
  const res = await rpc_call_for_spec({
587
- app: test_app.app,
585
+ app: { request: fixture.fresh_transport() },
588
586
  path: rpc_path,
589
587
  spec: account_verify_action_spec,
590
588
  params: undefined,
@@ -596,20 +594,24 @@ export const describe_standard_integration_tests = (options) => {
596
594
  // --- 7. Token revocation ---
597
595
  describe('token revocation', () => {
598
596
  test('revoked API token returns 401', async () => {
599
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
597
+ const fixture = await options.setup_test();
600
598
  // Create a new token via RPC
601
599
  const create_res = await rpc_call_for_spec({
602
- app: test_app.app,
600
+ app: { request: fixture.transport },
603
601
  path: rpc_path,
604
602
  spec: account_token_create_action_spec,
605
603
  params: { name: 'test-revoke' },
606
- headers: test_app.create_session_headers(),
604
+ headers: fixture.create_session_headers(),
607
605
  });
608
606
  assert.ok(create_res.ok, 'account_token_create should succeed');
609
607
  const { token, id } = create_res.result;
610
- // Verify token works
608
+ // Verify token works — fresh transport so the per-test session
609
+ // cookie in `fixture.transport`'s jar can't give a false pass
610
+ // when the bearer is the credential under test. `origin: null`
611
+ // so the transport doesn't auto-add a default Origin (which
612
+ // would discard the bearer as browser-context cross-process).
611
613
  const use_res = await rpc_call_for_spec({
612
- app: test_app.app,
614
+ app: { request: fixture.fresh_transport({ origin: null }) },
613
615
  path: rpc_path,
614
616
  spec: account_verify_action_spec,
615
617
  params: undefined,
@@ -619,16 +621,17 @@ export const describe_standard_integration_tests = (options) => {
619
621
  assert.strictEqual(use_res.status, 200);
620
622
  // Revoke via RPC
621
623
  const revoke_res = await rpc_call_for_spec({
622
- app: test_app.app,
624
+ app: { request: fixture.transport },
623
625
  path: rpc_path,
624
626
  spec: account_token_revoke_action_spec,
625
627
  params: { token_id: id },
626
- headers: test_app.create_session_headers(),
628
+ headers: fixture.create_session_headers(),
627
629
  });
628
630
  assert.ok(revoke_res.ok, 'account_token_revoke should succeed');
629
- // Token should no longer work
631
+ // Token should no longer work — fresh transport same reason as
632
+ // the `use_res` call above.
630
633
  const after_res = await rpc_call_for_spec({
631
- app: test_app.app,
634
+ app: { request: fixture.fresh_transport({ origin: null }) },
632
635
  path: rpc_path,
633
636
  spec: account_verify_action_spec,
634
637
  params: undefined,
@@ -641,36 +644,36 @@ export const describe_standard_integration_tests = (options) => {
641
644
  // --- 8. Cross-account isolation ---
642
645
  describe('cross-account isolation', () => {
643
646
  test('non-admin cannot access admin routes', async () => {
644
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
647
+ const fixture = await options.setup_test();
645
648
  // admin routes are optional in the base suite — admin-specific coverage
646
649
  // lives in describe_standard_admin_integration_tests
647
- const admin_route = test_app.route_specs.find((s) => s.auth.roles?.includes('admin') ?? false);
650
+ const admin_route = route_specs.find((s) => s.auth.roles?.includes('admin') ?? false);
648
651
  if (!admin_route)
649
652
  return;
650
- const res = await test_app.app.request(admin_route.path, {
653
+ const res = await fixture.transport(admin_route.path, {
651
654
  method: admin_route.method,
652
- headers: test_app.create_session_headers(),
655
+ headers: fixture.create_session_headers(),
653
656
  });
654
657
  assert.strictEqual(res.status, 403);
655
658
  const body = await res.json();
656
659
  assert.strictEqual(body.error, 'insufficient_permissions');
657
660
  });
658
661
  test("user A cannot revoke user B's sessions", async () => {
659
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
662
+ const fixture = await options.setup_test();
660
663
  // Create a second account
661
- const user_b = await test_app.create_account({ username: 'user_b' });
664
+ const user_b = await fixture.create_account({ username: 'user_b' });
662
665
  // User A revokes all their own sessions
663
666
  const revoke_res = await rpc_call_for_spec({
664
- app: test_app.app,
667
+ app: { request: fixture.transport },
665
668
  path: rpc_path,
666
669
  spec: account_session_revoke_all_action_spec,
667
670
  params: undefined,
668
- headers: test_app.create_session_headers(),
671
+ headers: fixture.create_session_headers(),
669
672
  });
670
673
  assert.ok(revoke_res.ok, 'account_session_revoke_all should succeed');
671
674
  // User B's session should still work
672
675
  const verify_b = await rpc_call_for_spec({
673
- app: test_app.app,
676
+ app: { request: fixture.transport },
674
677
  path: rpc_path,
675
678
  spec: account_verify_action_spec,
676
679
  params: undefined,
@@ -679,12 +682,12 @@ export const describe_standard_integration_tests = (options) => {
679
682
  assert.strictEqual(verify_b.status, 200);
680
683
  });
681
684
  test("user A cannot revoke user B's session by ID", async () => {
682
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
683
- const user_b = await test_app.create_account({ username: 'user_b' });
685
+ const fixture = await options.setup_test();
686
+ const user_b = await fixture.create_account({ username: 'user_b' });
684
687
  const user_b_headers = { cookie: `${cookie_name}=${user_b.session_cookie}` };
685
688
  // Get user B's session ID by listing as user B
686
689
  const list_res = await rpc_call_for_spec({
687
- app: test_app.app,
690
+ app: { request: fixture.transport },
688
691
  path: rpc_path,
689
692
  spec: account_session_list_action_spec,
690
693
  params: undefined,
@@ -695,17 +698,17 @@ export const describe_standard_integration_tests = (options) => {
695
698
  const session_id_b = list_res.result.sessions[0].id;
696
699
  // User A tries to revoke user B's session by ID
697
700
  const revoke_res = await rpc_call_for_spec({
698
- app: test_app.app,
701
+ app: { request: fixture.transport },
699
702
  path: rpc_path,
700
703
  spec: account_session_revoke_action_spec,
701
704
  params: { session_id: session_id_b },
702
- headers: test_app.create_session_headers(),
705
+ headers: fixture.create_session_headers(),
703
706
  });
704
707
  assert.ok(revoke_res.ok, 'account_session_revoke should succeed');
705
708
  assert.strictEqual(revoke_res.result.revoked, false, 'Should not revoke another account session');
706
709
  // User B's session should still work
707
710
  const verify_b = await rpc_call_for_spec({
708
- app: test_app.app,
711
+ app: { request: fixture.transport },
709
712
  path: rpc_path,
710
713
  spec: account_verify_action_spec,
711
714
  params: undefined,
@@ -714,12 +717,12 @@ export const describe_standard_integration_tests = (options) => {
714
717
  assert.strictEqual(verify_b.status, 200);
715
718
  });
716
719
  test("user A cannot revoke user B's token by ID", async () => {
717
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
718
- const user_b = await test_app.create_account({ username: 'user_b' });
720
+ const fixture = await options.setup_test();
721
+ const user_b = await fixture.create_account({ username: 'user_b' });
719
722
  const user_b_headers = { cookie: `${cookie_name}=${user_b.session_cookie}` };
720
723
  // Get user B's token ID by listing as user B
721
724
  const list_res = await rpc_call_for_spec({
722
- app: test_app.app,
725
+ app: { request: fixture.transport },
723
726
  path: rpc_path,
724
727
  spec: account_token_list_action_spec,
725
728
  params: undefined,
@@ -730,17 +733,17 @@ export const describe_standard_integration_tests = (options) => {
730
733
  const token_id_b = list_res.result.tokens[0].id;
731
734
  // User A tries to revoke user B's token by ID
732
735
  const revoke_res = await rpc_call_for_spec({
733
- app: test_app.app,
736
+ app: { request: fixture.transport },
734
737
  path: rpc_path,
735
738
  spec: account_token_revoke_action_spec,
736
739
  params: { token_id: token_id_b },
737
- headers: test_app.create_session_headers(),
740
+ headers: fixture.create_session_headers(),
738
741
  });
739
742
  assert.ok(revoke_res.ok, 'account_token_revoke should succeed');
740
743
  assert.strictEqual(revoke_res.result.revoked, false, 'Should not revoke another account token');
741
744
  // User B's bearer token should still work
742
745
  const verify_b = await rpc_call_for_spec({
743
- app: test_app.app,
746
+ app: { request: fixture.transport },
744
747
  path: rpc_path,
745
748
  spec: account_verify_action_spec,
746
749
  params: undefined,
@@ -750,37 +753,37 @@ export const describe_standard_integration_tests = (options) => {
750
753
  assert.strictEqual(verify_b.status, 200);
751
754
  });
752
755
  test("user A's session list does not include user B's sessions", async () => {
753
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
754
- const user_b = await test_app.create_account({ username: 'user_b' });
756
+ const fixture = await options.setup_test();
757
+ const user_b = await fixture.create_account({ username: 'user_b' });
755
758
  // User A lists sessions
756
759
  const res = await rpc_call_for_spec({
757
- app: test_app.app,
760
+ app: { request: fixture.transport },
758
761
  path: rpc_path,
759
762
  spec: account_session_list_action_spec,
760
763
  params: undefined,
761
- headers: test_app.create_session_headers(),
764
+ headers: fixture.create_session_headers(),
762
765
  });
763
766
  assert.ok(res.ok, 'account_session_list should succeed');
764
767
  // Sessions should only belong to user A's account
765
768
  for (const session of res.result.sessions) {
766
- assert.strictEqual(session.account_id, test_app.backend.account.id, `Session ${session.id} should belong to user A, not user B (${user_b.account.id})`);
769
+ assert.strictEqual(session.account_id, fixture.account.id, `Session ${session.id} should belong to user A, not user B (${user_b.account.id})`);
767
770
  }
768
771
  });
769
772
  test("user A's token list does not include user B's tokens", async () => {
770
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
771
- const user_b = await test_app.create_account({ username: 'user_b' });
773
+ const fixture = await options.setup_test();
774
+ const user_b = await fixture.create_account({ username: 'user_b' });
772
775
  // User A lists tokens
773
776
  const res = await rpc_call_for_spec({
774
- app: test_app.app,
777
+ app: { request: fixture.transport },
775
778
  path: rpc_path,
776
779
  spec: account_token_list_action_spec,
777
780
  params: undefined,
778
- headers: test_app.create_session_headers(),
781
+ headers: fixture.create_session_headers(),
779
782
  });
780
783
  assert.ok(res.ok, 'account_token_list should succeed');
781
784
  // Tokens should only belong to user A's account
782
785
  for (const token of res.result.tokens) {
783
- assert.strictEqual(token.account_id, test_app.backend.account.id, `Token ${token.id} should belong to user A, not user B (${user_b.account.id})`);
786
+ assert.strictEqual(token.account_id, fixture.account.id, `Token ${token.id} should belong to user A, not user B (${user_b.account.id})`);
784
787
  }
785
788
  });
786
789
  });
@@ -791,10 +794,10 @@ export const describe_standard_integration_tests = (options) => {
791
794
  // /password remain as REST routes whose responses we exercise here.
792
795
  // RPC output validation is covered by `describe_rpc_round_trip_tests`.
793
796
  test('POST /login 401 response matches declared error schema', async () => {
794
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
795
- const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
797
+ const fixture = await options.setup_test();
798
+ const login_route = find_auth_route(route_specs, '/login', 'POST');
796
799
  assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
797
- const res = await test_app.app.request(login_route.path, {
800
+ const res = await fixture.transport(login_route.path, {
798
801
  method: 'POST',
799
802
  headers: {
800
803
  host: 'localhost',
@@ -807,25 +810,25 @@ export const describe_standard_integration_tests = (options) => {
807
810
  }),
808
811
  });
809
812
  assert.strictEqual(res.status, 401);
810
- await assert_response_matches_spec(test_app.route_specs, 'POST', login_route.path, res);
813
+ await assert_response_matches_spec(route_specs, 'POST', login_route.path, res);
811
814
  });
812
815
  test('POST /logout 200 response matches output schema', async () => {
813
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
814
- const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
816
+ const fixture = await options.setup_test();
817
+ const logout_route = find_auth_route(route_specs, '/logout', 'POST');
815
818
  assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
816
- const res = await test_app.app.request(logout_route.path, {
819
+ const res = await fixture.transport(logout_route.path, {
817
820
  method: 'POST',
818
- headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
821
+ headers: fixture.create_session_headers({ 'content-type': 'application/json' }),
819
822
  body: JSON.stringify({}),
820
823
  });
821
824
  assert.strictEqual(res.status, 200);
822
- await assert_response_matches_spec(test_app.route_specs, 'POST', logout_route.path, res);
825
+ await assert_response_matches_spec(route_specs, 'POST', logout_route.path, res);
823
826
  });
824
827
  test('POST /logout 401 response matches declared error schema', async () => {
825
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
826
- const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
828
+ const fixture = await options.setup_test();
829
+ const logout_route = find_auth_route(route_specs, '/logout', 'POST');
827
830
  assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
828
- const res = await test_app.app.request(logout_route.path, {
831
+ const res = await fixture.fresh_transport()(logout_route.path, {
829
832
  method: 'POST',
830
833
  headers: {
831
834
  host: 'localhost',
@@ -835,129 +838,92 @@ export const describe_standard_integration_tests = (options) => {
835
838
  body: JSON.stringify({}),
836
839
  });
837
840
  assert.strictEqual(res.status, 401);
838
- await assert_response_matches_spec(test_app.route_specs, 'POST', logout_route.path, res);
839
- });
840
- });
841
- // --- 10b. Rate limiting smoke test (full middleware stack) ---
842
- describe('rate limiting smoke test', () => {
843
- test('rate limiter fires in full middleware stack', async () => {
844
- const test_app = await create_test_app({
845
- ...build_test_app_options(options, get_db()),
846
- app_options: {
847
- ...options.app_options,
848
- // tight limiter: 2 attempts / 1 minute
849
- ip_rate_limiter: new RateLimiter({
850
- max_attempts: 2,
851
- window_ms: 60_000,
852
- cleanup_interval_ms: 0,
853
- }),
854
- },
855
- });
856
- const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
857
- if (!login_route)
858
- return; // skip if login route not wired
859
- const make_bad_login = (ip_header) => {
860
- const headers = {
861
- host: 'localhost',
862
- origin: 'http://localhost:5173',
863
- 'content-type': 'application/json',
864
- };
865
- if (ip_header) {
866
- headers['x-forwarded-for'] = ip_header;
867
- }
868
- return test_app.app.request(login_route.path, {
869
- method: 'POST',
870
- headers,
871
- body: JSON.stringify({ username: 'nobody', password: 'wrong' }),
872
- });
873
- };
874
- // exhaust the limiter (2 attempts)
875
- await make_bad_login();
876
- await make_bad_login();
877
- // third attempt should be rate-limited
878
- const limited_res = await make_bad_login();
879
- assert.strictEqual(limited_res.status, 429, 'Expected 429 after exceeding rate limit');
880
- const limited_body = await limited_res.clone().json();
881
- assert.strictEqual(limited_body.error, 'rate_limit_exceeded');
882
- await error_collector.assert_and_record(test_app.route_specs, 'POST', login_route.path, limited_res);
883
- // Retry-After header present
884
- const retry_after = limited_res.headers.get('Retry-After');
885
- assert.ok(retry_after, 'Expected Retry-After header on 429 response');
841
+ await assert_response_matches_spec(route_specs, 'POST', logout_route.path, res);
886
842
  });
887
843
  });
844
+ // Rate-limit behavior is covered end-to-end by
845
+ // `describe_rate_limiting_tests` against the full middleware stack.
846
+ // A per-suite smoke test isn't reintroduced here because the
847
+ // `setup_test` single-fixture model can't carry per-test rate-limiter
848
+ // overrides without each test re-constructing its own `TestApp`.
888
849
  // --- 10c2. Error coverage: unauthenticated access to auth-required routes ---
889
850
  describe('error coverage breadth', () => {
890
851
  test('exercises 401 on multiple auth-required routes for error coverage', async () => {
891
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
852
+ const fixture = await options.setup_test();
892
853
  // Hit several auth-required RPC methods without credentials to
893
854
  // broaden error coverage beyond just /login. RPC 401s are tracked
894
855
  // against the shared endpoint path. The dispatcher runs auth before
895
856
  // params validation, so any well-formed param body works — we just
896
857
  // need each call to be type-correct wrt its spec.
858
+ //
859
+ // Fresh transport so the per-test session cookie in
860
+ // `fixture.transport`'s jar doesn't leak into these unauthed
861
+ // probes and convert their expected 401s into 200s.
862
+ const unauthed = fixture.fresh_transport();
897
863
  const session_list = await rpc_call_for_spec({
898
- app: test_app.app,
864
+ app: { request: unauthed },
899
865
  path: rpc_path,
900
866
  spec: account_session_list_action_spec,
901
867
  params: undefined,
902
868
  headers: { host: 'localhost' },
903
869
  });
904
870
  assert.strictEqual(session_list.status, 401);
905
- error_collector.record(test_app.route_specs, 'POST', rpc_path, 401);
871
+ error_collector.record(route_specs, 'POST', rpc_path, 401);
906
872
  const session_revoke_all = await rpc_call_for_spec({
907
- app: test_app.app,
873
+ app: { request: unauthed },
908
874
  path: rpc_path,
909
875
  spec: account_session_revoke_all_action_spec,
910
876
  params: undefined,
911
877
  headers: { host: 'localhost' },
912
878
  });
913
879
  assert.strictEqual(session_revoke_all.status, 401);
914
- error_collector.record(test_app.route_specs, 'POST', rpc_path, 401);
880
+ error_collector.record(route_specs, 'POST', rpc_path, 401);
915
881
  const token_list = await rpc_call_for_spec({
916
- app: test_app.app,
882
+ app: { request: unauthed },
917
883
  path: rpc_path,
918
884
  spec: account_token_list_action_spec,
919
885
  params: undefined,
920
886
  headers: { host: 'localhost' },
921
887
  });
922
888
  assert.strictEqual(token_list.status, 401);
923
- error_collector.record(test_app.route_specs, 'POST', rpc_path, 401);
889
+ error_collector.record(route_specs, 'POST', rpc_path, 401);
924
890
  const token_create = await rpc_call_for_spec({
925
- app: test_app.app,
891
+ app: { request: unauthed },
926
892
  path: rpc_path,
927
893
  spec: account_token_create_action_spec,
928
894
  params: { name: 'unauth-probe' },
929
895
  headers: { host: 'localhost' },
930
896
  });
931
897
  assert.strictEqual(token_create.status, 401);
932
- error_collector.record(test_app.route_specs, 'POST', rpc_path, 401);
898
+ error_collector.record(route_specs, 'POST', rpc_path, 401);
933
899
  const verify = await rpc_call_for_spec({
934
- app: test_app.app,
900
+ app: { request: unauthed },
935
901
  path: rpc_path,
936
902
  spec: account_verify_action_spec,
937
903
  params: undefined,
938
904
  headers: { host: 'localhost' },
939
905
  });
940
906
  assert.strictEqual(verify.status, 401);
941
- error_collector.record(test_app.route_specs, 'POST', rpc_path, 401);
907
+ error_collector.record(route_specs, 'POST', rpc_path, 401);
942
908
  // Also exercise POST /logout without auth (still REST)
943
- const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
909
+ const logout_route = find_auth_route(route_specs, '/logout', 'POST');
944
910
  if (logout_route) {
945
- const res = await test_app.app.request(logout_route.path, {
911
+ const res = await unauthed(logout_route.path, {
946
912
  method: 'POST',
947
913
  headers: { host: 'localhost', 'content-type': 'application/json' },
948
914
  body: JSON.stringify({}),
949
915
  });
950
916
  assert.strictEqual(res.status, 401, 'POST /logout without auth should return 401');
951
- error_collector.record(test_app.route_specs, 'POST', logout_route.path, 401);
917
+ error_collector.record(route_specs, 'POST', logout_route.path, 401);
952
918
  }
953
919
  });
954
920
  });
955
921
  // --- 10c. Error response information leakage ---
956
922
  describe('error response information leakage', () => {
957
923
  test('401 responses contain no leaky fields', async () => {
958
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
924
+ const fixture = await options.setup_test();
959
925
  const res = await rpc_call_for_spec({
960
- app: test_app.app,
926
+ app: { request: fixture.fresh_transport() },
961
927
  path: rpc_path,
962
928
  spec: account_verify_action_spec,
963
929
  params: undefined,
@@ -973,11 +939,12 @@ export const describe_standard_integration_tests = (options) => {
973
939
  });
974
940
  // --- 11. Expired credential rejection ---
975
941
  describe('expired credential rejection', () => {
976
- test('expired session cookie returns 401', async () => {
977
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
978
- const expired_cookie = await create_expired_test_cookie(test_app.backend.keyring, options.session_options);
942
+ test_if(options.capabilities.in_process_only, 'expired session cookie returns 401', async () => {
943
+ const fixture = await options.setup_test();
944
+ assert(fixture.in_process);
945
+ const expired_cookie = await create_expired_test_cookie(fixture.keyring, options.session_options);
979
946
  const res = await rpc_call_for_spec({
980
- app: test_app.app,
947
+ app: { request: fixture.transport },
981
948
  path: rpc_path,
982
949
  spec: account_verify_action_spec,
983
950
  params: undefined,
@@ -985,12 +952,13 @@ export const describe_standard_integration_tests = (options) => {
985
952
  });
986
953
  assert.strictEqual(res.status, 401, 'Expired session cookie should be rejected');
987
954
  });
988
- test('expired session cookie returns 401 on mutation route', async () => {
989
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
990
- const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
955
+ test_if(options.capabilities.in_process_only, 'expired session cookie returns 401 on mutation route', async () => {
956
+ const fixture = await options.setup_test();
957
+ assert(fixture.in_process);
958
+ const logout_route = find_auth_route(route_specs, '/logout', 'POST');
991
959
  assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
992
- const expired_cookie = await create_expired_test_cookie(test_app.backend.keyring, options.session_options);
993
- const res = await test_app.app.request(logout_route.path, {
960
+ const expired_cookie = await create_expired_test_cookie(fixture.keyring, options.session_options);
961
+ const res = await fixture.transport(logout_route.path, {
994
962
  method: 'POST',
995
963
  headers: {
996
964
  host: 'localhost',
@@ -998,60 +966,60 @@ export const describe_standard_integration_tests = (options) => {
998
966
  },
999
967
  });
1000
968
  assert.strictEqual(res.status, 401, 'Expired session cookie should be rejected on POST');
1001
- error_collector.record(test_app.route_specs, 'POST', logout_route.path, 401);
969
+ error_collector.record(route_specs, 'POST', logout_route.path, 401);
1002
970
  });
1003
971
  });
1004
972
  // --- 12. Bearer token browser context on mutation routes ---
1005
973
  describe('bearer token browser context silently discarded on mutations', () => {
1006
974
  test('bearer token with Origin header discarded on POST logout', async () => {
1007
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
1008
- const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
975
+ const fixture = await options.setup_test();
976
+ const logout_route = find_auth_route(route_specs, '/logout', 'POST');
1009
977
  assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
1010
- const bearer_headers = test_app.create_bearer_headers({
978
+ const bearer_headers = fixture.create_bearer_headers({
1011
979
  'content-type': 'application/json',
1012
980
  });
1013
- const res = await test_app.app.request(logout_route.path, {
981
+ const res = await fixture.fresh_transport()(logout_route.path, {
1014
982
  method: 'POST',
1015
983
  headers: { ...bearer_headers, origin: 'http://localhost:5173' },
1016
984
  });
1017
985
  assert.strictEqual(res.status, 401, 'Bearer with Origin should be discarded → unauthenticated');
1018
- error_collector.record(test_app.route_specs, 'POST', logout_route.path, 401);
986
+ error_collector.record(route_specs, 'POST', logout_route.path, 401);
1019
987
  });
1020
988
  test('bearer token with Referer header discarded on POST password', async () => {
1021
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
1022
- const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
989
+ const fixture = await options.setup_test();
990
+ const password_route = find_auth_route(route_specs, '/password', 'POST');
1023
991
  assert.ok(password_route, 'Expected POST /password route — ensure create_route_specs includes account routes');
1024
- const bearer_headers = test_app.create_bearer_headers({
992
+ const bearer_headers = fixture.create_bearer_headers({
1025
993
  'content-type': 'application/json',
1026
994
  });
1027
- const res = await test_app.app.request(password_route.path, {
995
+ const res = await fixture.fresh_transport()(password_route.path, {
1028
996
  method: 'POST',
1029
997
  headers: { ...bearer_headers, referer: 'http://localhost:5173/admin' },
1030
998
  });
1031
999
  assert.strictEqual(res.status, 401, 'Bearer with Referer should be discarded → unauthenticated');
1032
- error_collector.record(test_app.route_specs, 'POST', password_route.path, 401);
1000
+ error_collector.record(route_specs, 'POST', password_route.path, 401);
1033
1001
  });
1034
1002
  });
1035
1003
  // --- 13. Password change revokes API tokens ---
1036
1004
  describe('password change revokes API tokens', () => {
1037
1005
  test('API tokens are invalidated after password change', async () => {
1038
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
1039
- const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
1006
+ const fixture = await options.setup_test();
1007
+ const password_route = find_auth_route(route_specs, '/password', 'POST');
1040
1008
  assert.ok(password_route, 'Expected POST /password route');
1041
1009
  // Create an API token via RPC
1042
1010
  const create_res = await rpc_call_for_spec({
1043
- app: test_app.app,
1011
+ app: { request: fixture.transport },
1044
1012
  path: rpc_path,
1045
1013
  spec: account_token_create_action_spec,
1046
1014
  params: { name: 'test-token' },
1047
- headers: test_app.create_session_headers(),
1015
+ headers: fixture.create_session_headers(),
1048
1016
  });
1049
1017
  assert.ok(create_res.ok, 'account_token_create should succeed');
1050
1018
  const { token: raw_token } = create_res.result;
1051
1019
  assert.ok(raw_token, 'Expected raw token in create response');
1052
1020
  // Verify bearer token works
1053
1021
  const verify_before = await rpc_call_for_spec({
1054
- app: test_app.app,
1022
+ app: { request: fixture.transport },
1055
1023
  path: rpc_path,
1056
1024
  spec: account_verify_action_spec,
1057
1025
  params: undefined,
@@ -1060,11 +1028,11 @@ export const describe_standard_integration_tests = (options) => {
1060
1028
  });
1061
1029
  assert.strictEqual(verify_before.status, 200, 'Bearer token should work before password change');
1062
1030
  // Change password (still REST)
1063
- const change_res = await test_app.app.request(password_route.path, {
1031
+ const change_res = await fixture.transport(password_route.path, {
1064
1032
  method: 'POST',
1065
- headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
1033
+ headers: fixture.create_session_headers({ 'content-type': 'application/json' }),
1066
1034
  body: JSON.stringify({
1067
- current_password: 'test-password-123',
1035
+ current_password: DEFAULT_TEST_PASSWORD,
1068
1036
  new_password: 'new-password-456',
1069
1037
  }),
1070
1038
  });
@@ -1074,7 +1042,7 @@ export const describe_standard_integration_tests = (options) => {
1074
1042
  assert.ok(change_body.tokens_revoked >= 1, 'Expected at least 1 token revoked');
1075
1043
  // Bearer token should now be invalid
1076
1044
  const verify_after = await rpc_call_for_spec({
1077
- app: test_app.app,
1045
+ app: { request: fixture.transport },
1078
1046
  path: rpc_path,
1079
1047
  spec: account_verify_action_spec,
1080
1048
  params: undefined,
@@ -1087,8 +1055,8 @@ export const describe_standard_integration_tests = (options) => {
1087
1055
  // --- 14. Signup invite edge cases ---
1088
1056
  describe('signup invite edge cases', () => {
1089
1057
  test('signup with non-matching email cannot claim another email invite', async () => {
1090
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
1091
- const signup_route = test_app.route_specs.find((s) => s.method === 'POST' && s.path.endsWith('/signup') && is_public_auth(s.auth));
1058
+ const fixture = await options.setup_test();
1059
+ const signup_route = route_specs.find((s) => s.method === 'POST' && s.path.endsWith('/signup') && is_public_auth(s.auth));
1092
1060
  if (!signup_route)
1093
1061
  return; // signup is optional
1094
1062
  // `invite_create` lives on the RPC surface; consumers that don't
@@ -1096,22 +1064,28 @@ export const describe_standard_integration_tests = (options) => {
1096
1064
  // rather than fail.
1097
1065
  if (!find_rpc_action(rpc_endpoints_for_setup, invite_create_action_spec.method))
1098
1066
  return;
1099
- // Create an admin to manage invites
1100
- const admin = await test_app.create_account({
1101
- username: 'invite_edge_admin',
1102
- roles: ['admin'],
1103
- });
1067
+ // Drive the admin-gated `invite_create` over the fixture's
1068
+ // session. In both modes the fixture is the fresh keeper,
1069
+ // holding `[ROLE_KEEPER, ROLE_ADMIN, ...extra_keeper_roles]`
1070
+ // from the bootstrap-equivalent seed step — `extra_keeper_roles:
1071
+ // [ROLE_ADMIN]` is technically redundant for the admin suite
1072
+ // but harmless. `fixture.create_session_headers()` resolves to
1073
+ // a session with admin role.
1074
+ // `open_signup` stays at production default (`false`) across both
1075
+ // in-process (per-test bootstrap default) and cross-process (the
1076
+ // invite-gated `mint_account` flow doesn't need it flipped), so
1077
+ // the assertion fires without any mid-test setting toggle.
1104
1078
  // Create invite for alice@example.com via RPC
1105
1079
  const invite_res = await rpc_call_for_spec({
1106
- app: test_app.app,
1080
+ app: { request: fixture.transport },
1107
1081
  path: rpc_path,
1108
1082
  spec: invite_create_action_spec,
1109
1083
  params: { email: 'alice@example.com' },
1110
- headers: { cookie: `${cookie_name}=${admin.session_cookie}` },
1084
+ headers: fixture.create_session_headers(),
1111
1085
  });
1112
1086
  assert.ok(invite_res.ok, `invite_create failed: ${invite_res.ok ? '' : JSON.stringify(invite_res.error)}`);
1113
1087
  // Try to sign up with a different email — should fail (no matching invite)
1114
- const signup_res = await test_app.app.request(signup_route.path, {
1088
+ const signup_res = await fixture.transport(signup_route.path, {
1115
1089
  method: 'POST',
1116
1090
  headers: {
1117
1091
  host: 'localhost',
@@ -1132,25 +1106,30 @@ export const describe_standard_integration_tests = (options) => {
1132
1106
  // --- 15. Signup response body identity ---
1133
1107
  describe('signup response body identity', () => {
1134
1108
  test('no-invite and conflict failure responses are structurally identical', async () => {
1135
- const test_app = await create_test_app(build_test_app_options(options, get_db()));
1109
+ const fixture = await options.setup_test();
1136
1110
  // Find signup route (POST ending in /signup, public)
1137
- const signup_route = test_app.route_specs.find((s) => s.method === 'POST' && s.path.endsWith('/signup') && is_public_auth(s.auth));
1111
+ const signup_route = route_specs.find((s) => s.method === 'POST' && s.path.endsWith('/signup') && is_public_auth(s.auth));
1138
1112
  if (!signup_route)
1139
1113
  return; // signup is optional
1140
1114
  // `invite_create` lives on the RPC surface; consumers that don't
1141
1115
  // wire admin RPC actions can't exercise invites.
1142
1116
  if (!find_rpc_action(rpc_endpoints_for_setup, invite_create_action_spec.method))
1143
1117
  return;
1144
- // We need admin access create an admin account
1145
- const admin = await test_app.create_account({
1146
- username: 'signup_test_admin',
1147
- roles: ['admin'],
1148
- });
1149
- const admin_headers = { cookie: `${cookie_name}=${admin.session_cookie}` };
1118
+ // Drive admin-gated calls over the fixture's session — see the
1119
+ // previous test's comment for the in-process / cross-process
1120
+ // admin-resolution paths. This test creates a separate
1121
+ // `existing_user` for the username-conflict assertion, so
1122
+ // reusing the fixture's admin session for the invite-creating
1123
+ // path has no cross-account-isolation impact on the test's intent.
1124
+ const admin_headers = fixture.create_session_headers();
1125
+ // Mint the conflict-test sibling account for the username-conflict
1126
+ // assertion below. Both transports preserve hardcoded usernames
1127
+ // now that fresh-keeper-per-test wipes the DB between tests.
1128
+ const existing_user = await fixture.create_account({ username: 'existing_user' });
1150
1129
  // Create an invite for a specific test email via RPC
1151
1130
  const test_email = 'signup-test@example.com';
1152
1131
  const invite_res = await rpc_call_for_spec({
1153
- app: test_app.app,
1132
+ app: { request: fixture.transport },
1154
1133
  path: rpc_path,
1155
1134
  spec: invite_create_action_spec,
1156
1135
  params: { email: test_email },
@@ -1158,7 +1137,7 @@ export const describe_standard_integration_tests = (options) => {
1158
1137
  });
1159
1138
  assert.ok(invite_res.ok, `invite_create failed: ${invite_res.ok ? '' : JSON.stringify(invite_res.error)}`);
1160
1139
  // Attempt 1: signup with a non-matching email (no invite match) → 403
1161
- const no_match_res = await test_app.app.request(signup_route.path, {
1140
+ const no_match_res = await fixture.transport(signup_route.path, {
1162
1141
  method: 'POST',
1163
1142
  headers: {
1164
1143
  host: 'localhost',
@@ -1173,14 +1152,10 @@ export const describe_standard_integration_tests = (options) => {
1173
1152
  });
1174
1153
  assert.strictEqual(no_match_res.status, 403, 'Expected 403 for non-matching invite');
1175
1154
  const no_match_body = await no_match_res.json();
1176
- // For conflict test: create a second account with a known username,
1177
- // then create an invite for a different email, then try signup with
1178
- // the invited email but the colliding username
1179
- const existing_user = await test_app.create_account({ username: 'existing_user' });
1180
1155
  // Create invite for a different email via RPC
1181
1156
  const conflict_email = 'conflict-test@example.com';
1182
1157
  const invite2_res = await rpc_call_for_spec({
1183
- app: test_app.app,
1158
+ app: { request: fixture.transport },
1184
1159
  path: rpc_path,
1185
1160
  spec: invite_create_action_spec,
1186
1161
  params: { email: conflict_email },
@@ -1188,7 +1163,7 @@ export const describe_standard_integration_tests = (options) => {
1188
1163
  });
1189
1164
  assert.ok(invite2_res.ok, `invite2_create failed: ${invite2_res.ok ? '' : JSON.stringify(invite2_res.error)}`);
1190
1165
  // Attempt 2: signup with the invited email but a colliding username → 409
1191
- const conflict_res = await test_app.app.request(signup_route.path, {
1166
+ const conflict_res = await fixture.transport(signup_route.path, {
1192
1167
  method: 'POST',
1193
1168
  headers: {
1194
1169
  host: 'localhost',