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