@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.
@@ -21,7 +21,7 @@ import { AUTH_MIGRATION_NS } from '../auth/migrations.js';
21
21
  import { create_test_app } from './app_server.js';
22
22
  import { create_pglite_factory, create_describe_db, AUTH_INTEGRATION_TRUNCATE_TABLES, } from './db.js';
23
23
  import { find_auth_route, assert_response_matches_spec, create_expired_test_cookie, assert_no_error_info_leakage, } from './integration_helpers.js';
24
- import { find_rpc_action, rpc_call, rpc_call_non_browser, require_rpc_endpoint_path, } from './rpc_helpers.js';
24
+ import { find_rpc_action, rpc_call_for_spec, require_rpc_endpoint_path, resolve_rpc_endpoints_for_setup, } from './rpc_helpers.js';
25
25
  import { RateLimiter } from '../rate_limiter.js';
26
26
  import { run_migrations } from '../db/migrate.js';
27
27
  import { ErrorCoverageCollector, assert_error_coverage, DEFAULT_INTEGRATION_ERROR_COVERAGE, } from './error_coverage.js';
@@ -30,12 +30,19 @@ import { account_verify_action_spec, account_session_list_action_spec, account_s
30
30
  import { invite_create_action_spec } from '../auth/admin_action_specs.js';
31
31
  /**
32
32
  * Build `CreateTestAppOptions` from standard options plus a database.
33
+ * Forwards `options.rpc_endpoints` to `app_options.rpc_endpoints` so
34
+ * `create_app_server` auto-mounts it per-test with the real ctx.
35
+ * `SuiteAppOptions` excludes `rpc_endpoints` so there's no way for
36
+ * `options.app_options` to collide with the suite-level field.
33
37
  */
34
38
  const build_test_app_options = (options, db) => ({
35
39
  session_options: options.session_options,
36
40
  create_route_specs: options.create_route_specs,
37
41
  db,
38
- app_options: options.app_options,
42
+ app_options: {
43
+ ...options.app_options,
44
+ rpc_endpoints: options.rpc_endpoints,
45
+ },
39
46
  });
40
47
  /**
41
48
  * Standard integration test suite for fuz_app auth routes.
@@ -53,8 +60,11 @@ const build_test_app_options = (options, db) => ({
53
60
  */
54
61
  export const describe_standard_integration_tests = (options) => {
55
62
  // Hard-fail early so consumers see a clear setup error instead of a
56
- // confusing test failure when `rpc_endpoints` is missing.
57
- const rpc_path = require_rpc_endpoint_path(options.rpc_endpoints);
63
+ // confusing test failure when `rpc_endpoints` is missing. Factory-form
64
+ // callers are resolved with a stub ctx purely to extract the endpoint
65
+ // path; real handlers run per-test via `app_options.rpc_endpoints`.
66
+ const rpc_endpoints_for_setup = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
67
+ const rpc_path = require_rpc_endpoint_path(rpc_endpoints_for_setup);
58
68
  const init_schema = async (db) => {
59
69
  await run_migrations(db, [AUTH_MIGRATION_NS]);
60
70
  };
@@ -211,10 +221,11 @@ export const describe_standard_integration_tests = (options) => {
211
221
  cookie: `${cookie_name}=${login_cookie}`,
212
222
  });
213
223
  // Verify works
214
- const verify_res = await rpc_call({
224
+ const verify_res = await rpc_call_for_spec({
215
225
  app: test_app.app,
216
226
  path: rpc_path,
217
- method: account_verify_action_spec.method,
227
+ spec: account_verify_action_spec,
228
+ params: null,
218
229
  headers: create_headers(),
219
230
  });
220
231
  assert.strictEqual(verify_res.status, 200);
@@ -228,10 +239,11 @@ export const describe_standard_integration_tests = (options) => {
228
239
  assert.strictEqual(logout_body.ok, true);
229
240
  assert.strictEqual(logout_body.username, test_app.backend.account.username, 'Logout response should include the username');
230
241
  // Verify fails after logout (session revoked)
231
- const verify_after = await rpc_call({
242
+ const verify_after = await rpc_call_for_spec({
232
243
  app: test_app.app,
233
244
  path: rpc_path,
234
- method: account_verify_action_spec.method,
245
+ spec: account_verify_action_spec,
246
+ params: null,
235
247
  headers: create_headers(),
236
248
  });
237
249
  assert.strictEqual(verify_after.status, 401);
@@ -299,20 +311,22 @@ export const describe_standard_integration_tests = (options) => {
299
311
  describe('session security', () => {
300
312
  test('no cookie on protected route returns 401', async () => {
301
313
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
302
- const res = await rpc_call({
314
+ const res = await rpc_call_for_spec({
303
315
  app: test_app.app,
304
316
  path: rpc_path,
305
- method: account_verify_action_spec.method,
317
+ spec: account_verify_action_spec,
318
+ params: null,
306
319
  headers: { host: 'localhost' },
307
320
  });
308
321
  assert.strictEqual(res.status, 401);
309
322
  });
310
323
  test('corrupted cookie returns 401', async () => {
311
324
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
312
- const res = await rpc_call({
325
+ const res = await rpc_call_for_spec({
313
326
  app: test_app.app,
314
327
  path: rpc_path,
315
- method: account_verify_action_spec.method,
328
+ spec: account_verify_action_spec,
329
+ params: null,
316
330
  headers: { cookie: `${cookie_name}=random_garbage_value` },
317
331
  });
318
332
  assert.strictEqual(res.status, 401);
@@ -320,10 +334,11 @@ export const describe_standard_integration_tests = (options) => {
320
334
  test('expired cookie returns 401', async () => {
321
335
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
322
336
  const expired_cookie = await create_expired_test_cookie(test_app.backend.keyring, options.session_options);
323
- const res = await rpc_call({
337
+ const res = await rpc_call_for_spec({
324
338
  app: test_app.app,
325
339
  path: rpc_path,
326
- method: account_verify_action_spec.method,
340
+ spec: account_verify_action_spec,
341
+ params: null,
327
342
  headers: { cookie: `${cookie_name}=${expired_cookie}` },
328
343
  });
329
344
  assert.strictEqual(res.status, 401);
@@ -335,33 +350,33 @@ export const describe_standard_integration_tests = (options) => {
335
350
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
336
351
  const headers = test_app.create_session_headers();
337
352
  // List own sessions to get the session ID
338
- const list_res = await rpc_call({
353
+ const list_res = await rpc_call_for_spec({
339
354
  app: test_app.app,
340
355
  path: rpc_path,
341
- method: account_session_list_action_spec.method,
356
+ spec: account_session_list_action_spec,
357
+ params: null,
342
358
  headers,
343
359
  });
344
360
  assert.ok(list_res.ok, 'account_session_list should succeed');
345
- const list_body = list_res.result;
346
- assert.ok(list_body.sessions.length >= 1);
347
- const session_id = list_body.sessions[0].id;
361
+ assert.ok(list_res.result.sessions.length >= 1);
362
+ const session_id = list_res.result.sessions[0].id;
348
363
  // Revoke that session by ID
349
- const revoke_res = await rpc_call({
364
+ const revoke_res = await rpc_call_for_spec({
350
365
  app: test_app.app,
351
366
  path: rpc_path,
352
- method: account_session_revoke_action_spec.method,
367
+ spec: account_session_revoke_action_spec,
353
368
  params: { session_id },
354
369
  headers,
355
370
  });
356
371
  assert.ok(revoke_res.ok, 'account_session_revoke should succeed');
357
- const revoke_body = revoke_res.result;
358
- assert.strictEqual(revoke_body.ok, true);
359
- assert.strictEqual(revoke_body.revoked, true);
372
+ assert.strictEqual(revoke_res.result.ok, true);
373
+ assert.strictEqual(revoke_res.result.revoked, true);
360
374
  // Session should no longer work
361
- const after = await rpc_call({
375
+ const after = await rpc_call_for_spec({
362
376
  app: test_app.app,
363
377
  path: rpc_path,
364
- method: account_verify_action_spec.method,
378
+ spec: account_verify_action_spec,
379
+ params: null,
365
380
  headers,
366
381
  });
367
382
  assert.strictEqual(after.status, 401);
@@ -370,26 +385,29 @@ export const describe_standard_integration_tests = (options) => {
370
385
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
371
386
  const headers = test_app.create_session_headers();
372
387
  // Verify works
373
- const before = await rpc_call({
388
+ const before = await rpc_call_for_spec({
374
389
  app: test_app.app,
375
390
  path: rpc_path,
376
- method: account_verify_action_spec.method,
391
+ spec: account_verify_action_spec,
392
+ params: null,
377
393
  headers,
378
394
  });
379
395
  assert.strictEqual(before.status, 200);
380
396
  // Revoke all sessions
381
- const revoke_res = await rpc_call({
397
+ const revoke_res = await rpc_call_for_spec({
382
398
  app: test_app.app,
383
399
  path: rpc_path,
384
- method: account_session_revoke_all_action_spec.method,
400
+ spec: account_session_revoke_all_action_spec,
401
+ params: null,
385
402
  headers,
386
403
  });
387
404
  assert.ok(revoke_res.ok, 'account_session_revoke_all should succeed');
388
405
  // Verify fails after revocation
389
- const after = await rpc_call({
406
+ const after = await rpc_call_for_spec({
390
407
  app: test_app.app,
391
408
  path: rpc_path,
392
- method: account_verify_action_spec.method,
409
+ spec: account_verify_action_spec,
410
+ params: null,
393
411
  headers,
394
412
  });
395
413
  assert.strictEqual(after.status, 401);
@@ -421,10 +439,11 @@ export const describe_standard_integration_tests = (options) => {
421
439
  assert.ok(typeof change_body.sessions_revoked === 'number', 'Expected sessions_revoked count');
422
440
  assert.ok(change_body.sessions_revoked >= 1, 'Expected at least 1 session revoked');
423
441
  // Old session should be invalid
424
- const verify_after = await rpc_call({
442
+ const verify_after = await rpc_call_for_spec({
425
443
  app: test_app.app,
426
444
  path: rpc_path,
427
- method: account_verify_action_spec.method,
445
+ spec: account_verify_action_spec,
446
+ params: null,
428
447
  headers: test_app.create_session_headers(),
429
448
  });
430
449
  assert.strictEqual(verify_after.status, 401);
@@ -462,10 +481,11 @@ export const describe_standard_integration_tests = (options) => {
462
481
  assert.strictEqual(res.status, 401);
463
482
  error_collector.record(test_app.route_specs, 'POST', password_route.path, 401);
464
483
  // Session should still be valid (password didn't change)
465
- const verify_res = await rpc_call({
484
+ const verify_res = await rpc_call_for_spec({
466
485
  app: test_app.app,
467
486
  path: rpc_path,
468
- method: account_verify_action_spec.method,
487
+ spec: account_verify_action_spec,
488
+ params: null,
469
489
  headers: test_app.create_session_headers(),
470
490
  });
471
491
  assert.strictEqual(verify_res.status, 200);
@@ -497,23 +517,26 @@ export const describe_standard_integration_tests = (options) => {
497
517
  });
498
518
  test('valid origin is accepted', async () => {
499
519
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
500
- const res = await rpc_call({
520
+ const res = await rpc_call_for_spec({
501
521
  app: test_app.app,
502
522
  path: rpc_path,
503
- method: account_verify_action_spec.method,
523
+ spec: account_verify_action_spec,
524
+ params: null,
504
525
  headers: test_app.create_session_headers(),
505
526
  });
506
527
  assert.strictEqual(res.status, 200);
507
528
  });
508
529
  test('no origin header is allowed (direct access)', async () => {
509
530
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
510
- // Probe the "no Origin / no Referer" path; `rpc_call_non_browser`
511
- // suppresses the default `origin` header.
512
- const res = await rpc_call_non_browser({
531
+ // Probe the "no Origin / no Referer" path; `suppress_default_origin`
532
+ // skips the default `origin` header.
533
+ const res = await rpc_call_for_spec({
513
534
  app: test_app.app,
514
535
  path: rpc_path,
515
- method: account_verify_action_spec.method,
536
+ spec: account_verify_action_spec,
537
+ params: null,
516
538
  headers: { cookie: `${cookie_name}=${test_app.backend.session_cookie}` },
539
+ suppress_default_origin: true,
517
540
  });
518
541
  assert.notStrictEqual(res.status, 403);
519
542
  });
@@ -522,21 +545,25 @@ export const describe_standard_integration_tests = (options) => {
522
545
  describe('bearer auth', () => {
523
546
  test('valid bearer token authenticates', async () => {
524
547
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
525
- const res = await rpc_call_non_browser({
548
+ const res = await rpc_call_for_spec({
526
549
  app: test_app.app,
527
550
  path: rpc_path,
528
- method: account_verify_action_spec.method,
551
+ spec: account_verify_action_spec,
552
+ params: null,
529
553
  headers: test_app.create_bearer_headers(),
554
+ suppress_default_origin: true,
530
555
  });
531
556
  assert.strictEqual(res.status, 200);
532
557
  });
533
558
  test('invalid bearer token returns 401', async () => {
534
559
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
535
- const res = await rpc_call_non_browser({
560
+ const res = await rpc_call_for_spec({
536
561
  app: test_app.app,
537
562
  path: rpc_path,
538
- method: account_verify_action_spec.method,
563
+ spec: account_verify_action_spec,
564
+ params: null,
539
565
  headers: { authorization: 'Bearer secret_fuz_token_invalid' },
566
+ suppress_default_origin: true,
540
567
  });
541
568
  assert.strictEqual(res.status, 401);
542
569
  });
@@ -544,18 +571,21 @@ export const describe_standard_integration_tests = (options) => {
544
571
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
545
572
  const bearer_headers = test_app.create_bearer_headers();
546
573
  // Without Origin — works.
547
- const ok_res = await rpc_call_non_browser({
574
+ const ok_res = await rpc_call_for_spec({
548
575
  app: test_app.app,
549
576
  path: rpc_path,
550
- method: account_verify_action_spec.method,
577
+ spec: account_verify_action_spec,
578
+ params: null,
551
579
  headers: bearer_headers,
580
+ suppress_default_origin: true,
552
581
  });
553
582
  assert.strictEqual(ok_res.status, 200);
554
583
  // With Origin — bearer silently discarded (browser context), falls through to no auth.
555
- const res = await rpc_call({
584
+ const res = await rpc_call_for_spec({
556
585
  app: test_app.app,
557
586
  path: rpc_path,
558
- method: account_verify_action_spec.method,
587
+ spec: account_verify_action_spec,
588
+ params: null,
559
589
  headers: { ...bearer_headers, origin: 'http://localhost:5173' },
560
590
  });
561
591
  assert.strictEqual(res.status, 401);
@@ -566,38 +596,42 @@ export const describe_standard_integration_tests = (options) => {
566
596
  test('revoked API token returns 401', async () => {
567
597
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
568
598
  // Create a new token via RPC
569
- const create_res = await rpc_call({
599
+ const create_res = await rpc_call_for_spec({
570
600
  app: test_app.app,
571
601
  path: rpc_path,
572
- method: account_token_create_action_spec.method,
602
+ spec: account_token_create_action_spec,
573
603
  params: { name: 'test-revoke' },
574
604
  headers: test_app.create_session_headers(),
575
605
  });
576
606
  assert.ok(create_res.ok, 'account_token_create should succeed');
577
607
  const { token, id } = create_res.result;
578
608
  // Verify token works
579
- const use_res = await rpc_call_non_browser({
609
+ const use_res = await rpc_call_for_spec({
580
610
  app: test_app.app,
581
611
  path: rpc_path,
582
- method: account_verify_action_spec.method,
612
+ spec: account_verify_action_spec,
613
+ params: null,
583
614
  headers: { authorization: `Bearer ${token}` },
615
+ suppress_default_origin: true,
584
616
  });
585
617
  assert.strictEqual(use_res.status, 200);
586
618
  // Revoke via RPC
587
- const revoke_res = await rpc_call({
619
+ const revoke_res = await rpc_call_for_spec({
588
620
  app: test_app.app,
589
621
  path: rpc_path,
590
- method: account_token_revoke_action_spec.method,
622
+ spec: account_token_revoke_action_spec,
591
623
  params: { token_id: id },
592
624
  headers: test_app.create_session_headers(),
593
625
  });
594
626
  assert.ok(revoke_res.ok, 'account_token_revoke should succeed');
595
627
  // Token should no longer work
596
- const after_res = await rpc_call_non_browser({
628
+ const after_res = await rpc_call_for_spec({
597
629
  app: test_app.app,
598
630
  path: rpc_path,
599
- method: account_verify_action_spec.method,
631
+ spec: account_verify_action_spec,
632
+ params: null,
600
633
  headers: { authorization: `Bearer ${token}` },
634
+ suppress_default_origin: true,
601
635
  });
602
636
  assert.strictEqual(after_res.status, 401);
603
637
  });
@@ -624,18 +658,20 @@ export const describe_standard_integration_tests = (options) => {
624
658
  // Create a second account
625
659
  const user_b = await test_app.create_account({ username: 'user_b' });
626
660
  // User A revokes all their own sessions
627
- const revoke_res = await rpc_call({
661
+ const revoke_res = await rpc_call_for_spec({
628
662
  app: test_app.app,
629
663
  path: rpc_path,
630
- method: account_session_revoke_all_action_spec.method,
664
+ spec: account_session_revoke_all_action_spec,
665
+ params: null,
631
666
  headers: test_app.create_session_headers(),
632
667
  });
633
668
  assert.ok(revoke_res.ok, 'account_session_revoke_all should succeed');
634
669
  // User B's session should still work
635
- const verify_b = await rpc_call({
670
+ const verify_b = await rpc_call_for_spec({
636
671
  app: test_app.app,
637
672
  path: rpc_path,
638
- method: account_verify_action_spec.method,
673
+ spec: account_verify_action_spec,
674
+ params: null,
639
675
  headers: { cookie: `${cookie_name}=${user_b.session_cookie}` },
640
676
  });
641
677
  assert.strictEqual(verify_b.status, 200);
@@ -645,32 +681,32 @@ export const describe_standard_integration_tests = (options) => {
645
681
  const user_b = await test_app.create_account({ username: 'user_b' });
646
682
  const user_b_headers = { cookie: `${cookie_name}=${user_b.session_cookie}` };
647
683
  // Get user B's session ID by listing as user B
648
- const list_res = await rpc_call({
684
+ const list_res = await rpc_call_for_spec({
649
685
  app: test_app.app,
650
686
  path: rpc_path,
651
- method: account_session_list_action_spec.method,
687
+ spec: account_session_list_action_spec,
688
+ params: null,
652
689
  headers: user_b_headers,
653
690
  });
654
691
  assert.ok(list_res.ok, 'account_session_list should succeed');
655
- const list_body = list_res.result;
656
- assert.ok(list_body.sessions.length >= 1);
657
- const session_id_b = list_body.sessions[0].id;
692
+ assert.ok(list_res.result.sessions.length >= 1);
693
+ const session_id_b = list_res.result.sessions[0].id;
658
694
  // User A tries to revoke user B's session by ID
659
- const revoke_res = await rpc_call({
695
+ const revoke_res = await rpc_call_for_spec({
660
696
  app: test_app.app,
661
697
  path: rpc_path,
662
- method: account_session_revoke_action_spec.method,
698
+ spec: account_session_revoke_action_spec,
663
699
  params: { session_id: session_id_b },
664
700
  headers: test_app.create_session_headers(),
665
701
  });
666
702
  assert.ok(revoke_res.ok, 'account_session_revoke should succeed');
667
- const revoke_body = revoke_res.result;
668
- assert.strictEqual(revoke_body.revoked, false, 'Should not revoke another account session');
703
+ assert.strictEqual(revoke_res.result.revoked, false, 'Should not revoke another account session');
669
704
  // User B's session should still work
670
- const verify_b = await rpc_call({
705
+ const verify_b = await rpc_call_for_spec({
671
706
  app: test_app.app,
672
707
  path: rpc_path,
673
- method: account_verify_action_spec.method,
708
+ spec: account_verify_action_spec,
709
+ params: null,
674
710
  headers: user_b_headers,
675
711
  });
676
712
  assert.strictEqual(verify_b.status, 200);
@@ -680,33 +716,34 @@ export const describe_standard_integration_tests = (options) => {
680
716
  const user_b = await test_app.create_account({ username: 'user_b' });
681
717
  const user_b_headers = { cookie: `${cookie_name}=${user_b.session_cookie}` };
682
718
  // Get user B's token ID by listing as user B
683
- const list_res = await rpc_call({
719
+ const list_res = await rpc_call_for_spec({
684
720
  app: test_app.app,
685
721
  path: rpc_path,
686
- method: account_token_list_action_spec.method,
722
+ spec: account_token_list_action_spec,
723
+ params: null,
687
724
  headers: user_b_headers,
688
725
  });
689
726
  assert.ok(list_res.ok, 'account_token_list should succeed');
690
- const list_body = list_res.result;
691
- assert.ok(list_body.tokens.length >= 1);
692
- const token_id_b = list_body.tokens[0].id;
727
+ assert.ok(list_res.result.tokens.length >= 1);
728
+ const token_id_b = list_res.result.tokens[0].id;
693
729
  // User A tries to revoke user B's token by ID
694
- const revoke_res = await rpc_call({
730
+ const revoke_res = await rpc_call_for_spec({
695
731
  app: test_app.app,
696
732
  path: rpc_path,
697
- method: account_token_revoke_action_spec.method,
733
+ spec: account_token_revoke_action_spec,
698
734
  params: { token_id: token_id_b },
699
735
  headers: test_app.create_session_headers(),
700
736
  });
701
737
  assert.ok(revoke_res.ok, 'account_token_revoke should succeed');
702
- const revoke_body = revoke_res.result;
703
- assert.strictEqual(revoke_body.revoked, false, 'Should not revoke another account token');
738
+ assert.strictEqual(revoke_res.result.revoked, false, 'Should not revoke another account token');
704
739
  // User B's bearer token should still work
705
- const verify_b = await rpc_call_non_browser({
740
+ const verify_b = await rpc_call_for_spec({
706
741
  app: test_app.app,
707
742
  path: rpc_path,
708
- method: account_verify_action_spec.method,
743
+ spec: account_verify_action_spec,
744
+ params: null,
709
745
  headers: { authorization: `Bearer ${user_b.api_token}` },
746
+ suppress_default_origin: true,
710
747
  });
711
748
  assert.strictEqual(verify_b.status, 200);
712
749
  });
@@ -714,16 +751,16 @@ export const describe_standard_integration_tests = (options) => {
714
751
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
715
752
  const user_b = await test_app.create_account({ username: 'user_b' });
716
753
  // User A lists sessions
717
- const res = await rpc_call({
754
+ const res = await rpc_call_for_spec({
718
755
  app: test_app.app,
719
756
  path: rpc_path,
720
- method: account_session_list_action_spec.method,
757
+ spec: account_session_list_action_spec,
758
+ params: null,
721
759
  headers: test_app.create_session_headers(),
722
760
  });
723
761
  assert.ok(res.ok, 'account_session_list should succeed');
724
- const body = res.result;
725
762
  // Sessions should only belong to user A's account
726
- for (const session of body.sessions) {
763
+ for (const session of res.result.sessions) {
727
764
  assert.strictEqual(session.account_id, test_app.backend.account.id, `Session ${session.id} should belong to user A, not user B (${user_b.account.id})`);
728
765
  }
729
766
  });
@@ -731,16 +768,16 @@ export const describe_standard_integration_tests = (options) => {
731
768
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
732
769
  const user_b = await test_app.create_account({ username: 'user_b' });
733
770
  // User A lists tokens
734
- const res = await rpc_call({
771
+ const res = await rpc_call_for_spec({
735
772
  app: test_app.app,
736
773
  path: rpc_path,
737
- method: account_token_list_action_spec.method,
774
+ spec: account_token_list_action_spec,
775
+ params: null,
738
776
  headers: test_app.create_session_headers(),
739
777
  });
740
778
  assert.ok(res.ok, 'account_token_list should succeed');
741
- const body = res.result;
742
779
  // Tokens should only belong to user A's account
743
- for (const token of body.tokens) {
780
+ for (const token of res.result.tokens) {
744
781
  assert.strictEqual(token.account_id, test_app.backend.account.id, `Token ${token.id} should belong to user A, not user B (${user_b.account.id})`);
745
782
  }
746
783
  });
@@ -854,24 +891,54 @@ export const describe_standard_integration_tests = (options) => {
854
891
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
855
892
  // Hit several auth-required RPC methods without credentials to
856
893
  // broaden error coverage beyond just /login. RPC 401s are tracked
857
- // against the shared endpoint path.
858
- const rpc_methods = [
859
- account_session_list_action_spec.method,
860
- account_session_revoke_all_action_spec.method,
861
- account_token_list_action_spec.method,
862
- account_token_create_action_spec.method,
863
- account_verify_action_spec.method,
864
- ];
865
- for (const method of rpc_methods) {
866
- const res = await rpc_call({
867
- app: test_app.app,
868
- path: rpc_path,
869
- method,
870
- headers: { host: 'localhost' },
871
- });
872
- assert.strictEqual(res.status, 401, `${method} without auth should return 401 (dispatcher runs auth before params)`);
873
- error_collector.record(test_app.route_specs, 'POST', rpc_path, 401);
874
- }
894
+ // against the shared endpoint path. The dispatcher runs auth before
895
+ // params validation, so any well-formed param body works — we just
896
+ // need each call to be type-correct wrt its spec.
897
+ const session_list = await rpc_call_for_spec({
898
+ app: test_app.app,
899
+ path: rpc_path,
900
+ spec: account_session_list_action_spec,
901
+ params: null,
902
+ headers: { host: 'localhost' },
903
+ });
904
+ assert.strictEqual(session_list.status, 401);
905
+ error_collector.record(test_app.route_specs, 'POST', rpc_path, 401);
906
+ const session_revoke_all = await rpc_call_for_spec({
907
+ app: test_app.app,
908
+ path: rpc_path,
909
+ spec: account_session_revoke_all_action_spec,
910
+ params: null,
911
+ headers: { host: 'localhost' },
912
+ });
913
+ assert.strictEqual(session_revoke_all.status, 401);
914
+ error_collector.record(test_app.route_specs, 'POST', rpc_path, 401);
915
+ const token_list = await rpc_call_for_spec({
916
+ app: test_app.app,
917
+ path: rpc_path,
918
+ spec: account_token_list_action_spec,
919
+ params: null,
920
+ headers: { host: 'localhost' },
921
+ });
922
+ assert.strictEqual(token_list.status, 401);
923
+ error_collector.record(test_app.route_specs, 'POST', rpc_path, 401);
924
+ const token_create = await rpc_call_for_spec({
925
+ app: test_app.app,
926
+ path: rpc_path,
927
+ spec: account_token_create_action_spec,
928
+ params: { name: 'unauth-probe' },
929
+ headers: { host: 'localhost' },
930
+ });
931
+ assert.strictEqual(token_create.status, 401);
932
+ error_collector.record(test_app.route_specs, 'POST', rpc_path, 401);
933
+ const verify = await rpc_call_for_spec({
934
+ app: test_app.app,
935
+ path: rpc_path,
936
+ spec: account_verify_action_spec,
937
+ params: null,
938
+ headers: { host: 'localhost' },
939
+ });
940
+ assert.strictEqual(verify.status, 401);
941
+ error_collector.record(test_app.route_specs, 'POST', rpc_path, 401);
875
942
  // Also exercise POST /logout without auth (still REST)
876
943
  const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
877
944
  if (logout_route) {
@@ -889,10 +956,11 @@ export const describe_standard_integration_tests = (options) => {
889
956
  describe('error response information leakage', () => {
890
957
  test('401 responses contain no leaky fields', async () => {
891
958
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
892
- const res = await rpc_call({
959
+ const res = await rpc_call_for_spec({
893
960
  app: test_app.app,
894
961
  path: rpc_path,
895
- method: account_verify_action_spec.method,
962
+ spec: account_verify_action_spec,
963
+ params: null,
896
964
  headers: { host: 'localhost' },
897
965
  });
898
966
  assert.strictEqual(res.status, 401);
@@ -908,10 +976,11 @@ export const describe_standard_integration_tests = (options) => {
908
976
  test('expired session cookie returns 401', async () => {
909
977
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
910
978
  const expired_cookie = await create_expired_test_cookie(test_app.backend.keyring, options.session_options);
911
- const res = await rpc_call({
979
+ const res = await rpc_call_for_spec({
912
980
  app: test_app.app,
913
981
  path: rpc_path,
914
- method: account_verify_action_spec.method,
982
+ spec: account_verify_action_spec,
983
+ params: null,
915
984
  headers: { cookie: `${cookie_name}=${expired_cookie}` },
916
985
  });
917
986
  assert.strictEqual(res.status, 401, 'Expired session cookie should be rejected');
@@ -970,10 +1039,10 @@ export const describe_standard_integration_tests = (options) => {
970
1039
  const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
971
1040
  assert.ok(password_route, 'Expected POST /password route');
972
1041
  // Create an API token via RPC
973
- const create_res = await rpc_call({
1042
+ const create_res = await rpc_call_for_spec({
974
1043
  app: test_app.app,
975
1044
  path: rpc_path,
976
- method: account_token_create_action_spec.method,
1045
+ spec: account_token_create_action_spec,
977
1046
  params: { name: 'test-token' },
978
1047
  headers: test_app.create_session_headers(),
979
1048
  });
@@ -981,11 +1050,13 @@ export const describe_standard_integration_tests = (options) => {
981
1050
  const { token: raw_token } = create_res.result;
982
1051
  assert.ok(raw_token, 'Expected raw token in create response');
983
1052
  // Verify bearer token works
984
- const verify_before = await rpc_call_non_browser({
1053
+ const verify_before = await rpc_call_for_spec({
985
1054
  app: test_app.app,
986
1055
  path: rpc_path,
987
- method: account_verify_action_spec.method,
1056
+ spec: account_verify_action_spec,
1057
+ params: null,
988
1058
  headers: { authorization: `Bearer ${raw_token}` },
1059
+ suppress_default_origin: true,
989
1060
  });
990
1061
  assert.strictEqual(verify_before.status, 200, 'Bearer token should work before password change');
991
1062
  // Change password (still REST)
@@ -1002,11 +1073,13 @@ export const describe_standard_integration_tests = (options) => {
1002
1073
  assert.ok(typeof change_body.tokens_revoked === 'number', 'Expected tokens_revoked count');
1003
1074
  assert.ok(change_body.tokens_revoked >= 1, 'Expected at least 1 token revoked');
1004
1075
  // Bearer token should now be invalid
1005
- const verify_after = await rpc_call_non_browser({
1076
+ const verify_after = await rpc_call_for_spec({
1006
1077
  app: test_app.app,
1007
1078
  path: rpc_path,
1008
- method: account_verify_action_spec.method,
1079
+ spec: account_verify_action_spec,
1080
+ params: null,
1009
1081
  headers: { authorization: `Bearer ${raw_token}` },
1082
+ suppress_default_origin: true,
1010
1083
  });
1011
1084
  assert.strictEqual(verify_after.status, 401, 'Bearer token should be rejected after password change');
1012
1085
  });
@@ -1021,7 +1094,7 @@ export const describe_standard_integration_tests = (options) => {
1021
1094
  // `invite_create` became RPC-only in the 2026-04-23 migration.
1022
1095
  // Consumers that don't wire admin RPC actions can't exercise invites;
1023
1096
  // skip the test rather than fail.
1024
- if (!find_rpc_action(options.rpc_endpoints, invite_create_action_spec.method))
1097
+ if (!find_rpc_action(rpc_endpoints_for_setup, invite_create_action_spec.method))
1025
1098
  return;
1026
1099
  // Create an admin to manage invites
1027
1100
  const admin = await test_app.create_account({
@@ -1029,10 +1102,10 @@ export const describe_standard_integration_tests = (options) => {
1029
1102
  roles: ['admin'],
1030
1103
  });
1031
1104
  // Create invite for alice@example.com via RPC
1032
- const invite_res = await rpc_call({
1105
+ const invite_res = await rpc_call_for_spec({
1033
1106
  app: test_app.app,
1034
1107
  path: rpc_path,
1035
- method: invite_create_action_spec.method,
1108
+ spec: invite_create_action_spec,
1036
1109
  params: { email: 'alice@example.com' },
1037
1110
  headers: { cookie: `${cookie_name}=${admin.session_cookie}` },
1038
1111
  });
@@ -1066,7 +1139,7 @@ export const describe_standard_integration_tests = (options) => {
1066
1139
  return; // signup is optional
1067
1140
  // `invite_create` became RPC-only in the 2026-04-23 migration.
1068
1141
  // Consumers that don't wire admin RPC actions can't exercise invites.
1069
- if (!find_rpc_action(options.rpc_endpoints, invite_create_action_spec.method))
1142
+ if (!find_rpc_action(rpc_endpoints_for_setup, invite_create_action_spec.method))
1070
1143
  return;
1071
1144
  // We need admin access — create an admin account
1072
1145
  const admin = await test_app.create_account({
@@ -1076,10 +1149,10 @@ export const describe_standard_integration_tests = (options) => {
1076
1149
  const admin_headers = { cookie: `${cookie_name}=${admin.session_cookie}` };
1077
1150
  // Create an invite for a specific test email via RPC
1078
1151
  const test_email = 'signup-test@example.com';
1079
- const invite_res = await rpc_call({
1152
+ const invite_res = await rpc_call_for_spec({
1080
1153
  app: test_app.app,
1081
1154
  path: rpc_path,
1082
- method: invite_create_action_spec.method,
1155
+ spec: invite_create_action_spec,
1083
1156
  params: { email: test_email },
1084
1157
  headers: admin_headers,
1085
1158
  });
@@ -1106,14 +1179,14 @@ export const describe_standard_integration_tests = (options) => {
1106
1179
  const existing_user = await test_app.create_account({ username: 'existing_user' });
1107
1180
  // Create invite for a different email via RPC
1108
1181
  const conflict_email = 'conflict-test@example.com';
1109
- const invite2_res = await rpc_call({
1182
+ const invite2_res = await rpc_call_for_spec({
1110
1183
  app: test_app.app,
1111
1184
  path: rpc_path,
1112
- method: invite_create_action_spec.method,
1185
+ spec: invite_create_action_spec,
1113
1186
  params: { email: conflict_email },
1114
1187
  headers: admin_headers,
1115
1188
  });
1116
- assert.ok(invite2_res.ok, `invite_create failed: ${invite2_res.ok ? '' : JSON.stringify(invite2_res.error)}`);
1189
+ assert.ok(invite2_res.ok, `invite2_create failed: ${invite2_res.ok ? '' : JSON.stringify(invite2_res.error)}`);
1117
1190
  // Attempt 2: signup with the invited email but a colliding username → 409
1118
1191
  const conflict_res = await test_app.app.request(signup_route.path, {
1119
1192
  method: 'POST',