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