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