@fuzdev/fuz_app 0.33.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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, resolve_rpc_endpoints_for_setup, } 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';
@@ -221,10 +221,11 @@ export const describe_standard_integration_tests = (options) => {
221
221
  cookie: `${cookie_name}=${login_cookie}`,
222
222
  });
223
223
  // Verify works
224
- const verify_res = await rpc_call({
224
+ const verify_res = await rpc_call_for_spec({
225
225
  app: test_app.app,
226
226
  path: rpc_path,
227
- method: account_verify_action_spec.method,
227
+ spec: account_verify_action_spec,
228
+ params: null,
228
229
  headers: create_headers(),
229
230
  });
230
231
  assert.strictEqual(verify_res.status, 200);
@@ -238,10 +239,11 @@ export const describe_standard_integration_tests = (options) => {
238
239
  assert.strictEqual(logout_body.ok, true);
239
240
  assert.strictEqual(logout_body.username, test_app.backend.account.username, 'Logout response should include the username');
240
241
  // Verify fails after logout (session revoked)
241
- const verify_after = await rpc_call({
242
+ const verify_after = await rpc_call_for_spec({
242
243
  app: test_app.app,
243
244
  path: rpc_path,
244
- method: account_verify_action_spec.method,
245
+ spec: account_verify_action_spec,
246
+ params: null,
245
247
  headers: create_headers(),
246
248
  });
247
249
  assert.strictEqual(verify_after.status, 401);
@@ -309,20 +311,22 @@ export const describe_standard_integration_tests = (options) => {
309
311
  describe('session security', () => {
310
312
  test('no cookie on protected route returns 401', async () => {
311
313
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
312
- const res = await rpc_call({
314
+ const res = await rpc_call_for_spec({
313
315
  app: test_app.app,
314
316
  path: rpc_path,
315
- method: account_verify_action_spec.method,
317
+ spec: account_verify_action_spec,
318
+ params: null,
316
319
  headers: { host: 'localhost' },
317
320
  });
318
321
  assert.strictEqual(res.status, 401);
319
322
  });
320
323
  test('corrupted cookie returns 401', async () => {
321
324
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
322
- const res = await rpc_call({
325
+ const res = await rpc_call_for_spec({
323
326
  app: test_app.app,
324
327
  path: rpc_path,
325
- method: account_verify_action_spec.method,
328
+ spec: account_verify_action_spec,
329
+ params: null,
326
330
  headers: { cookie: `${cookie_name}=random_garbage_value` },
327
331
  });
328
332
  assert.strictEqual(res.status, 401);
@@ -330,10 +334,11 @@ export const describe_standard_integration_tests = (options) => {
330
334
  test('expired cookie returns 401', async () => {
331
335
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
332
336
  const expired_cookie = await create_expired_test_cookie(test_app.backend.keyring, options.session_options);
333
- const res = await rpc_call({
337
+ const res = await rpc_call_for_spec({
334
338
  app: test_app.app,
335
339
  path: rpc_path,
336
- method: account_verify_action_spec.method,
340
+ spec: account_verify_action_spec,
341
+ params: null,
337
342
  headers: { cookie: `${cookie_name}=${expired_cookie}` },
338
343
  });
339
344
  assert.strictEqual(res.status, 401);
@@ -345,33 +350,33 @@ export const describe_standard_integration_tests = (options) => {
345
350
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
346
351
  const headers = test_app.create_session_headers();
347
352
  // List own sessions to get the session ID
348
- const list_res = await rpc_call({
353
+ const list_res = await rpc_call_for_spec({
349
354
  app: test_app.app,
350
355
  path: rpc_path,
351
- method: account_session_list_action_spec.method,
356
+ spec: account_session_list_action_spec,
357
+ params: null,
352
358
  headers,
353
359
  });
354
360
  assert.ok(list_res.ok, 'account_session_list should succeed');
355
- const list_body = list_res.result;
356
- assert.ok(list_body.sessions.length >= 1);
357
- 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;
358
363
  // Revoke that session by ID
359
- const revoke_res = await rpc_call({
364
+ const revoke_res = await rpc_call_for_spec({
360
365
  app: test_app.app,
361
366
  path: rpc_path,
362
- method: account_session_revoke_action_spec.method,
367
+ spec: account_session_revoke_action_spec,
363
368
  params: { session_id },
364
369
  headers,
365
370
  });
366
371
  assert.ok(revoke_res.ok, 'account_session_revoke should succeed');
367
- const revoke_body = revoke_res.result;
368
- assert.strictEqual(revoke_body.ok, true);
369
- assert.strictEqual(revoke_body.revoked, true);
372
+ assert.strictEqual(revoke_res.result.ok, true);
373
+ assert.strictEqual(revoke_res.result.revoked, true);
370
374
  // Session should no longer work
371
- const after = await rpc_call({
375
+ const after = await rpc_call_for_spec({
372
376
  app: test_app.app,
373
377
  path: rpc_path,
374
- method: account_verify_action_spec.method,
378
+ spec: account_verify_action_spec,
379
+ params: null,
375
380
  headers,
376
381
  });
377
382
  assert.strictEqual(after.status, 401);
@@ -380,26 +385,29 @@ export const describe_standard_integration_tests = (options) => {
380
385
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
381
386
  const headers = test_app.create_session_headers();
382
387
  // Verify works
383
- const before = await rpc_call({
388
+ const before = await rpc_call_for_spec({
384
389
  app: test_app.app,
385
390
  path: rpc_path,
386
- method: account_verify_action_spec.method,
391
+ spec: account_verify_action_spec,
392
+ params: null,
387
393
  headers,
388
394
  });
389
395
  assert.strictEqual(before.status, 200);
390
396
  // Revoke all sessions
391
- const revoke_res = await rpc_call({
397
+ const revoke_res = await rpc_call_for_spec({
392
398
  app: test_app.app,
393
399
  path: rpc_path,
394
- method: account_session_revoke_all_action_spec.method,
400
+ spec: account_session_revoke_all_action_spec,
401
+ params: null,
395
402
  headers,
396
403
  });
397
404
  assert.ok(revoke_res.ok, 'account_session_revoke_all should succeed');
398
405
  // Verify fails after revocation
399
- const after = await rpc_call({
406
+ const after = await rpc_call_for_spec({
400
407
  app: test_app.app,
401
408
  path: rpc_path,
402
- method: account_verify_action_spec.method,
409
+ spec: account_verify_action_spec,
410
+ params: null,
403
411
  headers,
404
412
  });
405
413
  assert.strictEqual(after.status, 401);
@@ -431,10 +439,11 @@ export const describe_standard_integration_tests = (options) => {
431
439
  assert.ok(typeof change_body.sessions_revoked === 'number', 'Expected sessions_revoked count');
432
440
  assert.ok(change_body.sessions_revoked >= 1, 'Expected at least 1 session revoked');
433
441
  // Old session should be invalid
434
- const verify_after = await rpc_call({
442
+ const verify_after = await rpc_call_for_spec({
435
443
  app: test_app.app,
436
444
  path: rpc_path,
437
- method: account_verify_action_spec.method,
445
+ spec: account_verify_action_spec,
446
+ params: null,
438
447
  headers: test_app.create_session_headers(),
439
448
  });
440
449
  assert.strictEqual(verify_after.status, 401);
@@ -472,10 +481,11 @@ export const describe_standard_integration_tests = (options) => {
472
481
  assert.strictEqual(res.status, 401);
473
482
  error_collector.record(test_app.route_specs, 'POST', password_route.path, 401);
474
483
  // Session should still be valid (password didn't change)
475
- const verify_res = await rpc_call({
484
+ const verify_res = await rpc_call_for_spec({
476
485
  app: test_app.app,
477
486
  path: rpc_path,
478
- method: account_verify_action_spec.method,
487
+ spec: account_verify_action_spec,
488
+ params: null,
479
489
  headers: test_app.create_session_headers(),
480
490
  });
481
491
  assert.strictEqual(verify_res.status, 200);
@@ -507,23 +517,26 @@ export const describe_standard_integration_tests = (options) => {
507
517
  });
508
518
  test('valid origin is accepted', async () => {
509
519
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
510
- const res = await rpc_call({
520
+ const res = await rpc_call_for_spec({
511
521
  app: test_app.app,
512
522
  path: rpc_path,
513
- method: account_verify_action_spec.method,
523
+ spec: account_verify_action_spec,
524
+ params: null,
514
525
  headers: test_app.create_session_headers(),
515
526
  });
516
527
  assert.strictEqual(res.status, 200);
517
528
  });
518
529
  test('no origin header is allowed (direct access)', async () => {
519
530
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
520
- // Probe the "no Origin / no Referer" path; `rpc_call_non_browser`
521
- // suppresses the default `origin` header.
522
- 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({
523
534
  app: test_app.app,
524
535
  path: rpc_path,
525
- method: account_verify_action_spec.method,
536
+ spec: account_verify_action_spec,
537
+ params: null,
526
538
  headers: { cookie: `${cookie_name}=${test_app.backend.session_cookie}` },
539
+ suppress_default_origin: true,
527
540
  });
528
541
  assert.notStrictEqual(res.status, 403);
529
542
  });
@@ -532,21 +545,25 @@ export const describe_standard_integration_tests = (options) => {
532
545
  describe('bearer auth', () => {
533
546
  test('valid bearer token authenticates', async () => {
534
547
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
535
- const res = await rpc_call_non_browser({
548
+ const res = await rpc_call_for_spec({
536
549
  app: test_app.app,
537
550
  path: rpc_path,
538
- method: account_verify_action_spec.method,
551
+ spec: account_verify_action_spec,
552
+ params: null,
539
553
  headers: test_app.create_bearer_headers(),
554
+ suppress_default_origin: true,
540
555
  });
541
556
  assert.strictEqual(res.status, 200);
542
557
  });
543
558
  test('invalid bearer token returns 401', async () => {
544
559
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
545
- const res = await rpc_call_non_browser({
560
+ const res = await rpc_call_for_spec({
546
561
  app: test_app.app,
547
562
  path: rpc_path,
548
- method: account_verify_action_spec.method,
563
+ spec: account_verify_action_spec,
564
+ params: null,
549
565
  headers: { authorization: 'Bearer secret_fuz_token_invalid' },
566
+ suppress_default_origin: true,
550
567
  });
551
568
  assert.strictEqual(res.status, 401);
552
569
  });
@@ -554,18 +571,21 @@ export const describe_standard_integration_tests = (options) => {
554
571
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
555
572
  const bearer_headers = test_app.create_bearer_headers();
556
573
  // Without Origin — works.
557
- const ok_res = await rpc_call_non_browser({
574
+ const ok_res = await rpc_call_for_spec({
558
575
  app: test_app.app,
559
576
  path: rpc_path,
560
- method: account_verify_action_spec.method,
577
+ spec: account_verify_action_spec,
578
+ params: null,
561
579
  headers: bearer_headers,
580
+ suppress_default_origin: true,
562
581
  });
563
582
  assert.strictEqual(ok_res.status, 200);
564
583
  // With Origin — bearer silently discarded (browser context), falls through to no auth.
565
- const res = await rpc_call({
584
+ const res = await rpc_call_for_spec({
566
585
  app: test_app.app,
567
586
  path: rpc_path,
568
- method: account_verify_action_spec.method,
587
+ spec: account_verify_action_spec,
588
+ params: null,
569
589
  headers: { ...bearer_headers, origin: 'http://localhost:5173' },
570
590
  });
571
591
  assert.strictEqual(res.status, 401);
@@ -576,38 +596,42 @@ export const describe_standard_integration_tests = (options) => {
576
596
  test('revoked API token returns 401', async () => {
577
597
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
578
598
  // Create a new token via RPC
579
- const create_res = await rpc_call({
599
+ const create_res = await rpc_call_for_spec({
580
600
  app: test_app.app,
581
601
  path: rpc_path,
582
- method: account_token_create_action_spec.method,
602
+ spec: account_token_create_action_spec,
583
603
  params: { name: 'test-revoke' },
584
604
  headers: test_app.create_session_headers(),
585
605
  });
586
606
  assert.ok(create_res.ok, 'account_token_create should succeed');
587
607
  const { token, id } = create_res.result;
588
608
  // Verify token works
589
- const use_res = await rpc_call_non_browser({
609
+ const use_res = await rpc_call_for_spec({
590
610
  app: test_app.app,
591
611
  path: rpc_path,
592
- method: account_verify_action_spec.method,
612
+ spec: account_verify_action_spec,
613
+ params: null,
593
614
  headers: { authorization: `Bearer ${token}` },
615
+ suppress_default_origin: true,
594
616
  });
595
617
  assert.strictEqual(use_res.status, 200);
596
618
  // Revoke via RPC
597
- const revoke_res = await rpc_call({
619
+ const revoke_res = await rpc_call_for_spec({
598
620
  app: test_app.app,
599
621
  path: rpc_path,
600
- method: account_token_revoke_action_spec.method,
622
+ spec: account_token_revoke_action_spec,
601
623
  params: { token_id: id },
602
624
  headers: test_app.create_session_headers(),
603
625
  });
604
626
  assert.ok(revoke_res.ok, 'account_token_revoke should succeed');
605
627
  // Token should no longer work
606
- const after_res = await rpc_call_non_browser({
628
+ const after_res = await rpc_call_for_spec({
607
629
  app: test_app.app,
608
630
  path: rpc_path,
609
- method: account_verify_action_spec.method,
631
+ spec: account_verify_action_spec,
632
+ params: null,
610
633
  headers: { authorization: `Bearer ${token}` },
634
+ suppress_default_origin: true,
611
635
  });
612
636
  assert.strictEqual(after_res.status, 401);
613
637
  });
@@ -634,18 +658,20 @@ export const describe_standard_integration_tests = (options) => {
634
658
  // Create a second account
635
659
  const user_b = await test_app.create_account({ username: 'user_b' });
636
660
  // User A revokes all their own sessions
637
- const revoke_res = await rpc_call({
661
+ const revoke_res = await rpc_call_for_spec({
638
662
  app: test_app.app,
639
663
  path: rpc_path,
640
- method: account_session_revoke_all_action_spec.method,
664
+ spec: account_session_revoke_all_action_spec,
665
+ params: null,
641
666
  headers: test_app.create_session_headers(),
642
667
  });
643
668
  assert.ok(revoke_res.ok, 'account_session_revoke_all should succeed');
644
669
  // User B's session should still work
645
- const verify_b = await rpc_call({
670
+ const verify_b = await rpc_call_for_spec({
646
671
  app: test_app.app,
647
672
  path: rpc_path,
648
- method: account_verify_action_spec.method,
673
+ spec: account_verify_action_spec,
674
+ params: null,
649
675
  headers: { cookie: `${cookie_name}=${user_b.session_cookie}` },
650
676
  });
651
677
  assert.strictEqual(verify_b.status, 200);
@@ -655,32 +681,32 @@ export const describe_standard_integration_tests = (options) => {
655
681
  const user_b = await test_app.create_account({ username: 'user_b' });
656
682
  const user_b_headers = { cookie: `${cookie_name}=${user_b.session_cookie}` };
657
683
  // Get user B's session ID by listing as user B
658
- const list_res = await rpc_call({
684
+ const list_res = await rpc_call_for_spec({
659
685
  app: test_app.app,
660
686
  path: rpc_path,
661
- method: account_session_list_action_spec.method,
687
+ spec: account_session_list_action_spec,
688
+ params: null,
662
689
  headers: user_b_headers,
663
690
  });
664
691
  assert.ok(list_res.ok, 'account_session_list should succeed');
665
- const list_body = list_res.result;
666
- assert.ok(list_body.sessions.length >= 1);
667
- 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;
668
694
  // User A tries to revoke user B's session by ID
669
- const revoke_res = await rpc_call({
695
+ const revoke_res = await rpc_call_for_spec({
670
696
  app: test_app.app,
671
697
  path: rpc_path,
672
- method: account_session_revoke_action_spec.method,
698
+ spec: account_session_revoke_action_spec,
673
699
  params: { session_id: session_id_b },
674
700
  headers: test_app.create_session_headers(),
675
701
  });
676
702
  assert.ok(revoke_res.ok, 'account_session_revoke should succeed');
677
- const revoke_body = revoke_res.result;
678
- 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');
679
704
  // User B's session should still work
680
- const verify_b = await rpc_call({
705
+ const verify_b = await rpc_call_for_spec({
681
706
  app: test_app.app,
682
707
  path: rpc_path,
683
- method: account_verify_action_spec.method,
708
+ spec: account_verify_action_spec,
709
+ params: null,
684
710
  headers: user_b_headers,
685
711
  });
686
712
  assert.strictEqual(verify_b.status, 200);
@@ -690,33 +716,34 @@ export const describe_standard_integration_tests = (options) => {
690
716
  const user_b = await test_app.create_account({ username: 'user_b' });
691
717
  const user_b_headers = { cookie: `${cookie_name}=${user_b.session_cookie}` };
692
718
  // Get user B's token ID by listing as user B
693
- const list_res = await rpc_call({
719
+ const list_res = await rpc_call_for_spec({
694
720
  app: test_app.app,
695
721
  path: rpc_path,
696
- method: account_token_list_action_spec.method,
722
+ spec: account_token_list_action_spec,
723
+ params: null,
697
724
  headers: user_b_headers,
698
725
  });
699
726
  assert.ok(list_res.ok, 'account_token_list should succeed');
700
- const list_body = list_res.result;
701
- assert.ok(list_body.tokens.length >= 1);
702
- 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;
703
729
  // User A tries to revoke user B's token by ID
704
- const revoke_res = await rpc_call({
730
+ const revoke_res = await rpc_call_for_spec({
705
731
  app: test_app.app,
706
732
  path: rpc_path,
707
- method: account_token_revoke_action_spec.method,
733
+ spec: account_token_revoke_action_spec,
708
734
  params: { token_id: token_id_b },
709
735
  headers: test_app.create_session_headers(),
710
736
  });
711
737
  assert.ok(revoke_res.ok, 'account_token_revoke should succeed');
712
- const revoke_body = revoke_res.result;
713
- 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');
714
739
  // User B's bearer token should still work
715
- const verify_b = await rpc_call_non_browser({
740
+ const verify_b = await rpc_call_for_spec({
716
741
  app: test_app.app,
717
742
  path: rpc_path,
718
- method: account_verify_action_spec.method,
743
+ spec: account_verify_action_spec,
744
+ params: null,
719
745
  headers: { authorization: `Bearer ${user_b.api_token}` },
746
+ suppress_default_origin: true,
720
747
  });
721
748
  assert.strictEqual(verify_b.status, 200);
722
749
  });
@@ -724,16 +751,16 @@ export const describe_standard_integration_tests = (options) => {
724
751
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
725
752
  const user_b = await test_app.create_account({ username: 'user_b' });
726
753
  // User A lists sessions
727
- const res = await rpc_call({
754
+ const res = await rpc_call_for_spec({
728
755
  app: test_app.app,
729
756
  path: rpc_path,
730
- method: account_session_list_action_spec.method,
757
+ spec: account_session_list_action_spec,
758
+ params: null,
731
759
  headers: test_app.create_session_headers(),
732
760
  });
733
761
  assert.ok(res.ok, 'account_session_list should succeed');
734
- const body = res.result;
735
762
  // Sessions should only belong to user A's account
736
- for (const session of body.sessions) {
763
+ for (const session of res.result.sessions) {
737
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})`);
738
765
  }
739
766
  });
@@ -741,16 +768,16 @@ export const describe_standard_integration_tests = (options) => {
741
768
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
742
769
  const user_b = await test_app.create_account({ username: 'user_b' });
743
770
  // User A lists tokens
744
- const res = await rpc_call({
771
+ const res = await rpc_call_for_spec({
745
772
  app: test_app.app,
746
773
  path: rpc_path,
747
- method: account_token_list_action_spec.method,
774
+ spec: account_token_list_action_spec,
775
+ params: null,
748
776
  headers: test_app.create_session_headers(),
749
777
  });
750
778
  assert.ok(res.ok, 'account_token_list should succeed');
751
- const body = res.result;
752
779
  // Tokens should only belong to user A's account
753
- for (const token of body.tokens) {
780
+ for (const token of res.result.tokens) {
754
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})`);
755
782
  }
756
783
  });
@@ -864,24 +891,54 @@ export const describe_standard_integration_tests = (options) => {
864
891
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
865
892
  // Hit several auth-required RPC methods without credentials to
866
893
  // broaden error coverage beyond just /login. RPC 401s are tracked
867
- // against the shared endpoint path.
868
- const rpc_methods = [
869
- account_session_list_action_spec.method,
870
- account_session_revoke_all_action_spec.method,
871
- account_token_list_action_spec.method,
872
- account_token_create_action_spec.method,
873
- account_verify_action_spec.method,
874
- ];
875
- for (const method of rpc_methods) {
876
- const res = await rpc_call({
877
- app: test_app.app,
878
- path: rpc_path,
879
- method,
880
- headers: { host: 'localhost' },
881
- });
882
- assert.strictEqual(res.status, 401, `${method} without auth should return 401 (dispatcher runs auth before params)`);
883
- error_collector.record(test_app.route_specs, 'POST', rpc_path, 401);
884
- }
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);
885
942
  // Also exercise POST /logout without auth (still REST)
886
943
  const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
887
944
  if (logout_route) {
@@ -899,10 +956,11 @@ export const describe_standard_integration_tests = (options) => {
899
956
  describe('error response information leakage', () => {
900
957
  test('401 responses contain no leaky fields', async () => {
901
958
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
902
- const res = await rpc_call({
959
+ const res = await rpc_call_for_spec({
903
960
  app: test_app.app,
904
961
  path: rpc_path,
905
- method: account_verify_action_spec.method,
962
+ spec: account_verify_action_spec,
963
+ params: null,
906
964
  headers: { host: 'localhost' },
907
965
  });
908
966
  assert.strictEqual(res.status, 401);
@@ -918,10 +976,11 @@ export const describe_standard_integration_tests = (options) => {
918
976
  test('expired session cookie returns 401', async () => {
919
977
  const test_app = await create_test_app(build_test_app_options(options, get_db()));
920
978
  const expired_cookie = await create_expired_test_cookie(test_app.backend.keyring, options.session_options);
921
- const res = await rpc_call({
979
+ const res = await rpc_call_for_spec({
922
980
  app: test_app.app,
923
981
  path: rpc_path,
924
- method: account_verify_action_spec.method,
982
+ spec: account_verify_action_spec,
983
+ params: null,
925
984
  headers: { cookie: `${cookie_name}=${expired_cookie}` },
926
985
  });
927
986
  assert.strictEqual(res.status, 401, 'Expired session cookie should be rejected');
@@ -980,10 +1039,10 @@ export const describe_standard_integration_tests = (options) => {
980
1039
  const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
981
1040
  assert.ok(password_route, 'Expected POST /password route');
982
1041
  // Create an API token via RPC
983
- const create_res = await rpc_call({
1042
+ const create_res = await rpc_call_for_spec({
984
1043
  app: test_app.app,
985
1044
  path: rpc_path,
986
- method: account_token_create_action_spec.method,
1045
+ spec: account_token_create_action_spec,
987
1046
  params: { name: 'test-token' },
988
1047
  headers: test_app.create_session_headers(),
989
1048
  });
@@ -991,11 +1050,13 @@ export const describe_standard_integration_tests = (options) => {
991
1050
  const { token: raw_token } = create_res.result;
992
1051
  assert.ok(raw_token, 'Expected raw token in create response');
993
1052
  // Verify bearer token works
994
- const verify_before = await rpc_call_non_browser({
1053
+ const verify_before = await rpc_call_for_spec({
995
1054
  app: test_app.app,
996
1055
  path: rpc_path,
997
- method: account_verify_action_spec.method,
1056
+ spec: account_verify_action_spec,
1057
+ params: null,
998
1058
  headers: { authorization: `Bearer ${raw_token}` },
1059
+ suppress_default_origin: true,
999
1060
  });
1000
1061
  assert.strictEqual(verify_before.status, 200, 'Bearer token should work before password change');
1001
1062
  // Change password (still REST)
@@ -1012,11 +1073,13 @@ export const describe_standard_integration_tests = (options) => {
1012
1073
  assert.ok(typeof change_body.tokens_revoked === 'number', 'Expected tokens_revoked count');
1013
1074
  assert.ok(change_body.tokens_revoked >= 1, 'Expected at least 1 token revoked');
1014
1075
  // Bearer token should now be invalid
1015
- const verify_after = await rpc_call_non_browser({
1076
+ const verify_after = await rpc_call_for_spec({
1016
1077
  app: test_app.app,
1017
1078
  path: rpc_path,
1018
- method: account_verify_action_spec.method,
1079
+ spec: account_verify_action_spec,
1080
+ params: null,
1019
1081
  headers: { authorization: `Bearer ${raw_token}` },
1082
+ suppress_default_origin: true,
1020
1083
  });
1021
1084
  assert.strictEqual(verify_after.status, 401, 'Bearer token should be rejected after password change');
1022
1085
  });
@@ -1039,10 +1102,10 @@ export const describe_standard_integration_tests = (options) => {
1039
1102
  roles: ['admin'],
1040
1103
  });
1041
1104
  // Create invite for alice@example.com via RPC
1042
- const invite_res = await rpc_call({
1105
+ const invite_res = await rpc_call_for_spec({
1043
1106
  app: test_app.app,
1044
1107
  path: rpc_path,
1045
- method: invite_create_action_spec.method,
1108
+ spec: invite_create_action_spec,
1046
1109
  params: { email: 'alice@example.com' },
1047
1110
  headers: { cookie: `${cookie_name}=${admin.session_cookie}` },
1048
1111
  });
@@ -1086,10 +1149,10 @@ export const describe_standard_integration_tests = (options) => {
1086
1149
  const admin_headers = { cookie: `${cookie_name}=${admin.session_cookie}` };
1087
1150
  // Create an invite for a specific test email via RPC
1088
1151
  const test_email = 'signup-test@example.com';
1089
- const invite_res = await rpc_call({
1152
+ const invite_res = await rpc_call_for_spec({
1090
1153
  app: test_app.app,
1091
1154
  path: rpc_path,
1092
- method: invite_create_action_spec.method,
1155
+ spec: invite_create_action_spec,
1093
1156
  params: { email: test_email },
1094
1157
  headers: admin_headers,
1095
1158
  });
@@ -1116,14 +1179,14 @@ export const describe_standard_integration_tests = (options) => {
1116
1179
  const existing_user = await test_app.create_account({ username: 'existing_user' });
1117
1180
  // Create invite for a different email via RPC
1118
1181
  const conflict_email = 'conflict-test@example.com';
1119
- const invite2_res = await rpc_call({
1182
+ const invite2_res = await rpc_call_for_spec({
1120
1183
  app: test_app.app,
1121
1184
  path: rpc_path,
1122
- method: invite_create_action_spec.method,
1185
+ spec: invite_create_action_spec,
1123
1186
  params: { email: conflict_email },
1124
1187
  headers: admin_headers,
1125
1188
  });
1126
- 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)}`);
1127
1190
  // Attempt 2: signup with the invited email but a colliding username → 409
1128
1191
  const conflict_res = await test_app.app.request(signup_route.path, {
1129
1192
  method: 'POST',