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