@fuzdev/fuz_app 0.33.0 → 0.34.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
  */
@@ -107,15 +122,15 @@ export const describe_standard_admin_integration_tests = (options) => {
107
122
  * `describe_rpc_round_trip_tests` + fuz_app's own action suite.
108
123
  */
109
124
  const offer_and_accept = async (args) => {
110
- const res = await rpc_call({
125
+ const res = await rpc_call_for_spec({
111
126
  app: args.app,
112
127
  path: rpc_path,
113
- method: permit_offer_create_action_spec.method,
128
+ spec: permit_offer_create_action_spec,
114
129
  params: { to_account_id: args.to_account_id, role: args.role },
115
130
  headers: args.admin_headers,
116
131
  });
117
132
  assert.ok(res.ok, `permit_offer_create failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
118
- const offer = res.result.offer;
133
+ const { offer } = res.result;
119
134
  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
135
  return { offer_id: offer.id, permit_id: accept_result.permit.id };
121
136
  };
@@ -124,28 +139,29 @@ export const describe_standard_admin_integration_tests = (options) => {
124
139
  test('admin can list all accounts', async () => {
125
140
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
126
141
  const user_two = await test_app.create_account({ username: 'user_two' });
127
- const res = await rpc_call({
142
+ const res = await rpc_call_for_spec({
128
143
  app: test_app.app,
129
144
  path: rpc_path,
130
- method: admin_account_list_action_spec.method,
145
+ spec: admin_account_list_action_spec,
146
+ params: null,
131
147
  headers: test_app.create_session_headers(),
132
148
  });
133
149
  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');
150
+ assert.ok(Array.isArray(res.result.accounts), 'Expected accounts array');
151
+ assert.ok(res.result.accounts.length >= 2, 'Expected at least 2 accounts');
152
+ assert.ok(Array.isArray(res.result.grantable_roles), 'Expected grantable_roles array');
138
153
  // Verify user_two appears in the listing
139
- const found = result.accounts.find((e) => e.account.id === user_two.account.id);
154
+ const found = res.result.accounts.find((e) => e.account.id === user_two.account.id);
140
155
  assert.ok(found, 'Expected user_two in accounts listing');
141
156
  });
142
157
  test('non-admin cannot list accounts', async () => {
143
158
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db(), [ROLE_KEEPER]));
144
159
  captured_route_specs ??= test_app.route_specs;
145
- const res = await rpc_call({
160
+ const res = await rpc_call_for_spec({
146
161
  app: test_app.app,
147
162
  path: rpc_path,
148
- method: admin_account_list_action_spec.method,
163
+ spec: admin_account_list_action_spec,
164
+ params: null,
149
165
  headers: test_app.create_session_headers(),
150
166
  });
151
167
  assert.ok(!res.ok, 'Expected admin_account_list to fail for non-admin');
@@ -165,45 +181,46 @@ export const describe_standard_admin_integration_tests = (options) => {
165
181
  test('admin can list all active sessions', async () => {
166
182
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
167
183
  await test_app.create_account({ username: 'user_two' });
168
- const res = await rpc_call({
184
+ const res = await rpc_call_for_spec({
169
185
  app: test_app.app,
170
186
  path: rpc_path,
171
- method: admin_session_list_action_spec.method,
187
+ spec: admin_session_list_action_spec,
188
+ params: null,
172
189
  headers: test_app.create_session_headers(),
173
190
  });
174
191
  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');
192
+ assert.ok(Array.isArray(res.result.sessions), 'Expected sessions array');
193
+ assert.ok(res.result.sessions.length >= 2, 'Expected sessions from multiple accounts');
178
194
  });
179
195
  test('admin can revoke all sessions for another account', async () => {
180
196
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
181
197
  const user_two = await test_app.create_account({ username: 'user_two' });
182
198
  // Verify user_two's session works via `account_verify` RPC
183
- const before = await rpc_call({
199
+ const before = await rpc_call_for_spec({
184
200
  app: test_app.app,
185
201
  path: rpc_path,
186
- method: account_verify_action_spec.method,
202
+ spec: account_verify_action_spec,
203
+ params: null,
187
204
  headers: create_headers(user_two.session_cookie),
188
205
  });
189
206
  assert.strictEqual(before.status, 200);
190
207
  // Admin revokes all sessions for user_two via RPC
191
- const res = await rpc_call({
208
+ const res = await rpc_call_for_spec({
192
209
  app: test_app.app,
193
210
  path: rpc_path,
194
- method: admin_session_revoke_all_action_spec.method,
211
+ spec: admin_session_revoke_all_action_spec,
195
212
  params: { account_id: user_two.account.id },
196
213
  headers: test_app.create_session_headers(),
197
214
  });
198
215
  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');
216
+ assert.strictEqual(res.result.ok, true);
217
+ assert.ok(res.result.count >= 1, 'Expected at least 1 revoked session');
202
218
  // Verify user_two's session no longer works
203
- const after = await rpc_call({
219
+ const after = await rpc_call_for_spec({
204
220
  app: test_app.app,
205
221
  path: rpc_path,
206
- method: account_verify_action_spec.method,
222
+ spec: account_verify_action_spec,
223
+ params: null,
207
224
  headers: create_headers(user_two.session_cookie),
208
225
  });
209
226
  assert.strictEqual(after.status, 401);
@@ -211,22 +228,22 @@ export const describe_standard_admin_integration_tests = (options) => {
211
228
  test('admin revoking own sessions invalidates own session', async () => {
212
229
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
213
230
  // Admin revokes own sessions via RPC
214
- const res = await rpc_call({
231
+ const res = await rpc_call_for_spec({
215
232
  app: test_app.app,
216
233
  path: rpc_path,
217
- method: admin_session_revoke_all_action_spec.method,
234
+ spec: admin_session_revoke_all_action_spec,
218
235
  params: { account_id: test_app.backend.account.id },
219
236
  headers: test_app.create_session_headers(),
220
237
  });
221
238
  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');
239
+ assert.strictEqual(res.result.ok, true);
240
+ assert.ok(res.result.count >= 1, 'Expected at least 1 revoked session');
225
241
  // Admin's own session should no longer work
226
- const after = await rpc_call({
242
+ const after = await rpc_call_for_spec({
227
243
  app: test_app.app,
228
244
  path: rpc_path,
229
- method: account_verify_action_spec.method,
245
+ spec: account_verify_action_spec,
246
+ params: null,
230
247
  headers: test_app.create_session_headers(),
231
248
  });
232
249
  assert.strictEqual(after.status, 401);
@@ -238,31 +255,34 @@ export const describe_standard_admin_integration_tests = (options) => {
238
255
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
239
256
  const user_two = await test_app.create_account({ username: 'user_two' });
240
257
  // Verify user_two's bearer token works via `account_verify` RPC
241
- const before = await rpc_call_non_browser({
258
+ const before = await rpc_call_for_spec({
242
259
  app: test_app.app,
243
260
  path: rpc_path,
244
- method: account_verify_action_spec.method,
261
+ spec: account_verify_action_spec,
262
+ params: null,
245
263
  headers: { authorization: `Bearer ${user_two.api_token}` },
264
+ suppress_default_origin: true,
246
265
  });
247
266
  assert.strictEqual(before.status, 200);
248
267
  // Admin revokes all tokens for user_two via RPC
249
- const res = await rpc_call({
268
+ const res = await rpc_call_for_spec({
250
269
  app: test_app.app,
251
270
  path: rpc_path,
252
- method: admin_token_revoke_all_action_spec.method,
271
+ spec: admin_token_revoke_all_action_spec,
253
272
  params: { account_id: user_two.account.id },
254
273
  headers: test_app.create_session_headers(),
255
274
  });
256
275
  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');
276
+ assert.strictEqual(res.result.ok, true);
277
+ assert.ok(res.result.count >= 1, 'Expected at least 1 revoked token');
260
278
  // Verify user_two's bearer token no longer works
261
- const after = await rpc_call_non_browser({
279
+ const after = await rpc_call_for_spec({
262
280
  app: test_app.app,
263
281
  path: rpc_path,
264
- method: account_verify_action_spec.method,
282
+ spec: account_verify_action_spec,
283
+ params: null,
265
284
  headers: { authorization: `Bearer ${user_two.api_token}` },
285
+ suppress_default_origin: true,
266
286
  });
267
287
  assert.strictEqual(after.status, 401);
268
288
  });
@@ -271,40 +291,39 @@ export const describe_standard_admin_integration_tests = (options) => {
271
291
  describe('audit log RPC reads', () => {
272
292
  test('admin can list audit log events', async () => {
273
293
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
274
- const res = await rpc_call({
294
+ const res = await rpc_call_for_spec({
275
295
  app: test_app.app,
276
296
  path: rpc_path,
277
- method: audit_log_list_action_spec.method,
297
+ spec: audit_log_list_action_spec,
298
+ params: {},
278
299
  headers: test_app.create_session_headers(),
279
300
  });
280
301
  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');
302
+ assert.ok(Array.isArray(res.result.events), 'Expected events array');
283
303
  });
284
304
  test('audit log supports event_type filter', async () => {
285
305
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
286
306
  // Admin offer emits `permit_offer_create`. The downstream
287
307
  // `permit_grant` only fires on accept — out of scope for this test.
288
308
  const user_two = await test_app.create_account({ username: 'user_two' });
289
- const offer_res = await rpc_call({
309
+ const offer_res = await rpc_call_for_spec({
290
310
  app: test_app.app,
291
311
  path: rpc_path,
292
- method: permit_offer_create_action_spec.method,
312
+ spec: permit_offer_create_action_spec,
293
313
  params: { to_account_id: user_two.account.id, role: grantable_role },
294
314
  headers: test_app.create_session_headers(),
295
315
  });
296
316
  assert.ok(offer_res.ok, 'permit_offer_create should succeed');
297
- const res = await rpc_call({
317
+ const res = await rpc_call_for_spec({
298
318
  app: test_app.app,
299
319
  path: rpc_path,
300
- method: audit_log_list_action_spec.method,
320
+ spec: audit_log_list_action_spec,
301
321
  params: { event_type: 'permit_offer_create' },
302
322
  headers: test_app.create_session_headers(),
303
323
  });
304
324
  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) {
325
+ assert.ok(res.result.events.length >= 1, 'Expected at least 1 permit_offer_create event');
326
+ for (const event of res.result.events) {
308
327
  assert.strictEqual(event.event_type, 'permit_offer_create');
309
328
  }
310
329
  });
@@ -319,15 +338,15 @@ export const describe_standard_admin_integration_tests = (options) => {
319
338
  to_account_id: user_two.account.id,
320
339
  role: grantable_role,
321
340
  });
322
- const res = await rpc_call({
341
+ const res = await rpc_call_for_spec({
323
342
  app: test_app.app,
324
343
  path: rpc_path,
325
- method: audit_log_permit_history_action_spec.method,
344
+ spec: audit_log_permit_history_action_spec,
345
+ params: {},
326
346
  headers: test_app.create_session_headers(),
327
347
  });
328
348
  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');
349
+ assert.ok(res.result.events.length >= 1, 'Expected at least 1 permit history event');
331
350
  });
332
351
  });
333
352
  // --- 6. Admin audit trail ---
@@ -343,86 +362,83 @@ export const describe_standard_admin_integration_tests = (options) => {
343
362
  granted_by: test_app.backend.actor.id,
344
363
  });
345
364
  // Revoke via RPC
346
- const revoke_res = await rpc_call({
365
+ const revoke_res = await rpc_call_for_spec({
347
366
  app: test_app.app,
348
367
  path: rpc_path,
349
- method: permit_revoke_action_spec.method,
368
+ spec: permit_revoke_action_spec,
350
369
  params: { actor_id: target_actor.id, permit_id: permit.id },
351
370
  headers: test_app.create_session_headers(),
352
371
  });
353
372
  assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
354
373
  // Check audit log for permit_revoke event
355
- const audit_res = await rpc_call({
374
+ const audit_res = await rpc_call_for_spec({
356
375
  app: test_app.app,
357
376
  path: rpc_path,
358
- method: audit_log_list_action_spec.method,
377
+ spec: audit_log_list_action_spec,
359
378
  params: { event_type: 'permit_revoke' },
360
379
  headers: test_app.create_session_headers(),
361
380
  });
362
381
  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');
382
+ assert.ok(audit_res.result.events.length >= 1, 'Expected permit_revoke audit event');
383
+ assert.strictEqual(audit_res.result.events[0].event_type, 'permit_revoke');
366
384
  });
367
385
  test('admin session revoke-all creates audit event', async () => {
368
386
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
369
387
  const user_two = await test_app.create_account({ username: 'user_two' });
370
388
  // Revoke all sessions for user_two via RPC
371
- const revoke_res = await rpc_call({
389
+ const revoke_res = await rpc_call_for_spec({
372
390
  app: test_app.app,
373
391
  path: rpc_path,
374
- method: admin_session_revoke_all_action_spec.method,
392
+ spec: admin_session_revoke_all_action_spec,
375
393
  params: { account_id: user_two.account.id },
376
394
  headers: test_app.create_session_headers(),
377
395
  });
378
396
  assert.ok(revoke_res.ok, `admin_session_revoke_all failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
379
397
  // Check audit log
380
- const audit_res = await rpc_call({
398
+ const audit_res = await rpc_call_for_spec({
381
399
  app: test_app.app,
382
400
  path: rpc_path,
383
- method: audit_log_list_action_spec.method,
401
+ spec: audit_log_list_action_spec,
384
402
  params: { event_type: 'session_revoke_all' },
385
403
  headers: test_app.create_session_headers(),
386
404
  });
387
405
  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');
406
+ assert.ok(audit_res.result.events.length >= 1, 'Expected session_revoke_all audit event');
407
+ assert.strictEqual(audit_res.result.events[0].event_type, 'session_revoke_all');
391
408
  });
392
409
  test('admin token revoke-all creates audit event', async () => {
393
410
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
394
411
  const user_two = await test_app.create_account({ username: 'user_two' });
395
412
  // Revoke all tokens for user_two via RPC
396
- const revoke_res = await rpc_call({
413
+ const revoke_res = await rpc_call_for_spec({
397
414
  app: test_app.app,
398
415
  path: rpc_path,
399
- method: admin_token_revoke_all_action_spec.method,
416
+ spec: admin_token_revoke_all_action_spec,
400
417
  params: { account_id: user_two.account.id },
401
418
  headers: test_app.create_session_headers(),
402
419
  });
403
420
  assert.ok(revoke_res.ok, `admin_token_revoke_all failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
404
421
  // Check audit log
405
- const audit_res = await rpc_call({
422
+ const audit_res = await rpc_call_for_spec({
406
423
  app: test_app.app,
407
424
  path: rpc_path,
408
- method: audit_log_list_action_spec.method,
425
+ spec: audit_log_list_action_spec,
409
426
  params: { event_type: 'token_revoke_all' },
410
427
  headers: test_app.create_session_headers(),
411
428
  });
412
429
  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');
430
+ assert.ok(audit_res.result.events.length >= 1, 'Expected token_revoke_all audit event');
431
+ assert.strictEqual(audit_res.result.events[0].event_type, 'token_revoke_all');
416
432
  });
417
433
  test('admin session revoke-all 404 emits failure audit', async () => {
418
434
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
419
435
  // `Uuid = z.uuid()` is v4-strict; use a valid v4 shape so we hit the
420
436
  // handler's account lookup rather than failing at param validation.
421
437
  const missing_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaa01';
422
- const res = await rpc_call({
438
+ const res = await rpc_call_for_spec({
423
439
  app: test_app.app,
424
440
  path: rpc_path,
425
- method: admin_session_revoke_all_action_spec.method,
441
+ spec: admin_session_revoke_all_action_spec,
426
442
  params: { account_id: missing_id },
427
443
  headers: test_app.create_session_headers(),
428
444
  });
@@ -432,48 +448,48 @@ export const describe_standard_admin_integration_tests = (options) => {
432
448
  // Failure audit row should be visible on the audit-log feed.
433
449
  // `target_account_id` is null (FK prevents referencing a missing id)
434
450
  // — the probed id is preserved under `metadata.attempted_account_id`.
435
- const audit_res = await rpc_call({
451
+ const audit_res = await rpc_call_for_spec({
436
452
  app: test_app.app,
437
453
  path: rpc_path,
438
- method: audit_log_list_action_spec.method,
454
+ spec: audit_log_list_action_spec,
439
455
  params: { event_type: 'session_revoke_all' },
440
456
  headers: test_app.create_session_headers(),
441
457
  });
442
458
  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');
459
+ const failure = audit_res.result.events.find((e) => e.outcome === 'failure');
445
460
  assert.ok(failure, 'Expected a failure-outcome session_revoke_all audit event');
446
461
  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);
462
+ const failure_meta = failure.metadata;
463
+ assert.strictEqual(failure_meta.reason, 'account_not_found');
464
+ assert.strictEqual(failure_meta.attempted_account_id, missing_id);
449
465
  });
450
466
  test('admin token revoke-all 404 emits failure audit', async () => {
451
467
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
452
468
  const missing_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaa02';
453
- const res = await rpc_call({
469
+ const res = await rpc_call_for_spec({
454
470
  app: test_app.app,
455
471
  path: rpc_path,
456
- method: admin_token_revoke_all_action_spec.method,
472
+ spec: admin_token_revoke_all_action_spec,
457
473
  params: { account_id: missing_id },
458
474
  headers: test_app.create_session_headers(),
459
475
  });
460
476
  assert.ok(!res.ok, 'Expected 404 for missing account');
461
477
  assert.strictEqual(res.status, 404);
462
478
  assert.strictEqual(res.error.data.reason, 'account_not_found');
463
- const audit_res = await rpc_call({
479
+ const audit_res = await rpc_call_for_spec({
464
480
  app: test_app.app,
465
481
  path: rpc_path,
466
- method: audit_log_list_action_spec.method,
482
+ spec: audit_log_list_action_spec,
467
483
  params: { event_type: 'token_revoke_all' },
468
484
  headers: test_app.create_session_headers(),
469
485
  });
470
486
  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');
487
+ const failure = audit_res.result.events.find((e) => e.outcome === 'failure');
473
488
  assert.ok(failure, 'Expected a failure-outcome token_revoke_all audit event');
474
489
  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);
490
+ const failure_meta = failure.metadata;
491
+ assert.strictEqual(failure_meta.reason, 'account_not_found');
492
+ assert.strictEqual(failure_meta.attempted_account_id, missing_id);
477
493
  });
478
494
  });
479
495
  // --- 7. Audit log completeness ---
@@ -527,19 +543,19 @@ export const describe_standard_admin_integration_tests = (options) => {
527
543
  // 4. revoke permit (RPC)
528
544
  const target_actor = await query_actor_by_account({ db: get_db() }, user_two.account.id);
529
545
  assert.ok(target_actor);
530
- const revoke_res = await rpc_call({
546
+ const revoke_res = await rpc_call_for_spec({
531
547
  app: test_app.app,
532
548
  path: rpc_path,
533
- method: permit_revoke_action_spec.method,
549
+ spec: permit_revoke_action_spec,
534
550
  params: { actor_id: target_actor.id, permit_id },
535
551
  headers: test_app.create_session_headers(),
536
552
  });
537
553
  assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
538
554
  // 5. create token (RPC)
539
- const token_res = await rpc_call({
555
+ const token_res = await rpc_call_for_spec({
540
556
  app: test_app.app,
541
557
  path: rpc_path,
542
- method: account_token_create_action_spec.method,
558
+ spec: account_token_create_action_spec,
543
559
  params: { name: 'audit-test-token' },
544
560
  headers: test_app.create_session_headers(),
545
561
  });
@@ -578,15 +594,15 @@ export const describe_standard_admin_integration_tests = (options) => {
578
594
  origin: 'http://localhost:5173',
579
595
  cookie: `${cookie_name}=${relogin_match[1]}`,
580
596
  };
581
- const audit_res = await rpc_call({
597
+ const audit_res = await rpc_call_for_spec({
582
598
  app: test_app.app,
583
599
  path: rpc_path,
584
- method: audit_log_list_action_spec.method,
600
+ spec: audit_log_list_action_spec,
601
+ params: {},
585
602
  headers: relogin_headers,
586
603
  });
587
604
  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;
605
+ const events = audit_res.result.events;
590
606
  // check that each operation produced at least one event.
591
607
  // `permit_offer_create` fires on the admin RPC; `permit_grant`
592
608
  // fires when the recipient accepts (driven by offer_and_accept).
@@ -625,16 +641,15 @@ export const describe_standard_admin_integration_tests = (options) => {
625
641
  granted_by: test_app.backend.actor.id,
626
642
  });
627
643
  // Admin B revokes their own permit via RPC — should succeed
628
- const revoke_res = await rpc_call({
644
+ const revoke_res = await rpc_call_for_spec({
629
645
  app: test_app.app,
630
646
  path: rpc_path,
631
- method: permit_revoke_action_spec.method,
647
+ spec: permit_revoke_action_spec,
632
648
  params: { actor_id: admin_b.actor.id, permit_id: permit.id },
633
649
  headers: create_headers(admin_b.session_cookie),
634
650
  });
635
651
  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);
652
+ assert.strictEqual(revoke_res.result.revoked, true);
638
653
  });
639
654
  test('admin revoke-all sessions for another admin works', async () => {
640
655
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
@@ -643,17 +658,16 @@ export const describe_standard_admin_integration_tests = (options) => {
643
658
  roles: ['admin'],
644
659
  });
645
660
  // Admin A revokes all of admin B's sessions via RPC
646
- const res = await rpc_call({
661
+ const res = await rpc_call_for_spec({
647
662
  app: test_app.app,
648
663
  path: rpc_path,
649
- method: admin_session_revoke_all_action_spec.method,
664
+ spec: admin_session_revoke_all_action_spec,
650
665
  params: { account_id: admin_b.account.id },
651
666
  headers: create_headers(test_app.backend.session_cookie),
652
667
  });
653
668
  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');
669
+ assert.ok(typeof res.result.count === 'number', 'Expected count field in response');
670
+ assert.ok(res.result.count >= 1, 'Expected at least 1 session revoked');
657
671
  });
658
672
  test('admin revoke-all tokens for another admin works', async () => {
659
673
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
@@ -662,36 +676,36 @@ export const describe_standard_admin_integration_tests = (options) => {
662
676
  roles: ['admin'],
663
677
  });
664
678
  // Admin B creates an API token via RPC
665
- const token_res = await rpc_call({
679
+ const token_res = await rpc_call_for_spec({
666
680
  app: test_app.app,
667
681
  path: rpc_path,
668
- method: account_token_create_action_spec.method,
682
+ spec: account_token_create_action_spec,
669
683
  params: { name: 'admin-b-token' },
670
684
  headers: create_headers(admin_b.session_cookie),
671
685
  });
672
686
  assert.ok(token_res.ok, `account_token_create failed: ${token_res.ok ? '' : JSON.stringify(token_res.error)}`);
673
687
  // Admin A revokes all of admin B's tokens via RPC
674
- const res = await rpc_call({
688
+ const res = await rpc_call_for_spec({
675
689
  app: test_app.app,
676
690
  path: rpc_path,
677
- method: admin_token_revoke_all_action_spec.method,
691
+ spec: admin_token_revoke_all_action_spec,
678
692
  params: { account_id: admin_b.account.id },
679
693
  headers: create_headers(test_app.backend.session_cookie),
680
694
  });
681
695
  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');
696
+ assert.ok(typeof res.result.count === 'number', 'Expected count field in response');
684
697
  // Token was created above, so revoke should evict at least one.
685
- assert.ok(result.count >= 1, 'Expected at least 1 token revoked');
698
+ assert.ok(res.result.count >= 1, 'Expected at least 1 token revoked');
686
699
  });
687
700
  test('non-admin cannot access admin routes for another account', async () => {
688
701
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
689
702
  const regular_user = await test_app.create_account({ username: 'regular_user_iso' });
690
703
  // Regular user tries to list accounts via the admin RPC — should 403
691
- const res = await rpc_call({
704
+ const res = await rpc_call_for_spec({
692
705
  app: test_app.app,
693
706
  path: rpc_path,
694
- method: admin_account_list_action_spec.method,
707
+ spec: admin_account_list_action_spec,
708
+ params: null,
695
709
  headers: create_headers(regular_user.session_cookie),
696
710
  });
697
711
  assert.ok(!res.ok, 'Expected admin_account_list to fail for non-admin');
@@ -703,10 +717,16 @@ export const describe_standard_admin_integration_tests = (options) => {
703
717
  test('exercises 401/403 on admin routes for error coverage', async () => {
704
718
  const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
705
719
  captured_route_specs ??= test_app.route_specs;
720
+ // Post-RPC migration, `/api/admin` is nearly empty — admin reads
721
+ // and mutations live on the RPC endpoint behind spec-level role
722
+ // auth. The path-prefix carve is still the right scope here
723
+ // because error coverage is tracked against REST `RouteSpec`s,
724
+ // not RPC method specs (`describe_rpc_round_trip_tests` covers
725
+ // the admin RPC surface separately).
706
726
  const prefix = options.admin_prefix ?? '/api/admin';
707
727
  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)) {
728
+ // Hit admin routes without auth to exercise 401 error schemas.
729
+ for (const route of admin_routes) {
710
730
  const res = await test_app.app.request(route.path, {
711
731
  method: route.method,
712
732
  headers: { host: 'localhost' },
@@ -717,24 +737,5 @@ export const describe_standard_admin_integration_tests = (options) => {
717
737
  }
718
738
  });
719
739
  });
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
740
  });
740
741
  };