@fuzdev/fuz_app 0.33.0 → 0.35.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.
@@ -10,6 +10,19 @@ import './assert_dev_env.js';
10
10
  * Consumers call it with their route factory, session config, role schema,
11
11
  * and RPC endpoint specs — all admin route tests come for free.
12
12
  *
13
+ * Scope: admin *semantics* — cross-admin isolation, permit grant/revoke
14
+ * flow, session/token revoke-all, audit writes. Output-schema conformance
15
+ * for admin methods is **not** the concern of this suite; it lives in:
16
+ *
17
+ * - `describe_rpc_round_trip_tests` — every RPC method (admin methods
18
+ * included) is hit with a spec-generated valid body and the 2xx result
19
+ * is validated against `spec.output`.
20
+ * - `describe_round_trip_validation` — every REST route is hit and
21
+ * validated against its declared `output` / error schemas (SSE routes
22
+ * skipped via `Content-Type: text/event-stream`).
23
+ * - `describe_sse_route_tests` — SSE frames validated against their
24
+ * declared `EventSpec`.
25
+ *
13
26
  * @module
14
27
  */
15
28
  import { describe, test, assert, afterAll } from 'vitest';
@@ -17,10 +30,10 @@ import { ROLE_KEEPER, ROLE_ADMIN } from '../auth/role_schema.js';
17
30
  import { AUTH_MIGRATION_NS } from '../auth/migrations.js';
18
31
  import { create_test_app } from './app_server.js';
19
32
  import { create_pglite_factory, create_describe_db, AUTH_INTEGRATION_TRUNCATE_TABLES, } from './db.js';
20
- import { find_auth_route, assert_response_matches_spec } from './integration_helpers.js';
33
+ import { find_auth_route } from './integration_helpers.js';
21
34
  import { run_migrations } from '../db/migrate.js';
22
35
  import { ErrorCoverageCollector, assert_error_coverage, DEFAULT_INTEGRATION_ERROR_COVERAGE, } from './error_coverage.js';
23
- import { rpc_call, rpc_call_non_browser, require_rpc_endpoint_path, resolve_rpc_endpoints_for_setup, } from './rpc_helpers.js';
36
+ import { rpc_call_for_spec, require_rpc_endpoint_path, resolve_rpc_endpoints_for_setup, } from './rpc_helpers.js';
24
37
  import { permit_offer_create_action_spec, permit_revoke_action_spec, } from '../auth/permit_offer_action_specs.js';
25
38
  import { admin_account_list_action_spec, admin_session_list_action_spec, admin_session_revoke_all_action_spec, admin_token_revoke_all_action_spec, audit_log_list_action_spec, audit_log_permit_history_action_spec, } from '../auth/admin_action_specs.js';
26
39
  import { account_token_create_action_spec, account_verify_action_spec, } from '../auth/account_action_specs.js';
@@ -54,8 +67,10 @@ const build_admin_test_app_options = (options, db, roles) => ({
54
67
  * Standard admin integration test suite for fuz_app admin routes.
55
68
  *
56
69
  * Exercises account listing, permit grant/revoke (via RPC), session
57
- * management, token management, audit log routes, admin-to-admin isolation,
58
- * and response schema validation.
70
+ * management, token management, audit log reads, admin-to-admin
71
+ * isolation, and 401/403 error-coverage on the admin REST surface.
72
+ * Output-schema conformance is not in scope — see the module docstring
73
+ * for the suites that cover it.
59
74
  *
60
75
  * @param options - session config, route factory, role schema, RPC endpoints
61
76
  */
@@ -86,10 +101,30 @@ export const describe_standard_admin_integration_tests = (options) => {
86
101
  // admin REST route remaining is the optional
87
102
  // `GET /audit-log/stream` SSE, plus the shared RPC endpoint
88
103
  // path itself (admin methods live behind spec-level role auth).
104
+ // The `/audit-log/stream` suffix tracks the hardcoded path in
105
+ // `auth/audit_log_routes.ts` — if consumers ever need to mount
106
+ // the audit SSE at a different suffix, promote this to an
107
+ // `audit_log_path_suffix` option on
108
+ // `StandardAdminIntegrationTestOptions`.
89
109
  const admin_routes = captured_route_specs.filter((s) => s.path.endsWith('/audit-log/stream') &&
90
110
  s.auth.type === 'role' &&
91
111
  s.auth.role === 'admin');
92
- assert_error_coverage(error_collector, admin_routes.length > 0 ? admin_routes : captured_route_specs, { min_coverage: DEFAULT_INTEGRATION_ERROR_COVERAGE });
112
+ // Adaptive threshold: when the scoped admin REST surface is
113
+ // effectively empty (0–1 routes, typical post-RPC-migration),
114
+ // the 20% baseline is meaningless — a single SSE route that
115
+ // can't be exercised against an error schema drops the ratio
116
+ // to 0.0%. Log an informational skip instead of asserting.
117
+ // The admin RPC surface is covered by
118
+ // `describe_rpc_round_trip_tests`, not this collector.
119
+ if (admin_routes.length <= 1) {
120
+ console.log(`[error coverage] skipped admin REST coverage assertion — ` +
121
+ `scoped surface has ${admin_routes.length} route(s); ` +
122
+ `admin RPC surface is covered by describe_rpc_round_trip_tests`);
123
+ return;
124
+ }
125
+ assert_error_coverage(error_collector, admin_routes, {
126
+ min_coverage: DEFAULT_INTEGRATION_ERROR_COVERAGE,
127
+ });
93
128
  }
94
129
  });
