@fuzdev/fuz_app 0.63.0 → 0.65.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 +525 -827
- package/dist/actions/broadcast_api.d.ts +1 -1
- package/dist/actions/broadcast_api.js +1 -1
- package/dist/actions/cancel.d.ts +2 -2
- package/dist/actions/cancel.js +3 -3
- package/dist/actions/connection_closer.d.ts +65 -0
- package/dist/actions/connection_closer.d.ts.map +1 -0
- package/dist/actions/connection_closer.js +38 -0
- package/dist/actions/register_action_ws.d.ts +2 -2
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +23 -2
- package/dist/actions/register_ws_endpoint.d.ts +12 -10
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +5 -5
- package/dist/actions/transports_ws_auth_guard.d.ts +25 -10
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +24 -9
- package/dist/actions/ws_endpoint_spec.d.ts +119 -0
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -0
- package/dist/actions/ws_endpoint_spec.js +13 -0
- package/dist/auth/CLAUDE.md +592 -1808
- package/dist/auth/account_action_specs.d.ts +1 -1
- package/dist/auth/account_actions.d.ts +13 -0
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +31 -1
- package/dist/auth/account_routes.d.ts +12 -2
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +55 -8
- package/dist/auth/account_schema.d.ts +4 -4
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.d.ts +8 -8
- package/dist/auth/admin_actions.d.ts +11 -0
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +25 -0
- package/dist/auth/api_token_queries.js +1 -1
- package/dist/auth/audit_emitter.d.ts +56 -12
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +38 -12
- package/dist/auth/audit_log_ddl.d.ts +1 -1
- package/dist/auth/audit_log_ddl.d.ts.map +1 -1
- package/dist/auth/audit_log_ddl.js +1 -1
- package/dist/auth/audit_log_schema.d.ts +5 -3
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +5 -3
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +1 -5
- package/dist/auth/bootstrap_routes.d.ts +8 -2
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +15 -11
- package/dist/auth/invite_schema.d.ts +2 -2
- package/dist/auth/keyring.d.ts +6 -6
- package/dist/auth/keyring.js +8 -8
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_actions.js +4 -2
- package/dist/auth/signup_routes.d.ts +1 -1
- package/dist/auth/standard_rpc_actions.d.ts +1 -0
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +1 -0
- package/dist/db/create_db.d.ts.map +1 -1
- package/dist/db/create_db.js +13 -0
- package/dist/dev/setup.d.ts +2 -2
- package/dist/dev/setup.js +3 -3
- package/dist/http/CLAUDE.md +225 -483
- package/dist/http/error_schemas.d.ts +0 -4
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +0 -4
- package/dist/http/ip_canonical.d.ts +100 -0
- package/dist/http/ip_canonical.d.ts.map +1 -0
- package/dist/http/ip_canonical.js +195 -0
- package/dist/http/origin.d.ts +14 -6
- package/dist/http/origin.d.ts.map +1 -1
- package/dist/http/origin.js +14 -32
- package/dist/http/pending_effects.d.ts +1 -1
- package/dist/http/pending_effects.js +1 -1
- package/dist/http/proxy.d.ts +13 -5
- package/dist/http/proxy.d.ts.map +1 -1
- package/dist/http/proxy.js +15 -23
- package/dist/http/surface.d.ts +50 -0
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +27 -1
- package/dist/primitive_schemas.d.ts +20 -4
- package/dist/primitive_schemas.d.ts.map +1 -1
- package/dist/primitive_schemas.js +25 -4
- package/dist/realtime/sse_auth_guard.d.ts +16 -4
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +15 -3
- package/dist/runtime/mock.js +1 -1
- package/dist/server/app_backend.d.ts +66 -19
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +57 -34
- package/dist/server/app_server.d.ts +101 -10
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +105 -6
- package/dist/server/env.d.ts +7 -7
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +14 -14
- package/dist/server/startup.d.ts.map +1 -1
- package/dist/server/startup.js +12 -0
- package/dist/server/static.d.ts +4 -4
- package/dist/server/static.js +7 -7
- package/dist/testing/CLAUDE.md +269 -59
- package/dist/testing/admin_integration.d.ts +18 -23
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +159 -202
- package/dist/testing/adversarial_headers.d.ts +6 -0
- package/dist/testing/adversarial_headers.d.ts.map +1 -1
- package/dist/testing/adversarial_headers.js +13 -5
- package/dist/testing/app_server.d.ts +148 -60
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +143 -54
- package/dist/testing/attack_surface.d.ts +8 -7
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +12 -8
- package/dist/testing/audit_completeness.d.ts +23 -22
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +199 -158
- package/dist/testing/audit_drift_guard.d.ts +116 -0
- package/dist/testing/audit_drift_guard.d.ts.map +1 -0
- package/dist/testing/audit_drift_guard.js +134 -0
- package/dist/testing/bootstrap_success.d.ts +28 -0
- package/dist/testing/bootstrap_success.d.ts.map +1 -0
- package/dist/testing/bootstrap_success.js +144 -0
- package/dist/testing/connection_closer_helpers.d.ts +44 -0
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -0
- package/dist/testing/connection_closer_helpers.js +48 -0
- package/dist/testing/cross_backend/capabilities.d.ts +64 -0
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
- package/dist/testing/cross_backend/capabilities.js +47 -0
- package/dist/testing/cross_backend/setup.d.ts +215 -0
- package/dist/testing/cross_backend/setup.d.ts.map +1 -0
- package/dist/testing/cross_backend/setup.js +101 -0
- package/dist/testing/data_exposure.d.ts +14 -15
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +127 -146
- package/dist/testing/db_entities.d.ts +11 -1
- package/dist/testing/db_entities.d.ts.map +1 -1
- package/dist/testing/db_entities.js +13 -1
- package/dist/testing/integration.d.ts +35 -21
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +231 -293
- package/dist/testing/integration_helpers.d.ts +16 -6
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +7 -7
- package/dist/testing/mock_fs.d.ts.map +1 -1
- package/dist/testing/mock_fs.js +0 -2
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +13 -4
- package/dist/testing/role_grant_helpers.d.ts +31 -0
- package/dist/testing/role_grant_helpers.d.ts.map +1 -0
- package/dist/testing/role_grant_helpers.js +46 -0
- package/dist/testing/round_trip.d.ts +21 -16
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +65 -86
- package/dist/testing/rpc_helpers.d.ts +2 -1
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.d.ts +24 -21
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +91 -106
- package/dist/testing/schema_introspect.d.ts +106 -0
- package/dist/testing/schema_introspect.d.ts.map +1 -0
- package/dist/testing/schema_introspect.js +123 -0
- package/dist/testing/schema_parity.d.ts +144 -0
- package/dist/testing/schema_parity.d.ts.map +1 -0
- package/dist/testing/schema_parity.js +233 -0
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +12 -6
- package/dist/testing/standard.d.ts +57 -25
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +62 -5
- package/dist/testing/stubs.d.ts +22 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +28 -21
- package/dist/testing/surface_invariants.d.ts +66 -1
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +103 -1
- package/dist/testing/transports/surface_source.d.ts +51 -0
- package/dist/testing/transports/surface_source.d.ts.map +1 -0
- package/dist/testing/transports/surface_source.js +19 -0
- package/dist/ui/SurfaceExplorer.svelte +161 -2
- package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -16,35 +16,14 @@ import './assert_dev_env.js';
|
|
|
16
16
|
*
|
|
17
17
|
* @module
|
|
18
18
|
*/
|
|
19
|
-
import { describe, test, assert,
|
|
20
|
-
import { auth_migration_ns } from '../auth/migrations.js';
|
|
21
|
-
import { create_test_app } from './app_server.js';
|
|
22
|
-
import { create_pglite_factory, create_describe_db, auth_integration_truncate_tables, } from './db.js';
|
|
19
|
+
import { describe, test, assert, afterAll } from 'vitest';
|
|
23
20
|
import { find_auth_route, assert_response_matches_spec, create_expired_test_cookie, assert_no_error_info_leakage, } from './integration_helpers.js';
|
|
24
21
|
import { find_rpc_action, rpc_call_for_spec, require_rpc_endpoint_path, resolve_rpc_endpoints_for_setup, } from './rpc_helpers.js';
|
|
25
|
-
import { RateLimiter } from '../rate_limiter.js';
|
|
26
|
-
import { run_migrations } from '../db/migrate.js';
|
|
27
22
|
import { ErrorCoverageCollector, assert_error_coverage, DEFAULT_INTEGRATION_ERROR_COVERAGE, } from './error_coverage.js';
|
|
28
23
|
import { ApiError, ERROR_FORBIDDEN_ORIGIN } from '../http/error_schemas.js';
|
|
29
24
|
import { is_public_auth } from '../http/auth_shape.js';
|
|
30
25
|
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';
|
|
31
26
|
import { invite_create_action_spec } from '../auth/admin_action_specs.js';
|
|
32
|
-
/**
|
|
33
|
-
* Build `CreateTestAppOptions` from standard options plus a database.
|
|
34
|
-
* Forwards `options.rpc_endpoints` to `app_options.rpc_endpoints` so
|
|
35
|
-
* `create_app_server` auto-mounts it per-test with the real ctx.
|
|
36
|
-
* `SuiteAppOptions` excludes `rpc_endpoints` so there's no way for
|
|
37
|
-
* `options.app_options` to collide with the suite-level field.
|
|
38
|
-
*/
|
|
39
|
-
const build_test_app_options = (options, db) => ({
|
|
40
|
-
session_options: options.session_options,
|
|
41
|
-
create_route_specs: options.create_route_specs,
|
|
42
|
-
db,
|
|
43
|
-
app_options: {
|
|
44
|
-
...options.app_options,
|
|
45
|
-
rpc_endpoints: options.rpc_endpoints,
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
27
|
/**
|
|
49
28
|
* Standard integration test suite for fuz_app auth routes.
|
|
50
29
|
*
|
|
@@ -63,56 +42,52 @@ const build_test_app_options = (options, db) => ({
|
|
|
63
42
|
* `account_verify` / `account_session_*` / `account_token_*`.
|
|
64
43
|
*/
|
|
65
44
|
export const describe_standard_integration_tests = (options) => {
|
|
45
|
+
if (options.surface_source.kind !== 'inline') {
|
|
46
|
+
throw new Error("describe_standard_integration_tests requires surface_source.kind === 'inline' — " +
|
|
47
|
+
'the cross-process snapshot variant lands with the spawned-backend transport');
|
|
48
|
+
}
|
|
49
|
+
const route_specs = options.surface_source.spec.route_specs;
|
|
66
50
|
// Hard-fail early so consumers see a clear setup error instead of a
|
|
67
51
|
// confusing test failure when `rpc_endpoints` is missing. Factory-form
|
|
68
52
|
// callers are resolved with a stub ctx purely to extract the endpoint
|
|
69
|
-
// path;
|
|
53
|
+
// path; the running backend handles live dispatch.
|
|
70
54
|
const rpc_endpoints_for_setup = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
|
|
71
55
|
const rpc_path = require_rpc_endpoint_path(rpc_endpoints_for_setup);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
};
|
|
75
|
-
const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
|
|
76
|
-
const describe_db = create_describe_db(factories, auth_integration_truncate_tables);
|
|
77
|
-
describe_db('standard_integration', (get_db) => {
|
|
56
|
+
void options.capabilities;
|
|
57
|
+
describe('standard_integration', () => {
|
|
78
58
|
const { cookie_name } = options.session_options;
|
|
79
59
|
// Error coverage tracking across test groups
|
|
80
60
|
const error_collector = new ErrorCoverageCollector();
|
|
81
|
-
let captured_route_specs = null;
|
|
82
|
-
beforeAll(async () => {
|
|
83
|
-
// Capture route specs once up front so coverage assertion runs even if
|
|
84
|
-
// individual tests are skipped or fail early. Route specs are immutable
|
|
85
|
-
// config; the transient test app is discarded.
|
|
86
|
-
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
87
|
-
captured_route_specs = test_app.route_specs;
|
|
88
|
-
});
|
|
89
61
|
afterAll(() => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
62
|
+
// Scope coverage to auth routes this suite actually exercises:
|
|
63
|
+
// login / logout / password drivers + signup invite edge cases
|
|
64
|
+
// (when the consumer wires signup) + the shared RPC endpoint.
|
|
65
|
+
// Bootstrap (when the consumer wires it via the top-level `bootstrap`
|
|
66
|
+
// option in `mode: 'live'`) is intentionally excluded — this suite has
|
|
67
|
+
// no describe block that drives bootstrap; the dedicated
|
|
68
|
+
// `describe_bootstrap_success_tests` suite picks it up via
|
|
69
|
+
// `create_test_app_for_bootstrap`. Consumer-specific
|
|
70
|
+
// routes would dilute the ratio; admin-role routes are scoped to
|
|
71
|
+
// the admin suite instead.
|
|
72
|
+
const auth_routes = route_specs.filter((s) => {
|
|
73
|
+
if (s.auth.roles?.includes('admin') ?? false)
|
|
74
|
+
return false;
|
|
75
|
+
const rest_suffixes = ['/login', '/logout', '/password', '/signup'];
|
|
76
|
+
if (rest_suffixes.some((suffix) => s.path.endsWith(suffix)))
|
|
77
|
+
return true;
|
|
78
|
+
return s.path === rpc_path;
|
|
79
|
+
});
|
|
80
|
+
assert_error_coverage(error_collector, auth_routes.length > 0 ? auth_routes : route_specs, {
|
|
81
|
+
min_coverage: options.error_coverage_min ?? DEFAULT_INTEGRATION_ERROR_COVERAGE,
|
|
82
|
+
});
|
|
108
83
|
});
|
|
109
84
|
// --- 1. Login/logout lifecycle ---
|
|
110
85
|
describe('login/logout lifecycle', () => {
|
|
111
86
|
test('login with correct credentials returns 200 with Set-Cookie', async () => {
|
|
112
|
-
const
|
|
113
|
-
const login_route = find_auth_route(
|
|
87
|
+
const fixture = await options.setup_test();
|
|
88
|
+
const login_route = find_auth_route(route_specs, '/login', 'POST');
|
|
114
89
|
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
115
|
-
const res = await
|
|
90
|
+
const res = await fixture.transport(login_route.path, {
|
|
116
91
|
method: 'POST',
|
|
117
92
|
headers: {
|
|
118
93
|
host: 'localhost',
|
|
@@ -120,7 +95,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
120
95
|
'content-type': 'application/json',
|
|
121
96
|
},
|
|
122
97
|
body: JSON.stringify({
|
|
123
|
-
username:
|
|
98
|
+
username: fixture.account.username,
|
|
124
99
|
password: 'test-password-123',
|
|
125
100
|
}),
|
|
126
101
|
});
|
|
@@ -132,10 +107,10 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
132
107
|
assert.ok(set_cookie.includes(`${cookie_name}=`), `Expected ${cookie_name} cookie`);
|
|
133
108
|
});
|
|
134
109
|
test('login with wrong password returns 401', async () => {
|
|
135
|
-
const
|
|
136
|
-
const login_route = find_auth_route(
|
|
110
|
+
const fixture = await options.setup_test();
|
|
111
|
+
const login_route = find_auth_route(route_specs, '/login', 'POST');
|
|
137
112
|
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
138
|
-
const res = await
|
|
113
|
+
const res = await fixture.transport(login_route.path, {
|
|
139
114
|
method: 'POST',
|
|
140
115
|
headers: {
|
|
141
116
|
host: 'localhost',
|
|
@@ -143,20 +118,20 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
143
118
|
'content-type': 'application/json',
|
|
144
119
|
},
|
|
145
120
|
body: JSON.stringify({
|
|
146
|
-
username:
|
|
121
|
+
username: fixture.account.username,
|
|
147
122
|
password: 'wrong-password',
|
|
148
123
|
}),
|
|
149
124
|
});
|
|
150
125
|
assert.strictEqual(res.status, 401);
|
|
151
126
|
const body = await res.clone().json();
|
|
152
127
|
assert.strictEqual(body.error, 'invalid_credentials');
|
|
153
|
-
await error_collector.assert_and_record(
|
|
128
|
+
await error_collector.assert_and_record(route_specs, 'POST', login_route.path, res);
|
|
154
129
|
});
|
|
155
130
|
test('login with nonexistent user returns 401', async () => {
|
|
156
|
-
const
|
|
157
|
-
const login_route = find_auth_route(
|
|
131
|
+
const fixture = await options.setup_test();
|
|
132
|
+
const login_route = find_auth_route(route_specs, '/login', 'POST');
|
|
158
133
|
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
159
|
-
const res = await
|
|
134
|
+
const res = await fixture.transport(login_route.path, {
|
|
160
135
|
method: 'POST',
|
|
161
136
|
headers: {
|
|
162
137
|
host: 'localhost',
|
|
@@ -171,13 +146,13 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
171
146
|
assert.strictEqual(res.status, 401);
|
|
172
147
|
const body = await res.clone().json();
|
|
173
148
|
assert.strictEqual(body.error, 'invalid_credentials');
|
|
174
|
-
await error_collector.assert_and_record(
|
|
149
|
+
await error_collector.assert_and_record(route_specs, 'POST', login_route.path, res);
|
|
175
150
|
});
|
|
176
151
|
test('login trims whitespace from username', async () => {
|
|
177
|
-
const
|
|
178
|
-
const login_route = find_auth_route(
|
|
152
|
+
const fixture = await options.setup_test();
|
|
153
|
+
const login_route = find_auth_route(route_specs, '/login', 'POST');
|
|
179
154
|
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
180
|
-
const res = await
|
|
155
|
+
const res = await fixture.transport(login_route.path, {
|
|
181
156
|
method: 'POST',
|
|
182
157
|
headers: {
|
|
183
158
|
host: 'localhost',
|
|
@@ -185,7 +160,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
185
160
|
'content-type': 'application/json',
|
|
186
161
|
},
|
|
187
162
|
body: JSON.stringify({
|
|
188
|
-
username: ` ${
|
|
163
|
+
username: ` ${fixture.account.username} `,
|
|
189
164
|
password: 'test-password-123',
|
|
190
165
|
}),
|
|
191
166
|
});
|
|
@@ -194,13 +169,13 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
194
169
|
assert.strictEqual(body.ok, true);
|
|
195
170
|
});
|
|
196
171
|
test('full cycle: login → verify → logout → verify fails', async () => {
|
|
197
|
-
const
|
|
198
|
-
const login_route = find_auth_route(
|
|
199
|
-
const logout_route = find_auth_route(
|
|
172
|
+
const fixture = await options.setup_test();
|
|
173
|
+
const login_route = find_auth_route(route_specs, '/login', 'POST');
|
|
174
|
+
const logout_route = find_auth_route(route_specs, '/logout', 'POST');
|
|
200
175
|
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
201
176
|
assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
|
|
202
177
|
// Login
|
|
203
|
-
const login_res = await
|
|
178
|
+
const login_res = await fixture.transport(login_route.path, {
|
|
204
179
|
method: 'POST',
|
|
205
180
|
headers: {
|
|
206
181
|
host: 'localhost',
|
|
@@ -208,7 +183,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
208
183
|
'content-type': 'application/json',
|
|
209
184
|
},
|
|
210
185
|
body: JSON.stringify({
|
|
211
|
-
username:
|
|
186
|
+
username: fixture.account.username,
|
|
212
187
|
password: 'test-password-123',
|
|
213
188
|
}),
|
|
214
189
|
});
|
|
@@ -226,7 +201,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
226
201
|
});
|
|
227
202
|
// Verify works
|
|
228
203
|
const verify_res = await rpc_call_for_spec({
|
|
229
|
-
app:
|
|
204
|
+
app: { request: fixture.transport },
|
|
230
205
|
path: rpc_path,
|
|
231
206
|
spec: account_verify_action_spec,
|
|
232
207
|
params: undefined,
|
|
@@ -234,17 +209,17 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
234
209
|
});
|
|
235
210
|
assert.strictEqual(verify_res.status, 200);
|
|
236
211
|
// Logout
|
|
237
|
-
const logout_res = await
|
|
212
|
+
const logout_res = await fixture.transport(logout_route.path, {
|
|
238
213
|
method: 'POST',
|
|
239
214
|
headers: create_headers(),
|
|
240
215
|
});
|
|
241
216
|
assert.strictEqual(logout_res.status, 200);
|
|
242
217
|
const logout_body = await logout_res.json();
|
|
243
218
|
assert.strictEqual(logout_body.ok, true);
|
|
244
|
-
assert.strictEqual(logout_body.username,
|
|
219
|
+
assert.strictEqual(logout_body.username, fixture.account.username, 'Logout response should include the username');
|
|
245
220
|
// Verify fails after logout (session revoked)
|
|
246
221
|
const verify_after = await rpc_call_for_spec({
|
|
247
|
-
app:
|
|
222
|
+
app: { request: fixture.transport },
|
|
248
223
|
path: rpc_path,
|
|
249
224
|
spec: account_verify_action_spec,
|
|
250
225
|
params: undefined,
|
|
@@ -256,10 +231,10 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
256
231
|
// --- 1b. Login response body identity (account enumeration prevention) ---
|
|
257
232
|
describe('login response body identity', () => {
|
|
258
233
|
test('nonexistent user and wrong password responses are structurally identical', async () => {
|
|
259
|
-
const
|
|
260
|
-
const login_route = find_auth_route(
|
|
234
|
+
const fixture = await options.setup_test();
|
|
235
|
+
const login_route = find_auth_route(route_specs, '/login', 'POST');
|
|
261
236
|
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
262
|
-
const make_login = (username, password) =>
|
|
237
|
+
const make_login = (username, password) => fixture.transport(login_route.path, {
|
|
263
238
|
method: 'POST',
|
|
264
239
|
headers: {
|
|
265
240
|
host: 'localhost',
|
|
@@ -269,7 +244,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
269
244
|
body: JSON.stringify({ username, password }),
|
|
270
245
|
});
|
|
271
246
|
// wrong password for existing user
|
|
272
|
-
const wrong_pw_res = await make_login(
|
|
247
|
+
const wrong_pw_res = await make_login(fixture.account.username, 'wrong-password-999');
|
|
273
248
|
assert.strictEqual(wrong_pw_res.status, 401);
|
|
274
249
|
const wrong_pw_body = await wrong_pw_res.json();
|
|
275
250
|
// nonexistent user
|
|
@@ -286,10 +261,10 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
286
261
|
// --- 2. Cookie attributes ---
|
|
287
262
|
describe('cookie attributes', () => {
|
|
288
263
|
test('session cookie has secure attributes', async () => {
|
|
289
|
-
const
|
|
290
|
-
const login_route = find_auth_route(
|
|
264
|
+
const fixture = await options.setup_test();
|
|
265
|
+
const login_route = find_auth_route(route_specs, '/login', 'POST');
|
|
291
266
|
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
292
|
-
const res = await
|
|
267
|
+
const res = await fixture.transport(login_route.path, {
|
|
293
268
|
method: 'POST',
|
|
294
269
|
headers: {
|
|
295
270
|
host: 'localhost',
|
|
@@ -297,7 +272,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
297
272
|
'content-type': 'application/json',
|
|
298
273
|
},
|
|
299
274
|
body: JSON.stringify({
|
|
300
|
-
username:
|
|
275
|
+
username: fixture.account.username,
|
|
301
276
|
password: 'test-password-123',
|
|
302
277
|
}),
|
|
303
278
|
});
|
|
@@ -314,9 +289,9 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
314
289
|
// --- 3. Session security ---
|
|
315
290
|
describe('session security', () => {
|
|
316
291
|
test('no cookie on protected route returns 401', async () => {
|
|
317
|
-
const
|
|
292
|
+
const fixture = await options.setup_test();
|
|
318
293
|
const res = await rpc_call_for_spec({
|
|
319
|
-
app:
|
|
294
|
+
app: { request: fixture.transport },
|
|
320
295
|
path: rpc_path,
|
|
321
296
|
spec: account_verify_action_spec,
|
|
322
297
|
params: undefined,
|
|
@@ -325,9 +300,9 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
325
300
|
assert.strictEqual(res.status, 401);
|
|
326
301
|
});
|
|
327
302
|
test('corrupted cookie returns 401', async () => {
|
|
328
|
-
const
|
|
303
|
+
const fixture = await options.setup_test();
|
|
329
304
|
const res = await rpc_call_for_spec({
|
|
330
|
-
app:
|
|
305
|
+
app: { request: fixture.transport },
|
|
331
306
|
path: rpc_path,
|
|
332
307
|
spec: account_verify_action_spec,
|
|
333
308
|
params: undefined,
|
|
@@ -336,10 +311,11 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
336
311
|
assert.strictEqual(res.status, 401);
|
|
337
312
|
});
|
|
338
313
|
test('expired cookie returns 401', async () => {
|
|
339
|
-
const
|
|
340
|
-
|
|
314
|
+
const fixture = await options.setup_test();
|
|
315
|
+
assert(fixture.in_process, 'expired-cookie generation requires in-process keyring');
|
|
316
|
+
const expired_cookie = await create_expired_test_cookie(fixture.keyring, options.session_options);
|
|
341
317
|
const res = await rpc_call_for_spec({
|
|
342
|
-
app:
|
|
318
|
+
app: { request: fixture.transport },
|
|
343
319
|
path: rpc_path,
|
|
344
320
|
spec: account_verify_action_spec,
|
|
345
321
|
params: undefined,
|
|
@@ -351,11 +327,11 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
351
327
|
// --- 4. Session revocation ---
|
|
352
328
|
describe('session revocation', () => {
|
|
353
329
|
test('revoke single session by ID invalidates that session', async () => {
|
|
354
|
-
const
|
|
355
|
-
const headers =
|
|
330
|
+
const fixture = await options.setup_test();
|
|
331
|
+
const headers = fixture.create_session_headers();
|
|
356
332
|
// List own sessions to get the session ID
|
|
357
333
|
const list_res = await rpc_call_for_spec({
|
|
358
|
-
app:
|
|
334
|
+
app: { request: fixture.transport },
|
|
359
335
|
path: rpc_path,
|
|
360
336
|
spec: account_session_list_action_spec,
|
|
361
337
|
params: undefined,
|
|
@@ -366,7 +342,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
366
342
|
const session_id = list_res.result.sessions[0].id;
|
|
367
343
|
// Revoke that session by ID
|
|
368
344
|
const revoke_res = await rpc_call_for_spec({
|
|
369
|
-
app:
|
|
345
|
+
app: { request: fixture.transport },
|
|
370
346
|
path: rpc_path,
|
|
371
347
|
spec: account_session_revoke_action_spec,
|
|
372
348
|
params: { session_id },
|
|
@@ -377,7 +353,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
377
353
|
assert.strictEqual(revoke_res.result.revoked, true);
|
|
378
354
|
// Session should no longer work
|
|
379
355
|
const after = await rpc_call_for_spec({
|
|
380
|
-
app:
|
|
356
|
+
app: { request: fixture.transport },
|
|
381
357
|
path: rpc_path,
|
|
382
358
|
spec: account_verify_action_spec,
|
|
383
359
|
params: undefined,
|
|
@@ -386,11 +362,11 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
386
362
|
assert.strictEqual(after.status, 401);
|
|
387
363
|
});
|
|
388
364
|
test('revoke-all invalidates existing session', async () => {
|
|
389
|
-
const
|
|
390
|
-
const headers =
|
|
365
|
+
const fixture = await options.setup_test();
|
|
366
|
+
const headers = fixture.create_session_headers();
|
|
391
367
|
// Verify works
|
|
392
368
|
const before = await rpc_call_for_spec({
|
|
393
|
-
app:
|
|
369
|
+
app: { request: fixture.transport },
|
|
394
370
|
path: rpc_path,
|
|
395
371
|
spec: account_verify_action_spec,
|
|
396
372
|
params: undefined,
|
|
@@ -399,7 +375,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
399
375
|
assert.strictEqual(before.status, 200);
|
|
400
376
|
// Revoke all sessions
|
|
401
377
|
const revoke_res = await rpc_call_for_spec({
|
|
402
|
-
app:
|
|
378
|
+
app: { request: fixture.transport },
|
|
403
379
|
path: rpc_path,
|
|
404
380
|
spec: account_session_revoke_all_action_spec,
|
|
405
381
|
params: undefined,
|
|
@@ -408,7 +384,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
408
384
|
assert.ok(revoke_res.ok, 'account_session_revoke_all should succeed');
|
|
409
385
|
// Verify fails after revocation
|
|
410
386
|
const after = await rpc_call_for_spec({
|
|
411
|
-
app:
|
|
387
|
+
app: { request: fixture.transport },
|
|
412
388
|
path: rpc_path,
|
|
413
389
|
spec: account_verify_action_spec,
|
|
414
390
|
params: undefined,
|
|
@@ -420,16 +396,16 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
420
396
|
// --- 4b. Password change ---
|
|
421
397
|
describe('password change', () => {
|
|
422
398
|
test('password change invalidates all sessions and allows login with new password', async () => {
|
|
423
|
-
const
|
|
424
|
-
const login_route = find_auth_route(
|
|
425
|
-
const password_route = find_auth_route(
|
|
399
|
+
const fixture = await options.setup_test();
|
|
400
|
+
const login_route = find_auth_route(route_specs, '/login', 'POST');
|
|
401
|
+
const password_route = find_auth_route(route_specs, '/password', 'POST');
|
|
426
402
|
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
427
403
|
assert.ok(password_route, 'Expected POST /password route — ensure create_route_specs includes account routes');
|
|
428
|
-
const headers =
|
|
404
|
+
const headers = fixture.create_session_headers({
|
|
429
405
|
'content-type': 'application/json',
|
|
430
406
|
});
|
|
431
407
|
// Change password
|
|
432
|
-
const change_res = await
|
|
408
|
+
const change_res = await fixture.transport(password_route.path, {
|
|
433
409
|
method: 'POST',
|
|
434
410
|
headers,
|
|
435
411
|
body: JSON.stringify({
|
|
@@ -444,15 +420,15 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
444
420
|
assert.ok(change_body.sessions_revoked >= 1, 'Expected at least 1 session revoked');
|
|
445
421
|
// Old session should be invalid
|
|
446
422
|
const verify_after = await rpc_call_for_spec({
|
|
447
|
-
app:
|
|
423
|
+
app: { request: fixture.transport },
|
|
448
424
|
path: rpc_path,
|
|
449
425
|
spec: account_verify_action_spec,
|
|
450
426
|
params: undefined,
|
|
451
|
-
headers:
|
|
427
|
+
headers: fixture.create_session_headers(),
|
|
452
428
|
});
|
|
453
429
|
assert.strictEqual(verify_after.status, 401);
|
|
454
430
|
// Login with new password works
|
|
455
|
-
const login_res = await
|
|
431
|
+
const login_res = await fixture.transport(login_route.path, {
|
|
456
432
|
method: 'POST',
|
|
457
433
|
headers: {
|
|
458
434
|
host: 'localhost',
|
|
@@ -460,7 +436,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
460
436
|
'content-type': 'application/json',
|
|
461
437
|
},
|
|
462
438
|
body: JSON.stringify({
|
|
463
|
-
username:
|
|
439
|
+
username: fixture.account.username,
|
|
464
440
|
password: 'new-password-456',
|
|
465
441
|
}),
|
|
466
442
|
});
|
|
@@ -469,12 +445,12 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
469
445
|
assert.strictEqual(login_body.ok, true);
|
|
470
446
|
});
|
|
471
447
|
test('password change with wrong current password returns 401', async () => {
|
|
472
|
-
const
|
|
473
|
-
const password_route = find_auth_route(
|
|
448
|
+
const fixture = await options.setup_test();
|
|
449
|
+
const password_route = find_auth_route(route_specs, '/password', 'POST');
|
|
474
450
|
assert.ok(password_route, 'Expected POST /password route — ensure create_route_specs includes account routes');
|
|
475
|
-
const res = await
|
|
451
|
+
const res = await fixture.transport(password_route.path, {
|
|
476
452
|
method: 'POST',
|
|
477
|
-
headers:
|
|
453
|
+
headers: fixture.create_session_headers({
|
|
478
454
|
'content-type': 'application/json',
|
|
479
455
|
}),
|
|
480
456
|
body: JSON.stringify({
|
|
@@ -483,14 +459,14 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
483
459
|
}),
|
|
484
460
|
});
|
|
485
461
|
assert.strictEqual(res.status, 401);
|
|
486
|
-
error_collector.record(
|
|
462
|
+
error_collector.record(route_specs, 'POST', password_route.path, 401);
|
|
487
463
|
// Session should still be valid (password didn't change)
|
|
488
464
|
const verify_res = await rpc_call_for_spec({
|
|
489
|
-
app:
|
|
465
|
+
app: { request: fixture.transport },
|
|
490
466
|
path: rpc_path,
|
|
491
467
|
spec: account_verify_action_spec,
|
|
492
468
|
params: undefined,
|
|
493
|
-
headers:
|
|
469
|
+
headers: fixture.create_session_headers(),
|
|
494
470
|
});
|
|
495
471
|
assert.strictEqual(verify_res.status, 200);
|
|
496
472
|
});
|
|
@@ -498,16 +474,17 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
498
474
|
// --- 5. Origin verification ---
|
|
499
475
|
describe('origin verification', () => {
|
|
500
476
|
test('evil origin is rejected with 403', async () => {
|
|
501
|
-
const
|
|
477
|
+
const fixture = await options.setup_test();
|
|
478
|
+
assert(fixture.in_process, 'manual cookie composition requires backend_internals');
|
|
502
479
|
// `verify_request_source` runs before the RPC dispatcher and returns a
|
|
503
480
|
// plain REST `{error}` body — not a JSON-RPC envelope. Skip `rpc_call`.
|
|
504
|
-
const res = await
|
|
481
|
+
const res = await fixture.transport(rpc_path, {
|
|
505
482
|
method: 'POST',
|
|
506
483
|
headers: {
|
|
507
484
|
host: 'localhost',
|
|
508
485
|
origin: 'http://evil.com',
|
|
509
486
|
'content-type': 'application/json',
|
|
510
|
-
cookie: `${cookie_name}=${
|
|
487
|
+
cookie: `${cookie_name}=${fixture.backend_internals.session_cookie}`,
|
|
511
488
|
},
|
|
512
489
|
body: JSON.stringify({
|
|
513
490
|
jsonrpc: '2.0',
|
|
@@ -520,26 +497,27 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
520
497
|
assert.strictEqual(body.error, ERROR_FORBIDDEN_ORIGIN);
|
|
521
498
|
});
|
|
522
499
|
test('valid origin is accepted', async () => {
|
|
523
|
-
const
|
|
500
|
+
const fixture = await options.setup_test();
|
|
524
501
|
const res = await rpc_call_for_spec({
|
|
525
|
-
app:
|
|
502
|
+
app: { request: fixture.transport },
|
|
526
503
|
path: rpc_path,
|
|
527
504
|
spec: account_verify_action_spec,
|
|
528
505
|
params: undefined,
|
|
529
|
-
headers:
|
|
506
|
+
headers: fixture.create_session_headers(),
|
|
530
507
|
});
|
|
531
508
|
assert.strictEqual(res.status, 200);
|
|
532
509
|
});
|
|
533
510
|
test('no origin header is allowed (direct access)', async () => {
|
|
534
|
-
const
|
|
511
|
+
const fixture = await options.setup_test();
|
|
512
|
+
assert(fixture.in_process, 'manual cookie composition requires backend_internals');
|
|
535
513
|
// Probe the "no Origin / no Referer" path; `suppress_default_origin`
|
|
536
514
|
// skips the default `origin` header.
|
|
537
515
|
const res = await rpc_call_for_spec({
|
|
538
|
-
app:
|
|
516
|
+
app: { request: fixture.transport },
|
|
539
517
|
path: rpc_path,
|
|
540
518
|
spec: account_verify_action_spec,
|
|
541
519
|
params: undefined,
|
|
542
|
-
headers: { cookie: `${cookie_name}=${
|
|
520
|
+
headers: { cookie: `${cookie_name}=${fixture.backend_internals.session_cookie}` },
|
|
543
521
|
suppress_default_origin: true,
|
|
544
522
|
});
|
|
545
523
|
assert.notStrictEqual(res.status, 403);
|
|
@@ -548,21 +526,21 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
548
526
|
// --- 6. Bearer auth ---
|
|
549
527
|
describe('bearer auth', () => {
|
|
550
528
|
test('valid bearer token authenticates', async () => {
|
|
551
|
-
const
|
|
529
|
+
const fixture = await options.setup_test();
|
|
552
530
|
const res = await rpc_call_for_spec({
|
|
553
|
-
app:
|
|
531
|
+
app: { request: fixture.transport },
|
|
554
532
|
path: rpc_path,
|
|
555
533
|
spec: account_verify_action_spec,
|
|
556
534
|
params: undefined,
|
|
557
|
-
headers:
|
|
535
|
+
headers: fixture.create_bearer_headers(),
|
|
558
536
|
suppress_default_origin: true,
|
|
559
537
|
});
|
|
560
538
|
assert.strictEqual(res.status, 200);
|
|
561
539
|
});
|
|
562
540
|
test('invalid bearer token returns 401', async () => {
|
|
563
|
-
const
|
|
541
|
+
const fixture = await options.setup_test();
|
|
564
542
|
const res = await rpc_call_for_spec({
|
|
565
|
-
app:
|
|
543
|
+
app: { request: fixture.transport },
|
|
566
544
|
path: rpc_path,
|
|
567
545
|
spec: account_verify_action_spec,
|
|
568
546
|
params: undefined,
|
|
@@ -572,11 +550,11 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
572
550
|
assert.strictEqual(res.status, 401);
|
|
573
551
|
});
|
|
574
552
|
test('bearer token with Origin header is rejected', async () => {
|
|
575
|
-
const
|
|
576
|
-
const bearer_headers =
|
|
553
|
+
const fixture = await options.setup_test();
|
|
554
|
+
const bearer_headers = fixture.create_bearer_headers();
|
|
577
555
|
// Without Origin — works.
|
|
578
556
|
const ok_res = await rpc_call_for_spec({
|
|
579
|
-
app:
|
|
557
|
+
app: { request: fixture.transport },
|
|
580
558
|
path: rpc_path,
|
|
581
559
|
spec: account_verify_action_spec,
|
|
582
560
|
params: undefined,
|
|
@@ -586,7 +564,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
586
564
|
assert.strictEqual(ok_res.status, 200);
|
|
587
565
|
// With Origin — bearer silently discarded (browser context), falls through to no auth.
|
|
588
566
|
const res = await rpc_call_for_spec({
|
|
589
|
-
app:
|
|
567
|
+
app: { request: fixture.transport },
|
|
590
568
|
path: rpc_path,
|
|
591
569
|
spec: account_verify_action_spec,
|
|
592
570
|
params: undefined,
|
|
@@ -598,20 +576,20 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
598
576
|
// --- 7. Token revocation ---
|
|
599
577
|
describe('token revocation', () => {
|
|
600
578
|
test('revoked API token returns 401', async () => {
|
|
601
|
-
const
|
|
579
|
+
const fixture = await options.setup_test();
|
|
602
580
|
// Create a new token via RPC
|
|
603
581
|
const create_res = await rpc_call_for_spec({
|
|
604
|
-
app:
|
|
582
|
+
app: { request: fixture.transport },
|
|
605
583
|
path: rpc_path,
|
|
606
584
|
spec: account_token_create_action_spec,
|
|
607
585
|
params: { name: 'test-revoke' },
|
|
608
|
-
headers:
|
|
586
|
+
headers: fixture.create_session_headers(),
|
|
609
587
|
});
|
|
610
588
|
assert.ok(create_res.ok, 'account_token_create should succeed');
|
|
611
589
|
const { token, id } = create_res.result;
|
|
612
590
|
// Verify token works
|
|
613
591
|
const use_res = await rpc_call_for_spec({
|
|
614
|
-
app:
|
|
592
|
+
app: { request: fixture.transport },
|
|
615
593
|
path: rpc_path,
|
|
616
594
|
spec: account_verify_action_spec,
|
|
617
595
|
params: undefined,
|
|
@@ -621,16 +599,16 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
621
599
|
assert.strictEqual(use_res.status, 200);
|
|
622
600
|
// Revoke via RPC
|
|
623
601
|
const revoke_res = await rpc_call_for_spec({
|
|
624
|
-
app:
|
|
602
|
+
app: { request: fixture.transport },
|
|
625
603
|
path: rpc_path,
|
|
626
604
|
spec: account_token_revoke_action_spec,
|
|
627
605
|
params: { token_id: id },
|
|
628
|
-
headers:
|
|
606
|
+
headers: fixture.create_session_headers(),
|
|
629
607
|
});
|
|
630
608
|
assert.ok(revoke_res.ok, 'account_token_revoke should succeed');
|
|
631
609
|
// Token should no longer work
|
|
632
610
|
const after_res = await rpc_call_for_spec({
|
|
633
|
-
app:
|
|
611
|
+
app: { request: fixture.transport },
|
|
634
612
|
path: rpc_path,
|
|
635
613
|
spec: account_verify_action_spec,
|
|
636
614
|
params: undefined,
|
|
@@ -643,36 +621,36 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
643
621
|
// --- 8. Cross-account isolation ---
|
|
644
622
|
describe('cross-account isolation', () => {
|
|
645
623
|
test('non-admin cannot access admin routes', async () => {
|
|
646
|
-
const
|
|
624
|
+
const fixture = await options.setup_test();
|
|
647
625
|
// admin routes are optional in the base suite — admin-specific coverage
|
|
648
626
|
// lives in describe_standard_admin_integration_tests
|
|
649
|
-
const admin_route =
|
|
627
|
+
const admin_route = route_specs.find((s) => s.auth.roles?.includes('admin') ?? false);
|
|
650
628
|
if (!admin_route)
|
|
651
629
|
return;
|
|
652
|
-
const res = await
|
|
630
|
+
const res = await fixture.transport(admin_route.path, {
|
|
653
631
|
method: admin_route.method,
|
|
654
|
-
headers:
|
|
632
|
+
headers: fixture.create_session_headers(),
|
|
655
633
|
});
|
|
656
634
|
assert.strictEqual(res.status, 403);
|
|
657
635
|
const body = await res.json();
|
|
658
636
|
assert.strictEqual(body.error, 'insufficient_permissions');
|
|
659
637
|
});
|
|
660
638
|
test("user A cannot revoke user B's sessions", async () => {
|
|
661
|
-
const
|
|
639
|
+
const fixture = await options.setup_test();
|
|
662
640
|
// Create a second account
|
|
663
|
-
const user_b = await
|
|
641
|
+
const user_b = await fixture.create_account({ username: 'user_b' });
|
|
664
642
|
// User A revokes all their own sessions
|
|
665
643
|
const revoke_res = await rpc_call_for_spec({
|
|
666
|
-
app:
|
|
644
|
+
app: { request: fixture.transport },
|
|
667
645
|
path: rpc_path,
|
|
668
646
|
spec: account_session_revoke_all_action_spec,
|
|
669
647
|
params: undefined,
|
|
670
|
-
headers:
|
|
648
|
+
headers: fixture.create_session_headers(),
|
|
671
649
|
});
|
|
672
650
|
assert.ok(revoke_res.ok, 'account_session_revoke_all should succeed');
|
|
673
651
|
// User B's session should still work
|
|
674
652
|
const verify_b = await rpc_call_for_spec({
|
|
675
|
-
app:
|
|
653
|
+
app: { request: fixture.transport },
|
|
676
654
|
path: rpc_path,
|
|
677
655
|
spec: account_verify_action_spec,
|
|
678
656
|
params: undefined,
|
|
@@ -681,12 +659,12 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
681
659
|
assert.strictEqual(verify_b.status, 200);
|
|
682
660
|
});
|
|
683
661
|
test("user A cannot revoke user B's session by ID", async () => {
|
|
684
|
-
const
|
|
685
|
-
const user_b = await
|
|
662
|
+
const fixture = await options.setup_test();
|
|
663
|
+
const user_b = await fixture.create_account({ username: 'user_b' });
|
|
686
664
|
const user_b_headers = { cookie: `${cookie_name}=${user_b.session_cookie}` };
|
|
687
665
|
// Get user B's session ID by listing as user B
|
|
688
666
|
const list_res = await rpc_call_for_spec({
|
|
689
|
-
app:
|
|
667
|
+
app: { request: fixture.transport },
|
|
690
668
|
path: rpc_path,
|
|
691
669
|
spec: account_session_list_action_spec,
|
|
692
670
|
params: undefined,
|
|
@@ -697,17 +675,17 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
697
675
|
const session_id_b = list_res.result.sessions[0].id;
|
|
698
676
|
// User A tries to revoke user B's session by ID
|
|
699
677
|
const revoke_res = await rpc_call_for_spec({
|
|
700
|
-
app:
|
|
678
|
+
app: { request: fixture.transport },
|
|
701
679
|
path: rpc_path,
|
|
702
680
|
spec: account_session_revoke_action_spec,
|
|
703
681
|
params: { session_id: session_id_b },
|
|
704
|
-
headers:
|
|
682
|
+
headers: fixture.create_session_headers(),
|
|
705
683
|
});
|
|
706
684
|
assert.ok(revoke_res.ok, 'account_session_revoke should succeed');
|
|
707
685
|
assert.strictEqual(revoke_res.result.revoked, false, 'Should not revoke another account session');
|
|
708
686
|
// User B's session should still work
|
|
709
687
|
const verify_b = await rpc_call_for_spec({
|
|
710
|
-
app:
|
|
688
|
+
app: { request: fixture.transport },
|
|
711
689
|
path: rpc_path,
|
|
712
690
|
spec: account_verify_action_spec,
|
|
713
691
|
params: undefined,
|
|
@@ -716,12 +694,12 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
716
694
|
assert.strictEqual(verify_b.status, 200);
|
|
717
695
|
});
|
|
718
696
|
test("user A cannot revoke user B's token by ID", async () => {
|
|
719
|
-
const
|
|
720
|
-
const user_b = await
|
|
697
|
+
const fixture = await options.setup_test();
|
|
698
|
+
const user_b = await fixture.create_account({ username: 'user_b' });
|
|
721
699
|
const user_b_headers = { cookie: `${cookie_name}=${user_b.session_cookie}` };
|
|
722
700
|
// Get user B's token ID by listing as user B
|
|
723
701
|
const list_res = await rpc_call_for_spec({
|
|
724
|
-
app:
|
|
702
|
+
app: { request: fixture.transport },
|
|
725
703
|
path: rpc_path,
|
|
726
704
|
spec: account_token_list_action_spec,
|
|
727
705
|
params: undefined,
|
|
@@ -732,17 +710,17 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
732
710
|
const token_id_b = list_res.result.tokens[0].id;
|
|
733
711
|
// User A tries to revoke user B's token by ID
|
|
734
712
|
const revoke_res = await rpc_call_for_spec({
|
|
735
|
-
app:
|
|
713
|
+
app: { request: fixture.transport },
|
|
736
714
|
path: rpc_path,
|
|
737
715
|
spec: account_token_revoke_action_spec,
|
|
738
716
|
params: { token_id: token_id_b },
|
|
739
|
-
headers:
|
|
717
|
+
headers: fixture.create_session_headers(),
|
|
740
718
|
});
|
|
741
719
|
assert.ok(revoke_res.ok, 'account_token_revoke should succeed');
|
|
742
720
|
assert.strictEqual(revoke_res.result.revoked, false, 'Should not revoke another account token');
|
|
743
721
|
// User B's bearer token should still work
|
|
744
722
|
const verify_b = await rpc_call_for_spec({
|
|
745
|
-
app:
|
|
723
|
+
app: { request: fixture.transport },
|
|
746
724
|
path: rpc_path,
|
|
747
725
|
spec: account_verify_action_spec,
|
|
748
726
|
params: undefined,
|
|
@@ -752,37 +730,37 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
752
730
|
assert.strictEqual(verify_b.status, 200);
|
|
753
731
|
});
|
|
754
732
|
test("user A's session list does not include user B's sessions", async () => {
|
|
755
|
-
const
|
|
756
|
-
const user_b = await
|
|
733
|
+
const fixture = await options.setup_test();
|
|
734
|
+
const user_b = await fixture.create_account({ username: 'user_b' });
|
|
757
735
|
// User A lists sessions
|
|
758
736
|
const res = await rpc_call_for_spec({
|
|
759
|
-
app:
|
|
737
|
+
app: { request: fixture.transport },
|
|
760
738
|
path: rpc_path,
|
|
761
739
|
spec: account_session_list_action_spec,
|
|
762
740
|
params: undefined,
|
|
763
|
-
headers:
|
|
741
|
+
headers: fixture.create_session_headers(),
|
|
764
742
|
});
|
|
765
743
|
assert.ok(res.ok, 'account_session_list should succeed');
|
|
766
744
|
// Sessions should only belong to user A's account
|
|
767
745
|
for (const session of res.result.sessions) {
|
|
768
|
-
assert.strictEqual(session.account_id,
|
|
746
|
+
assert.strictEqual(session.account_id, fixture.account.id, `Session ${session.id} should belong to user A, not user B (${user_b.account.id})`);
|
|
769
747
|
}
|
|
770
748
|
});
|
|
771
749
|
test("user A's token list does not include user B's tokens", async () => {
|
|
772
|
-
const
|
|
773
|
-
const user_b = await
|
|
750
|
+
const fixture = await options.setup_test();
|
|
751
|
+
const user_b = await fixture.create_account({ username: 'user_b' });
|
|
774
752
|
// User A lists tokens
|
|
775
753
|
const res = await rpc_call_for_spec({
|
|
776
|
-
app:
|
|
754
|
+
app: { request: fixture.transport },
|
|
777
755
|
path: rpc_path,
|
|
778
756
|
spec: account_token_list_action_spec,
|
|
779
757
|
params: undefined,
|
|
780
|
-
headers:
|
|
758
|
+
headers: fixture.create_session_headers(),
|
|
781
759
|
});
|
|
782
760
|
assert.ok(res.ok, 'account_token_list should succeed');
|
|
783
761
|
// Tokens should only belong to user A's account
|
|
784
762
|
for (const token of res.result.tokens) {
|
|
785
|
-
assert.strictEqual(token.account_id,
|
|
763
|
+
assert.strictEqual(token.account_id, fixture.account.id, `Token ${token.id} should belong to user A, not user B (${user_b.account.id})`);
|
|
786
764
|
}
|
|
787
765
|
});
|
|
788
766
|
});
|
|
@@ -793,10 +771,10 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
793
771
|
// /password remain as REST routes whose responses we exercise here.
|
|
794
772
|
// RPC output validation is covered by `describe_rpc_round_trip_tests`.
|
|
795
773
|
test('POST /login 401 response matches declared error schema', async () => {
|
|
796
|
-
const
|
|
797
|
-
const login_route = find_auth_route(
|
|
774
|
+
const fixture = await options.setup_test();
|
|
775
|
+
const login_route = find_auth_route(route_specs, '/login', 'POST');
|
|
798
776
|
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
799
|
-
const res = await
|
|
777
|
+
const res = await fixture.transport(login_route.path, {
|
|
800
778
|
method: 'POST',
|
|
801
779
|
headers: {
|
|
802
780
|
host: 'localhost',
|
|
@@ -809,25 +787,25 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
809
787
|
}),
|
|
810
788
|
});
|
|
811
789
|
assert.strictEqual(res.status, 401);
|
|
812
|
-
await assert_response_matches_spec(
|
|
790
|
+
await assert_response_matches_spec(route_specs, 'POST', login_route.path, res);
|
|
813
791
|
});
|
|
814
792
|
test('POST /logout 200 response matches output schema', async () => {
|
|
815
|
-
const
|
|
816
|
-
const logout_route = find_auth_route(
|
|
793
|
+
const fixture = await options.setup_test();
|
|
794
|
+
const logout_route = find_auth_route(route_specs, '/logout', 'POST');
|
|
817
795
|
assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
|
|
818
|
-
const res = await
|
|
796
|
+
const res = await fixture.transport(logout_route.path, {
|
|
819
797
|
method: 'POST',
|
|
820
|
-
headers:
|
|
798
|
+
headers: fixture.create_session_headers({ 'content-type': 'application/json' }),
|
|
821
799
|
body: JSON.stringify({}),
|
|
822
800
|
});
|
|
823
801
|
assert.strictEqual(res.status, 200);
|
|
824
|
-
await assert_response_matches_spec(
|
|
802
|
+
await assert_response_matches_spec(route_specs, 'POST', logout_route.path, res);
|
|
825
803
|
});
|
|
826
804
|
test('POST /logout 401 response matches declared error schema', async () => {
|
|
827
|
-
const
|
|
828
|
-
const logout_route = find_auth_route(
|
|
805
|
+
const fixture = await options.setup_test();
|
|
806
|
+
const logout_route = find_auth_route(route_specs, '/logout', 'POST');
|
|
829
807
|
assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
|
|
830
|
-
const res = await
|
|
808
|
+
const res = await fixture.transport(logout_route.path, {
|
|
831
809
|
method: 'POST',
|
|
832
810
|
headers: {
|
|
833
811
|
host: 'localhost',
|
|
@@ -837,129 +815,87 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
837
815
|
body: JSON.stringify({}),
|
|
838
816
|
});
|
|
839
817
|
assert.strictEqual(res.status, 401);
|
|
840
|
-
await assert_response_matches_spec(
|
|
841
|
-
});
|
|
842
|
-
});
|
|
843
|
-
// --- 10b. Rate limiting smoke test (full middleware stack) ---
|
|
844
|
-
describe('rate limiting smoke test', () => {
|
|
845
|
-
test('rate limiter fires in full middleware stack', async () => {
|
|
846
|
-
const test_app = await create_test_app({
|
|
847
|
-
...build_test_app_options(options, get_db()),
|
|
848
|
-
app_options: {
|
|
849
|
-
...options.app_options,
|
|
850
|
-
// tight limiter: 2 attempts / 1 minute
|
|
851
|
-
ip_rate_limiter: new RateLimiter({
|
|
852
|
-
max_attempts: 2,
|
|
853
|
-
window_ms: 60_000,
|
|
854
|
-
cleanup_interval_ms: 0,
|
|
855
|
-
}),
|
|
856
|
-
},
|
|
857
|
-
});
|
|
858
|
-
const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
|
|
859
|
-
if (!login_route)
|
|
860
|
-
return; // skip if login route not wired
|
|
861
|
-
const make_bad_login = (ip_header) => {
|
|
862
|
-
const headers = {
|
|
863
|
-
host: 'localhost',
|
|
864
|
-
origin: 'http://localhost:5173',
|
|
865
|
-
'content-type': 'application/json',
|
|
866
|
-
};
|
|
867
|
-
if (ip_header) {
|
|
868
|
-
headers['x-forwarded-for'] = ip_header;
|
|
869
|
-
}
|
|
870
|
-
return test_app.app.request(login_route.path, {
|
|
871
|
-
method: 'POST',
|
|
872
|
-
headers,
|
|
873
|
-
body: JSON.stringify({ username: 'nobody', password: 'wrong' }),
|
|
874
|
-
});
|
|
875
|
-
};
|
|
876
|
-
// exhaust the limiter (2 attempts)
|
|
877
|
-
await make_bad_login();
|
|
878
|
-
await make_bad_login();
|
|
879
|
-
// third attempt should be rate-limited
|
|
880
|
-
const limited_res = await make_bad_login();
|
|
881
|
-
assert.strictEqual(limited_res.status, 429, 'Expected 429 after exceeding rate limit');
|
|
882
|
-
const limited_body = await limited_res.clone().json();
|
|
883
|
-
assert.strictEqual(limited_body.error, 'rate_limit_exceeded');
|
|
884
|
-
await error_collector.assert_and_record(test_app.route_specs, 'POST', login_route.path, limited_res);
|
|
885
|
-
// Retry-After header present
|
|
886
|
-
const retry_after = limited_res.headers.get('Retry-After');
|
|
887
|
-
assert.ok(retry_after, 'Expected Retry-After header on 429 response');
|
|
818
|
+
await assert_response_matches_spec(route_specs, 'POST', logout_route.path, res);
|
|
888
819
|
});
|
|
889
820
|
});
|
|
821
|
+
// Rate-limit behavior is covered end-to-end by
|
|
822
|
+
// `describe_rate_limiting_tests` against the full middleware stack.
|
|
823
|
+
// A per-suite smoke test isn't reintroduced here because the
|
|
824
|
+
// `setup_test` single-fixture model can't carry per-test rate-limiter
|
|
825
|
+
// overrides without each test re-constructing its own `TestApp`.
|
|
890
826
|
// --- 10c2. Error coverage: unauthenticated access to auth-required routes ---
|
|
891
827
|
describe('error coverage breadth', () => {
|
|
892
828
|
test('exercises 401 on multiple auth-required routes for error coverage', async () => {
|
|
893
|
-
const
|
|
829
|
+
const fixture = await options.setup_test();
|
|
894
830
|
// Hit several auth-required RPC methods without credentials to
|
|
895
831
|
// broaden error coverage beyond just /login. RPC 401s are tracked
|
|
896
832
|
// against the shared endpoint path. The dispatcher runs auth before
|
|
897
833
|
// params validation, so any well-formed param body works — we just
|
|
898
834
|
// need each call to be type-correct wrt its spec.
|
|
899
835
|
const session_list = await rpc_call_for_spec({
|
|
900
|
-
app:
|
|
836
|
+
app: { request: fixture.transport },
|
|
901
837
|
path: rpc_path,
|
|
902
838
|
spec: account_session_list_action_spec,
|
|
903
839
|
params: undefined,
|
|
904
840
|
headers: { host: 'localhost' },
|
|
905
841
|
});
|
|
906
842
|
assert.strictEqual(session_list.status, 401);
|
|
907
|
-
error_collector.record(
|
|
843
|
+
error_collector.record(route_specs, 'POST', rpc_path, 401);
|
|
908
844
|
const session_revoke_all = await rpc_call_for_spec({
|
|
909
|
-
app:
|
|
845
|
+
app: { request: fixture.transport },
|
|
910
846
|
path: rpc_path,
|
|
911
847
|
spec: account_session_revoke_all_action_spec,
|
|
912
848
|
params: undefined,
|
|
913
849
|
headers: { host: 'localhost' },
|
|
914
850
|
});
|
|
915
851
|
assert.strictEqual(session_revoke_all.status, 401);
|
|
916
|
-
error_collector.record(
|
|
852
|
+
error_collector.record(route_specs, 'POST', rpc_path, 401);
|
|
917
853
|
const token_list = await rpc_call_for_spec({
|
|
918
|
-
app:
|
|
854
|
+
app: { request: fixture.transport },
|
|
919
855
|
path: rpc_path,
|
|
920
856
|
spec: account_token_list_action_spec,
|
|
921
857
|
params: undefined,
|
|
922
858
|
headers: { host: 'localhost' },
|
|
923
859
|
});
|
|
924
860
|
assert.strictEqual(token_list.status, 401);
|
|
925
|
-
error_collector.record(
|
|
861
|
+
error_collector.record(route_specs, 'POST', rpc_path, 401);
|
|
926
862
|
const token_create = await rpc_call_for_spec({
|
|
927
|
-
app:
|
|
863
|
+
app: { request: fixture.transport },
|
|
928
864
|
path: rpc_path,
|
|
929
865
|
spec: account_token_create_action_spec,
|
|
930
866
|
params: { name: 'unauth-probe' },
|
|
931
867
|
headers: { host: 'localhost' },
|
|
932
868
|
});
|
|
933
869
|
assert.strictEqual(token_create.status, 401);
|
|
934
|
-
error_collector.record(
|
|
870
|
+
error_collector.record(route_specs, 'POST', rpc_path, 401);
|
|
935
871
|
const verify = await rpc_call_for_spec({
|
|
936
|
-
app:
|
|
872
|
+
app: { request: fixture.transport },
|
|
937
873
|
path: rpc_path,
|
|
938
874
|
spec: account_verify_action_spec,
|
|
939
875
|
params: undefined,
|
|
940
876
|
headers: { host: 'localhost' },
|
|
941
877
|
});
|
|
942
878
|
assert.strictEqual(verify.status, 401);
|
|
943
|
-
error_collector.record(
|
|
879
|
+
error_collector.record(route_specs, 'POST', rpc_path, 401);
|
|
944
880
|
// Also exercise POST /logout without auth (still REST)
|
|
945
|
-
const logout_route = find_auth_route(
|
|
881
|
+
const logout_route = find_auth_route(route_specs, '/logout', 'POST');
|
|
946
882
|
if (logout_route) {
|
|
947
|
-
const res = await
|
|
883
|
+
const res = await fixture.transport(logout_route.path, {
|
|
948
884
|
method: 'POST',
|
|
949
885
|
headers: { host: 'localhost', 'content-type': 'application/json' },
|
|
950
886
|
body: JSON.stringify({}),
|
|
951
887
|
});
|
|
952
888
|
assert.strictEqual(res.status, 401, 'POST /logout without auth should return 401');
|
|
953
|
-
error_collector.record(
|
|
889
|
+
error_collector.record(route_specs, 'POST', logout_route.path, 401);
|
|
954
890
|
}
|
|
955
891
|
});
|
|
956
892
|
});
|
|
957
893
|
// --- 10c. Error response information leakage ---
|
|
958
894
|
describe('error response information leakage', () => {
|
|
959
895
|
test('401 responses contain no leaky fields', async () => {
|
|
960
|
-
const
|
|
896
|
+
const fixture = await options.setup_test();
|
|
961
897
|
const res = await rpc_call_for_spec({
|
|
962
|
-
app:
|
|
898
|
+
app: { request: fixture.transport },
|
|
963
899
|
path: rpc_path,
|
|
964
900
|
spec: account_verify_action_spec,
|
|
965
901
|
params: undefined,
|
|
@@ -976,10 +912,11 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
976
912
|
// --- 11. Expired credential rejection ---
|
|
977
913
|
describe('expired credential rejection', () => {
|
|
978
914
|
test('expired session cookie returns 401', async () => {
|
|
979
|
-
const
|
|
980
|
-
|
|
915
|
+
const fixture = await options.setup_test();
|
|
916
|
+
assert(fixture.in_process, 'expired-cookie generation requires in-process keyring');
|
|
917
|
+
const expired_cookie = await create_expired_test_cookie(fixture.keyring, options.session_options);
|
|
981
918
|
const res = await rpc_call_for_spec({
|
|
982
|
-
app:
|
|
919
|
+
app: { request: fixture.transport },
|
|
983
920
|
path: rpc_path,
|
|
984
921
|
spec: account_verify_action_spec,
|
|
985
922
|
params: undefined,
|
|
@@ -988,11 +925,12 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
988
925
|
assert.strictEqual(res.status, 401, 'Expired session cookie should be rejected');
|
|
989
926
|
});
|
|
990
927
|
test('expired session cookie returns 401 on mutation route', async () => {
|
|
991
|
-
const
|
|
992
|
-
|
|
928
|
+
const fixture = await options.setup_test();
|
|
929
|
+
assert(fixture.in_process, 'expired-cookie generation requires in-process keyring');
|
|
930
|
+
const logout_route = find_auth_route(route_specs, '/logout', 'POST');
|
|
993
931
|
assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
|
|
994
|
-
const expired_cookie = await create_expired_test_cookie(
|
|
995
|
-
const res = await
|
|
932
|
+
const expired_cookie = await create_expired_test_cookie(fixture.keyring, options.session_options);
|
|
933
|
+
const res = await fixture.transport(logout_route.path, {
|
|
996
934
|
method: 'POST',
|
|
997
935
|
headers: {
|
|
998
936
|
host: 'localhost',
|
|
@@ -1000,60 +938,60 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
1000
938
|
},
|
|
1001
939
|
});
|
|
1002
940
|
assert.strictEqual(res.status, 401, 'Expired session cookie should be rejected on POST');
|
|
1003
|
-
error_collector.record(
|
|
941
|
+
error_collector.record(route_specs, 'POST', logout_route.path, 401);
|
|
1004
942
|
});
|
|
1005
943
|
});
|
|
1006
944
|
// --- 12. Bearer token browser context on mutation routes ---
|
|
1007
945
|
describe('bearer token browser context silently discarded on mutations', () => {
|
|
1008
946
|
test('bearer token with Origin header discarded on POST logout', async () => {
|
|
1009
|
-
const
|
|
1010
|
-
const logout_route = find_auth_route(
|
|
947
|
+
const fixture = await options.setup_test();
|
|
948
|
+
const logout_route = find_auth_route(route_specs, '/logout', 'POST');
|
|
1011
949
|
assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
|
|
1012
|
-
const bearer_headers =
|
|
950
|
+
const bearer_headers = fixture.create_bearer_headers({
|
|
1013
951
|
'content-type': 'application/json',
|
|
1014
952
|
});
|
|
1015
|
-
const res = await
|
|
953
|
+
const res = await fixture.transport(logout_route.path, {
|
|
1016
954
|
method: 'POST',
|
|
1017
955
|
headers: { ...bearer_headers, origin: 'http://localhost:5173' },
|
|
1018
956
|
});
|
|
1019
957
|
assert.strictEqual(res.status, 401, 'Bearer with Origin should be discarded → unauthenticated');
|
|
1020
|
-
error_collector.record(
|
|
958
|
+
error_collector.record(route_specs, 'POST', logout_route.path, 401);
|
|
1021
959
|
});
|
|
1022
960
|
test('bearer token with Referer header discarded on POST password', async () => {
|
|
1023
|
-
const
|
|
1024
|
-
const password_route = find_auth_route(
|
|
961
|
+
const fixture = await options.setup_test();
|
|
962
|
+
const password_route = find_auth_route(route_specs, '/password', 'POST');
|
|
1025
963
|
assert.ok(password_route, 'Expected POST /password route — ensure create_route_specs includes account routes');
|
|
1026
|
-
const bearer_headers =
|
|
964
|
+
const bearer_headers = fixture.create_bearer_headers({
|
|
1027
965
|
'content-type': 'application/json',
|
|
1028
966
|
});
|
|
1029
|
-
const res = await
|
|
967
|
+
const res = await fixture.transport(password_route.path, {
|
|
1030
968
|
method: 'POST',
|
|
1031
969
|
headers: { ...bearer_headers, referer: 'http://localhost:5173/admin' },
|
|
1032
970
|
});
|
|
1033
971
|
assert.strictEqual(res.status, 401, 'Bearer with Referer should be discarded → unauthenticated');
|
|
1034
|
-
error_collector.record(
|
|
972
|
+
error_collector.record(route_specs, 'POST', password_route.path, 401);
|
|
1035
973
|
});
|
|
1036
974
|
});
|
|
1037
975
|
// --- 13. Password change revokes API tokens ---
|
|
1038
976
|
describe('password change revokes API tokens', () => {
|
|
1039
977
|
test('API tokens are invalidated after password change', async () => {
|
|
1040
|
-
const
|
|
1041
|
-
const password_route = find_auth_route(
|
|
978
|
+
const fixture = await options.setup_test();
|
|
979
|
+
const password_route = find_auth_route(route_specs, '/password', 'POST');
|
|
1042
980
|
assert.ok(password_route, 'Expected POST /password route');
|
|
1043
981
|
// Create an API token via RPC
|
|
1044
982
|
const create_res = await rpc_call_for_spec({
|
|
1045
|
-
app:
|
|
983
|
+
app: { request: fixture.transport },
|
|
1046
984
|
path: rpc_path,
|
|
1047
985
|
spec: account_token_create_action_spec,
|
|
1048
986
|
params: { name: 'test-token' },
|
|
1049
|
-
headers:
|
|
987
|
+
headers: fixture.create_session_headers(),
|
|
1050
988
|
});
|
|
1051
989
|
assert.ok(create_res.ok, 'account_token_create should succeed');
|
|
1052
990
|
const { token: raw_token } = create_res.result;
|
|
1053
991
|
assert.ok(raw_token, 'Expected raw token in create response');
|
|
1054
992
|
// Verify bearer token works
|
|
1055
993
|
const verify_before = await rpc_call_for_spec({
|
|
1056
|
-
app:
|
|
994
|
+
app: { request: fixture.transport },
|
|
1057
995
|
path: rpc_path,
|
|
1058
996
|
spec: account_verify_action_spec,
|
|
1059
997
|
params: undefined,
|
|
@@ -1062,9 +1000,9 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
1062
1000
|
});
|
|
1063
1001
|
assert.strictEqual(verify_before.status, 200, 'Bearer token should work before password change');
|
|
1064
1002
|
// Change password (still REST)
|
|
1065
|
-
const change_res = await
|
|
1003
|
+
const change_res = await fixture.transport(password_route.path, {
|
|
1066
1004
|
method: 'POST',
|
|
1067
|
-
headers:
|
|
1005
|
+
headers: fixture.create_session_headers({ 'content-type': 'application/json' }),
|
|
1068
1006
|
body: JSON.stringify({
|
|
1069
1007
|
current_password: 'test-password-123',
|
|
1070
1008
|
new_password: 'new-password-456',
|
|
@@ -1076,7 +1014,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
1076
1014
|
assert.ok(change_body.tokens_revoked >= 1, 'Expected at least 1 token revoked');
|
|
1077
1015
|
// Bearer token should now be invalid
|
|
1078
1016
|
const verify_after = await rpc_call_for_spec({
|
|
1079
|
-
app:
|
|
1017
|
+
app: { request: fixture.transport },
|
|
1080
1018
|
path: rpc_path,
|
|
1081
1019
|
spec: account_verify_action_spec,
|
|
1082
1020
|
params: undefined,
|
|
@@ -1089,8 +1027,8 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
1089
1027
|
// --- 14. Signup invite edge cases ---
|
|
1090
1028
|
describe('signup invite edge cases', () => {
|
|
1091
1029
|
test('signup with non-matching email cannot claim another email invite', async () => {
|
|
1092
|
-
const
|
|
1093
|
-
const signup_route =
|
|
1030
|
+
const fixture = await options.setup_test();
|
|
1031
|
+
const signup_route = route_specs.find((s) => s.method === 'POST' && s.path.endsWith('/signup') && is_public_auth(s.auth));
|
|
1094
1032
|
if (!signup_route)
|
|
1095
1033
|
return; // signup is optional
|
|
1096
1034
|
// `invite_create` lives on the RPC surface; consumers that don't
|
|
@@ -1099,13 +1037,13 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
1099
1037
|
if (!find_rpc_action(rpc_endpoints_for_setup, invite_create_action_spec.method))
|
|
1100
1038
|
return;
|
|
1101
1039
|
// Create an admin to manage invites
|
|
1102
|
-
const admin = await
|
|
1040
|
+
const admin = await fixture.create_account({
|
|
1103
1041
|
username: 'invite_edge_admin',
|
|
1104
1042
|
roles: ['admin'],
|
|
1105
1043
|
});
|
|
1106
1044
|
// Create invite for alice@example.com via RPC
|
|
1107
1045
|
const invite_res = await rpc_call_for_spec({
|
|
1108
|
-
app:
|
|
1046
|
+
app: { request: fixture.transport },
|
|
1109
1047
|
path: rpc_path,
|
|
1110
1048
|
spec: invite_create_action_spec,
|
|
1111
1049
|
params: { email: 'alice@example.com' },
|
|
@@ -1113,7 +1051,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
1113
1051
|
});
|
|
1114
1052
|
assert.ok(invite_res.ok, `invite_create failed: ${invite_res.ok ? '' : JSON.stringify(invite_res.error)}`);
|
|
1115
1053
|
// Try to sign up with a different email — should fail (no matching invite)
|
|
1116
|
-
const signup_res = await
|
|
1054
|
+
const signup_res = await fixture.transport(signup_route.path, {
|
|
1117
1055
|
method: 'POST',
|
|
1118
1056
|
headers: {
|
|
1119
1057
|
host: 'localhost',
|
|
@@ -1134,9 +1072,9 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
1134
1072
|
// --- 15. Signup response body identity ---
|
|
1135
1073
|
describe('signup response body identity', () => {
|
|
1136
1074
|
test('no-invite and conflict failure responses are structurally identical', async () => {
|
|
1137
|
-
const
|
|
1075
|
+
const fixture = await options.setup_test();
|
|
1138
1076
|
// Find signup route (POST ending in /signup, public)
|
|
1139
|
-
const signup_route =
|
|
1077
|
+
const signup_route = route_specs.find((s) => s.method === 'POST' && s.path.endsWith('/signup') && is_public_auth(s.auth));
|
|
1140
1078
|
if (!signup_route)
|
|
1141
1079
|
return; // signup is optional
|
|
1142
1080
|
// `invite_create` lives on the RPC surface; consumers that don't
|
|
@@ -1144,7 +1082,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
1144
1082
|
if (!find_rpc_action(rpc_endpoints_for_setup, invite_create_action_spec.method))
|
|
1145
1083
|
return;
|
|
1146
1084
|
// We need admin access — create an admin account
|
|
1147
|
-
const admin = await
|
|
1085
|
+
const admin = await fixture.create_account({
|
|
1148
1086
|
username: 'signup_test_admin',
|
|
1149
1087
|
roles: ['admin'],
|
|
1150
1088
|
});
|
|
@@ -1152,7 +1090,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
1152
1090
|
// Create an invite for a specific test email via RPC
|
|
1153
1091
|
const test_email = 'signup-test@example.com';
|
|
1154
1092
|
const invite_res = await rpc_call_for_spec({
|
|
1155
|
-
app:
|
|
1093
|
+
app: { request: fixture.transport },
|
|
1156
1094
|
path: rpc_path,
|
|
1157
1095
|
spec: invite_create_action_spec,
|
|
1158
1096
|
params: { email: test_email },
|
|
@@ -1160,7 +1098,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
1160
1098
|
});
|
|
1161
1099
|
assert.ok(invite_res.ok, `invite_create failed: ${invite_res.ok ? '' : JSON.stringify(invite_res.error)}`);
|
|
1162
1100
|
// Attempt 1: signup with a non-matching email (no invite match) → 403
|
|
1163
|
-
const no_match_res = await
|
|
1101
|
+
const no_match_res = await fixture.transport(signup_route.path, {
|
|
1164
1102
|
method: 'POST',
|
|
1165
1103
|
headers: {
|
|
1166
1104
|
host: 'localhost',
|
|
@@ -1178,11 +1116,11 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
1178
1116
|
// For conflict test: create a second account with a known username,
|
|
1179
1117
|
// then create an invite for a different email, then try signup with
|
|
1180
1118
|
// the invited email but the colliding username
|
|
1181
|
-
const existing_user = await
|
|
1119
|
+
const existing_user = await fixture.create_account({ username: 'existing_user' });
|
|
1182
1120
|
// Create invite for a different email via RPC
|
|
1183
1121
|
const conflict_email = 'conflict-test@example.com';
|
|
1184
1122
|
const invite2_res = await rpc_call_for_spec({
|
|
1185
|
-
app:
|
|
1123
|
+
app: { request: fixture.transport },
|
|
1186
1124
|
path: rpc_path,
|
|
1187
1125
|
spec: invite_create_action_spec,
|
|
1188
1126
|
params: { email: conflict_email },
|
|
@@ -1190,7 +1128,7 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
1190
1128
|
});
|
|
1191
1129
|
assert.ok(invite2_res.ok, `invite2_create failed: ${invite2_res.ok ? '' : JSON.stringify(invite2_res.error)}`);
|
|
1192
1130
|
// Attempt 2: signup with the invited email but a colliding username → 409
|
|
1193
|
-
const conflict_res = await
|
|
1131
|
+
const conflict_res = await fixture.transport(signup_route.path, {
|
|
1194
1132
|
method: 'POST',
|
|
1195
1133
|
headers: {
|
|
1196
1134
|
host: 'localhost',
|