@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.
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +6 -1
- package/dist/testing/CLAUDE.md +26 -10
- package/dist/testing/admin_integration.d.ts +21 -9
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +152 -148
- package/dist/testing/app_server.d.ts +10 -0
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/audit_completeness.d.ts +8 -4
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +40 -45
- package/dist/testing/integration.d.ts +16 -6
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +202 -129
- package/dist/testing/rate_limiting.d.ts +13 -4
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +9 -3
- package/dist/testing/rpc_helpers.d.ts +29 -0
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +20 -0
- package/dist/testing/rpc_round_trip.d.ts +16 -5
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +11 -5
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +25 -1
- package/dist/testing/sse_round_trip.d.ts +13 -5
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +11 -5
- package/dist/testing/standard.d.ts +7 -2
- package/dist/testing/standard.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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,
|
|
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:
|
|
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
|
-
|
|
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
|
|
224
|
+
const verify_res = await rpc_call_for_spec({
|
|
215
225
|
app: test_app.app,
|
|
216
226
|
path: rpc_path,
|
|
217
|
-
|
|
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
|
|
242
|
+
const verify_after = await rpc_call_for_spec({
|
|
232
243
|
app: test_app.app,
|
|
233
244
|
path: rpc_path,
|
|
234
|
-
|
|
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
|
|
314
|
+
const res = await rpc_call_for_spec({
|
|
303
315
|
app: test_app.app,
|
|
304
316
|
path: rpc_path,
|
|
305
|
-
|
|
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
|
|
325
|
+
const res = await rpc_call_for_spec({
|
|
313
326
|
app: test_app.app,
|
|
314
327
|
path: rpc_path,
|
|
315
|
-
|
|
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
|
|
337
|
+
const res = await rpc_call_for_spec({
|
|
324
338
|
app: test_app.app,
|
|
325
339
|
path: rpc_path,
|
|
326
|
-
|
|
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
|
|
353
|
+
const list_res = await rpc_call_for_spec({
|
|
339
354
|
app: test_app.app,
|
|
340
355
|
path: rpc_path,
|
|
341
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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
|
|
364
|
+
const revoke_res = await rpc_call_for_spec({
|
|
350
365
|
app: test_app.app,
|
|
351
366
|
path: rpc_path,
|
|
352
|
-
|
|
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
|
-
|
|
358
|
-
assert.strictEqual(
|
|
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
|
|
375
|
+
const after = await rpc_call_for_spec({
|
|
362
376
|
app: test_app.app,
|
|
363
377
|
path: rpc_path,
|
|
364
|
-
|
|
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
|
|
388
|
+
const before = await rpc_call_for_spec({
|
|
374
389
|
app: test_app.app,
|
|
375
390
|
path: rpc_path,
|
|
376
|
-
|
|
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
|
|
397
|
+
const revoke_res = await rpc_call_for_spec({
|
|
382
398
|
app: test_app.app,
|
|
383
399
|
path: rpc_path,
|
|
384
|
-
|
|
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
|
|
406
|
+
const after = await rpc_call_for_spec({
|
|
390
407
|
app: test_app.app,
|
|
391
408
|
path: rpc_path,
|
|
392
|
-
|
|
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
|
|
442
|
+
const verify_after = await rpc_call_for_spec({
|
|
425
443
|
app: test_app.app,
|
|
426
444
|
path: rpc_path,
|
|
427
|
-
|
|
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
|
|
484
|
+
const verify_res = await rpc_call_for_spec({
|
|
466
485
|
app: test_app.app,
|
|
467
486
|
path: rpc_path,
|
|
468
|
-
|
|
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
|
|
520
|
+
const res = await rpc_call_for_spec({
|
|
501
521
|
app: test_app.app,
|
|
502
522
|
path: rpc_path,
|
|
503
|
-
|
|
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; `
|
|
511
|
-
//
|
|
512
|
-
const res = await
|
|
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
|
-
|
|
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
|
|
548
|
+
const res = await rpc_call_for_spec({
|
|
526
549
|
app: test_app.app,
|
|
527
550
|
path: rpc_path,
|
|
528
|
-
|
|
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
|
|
560
|
+
const res = await rpc_call_for_spec({
|
|
536
561
|
app: test_app.app,
|
|
537
562
|
path: rpc_path,
|
|
538
|
-
|
|
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
|
|
574
|
+
const ok_res = await rpc_call_for_spec({
|
|
548
575
|
app: test_app.app,
|
|
549
576
|
path: rpc_path,
|
|
550
|
-
|
|
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
|
|
584
|
+
const res = await rpc_call_for_spec({
|
|
556
585
|
app: test_app.app,
|
|
557
586
|
path: rpc_path,
|
|
558
|
-
|
|
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
|
|
599
|
+
const create_res = await rpc_call_for_spec({
|
|
570
600
|
app: test_app.app,
|
|
571
601
|
path: rpc_path,
|
|
572
|
-
|
|
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
|
|
609
|
+
const use_res = await rpc_call_for_spec({
|
|
580
610
|
app: test_app.app,
|
|
581
611
|
path: rpc_path,
|
|
582
|
-
|
|
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
|
|
619
|
+
const revoke_res = await rpc_call_for_spec({
|
|
588
620
|
app: test_app.app,
|
|
589
621
|
path: rpc_path,
|
|
590
|
-
|
|
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
|
|
628
|
+
const after_res = await rpc_call_for_spec({
|
|
597
629
|
app: test_app.app,
|
|
598
630
|
path: rpc_path,
|
|
599
|
-
|
|
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
|
|
661
|
+
const revoke_res = await rpc_call_for_spec({
|
|
628
662
|
app: test_app.app,
|
|
629
663
|
path: rpc_path,
|
|
630
|
-
|
|
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
|
|
670
|
+
const verify_b = await rpc_call_for_spec({
|
|
636
671
|
app: test_app.app,
|
|
637
672
|
path: rpc_path,
|
|
638
|
-
|
|
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
|
|
684
|
+
const list_res = await rpc_call_for_spec({
|
|
649
685
|
app: test_app.app,
|
|
650
686
|
path: rpc_path,
|
|
651
|
-
|
|
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
|
-
|
|
656
|
-
|
|
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
|
|
695
|
+
const revoke_res = await rpc_call_for_spec({
|
|
660
696
|
app: test_app.app,
|
|
661
697
|
path: rpc_path,
|
|
662
|
-
|
|
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
|
-
|
|
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
|
|
705
|
+
const verify_b = await rpc_call_for_spec({
|
|
671
706
|
app: test_app.app,
|
|
672
707
|
path: rpc_path,
|
|
673
|
-
|
|
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
|
|
719
|
+
const list_res = await rpc_call_for_spec({
|
|
684
720
|
app: test_app.app,
|
|
685
721
|
path: rpc_path,
|
|
686
|
-
|
|
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
|
-
|
|
691
|
-
|
|
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
|
|
730
|
+
const revoke_res = await rpc_call_for_spec({
|
|
695
731
|
app: test_app.app,
|
|
696
732
|
path: rpc_path,
|
|
697
|
-
|
|
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
|
-
|
|
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
|
|
740
|
+
const verify_b = await rpc_call_for_spec({
|
|
706
741
|
app: test_app.app,
|
|
707
742
|
path: rpc_path,
|
|
708
|
-
|
|
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
|
|
754
|
+
const res = await rpc_call_for_spec({
|
|
718
755
|
app: test_app.app,
|
|
719
756
|
path: rpc_path,
|
|
720
|
-
|
|
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
|
|
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
|
|
771
|
+
const res = await rpc_call_for_spec({
|
|
735
772
|
app: test_app.app,
|
|
736
773
|
path: rpc_path,
|
|
737
|
-
|
|
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
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
|
959
|
+
const res = await rpc_call_for_spec({
|
|
893
960
|
app: test_app.app,
|
|
894
961
|
path: rpc_path,
|
|
895
|
-
|
|
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
|
|
979
|
+
const res = await rpc_call_for_spec({
|
|
912
980
|
app: test_app.app,
|
|
913
981
|
path: rpc_path,
|
|
914
|
-
|
|
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
|
|
1042
|
+
const create_res = await rpc_call_for_spec({
|
|
974
1043
|
app: test_app.app,
|
|
975
1044
|
path: rpc_path,
|
|
976
|
-
|
|
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
|
|
1053
|
+
const verify_before = await rpc_call_for_spec({
|
|
985
1054
|
app: test_app.app,
|
|
986
1055
|
path: rpc_path,
|
|
987
|
-
|
|
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
|
|
1076
|
+
const verify_after = await rpc_call_for_spec({
|
|
1006
1077
|
app: test_app.app,
|
|
1007
1078
|
path: rpc_path,
|
|
1008
|
-
|
|
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(
|
|
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
|
|
1105
|
+
const invite_res = await rpc_call_for_spec({
|
|
1033
1106
|
app: test_app.app,
|
|
1034
1107
|
path: rpc_path,
|
|
1035
|
-
|
|
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(
|
|
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
|
|
1152
|
+
const invite_res = await rpc_call_for_spec({
|
|
1080
1153
|
app: test_app.app,
|
|
1081
1154
|
path: rpc_path,
|
|
1082
|
-
|
|
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
|
|
1182
|
+
const invite2_res = await rpc_call_for_spec({
|
|
1110
1183
|
app: test_app.app,
|
|
1111
1184
|
path: rpc_path,
|
|
1112
|
-
|
|
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, `
|
|
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',
|