@fuzdev/fuz_app 0.29.0 → 0.31.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/CLAUDE.md +630 -0
- package/dist/actions/action_rpc.d.ts +29 -0
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +42 -6
- package/dist/actions/action_types.d.ts +2 -2
- package/dist/actions/cancel.d.ts +12 -13
- package/dist/actions/cancel.d.ts.map +1 -1
- package/dist/actions/cancel.js +10 -13
- package/dist/actions/heartbeat.d.ts +8 -13
- package/dist/actions/heartbeat.d.ts.map +1 -1
- package/dist/actions/heartbeat.js +5 -8
- package/dist/actions/register_action_ws.d.ts +3 -3
- package/dist/actions/register_action_ws.js +2 -2
- package/dist/actions/register_ws_endpoint.d.ts +4 -4
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +3 -3
- package/dist/actions/socket.svelte.d.ts +16 -16
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +15 -15
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.d.ts +15 -0
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +17 -0
- package/dist/auth/CLAUDE.md +923 -0
- package/dist/auth/account_action_specs.d.ts +216 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -0
- package/dist/auth/account_action_specs.js +159 -0
- package/dist/auth/account_actions.d.ts +51 -0
- package/dist/auth/account_actions.d.ts.map +1 -0
- package/dist/auth/account_actions.js +119 -0
- package/dist/auth/account_queries.d.ts +6 -2
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +40 -4
- package/dist/auth/account_routes.d.ts +94 -16
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +108 -180
- package/dist/auth/account_schema.d.ts +85 -30
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +40 -8
- package/dist/auth/admin_action_specs.d.ts +674 -0
- package/dist/auth/admin_action_specs.d.ts.map +1 -0
- package/dist/auth/admin_action_specs.js +287 -0
- package/dist/auth/admin_actions.d.ts +69 -0
- package/dist/auth/admin_actions.d.ts.map +1 -0
- package/dist/auth/admin_actions.js +256 -0
- package/dist/auth/api_token.d.ts +10 -0
- package/dist/auth/api_token.d.ts.map +1 -1
- package/dist/auth/api_token.js +9 -0
- package/dist/auth/api_token_queries.d.ts +3 -3
- package/dist/auth/api_token_queries.js +3 -3
- package/dist/auth/app_settings_schema.d.ts +4 -3
- package/dist/auth/app_settings_schema.d.ts.map +1 -1
- package/dist/auth/app_settings_schema.js +2 -1
- package/dist/auth/audit_log_routes.d.ts +14 -6
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +22 -79
- package/dist/auth/audit_log_schema.d.ts +100 -29
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +83 -11
- package/dist/auth/bootstrap_routes.d.ts +14 -0
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +10 -3
- package/dist/auth/cleanup.d.ts +63 -0
- package/dist/auth/cleanup.d.ts.map +1 -0
- package/dist/auth/cleanup.js +80 -0
- package/dist/auth/invite_schema.d.ts +11 -10
- package/dist/auth/invite_schema.d.ts.map +1 -1
- package/dist/auth/invite_schema.js +4 -3
- package/dist/auth/migrations.d.ts +6 -0
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +28 -0
- package/dist/auth/permit_offer_action_specs.d.ts +364 -0
- package/dist/auth/permit_offer_action_specs.d.ts.map +1 -0
- package/dist/auth/permit_offer_action_specs.js +216 -0
- package/dist/auth/permit_offer_actions.d.ts +96 -0
- package/dist/auth/permit_offer_actions.d.ts.map +1 -0
- package/dist/auth/permit_offer_actions.js +428 -0
- package/dist/auth/permit_offer_notifications.d.ts +361 -0
- package/dist/auth/permit_offer_notifications.d.ts.map +1 -0
- package/dist/auth/permit_offer_notifications.js +179 -0
- package/dist/auth/permit_offer_queries.d.ts +165 -0
- package/dist/auth/permit_offer_queries.d.ts.map +1 -0
- package/dist/auth/permit_offer_queries.js +390 -0
- package/dist/auth/permit_offer_schema.d.ts +103 -0
- package/dist/auth/permit_offer_schema.d.ts.map +1 -0
- package/dist/auth/permit_offer_schema.js +142 -0
- package/dist/auth/permit_queries.d.ts +77 -14
- package/dist/auth/permit_queries.d.ts.map +1 -1
- package/dist/auth/permit_queries.js +119 -24
- package/dist/auth/session_queries.d.ts +4 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +4 -2
- package/dist/auth/signup_routes.d.ts +13 -0
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +14 -7
- package/dist/http/CLAUDE.md +584 -0
- package/dist/http/pending_effects.d.ts +29 -0
- package/dist/http/pending_effects.d.ts.map +1 -0
- package/dist/http/pending_effects.js +31 -0
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/route_spec.js +4 -3
- package/dist/rate_limiter.d.ts +30 -0
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +25 -2
- package/dist/realtime/sse_auth_guard.d.ts +2 -0
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +5 -3
- package/dist/testing/CLAUDE.md +668 -1
- package/dist/testing/admin_integration.d.ts +10 -7
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +382 -482
- package/dist/testing/app_server.d.ts +7 -6
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/attack_surface.d.ts +9 -3
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +4 -4
- package/dist/testing/audit_completeness.d.ts +6 -0
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +158 -134
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +4 -33
- package/dist/testing/db.d.ts +1 -1
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +2 -0
- package/dist/testing/entities.d.ts +35 -13
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +17 -0
- package/dist/testing/integration.d.ts +10 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +352 -340
- package/dist/testing/integration_helpers.d.ts +16 -5
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +24 -4
- package/dist/testing/rate_limiting.d.ts +7 -0
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +41 -10
- package/dist/testing/rpc_helpers.d.ts +153 -1
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +184 -8
- package/dist/testing/sse_round_trip.d.ts +8 -0
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +10 -3
- package/dist/testing/standard.d.ts +9 -1
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +6 -2
- package/dist/testing/surface_invariants.d.ts +7 -3
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +5 -4
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +9 -38
- package/dist/ui/AccountSessions.svelte +8 -4
- package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
- package/dist/ui/AdminAccounts.svelte +61 -33
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminAuditLog.svelte +3 -2
- package/dist/ui/AdminAuditLog.svelte.d.ts.map +1 -1
- package/dist/ui/AdminInvites.svelte +3 -2
- package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +14 -9
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/AdminPermitHistory.svelte +3 -2
- package/dist/ui/AdminPermitHistory.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSessions.svelte +29 -25
- package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +351 -0
- package/dist/ui/OpenSignupToggle.svelte +6 -3
- package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -1
- package/dist/ui/PermitOfferForm.svelte +141 -0
- package/dist/ui/PermitOfferForm.svelte.d.ts +14 -0
- package/dist/ui/PermitOfferForm.svelte.d.ts.map +1 -0
- package/dist/ui/PermitOfferHistory.svelte +109 -0
- package/dist/ui/PermitOfferHistory.svelte.d.ts +11 -0
- package/dist/ui/PermitOfferHistory.svelte.d.ts.map +1 -0
- package/dist/ui/PermitOfferInbox.svelte +121 -0
- package/dist/ui/PermitOfferInbox.svelte.d.ts +12 -0
- package/dist/ui/PermitOfferInbox.svelte.d.ts.map +1 -0
- package/dist/ui/account_sessions_state.svelte.d.ts +53 -3
- package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.js +39 -16
- package/dist/ui/admin_accounts_state.svelte.d.ts +118 -2
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +99 -23
- package/dist/ui/admin_invites_state.svelte.d.ts +47 -1
- package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_invites_state.svelte.js +38 -26
- package/dist/ui/admin_sessions_state.svelte.d.ts +26 -0
- package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_sessions_state.svelte.js +35 -21
- package/dist/ui/app_settings_state.svelte.d.ts +39 -0
- package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
- package/dist/ui/app_settings_state.svelte.js +34 -18
- package/dist/ui/audit_log_state.svelte.d.ts +40 -3
- package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
- package/dist/ui/audit_log_state.svelte.js +36 -42
- package/dist/ui/auth_state.svelte.d.ts +4 -3
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +4 -1
- package/dist/ui/permit_offers_state.svelte.d.ts +125 -0
- package/dist/ui/permit_offers_state.svelte.d.ts.map +1 -0
- package/dist/ui/permit_offers_state.svelte.js +197 -0
- package/package.json +3 -3
- package/dist/auth/admin_routes.d.ts +0 -29
- package/dist/auth/admin_routes.d.ts.map +0 -1
- package/dist/auth/admin_routes.js +0 -226
- package/dist/auth/app_settings_routes.d.ts +0 -27
- package/dist/auth/app_settings_routes.d.ts.map +0 -1
- package/dist/auth/app_settings_routes.js +0 -66
- package/dist/auth/invite_routes.d.ts +0 -18
- package/dist/auth/invite_routes.d.ts.map +0 -1
- package/dist/auth/invite_routes.js +0 -129
|
@@ -16,14 +16,18 @@ import './assert_dev_env.js';
|
|
|
16
16
|
*
|
|
17
17
|
* @module
|
|
18
18
|
*/
|
|
19
|
-
import { describe, test, assert, afterAll } from 'vitest';
|
|
19
|
+
import { describe, test, assert, beforeAll, afterAll } from 'vitest';
|
|
20
20
|
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
25
|
import { RateLimiter } from '../rate_limiter.js';
|
|
25
26
|
import { run_migrations } from '../db/migrate.js';
|
|
26
27
|
import { ErrorCoverageCollector, assert_error_coverage, DEFAULT_INTEGRATION_ERROR_COVERAGE, } from './error_coverage.js';
|
|
28
|
+
import { ApiError, ERROR_FORBIDDEN_ORIGIN } from '../http/error_schemas.js';
|
|
29
|
+
import { account_verify_action_spec, account_session_list_action_spec, account_session_revoke_action_spec, account_session_revoke_all_action_spec, account_token_create_action_spec, account_token_list_action_spec, account_token_revoke_action_spec, } from '../auth/account_action_specs.js';
|
|
30
|
+
import { invite_create_action_spec } from '../auth/admin_action_specs.js';
|
|
27
31
|
/**
|
|
28
32
|
* Build `CreateTestAppOptions` from standard options plus a database.
|
|
29
33
|
*/
|
|
@@ -48,6 +52,9 @@ const build_test_app_options = (options, db) => ({
|
|
|
48
52
|
* @param options - session config and route factory
|
|
49
53
|
*/
|
|
50
54
|
export const describe_standard_integration_tests = (options) => {
|
|
55
|
+
// 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);
|
|
51
58
|
const init_schema = async (db) => {
|
|
52
59
|
await run_migrations(db, [AUTH_MIGRATION_NS]);
|
|
53
60
|
};
|
|
@@ -58,27 +65,28 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
58
65
|
// Error coverage tracking across test groups
|
|
59
66
|
const error_collector = new ErrorCoverageCollector();
|
|
60
67
|
let captured_route_specs = null;
|
|
68
|
+
beforeAll(async () => {
|
|
69
|
+
// Capture route specs once up front so coverage assertion runs even if
|
|
70
|
+
// individual tests are skipped or fail early. Route specs are immutable
|
|
71
|
+
// config; the transient test app is discarded.
|
|
72
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
73
|
+
captured_route_specs = test_app.route_specs;
|
|
74
|
+
});
|
|
61
75
|
afterAll(() => {
|
|
62
76
|
if (captured_route_specs) {
|
|
63
|
-
// Scope coverage to auth
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
'
|
|
70
|
-
|
|
71
|
-
'/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
'/bootstrap',
|
|
77
|
-
];
|
|
78
|
-
const auth_routes = captured_route_specs.filter((s) => (auth_suffixes.some((suffix) => s.path.endsWith(suffix)) ||
|
|
79
|
-
s.path.includes('/sessions/:') ||
|
|
80
|
-
s.path.includes('/tokens/:')) &&
|
|
81
|
-
!(s.auth.type === 'role' && s.auth.role === 'admin'));
|
|
77
|
+
// Scope coverage to auth routes this suite actually exercises:
|
|
78
|
+
// the REST auth surface (login/logout/password/signup/bootstrap)
|
|
79
|
+
// plus the shared RPC endpoint. Consumer-specific routes would
|
|
80
|
+
// dilute the coverage percentage; admin-role routes are scoped
|
|
81
|
+
// to the admin suite instead.
|
|
82
|
+
const auth_routes = captured_route_specs.filter((s) => {
|
|
83
|
+
if (s.auth.type === 'role' && s.auth.role === 'admin')
|
|
84
|
+
return false;
|
|
85
|
+
const rest_suffixes = ['/login', '/logout', '/password', '/signup', '/bootstrap'];
|
|
86
|
+
if (rest_suffixes.some((suffix) => s.path.endsWith(suffix)))
|
|
87
|
+
return true;
|
|
88
|
+
return s.path === rpc_path;
|
|
89
|
+
});
|
|
82
90
|
assert_error_coverage(error_collector, auth_routes.length > 0 ? auth_routes : captured_route_specs, {
|
|
83
91
|
min_coverage: DEFAULT_INTEGRATION_ERROR_COVERAGE,
|
|
84
92
|
});
|
|
@@ -111,7 +119,6 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
111
119
|
});
|
|
112
120
|
test('login with wrong password returns 401', async () => {
|
|
113
121
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
114
|
-
captured_route_specs ??= test_app.route_specs;
|
|
115
122
|
const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
|
|
116
123
|
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
117
124
|
const res = await test_app.app.request(login_route.path, {
|
|
@@ -175,10 +182,8 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
175
182
|
test('full cycle: login → verify → logout → verify fails', async () => {
|
|
176
183
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
177
184
|
const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
|
|
178
|
-
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
179
185
|
const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
|
|
180
186
|
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
181
|
-
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
182
187
|
assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
|
|
183
188
|
// Login
|
|
184
189
|
const login_res = await test_app.app.request(login_route.path, {
|
|
@@ -206,7 +211,10 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
206
211
|
cookie: `${cookie_name}=${login_cookie}`,
|
|
207
212
|
});
|
|
208
213
|
// Verify works
|
|
209
|
-
const verify_res = await
|
|
214
|
+
const verify_res = await rpc_call({
|
|
215
|
+
app: test_app.app,
|
|
216
|
+
path: rpc_path,
|
|
217
|
+
method: account_verify_action_spec.method,
|
|
210
218
|
headers: create_headers(),
|
|
211
219
|
});
|
|
212
220
|
assert.strictEqual(verify_res.status, 200);
|
|
@@ -220,7 +228,10 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
220
228
|
assert.strictEqual(logout_body.ok, true);
|
|
221
229
|
assert.strictEqual(logout_body.username, test_app.backend.account.username, 'Logout response should include the username');
|
|
222
230
|
// Verify fails after logout (session revoked)
|
|
223
|
-
const verify_after = await
|
|
231
|
+
const verify_after = await rpc_call({
|
|
232
|
+
app: test_app.app,
|
|
233
|
+
path: rpc_path,
|
|
234
|
+
method: account_verify_action_spec.method,
|
|
224
235
|
headers: create_headers(),
|
|
225
236
|
});
|
|
226
237
|
assert.strictEqual(verify_after.status, 401);
|
|
@@ -288,93 +299,99 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
288
299
|
describe('session security', () => {
|
|
289
300
|
test('no cookie on protected route returns 401', async () => {
|
|
290
301
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
302
|
+
const res = await rpc_call({
|
|
303
|
+
app: test_app.app,
|
|
304
|
+
path: rpc_path,
|
|
305
|
+
method: account_verify_action_spec.method,
|
|
294
306
|
headers: { host: 'localhost' },
|
|
295
307
|
});
|
|
296
308
|
assert.strictEqual(res.status, 401);
|
|
297
|
-
error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
|
|
298
309
|
});
|
|
299
310
|
test('corrupted cookie returns 401', async () => {
|
|
300
311
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
cookie: `${cookie_name}=random_garbage_value`,
|
|
307
|
-
},
|
|
312
|
+
const res = await rpc_call({
|
|
313
|
+
app: test_app.app,
|
|
314
|
+
path: rpc_path,
|
|
315
|
+
method: account_verify_action_spec.method,
|
|
316
|
+
headers: { cookie: `${cookie_name}=random_garbage_value` },
|
|
308
317
|
});
|
|
309
318
|
assert.strictEqual(res.status, 401);
|
|
310
|
-
error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
|
|
311
319
|
});
|
|
312
320
|
test('expired cookie returns 401', async () => {
|
|
313
321
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
314
|
-
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
315
|
-
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
316
322
|
const expired_cookie = await create_expired_test_cookie(test_app.backend.keyring, options.session_options);
|
|
317
|
-
const res = await
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
},
|
|
323
|
+
const res = await rpc_call({
|
|
324
|
+
app: test_app.app,
|
|
325
|
+
path: rpc_path,
|
|
326
|
+
method: account_verify_action_spec.method,
|
|
327
|
+
headers: { cookie: `${cookie_name}=${expired_cookie}` },
|
|
322
328
|
});
|
|
323
329
|
assert.strictEqual(res.status, 401);
|
|
324
|
-
error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
|
|
325
330
|
});
|
|
326
331
|
});
|
|
327
332
|
// --- 4. Session revocation ---
|
|
328
333
|
describe('session revocation', () => {
|
|
329
334
|
test('revoke single session by ID invalidates that session', async () => {
|
|
330
335
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
331
|
-
const sessions_route = find_auth_route(test_app.route_specs, '/sessions', 'GET');
|
|
332
|
-
const revoke_route = test_app.route_specs.find((s) => s.method === 'POST' &&
|
|
333
|
-
s.path.endsWith('/sessions/:id/revoke') &&
|
|
334
|
-
s.auth.type === 'authenticated');
|
|
335
|
-
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
336
|
-
assert.ok(sessions_route, 'Expected GET /sessions route — ensure create_route_specs includes account routes');
|
|
337
|
-
assert.ok(revoke_route, 'Expected POST /sessions/:id/revoke route — ensure create_route_specs includes account routes');
|
|
338
|
-
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
339
336
|
const headers = test_app.create_session_headers();
|
|
340
337
|
// List own sessions to get the session ID
|
|
341
|
-
const list_res = await
|
|
342
|
-
|
|
343
|
-
|
|
338
|
+
const list_res = await rpc_call({
|
|
339
|
+
app: test_app.app,
|
|
340
|
+
path: rpc_path,
|
|
341
|
+
method: account_session_list_action_spec.method,
|
|
342
|
+
headers,
|
|
343
|
+
});
|
|
344
|
+
assert.ok(list_res.ok, 'account_session_list should succeed');
|
|
345
|
+
const list_body = list_res.result;
|
|
344
346
|
assert.ok(list_body.sessions.length >= 1);
|
|
345
347
|
const session_id = list_body.sessions[0].id;
|
|
346
348
|
// Revoke that session by ID
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
349
|
+
const revoke_res = await rpc_call({
|
|
350
|
+
app: test_app.app,
|
|
351
|
+
path: rpc_path,
|
|
352
|
+
method: account_session_revoke_action_spec.method,
|
|
353
|
+
params: { session_id },
|
|
350
354
|
headers,
|
|
351
355
|
});
|
|
352
|
-
assert.
|
|
353
|
-
const revoke_body =
|
|
356
|
+
assert.ok(revoke_res.ok, 'account_session_revoke should succeed');
|
|
357
|
+
const revoke_body = revoke_res.result;
|
|
354
358
|
assert.strictEqual(revoke_body.ok, true);
|
|
355
359
|
assert.strictEqual(revoke_body.revoked, true);
|
|
356
360
|
// Session should no longer work
|
|
357
|
-
const after = await
|
|
361
|
+
const after = await rpc_call({
|
|
362
|
+
app: test_app.app,
|
|
363
|
+
path: rpc_path,
|
|
364
|
+
method: account_verify_action_spec.method,
|
|
365
|
+
headers,
|
|
366
|
+
});
|
|
358
367
|
assert.strictEqual(after.status, 401);
|
|
359
368
|
});
|
|
360
369
|
test('revoke-all invalidates existing session', async () => {
|
|
361
370
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
362
|
-
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
363
|
-
const revoke_route = find_auth_route(test_app.route_specs, '/sessions/revoke-all', 'POST');
|
|
364
|
-
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
365
|
-
assert.ok(revoke_route, 'Expected POST /sessions/revoke-all route — ensure create_route_specs includes account routes');
|
|
366
371
|
const headers = test_app.create_session_headers();
|
|
367
372
|
// Verify works
|
|
368
|
-
const before = await
|
|
373
|
+
const before = await rpc_call({
|
|
374
|
+
app: test_app.app,
|
|
375
|
+
path: rpc_path,
|
|
376
|
+
method: account_verify_action_spec.method,
|
|
377
|
+
headers,
|
|
378
|
+
});
|
|
369
379
|
assert.strictEqual(before.status, 200);
|
|
370
380
|
// Revoke all sessions
|
|
371
|
-
const revoke_res = await
|
|
372
|
-
|
|
381
|
+
const revoke_res = await rpc_call({
|
|
382
|
+
app: test_app.app,
|
|
383
|
+
path: rpc_path,
|
|
384
|
+
method: account_session_revoke_all_action_spec.method,
|
|
373
385
|
headers,
|
|
374
386
|
});
|
|
375
|
-
assert.
|
|
387
|
+
assert.ok(revoke_res.ok, 'account_session_revoke_all should succeed');
|
|
376
388
|
// Verify fails after revocation
|
|
377
|
-
const after = await
|
|
389
|
+
const after = await rpc_call({
|
|
390
|
+
app: test_app.app,
|
|
391
|
+
path: rpc_path,
|
|
392
|
+
method: account_verify_action_spec.method,
|
|
393
|
+
headers,
|
|
394
|
+
});
|
|
378
395
|
assert.strictEqual(after.status, 401);
|
|
379
396
|
});
|
|
380
397
|
});
|
|
@@ -384,10 +401,8 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
384
401
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
385
402
|
const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
|
|
386
403
|
const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
|
|
387
|
-
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
388
404
|
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
389
405
|
assert.ok(password_route, 'Expected POST /password route — ensure create_route_specs includes account routes');
|
|
390
|
-
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
391
406
|
const headers = test_app.create_session_headers({
|
|
392
407
|
'content-type': 'application/json',
|
|
393
408
|
});
|
|
@@ -406,7 +421,10 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
406
421
|
assert.ok(typeof change_body.sessions_revoked === 'number', 'Expected sessions_revoked count');
|
|
407
422
|
assert.ok(change_body.sessions_revoked >= 1, 'Expected at least 1 session revoked');
|
|
408
423
|
// Old session should be invalid
|
|
409
|
-
const verify_after = await
|
|
424
|
+
const verify_after = await rpc_call({
|
|
425
|
+
app: test_app.app,
|
|
426
|
+
path: rpc_path,
|
|
427
|
+
method: account_verify_action_spec.method,
|
|
410
428
|
headers: test_app.create_session_headers(),
|
|
411
429
|
});
|
|
412
430
|
assert.strictEqual(verify_after.status, 401);
|
|
@@ -430,9 +448,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
430
448
|
test('password change with wrong current password returns 401', async () => {
|
|
431
449
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
432
450
|
const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
|
|
433
|
-
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
434
451
|
assert.ok(password_route, 'Expected POST /password route — ensure create_route_specs includes account routes');
|
|
435
|
-
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
436
452
|
const res = await test_app.app.request(password_route.path, {
|
|
437
453
|
method: 'POST',
|
|
438
454
|
headers: test_app.create_session_headers({
|
|
@@ -446,7 +462,10 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
446
462
|
assert.strictEqual(res.status, 401);
|
|
447
463
|
error_collector.record(test_app.route_specs, 'POST', password_route.path, 401);
|
|
448
464
|
// Session should still be valid (password didn't change)
|
|
449
|
-
const verify_res = await
|
|
465
|
+
const verify_res = await rpc_call({
|
|
466
|
+
app: test_app.app,
|
|
467
|
+
path: rpc_path,
|
|
468
|
+
method: account_verify_action_spec.method,
|
|
450
469
|
headers: test_app.create_session_headers(),
|
|
451
470
|
});
|
|
452
471
|
assert.strictEqual(verify_res.status, 200);
|
|
@@ -456,37 +475,45 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
456
475
|
describe('origin verification', () => {
|
|
457
476
|
test('evil origin is rejected with 403', async () => {
|
|
458
477
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const res = await test_app.app.request(
|
|
478
|
+
// `verify_request_source` runs before the RPC dispatcher and returns a
|
|
479
|
+
// plain REST `{error}` body — not a JSON-RPC envelope. Skip `rpc_call`.
|
|
480
|
+
const res = await test_app.app.request(rpc_path, {
|
|
481
|
+
method: 'POST',
|
|
462
482
|
headers: {
|
|
463
483
|
host: 'localhost',
|
|
464
484
|
origin: 'http://evil.com',
|
|
485
|
+
'content-type': 'application/json',
|
|
465
486
|
cookie: `${cookie_name}=${test_app.backend.session_cookie}`,
|
|
466
487
|
},
|
|
488
|
+
body: JSON.stringify({
|
|
489
|
+
jsonrpc: '2.0',
|
|
490
|
+
method: account_verify_action_spec.method,
|
|
491
|
+
id: 'evil-origin',
|
|
492
|
+
}),
|
|
467
493
|
});
|
|
468
494
|
assert.strictEqual(res.status, 403);
|
|
469
|
-
const body = await res.json();
|
|
470
|
-
assert.strictEqual(body.error,
|
|
495
|
+
const body = ApiError.parse(await res.json());
|
|
496
|
+
assert.strictEqual(body.error, ERROR_FORBIDDEN_ORIGIN);
|
|
471
497
|
});
|
|
472
498
|
test('valid origin is accepted', async () => {
|
|
473
499
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
500
|
+
const res = await rpc_call({
|
|
501
|
+
app: test_app.app,
|
|
502
|
+
path: rpc_path,
|
|
503
|
+
method: account_verify_action_spec.method,
|
|
477
504
|
headers: test_app.create_session_headers(),
|
|
478
505
|
});
|
|
479
506
|
assert.strictEqual(res.status, 200);
|
|
480
507
|
});
|
|
481
508
|
test('no origin header is allowed (direct access)', async () => {
|
|
482
509
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const res = await
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
},
|
|
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({
|
|
513
|
+
app: test_app.app,
|
|
514
|
+
path: rpc_path,
|
|
515
|
+
method: account_verify_action_spec.method,
|
|
516
|
+
headers: { cookie: `${cookie_name}=${test_app.backend.session_cookie}` },
|
|
490
517
|
});
|
|
491
518
|
assert.notStrictEqual(res.status, 403);
|
|
492
519
|
});
|
|
@@ -495,86 +522,84 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
495
522
|
describe('bearer auth', () => {
|
|
496
523
|
test('valid bearer token authenticates', async () => {
|
|
497
524
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
525
|
+
const res = await rpc_call_non_browser({
|
|
526
|
+
app: test_app.app,
|
|
527
|
+
path: rpc_path,
|
|
528
|
+
method: account_verify_action_spec.method,
|
|
501
529
|
headers: test_app.create_bearer_headers(),
|
|
502
530
|
});
|
|
503
531
|
assert.strictEqual(res.status, 200);
|
|
504
532
|
});
|
|
505
533
|
test('invalid bearer token returns 401', async () => {
|
|
506
534
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
authorization: 'Bearer secret_fuz_token_invalid',
|
|
513
|
-
},
|
|
535
|
+
const res = await rpc_call_non_browser({
|
|
536
|
+
app: test_app.app,
|
|
537
|
+
path: rpc_path,
|
|
538
|
+
method: account_verify_action_spec.method,
|
|
539
|
+
headers: { authorization: 'Bearer secret_fuz_token_invalid' },
|
|
514
540
|
});
|
|
515
541
|
assert.strictEqual(res.status, 401);
|
|
516
|
-
error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
|
|
517
542
|
});
|
|
518
543
|
test('bearer token with Origin header is rejected', async () => {
|
|
519
544
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
520
|
-
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
521
|
-
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
522
545
|
const bearer_headers = test_app.create_bearer_headers();
|
|
523
|
-
// Without Origin — works
|
|
524
|
-
const ok_res = await
|
|
546
|
+
// Without Origin — works.
|
|
547
|
+
const ok_res = await rpc_call_non_browser({
|
|
548
|
+
app: test_app.app,
|
|
549
|
+
path: rpc_path,
|
|
550
|
+
method: account_verify_action_spec.method,
|
|
525
551
|
headers: bearer_headers,
|
|
526
552
|
});
|
|
527
553
|
assert.strictEqual(ok_res.status, 200);
|
|
528
|
-
// With Origin — bearer silently discarded (browser context), falls through to no auth
|
|
529
|
-
const res = await
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
},
|
|
554
|
+
// With Origin — bearer silently discarded (browser context), falls through to no auth.
|
|
555
|
+
const res = await rpc_call({
|
|
556
|
+
app: test_app.app,
|
|
557
|
+
path: rpc_path,
|
|
558
|
+
method: account_verify_action_spec.method,
|
|
559
|
+
headers: { ...bearer_headers, origin: 'http://localhost:5173' },
|
|
534
560
|
});
|
|
535
561
|
assert.strictEqual(res.status, 401);
|
|
536
|
-
error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
|
|
537
562
|
});
|
|
538
563
|
});
|
|
539
564
|
// --- 7. Token revocation ---
|
|
540
565
|
describe('token revocation', () => {
|
|
541
566
|
test('revoked API token returns 401', async () => {
|
|
542
567
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
543
|
-
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const create_res = await test_app.app.request(create_token_route.path, {
|
|
551
|
-
method: 'POST',
|
|
552
|
-
headers: {
|
|
553
|
-
...test_app.create_session_headers(),
|
|
554
|
-
'content-type': 'application/json',
|
|
555
|
-
},
|
|
556
|
-
body: JSON.stringify({ name: 'test-revoke' }),
|
|
568
|
+
// Create a new token via RPC
|
|
569
|
+
const create_res = await rpc_call({
|
|
570
|
+
app: test_app.app,
|
|
571
|
+
path: rpc_path,
|
|
572
|
+
method: account_token_create_action_spec.method,
|
|
573
|
+
params: { name: 'test-revoke' },
|
|
574
|
+
headers: test_app.create_session_headers(),
|
|
557
575
|
});
|
|
558
|
-
assert.
|
|
559
|
-
const { token, id } =
|
|
576
|
+
assert.ok(create_res.ok, 'account_token_create should succeed');
|
|
577
|
+
const { token, id } = create_res.result;
|
|
560
578
|
// Verify token works
|
|
561
|
-
const use_res = await
|
|
562
|
-
|
|
579
|
+
const use_res = await rpc_call_non_browser({
|
|
580
|
+
app: test_app.app,
|
|
581
|
+
path: rpc_path,
|
|
582
|
+
method: account_verify_action_spec.method,
|
|
583
|
+
headers: { authorization: `Bearer ${token}` },
|
|
563
584
|
});
|
|
564
585
|
assert.strictEqual(use_res.status, 200);
|
|
565
|
-
// Revoke via
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
586
|
+
// Revoke via RPC
|
|
587
|
+
const revoke_res = await rpc_call({
|
|
588
|
+
app: test_app.app,
|
|
589
|
+
path: rpc_path,
|
|
590
|
+
method: account_token_revoke_action_spec.method,
|
|
591
|
+
params: { token_id: id },
|
|
569
592
|
headers: test_app.create_session_headers(),
|
|
570
593
|
});
|
|
571
|
-
assert.
|
|
594
|
+
assert.ok(revoke_res.ok, 'account_token_revoke should succeed');
|
|
572
595
|
// Token should no longer work
|
|
573
|
-
const after_res = await
|
|
574
|
-
|
|
596
|
+
const after_res = await rpc_call_non_browser({
|
|
597
|
+
app: test_app.app,
|
|
598
|
+
path: rpc_path,
|
|
599
|
+
method: account_verify_action_spec.method,
|
|
600
|
+
headers: { authorization: `Bearer ${token}` },
|
|
575
601
|
});
|
|
576
602
|
assert.strictEqual(after_res.status, 401);
|
|
577
|
-
error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
|
|
578
603
|
});
|
|
579
604
|
});
|
|
580
605
|
// --- 8. Cross-account isolation ---
|
|
@@ -596,114 +621,107 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
596
621
|
});
|
|
597
622
|
test("user A cannot revoke user B's sessions", async () => {
|
|
598
623
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
599
|
-
const revoke_all_route = find_auth_route(test_app.route_specs, '/sessions/revoke-all', 'POST');
|
|
600
|
-
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
601
|
-
assert.ok(revoke_all_route, 'Expected POST /sessions/revoke-all route — ensure create_route_specs includes account routes');
|
|
602
|
-
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
603
624
|
// Create a second account
|
|
604
625
|
const user_b = await test_app.create_account({ username: 'user_b' });
|
|
605
626
|
// User A revokes all their own sessions
|
|
606
|
-
const revoke_res = await
|
|
607
|
-
|
|
627
|
+
const revoke_res = await rpc_call({
|
|
628
|
+
app: test_app.app,
|
|
629
|
+
path: rpc_path,
|
|
630
|
+
method: account_session_revoke_all_action_spec.method,
|
|
608
631
|
headers: test_app.create_session_headers(),
|
|
609
632
|
});
|
|
610
|
-
assert.
|
|
633
|
+
assert.ok(revoke_res.ok, 'account_session_revoke_all should succeed');
|
|
611
634
|
// User B's session should still work
|
|
612
|
-
const verify_b = await
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
},
|
|
635
|
+
const verify_b = await rpc_call({
|
|
636
|
+
app: test_app.app,
|
|
637
|
+
path: rpc_path,
|
|
638
|
+
method: account_verify_action_spec.method,
|
|
639
|
+
headers: { cookie: `${cookie_name}=${user_b.session_cookie}` },
|
|
617
640
|
});
|
|
618
641
|
assert.strictEqual(verify_b.status, 200);
|
|
619
642
|
});
|
|
620
643
|
test("user A cannot revoke user B's session by ID", async () => {
|
|
621
644
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
622
|
-
const sessions_route = find_auth_route(test_app.route_specs, '/sessions', 'GET');
|
|
623
|
-
const revoke_route = test_app.route_specs.find((s) => s.method === 'POST' &&
|
|
624
|
-
s.path.endsWith('/sessions/:id/revoke') &&
|
|
625
|
-
s.auth.type === 'authenticated');
|
|
626
|
-
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
627
|
-
assert.ok(sessions_route, 'Expected GET /sessions route — ensure create_route_specs includes account routes');
|
|
628
|
-
assert.ok(revoke_route, 'Expected POST /sessions/:id/revoke route — ensure create_route_specs includes account routes');
|
|
629
|
-
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
630
645
|
const user_b = await test_app.create_account({ username: 'user_b' });
|
|
631
|
-
const user_b_headers = {
|
|
632
|
-
host: 'localhost',
|
|
633
|
-
cookie: `${cookie_name}=${user_b.session_cookie}`,
|
|
634
|
-
};
|
|
646
|
+
const user_b_headers = { cookie: `${cookie_name}=${user_b.session_cookie}` };
|
|
635
647
|
// Get user B's session ID by listing as user B
|
|
636
|
-
const list_res = await
|
|
648
|
+
const list_res = await rpc_call({
|
|
649
|
+
app: test_app.app,
|
|
650
|
+
path: rpc_path,
|
|
651
|
+
method: account_session_list_action_spec.method,
|
|
637
652
|
headers: user_b_headers,
|
|
638
653
|
});
|
|
639
|
-
assert.
|
|
640
|
-
const list_body =
|
|
654
|
+
assert.ok(list_res.ok, 'account_session_list should succeed');
|
|
655
|
+
const list_body = list_res.result;
|
|
641
656
|
assert.ok(list_body.sessions.length >= 1);
|
|
642
657
|
const session_id_b = list_body.sessions[0].id;
|
|
643
658
|
// User A tries to revoke user B's session by ID
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
659
|
+
const revoke_res = await rpc_call({
|
|
660
|
+
app: test_app.app,
|
|
661
|
+
path: rpc_path,
|
|
662
|
+
method: account_session_revoke_action_spec.method,
|
|
663
|
+
params: { session_id: session_id_b },
|
|
647
664
|
headers: test_app.create_session_headers(),
|
|
648
665
|
});
|
|
649
|
-
assert.
|
|
650
|
-
const revoke_body =
|
|
666
|
+
assert.ok(revoke_res.ok, 'account_session_revoke should succeed');
|
|
667
|
+
const revoke_body = revoke_res.result;
|
|
651
668
|
assert.strictEqual(revoke_body.revoked, false, 'Should not revoke another account session');
|
|
652
669
|
// User B's session should still work
|
|
653
|
-
const verify_b = await
|
|
670
|
+
const verify_b = await rpc_call({
|
|
671
|
+
app: test_app.app,
|
|
672
|
+
path: rpc_path,
|
|
673
|
+
method: account_verify_action_spec.method,
|
|
654
674
|
headers: user_b_headers,
|
|
655
675
|
});
|
|
656
676
|
assert.strictEqual(verify_b.status, 200);
|
|
657
677
|
});
|
|
658
678
|
test("user A cannot revoke user B's token by ID", async () => {
|
|
659
679
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
660
|
-
const tokens_route = find_auth_route(test_app.route_specs, '/tokens', 'GET');
|
|
661
|
-
const revoke_route = test_app.route_specs.find((s) => s.method === 'POST' &&
|
|
662
|
-
s.path.endsWith('/tokens/:id/revoke') &&
|
|
663
|
-
s.auth.type === 'authenticated');
|
|
664
|
-
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
665
|
-
assert.ok(tokens_route, 'Expected GET /tokens route — ensure create_route_specs includes account routes');
|
|
666
|
-
assert.ok(revoke_route, 'Expected POST /tokens/:id/revoke route — ensure create_route_specs includes account routes');
|
|
667
|
-
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
668
680
|
const user_b = await test_app.create_account({ username: 'user_b' });
|
|
669
|
-
const user_b_headers = {
|
|
670
|
-
host: 'localhost',
|
|
671
|
-
cookie: `${cookie_name}=${user_b.session_cookie}`,
|
|
672
|
-
};
|
|
681
|
+
const user_b_headers = { cookie: `${cookie_name}=${user_b.session_cookie}` };
|
|
673
682
|
// Get user B's token ID by listing as user B
|
|
674
|
-
const list_res = await
|
|
683
|
+
const list_res = await rpc_call({
|
|
684
|
+
app: test_app.app,
|
|
685
|
+
path: rpc_path,
|
|
686
|
+
method: account_token_list_action_spec.method,
|
|
675
687
|
headers: user_b_headers,
|
|
676
688
|
});
|
|
677
|
-
assert.
|
|
678
|
-
const list_body =
|
|
689
|
+
assert.ok(list_res.ok, 'account_token_list should succeed');
|
|
690
|
+
const list_body = list_res.result;
|
|
679
691
|
assert.ok(list_body.tokens.length >= 1);
|
|
680
692
|
const token_id_b = list_body.tokens[0].id;
|
|
681
693
|
// User A tries to revoke user B's token by ID
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
694
|
+
const revoke_res = await rpc_call({
|
|
695
|
+
app: test_app.app,
|
|
696
|
+
path: rpc_path,
|
|
697
|
+
method: account_token_revoke_action_spec.method,
|
|
698
|
+
params: { token_id: token_id_b },
|
|
685
699
|
headers: test_app.create_session_headers(),
|
|
686
700
|
});
|
|
687
|
-
assert.
|
|
688
|
-
const revoke_body =
|
|
701
|
+
assert.ok(revoke_res.ok, 'account_token_revoke should succeed');
|
|
702
|
+
const revoke_body = revoke_res.result;
|
|
689
703
|
assert.strictEqual(revoke_body.revoked, false, 'Should not revoke another account token');
|
|
690
704
|
// User B's bearer token should still work
|
|
691
|
-
const verify_b = await
|
|
692
|
-
|
|
705
|
+
const verify_b = await rpc_call_non_browser({
|
|
706
|
+
app: test_app.app,
|
|
707
|
+
path: rpc_path,
|
|
708
|
+
method: account_verify_action_spec.method,
|
|
709
|
+
headers: { authorization: `Bearer ${user_b.api_token}` },
|
|
693
710
|
});
|
|
694
711
|
assert.strictEqual(verify_b.status, 200);
|
|
695
712
|
});
|
|
696
713
|
test("user A's session list does not include user B's sessions", async () => {
|
|
697
714
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
698
|
-
const sessions_route = find_auth_route(test_app.route_specs, '/sessions', 'GET');
|
|
699
|
-
assert.ok(sessions_route, 'Expected GET /sessions route — ensure create_route_specs includes account routes');
|
|
700
715
|
const user_b = await test_app.create_account({ username: 'user_b' });
|
|
701
716
|
// User A lists sessions
|
|
702
|
-
const res = await
|
|
717
|
+
const res = await rpc_call({
|
|
718
|
+
app: test_app.app,
|
|
719
|
+
path: rpc_path,
|
|
720
|
+
method: account_session_list_action_spec.method,
|
|
703
721
|
headers: test_app.create_session_headers(),
|
|
704
722
|
});
|
|
705
|
-
assert.
|
|
706
|
-
const body =
|
|
723
|
+
assert.ok(res.ok, 'account_session_list should succeed');
|
|
724
|
+
const body = res.result;
|
|
707
725
|
// Sessions should only belong to user A's account
|
|
708
726
|
for (const session of body.sessions) {
|
|
709
727
|
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})`);
|
|
@@ -711,15 +729,16 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
711
729
|
});
|
|
712
730
|
test("user A's token list does not include user B's tokens", async () => {
|
|
713
731
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
714
|
-
const tokens_route = find_auth_route(test_app.route_specs, '/tokens', 'GET');
|
|
715
|
-
assert.ok(tokens_route, 'Expected GET /tokens route — ensure create_route_specs includes account routes');
|
|
716
732
|
const user_b = await test_app.create_account({ username: 'user_b' });
|
|
717
733
|
// User A lists tokens
|
|
718
|
-
const res = await
|
|
734
|
+
const res = await rpc_call({
|
|
735
|
+
app: test_app.app,
|
|
736
|
+
path: rpc_path,
|
|
737
|
+
method: account_token_list_action_spec.method,
|
|
719
738
|
headers: test_app.create_session_headers(),
|
|
720
739
|
});
|
|
721
|
-
assert.
|
|
722
|
-
const body =
|
|
740
|
+
assert.ok(res.ok, 'account_token_list should succeed');
|
|
741
|
+
const body = res.result;
|
|
723
742
|
// Tokens should only belong to user A's account
|
|
724
743
|
for (const token of body.tokens) {
|
|
725
744
|
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})`);
|
|
@@ -728,58 +747,58 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
728
747
|
});
|
|
729
748
|
// --- 9. Response body validation ---
|
|
730
749
|
describe('response body validation', () => {
|
|
731
|
-
|
|
750
|
+
// `assert_response_matches_spec` validates REST `RouteSpec` outputs.
|
|
751
|
+
// The account REST routes that used to cover this (/verify, /sessions,
|
|
752
|
+
// /tokens, /tokens/create) moved to RPC in the 2026-04-23 migration,
|
|
753
|
+
// so we exercise the remaining REST endpoints (/login, /logout,
|
|
754
|
+
// /password) against their declared schemas. RPC output validation is
|
|
755
|
+
// covered by `describe_rpc_round_trip_tests`.
|
|
756
|
+
test('POST /login 401 response matches declared error schema', async () => {
|
|
732
757
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
733
|
-
const
|
|
734
|
-
assert.ok(
|
|
735
|
-
const res = await test_app.app.request(
|
|
736
|
-
|
|
758
|
+
const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
|
|
759
|
+
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
760
|
+
const res = await test_app.app.request(login_route.path, {
|
|
761
|
+
method: 'POST',
|
|
762
|
+
headers: {
|
|
763
|
+
host: 'localhost',
|
|
764
|
+
origin: 'http://localhost:5173',
|
|
765
|
+
'content-type': 'application/json',
|
|
766
|
+
},
|
|
767
|
+
body: JSON.stringify({
|
|
768
|
+
username: 'nonexistent_user_xyz',
|
|
769
|
+
password: 'any-password',
|
|
770
|
+
}),
|
|
737
771
|
});
|
|
738
772
|
assert.strictEqual(res.status, 401);
|
|
739
|
-
|
|
740
|
-
await assert_response_matches_spec(test_app.route_specs, 'GET', verify_route.path, res);
|
|
741
|
-
});
|
|
742
|
-
test('GET /verify 200 response matches output schema', async () => {
|
|
743
|
-
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
744
|
-
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
745
|
-
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
746
|
-
const res = await test_app.app.request(verify_route.path, {
|
|
747
|
-
headers: test_app.create_session_headers(),
|
|
748
|
-
});
|
|
749
|
-
assert.strictEqual(res.status, 200);
|
|
750
|
-
await assert_response_matches_spec(test_app.route_specs, 'GET', verify_route.path, res);
|
|
773
|
+
await assert_response_matches_spec(test_app.route_specs, 'POST', login_route.path, res);
|
|
751
774
|
});
|
|
752
|
-
test('
|
|
775
|
+
test('POST /logout 200 response matches output schema', async () => {
|
|
753
776
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
754
|
-
const
|
|
755
|
-
assert.ok(
|
|
756
|
-
const res = await test_app.app.request(
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
await assert_response_matches_spec(test_app.route_specs, 'GET', sessions_route.path, res);
|
|
761
|
-
});
|
|
762
|
-
test('GET /tokens 200 response matches output schema', async () => {
|
|
763
|
-
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
764
|
-
const tokens_route = find_auth_route(test_app.route_specs, '/tokens', 'GET');
|
|
765
|
-
assert.ok(tokens_route, 'Expected GET /tokens route — ensure create_route_specs includes account routes');
|
|
766
|
-
const res = await test_app.app.request(tokens_route.path, {
|
|
767
|
-
headers: test_app.create_session_headers(),
|
|
777
|
+
const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
|
|
778
|
+
assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
|
|
779
|
+
const res = await test_app.app.request(logout_route.path, {
|
|
780
|
+
method: 'POST',
|
|
781
|
+
headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
|
|
782
|
+
body: JSON.stringify({}),
|
|
768
783
|
});
|
|
769
784
|
assert.strictEqual(res.status, 200);
|
|
770
|
-
await assert_response_matches_spec(test_app.route_specs, '
|
|
785
|
+
await assert_response_matches_spec(test_app.route_specs, 'POST', logout_route.path, res);
|
|
771
786
|
});
|
|
772
|
-
test('POST /
|
|
787
|
+
test('POST /logout 401 response matches declared error schema', async () => {
|
|
773
788
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
774
|
-
const
|
|
775
|
-
assert.ok(
|
|
776
|
-
const res = await test_app.app.request(
|
|
789
|
+
const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
|
|
790
|
+
assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
|
|
791
|
+
const res = await test_app.app.request(logout_route.path, {
|
|
777
792
|
method: 'POST',
|
|
778
|
-
headers:
|
|
779
|
-
|
|
793
|
+
headers: {
|
|
794
|
+
host: 'localhost',
|
|
795
|
+
origin: 'http://localhost:5173',
|
|
796
|
+
'content-type': 'application/json',
|
|
797
|
+
},
|
|
798
|
+
body: JSON.stringify({}),
|
|
780
799
|
});
|
|
781
|
-
assert.strictEqual(res.status,
|
|
782
|
-
await assert_response_matches_spec(test_app.route_specs, 'POST',
|
|
800
|
+
assert.strictEqual(res.status, 401);
|
|
801
|
+
await assert_response_matches_spec(test_app.route_specs, 'POST', logout_route.path, res);
|
|
783
802
|
});
|
|
784
803
|
});
|
|
785
804
|
// --- 10b. Rate limiting smoke test (full middleware stack) ---
|
|
@@ -833,31 +852,36 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
833
852
|
describe('error coverage breadth', () => {
|
|
834
853
|
test('exercises 401 on multiple auth-required routes for error coverage', async () => {
|
|
835
854
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
836
|
-
// Hit several auth-required
|
|
837
|
-
// error coverage beyond just /
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
855
|
+
// Hit several auth-required RPC methods without credentials to
|
|
856
|
+
// 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,
|
|
845
870
|
headers: { host: 'localhost' },
|
|
846
871
|
});
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
}
|
|
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);
|
|
850
874
|
}
|
|
851
|
-
// Also exercise POST /logout without auth
|
|
875
|
+
// Also exercise POST /logout without auth (still REST)
|
|
852
876
|
const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
|
|
853
877
|
if (logout_route) {
|
|
854
878
|
const res = await test_app.app.request(logout_route.path, {
|
|
855
879
|
method: 'POST',
|
|
856
|
-
headers: { host: 'localhost' },
|
|
880
|
+
headers: { host: 'localhost', 'content-type': 'application/json' },
|
|
881
|
+
body: JSON.stringify({}),
|
|
857
882
|
});
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
}
|
|
883
|
+
assert.strictEqual(res.status, 401, 'POST /logout without auth should return 401');
|
|
884
|
+
error_collector.record(test_app.route_specs, 'POST', logout_route.path, 401);
|
|
861
885
|
}
|
|
862
886
|
});
|
|
863
887
|
});
|
|
@@ -865,32 +889,32 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
865
889
|
describe('error response information leakage', () => {
|
|
866
890
|
test('401 responses contain no leaky fields', async () => {
|
|
867
891
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
892
|
+
const res = await rpc_call({
|
|
893
|
+
app: test_app.app,
|
|
894
|
+
path: rpc_path,
|
|
895
|
+
method: account_verify_action_spec.method,
|
|
872
896
|
headers: { host: 'localhost' },
|
|
873
897
|
});
|
|
874
898
|
assert.strictEqual(res.status, 401);
|
|
875
|
-
|
|
876
|
-
|
|
899
|
+
assert.ok(!res.ok);
|
|
900
|
+
// Check every field on the JSON-RPC `error` object — `data` carries the
|
|
901
|
+
// handler-authored shape, but `message` and any sibling fields should
|
|
902
|
+
// equally be free of stack traces, file paths, or other internals.
|
|
903
|
+
assert_no_error_info_leakage(res.error, `RPC ${account_verify_action_spec.method} 401 error envelope`);
|
|
877
904
|
});
|
|
878
905
|
});
|
|
879
906
|
// --- 11. Expired credential rejection ---
|
|
880
907
|
describe('expired credential rejection', () => {
|
|
881
908
|
test('expired session cookie returns 401', async () => {
|
|
882
909
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
883
|
-
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
884
|
-
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
885
910
|
const expired_cookie = await create_expired_test_cookie(test_app.backend.keyring, options.session_options);
|
|
886
|
-
const res = await
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
},
|
|
911
|
+
const res = await rpc_call({
|
|
912
|
+
app: test_app.app,
|
|
913
|
+
path: rpc_path,
|
|
914
|
+
method: account_verify_action_spec.method,
|
|
915
|
+
headers: { cookie: `${cookie_name}=${expired_cookie}` },
|
|
891
916
|
});
|
|
892
917
|
assert.strictEqual(res.status, 401, 'Expired session cookie should be rejected');
|
|
893
|
-
error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
|
|
894
918
|
});
|
|
895
919
|
test('expired session cookie returns 401 on mutation route', async () => {
|
|
896
920
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
@@ -943,27 +967,28 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
943
967
|
describe('password change revokes API tokens', () => {
|
|
944
968
|
test('API tokens are invalidated after password change', async () => {
|
|
945
969
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
946
|
-
const token_create_route = find_auth_route(test_app.route_specs, '/tokens/create', 'POST');
|
|
947
970
|
const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
|
|
948
|
-
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
949
|
-
assert.ok(token_create_route, 'Expected POST /tokens/create route');
|
|
950
971
|
assert.ok(password_route, 'Expected POST /password route');
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
972
|
+
// Create an API token via RPC
|
|
973
|
+
const create_res = await rpc_call({
|
|
974
|
+
app: test_app.app,
|
|
975
|
+
path: rpc_path,
|
|
976
|
+
method: account_token_create_action_spec.method,
|
|
977
|
+
params: { name: 'test-token' },
|
|
978
|
+
headers: test_app.create_session_headers(),
|
|
957
979
|
});
|
|
958
|
-
assert.
|
|
959
|
-
const { token: raw_token } =
|
|
980
|
+
assert.ok(create_res.ok, 'account_token_create should succeed');
|
|
981
|
+
const { token: raw_token } = create_res.result;
|
|
960
982
|
assert.ok(raw_token, 'Expected raw token in create response');
|
|
961
983
|
// Verify bearer token works
|
|
962
|
-
const verify_before = await
|
|
963
|
-
|
|
984
|
+
const verify_before = await rpc_call_non_browser({
|
|
985
|
+
app: test_app.app,
|
|
986
|
+
path: rpc_path,
|
|
987
|
+
method: account_verify_action_spec.method,
|
|
988
|
+
headers: { authorization: `Bearer ${raw_token}` },
|
|
964
989
|
});
|
|
965
990
|
assert.strictEqual(verify_before.status, 200, 'Bearer token should work before password change');
|
|
966
|
-
// Change password
|
|
991
|
+
// Change password (still REST)
|
|
967
992
|
const change_res = await test_app.app.request(password_route.path, {
|
|
968
993
|
method: 'POST',
|
|
969
994
|
headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
|
|
@@ -977,8 +1002,11 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
977
1002
|
assert.ok(typeof change_body.tokens_revoked === 'number', 'Expected tokens_revoked count');
|
|
978
1003
|
assert.ok(change_body.tokens_revoked >= 1, 'Expected at least 1 token revoked');
|
|
979
1004
|
// Bearer token should now be invalid
|
|
980
|
-
const verify_after = await
|
|
981
|
-
|
|
1005
|
+
const verify_after = await rpc_call_non_browser({
|
|
1006
|
+
app: test_app.app,
|
|
1007
|
+
path: rpc_path,
|
|
1008
|
+
method: account_verify_action_spec.method,
|
|
1009
|
+
headers: { authorization: `Bearer ${raw_token}` },
|
|
982
1010
|
});
|
|
983
1011
|
assert.strictEqual(verify_after.status, 401, 'Bearer token should be rejected after password change');
|
|
984
1012
|
});
|
|
@@ -990,30 +1018,25 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
990
1018
|
const signup_route = test_app.route_specs.find((s) => s.method === 'POST' && s.path.endsWith('/signup') && s.auth.type === 'none');
|
|
991
1019
|
if (!signup_route)
|
|
992
1020
|
return; // signup is optional
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
return; // invite routes are optional
|
|
1021
|
+
// `invite_create` became RPC-only in the 2026-04-23 migration.
|
|
1022
|
+
// Consumers that don't wire admin RPC actions can't exercise invites;
|
|
1023
|
+
// skip the test rather than fail.
|
|
1024
|
+
if (!find_rpc_action(options.rpc_endpoints, invite_create_action_spec.method))
|
|
1025
|
+
return;
|
|
999
1026
|
// Create an admin to manage invites
|
|
1000
1027
|
const admin = await test_app.create_account({
|
|
1001
1028
|
username: 'invite_edge_admin',
|
|
1002
1029
|
roles: ['admin'],
|
|
1003
1030
|
});
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
const invite_res = await test_app.app.request(invite_route.path, {
|
|
1012
|
-
method: 'POST',
|
|
1013
|
-
headers: admin_headers,
|
|
1014
|
-
body: JSON.stringify({ email: 'alice@example.com' }),
|
|
1031
|
+
// Create invite for alice@example.com via RPC
|
|
1032
|
+
const invite_res = await rpc_call({
|
|
1033
|
+
app: test_app.app,
|
|
1034
|
+
path: rpc_path,
|
|
1035
|
+
method: invite_create_action_spec.method,
|
|
1036
|
+
params: { email: 'alice@example.com' },
|
|
1037
|
+
headers: { cookie: `${cookie_name}=${admin.session_cookie}` },
|
|
1015
1038
|
});
|
|
1016
|
-
assert.
|
|
1039
|
+
assert.ok(invite_res.ok, `invite_create failed: ${invite_res.ok ? '' : JSON.stringify(invite_res.error)}`);
|
|
1017
1040
|
// Try to sign up with a different email — should fail (no matching invite)
|
|
1018
1041
|
const signup_res = await test_app.app.request(signup_route.path, {
|
|
1019
1042
|
method: 'POST',
|
|
@@ -1041,39 +1064,26 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
1041
1064
|
const signup_route = test_app.route_specs.find((s) => s.method === 'POST' && s.path.endsWith('/signup') && s.auth.type === 'none');
|
|
1042
1065
|
if (!signup_route)
|
|
1043
1066
|
return; // signup is optional
|
|
1044
|
-
//
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
s.auth.type === 'role' &&
|
|
1048
|
-
s.auth.role === 'admin');
|
|
1049
|
-
if (!invite_route)
|
|
1050
|
-
return; // invite routes are optional
|
|
1051
|
-
// Find admin accounts route to get admin's account ID
|
|
1052
|
-
const accounts_route = test_app.route_specs.find((s) => s.method === 'GET' &&
|
|
1053
|
-
s.path.endsWith('/accounts') &&
|
|
1054
|
-
s.auth.type === 'role' &&
|
|
1055
|
-
s.auth.role === 'admin');
|
|
1056
|
-
if (!accounts_route)
|
|
1067
|
+
// `invite_create` became RPC-only in the 2026-04-23 migration.
|
|
1068
|
+
// 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))
|
|
1057
1070
|
return;
|
|
1058
1071
|
// We need admin access — create an admin account
|
|
1059
1072
|
const admin = await test_app.create_account({
|
|
1060
1073
|
username: 'signup_test_admin',
|
|
1061
1074
|
roles: ['admin'],
|
|
1062
1075
|
});
|
|
1063
|
-
const admin_headers = {
|
|
1064
|
-
|
|
1065
|
-
origin: 'http://localhost:5173',
|
|
1066
|
-
cookie: `${cookie_name}=${admin.session_cookie}`,
|
|
1067
|
-
'content-type': 'application/json',
|
|
1068
|
-
};
|
|
1069
|
-
// Create an invite for a specific test email
|
|
1076
|
+
const admin_headers = { cookie: `${cookie_name}=${admin.session_cookie}` };
|
|
1077
|
+
// Create an invite for a specific test email via RPC
|
|
1070
1078
|
const test_email = 'signup-test@example.com';
|
|
1071
|
-
const invite_res = await
|
|
1072
|
-
|
|
1079
|
+
const invite_res = await rpc_call({
|
|
1080
|
+
app: test_app.app,
|
|
1081
|
+
path: rpc_path,
|
|
1082
|
+
method: invite_create_action_spec.method,
|
|
1083
|
+
params: { email: test_email },
|
|
1073
1084
|
headers: admin_headers,
|
|
1074
|
-
body: JSON.stringify({ email: test_email }),
|
|
1075
1085
|
});
|
|
1076
|
-
assert.
|
|
1086
|
+
assert.ok(invite_res.ok, `invite_create failed: ${invite_res.ok ? '' : JSON.stringify(invite_res.error)}`);
|
|
1077
1087
|
// Attempt 1: signup with a non-matching email (no invite match) → 403
|
|
1078
1088
|
const no_match_res = await test_app.app.request(signup_route.path, {
|
|
1079
1089
|
method: 'POST',
|
|
@@ -1094,14 +1104,16 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
1094
1104
|
// then create an invite for a different email, then try signup with
|
|
1095
1105
|
// the invited email but the colliding username
|
|
1096
1106
|
const existing_user = await test_app.create_account({ username: 'existing_user' });
|
|
1097
|
-
// Create invite for a different email
|
|
1107
|
+
// Create invite for a different email via RPC
|
|
1098
1108
|
const conflict_email = 'conflict-test@example.com';
|
|
1099
|
-
const invite2_res = await
|
|
1100
|
-
|
|
1109
|
+
const invite2_res = await rpc_call({
|
|
1110
|
+
app: test_app.app,
|
|
1111
|
+
path: rpc_path,
|
|
1112
|
+
method: invite_create_action_spec.method,
|
|
1113
|
+
params: { email: conflict_email },
|
|
1101
1114
|
headers: admin_headers,
|
|
1102
|
-
body: JSON.stringify({ email: conflict_email }),
|
|
1103
1115
|
});
|
|
1104
|
-
assert.
|
|
1116
|
+
assert.ok(invite2_res.ok, `invite_create failed: ${invite2_res.ok ? '' : JSON.stringify(invite2_res.error)}`);
|
|
1105
1117
|
// Attempt 2: signup with the invited email but a colliding username → 409
|
|
1106
1118
|
const conflict_res = await test_app.app.request(signup_route.path, {
|
|
1107
1119
|
method: 'POST',
|