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