@fuzdev/fuz_app 0.33.0 → 0.35.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/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +6 -1
- package/dist/actions/rpc_client.d.ts +23 -8
- package/dist/actions/rpc_client.d.ts.map +1 -1
- package/dist/actions/rpc_client.js +28 -9
- package/dist/auth/CLAUDE.md +28 -14
- package/dist/auth/standard_rpc_actions.d.ts +57 -0
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -0
- package/dist/auth/standard_rpc_actions.js +39 -0
- package/dist/server/app_server.d.ts +1 -1
- package/dist/testing/CLAUDE.md +24 -5
- package/dist/testing/admin_integration.d.ts +8 -6
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +167 -146
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +34 -31
- package/dist/testing/integration.d.ts +3 -3
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +187 -124
- package/dist/testing/rpc_helpers.d.ts +4 -4
- package/dist/testing/rpc_helpers.js +3 -3
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +68 -1
- package/dist/ui/admin_rpc_adapters.d.ts +10 -1
- package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
- package/dist/ui/admin_rpc_adapters.js +10 -1
- package/package.json +1 -1
- package/dist/auth/admin_rpc_actions.d.ts +0 -49
- package/dist/auth/admin_rpc_actions.d.ts.map +0 -1
- package/dist/auth/admin_rpc_actions.js +0 -32
|
@@ -10,6 +10,19 @@ import './assert_dev_env.js';
|
|
|
10
10
|
* Consumers call it with their route factory, session config, role schema,
|
|
11
11
|
* and RPC endpoint specs — all admin route tests come for free.
|
|
12
12
|
*
|
|
13
|
+
* Scope: admin *semantics* — cross-admin isolation, permit grant/revoke
|
|
14
|
+
* flow, session/token revoke-all, audit writes. Output-schema conformance
|
|
15
|
+
* for admin methods is **not** the concern of this suite; it lives in:
|
|
16
|
+
*
|
|
17
|
+
* - `describe_rpc_round_trip_tests` — every RPC method (admin methods
|
|
18
|
+
* included) is hit with a spec-generated valid body and the 2xx result
|
|
19
|
+
* is validated against `spec.output`.
|
|
20
|
+
* - `describe_round_trip_validation` — every REST route is hit and
|
|
21
|
+
* validated against its declared `output` / error schemas (SSE routes
|
|
22
|
+
* skipped via `Content-Type: text/event-stream`).
|
|
23
|
+
* - `describe_sse_route_tests` — SSE frames validated against their
|
|
24
|
+
* declared `EventSpec`.
|
|
25
|
+
*
|
|
13
26
|
* @module
|
|
14
27
|
*/
|
|
15
28
|
import { describe, test, assert, afterAll } from 'vitest';
|
|
@@ -17,10 +30,10 @@ import { ROLE_KEEPER, ROLE_ADMIN } from '../auth/role_schema.js';
|
|
|
17
30
|
import { AUTH_MIGRATION_NS } from '../auth/migrations.js';
|
|
18
31
|
import { create_test_app } from './app_server.js';
|
|
19
32
|
import { create_pglite_factory, create_describe_db, AUTH_INTEGRATION_TRUNCATE_TABLES, } from './db.js';
|
|
20
|
-
import { find_auth_route
|
|
33
|
+
import { find_auth_route } from './integration_helpers.js';
|
|
21
34
|
import { run_migrations } from '../db/migrate.js';
|
|
22
35
|
import { ErrorCoverageCollector, assert_error_coverage, DEFAULT_INTEGRATION_ERROR_COVERAGE, } from './error_coverage.js';
|
|
23
|
-
import {
|
|
36
|
+
import { rpc_call_for_spec, require_rpc_endpoint_path, resolve_rpc_endpoints_for_setup, } from './rpc_helpers.js';
|
|
24
37
|
import { permit_offer_create_action_spec, permit_revoke_action_spec, } from '../auth/permit_offer_action_specs.js';
|
|
25
38
|
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_permit_history_action_spec, } from '../auth/admin_action_specs.js';
|
|
26
39
|
import { account_token_create_action_spec, account_verify_action_spec, } from '../auth/account_action_specs.js';
|
|
@@ -54,8 +67,10 @@ const build_admin_test_app_options = (options, db, roles) => ({
|
|
|
54
67
|
* Standard admin integration test suite for fuz_app admin routes.
|
|
55
68
|
*
|
|
56
69
|
* Exercises account listing, permit grant/revoke (via RPC), session
|
|
57
|
-
* management, token management, audit log
|
|
58
|
-
* and
|
|
70
|
+
* management, token management, audit log reads, admin-to-admin
|
|
71
|
+
* isolation, and 401/403 error-coverage on the admin REST surface.
|
|
72
|
+
* Output-schema conformance is not in scope — see the module docstring
|
|
73
|
+
* for the suites that cover it.
|
|
59
74
|
*
|
|
60
75
|
* @param options - session config, route factory, role schema, RPC endpoints
|
|
61
76
|
*/
|
|
@@ -86,10 +101,30 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
86
101
|
// admin REST route remaining is the optional
|
|
87
102
|
// `GET /audit-log/stream` SSE, plus the shared RPC endpoint
|
|
88
103
|
// path itself (admin methods live behind spec-level role auth).
|
|
104
|
+
// The `/audit-log/stream` suffix tracks the hardcoded path in
|
|
105
|
+
// `auth/audit_log_routes.ts` — if consumers ever need to mount
|
|
106
|
+
// the audit SSE at a different suffix, promote this to an
|
|
107
|
+
// `audit_log_path_suffix` option on
|
|
108
|
+
// `StandardAdminIntegrationTestOptions`.
|
|
89
109
|
const admin_routes = captured_route_specs.filter((s) => s.path.endsWith('/audit-log/stream') &&
|
|
90
110
|
s.auth.type === 'role' &&
|
|
91
111
|
s.auth.role === 'admin');
|
|
92
|
-
|
|
112
|
+
// Adaptive threshold: when the scoped admin REST surface is
|
|
113
|
+
// effectively empty (0–1 routes, typical post-RPC-migration),
|
|
114
|
+
// the 20% baseline is meaningless — a single SSE route that
|
|
115
|
+
// can't be exercised against an error schema drops the ratio
|
|
116
|
+
// to 0.0%. Log an informational skip instead of asserting.
|
|
117
|
+
// The admin RPC surface is covered by
|
|
118
|
+
// `describe_rpc_round_trip_tests`, not this collector.
|
|
119
|
+
if (admin_routes.length <= 1) {
|
|
120
|
+
console.log(`[error coverage] skipped admin REST coverage assertion — ` +
|
|
121
|
+
`scoped surface has ${admin_routes.length} route(s); ` +
|
|
122
|
+
`admin RPC surface is covered by describe_rpc_round_trip_tests`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
assert_error_coverage(error_collector, admin_routes, {
|
|
126
|
+
min_coverage: DEFAULT_INTEGRATION_ERROR_COVERAGE,
|
|
127
|
+
});
|
|
93
128
|
}
|
|
94
129
|
});
|
|
95
130
|
/** Make request headers for a given session cookie. */
|
|
@@ -107,15 +142,15 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
107
142
|
* `describe_rpc_round_trip_tests` + fuz_app's own action suite.
|
|
108
143
|
*/
|
|
109
144
|
const offer_and_accept = async (args) => {
|
|
110
|
-
const res = await
|
|
145
|
+
const res = await rpc_call_for_spec({
|
|
111
146
|
app: args.app,
|
|
112
147
|
path: rpc_path,
|
|
113
|
-
|
|
148
|
+
spec: permit_offer_create_action_spec,
|
|
114
149
|
params: { to_account_id: args.to_account_id, role: args.role },
|
|
115
150
|
headers: args.admin_headers,
|
|
116
151
|
});
|
|
117
152
|
assert.ok(res.ok, `permit_offer_create failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
118
|
-
const offer = res.result
|
|
153
|
+
const { offer } = res.result;
|
|
119
154
|
const accept_result = await get_db().transaction(async (tx) => query_accept_offer({ db: tx }, { offer_id: offer.id, to_account_id: args.to_account_id, ip: null }));
|
|
120
155
|
return { offer_id: offer.id, permit_id: accept_result.permit.id };
|
|
121
156
|
};
|
|
@@ -124,28 +159,29 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
124
159
|
test('admin can list all accounts', async () => {
|
|
125
160
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
126
161
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
127
|
-
const res = await
|
|
162
|
+
const res = await rpc_call_for_spec({
|
|
128
163
|
app: test_app.app,
|
|
129
164
|
path: rpc_path,
|
|
130
|
-
|
|
165
|
+
spec: admin_account_list_action_spec,
|
|
166
|
+
params: null,
|
|
131
167
|
headers: test_app.create_session_headers(),
|
|
132
168
|
});
|
|
133
169
|
assert.ok(res.ok, `admin_account_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
134
|
-
|
|
135
|
-
assert.ok(
|
|
136
|
-
assert.ok(result.
|
|
137
|
-
assert.ok(Array.isArray(result.grantable_roles), 'Expected grantable_roles array');
|
|
170
|
+
assert.ok(Array.isArray(res.result.accounts), 'Expected accounts array');
|
|
171
|
+
assert.ok(res.result.accounts.length >= 2, 'Expected at least 2 accounts');
|
|
172
|
+
assert.ok(Array.isArray(res.result.grantable_roles), 'Expected grantable_roles array');
|
|
138
173
|
// Verify user_two appears in the listing
|
|
139
|
-
const found = result.accounts.find((e) => e.account.id === user_two.account.id);
|
|
174
|
+
const found = res.result.accounts.find((e) => e.account.id === user_two.account.id);
|
|
140
175
|
assert.ok(found, 'Expected user_two in accounts listing');
|
|
141
176
|
});
|
|
142
177
|
test('non-admin cannot list accounts', async () => {
|
|
143
178
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db(), [ROLE_KEEPER]));
|
|
144
179
|
captured_route_specs ??= test_app.route_specs;
|
|
145
|
-
const res = await
|
|
180
|
+
const res = await rpc_call_for_spec({
|
|
146
181
|
app: test_app.app,
|
|
147
182
|
path: rpc_path,
|
|
148
|
-
|
|
183
|
+
spec: admin_account_list_action_spec,
|
|
184
|
+
params: null,
|
|
149
185
|
headers: test_app.create_session_headers(),
|
|
150
186
|
});
|
|
151
187
|
assert.ok(!res.ok, 'Expected admin_account_list to fail for non-admin');
|
|
@@ -165,45 +201,46 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
165
201
|
test('admin can list all active sessions', async () => {
|
|
166
202
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
167
203
|
await test_app.create_account({ username: 'user_two' });
|
|
168
|
-
const res = await
|
|
204
|
+
const res = await rpc_call_for_spec({
|
|
169
205
|
app: test_app.app,
|
|
170
206
|
path: rpc_path,
|
|
171
|
-
|
|
207
|
+
spec: admin_session_list_action_spec,
|
|
208
|
+
params: null,
|
|
172
209
|
headers: test_app.create_session_headers(),
|
|
173
210
|
});
|
|
174
211
|
assert.ok(res.ok, `admin_session_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
175
|
-
|
|
176
|
-
assert.ok(
|
|
177
|
-
assert.ok(body.sessions.length >= 2, 'Expected sessions from multiple accounts');
|
|
212
|
+
assert.ok(Array.isArray(res.result.sessions), 'Expected sessions array');
|
|
213
|
+
assert.ok(res.result.sessions.length >= 2, 'Expected sessions from multiple accounts');
|
|
178
214
|
});
|
|
179
215
|
test('admin can revoke all sessions for another account', async () => {
|
|
180
216
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
181
217
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
182
218
|
// Verify user_two's session works via `account_verify` RPC
|
|
183
|
-
const before = await
|
|
219
|
+
const before = await rpc_call_for_spec({
|
|
184
220
|
app: test_app.app,
|
|
185
221
|
path: rpc_path,
|
|
186
|
-
|
|
222
|
+
spec: account_verify_action_spec,
|
|
223
|
+
params: null,
|
|
187
224
|
headers: create_headers(user_two.session_cookie),
|
|
188
225
|
});
|
|
189
226
|
assert.strictEqual(before.status, 200);
|
|
190
227
|
// Admin revokes all sessions for user_two via RPC
|
|
191
|
-
const res = await
|
|
228
|
+
const res = await rpc_call_for_spec({
|
|
192
229
|
app: test_app.app,
|
|
193
230
|
path: rpc_path,
|
|
194
|
-
|
|
231
|
+
spec: admin_session_revoke_all_action_spec,
|
|
195
232
|
params: { account_id: user_two.account.id },
|
|
196
233
|
headers: test_app.create_session_headers(),
|
|
197
234
|
});
|
|
198
235
|
assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
199
|
-
|
|
200
|
-
assert.
|
|
201
|
-
assert.ok(result.count >= 1, 'Expected at least 1 revoked session');
|
|
236
|
+
assert.strictEqual(res.result.ok, true);
|
|
237
|
+
assert.ok(res.result.count >= 1, 'Expected at least 1 revoked session');
|
|
202
238
|
// Verify user_two's session no longer works
|
|
203
|
-
const after = await
|
|
239
|
+
const after = await rpc_call_for_spec({
|
|
204
240
|
app: test_app.app,
|
|
205
241
|
path: rpc_path,
|
|
206
|
-
|
|
242
|
+
spec: account_verify_action_spec,
|
|
243
|
+
params: null,
|
|
207
244
|
headers: create_headers(user_two.session_cookie),
|
|
208
245
|
});
|
|
209
246
|
assert.strictEqual(after.status, 401);
|
|
@@ -211,22 +248,22 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
211
248
|
test('admin revoking own sessions invalidates own session', async () => {
|
|
212
249
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
213
250
|
// Admin revokes own sessions via RPC
|
|
214
|
-
const res = await
|
|
251
|
+
const res = await rpc_call_for_spec({
|
|
215
252
|
app: test_app.app,
|
|
216
253
|
path: rpc_path,
|
|
217
|
-
|
|
254
|
+
spec: admin_session_revoke_all_action_spec,
|
|
218
255
|
params: { account_id: test_app.backend.account.id },
|
|
219
256
|
headers: test_app.create_session_headers(),
|
|
220
257
|
});
|
|
221
258
|
assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
222
|
-
|
|
223
|
-
assert.
|
|
224
|
-
assert.ok(result.count >= 1, 'Expected at least 1 revoked session');
|
|
259
|
+
assert.strictEqual(res.result.ok, true);
|
|
260
|
+
assert.ok(res.result.count >= 1, 'Expected at least 1 revoked session');
|
|
225
261
|
// Admin's own session should no longer work
|
|
226
|
-
const after = await
|
|
262
|
+
const after = await rpc_call_for_spec({
|
|
227
263
|
app: test_app.app,
|
|
228
264
|
path: rpc_path,
|
|
229
|
-
|
|
265
|
+
spec: account_verify_action_spec,
|
|
266
|
+
params: null,
|
|
230
267
|
headers: test_app.create_session_headers(),
|
|
231
268
|
});
|
|
232
269
|
assert.strictEqual(after.status, 401);
|
|
@@ -238,31 +275,34 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
238
275
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
239
276
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
240
277
|
// Verify user_two's bearer token works via `account_verify` RPC
|
|
241
|
-
const before = await
|
|
278
|
+
const before = await rpc_call_for_spec({
|
|
242
279
|
app: test_app.app,
|
|
243
280
|
path: rpc_path,
|
|
244
|
-
|
|
281
|
+
spec: account_verify_action_spec,
|
|
282
|
+
params: null,
|
|
245
283
|
headers: { authorization: `Bearer ${user_two.api_token}` },
|
|
284
|
+
suppress_default_origin: true,
|
|
246
285
|
});
|
|
247
286
|
assert.strictEqual(before.status, 200);
|
|
248
287
|
// Admin revokes all tokens for user_two via RPC
|
|
249
|
-
const res = await
|
|
288
|
+
const res = await rpc_call_for_spec({
|
|
250
289
|
app: test_app.app,
|
|
251
290
|
path: rpc_path,
|
|
252
|
-
|
|
291
|
+
spec: admin_token_revoke_all_action_spec,
|
|
253
292
|
params: { account_id: user_two.account.id },
|
|
254
293
|
headers: test_app.create_session_headers(),
|
|
255
294
|
});
|
|
256
295
|
assert.ok(res.ok, `admin_token_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
257
|
-
|
|
258
|
-
assert.
|
|
259
|
-
assert.ok(result.count >= 1, 'Expected at least 1 revoked token');
|
|
296
|
+
assert.strictEqual(res.result.ok, true);
|
|
297
|
+
assert.ok(res.result.count >= 1, 'Expected at least 1 revoked token');
|
|
260
298
|
// Verify user_two's bearer token no longer works
|
|
261
|
-
const after = await
|
|
299
|
+
const after = await rpc_call_for_spec({
|
|
262
300
|
app: test_app.app,
|
|
263
301
|
path: rpc_path,
|
|
264
|
-
|
|
302
|
+
spec: account_verify_action_spec,
|
|
303
|
+
params: null,
|
|
265
304
|
headers: { authorization: `Bearer ${user_two.api_token}` },
|
|
305
|
+
suppress_default_origin: true,
|
|
266
306
|
});
|
|
267
307
|
assert.strictEqual(after.status, 401);
|
|
268
308
|
});
|
|
@@ -271,40 +311,39 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
271
311
|
describe('audit log RPC reads', () => {
|
|
272
312
|
test('admin can list audit log events', async () => {
|
|
273
313
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
274
|
-
const res = await
|
|
314
|
+
const res = await rpc_call_for_spec({
|
|
275
315
|
app: test_app.app,
|
|
276
316
|
path: rpc_path,
|
|
277
|
-
|
|
317
|
+
spec: audit_log_list_action_spec,
|
|
318
|
+
params: {},
|
|
278
319
|
headers: test_app.create_session_headers(),
|
|
279
320
|
});
|
|
280
321
|
assert.ok(res.ok, `audit_log_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
281
|
-
|
|
282
|
-
assert.ok(Array.isArray(body.events), 'Expected events array');
|
|
322
|
+
assert.ok(Array.isArray(res.result.events), 'Expected events array');
|
|
283
323
|
});
|
|
284
324
|
test('audit log supports event_type filter', async () => {
|
|
285
325
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
286
326
|
// Admin offer emits `permit_offer_create`. The downstream
|
|
287
327
|
// `permit_grant` only fires on accept — out of scope for this test.
|
|
288
328
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
289
|
-
const offer_res = await
|
|
329
|
+
const offer_res = await rpc_call_for_spec({
|
|
290
330
|
app: test_app.app,
|
|
291
331
|
path: rpc_path,
|
|
292
|
-
|
|
332
|
+
spec: permit_offer_create_action_spec,
|
|
293
333
|
params: { to_account_id: user_two.account.id, role: grantable_role },
|
|
294
334
|
headers: test_app.create_session_headers(),
|
|
295
335
|
});
|
|
296
336
|
assert.ok(offer_res.ok, 'permit_offer_create should succeed');
|
|
297
|
-
const res = await
|
|
337
|
+
const res = await rpc_call_for_spec({
|
|
298
338
|
app: test_app.app,
|
|
299
339
|
path: rpc_path,
|
|
300
|
-
|
|
340
|
+
spec: audit_log_list_action_spec,
|
|
301
341
|
params: { event_type: 'permit_offer_create' },
|
|
302
342
|
headers: test_app.create_session_headers(),
|
|
303
343
|
});
|
|
304
344
|
assert.ok(res.ok, `audit_log_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
for (const event of body.events) {
|
|
345
|
+
assert.ok(res.result.events.length >= 1, 'Expected at least 1 permit_offer_create event');
|
|
346
|
+
for (const event of res.result.events) {
|
|
308
347
|
assert.strictEqual(event.event_type, 'permit_offer_create');
|
|
309
348
|
}
|
|
310
349
|
});
|
|
@@ -319,15 +358,15 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
319
358
|
to_account_id: user_two.account.id,
|
|
320
359
|
role: grantable_role,
|
|
321
360
|
});
|
|
322
|
-
const res = await
|
|
361
|
+
const res = await rpc_call_for_spec({
|
|
323
362
|
app: test_app.app,
|
|
324
363
|
path: rpc_path,
|
|
325
|
-
|
|
364
|
+
spec: audit_log_permit_history_action_spec,
|
|
365
|
+
params: {},
|
|
326
366
|
headers: test_app.create_session_headers(),
|
|
327
367
|
});
|
|
328
368
|
assert.ok(res.ok, `audit_log_permit_history failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
329
|
-
|
|
330
|
-
assert.ok(body.events.length >= 1, 'Expected at least 1 permit history event');
|
|
369
|
+
assert.ok(res.result.events.length >= 1, 'Expected at least 1 permit history event');
|
|
331
370
|
});
|
|
332
371
|
});
|
|
333
372
|
// --- 6. Admin audit trail ---
|
|
@@ -343,86 +382,83 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
343
382
|
granted_by: test_app.backend.actor.id,
|
|
344
383
|
});
|
|
345
384
|
// Revoke via RPC
|
|
346
|
-
const revoke_res = await
|
|
385
|
+
const revoke_res = await rpc_call_for_spec({
|
|
347
386
|
app: test_app.app,
|
|
348
387
|
path: rpc_path,
|
|
349
|
-
|
|
388
|
+
spec: permit_revoke_action_spec,
|
|
350
389
|
params: { actor_id: target_actor.id, permit_id: permit.id },
|
|
351
390
|
headers: test_app.create_session_headers(),
|
|
352
391
|
});
|
|
353
392
|
assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
354
393
|
// Check audit log for permit_revoke event
|
|
355
|
-
const audit_res = await
|
|
394
|
+
const audit_res = await rpc_call_for_spec({
|
|
356
395
|
app: test_app.app,
|
|
357
396
|
path: rpc_path,
|
|
358
|
-
|
|
397
|
+
spec: audit_log_list_action_spec,
|
|
359
398
|
params: { event_type: 'permit_revoke' },
|
|
360
399
|
headers: test_app.create_session_headers(),
|
|
361
400
|
});
|
|
362
401
|
assert.ok(audit_res.ok, `audit_log_list failed: ${audit_res.ok ? '' : JSON.stringify(audit_res.error)}`);
|
|
363
|
-
|
|
364
|
-
assert.
|
|
365
|
-
assert.strictEqual(audit_body.events[0].event_type, 'permit_revoke');
|
|
402
|
+
assert.ok(audit_res.result.events.length >= 1, 'Expected permit_revoke audit event');
|
|
403
|
+
assert.strictEqual(audit_res.result.events[0].event_type, 'permit_revoke');
|
|
366
404
|
});
|
|
367
405
|
test('admin session revoke-all creates audit event', async () => {
|
|
368
406
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
369
407
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
370
408
|
// Revoke all sessions for user_two via RPC
|
|
371
|
-
const revoke_res = await
|
|
409
|
+
const revoke_res = await rpc_call_for_spec({
|
|
372
410
|
app: test_app.app,
|
|
373
411
|
path: rpc_path,
|
|
374
|
-
|
|
412
|
+
spec: admin_session_revoke_all_action_spec,
|
|
375
413
|
params: { account_id: user_two.account.id },
|
|
376
414
|
headers: test_app.create_session_headers(),
|
|
377
415
|
});
|
|
378
416
|
assert.ok(revoke_res.ok, `admin_session_revoke_all failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
379
417
|
// Check audit log
|
|
380
|
-
const audit_res = await
|
|
418
|
+
const audit_res = await rpc_call_for_spec({
|
|
381
419
|
app: test_app.app,
|
|
382
420
|
path: rpc_path,
|
|
383
|
-
|
|
421
|
+
spec: audit_log_list_action_spec,
|
|
384
422
|
params: { event_type: 'session_revoke_all' },
|
|
385
423
|
headers: test_app.create_session_headers(),
|
|
386
424
|
});
|
|
387
425
|
assert.ok(audit_res.ok, 'audit_log_list should succeed');
|
|
388
|
-
|
|
389
|
-
assert.
|
|
390
|
-
assert.strictEqual(audit_body.events[0].event_type, 'session_revoke_all');
|
|
426
|
+
assert.ok(audit_res.result.events.length >= 1, 'Expected session_revoke_all audit event');
|
|
427
|
+
assert.strictEqual(audit_res.result.events[0].event_type, 'session_revoke_all');
|
|
391
428
|
});
|
|
392
429
|
test('admin token revoke-all creates audit event', async () => {
|
|
393
430
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
394
431
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
395
432
|
// Revoke all tokens for user_two via RPC
|
|
396
|
-
const revoke_res = await
|
|
433
|
+
const revoke_res = await rpc_call_for_spec({
|
|
397
434
|
app: test_app.app,
|
|
398
435
|
path: rpc_path,
|
|
399
|
-
|
|
436
|
+
spec: admin_token_revoke_all_action_spec,
|
|
400
437
|
params: { account_id: user_two.account.id },
|
|
401
438
|
headers: test_app.create_session_headers(),
|
|
402
439
|
});
|
|
403
440
|
assert.ok(revoke_res.ok, `admin_token_revoke_all failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
404
441
|
// Check audit log
|
|
405
|
-
const audit_res = await
|
|
442
|
+
const audit_res = await rpc_call_for_spec({
|
|
406
443
|
app: test_app.app,
|
|
407
444
|
path: rpc_path,
|
|
408
|
-
|
|
445
|
+
spec: audit_log_list_action_spec,
|
|
409
446
|
params: { event_type: 'token_revoke_all' },
|
|
410
447
|
headers: test_app.create_session_headers(),
|
|
411
448
|
});
|
|
412
449
|
assert.ok(audit_res.ok, 'audit_log_list should succeed');
|
|
413
|
-
|
|
414
|
-
assert.
|
|
415
|
-
assert.strictEqual(audit_body.events[0].event_type, 'token_revoke_all');
|
|
450
|
+
assert.ok(audit_res.result.events.length >= 1, 'Expected token_revoke_all audit event');
|
|
451
|
+
assert.strictEqual(audit_res.result.events[0].event_type, 'token_revoke_all');
|
|
416
452
|
});
|
|
417
453
|
test('admin session revoke-all 404 emits failure audit', async () => {
|
|
418
454
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
419
455
|
// `Uuid = z.uuid()` is v4-strict; use a valid v4 shape so we hit the
|
|
420
456
|
// handler's account lookup rather than failing at param validation.
|
|
421
457
|
const missing_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaa01';
|
|
422
|
-
const res = await
|
|
458
|
+
const res = await rpc_call_for_spec({
|
|
423
459
|
app: test_app.app,
|
|
424
460
|
path: rpc_path,
|
|
425
|
-
|
|
461
|
+
spec: admin_session_revoke_all_action_spec,
|
|
426
462
|
params: { account_id: missing_id },
|
|
427
463
|
headers: test_app.create_session_headers(),
|
|
428
464
|
});
|
|
@@ -432,48 +468,48 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
432
468
|
// Failure audit row should be visible on the audit-log feed.
|
|
433
469
|
// `target_account_id` is null (FK prevents referencing a missing id)
|
|
434
470
|
// — the probed id is preserved under `metadata.attempted_account_id`.
|
|
435
|
-
const audit_res = await
|
|
471
|
+
const audit_res = await rpc_call_for_spec({
|
|
436
472
|
app: test_app.app,
|
|
437
473
|
path: rpc_path,
|
|
438
|
-
|
|
474
|
+
spec: audit_log_list_action_spec,
|
|
439
475
|
params: { event_type: 'session_revoke_all' },
|
|
440
476
|
headers: test_app.create_session_headers(),
|
|
441
477
|
});
|
|
442
478
|
assert.ok(audit_res.ok, 'audit_log_list should succeed');
|
|
443
|
-
const
|
|
444
|
-
const failure = audit_body.events.find((e) => e.outcome === 'failure');
|
|
479
|
+
const failure = audit_res.result.events.find((e) => e.outcome === 'failure');
|
|
445
480
|
assert.ok(failure, 'Expected a failure-outcome session_revoke_all audit event');
|
|
446
481
|
assert.strictEqual(failure.target_account_id, null);
|
|
447
|
-
|
|
448
|
-
assert.strictEqual(
|
|
482
|
+
const failure_meta = failure.metadata;
|
|
483
|
+
assert.strictEqual(failure_meta.reason, 'account_not_found');
|
|
484
|
+
assert.strictEqual(failure_meta.attempted_account_id, missing_id);
|
|
449
485
|
});
|
|
450
486
|
test('admin token revoke-all 404 emits failure audit', async () => {
|
|
451
487
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
452
488
|
const missing_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaa02';
|
|
453
|
-
const res = await
|
|
489
|
+
const res = await rpc_call_for_spec({
|
|
454
490
|
app: test_app.app,
|
|
455
491
|
path: rpc_path,
|
|
456
|
-
|
|
492
|
+
spec: admin_token_revoke_all_action_spec,
|
|
457
493
|
params: { account_id: missing_id },
|
|
458
494
|
headers: test_app.create_session_headers(),
|
|
459
495
|
});
|
|
460
496
|
assert.ok(!res.ok, 'Expected 404 for missing account');
|
|
461
497
|
assert.strictEqual(res.status, 404);
|
|
462
498
|
assert.strictEqual(res.error.data.reason, 'account_not_found');
|
|
463
|
-
const audit_res = await
|
|
499
|
+
const audit_res = await rpc_call_for_spec({
|
|
464
500
|
app: test_app.app,
|
|
465
501
|
path: rpc_path,
|
|
466
|
-
|
|
502
|
+
spec: audit_log_list_action_spec,
|
|
467
503
|
params: { event_type: 'token_revoke_all' },
|
|
468
504
|
headers: test_app.create_session_headers(),
|
|
469
505
|
});
|
|
470
506
|
assert.ok(audit_res.ok, 'audit_log_list should succeed');
|
|
471
|
-
const
|
|
472
|
-
const failure = audit_body.events.find((e) => e.outcome === 'failure');
|
|
507
|
+
const failure = audit_res.result.events.find((e) => e.outcome === 'failure');
|
|
473
508
|
assert.ok(failure, 'Expected a failure-outcome token_revoke_all audit event');
|
|
474
509
|
assert.strictEqual(failure.target_account_id, null);
|
|
475
|
-
|
|
476
|
-
assert.strictEqual(
|
|
510
|
+
const failure_meta = failure.metadata;
|
|
511
|
+
assert.strictEqual(failure_meta.reason, 'account_not_found');
|
|
512
|
+
assert.strictEqual(failure_meta.attempted_account_id, missing_id);
|
|
477
513
|
});
|
|
478
514
|
});
|
|
479
515
|
// --- 7. Audit log completeness ---
|
|
@@ -527,19 +563,19 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
527
563
|
// 4. revoke permit (RPC)
|
|
528
564
|
const target_actor = await query_actor_by_account({ db: get_db() }, user_two.account.id);
|
|
529
565
|
assert.ok(target_actor);
|
|
530
|
-
const revoke_res = await
|
|
566
|
+
const revoke_res = await rpc_call_for_spec({
|
|
531
567
|
app: test_app.app,
|
|
532
568
|
path: rpc_path,
|
|
533
|
-
|
|
569
|
+
spec: permit_revoke_action_spec,
|
|
534
570
|
params: { actor_id: target_actor.id, permit_id },
|
|
535
571
|
headers: test_app.create_session_headers(),
|
|
536
572
|
});
|
|
537
573
|
assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
538
574
|
// 5. create token (RPC)
|
|
539
|
-
const token_res = await
|
|
575
|
+
const token_res = await rpc_call_for_spec({
|
|
540
576
|
app: test_app.app,
|
|
541
577
|
path: rpc_path,
|
|
542
|
-
|
|
578
|
+
spec: account_token_create_action_spec,
|
|
543
579
|
params: { name: 'audit-test-token' },
|
|
544
580
|
headers: test_app.create_session_headers(),
|
|
545
581
|
});
|
|
@@ -578,15 +614,15 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
578
614
|
origin: 'http://localhost:5173',
|
|
579
615
|
cookie: `${cookie_name}=${relogin_match[1]}`,
|
|
580
616
|
};
|
|
581
|
-
const audit_res = await
|
|
617
|
+
const audit_res = await rpc_call_for_spec({
|
|
582
618
|
app: test_app.app,
|
|
583
619
|
path: rpc_path,
|
|
584
|
-
|
|
620
|
+
spec: audit_log_list_action_spec,
|
|
621
|
+
params: {},
|
|
585
622
|
headers: relogin_headers,
|
|
586
623
|
});
|
|
587
624
|
assert.ok(audit_res.ok, `audit_log_list failed: ${audit_res.ok ? '' : JSON.stringify(audit_res.error)}`);
|
|
588
|
-
const
|
|
589
|
-
const events = audit_body.events;
|
|
625
|
+
const events = audit_res.result.events;
|
|
590
626
|
// check that each operation produced at least one event.
|
|
591
627
|
// `permit_offer_create` fires on the admin RPC; `permit_grant`
|
|
592
628
|
// fires when the recipient accepts (driven by offer_and_accept).
|
|
@@ -625,16 +661,15 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
625
661
|
granted_by: test_app.backend.actor.id,
|
|
626
662
|
});
|
|
627
663
|
// Admin B revokes their own permit via RPC — should succeed
|
|
628
|
-
const revoke_res = await
|
|
664
|
+
const revoke_res = await rpc_call_for_spec({
|
|
629
665
|
app: test_app.app,
|
|
630
666
|
path: rpc_path,
|
|
631
|
-
|
|
667
|
+
spec: permit_revoke_action_spec,
|
|
632
668
|
params: { actor_id: admin_b.actor.id, permit_id: permit.id },
|
|
633
669
|
headers: create_headers(admin_b.session_cookie),
|
|
634
670
|
});
|
|
635
671
|
assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
636
|
-
|
|
637
|
-
assert.strictEqual(result.revoked, true);
|
|
672
|
+
assert.strictEqual(revoke_res.result.revoked, true);
|
|
638
673
|
});
|
|
639
674
|
test('admin revoke-all sessions for another admin works', async () => {
|
|
640
675
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
@@ -643,17 +678,16 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
643
678
|
roles: ['admin'],
|
|
644
679
|
});
|
|
645
680
|
// Admin A revokes all of admin B's sessions via RPC
|
|
646
|
-
const res = await
|
|
681
|
+
const res = await rpc_call_for_spec({
|
|
647
682
|
app: test_app.app,
|
|
648
683
|
path: rpc_path,
|
|
649
|
-
|
|
684
|
+
spec: admin_session_revoke_all_action_spec,
|
|
650
685
|
params: { account_id: admin_b.account.id },
|
|
651
686
|
headers: create_headers(test_app.backend.session_cookie),
|
|
652
687
|
});
|
|
653
688
|
assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
654
|
-
|
|
655
|
-
assert.ok(
|
|
656
|
-
assert.ok(result.count >= 1, 'Expected at least 1 session revoked');
|
|
689
|
+
assert.ok(typeof res.result.count === 'number', 'Expected count field in response');
|
|
690
|
+
assert.ok(res.result.count >= 1, 'Expected at least 1 session revoked');
|
|
657
691
|
});
|
|
658
692
|
test('admin revoke-all tokens for another admin works', async () => {
|
|
659
693
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
@@ -662,36 +696,36 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
662
696
|
roles: ['admin'],
|
|
663
697
|
});
|
|
664
698
|
// Admin B creates an API token via RPC
|
|
665
|
-
const token_res = await
|
|
699
|
+
const token_res = await rpc_call_for_spec({
|
|
666
700
|
app: test_app.app,
|
|
667
701
|
path: rpc_path,
|
|
668
|
-
|
|
702
|
+
spec: account_token_create_action_spec,
|
|
669
703
|
params: { name: 'admin-b-token' },
|
|
670
704
|
headers: create_headers(admin_b.session_cookie),
|
|
671
705
|
});
|
|
672
706
|
assert.ok(token_res.ok, `account_token_create failed: ${token_res.ok ? '' : JSON.stringify(token_res.error)}`);
|
|
673
707
|
// Admin A revokes all of admin B's tokens via RPC
|
|
674
|
-
const res = await
|
|
708
|
+
const res = await rpc_call_for_spec({
|
|
675
709
|
app: test_app.app,
|
|
676
710
|
path: rpc_path,
|
|
677
|
-
|
|
711
|
+
spec: admin_token_revoke_all_action_spec,
|
|
678
712
|
params: { account_id: admin_b.account.id },
|
|
679
713
|
headers: create_headers(test_app.backend.session_cookie),
|
|
680
714
|
});
|
|
681
715
|
assert.ok(res.ok, `admin_token_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
682
|
-
|
|
683
|
-
assert.ok(typeof result.count === 'number', 'Expected count field in response');
|
|
716
|
+
assert.ok(typeof res.result.count === 'number', 'Expected count field in response');
|
|
684
717
|
// Token was created above, so revoke should evict at least one.
|
|
685
|
-
assert.ok(result.count >= 1, 'Expected at least 1 token revoked');
|
|
718
|
+
assert.ok(res.result.count >= 1, 'Expected at least 1 token revoked');
|
|
686
719
|
});
|
|
687
720
|
test('non-admin cannot access admin routes for another account', async () => {
|
|
688
721
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
689
722
|
const regular_user = await test_app.create_account({ username: 'regular_user_iso' });
|
|
690
723
|
// Regular user tries to list accounts via the admin RPC — should 403
|
|
691
|
-
const res = await
|
|
724
|
+
const res = await rpc_call_for_spec({
|
|
692
725
|
app: test_app.app,
|
|
693
726
|
path: rpc_path,
|
|
694
|
-
|
|
727
|
+
spec: admin_account_list_action_spec,
|
|
728
|
+
params: null,
|
|
695
729
|
headers: create_headers(regular_user.session_cookie),
|
|
696
730
|
});
|
|
697
731
|
assert.ok(!res.ok, 'Expected admin_account_list to fail for non-admin');
|
|
@@ -703,10 +737,16 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
703
737
|
test('exercises 401/403 on admin routes for error coverage', async () => {
|
|
704
738
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
705
739
|
captured_route_specs ??= test_app.route_specs;
|
|
740
|
+
// Post-RPC migration, `/api/admin` is nearly empty — admin reads
|
|
741
|
+
// and mutations live on the RPC endpoint behind spec-level role
|
|
742
|
+
// auth. The path-prefix carve is still the right scope here
|
|
743
|
+
// because error coverage is tracked against REST `RouteSpec`s,
|
|
744
|
+
// not RPC method specs (`describe_rpc_round_trip_tests` covers
|
|
745
|
+
// the admin RPC surface separately).
|
|
706
746
|
const prefix = options.admin_prefix ?? '/api/admin';
|
|
707
747
|
const admin_routes = test_app.route_specs.filter((s) => s.path.startsWith(prefix) && s.auth.type === 'role' && s.auth.role === 'admin');
|
|
708
|
-
// Hit admin routes without auth to exercise 401 error schemas
|
|
709
|
-
for (const route of admin_routes
|
|
748
|
+
// Hit admin routes without auth to exercise 401 error schemas.
|
|
749
|
+
for (const route of admin_routes) {
|
|
710
750
|
const res = await test_app.app.request(route.path, {
|
|
711
751
|
method: route.method,
|
|
712
752
|
headers: { host: 'localhost' },
|
|
@@ -717,24 +757,5 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
717
757
|
}
|
|
718
758
|
});
|
|
719
759
|
});
|
|
720
|
-
// --- 8. Admin response schema validation ---
|
|
721
|
-
describe('admin response schema validation', () => {
|
|
722
|
-
test('admin route 200 responses match declared output schemas', async () => {
|
|
723
|
-
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
724
|
-
const prefix = options.admin_prefix ?? '/api/admin';
|
|
725
|
-
const admin_get_routes = test_app.route_specs.filter((s) => s.method === 'GET' &&
|
|
726
|
-
s.path.startsWith(prefix) &&
|
|
727
|
-
s.auth.type === 'role' &&
|
|
728
|
-
s.auth.role === 'admin');
|
|
729
|
-
assert.ok(admin_get_routes.length > 0, 'Expected at least one admin GET route — ensure create_route_specs includes admin routes');
|
|
730
|
-
for (const route of admin_get_routes) {
|
|
731
|
-
const res = await test_app.app.request(route.path, {
|
|
732
|
-
headers: test_app.create_session_headers(),
|
|
733
|
-
});
|
|
734
|
-
assert.strictEqual(res.status, 200, `${route.method} ${route.path} should return 200`);
|
|
735
|
-
await assert_response_matches_spec(test_app.route_specs, route.method, route.path, res);
|
|
736
|
-
}
|
|
737
|
-
});
|
|
738
|
-
});
|
|
739
760
|
});
|
|
740
761
|
};
|