95
130
  /** Make request headers for a given session cookie. */
@@ -107,15 +142,15 @@ export const describe_standard_admin_integration_tests = (options) => {
107
142
  * `describe_rpc_round_trip_tests` + fuz_app's own action suite.
108
143
  */
109
144
  const offer_and_accept = async (args) => {
110
- const res = await rpc_call({
145
+ const res = await rpc_call_for_spec({
111
146
  app: args.app,
112
147
  path: rpc_path,
113
- method: permit_offer_create_action_spec.method,
148
+ spec: permit_offer_create_action_spec,
114
149
  params: { to_account_id: args.to_account_id, role: args.role },
115
150
  headers: args.admin_headers,
116
151
  });
117
152
  assert.ok(res.ok, `permit_offer_create failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
118
- const offer = res.result.offer;
153
+ const { offer } = res.result;
119
154
  const accept_result = await get_db().transaction(async (tx) => query_accept_offer({ db: tx }, { offer_id: offer.id, to_account_id: args.to_account_id, ip: null }));
120
155
  return { offer_id: offer.id, permit_id: accept_result.permit.id };
121
156
  };
@@ -124,28 +159,29 @@ export const describe_standard_admin_integration_tests = (options) => {
124
159
  test('admin can list all accounts', async () => {
125
160
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
126
161
  const user_two = await test_app.create_account({ username: 'user_two' });
127
- const res = await rpc_call({
162
+ const res = await rpc_call_for_spec({
128
163
  app: test_app.app,
129
164
  path: rpc_path,
130
- method: admin_account_list_action_spec.method,
165
+ spec: admin_account_list_action_spec,
166
+ params: null,
131
167
  headers: test_app.create_session_headers(),
132
168
  });
133
169
  assert.ok(res.ok, `admin_account_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
134
- const result = res.result;
135
- assert.ok(Array.isArray(result.accounts), 'Expected accounts array');
136
- assert.ok(result.accounts.length >= 2, 'Expected at least 2 accounts');
137
- assert.ok(Array.isArray(result.grantable_roles), 'Expected grantable_roles array');
170
+ assert.ok(Array.isArray(res.result.accounts), 'Expected accounts array');
171
+ assert.ok(res.result.accounts.length >= 2, 'Expected at least 2 accounts');
172
+ assert.ok(Array.isArray(res.result.grantable_roles), 'Expected grantable_roles array');
138
173
  // Verify user_two appears in the listing
139
- const found = result.accounts.find((e) => e.account.id === user_two.account.id);
174
+ const found = res.result.accounts.find((e) => e.account.id === user_two.account.id);
140
175
  assert.ok(found, 'Expected user_two in accounts listing');
141
176
  });
142
177
  test('non-admin cannot list accounts', async () => {
143
178
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db(), [ROLE_KEEPER]));
144
179
  captured_route_specs ??= test_app.route_specs;
145
- const res = await rpc_call({
180
+ const res = await rpc_call_for_spec({
146
181
  app: test_app.app,
147
182
  path: rpc_path,
148
- method: admin_account_list_action_spec.method,
183
+ spec: admin_account_list_action_spec,
184
+ params: null,
149
185
  headers: test_app.create_session_headers(),
150
186
  });
151
187
  assert.ok(!res.ok, 'Expected admin_account_list to fail for non-admin');
@@ -165,45 +201,46 @@ export const describe_standard_admin_integration_tests = (options) => {
165
201
  test('admin can list all active sessions', async () => {
166
202
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
167
203
  await test_app.create_account({ username: 'user_two' });
168
- const res = await rpc_call({
204
+ const res = await rpc_call_for_spec({
169
205
  app: test_app.app,
170
206
  path: rpc_path,
171
- method: admin_session_list_action_spec.method,
207
+ spec: admin_session_list_action_spec,
208
+ params: null,
172
209
  headers: test_app.create_session_headers(),
173
210
  });
174
211
  assert.ok(res.ok, `admin_session_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
175
- const body = res.result;
176
- assert.ok(Array.isArray(body.sessions), 'Expected sessions array');
177
- assert.ok(body.sessions.length >= 2, 'Expected sessions from multiple accounts');
212
+ assert.ok(Array.isArray(res.result.sessions), 'Expected sessions array');
213
+ assert.ok(res.result.sessions.length >= 2, 'Expected sessions from multiple accounts');
178
214
  });
179
215
  test('admin can revoke all sessions for another account', async () => {
180
216
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
181
217
  const user_two = await test_app.create_account({ username: 'user_two' });
182
218
  // Verify user_two's session works via `account_verify` RPC
183
- const before = await rpc_call({
219
+ const before = await rpc_call_for_spec({
184
220
  app: test_app.app,
185
221
  path: rpc_path,
186
- method: account_verify_action_spec.method,
222
+ spec: account_verify_action_spec,
223
+ params: null,
187
224
  headers: create_headers(user_two.session_cookie),
188
225
  });
189
226
  assert.strictEqual(before.status, 200);
190
227
  // Admin revokes all sessions for user_two via RPC
191
- const res = await rpc_call({
228
+ const res = await rpc_call_for_spec({
192
229
  app: test_app.app,
193
230
  path: rpc_path,
194
- method: admin_session_revoke_all_action_spec.method,
231
+ spec: admin_session_revoke_all_action_spec,
195
232
  params: { account_id: user_two.account.id },
196
233
  headers: test_app.create_session_headers(),
197
234
  });
198
235
  assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
199
- const result = res.result;
200
- assert.strictEqual(result.ok, true);
201
- assert.ok(result.count >= 1, 'Expected at least 1 revoked session');
236
+ assert.strictEqual(res.result.ok, true);
237
+ assert.ok(res.result.count >= 1, 'Expected at least 1 revoked session');
202
238
  // Verify user_two's session no longer works
203
- const after = await rpc_call({
239
+ const after = await rpc_call_for_spec({
204
240
  app: test_app.app,
205
241
  path: rpc_path,
206
- method: account_verify_action_spec.method,
242
+ spec: account_verify_action_spec,
243
+ params: null,
207
244
  headers: create_headers(user_two.session_cookie),
208
245
  });
209
246
  assert.strictEqual(after.status, 401);
@@ -211,22 +248,22 @@ export const describe_standard_admin_integration_tests = (options) => {
211
248
  test('admin revoking own sessions invalidates own session', async () => {
212
249
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
213
250
  // Admin revokes own sessions via RPC
214
- const res = await rpc_call({
251
+ const res = await rpc_call_for_spec({
215
252
  app: test_app.app,
216
253
  path: rpc_path,
217
- method: admin_session_revoke_all_action_spec.method,
254
+ spec: admin_session_revoke_all_action_spec,
218
255
  params: { account_id: test_app.backend.account.id },
219
256
  headers: test_app.create_session_headers(),
220
257
  });
221
258
  assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
222
- const result = res.result;
223
- assert.strictEqual(result.ok, true);
224
- assert.ok(result.count >= 1, 'Expected at least 1 revoked session');
259
+ assert.strictEqual(res.result.ok, true);
260
+ assert.ok(res.result.count >= 1, 'Expected at least 1 revoked session');
225
261
  // Admin's own session should no longer work
226
- const after = await rpc_call({
262
+ const after = await rpc_call_for_spec({
227
263
  app: test_app.app,
228
264
  path: rpc_path,
229
- method: account_verify_action_spec.method,
265
+ spec: account_verify_action_spec,
266
+ params: null,
230
267
  headers: test_app.create_session_headers(),
231
268
  });
232
269
  assert.strictEqual(after.status, 401);
@@ -238,31 +275,34 @@ export const describe_standard_admin_integration_tests = (options) => {
238
275
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
239
276
  const user_two = await test_app.create_account({ username: 'user_two' });
240
277
  // Verify user_two's bearer token works via `account_verify` RPC
241
- const before = await rpc_call_non_browser({
278
+ const before = await rpc_call_for_spec({
242
279
  app: test_app.app,
243
280
  path: rpc_path,
244
- method: account_verify_action_spec.method,
281
+ spec: account_verify_action_spec,
282
+ params: null,
245
283
  headers: { authorization: `Bearer ${user_two.api_token}` },
284
+ suppress_default_origin: true,
246
285
  });
247
286
  assert.strictEqual(before.status, 200);
248
287
  // Admin revokes all tokens for user_two via RPC
249
- const res = await rpc_call({
288
+ const res = await rpc_call_for_spec({
250
289
  app: test_app.app,
251
290
  path: rpc_path,
252
- method: admin_token_revoke_all_action_spec.method,
291
+ spec: admin_token_revoke_all_action_spec,
253
292
  params: { account_id: user_two.account.id },
254
293
  headers: test_app.create_session_headers(),
255
294
  });
256
295
  assert.ok(res.ok, `admin_token_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
257
- const result = res.result;
258
- assert.strictEqual(result.ok, true);
259
- assert.ok(result.count >= 1, 'Expected at least 1 revoked token');
296
+ assert.strictEqual(res.result.ok, true);
297
+ assert.ok(res.result.count >= 1, 'Expected at least 1 revoked token');
260
298
  // Verify user_two's bearer token no longer works
261
- const after = await rpc_call_non_browser({
299
+ const after = await rpc_call_for_spec({
262
300
  app: test_app.app,
263
301
  path: rpc_path,
264
- method: account_verify_action_spec.method,
302
+ spec: account_verify_action_spec,
303
+ params: null,
265
304
  headers: { authorization: `Bearer ${user_two.api_token}` },
305
+ suppress_default_origin: true,
266
306
  });
267
307
  assert.strictEqual(after.status, 401);
268
308
  });
@@ -271,40 +311,39 @@ export const describe_standard_admin_integration_tests = (options) => {
271
311
  describe('audit log RPC reads', () => {
272
312
  test('admin can list audit log events', async () => {
273
313
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
274
- const res = await rpc_call({
314
+ const res = await rpc_call_for_spec({
275
315
  app: test_app.app,
276
316
  path: rpc_path,
277
- method: audit_log_list_action_spec.method,
317
+ spec: audit_log_list_action_spec,
318
+ params: {},
278
319
  headers: test_app.create_session_headers(),
279
320
  });
280
321
  assert.ok(res.ok, `audit_log_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
281
- const body = res.result;
282
- assert.ok(Array.isArray(body.events), 'Expected events array');
322
+ assert.ok(Array.isArray(res.result.events), 'Expected events array');
283
323
  });
284
324
  test('audit log supports event_type filter', async () => {
285
325
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
286
326
  // Admin offer emits `permit_offer_create`. The downstream
287
327
  // `permit_grant` only fires on accept — out of scope for this test.
288
328
  const user_two = await test_app.create_account({ username: 'user_two' });
289
- const offer_res = await rpc_call({
329
+ const offer_res = await rpc_call_for_spec({
290
330
  app: test_app.app,
291
331
  path: rpc_path,
292
- method: permit_offer_create_action_spec.method,
332
+ spec: permit_offer_create_action_spec,
293
333
  params: { to_account_id: user_two.account.id, role: grantable_role },
294
334
  headers: test_app.create_session_headers(),
295
335
  });
296
336
  assert.ok(offer_res.ok, 'permit_offer_create should succeed');
297
- const res = await rpc_call({
337
+ const res = await rpc_call_for_spec({
298
338
  app: test_app.app,
299
339
  path: rpc_path,
300
- method: audit_log_list_action_spec.method,
340
+ spec: audit_log_list_action_spec,
301
341
  params: { event_type: 'permit_offer_create' },
302
342
  headers: test_app.create_session_headers(),
303
343
  });
304
344
  assert.ok(res.ok, `audit_log_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
305
- const body = res.result;
306
- assert.ok(body.events.length >= 1, 'Expected at least 1 permit_offer_create event');
307
- for (const event of body.events) {
345
+ assert.ok(res.result.events.length >= 1, 'Expected at least 1 permit_offer_create event');
346
+ for (const event of res.result.events) {
308
347
  assert.strictEqual(event.event_type, 'permit_offer_create');
309
348
  }
310
349
  });
@@ -319,15 +358,15 @@ export const describe_standard_admin_integration_tests = (options) => {
319
358
  to_account_id: user_two.account.id,
320
359
  role: grantable_role,
321
360
  });
322
- const res = await rpc_call({
361
+ const res = await rpc_call_for_spec({
323
362
  app: test_app.app,
324
363
  path: rpc_path,
325
- method: audit_log_permit_history_action_spec.method,
364
+ spec: audit_log_permit_history_action_spec,
365
+ params: {},
326
366
  headers: test_app.create_session_headers(),
327
367
  });
328
368
  assert.ok(res.ok, `audit_log_permit_history failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
329
- const body = res.result;
330
- assert.ok(body.events.length >= 1, 'Expected at least 1 permit history event');
369
+ assert.ok(res.result.events.length >= 1, 'Expected at least 1 permit history event');
331
370
  });
332
371
  });
333
372
  // --- 6. Admin audit trail ---
@@ -343,86 +382,83 @@ export const describe_standard_admin_integration_tests = (options) => {
343
382
  granted_by: test_app.backend.actor.id,
344
383
  });
345
384
  // Revoke via RPC
346
- const revoke_res = await rpc_call({
385
+ const revoke_res = await rpc_call_for_spec({
347
386
  app: test_app.app,
348
387
  path: rpc_path,
349
- method: permit_revoke_action_spec.method,
388
+ spec: permit_revoke_action_spec,
350
389
  params: { actor_id: target_actor.id, permit_id: permit.id },
351
390
  headers: test_app.create_session_headers(),
352
391
  });
353
392
  assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
354
393
  // Check audit log for permit_revoke event
355
- const audit_res = await rpc_call({
394
+ const audit_res = await rpc_call_for_spec({
356
395
  app: test_app.app,
357
396
  path: rpc_path,
358
- method: audit_log_list_action_spec.method,
397
+ spec: audit_log_list_action_spec,
359
398
  params: { event_type: 'permit_revoke' },
360
399
  headers: test_app.create_session_headers(),
361
400
  });
362
401
  assert.ok(audit_res.ok, `audit_log_list failed: ${audit_res.ok ? '' : JSON.stringify(audit_res.error)}`);
363
- const audit_body = audit_res.result;
364
- assert.ok(audit_body.events.length >= 1, 'Expected permit_revoke audit event');
365
- assert.strictEqual(audit_body.events[0].event_type, 'permit_revoke');
402
+ assert.ok(audit_res.result.events.length >= 1, 'Expected permit_revoke audit event');
403
+ assert.strictEqual(audit_res.result.events[0].event_type, 'permit_revoke');
366
404
  });
367
405
  test('admin session revoke-all creates audit event', async () => {
368
406
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
369
407
  const user_two = await test_app.create_account({ username: 'user_two' });
370
408
  // Revoke all sessions for user_two via RPC
371
- const revoke_res = await rpc_call({
409
+ const revoke_res = await rpc_call_for_spec({
372
410
  app: test_app.app,
373
411
  path: rpc_path,
374
- method: admin_session_revoke_all_action_spec.method,
412
+ spec: admin_session_revoke_all_action_spec,
375
413
  params: { account_id: user_two.account.id },
376
414
  headers: test_app.create_session_headers(),
377
415
  });
378
416
  assert.ok(revoke_res.ok, `admin_session_revoke_all failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
379
417
  // Check audit log
380
- const audit_res = await rpc_call({
418
+ const audit_res = await rpc_call_for_spec({
381
419
  app: test_app.app,
382
420
  path: rpc_path,
383
- method: audit_log_list_action_spec.method,
421
+ spec: audit_log_list_action_spec,
384
422
  params: { event_type: 'session_revoke_all' },
385
423
  headers: test_app.create_session_headers(),
386
424
  });
387
425
  assert.ok(audit_res.ok, 'audit_log_list should succeed');
388
- const audit_body = audit_res.result;
389
- assert.ok(audit_body.events.length >= 1, 'Expected session_revoke_all audit event');
390
- assert.strictEqual(audit_body.events[0].event_type, 'session_revoke_all');
426
+ assert.ok(audit_res.result.events.length >= 1, 'Expected session_revoke_all audit event');
427
+ assert.strictEqual(audit_res.result.events[0].event_type, 'session_revoke_all');
391
428
  });
392
429
  test('admin token revoke-all creates audit event', async () => {
393
430
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
394
431
  const user_two = await test_app.create_account({ username: 'user_two' });
395
432
  // Revoke all tokens for user_two via RPC
396
- const revoke_res = await rpc_call({
433
+ const revoke_res = await rpc_call_for_spec({
397
434
  app: test_app.app,
398
435
  path: rpc_path,
399
- method: admin_token_revoke_all_action_spec.method,
436
+ spec: admin_token_revoke_all_action_spec,
400
437
  params: { account_id: user_two.account.id },
401
438
  headers: test_app.create_session_headers(),
402
439
  });
403
440
  assert.ok(revoke_res.ok, `admin_token_revoke_all failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
404
441
  // Check audit log
405
- const audit_res = await rpc_call({
442
+ const audit_res = await rpc_call_for_spec({
406
443
  app: test_app.app,
407
444
  path: rpc_path,
408
- method: audit_log_list_action_spec.method,
445
+ spec: audit_log_list_action_spec,
409
446
  params: { event_type: 'token_revoke_all' },
410
447
  headers: test_app.create_session_headers(),
411
448
  });
412
449
  assert.ok(audit_res.ok, 'audit_log_list should succeed');
413
- const audit_body = audit_res.result;
414
- assert.ok(audit_body.events.length >= 1, 'Expected token_revoke_all audit event');
415
- assert.strictEqual(audit_body.events[0].event_type, 'token_revoke_all');
450
+ assert.ok(audit_res.result.events.length >= 1, 'Expected token_revoke_all audit event');
451
+ assert.strictEqual(audit_res.result.events[0].event_type, 'token_revoke_all');
416
452
  });
417
453
  test('admin session revoke-all 404 emits failure audit', async () => {
418
454
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
419
455
  // `Uuid = z.uuid()` is v4-strict; use a valid v4 shape so we hit the
420
456
  // handler's account lookup rather than failing at param validation.
421
457
  const missing_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaa01';
422
- const res = await rpc_call({
458
+ const res = await rpc_call_for_spec({
423
459
  app: test_app.app,
424
460
  path: rpc_path,
425
- method: admin_session_revoke_all_action_spec.method,
461
+ spec: admin_session_revoke_all_action_spec,
426
462
  params: { account_id: missing_id },
427
463
  headers: test_app.create_session_headers(),
428
464
  });
@@ -432,48 +468,48 @@ export const describe_standard_admin_integration_tests = (options) => {
432
468
  // Failure audit row should be visible on the audit-log feed.
433
469
  // `target_account_id` is null (FK prevents referencing a missing id)
434
470
  // — the probed id is preserved under `metadata.attempted_account_id`.
435
- const audit_res = await rpc_call({
471
+ const audit_res = await rpc_call_for_spec({
436
472
  app: test_app.app,
437
473
  path: rpc_path,
438
- method: audit_log_list_action_spec.method,
474
+ spec: audit_log_list_action_spec,
439
475
  params: { event_type: 'session_revoke_all' },
440
476
  headers: test_app.create_session_headers(),
441
477
  });
442
478
  assert.ok(audit_res.ok, 'audit_log_list should succeed');
443
- const audit_body = audit_res.result;
444
- const failure = audit_body.events.find((e) => e.outcome === 'failure');
479
+ const failure = audit_res.result.events.find((e) => e.outcome === 'failure');
445
480
  assert.ok(failure, 'Expected a failure-outcome session_revoke_all audit event');
446
481
  assert.strictEqual(failure.target_account_id, null);
447
- assert.strictEqual(failure.metadata.reason, 'account_not_found');
448
- assert.strictEqual(failure.metadata.attempted_account_id, missing_id);
482
+ const failure_meta = failure.metadata;
483
+ assert.strictEqual(failure_meta.reason, 'account_not_found');
484
+ assert.strictEqual(failure_meta.attempted_account_id, missing_id);
449
485
  });
450
486
  test('admin token revoke-all 404 emits failure audit', async () => {
451
487
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
452
488
  const missing_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaa02';
453
- const res = await rpc_call({
489
+ const res = await rpc_call_for_spec({
454
490
  app: test_app.app,
455
491
  path: rpc_path,
456
- method: admin_token_revoke_all_action_spec.method,
492
+ spec: admin_token_revoke_all_action_spec,
457
493
  params: { account_id: missing_id },
458
494
  headers: test_app.create_session_headers(),
459
495
  });
460
496
  assert.ok(!res.ok, 'Expected 404 for missing account');
461
497
  assert.strictEqual(res.status, 404);
462
498
  assert.strictEqual(res.error.data.reason, 'account_not_found');
463
- const audit_res = await rpc_call({
499
+ const audit_res = await rpc_call_for_spec({
464
500
  app: test_app.app,
465
501
  path: rpc_path,
466
- method: audit_log_list_action_spec.method,
502
+ spec: audit_log_list_action_spec,
467
503
  params: { event_type: 'token_revoke_all' },
468
504
  headers: test_app.create_session_headers(),
469
505
  });
470
506
  assert.ok(audit_res.ok, 'audit_log_list should succeed');
471
- const audit_body = audit_res.result;
472
- const failure = audit_body.events.find((e) => e.outcome === 'failure');
507
+ const failure = audit_res.result.events.find((e) => e.outcome === 'failure');
473
508
  assert.ok(failure, 'Expected a failure-outcome token_revoke_all audit event');
474
509
  assert.strictEqual(failure.target_account_id, null);
475
- assert.strictEqual(failure.metadata.reason, 'account_not_found');
476
- assert.strictEqual(failure.metadata.attempted_account_id, missing_id);
510
+ const failure_meta = failure.metadata;
511
+ assert.strictEqual(failure_meta.reason, 'account_not_found');
512
+ assert.strictEqual(failure_meta.attempted_account_id, missing_id);
477
513
  });
478
514
  });
479
515
  // --- 7. Audit log completeness ---
@@ -527,19 +563,19 @@ export const describe_standard_admin_integration_tests = (options) => {
527
563
  // 4. revoke permit (RPC)
528
564
  const target_actor = await query_actor_by_account({ db: get_db() }, user_two.account.id);
529
565
  assert.ok(target_actor);
530
- const revoke_res = await rpc_call({
566
+ const revoke_res = await rpc_call_for_spec({
531
567
  app: test_app.app,
532
568
  path: rpc_path,
533
- method: permit_revoke_action_spec.method,
569
+ spec: permit_revoke_action_spec,
534
570
  params: { actor_id: target_actor.id, permit_id },
535
571
  headers: test_app.create_session_headers(),
536
572
  });
537
573
  assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
538
574
  // 5. create token (RPC)
539
- const token_res = await rpc_call({
575
+ const token_res = await rpc_call_for_spec({
540
576
  app: test_app.app,
541
577
  path: rpc_path,
542
- method: account_token_create_action_spec.method,
578
+ spec: account_token_create_action_spec,
543
579
  params: { name: 'audit-test-token' },
544
580
  headers: test_app.create_session_headers(),
545
581
  });
@@ -578,15 +614,15 @@ export const describe_standard_admin_integration_tests = (options) => {
578
614
  origin: 'http://localhost:5173',
579
615
  cookie: `${cookie_name}=${relogin_match[1]}`,
580
616
  };
581
- const audit_res = await rpc_call({
617
+ const audit_res = await rpc_call_for_spec({
582
618
  app: test_app.app,
583
619
  path: rpc_path,
584
- method: audit_log_list_action_spec.method,
620
+ spec: audit_log_list_action_spec,
621
+ params: {},
585
622
  headers: relogin_headers,
586
623
  });
587
624
  assert.ok(audit_res.ok, `audit_log_list failed: ${audit_res.ok ? '' : JSON.stringify(audit_res.error)}`);
588
- const audit_body = audit_res.result;
589
- const events = audit_body.events;
625
+ const events = audit_res.result.events;
590
626
  // check that each operation produced at least one event.
591
627
  // `permit_offer_create` fires on the admin RPC; `permit_grant`
592
628
  // fires when the recipient accepts (driven by offer_and_accept).
@@ -625,16 +661,15 @@ export const describe_standard_admin_integration_tests = (options) => {
625
661
  granted_by: test_app.backend.actor.id,
626
662
  });
627
663
  // Admin B revokes their own permit via RPC — should succeed
628
- const revoke_res = await rpc_call({
664
+ const revoke_res = await rpc_call_for_spec({
629
665
  app: test_app.app,
630
666
  path: rpc_path,
631
- method: permit_revoke_action_spec.method,
667
+ spec: permit_revoke_action_spec,
632
668
  params: { actor_id: admin_b.actor.id, permit_id: permit.id },
633
669
  headers: create_headers(admin_b.session_cookie),
634
670
  });
635
671
  assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
636
- const result = revoke_res.result;
637
- assert.strictEqual(result.revoked, true);
672
+ assert.strictEqual(revoke_res.result.revoked, true);
638
673
  });
639
674
  test('admin revoke-all sessions for another admin works', async () => {
640
675
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
@@ -643,17 +678,16 @@ export const describe_standard_admin_integration_tests = (options) => {
643
678
  roles: ['admin'],
644
679
  });
645
680
  // Admin A revokes all of admin B's sessions via RPC
646
- const res = await rpc_call({
681
+ const res = await rpc_call_for_spec({
647
682
  app: test_app.app,
648
683
  path: rpc_path,
649
- method: admin_session_revoke_all_action_spec.method,
684
+ spec: admin_session_revoke_all_action_spec,
650
685
  params: { account_id: admin_b.account.id },
651
686
  headers: create_headers(test_app.backend.session_cookie),
652
687
  });
653
688
  assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
654
- const result = res.result;
655
- assert.ok(typeof result.count === 'number', 'Expected count field in response');
656
- assert.ok(result.count >= 1, 'Expected at least 1 session revoked');
689
+ assert.ok(typeof res.result.count === 'number', 'Expected count field in response');
690
+ assert.ok(res.result.count >= 1, 'Expected at least 1 session revoked');
657
691
  });
658
692
  test('admin revoke-all tokens for another admin works', async () => {
659
693
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
@@ -662,36 +696,36 @@ export const describe_standard_admin_integration_tests = (options) => {
662
696
  roles: ['admin'],
663
697
  });
664
698
  // Admin B creates an API token via RPC
665
- const token_res = await rpc_call({
699
+ const token_res = await rpc_call_for_spec({
666
700
  app: test_app.app,
667
701
  path: rpc_path,
668
- method: account_token_create_action_spec.method,
702
+ spec: account_token_create_action_spec,
669
703
  params: { name: 'admin-b-token' },
670
704
  headers: create_headers(admin_b.session_cookie),
671
705
  });
672
706
  assert.ok(token_res.ok, `account_token_create failed: ${token_res.ok ? '' : JSON.stringify(token_res.error)}`);
673
707
  // Admin A revokes all of admin B's tokens via RPC
674
- const res = await rpc_call({
708
+ const res = await rpc_call_for_spec({
675
709
  app: test_app.app,
676
710
  path: rpc_path,
677
- method: admin_token_revoke_all_action_spec.method,
711
+ spec: admin_token_revoke_all_action_spec,
678
712
  params: { account_id: admin_b.account.id },
679
713
  headers: create_headers(test_app.backend.session_cookie),
680
714
  });
681
715
  assert.ok(res.ok, `admin_token_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
682
- const result = res.result;
683
- assert.ok(typeof result.count === 'number', 'Expected count field in response');
716
+ assert.ok(typeof res.result.count === 'number', 'Expected count field in response');
684
717
  // Token was created above, so revoke should evict at least one.
685
- assert.ok(result.count >= 1, 'Expected at least 1 token revoked');
718
+ assert.ok(res.result.count >= 1, 'Expected at least 1 token revoked');
686
719
  });
687
720
  test('non-admin cannot access admin routes for another account', async () => {
688
721
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
689
722
  const regular_user = await test_app.create_account({ username: 'regular_user_iso' });
690
723
  // Regular user tries to list accounts via the admin RPC — should 403
691
- const res = await rpc_call({
724
+ const res = await rpc_call_for_spec({
692
725
  app: test_app.app,
693
726
  path: rpc_path,
694
- method: admin_account_list_action_spec.method,
727
+ spec: admin_account_list_action_spec,
728
+ params: null,
695
729
  headers: create_headers(regular_user.session_cookie),
696
730
  });
697
731
  assert.ok(!res.ok, 'Expected admin_account_list to fail for non-admin');
@@ -703,10 +737,16 @@ export const describe_standard_admin_integration_tests = (options) => {
703
737
  test('exercises 401/403 on admin routes for error coverage', async () => {
704
738
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
705
739
  captured_route_specs ??= test_app.route_specs;
740
+ // Post-RPC migration, `/api/admin` is nearly empty — admin reads
741
+ // and mutations live on the RPC endpoint behind spec-level role
742
+ // auth. The path-prefix carve is still the right scope here
743
+ // because error coverage is tracked against REST `RouteSpec`s,
744
+ // not RPC method specs (`describe_rpc_round_trip_tests` covers
745
+ // the admin RPC surface separately).
706
746
  const prefix = options.admin_prefix ?? '/api/admin';
707
747
  const admin_routes = test_app.route_specs.filter((s) => s.path.startsWith(prefix) && s.auth.type === 'role' && s.auth.role === 'admin');
708
- // Hit admin routes without auth to exercise 401 error schemas
709
- for (const route of admin_routes.slice(0, 5)) {
748
+ // Hit admin routes without auth to exercise 401 error schemas.
749
+ for (const route of admin_routes) {
710
750
  const res = await test_app.app.request(route.path, {
711
751
  method: route.method,
712
752
  headers: { host: 'localhost' },
@@ -717,24 +757,5 @@ export const describe_standard_admin_integration_tests = (options) => {
717
757
  }
718
758
  });
719
759
  });
720
- // --- 8. Admin response schema validation ---
721
- describe('admin response schema validation', () => {
722
- test('admin route 200 responses match declared output schemas', async () => {
723
- const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
724
- const prefix = options.admin_prefix ?? '/api/admin';
725
- const admin_get_routes = test_app.route_specs.filter((s) => s.method === 'GET' &&
726
- s.path.startsWith(prefix) &&
727
- s.auth.type === 'role' &&
728
- s.auth.role === 'admin');
729
- assert.ok(admin_get_routes.length > 0, 'Expected at least one admin GET route — ensure create_route_specs includes admin routes');
730
- for (const route of admin_get_routes) {
731
- const res = await test_app.app.request(route.path, {
732
- headers: test_app.create_session_headers(),
733
- });
734
- assert.strictEqual(res.status, 200, `${route.method} ${route.path} should return 200`);
735
- await assert_response_matches_spec(test_app.route_specs, route.method, route.path, res);
736
- }
737
- });
738
- });
739
760
  });
740
761
  };