@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.
Files changed (223) hide show
  1. package/dist/actions/CLAUDE.md +510 -946
  2. package/dist/actions/action_codegen.d.ts +1 -1
  3. package/dist/actions/action_codegen.js +1 -1
  4. package/dist/actions/action_event_data.d.ts +1 -1
  5. package/dist/actions/broadcast_api.d.ts +1 -1
  6. package/dist/actions/broadcast_api.js +1 -1
  7. package/dist/actions/cancel.d.ts +2 -2
  8. package/dist/actions/cancel.js +3 -3
  9. package/dist/actions/connection_closer.d.ts +1 -4
  10. package/dist/actions/connection_closer.d.ts.map +1 -1
  11. package/dist/actions/connection_closer.js +1 -4
  12. package/dist/actions/register_action_ws.d.ts +2 -2
  13. package/dist/actions/register_ws_endpoint.d.ts +1 -1
  14. package/dist/actions/transports_ws_auth_guard.d.ts +1 -2
  15. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  16. package/dist/actions/transports_ws_auth_guard.js +1 -2
  17. package/dist/auth/CLAUDE.md +570 -1871
  18. package/dist/auth/account_schema.d.ts +1 -1
  19. package/dist/auth/account_schema.d.ts.map +1 -1
  20. package/dist/auth/api_token_queries.js +1 -1
  21. package/dist/auth/audit_log_ddl.d.ts +1 -1
  22. package/dist/auth/audit_log_ddl.d.ts.map +1 -1
  23. package/dist/auth/audit_log_ddl.js +1 -1
  24. package/dist/auth/audit_log_schema.js +2 -2
  25. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  26. package/dist/auth/bootstrap_account.js +1 -5
  27. package/dist/auth/bootstrap_routes.d.ts +7 -1
  28. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  29. package/dist/auth/bootstrap_routes.js +15 -11
  30. package/dist/auth/daemon_token_middleware.d.ts +15 -5
  31. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  32. package/dist/auth/daemon_token_middleware.js +24 -15
  33. package/dist/auth/invite_queries.d.ts +17 -7
  34. package/dist/auth/invite_queries.d.ts.map +1 -1
  35. package/dist/auth/invite_queries.js +19 -8
  36. package/dist/auth/keyring.d.ts +6 -6
  37. package/dist/auth/keyring.js +8 -8
  38. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  39. package/dist/auth/role_grant_offer_actions.js +4 -2
  40. package/dist/auth/signup_routes.d.ts +47 -1
  41. package/dist/auth/signup_routes.d.ts.map +1 -1
  42. package/dist/auth/signup_routes.js +103 -52
  43. package/dist/db/create_db.d.ts.map +1 -1
  44. package/dist/db/create_db.js +13 -0
  45. package/dist/dev/setup.d.ts +2 -2
  46. package/dist/dev/setup.js +3 -3
  47. package/dist/env/resolve.d.ts +44 -7
  48. package/dist/env/resolve.d.ts.map +1 -1
  49. package/dist/env/resolve.js +94 -27
  50. package/dist/http/CLAUDE.md +243 -522
  51. package/dist/http/error_schemas.d.ts +0 -4
  52. package/dist/http/error_schemas.d.ts.map +1 -1
  53. package/dist/http/error_schemas.js +0 -4
  54. package/dist/http/ip_canonical.d.ts +5 -4
  55. package/dist/http/ip_canonical.d.ts.map +1 -1
  56. package/dist/http/ip_canonical.js +8 -4
  57. package/dist/http/jsonrpc.d.ts +23 -7
  58. package/dist/http/jsonrpc.d.ts.map +1 -1
  59. package/dist/http/jsonrpc.js +19 -3
  60. package/dist/http/origin.d.ts +1 -1
  61. package/dist/http/origin.js +1 -1
  62. package/dist/http/surface.d.ts +9 -2
  63. package/dist/http/surface.d.ts.map +1 -1
  64. package/dist/runtime/mock.d.ts +1 -1
  65. package/dist/runtime/mock.js +2 -2
  66. package/dist/server/app_server.d.ts +41 -10
  67. package/dist/server/app_server.d.ts.map +1 -1
  68. package/dist/server/app_server.js +10 -4
  69. package/dist/server/env.d.ts +7 -7
  70. package/dist/server/env.d.ts.map +1 -1
  71. package/dist/server/env.js +14 -14
  72. package/dist/server/static.d.ts +4 -4
  73. package/dist/server/static.js +7 -7
  74. package/dist/testing/CLAUDE.md +740 -418
  75. package/dist/testing/admin_integration.d.ts +18 -23
  76. package/dist/testing/admin_integration.d.ts.map +1 -1
  77. package/dist/testing/admin_integration.js +230 -216
  78. package/dist/testing/app_server.d.ts +141 -39
  79. package/dist/testing/app_server.d.ts.map +1 -1
  80. package/dist/testing/app_server.js +157 -44
  81. package/dist/testing/audit_completeness.d.ts +25 -22
  82. package/dist/testing/audit_completeness.d.ts.map +1 -1
  83. package/dist/testing/audit_completeness.js +198 -159
  84. package/dist/testing/bootstrap_success.d.ts +28 -0
  85. package/dist/testing/bootstrap_success.d.ts.map +1 -0
  86. package/dist/testing/bootstrap_success.js +144 -0
  87. package/dist/testing/cross_backend/backend_config.d.ts +113 -0
  88. package/dist/testing/cross_backend/backend_config.d.ts.map +1 -0
  89. package/dist/testing/cross_backend/backend_config.js +1 -0
  90. package/dist/testing/cross_backend/bench/bench_report.d.ts +46 -0
  91. package/dist/testing/cross_backend/bench/bench_report.d.ts.map +1 -0
  92. package/dist/testing/cross_backend/bench/bench_report.js +83 -0
  93. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +44 -0
  94. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts.map +1 -0
  95. package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +38 -0
  96. package/dist/testing/cross_backend/bench/scenario.d.ts +57 -0
  97. package/dist/testing/cross_backend/bench/scenario.d.ts.map +1 -0
  98. package/dist/testing/cross_backend/bench/scenario.js +28 -0
  99. package/dist/testing/cross_backend/bootstrap_backend.d.ts +41 -0
  100. package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -0
  101. package/dist/testing/cross_backend/bootstrap_backend.js +34 -0
  102. package/dist/testing/cross_backend/build_test_backend_paths.d.ts +24 -0
  103. package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -0
  104. package/dist/testing/cross_backend/build_test_backend_paths.js +33 -0
  105. package/dist/testing/cross_backend/capabilities.d.ts +65 -0
  106. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
  107. package/dist/testing/cross_backend/capabilities.js +47 -0
  108. package/dist/testing/cross_backend/default_backend_configs.d.ts +122 -0
  109. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -0
  110. package/dist/testing/cross_backend/default_backend_configs.js +111 -0
  111. package/dist/testing/cross_backend/default_secrets.d.ts +40 -0
  112. package/dist/testing/cross_backend/default_secrets.d.ts.map +1 -0
  113. package/dist/testing/cross_backend/default_secrets.js +39 -0
  114. package/dist/testing/cross_backend/default_spine_surface.d.ts +64 -0
  115. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -0
  116. package/dist/testing/cross_backend/default_spine_surface.js +121 -0
  117. package/dist/testing/cross_backend/setup.d.ts +451 -0
  118. package/dist/testing/cross_backend/setup.d.ts.map +1 -0
  119. package/dist/testing/cross_backend/setup.js +581 -0
  120. package/dist/testing/cross_backend/spawn_backend.d.ts +58 -0
  121. package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -0
  122. package/dist/testing/cross_backend/spawn_backend.js +229 -0
  123. package/dist/testing/cross_backend/spine_stub_backend_config.d.ts +66 -0
  124. package/dist/testing/cross_backend/spine_stub_backend_config.d.ts.map +1 -0
  125. package/dist/testing/cross_backend/spine_stub_backend_config.js +49 -0
  126. package/dist/testing/cross_backend/sse_round_trip.d.ts +37 -0
  127. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -0
  128. package/dist/testing/cross_backend/sse_round_trip.js +137 -0
  129. package/dist/testing/cross_backend/standard.d.ts +96 -0
  130. package/dist/testing/cross_backend/standard.d.ts.map +1 -0
  131. package/dist/testing/cross_backend/standard.js +49 -0
  132. package/dist/testing/cross_backend/testing_reset_actions.d.ts +171 -0
  133. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -0
  134. package/dist/testing/cross_backend/testing_reset_actions.js +213 -0
  135. package/dist/testing/cross_backend/testing_server_bun.d.ts +5 -0
  136. package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -0
  137. package/dist/testing/cross_backend/testing_server_bun.js +59 -0
  138. package/dist/testing/cross_backend/testing_server_core.d.ts +140 -0
  139. package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -0
  140. package/dist/testing/cross_backend/testing_server_core.js +68 -0
  141. package/dist/testing/cross_backend/testing_server_deno.d.ts +5 -0
  142. package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -0
  143. package/dist/testing/cross_backend/testing_server_deno.js +37 -0
  144. package/dist/testing/cross_backend/testing_server_node.d.ts +5 -0
  145. package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -0
  146. package/dist/testing/cross_backend/testing_server_node.js +50 -0
  147. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +72 -0
  148. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -0
  149. package/dist/testing/cross_backend/ts_spine_backend_config.js +112 -0
  150. package/dist/testing/cross_backend/ws_round_trip.d.ts +35 -0
  151. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -0
  152. package/dist/testing/cross_backend/ws_round_trip.js +113 -0
  153. package/dist/testing/data_exposure.d.ts +11 -14
  154. package/dist/testing/data_exposure.d.ts.map +1 -1
  155. package/dist/testing/data_exposure.js +123 -146
  156. package/dist/testing/db_entities.d.ts +22 -1
  157. package/dist/testing/db_entities.d.ts.map +1 -1
  158. package/dist/testing/db_entities.js +24 -1
  159. package/dist/testing/integration.d.ts +56 -21
  160. package/dist/testing/integration.d.ts.map +1 -1
  161. package/dist/testing/integration.js +294 -319
  162. package/dist/testing/integration_helpers.d.ts +16 -6
  163. package/dist/testing/integration_helpers.d.ts.map +1 -1
  164. package/dist/testing/integration_helpers.js +7 -7
  165. package/dist/testing/mock_fs.d.ts.map +1 -1
  166. package/dist/testing/mock_fs.js +0 -2
  167. package/dist/testing/rate_limiting.d.ts.map +1 -1
  168. package/dist/testing/rate_limiting.js +9 -0
  169. package/dist/testing/role_grant_helpers.d.ts +31 -0
  170. package/dist/testing/role_grant_helpers.d.ts.map +1 -0
  171. package/dist/testing/role_grant_helpers.js +46 -0
  172. package/dist/testing/round_trip.d.ts +20 -16
  173. package/dist/testing/round_trip.d.ts.map +1 -1
  174. package/dist/testing/round_trip.js +61 -86
  175. package/dist/testing/rpc_helpers.d.ts +10 -4
  176. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  177. package/dist/testing/rpc_helpers.js +1 -1
  178. package/dist/testing/rpc_round_trip.d.ts +24 -21
  179. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  180. package/dist/testing/rpc_round_trip.js +87 -104
  181. package/dist/testing/schema_introspect.d.ts +106 -0
  182. package/dist/testing/schema_introspect.d.ts.map +1 -0
  183. package/dist/testing/schema_introspect.js +123 -0
  184. package/dist/testing/schema_parity.d.ts +144 -0
  185. package/dist/testing/schema_parity.d.ts.map +1 -0
  186. package/dist/testing/schema_parity.js +233 -0
  187. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  188. package/dist/testing/sse_round_trip.js +1 -68
  189. package/dist/testing/standard.d.ts +56 -25
  190. package/dist/testing/standard.d.ts.map +1 -1
  191. package/dist/testing/standard.js +62 -5
  192. package/dist/testing/stubs.d.ts +21 -6
  193. package/dist/testing/stubs.d.ts.map +1 -1
  194. package/dist/testing/stubs.js +33 -23
  195. package/dist/testing/testing_rate_limiter.d.ts +59 -0
  196. package/dist/testing/testing_rate_limiter.d.ts.map +1 -0
  197. package/dist/testing/testing_rate_limiter.js +74 -0
  198. package/dist/testing/transports/bootstrap.d.ts +52 -0
  199. package/dist/testing/transports/bootstrap.d.ts.map +1 -0
  200. package/dist/testing/transports/bootstrap.js +70 -0
  201. package/dist/testing/transports/fetch_transport.d.ts +81 -0
  202. package/dist/testing/transports/fetch_transport.d.ts.map +1 -0
  203. package/dist/testing/transports/fetch_transport.js +74 -0
  204. package/dist/testing/transports/sse_frame_reader.d.ts +41 -0
  205. package/dist/testing/transports/sse_frame_reader.d.ts.map +1 -0
  206. package/dist/testing/transports/sse_frame_reader.js +84 -0
  207. package/dist/testing/transports/sse_transport.d.ts +54 -0
  208. package/dist/testing/transports/sse_transport.d.ts.map +1 -0
  209. package/dist/testing/transports/sse_transport.js +51 -0
  210. package/dist/testing/transports/ws_client.d.ts +108 -0
  211. package/dist/testing/transports/ws_client.d.ts.map +1 -0
  212. package/dist/testing/transports/ws_client.js +56 -0
  213. package/dist/testing/transports/ws_transport.d.ts +43 -0
  214. package/dist/testing/transports/ws_transport.d.ts.map +1 -0
  215. package/dist/testing/transports/ws_transport.js +169 -0
  216. package/dist/testing/ws_round_trip.d.ts +21 -103
  217. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  218. package/dist/testing/ws_round_trip.js +42 -40
  219. package/dist/ui/CLAUDE.md +5 -3
  220. package/dist/ui/MenuLink.svelte +16 -16
  221. package/dist/ui/MenuLink.svelte.d.ts +13 -4
  222. package/dist/ui/MenuLink.svelte.d.ts.map +1 -1
  223. 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 { ROLE_KEEPER, ROLE_ADMIN } from '../auth/role_schema.js';
