@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
|
@@ -26,20 +26,16 @@ import './assert_dev_env.js';
|
|
|
26
26
|
* @module
|
|
27
27
|
*/
|
|
28
28
|
import { describe, test, assert, afterAll } from 'vitest';
|
|
29
|
-
import {
|
|
29
|
+
import { ROLE_ADMIN } from '../auth/role_schema.js';
|
|
30
30
|
import { GRANT_PATH_ADMIN } from '../auth/grant_path_schema.js';
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import { create_pglite_factory, create_describe_db, auth_integration_truncate_tables, } from './db.js';
|
|
31
|
+
import { DEFAULT_TEST_PASSWORD } from './app_server.js';
|
|
32
|
+
import { role_grant_offer_and_accept } from './role_grant_helpers.js';
|
|
34
33
|
import { find_auth_route } from './integration_helpers.js';
|
|
35
|
-
import { run_migrations } from '../db/migrate.js';
|
|
36
34
|
import { ErrorCoverageCollector, assert_error_coverage, DEFAULT_INTEGRATION_ERROR_COVERAGE, } from './error_coverage.js';
|
|
37
35
|
import { rpc_call_for_spec, require_rpc_endpoint_path, resolve_rpc_endpoints_for_setup, } from './rpc_helpers.js';
|
|
38
36
|
import { role_grant_offer_create_action_spec, role_grant_revoke_action_spec, } from '../auth/role_grant_offer_action_specs.js';
|
|
39
|
-
import { admin_account_list_action_spec, admin_session_list_action_spec, admin_session_revoke_all_action_spec, admin_token_revoke_all_action_spec, audit_log_list_action_spec, audit_log_role_grant_history_action_spec, } from '../auth/admin_action_specs.js';
|
|
37
|
+
import { admin_account_list_action_spec, ADMIN_ACCOUNT_LIST_LIMIT_MAX, admin_session_list_action_spec, admin_session_revoke_all_action_spec, admin_token_revoke_all_action_spec, audit_log_list_action_spec, audit_log_role_grant_history_action_spec, } from '../auth/admin_action_specs.js';
|
|
40
38
|
import { account_token_create_action_spec, account_verify_action_spec, } from '../auth/account_action_specs.js';
|
|
41
|
-
import { query_create_role_grant } from '../auth/role_grant_queries.js';
|
|
42
|
-
import { query_accept_offer } from '../auth/role_grant_offer_queries.js';
|
|
43
39
|
/**
|
|
44
40
|
* Pick a role for admin-grant testing, preferring a non-admin app-defined
|
|
45
41
|
* role whose `RoleSpec.grant_paths` includes `'admin'` (the
|
|
@@ -53,17 +49,6 @@ const pick_grantable_role = (role_specs) => {
|
|
|
53
49
|
}
|
|
54
50
|
return ROLE_ADMIN; // fallback
|
|
55
51
|
};
|
|
56
|
-
/**
|
|
57
|
-
* Build `CreateTestAppOptions` from admin test options plus a database and roles.
|
|
58
|
-
*/
|
|
59
|
-
const build_admin_test_app_options = (options, db, roles) => ({
|
|
60
|
-
session_options: options.session_options,
|
|
61
|
-
create_route_specs: options.create_route_specs,
|
|
62
|
-
db,
|
|
63
|
-
roles: roles ?? [ROLE_KEEPER, ROLE_ADMIN],
|
|
64
|
-
rpc_endpoints: options.rpc_endpoints,
|
|
65
|
-
app_options: options.app_options,
|
|
66
|
-
});
|
|
67
52
|
/**
|
|
68
53
|
* Standard admin integration test suite for fuz_app admin routes.
|
|
69
54
|
*
|
|
@@ -79,56 +64,40 @@ const build_admin_test_app_options = (options, db, roles) => ({
|
|
|
79
64
|
* see a clear setup error rather than `method not found` mid-suite.
|
|
80
65
|
*/
|
|
81
66
|
export const describe_standard_admin_integration_tests = (options) => {
|
|
67
|
+
const route_specs = options.surface_source.route_specs;
|
|
82
68
|
// Hard-fail early so consumers see a clear setup error instead of a
|
|
83
|
-
// confusing test failure when `rpc_endpoints` is missing.
|
|
84
|
-
// callers are resolved with a stub ctx purely to extract the endpoint
|
|
85
|
-
// path; real handlers run per-test via the top-level `rpc_endpoints`
|
|
86
|
-
// slot on `CreateTestAppOptions`.
|
|
69
|
+
// confusing test failure when `rpc_endpoints` is missing.
|
|
87
70
|
const rpc_endpoints_for_setup = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
|
|
88
71
|
const rpc_path = require_rpc_endpoint_path(rpc_endpoints_for_setup);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
};
|
|
92
|
-
const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
|
|
93
|
-
const describe_db = create_describe_db(factories, auth_integration_truncate_tables);
|
|
94
|
-
describe_db('standard_admin_integration', (get_db) => {
|
|
72
|
+
void options.capabilities;
|
|
73
|
+
describe('standard_admin_integration', () => {
|
|
95
74
|
const { cookie_name } = options.session_options;
|
|
96
75
|
const { role_specs } = options.roles;
|
|
97
76
|
const grantable_role = pick_grantable_role(role_specs);
|
|
98
77
|
// Error coverage tracking across test groups
|
|
99
78
|
const error_collector = new ErrorCoverageCollector();
|
|
100
|
-
let captured_route_specs = null;
|
|
101
79
|
afterAll(() => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// of asserting.
|
|
120
|
-
// The admin RPC surface is covered by
|
|
121
|
-
// `describe_rpc_round_trip_tests`, not this collector.
|
|
122
|
-
if (admin_routes.length <= 1) {
|
|
123
|
-
console.log(`[error coverage] skipped admin REST coverage assertion — ` +
|
|
124
|
-
`scoped surface has ${admin_routes.length} route(s); ` +
|
|
125
|
-
`admin RPC surface is covered by describe_rpc_round_trip_tests`);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
assert_error_coverage(error_collector, admin_routes, {
|
|
129
|
-
min_coverage: DEFAULT_INTEGRATION_ERROR_COVERAGE,
|
|
130
|
-
});
|
|
80
|
+
// Scope coverage to admin auth-related routes. Account listing,
|
|
81
|
+
// session/token revoke-all, audit-log reads, and invite CRUD all
|
|
82
|
+
// live on the RPC surface; the only admin REST route remaining
|
|
83
|
+
// is the optional `GET /audit/stream` SSE (admin RPC methods
|
|
84
|
+
// live behind spec-level role auth on the shared endpoint path).
|
|
85
|
+
const admin_routes = route_specs.filter((s) => s.path.endsWith('/audit/stream') && (s.auth.roles?.includes('admin') ?? false));
|
|
86
|
+
// Adaptive threshold: when the scoped admin REST surface is
|
|
87
|
+
// effectively empty (0–1 routes — typical for the RPC-first
|
|
88
|
+
// admin surface), the 20% baseline is meaningless — a single
|
|
89
|
+
// SSE route that can't be exercised against an error schema
|
|
90
|
+
// drops the ratio to 0.0%. Log an informational skip instead
|
|
91
|
+
// of asserting.
|
|
92
|
+
if (admin_routes.length <= 1) {
|
|
93
|
+
console.log(`[error coverage] skipped admin REST coverage assertion — ` +
|
|
94
|
+
`scoped surface has ${admin_routes.length} route(s); ` +
|
|
95
|
+
`admin RPC surface is covered by describe_rpc_round_trip_tests`);
|
|
96
|
+
return;
|
|
131
97
|
}
|
|
98
|
+
assert_error_coverage(error_collector, admin_routes, {
|
|
99
|
+
min_coverage: DEFAULT_INTEGRATION_ERROR_COVERAGE,
|
|
100
|
+
});
|
|
132
101
|
});
|
|
133
102
|
/** Make request headers for a given session cookie. */
|
|
134
103
|
const create_headers = (session_cookie, extra) => ({
|
|
@@ -138,41 +107,30 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
138
107
|
...extra,
|
|
139
108
|
});
|
|
140
109
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* side; exercising the recipient's UI-wired accept path is covered by
|
|
145
|
-
* `describe_rpc_round_trip_tests` + fuz_app's own action suite.
|
|
110
|
+
* Suite-local wrapper around `role_grant_offer_and_accept` that closes
|
|
111
|
+
* over `rpc_path` so call sites stay terse. See the shared helper for
|
|
112
|
+
* the cross-suite contract.
|
|
146
113
|
*/
|
|
147
|
-
const offer_and_accept =
|
|
148
|
-
const res = await rpc_call_for_spec({
|
|
149
|
-
app: args.app,
|
|
150
|
-
path: rpc_path,
|
|
151
|
-
spec: role_grant_offer_create_action_spec,
|
|
152
|
-
params: { to_account_id: args.to_account_id, role: args.role },
|
|
153
|
-
headers: args.admin_headers,
|
|
154
|
-
});
|
|
155
|
-
assert.ok(res.ok, `role_grant_offer_create failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
156
|
-
const { offer } = res.result;
|
|
157
|
-
const accept_result = await get_db().transaction(async (tx) => query_accept_offer({ db: tx }, {
|
|
158
|
-
offer_id: offer.id,
|
|
159
|
-
to_account_id: args.to_account_id,
|
|
160
|
-
actor_id: args.to_actor_id,
|
|
161
|
-
ip: null,
|
|
162
|
-
}));
|
|
163
|
-
return { offer_id: offer.id, role_grant_id: accept_result.role_grant.id };
|
|
164
|
-
};
|
|
114
|
+
const offer_and_accept = (args) => role_grant_offer_and_accept({ ...args, rpc_path });
|
|
165
115
|
// --- 1. Admin account listing (RPC) ---
|
|
166
116
|
describe('admin account listing', () => {
|
|
167
117
|
test('admin can list all accounts', async () => {
|
|
168
|
-
const
|
|
169
|
-
const user_two = await
|
|
118
|
+
const fixture = await options.setup_test();
|
|
119
|
+
const user_two = await fixture.create_account({ username: 'user_two' });
|
|
120
|
+
// Request the max page size — cross-process the backend persists
|
|
121
|
+
// accounts across tests (`globalSetup` bootstraps once), so a
|
|
122
|
+
// long-running suite accumulates well past the default 50 and
|
|
123
|
+
// `user_two` (newest, `ORDER BY created_at ASC`) falls off the
|
|
124
|
+
// first page. The MAX (200) carries this suite as written; once
|
|
125
|
+
// the suite consistently grows past it, swap to an
|
|
126
|
+
// account-scoped admin RPC (search-by-id, ORDER BY DESC, etc.)
|
|
127
|
+
// rather than chase the limit upward.
|
|
170
128
|
const res = await rpc_call_for_spec({
|
|
171
|
-
app:
|
|
129
|
+
app: { request: fixture.transport },
|
|
172
130
|
path: rpc_path,
|
|
173
131
|
spec: admin_account_list_action_spec,
|
|
174
|
-
params: {},
|
|
175
|
-
headers:
|
|
132
|
+
params: { limit: ADMIN_ACCOUNT_LIST_LIMIT_MAX },
|
|
133
|
+
headers: fixture.create_session_headers(),
|
|
176
134
|
});
|
|
177
135
|
assert.ok(res.ok, `admin_account_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
178
136
|
assert.ok(Array.isArray(res.result.accounts), 'Expected accounts array');
|
|
@@ -183,18 +141,48 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
183
141
|
assert.ok(found, 'Expected user_two in accounts listing');
|
|
184
142
|
});
|
|
185
143
|
test('non-admin cannot list accounts', async () => {
|
|
186
|
-
const
|
|
187
|
-
|
|
144
|
+
const fixture = await options.setup_test();
|
|
145
|
+
// Mint a fresh no-roles account — fresh signups default to no
|
|
146
|
+
// roles, so the per-test account is non-admin by construction.
|
|
147
|
+
const non_admin = await fixture.create_account({ username: 'non_admin_user' });
|
|
188
148
|
const res = await rpc_call_for_spec({
|
|
189
|
-
app:
|
|
149
|
+
app: { request: fixture.transport },
|
|
190
150
|
path: rpc_path,
|
|
191
151
|
spec: admin_account_list_action_spec,
|
|
192
152
|
params: {},
|
|
193
|
-
headers:
|
|
153
|
+
headers: non_admin.create_session_headers(),
|
|
194
154
|
});
|
|
195
155
|
assert.ok(!res.ok, 'Expected admin_account_list to fail for non-admin');
|
|
196
156
|
assert.strictEqual(res.status, 403);
|
|
197
157
|
});
|
|
158
|
+
// Probe the keeper-vs-admin separation specifically: a keeper-only
|
|
159
|
+
// account (no admin role) must still 403 on admin RPCs. ROLE_KEEPER
|
|
160
|
+
// is bootstrap-only-grantable, so the only way to seed this account
|
|
161
|
+
// is via `extra_accounts` at the test-binary bootstrap-equivalent
|
|
162
|
+
// step. Consumers that want this probe wire
|
|
163
|
+
// `extra_accounts: [{username: 'non_admin_keeper', roles: [ROLE_KEEPER]}]`
|
|
164
|
+
// into their `default_*_suite_options(...)` call; otherwise the
|
|
165
|
+
// test runtime-skips and the looser no-roles probe above carries
|
|
166
|
+
// the suite's "non-admin gets 403" assertion alone.
|
|
167
|
+
test('keeper-only account cannot list accounts (keeper ≠ admin)', async (ctx) => {
|
|
168
|
+
const fixture = await options.setup_test();
|
|
169
|
+
const non_admin_keeper = fixture.extra_accounts.non_admin_keeper;
|
|
170
|
+
if (!non_admin_keeper) {
|
|
171
|
+
ctx.skip(`no \`extra_accounts['non_admin_keeper']\` declared on the fixture — wire ` +
|
|
172
|
+
`\`extra_accounts: [{username: 'non_admin_keeper', roles: [ROLE_KEEPER]}]\` ` +
|
|
173
|
+
`in the consumer's suite options to enable the keeper-vs-admin probe.`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const res = await rpc_call_for_spec({
|
|
177
|
+
app: { request: fixture.transport },
|
|
178
|
+
path: rpc_path,
|
|
179
|
+
spec: admin_account_list_action_spec,
|
|
180
|
+
params: {},
|
|
181
|
+
headers: non_admin_keeper.create_session_headers(),
|
|
182
|
+
});
|
|
183
|
+
assert.ok(!res.ok, 'Expected admin_account_list to fail for keeper-only account');
|
|
184
|
+
assert.strictEqual(res.status, 403);
|
|
185
|
+
});
|
|
198
186
|
});
|
|
199
187
|
// --- 2. Role grant create/revoke lifecycle ---
|
|
200
188
|
// Role grant create/revoke are RPC-only (see `role_grant_offer_create` /
|
|
@@ -207,25 +195,25 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
207
195
|
// --- 3. Admin session management ---
|
|
208
196
|
describe('admin session management', () => {
|
|
209
197
|
test('admin can list all active sessions', async () => {
|
|
210
|
-
const
|
|
211
|
-
await
|
|
198
|
+
const fixture = await options.setup_test();
|
|
199
|
+
await fixture.create_account({ username: 'user_two' });
|
|
212
200
|
const res = await rpc_call_for_spec({
|
|
213
|
-
app:
|
|
201
|
+
app: { request: fixture.transport },
|
|
214
202
|
path: rpc_path,
|
|
215
203
|
spec: admin_session_list_action_spec,
|
|
216
204
|
params: {},
|
|
217
|
-
headers:
|
|
205
|
+
headers: fixture.create_session_headers(),
|
|
218
206
|
});
|
|
219
207
|
assert.ok(res.ok, `admin_session_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
220
208
|
assert.ok(Array.isArray(res.result.sessions), 'Expected sessions array');
|
|
221
209
|
assert.ok(res.result.sessions.length >= 2, 'Expected sessions from multiple accounts');
|
|
222
210
|
});
|
|
223
211
|
test('admin can revoke all sessions for another account', async () => {
|
|
224
|
-
const
|
|
225
|
-
const user_two = await
|
|
212
|
+
const fixture = await options.setup_test();
|
|
213
|
+
const user_two = await fixture.create_account({ username: 'user_two' });
|
|
226
214
|
// Verify user_two's session works via `account_verify` RPC
|
|
227
215
|
const before = await rpc_call_for_spec({
|
|
228
|
-
app:
|
|
216
|
+
app: { request: fixture.transport },
|
|
229
217
|
path: rpc_path,
|
|
230
218
|
spec: account_verify_action_spec,
|
|
231
219
|
params: undefined,
|
|
@@ -234,18 +222,18 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
234
222
|
assert.strictEqual(before.status, 200);
|
|
235
223
|
// Admin revokes all sessions for user_two via RPC
|
|
236
224
|
const res = await rpc_call_for_spec({
|
|
237
|
-
app:
|
|
225
|
+
app: { request: fixture.transport },
|
|
238
226
|
path: rpc_path,
|
|
239
227
|
spec: admin_session_revoke_all_action_spec,
|
|
240
228
|
params: { account_id: user_two.account.id },
|
|
241
|
-
headers:
|
|
229
|
+
headers: fixture.create_session_headers(),
|
|
242
230
|
});
|
|
243
231
|
assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
244
232
|
assert.strictEqual(res.result.ok, true);
|
|
245
233
|
assert.ok(res.result.count >= 1, 'Expected at least 1 revoked session');
|
|
246
234
|
// Verify user_two's session no longer works
|
|
247
235
|
const after = await rpc_call_for_spec({
|
|
248
|
-
app:
|
|
236
|
+
app: { request: fixture.transport },
|
|
249
237
|
path: rpc_path,
|
|
250
238
|
spec: account_verify_action_spec,
|
|
251
239
|
params: undefined,
|
|
@@ -254,25 +242,25 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
254
242
|
assert.strictEqual(after.status, 401);
|
|
255
243
|
});
|
|
256
244
|
test('admin revoking own sessions invalidates own session', async () => {
|
|
257
|
-
const
|
|
245
|
+
const fixture = await options.setup_test();
|
|
258
246
|
// Admin revokes own sessions via RPC
|
|
259
247
|
const res = await rpc_call_for_spec({
|
|
260
|
-
app:
|
|
248
|
+
app: { request: fixture.transport },
|
|
261
249
|
path: rpc_path,
|
|
262
250
|
spec: admin_session_revoke_all_action_spec,
|
|
263
|
-
params: { account_id:
|
|
264
|
-
headers:
|
|
251
|
+
params: { account_id: fixture.account.id },
|
|
252
|
+
headers: fixture.create_session_headers(),
|
|
265
253
|
});
|
|
266
254
|
assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
267
255
|
assert.strictEqual(res.result.ok, true);
|
|
268
256
|
assert.ok(res.result.count >= 1, 'Expected at least 1 revoked session');
|
|
269
257
|
// Admin's own session should no longer work
|
|
270
258
|
const after = await rpc_call_for_spec({
|
|
271
|
-
app:
|
|
259
|
+
app: { request: fixture.transport },
|
|
272
260
|
path: rpc_path,
|
|
273
261
|
spec: account_verify_action_spec,
|
|
274
262
|
params: undefined,
|
|
275
|
-
headers:
|
|
263
|
+
headers: fixture.create_session_headers(),
|
|
276
264
|
});
|
|
277
265
|
assert.strictEqual(after.status, 401);
|
|
278
266
|
});
|
|
@@ -280,11 +268,16 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
280
268
|
// --- 4. Admin token management ---
|
|
281
269
|
describe('admin token management', () => {
|
|
282
270
|
test('admin can revoke all tokens for another account', async () => {
|
|
283
|
-
const
|
|
284
|
-
const user_two = await
|
|
285
|
-
// Verify user_two's bearer token works via `account_verify` RPC
|
|
271
|
+
const fixture = await options.setup_test();
|
|
272
|
+
const user_two = await fixture.create_account({ username: 'user_two' });
|
|
273
|
+
// Verify user_two's bearer token works via `account_verify` RPC.
|
|
274
|
+
// Fresh transport — admin's session cookie in `fixture.transport`'s
|
|
275
|
+
// jar would otherwise give a false 200 here, masking whether the
|
|
276
|
+
// bearer credential is the one being validated. `origin: null` so
|
|
277
|
+
// the transport doesn't auto-add a default Origin (which would
|
|
278
|
+
// discard the bearer as browser-context cross-process).
|
|
286
279
|
const before = await rpc_call_for_spec({
|
|
287
|
-
app:
|
|
280
|
+
app: { request: fixture.fresh_transport({ origin: null }) },
|
|
288
281
|
path: rpc_path,
|
|
289
282
|
spec: account_verify_action_spec,
|
|
290
283
|
params: undefined,
|
|
@@ -294,18 +287,19 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
294
287
|
assert.strictEqual(before.status, 200);
|
|
295
288
|
// Admin revokes all tokens for user_two via RPC
|
|
296
289
|
const res = await rpc_call_for_spec({
|
|
297
|
-
app:
|
|
290
|
+
app: { request: fixture.transport },
|
|
298
291
|
path: rpc_path,
|
|
299
292
|
spec: admin_token_revoke_all_action_spec,
|
|
300
293
|
params: { account_id: user_two.account.id },
|
|
301
|
-
headers:
|
|
294
|
+
headers: fixture.create_session_headers(),
|
|
302
295
|
});
|
|
303
296
|
assert.ok(res.ok, `admin_token_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
304
297
|
assert.strictEqual(res.result.ok, true);
|
|
305
298
|
assert.ok(res.result.count >= 1, 'Expected at least 1 revoked token');
|
|
306
|
-
// Verify user_two's bearer token no longer works
|
|
299
|
+
// Verify user_two's bearer token no longer works — fresh transport
|
|
300
|
+
// same reason as the `before` call above.
|
|
307
301
|
const after = await rpc_call_for_spec({
|
|
308
|
-
app:
|
|
302
|
+
app: { request: fixture.fresh_transport({ origin: null }) },
|
|
309
303
|
path: rpc_path,
|
|
310
304
|
spec: account_verify_action_spec,
|
|
311
305
|
params: undefined,
|
|
@@ -318,36 +312,36 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
318
312
|
// --- 5. Audit log RPC reads ---
|
|
319
313
|
describe('audit log RPC reads', () => {
|
|
320
314
|
test('admin can list audit log events', async () => {
|
|
321
|
-
const
|
|
315
|
+
const fixture = await options.setup_test();
|
|
322
316
|
const res = await rpc_call_for_spec({
|
|
323
|
-
app:
|
|
317
|
+
app: { request: fixture.transport },
|
|
324
318
|
path: rpc_path,
|
|
325
319
|
spec: audit_log_list_action_spec,
|
|
326
320
|
params: {},
|
|
327
|
-
headers:
|
|
321
|
+
headers: fixture.create_session_headers(),
|
|
328
322
|
});
|
|
329
323
|
assert.ok(res.ok, `audit_log_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
330
324
|
assert.ok(Array.isArray(res.result.events), 'Expected events array');
|
|
331
325
|
});
|
|
332
326
|
test('audit log supports event_type filter', async () => {
|
|
333
|
-
const
|
|
327
|
+
const fixture = await options.setup_test();
|
|
334
328
|
// Admin offer emits `role_grant_offer_create`. The downstream
|
|
335
329
|
// `role_grant_create` only fires on accept — out of scope for this test.
|
|
336
|
-
const user_two = await
|
|
330
|
+
const user_two = await fixture.create_account({ username: 'user_two' });
|
|
337
331
|
const offer_res = await rpc_call_for_spec({
|
|
338
|
-
app:
|
|
332
|
+
app: { request: fixture.transport },
|
|
339
333
|
path: rpc_path,
|
|
340
334
|
spec: role_grant_offer_create_action_spec,
|
|
341
335
|
params: { to_account_id: user_two.account.id, role: grantable_role },
|
|
342
|
-
headers:
|
|
336
|
+
headers: fixture.create_session_headers(),
|
|
343
337
|
});
|
|
344
338
|
assert.ok(offer_res.ok, 'role_grant_offer_create should succeed');
|
|
345
339
|
const res = await rpc_call_for_spec({
|
|
346
|
-
app:
|
|
340
|
+
app: { request: fixture.transport },
|
|
347
341
|
path: rpc_path,
|
|
348
342
|
spec: audit_log_list_action_spec,
|
|
349
343
|
params: { event_type: 'role_grant_offer_create' },
|
|
350
|
-
headers:
|
|
344
|
+
headers: fixture.create_session_headers(),
|
|
351
345
|
});
|
|
352
346
|
assert.ok(res.ok, `audit_log_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
353
347
|
assert.ok(res.result.events.length >= 1, 'Expected at least 1 role_grant_offer_create event');
|
|
@@ -356,23 +350,22 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
356
350
|
}
|
|
357
351
|
});
|
|
358
352
|
test('admin can view role_grant history', async () => {
|
|
359
|
-
const
|
|
353
|
+
const fixture = await options.setup_test();
|
|
360
354
|
// Drive the full consent flow so `role_grant_create` lands in the audit log
|
|
361
355
|
// — `query_audit_log_list_role_grant_history` filters to (role_grant_create, role_grant_revoke).
|
|
362
|
-
const user_two = await
|
|
356
|
+
const user_two = await fixture.create_account({ username: 'user_two' });
|
|
363
357
|
await offer_and_accept({
|
|
364
|
-
app:
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
to_actor_id: user_two.actor.id,
|
|
358
|
+
app: { request: fixture.transport },
|
|
359
|
+
grantor: fixture,
|
|
360
|
+
recipient: user_two,
|
|
368
361
|
role: grantable_role,
|
|
369
362
|
});
|
|
370
363
|
const res = await rpc_call_for_spec({
|
|
371
|
-
app:
|
|
364
|
+
app: { request: fixture.transport },
|
|
372
365
|
path: rpc_path,
|
|
373
366
|
spec: audit_log_role_grant_history_action_spec,
|
|
374
367
|
params: {},
|
|
375
|
-
headers:
|
|
368
|
+
headers: fixture.create_session_headers(),
|
|
376
369
|
});
|
|
377
370
|
assert.ok(res.ok, `audit_log_role_grant_history failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
378
371
|
assert.ok(res.result.events.length >= 1, 'Expected at least 1 role_grant history event');
|
|
@@ -381,93 +374,101 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
381
374
|
// --- 6. Admin audit trail ---
|
|
382
375
|
describe('admin audit trail', () => {
|
|
383
376
|
test('role_grant revoke creates audit event', async () => {
|
|
384
|
-
const
|
|
385
|
-
const user_two = await
|
|
386
|
-
|
|
387
|
-
|
|
377
|
+
const fixture = await options.setup_test();
|
|
378
|
+
const user_two = await fixture.create_account({ username: 'user_two' });
|
|
379
|
+
// Drive the production consent flow so the role_grant exists
|
|
380
|
+
// via the same code path real users go through. The
|
|
381
|
+
// audit_log_list filter below pulls only `role_grant_revoke`
|
|
382
|
+
// events so the extra `role_grant_offer_create` /
|
|
383
|
+
// `role_grant_offer_accepted` audits emitted upstream don't
|
|
384
|
+
// affect the assertion.
|
|
385
|
+
const { role_grant_id } = await role_grant_offer_and_accept({
|
|
386
|
+
app: { request: fixture.transport },
|
|
387
|
+
rpc_path,
|
|
388
|
+
grantor: fixture,
|
|
389
|
+
recipient: user_two,
|
|
388
390
|
role: grantable_role,
|
|
389
|
-
granted_by: test_app.backend.actor.id,
|
|
390
391
|
});
|
|
391
392
|
// Revoke via RPC
|
|
392
393
|
const revoke_res = await rpc_call_for_spec({
|
|
393
|
-
app:
|
|
394
|
+
app: { request: fixture.transport },
|
|
394
395
|
path: rpc_path,
|
|
395
396
|
spec: role_grant_revoke_action_spec,
|
|
396
|
-
params: { actor_id: user_two.actor.id, role_grant_id
|
|
397
|
-
headers:
|
|
397
|
+
params: { actor_id: user_two.actor.id, role_grant_id },
|
|
398
|
+
headers: fixture.create_session_headers(),
|
|
398
399
|
});
|
|
399
400
|
assert.ok(revoke_res.ok, `role_grant_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
400
401
|
// Check audit log for role_grant_revoke event
|
|
401
402
|
const audit_res = await rpc_call_for_spec({
|
|
402
|
-
app:
|
|
403
|
+
app: { request: fixture.transport },
|
|
403
404
|
path: rpc_path,
|
|
404
405
|
spec: audit_log_list_action_spec,
|
|
405
406
|
params: { event_type: 'role_grant_revoke' },
|
|
406
|
-
headers:
|
|
407
|
+
headers: fixture.create_session_headers(),
|
|
407
408
|
});
|
|
408
409
|
assert.ok(audit_res.ok, `audit_log_list failed: ${audit_res.ok ? '' : JSON.stringify(audit_res.error)}`);
|
|
409
410
|
assert.ok(audit_res.result.events.length >= 1, 'Expected role_grant_revoke audit event');
|
|
410
411
|
assert.strictEqual(audit_res.result.events[0].event_type, 'role_grant_revoke');
|
|
411
412
|
});
|
|
412
413
|
test('admin session revoke-all creates audit event', async () => {
|
|
413
|
-
const
|
|
414
|
-
const user_two = await
|
|
414
|
+
const fixture = await options.setup_test();
|
|
415
|
+
const user_two = await fixture.create_account({ username: 'user_two' });
|
|
415
416
|
// Revoke all sessions for user_two via RPC
|
|
416
417
|
const revoke_res = await rpc_call_for_spec({
|
|
417
|
-
app:
|
|
418
|
+
app: { request: fixture.transport },
|
|
418
419
|
path: rpc_path,
|
|
419
420
|
spec: admin_session_revoke_all_action_spec,
|
|
420
421
|
params: { account_id: user_two.account.id },
|
|
421
|
-
headers:
|
|
422
|
+
headers: fixture.create_session_headers(),
|
|
422
423
|
});
|
|
423
424
|
assert.ok(revoke_res.ok, `admin_session_revoke_all failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
424
425
|
// Check audit log
|
|
425
426
|
const audit_res = await rpc_call_for_spec({
|
|
426
|
-
app:
|
|
427
|
+
app: { request: fixture.transport },
|
|
427
428
|
path: rpc_path,
|
|
428
429
|
spec: audit_log_list_action_spec,
|
|
429
430
|
params: { event_type: 'session_revoke_all' },
|
|
430
|
-
headers:
|
|
431
|
+
headers: fixture.create_session_headers(),
|
|
431
432
|
});
|
|
432
433
|
assert.ok(audit_res.ok, 'audit_log_list should succeed');
|
|
433
434
|
assert.ok(audit_res.result.events.length >= 1, 'Expected session_revoke_all audit event');
|
|
434
435
|
assert.strictEqual(audit_res.result.events[0].event_type, 'session_revoke_all');
|
|
435
436
|
});
|
|
436
437
|
test('admin token revoke-all creates audit event', async () => {
|
|
437
|
-
const
|
|
438
|
-
const user_two = await
|
|
438
|
+
const fixture = await options.setup_test();
|
|
439
|
+
const user_two = await fixture.create_account({ username: 'user_two' });
|
|
439
440
|
// Revoke all tokens for user_two via RPC
|
|
440
441
|
const revoke_res = await rpc_call_for_spec({
|
|
441
|
-
app:
|
|
442
|
+
app: { request: fixture.transport },
|
|
442
443
|
path: rpc_path,
|
|
443
444
|
spec: admin_token_revoke_all_action_spec,
|
|
444
445
|
params: { account_id: user_two.account.id },
|
|
445
|
-
headers:
|
|
446
|
+
headers: fixture.create_session_headers(),
|
|
446
447
|
});
|
|
447
448
|
assert.ok(revoke_res.ok, `admin_token_revoke_all failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
448
449
|
// Check audit log
|
|
449
450
|
const audit_res = await rpc_call_for_spec({
|
|
450
|
-
app:
|
|
451
|
+
app: { request: fixture.transport },
|
|
451
452
|
path: rpc_path,
|
|
452
453
|
spec: audit_log_list_action_spec,
|
|
453
454
|
params: { event_type: 'token_revoke_all' },
|
|
454
|
-
headers:
|
|
455
|
+
headers: fixture.create_session_headers(),
|
|
455
456
|
});
|
|
456
457
|
assert.ok(audit_res.ok, 'audit_log_list should succeed');
|
|
457
458
|
assert.ok(audit_res.result.events.length >= 1, 'Expected token_revoke_all audit event');
|
|
458
459
|
assert.strictEqual(audit_res.result.events[0].event_type, 'token_revoke_all');
|
|
459
460
|
});
|
|
460
461
|
test('admin session revoke-all 404 emits failure audit', async () => {
|
|
461
|
-
const
|
|
462
|
+
const fixture = await options.setup_test();
|
|
462
463
|
// `Uuid = z.uuid()` is v4-strict; use a valid v4 shape so we hit the
|
|
463
464
|
// handler's account lookup rather than failing at param validation.
|
|
464
465
|
const missing_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaa01';
|
|
465
466
|
const res = await rpc_call_for_spec({
|
|
466
|
-
app:
|
|
467
|
+
app: { request: fixture.transport },
|
|
467
468
|
path: rpc_path,
|
|
468
469
|
spec: admin_session_revoke_all_action_spec,
|
|
469
470
|
params: { account_id: missing_id },
|
|
470
|
-
headers:
|
|
471
|
+
headers: fixture.create_session_headers(),
|
|
471
472
|
});
|
|
472
473
|
assert.ok(!res.ok, 'Expected 404 for missing account');
|
|
473
474
|
assert.strictEqual(res.status, 404);
|
|
@@ -476,11 +477,11 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
476
477
|
// `target_account_id` is null (FK prevents referencing a missing id)
|
|
477
478
|
// — the probed id is preserved under `metadata.attempted_account_id`.
|
|
478
479
|
const audit_res = await rpc_call_for_spec({
|
|
479
|
-
app:
|
|
480
|
+
app: { request: fixture.transport },
|
|
480
481
|
path: rpc_path,
|
|
481
482
|
spec: audit_log_list_action_spec,
|
|
482
483
|
params: { event_type: 'session_revoke_all' },
|
|
483
|
-
headers:
|
|
484
|
+
headers: fixture.create_session_headers(),
|
|
484
485
|
});
|
|
485
486
|
assert.ok(audit_res.ok, 'audit_log_list should succeed');
|
|
486
487
|
const failure = audit_res.result.events.find((e) => e.outcome === 'failure');
|
|
@@ -491,24 +492,24 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
491
492
|
assert.strictEqual(failure_meta.attempted_account_id, missing_id);
|
|
492
493
|
});
|
|
493
494
|
test('admin token revoke-all 404 emits failure audit', async () => {
|
|
494
|
-
const
|
|
495
|
+
const fixture = await options.setup_test();
|
|
495
496
|
const missing_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaa02';
|
|
496
497
|
const res = await rpc_call_for_spec({
|
|
497
|
-
app:
|
|
498
|
+
app: { request: fixture.transport },
|
|
498
499
|
path: rpc_path,
|
|
499
500
|
spec: admin_token_revoke_all_action_spec,
|
|
500
501
|
params: { account_id: missing_id },
|
|
501
|
-
headers:
|
|
502
|
+
headers: fixture.create_session_headers(),
|
|
502
503
|
});
|
|
503
504
|
assert.ok(!res.ok, 'Expected 404 for missing account');
|
|
504
505
|
assert.strictEqual(res.status, 404);
|
|
505
506
|
assert.strictEqual(res.error.data.reason, 'account_not_found');
|
|
506
507
|
const audit_res = await rpc_call_for_spec({
|
|
507
|
-
app:
|
|
508
|
+
app: { request: fixture.transport },
|
|
508
509
|
path: rpc_path,
|
|
509
510
|
spec: audit_log_list_action_spec,
|
|
510
511
|
params: { event_type: 'token_revoke_all' },
|
|
511
|
-
headers:
|
|
512
|
+
headers: fixture.create_session_headers(),
|
|
512
513
|
});
|
|
513
514
|
assert.ok(audit_res.ok, 'audit_log_list should succeed');
|
|
514
515
|
const failure = audit_res.result.events.find((e) => e.outcome === 'failure');
|
|
@@ -521,35 +522,47 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
521
522
|
});
|
|
522
523
|
// --- 7. Audit log completeness ---
|
|
523
524
|
describe('audit log completeness', () => {
|
|
524
|
-
test('auth mutations each produce
|
|
525
|
-
const
|
|
526
|
-
const login_route = find_auth_route(
|
|
527
|
-
const logout_route = find_auth_route(
|
|
528
|
-
const password_route = find_auth_route(
|
|
525
|
+
test('auth mutations each produce at least one audit event', async () => {
|
|
526
|
+
const fixture = await options.setup_test();
|
|
527
|
+
const login_route = find_auth_route(route_specs, '/login', 'POST');
|
|
528
|
+
const logout_route = find_auth_route(route_specs, '/logout', 'POST');
|
|
529
|
+
const password_route = find_auth_route(route_specs, '/password', 'POST');
|
|
529
530
|
// skip if required routes are missing (consumer may not wire all routes).
|
|
530
531
|
// Token creation goes through `account_token_create` RPC — always wired
|
|
531
532
|
// because `rpc_endpoints` is required at the suite level.
|
|
532
533
|
if (!login_route || !logout_route || !password_route)
|
|
533
534
|
return;
|
|
534
|
-
const user_two = await
|
|
535
|
-
// 1. login (user_two logs in)
|
|
536
|
-
|
|
535
|
+
const user_two = await fixture.create_account({ username: 'audit_user' });
|
|
536
|
+
// 1. login (user_two logs in) — fresh transport because the per-test
|
|
537
|
+
// session cookie in `fixture.transport`'s jar would otherwise be sent
|
|
538
|
+
// alongside this unauthenticated login attempt, and the route can
|
|
539
|
+
// reject the "already-authed" cookie-collision case with 401.
|
|
540
|
+
// Read the actual username off the returned account — the fresh-DB
|
|
541
|
+
// contract makes the supplied literal `'audit_user'` survive
|
|
542
|
+
// unchanged, but reading from `.account.username` is structurally
|
|
543
|
+
// robust against any future username-mangling at the mint layer.
|
|
544
|
+
const login_res = await fixture.fresh_transport()(login_route.path, {
|
|
537
545
|
method: 'POST',
|
|
538
546
|
headers: {
|
|
539
547
|
host: 'localhost',
|
|
540
548
|
origin: 'http://localhost:5173',
|
|
541
549
|
'content-type': 'application/json',
|
|
542
550
|
},
|
|
543
|
-
body: JSON.stringify({
|
|
551
|
+
body: JSON.stringify({
|
|
552
|
+
username: user_two.account.username,
|
|
553
|
+
password: DEFAULT_TEST_PASSWORD,
|
|
554
|
+
}),
|
|
544
555
|
});
|
|
545
556
|
assert.strictEqual(login_res.status, 200);
|
|
546
557
|
// extract user_two session cookie for logout
|
|
547
558
|
const set_cookie = login_res.headers.get('set-cookie');
|
|
548
559
|
const cookie_match = new RegExp(`${cookie_name}=([^;]+)`).exec(set_cookie ?? '');
|
|
549
560
|
const user_two_cookie = cookie_match?.[1];
|
|
550
|
-
// 2. logout (user_two logs out)
|
|
561
|
+
// 2. logout (user_two logs out) — fresh transport with the explicit
|
|
562
|
+
// user_two cookie, so the per-test session cookie can't interfere with
|
|
563
|
+
// the logout target.
|
|
551
564
|
if (user_two_cookie) {
|
|
552
|
-
await
|
|
565
|
+
await fixture.fresh_transport()(logout_route.path, {
|
|
553
566
|
method: 'POST',
|
|
554
567
|
headers: {
|
|
555
568
|
host: 'localhost',
|
|
@@ -562,44 +575,43 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
562
575
|
// consentful flow: offer + accept so both `role_grant_offer_create` and
|
|
563
576
|
// `role_grant_create` audit events land.
|
|
564
577
|
const { role_grant_id } = await offer_and_accept({
|
|
565
|
-
app:
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
to_actor_id: user_two.actor.id,
|
|
578
|
+
app: { request: fixture.transport },
|
|
579
|
+
grantor: fixture,
|
|
580
|
+
recipient: user_two,
|
|
569
581
|
role: grantable_role,
|
|
570
582
|
});
|
|
571
583
|
// 4. revoke role_grant (RPC)
|
|
572
584
|
const revoke_res = await rpc_call_for_spec({
|
|
573
|
-
app:
|
|
585
|
+
app: { request: fixture.transport },
|
|
574
586
|
path: rpc_path,
|
|
575
587
|
spec: role_grant_revoke_action_spec,
|
|
576
588
|
params: { actor_id: user_two.actor.id, role_grant_id },
|
|
577
|
-
headers:
|
|
589
|
+
headers: fixture.create_session_headers(),
|
|
578
590
|
});
|
|
579
591
|
assert.ok(revoke_res.ok, `role_grant_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
580
592
|
// 5. create token (RPC)
|
|
581
593
|
const token_res = await rpc_call_for_spec({
|
|
582
|
-
app:
|
|
594
|
+
app: { request: fixture.transport },
|
|
583
595
|
path: rpc_path,
|
|
584
596
|
spec: account_token_create_action_spec,
|
|
585
597
|
params: { name: 'audit-test-token' },
|
|
586
|
-
headers:
|
|
598
|
+
headers: fixture.create_session_headers(),
|
|
587
599
|
});
|
|
588
600
|
assert.ok(token_res.ok, `account_token_create failed: ${token_res.ok ? '' : JSON.stringify(token_res.error)}`);
|
|
589
601
|
// 6. password change
|
|
590
|
-
await
|
|
602
|
+
await fixture.transport(password_route.path, {
|
|
591
603
|
method: 'POST',
|
|
592
|
-
headers:
|
|
604
|
+
headers: fixture.create_session_headers({
|
|
593
605
|
'content-type': 'application/json',
|
|
594
606
|
}),
|
|
595
607
|
body: JSON.stringify({
|
|
596
|
-
current_password:
|
|
608
|
+
current_password: DEFAULT_TEST_PASSWORD,
|
|
597
609
|
new_password: 'new-audit-password-789',
|
|
598
610
|
}),
|
|
599
611
|
});
|
|
600
612
|
// query audit log and verify events
|
|
601
613
|
// re-login as admin since password change revoked sessions
|
|
602
|
-
const relogin_res = await
|
|
614
|
+
const relogin_res = await fixture.transport(login_route.path, {
|
|
603
615
|
method: 'POST',
|
|
604
616
|
headers: {
|
|
605
617
|
host: 'localhost',
|
|
@@ -607,7 +619,7 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
607
619
|
'content-type': 'application/json',
|
|
608
620
|
},
|
|
609
621
|
body: JSON.stringify({
|
|
610
|
-
username:
|
|
622
|
+
username: fixture.account.username,
|
|
611
623
|
password: 'new-audit-password-789',
|
|
612
624
|
}),
|
|
613
625
|
});
|
|
@@ -621,7 +633,7 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
621
633
|
cookie: `${cookie_name}=${relogin_match[1]}`,
|
|
622
634
|
};
|
|
623
635
|
const audit_res = await rpc_call_for_spec({
|
|
624
|
-
app:
|
|
636
|
+
app: { request: fixture.transport },
|
|
625
637
|
path: rpc_path,
|
|
626
638
|
spec: audit_log_list_action_spec,
|
|
627
639
|
params: {},
|
|
@@ -652,58 +664,61 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
652
664
|
// --- 8. Admin-to-admin isolation ---
|
|
653
665
|
describe('admin-to-admin isolation', () => {
|
|
654
666
|
test('admin B revoking own role_grant via RPC succeeds', async () => {
|
|
655
|
-
const
|
|
656
|
-
captured_route_specs ??= test_app.route_specs;
|
|
667
|
+
const fixture = await options.setup_test();
|
|
657
668
|
// Bootstrap user is admin A. Create admin B.
|
|
658
|
-
const admin_b = await
|
|
669
|
+
const admin_b = await fixture.create_account({
|
|
659
670
|
username: 'admin_b_iso',
|
|
660
671
|
roles: ['admin'],
|
|
661
672
|
});
|
|
662
|
-
// Seed an active role_grant
|
|
663
|
-
//
|
|
664
|
-
|
|
665
|
-
|
|
673
|
+
// Seed an active role_grant for admin B via the production
|
|
674
|
+
// consent flow. The revoke IDOR check is the subject of this
|
|
675
|
+
// test; the grant path is precondition setup, exercised
|
|
676
|
+
// via the same RPCs production drives.
|
|
677
|
+
const { role_grant_id } = await role_grant_offer_and_accept({
|
|
678
|
+
app: { request: fixture.transport },
|
|
679
|
+
rpc_path,
|
|
680
|
+
grantor: fixture,
|
|
681
|
+
recipient: admin_b,
|
|
666
682
|
role: grantable_role,
|
|
667
|
-
granted_by: test_app.backend.actor.id,
|
|
668
683
|
});
|
|
669
684
|
// Admin B revokes their own role_grant via RPC — should succeed
|
|
670
685
|
const revoke_res = await rpc_call_for_spec({
|
|
671
|
-
app:
|
|
686
|
+
app: { request: fixture.transport },
|
|
672
687
|
path: rpc_path,
|
|
673
688
|
spec: role_grant_revoke_action_spec,
|
|
674
|
-
params: { actor_id: admin_b.actor.id, role_grant_id
|
|
689
|
+
params: { actor_id: admin_b.actor.id, role_grant_id },
|
|
675
690
|
headers: create_headers(admin_b.session_cookie),
|
|
676
691
|
});
|
|
677
692
|
assert.ok(revoke_res.ok, `role_grant_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
678
693
|
assert.strictEqual(revoke_res.result.revoked, true);
|
|
679
694
|
});
|
|
680
695
|
test('admin revoke-all sessions for another admin works', async () => {
|
|
681
|
-
const
|
|
682
|
-
const admin_b = await
|
|
696
|
+
const fixture = await options.setup_test();
|
|
697
|
+
const admin_b = await fixture.create_account({
|
|
683
698
|
username: 'admin_b_sess',
|
|
684
699
|
roles: ['admin'],
|
|
685
700
|
});
|
|
686
701
|
// Admin A revokes all of admin B's sessions via RPC
|
|
687
702
|
const res = await rpc_call_for_spec({
|
|
688
|
-
app:
|
|
703
|
+
app: { request: fixture.transport },
|
|
689
704
|
path: rpc_path,
|
|
690
705
|
spec: admin_session_revoke_all_action_spec,
|
|
691
706
|
params: { account_id: admin_b.account.id },
|
|
692
|
-
headers:
|
|
707
|
+
headers: fixture.create_session_headers(),
|
|
693
708
|
});
|
|
694
709
|
assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
695
710
|
assert.ok(typeof res.result.count === 'number', 'Expected count field in response');
|
|
696
711
|
assert.ok(res.result.count >= 1, 'Expected at least 1 session revoked');
|
|
697
712
|
});
|
|
698
713
|
test('admin revoke-all tokens for another admin works', async () => {
|
|
699
|
-
const
|
|
700
|
-
const admin_b = await
|
|
714
|
+
const fixture = await options.setup_test();
|
|
715
|
+
const admin_b = await fixture.create_account({
|
|
701
716
|
username: 'admin_b_tok',
|
|
702
717
|
roles: ['admin'],
|
|
703
718
|
});
|
|
704
719
|
// Admin B creates an API token via RPC
|
|
705
720
|
const token_res = await rpc_call_for_spec({
|
|
706
|
-
app:
|
|
721
|
+
app: { request: fixture.transport },
|
|
707
722
|
path: rpc_path,
|
|
708
723
|
spec: account_token_create_action_spec,
|
|
709
724
|
params: { name: 'admin-b-token' },
|
|
@@ -712,11 +727,11 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
712
727
|
assert.ok(token_res.ok, `account_token_create failed: ${token_res.ok ? '' : JSON.stringify(token_res.error)}`);
|
|
713
728
|
// Admin A revokes all of admin B's tokens via RPC
|
|
714
729
|
const res = await rpc_call_for_spec({
|
|
715
|
-
app:
|
|
730
|
+
app: { request: fixture.transport },
|
|
716
731
|
path: rpc_path,
|
|
717
732
|
spec: admin_token_revoke_all_action_spec,
|
|
718
733
|
params: { account_id: admin_b.account.id },
|
|
719
|
-
headers:
|
|
734
|
+
headers: fixture.create_session_headers(),
|
|
720
735
|
});
|
|
721
736
|
assert.ok(res.ok, `admin_token_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
722
737
|
assert.ok(typeof res.result.count === 'number', 'Expected count field in response');
|
|
@@ -724,11 +739,11 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
724
739
|
assert.ok(res.result.count >= 1, 'Expected at least 1 token revoked');
|
|
725
740
|
});
|
|
726
741
|
test('non-admin cannot access admin routes for another account', async () => {
|
|
727
|
-
const
|
|
728
|
-
const regular_user = await
|
|
742
|
+
const fixture = await options.setup_test();
|
|
743
|
+
const regular_user = await fixture.create_account({ username: 'regular_user_iso' });
|
|
729
744
|
// Regular user tries to list accounts via the admin RPC — should 403
|
|
730
745
|
const res = await rpc_call_for_spec({
|
|
731
|
-
app:
|
|
746
|
+
app: { request: fixture.transport },
|
|
732
747
|
path: rpc_path,
|
|
733
748
|
spec: admin_account_list_action_spec,
|
|
734
749
|
params: {},
|
|
@@ -741,23 +756,22 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
741
756
|
// --- 8a. Error coverage: unauthenticated access to admin routes ---
|
|
742
757
|
describe('error coverage breadth', () => {
|
|
743
758
|
test('exercises 401/403 on admin routes for error coverage', async () => {
|
|
744
|
-
const
|
|
745
|
-
captured_route_specs ??= test_app.route_specs;
|
|
759
|
+
const fixture = await options.setup_test();
|
|
746
760
|
// `/api/admin` is nearly empty — admin reads and mutations live
|
|
747
761
|
// on the RPC endpoint behind spec-level role auth. The path-prefix carve is still the right scope here
|
|
748
762
|
// because error coverage is tracked against REST `RouteSpec`s,
|
|
749
763
|
// not RPC method specs (`describe_rpc_round_trip_tests` covers
|
|
750
764
|
// the admin RPC surface separately).
|
|
751
765
|
const prefix = options.admin_prefix ?? '/api/admin';
|
|
752
|
-
const admin_routes =
|
|
766
|
+
const admin_routes = route_specs.filter((s) => s.path.startsWith(prefix) && (s.auth.roles?.includes('admin') ?? false));
|
|
753
767
|
// Hit admin routes without auth to exercise 401 error schemas.
|
|
754
768
|
for (const route of admin_routes) {
|
|
755
|
-
const res = await
|
|
769
|
+
const res = await fixture.transport(route.path, {
|
|
756
770
|
method: route.method,
|
|
757
771
|
headers: { host: 'localhost' },
|
|
758
772
|
});
|
|
759
773
|
if (res.status === 401 || res.status === 403) {
|
|
760
|
-
error_collector.record(
|
|
774
|
+
error_collector.record(route_specs, route.method, route.path, res.status);
|
|
761
775
|
}
|
|
762
776
|
}
|
|
763
777
|
});
|