29
+ import { ROLE_ADMIN } from '../auth/role_schema.js';
30
30
  import { GRANT_PATH_ADMIN } from '../auth/grant_path_schema.js';
31
- import { auth_migration_ns } from '../auth/migrations.js';
32
- import { create_test_app } from './app_server.js';
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. Factory-form
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
- const init_schema = async (db) => {
90
- await run_migrations(db, [auth_migration_ns]);
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
- if (captured_route_specs) {
103
- // Scope coverage to admin auth-related routes. Account listing,
104
- // session/token revoke-all, audit-log reads, and invite CRUD all
105
- // live on the RPC surface; the only admin REST route remaining
106
- // is the optional `GET /audit/stream` SSE (admin RPC methods
107
- // live behind spec-level role auth on the shared endpoint path).
108
- // The `/audit/stream` suffix tracks the hardcoded path in
109
- // `auth/audit_log_routes.ts` if consumers ever need to mount
110
- // the audit SSE at a different suffix, promote this to an
111
- // `audit_log_path_suffix` option on
112
- // `StandardAdminIntegrationTestOptions`.
113
- const admin_routes = captured_route_specs.filter((s) => s.path.endsWith('/audit/stream') && (s.auth.roles?.includes('admin') ?? false));
114
- // Adaptive threshold: when the scoped admin REST surface is
115
- // effectively empty (0–1 routes typical for the RPC-first
116
- // admin surface), the 20% baseline is meaningless — a single
117
- // SSE route that can't be exercised against an error schema
118
- // drops the ratio to 0.0%. Log an informational skip instead
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
- * Drive the full consent flow (admin offer → recipient accept) and
142
- * return the materialized role_grant id. Accept is a direct transactional
143
- * `query_accept_offer` call because the suite focuses on the admin
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 = async (args) => {
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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
169
- const user_two = await test_app.create_account({ username: 'user_two' });
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: test_app.app,
129
+ app: { request: fixture.transport },
172
130
  path: rpc_path,
173
131
  spec: admin_account_list_action_spec,
174
- params: {},
175
- headers: test_app.create_session_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 test_app = await create_test_app(build_admin_test_app_options(options, get_db(), [ROLE_KEEPER]));
187
- captured_route_specs ??= test_app.route_specs;
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: test_app.app,
149
+ app: { request: fixture.transport },
190
150
  path: rpc_path,
191
151
  spec: admin_account_list_action_spec,
192
152
  params: {},
193
- headers: test_app.create_session_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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
211
- await test_app.create_account({ username: 'user_two' });
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: test_app.app,
201
+ app: { request: fixture.transport },
214
202
  path: rpc_path,
215
203
  spec: admin_session_list_action_spec,
216
204
  params: {},
217
- headers: test_app.create_session_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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
225
- const user_two = await test_app.create_account({ username: 'user_two' });
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: test_app.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: test_app.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: test_app.create_session_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: test_app.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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
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: test_app.app,
248
+ app: { request: fixture.transport },
261
249
  path: rpc_path,
262
250
  spec: admin_session_revoke_all_action_spec,
263
- params: { account_id: test_app.backend.account.id },
264
- headers: test_app.create_session_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: test_app.app,
259
+ app: { request: fixture.transport },
272
260
  path: rpc_path,
273
261
  spec: account_verify_action_spec,
274
262
  params: undefined,
275
- headers: test_app.create_session_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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
284
- const user_two = await test_app.create_account({ username: 'user_two' });
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: test_app.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: test_app.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: test_app.create_session_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: test_app.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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
315
+ const fixture = await options.setup_test();
322
316
  const res = await rpc_call_for_spec({
323
- app: test_app.app,
317
+ app: { request: fixture.transport },
324
318
  path: rpc_path,
325
319
  spec: audit_log_list_action_spec,
326
320
  params: {},
327
- headers: test_app.create_session_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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
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 test_app.create_account({ username: 'user_two' });
330
+ const user_two = await fixture.create_account({ username: 'user_two' });
337
331
  const offer_res = await rpc_call_for_spec({
338
- app: test_app.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: test_app.create_session_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: test_app.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: test_app.create_session_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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
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 test_app.create_account({ username: 'user_two' });
356
+ const user_two = await fixture.create_account({ username: 'user_two' });
363
357
  await offer_and_accept({
364
- app: test_app.app,
365
- admin_headers: test_app.create_session_headers(),
366
- to_account_id: user_two.account.id,
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: test_app.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: test_app.create_session_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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
385
- const user_two = await test_app.create_account({ username: 'user_two' });
386
- const role_grant = await query_create_role_grant({ db: get_db() }, {
387
- actor_id: user_two.actor.id,
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: test_app.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: role_grant.id },
397
- headers: test_app.create_session_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: test_app.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: test_app.create_session_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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
414
- const user_two = await test_app.create_account({ username: 'user_two' });
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: test_app.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: test_app.create_session_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: test_app.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: test_app.create_session_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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
438
- const user_two = await test_app.create_account({ username: 'user_two' });
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: test_app.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: test_app.create_session_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: test_app.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: test_app.create_session_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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
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: test_app.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: test_app.create_session_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: test_app.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: test_app.create_session_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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
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: test_app.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: test_app.create_session_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: test_app.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: test_app.create_session_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 exactly one audit event', async () => {
525
- const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
526
- const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
527
- const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
528
- const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
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 test_app.create_account({ username: 'audit_user' });
535
- // 1. login (user_two logs in)
536
- const login_res = await test_app.app.request(login_route.path, {
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({ username: 'audit_user', password: 'test-password-123' }),
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 test_app.app.request(logout_route.path, {
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: test_app.app,
566
- admin_headers: test_app.create_session_headers(),
567
- to_account_id: user_two.account.id,
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: test_app.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: test_app.create_session_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: test_app.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: test_app.create_session_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 test_app.app.request(password_route.path, {
602
+ await fixture.transport(password_route.path, {
591
603
  method: 'POST',
592
- headers: test_app.create_session_headers({
604
+ headers: fixture.create_session_headers({
593
605
  'content-type': 'application/json',
594
606
  }),
595
607
  body: JSON.stringify({
596
- current_password: 'test-password-123',
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 test_app.app.request(login_route.path, {
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: test_app.backend.account.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: test_app.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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
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 test_app.create_account({
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 directly the revoke IDOR check is the
663
- // subject of this test, not the grant→accept cycle.
664
- const role_grant = await query_create_role_grant({ db: get_db() }, {
665
- actor_id: admin_b.actor.id,
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: test_app.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: 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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
682
- const admin_b = await test_app.create_account({
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: test_app.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: create_headers(test_app.backend.session_cookie),
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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
700
- const admin_b = await test_app.create_account({
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: test_app.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: test_app.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: create_headers(test_app.backend.session_cookie),
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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
728
- const regular_user = await test_app.create_account({ username: 'regular_user_iso' });
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: test_app.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 test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
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 = test_app.route_specs.filter((s) => s.path.startsWith(prefix) && (s.auth.roles?.includes('admin') ?? false));
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 test_app.app.request(route.path, {
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(test_app.route_specs, route.method, route.path, res.status);
774
+ error_collector.record(route_specs, route.method, route.path, res.status);
761
775
  }
762
776
  }
763
777
  });