@fuzdev/fuz_app 0.33.0 → 0.34.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/testing/admin_integration.d.ts +7 -5
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +146 -145
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +34 -31
- package/dist/testing/integration.d.ts +2 -2
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +187 -124
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +25 -1
- package/package.json +1 -1
|
@@ -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
|
*/
|
|
@@ -107,15 +122,15 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
107
122
|
* `describe_rpc_round_trip_tests` + fuz_app's own action suite.
|
|
108
123
|
*/
|
|
109
124
|
const offer_and_accept = async (args) => {
|
|
110
|
-
const res = await
|
|
125
|
+
const res = await rpc_call_for_spec({
|
|
111
126
|
app: args.app,
|
|
112
127
|
path: rpc_path,
|
|
113
|
-
|
|
128
|
+
spec: permit_offer_create_action_spec,
|
|
114
129
|
params: { to_account_id: args.to_account_id, role: args.role },
|
|
115
130
|
headers: args.admin_headers,
|
|
116
131
|
});
|
|
117
132
|
assert.ok(res.ok, `permit_offer_create failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
118
|
-
const offer = res.result
|
|
133
|
+
const { offer } = res.result;
|
|
119
134
|
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
135
|
return { offer_id: offer.id, permit_id: accept_result.permit.id };
|
|
121
136
|
};
|
|
@@ -124,28 +139,29 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
124
139
|
test('admin can list all accounts', async () => {
|
|
125
140
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
126
141
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
127
|
-
const res = await
|
|
142
|
+
const res = await rpc_call_for_spec({
|
|
128
143
|
app: test_app.app,
|
|
129
144
|
path: rpc_path,
|
|
130
|
-
|
|
145
|
+
spec: admin_account_list_action_spec,
|
|
146
|
+
params: null,
|
|
131
147
|
headers: test_app.create_session_headers(),
|
|
132
148
|
});
|
|
133
149
|
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');
|
|
150
|
+
assert.ok(Array.isArray(res.result.accounts), 'Expected accounts array');
|
|
151
|
+
assert.ok(res.result.accounts.length >= 2, 'Expected at least 2 accounts');
|
|
152
|
+
assert.ok(Array.isArray(res.result.grantable_roles), 'Expected grantable_roles array');
|
|
138
153
|
// Verify user_two appears in the listing
|
|
139
|
-
const found = result.accounts.find((e) => e.account.id === user_two.account.id);
|
|
154
|
+
const found = res.result.accounts.find((e) => e.account.id === user_two.account.id);
|
|
140
155
|
assert.ok(found, 'Expected user_two in accounts listing');
|
|
141
156
|
});
|
|
142
157
|
test('non-admin cannot list accounts', async () => {
|
|
143
158
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db(), [ROLE_KEEPER]));
|
|
144
159
|
captured_route_specs ??= test_app.route_specs;
|
|
145
|
-
const res = await
|
|
160
|
+
const res = await rpc_call_for_spec({
|
|
146
161
|
app: test_app.app,
|
|
147
162
|
path: rpc_path,
|
|
148
|
-
|
|
163
|
+
spec: admin_account_list_action_spec,
|
|
164
|
+
params: null,
|
|
149
165
|
headers: test_app.create_session_headers(),
|
|
150
166
|
});
|
|
151
167
|
assert.ok(!res.ok, 'Expected admin_account_list to fail for non-admin');
|
|
@@ -165,45 +181,46 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
165
181
|
test('admin can list all active sessions', async () => {
|
|
166
182
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
167
183
|
await test_app.create_account({ username: 'user_two' });
|
|
168
|
-
const res = await
|
|
184
|
+
const res = await rpc_call_for_spec({
|
|
169
185
|
app: test_app.app,
|
|
170
186
|
path: rpc_path,
|
|
171
|
-
|
|
187
|
+
spec: admin_session_list_action_spec,
|
|
188
|
+
params: null,
|
|
172
189
|
headers: test_app.create_session_headers(),
|
|
173
190
|
});
|
|
174
191
|
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');
|
|
192
|
+
assert.ok(Array.isArray(res.result.sessions), 'Expected sessions array');
|
|
193
|
+
assert.ok(res.result.sessions.length >= 2, 'Expected sessions from multiple accounts');
|
|
178
194
|
});
|
|
179
195
|
test('admin can revoke all sessions for another account', async () => {
|
|
180
196
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
181
197
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
182
198
|
// Verify user_two's session works via `account_verify` RPC
|
|
183
|
-
const before = await
|
|
199
|
+
const before = await rpc_call_for_spec({
|
|
184
200
|
app: test_app.app,
|
|
185
201
|
path: rpc_path,
|
|
186
|
-
|
|
202
|
+
spec: account_verify_action_spec,
|
|
203
|
+
params: null,
|
|
187
204
|
headers: create_headers(user_two.session_cookie),
|
|
188
205
|
});
|
|
189
206
|
assert.strictEqual(before.status, 200);
|
|
190
207
|
// Admin revokes all sessions for user_two via RPC
|
|
191
|
-
const res = await
|
|
208
|
+
const res = await rpc_call_for_spec({
|
|
192
209
|
app: test_app.app,
|
|
193
210
|
path: rpc_path,
|
|
194
|
-
|
|
211
|
+
spec: admin_session_revoke_all_action_spec,
|
|
195
212
|
params: { account_id: user_two.account.id },
|
|
196
213
|
headers: test_app.create_session_headers(),
|
|
197
214
|
});
|
|
198
215
|
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');
|
|
216
|
+
assert.strictEqual(res.result.ok, true);
|
|
217
|
+
assert.ok(res.result.count >= 1, 'Expected at least 1 revoked session');
|
|
202
218
|
// Verify user_two's session no longer works
|
|
203
|
-
const after = await
|
|
219
|
+
const after = await rpc_call_for_spec({
|
|
204
220
|
app: test_app.app,
|
|
205
221
|
path: rpc_path,
|
|
206
|
-
|
|
222
|
+
spec: account_verify_action_spec,
|
|
223
|
+
params: null,
|
|
207
224
|
headers: create_headers(user_two.session_cookie),
|
|
208
225
|
});
|
|
209
226
|
assert.strictEqual(after.status, 401);
|
|
@@ -211,22 +228,22 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
211
228
|
test('admin revoking own sessions invalidates own session', async () => {
|
|
212
229
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
213
230
|
// Admin revokes own sessions via RPC
|
|
214
|
-
const res = await
|
|
231
|
+
const res = await rpc_call_for_spec({
|
|
215
232
|
app: test_app.app,
|
|
216
233
|
path: rpc_path,
|
|
217
|
-
|
|
234
|
+
spec: admin_session_revoke_all_action_spec,
|
|
218
235
|
params: { account_id: test_app.backend.account.id },
|
|
219
236
|
headers: test_app.create_session_headers(),
|
|
220
237
|
});
|
|
221
238
|
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');
|
|
239
|
+
assert.strictEqual(res.result.ok, true);
|
|
240
|
+
assert.ok(res.result.count >= 1, 'Expected at least 1 revoked session');
|
|
225
241
|
// Admin's own session should no longer work
|
|
226
|
-
const after = await
|
|
242
|
+
const after = await rpc_call_for_spec({
|
|
227
243
|
app: test_app.app,
|
|
228
244
|
path: rpc_path,
|
|
229
|
-
|
|
245
|
+
spec: account_verify_action_spec,
|
|
246
|
+
params: null,
|
|
230
247
|
headers: test_app.create_session_headers(),
|
|
231
248
|
});
|
|
232
249
|
assert.strictEqual(after.status, 401);
|
|
@@ -238,31 +255,34 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
238
255
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
239
256
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
240
257
|
// Verify user_two's bearer token works via `account_verify` RPC
|
|
241
|
-
const before = await
|
|
258
|
+
const before = await rpc_call_for_spec({
|
|
242
259
|
app: test_app.app,
|
|
243
260
|
path: rpc_path,
|
|
244
|
-
|
|
261
|
+
spec: account_verify_action_spec,
|
|
262
|
+
params: null,
|
|
245
263
|
headers: { authorization: `Bearer ${user_two.api_token}` },
|
|
264
|
+
suppress_default_origin: true,
|
|
246
265
|
});
|
|
247
266
|
assert.strictEqual(before.status, 200);
|
|
248
267
|
// Admin revokes all tokens for user_two via RPC
|
|
249
|
-
const res = await
|
|
268
|
+
const res = await rpc_call_for_spec({
|
|
250
269
|
app: test_app.app,
|
|
251
270
|
path: rpc_path,
|
|
252
|
-
|
|
271
|
+
spec: admin_token_revoke_all_action_spec,
|
|
253
272
|
params: { account_id: user_two.account.id },
|
|
254
273
|
headers: test_app.create_session_headers(),
|
|
255
274
|
});
|
|
256
275
|
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');
|
|
276
|
+
assert.strictEqual(res.result.ok, true);
|
|
277
|
+
assert.ok(res.result.count >= 1, 'Expected at least 1 revoked token');
|
|
260
278
|
// Verify user_two's bearer token no longer works
|
|
261
|
-
const after = await
|
|
279
|
+
const after = await rpc_call_for_spec({
|
|
262
280
|
app: test_app.app,
|
|
263
281
|
path: rpc_path,
|
|
264
|
-
|
|
282
|
+
spec: account_verify_action_spec,
|
|
283
|
+
params: null,
|
|
265
284
|
headers: { authorization: `Bearer ${user_two.api_token}` },
|
|
285
|
+
suppress_default_origin: true,
|
|
266
286
|
});
|
|
267
287
|
assert.strictEqual(after.status, 401);
|
|
268
288
|
});
|
|
@@ -271,40 +291,39 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
271
291
|
describe('audit log RPC reads', () => {
|
|
272
292
|
test('admin can list audit log events', async () => {
|
|
273
293
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
274
|
-
const res = await
|
|
294
|
+
const res = await rpc_call_for_spec({
|
|
275
295
|
app: test_app.app,
|
|
276
296
|
path: rpc_path,
|
|
277
|
-
|
|
297
|
+
spec: audit_log_list_action_spec,
|
|
298
|
+
params: {},
|
|
278
299
|
headers: test_app.create_session_headers(),
|
|
279
300
|
});
|
|
280
301
|
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');
|
|
302
|
+
assert.ok(Array.isArray(res.result.events), 'Expected events array');
|
|
283
303
|
});
|
|
284
304
|
test('audit log supports event_type filter', async () => {
|
|
285
305
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
286
306
|
// Admin offer emits `permit_offer_create`. The downstream
|
|
287
307
|
// `permit_grant` only fires on accept — out of scope for this test.
|
|
288
308
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
289
|
-
const offer_res = await
|
|
309
|
+
const offer_res = await rpc_call_for_spec({
|
|
290
310
|
app: test_app.app,
|
|
291
311
|
path: rpc_path,
|
|
292
|
-
|
|
312
|
+
spec: permit_offer_create_action_spec,
|
|
293
313
|
params: { to_account_id: user_two.account.id, role: grantable_role },
|
|
294
314
|
headers: test_app.create_session_headers(),
|
|
295
315
|
});
|
|
296
316
|
assert.ok(offer_res.ok, 'permit_offer_create should succeed');
|
|
297
|
-
const res = await
|
|
317
|
+
const res = await rpc_call_for_spec({
|
|
298
318
|
app: test_app.app,
|
|
299
319
|
path: rpc_path,
|
|
300
|
-
|
|
320
|
+
spec: audit_log_list_action_spec,
|
|
301
321
|
params: { event_type: 'permit_offer_create' },
|
|
302
322
|
headers: test_app.create_session_headers(),
|
|
303
323
|
});
|
|
304
324
|
assert.ok(res.ok, `audit_log_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
for (const event of body.events) {
|
|
325
|
+
assert.ok(res.result.events.length >= 1, 'Expected at least 1 permit_offer_create event');
|
|
326
|
+
for (const event of res.result.events) {
|
|
308
327
|
assert.strictEqual(event.event_type, 'permit_offer_create');
|
|
309
328
|
}
|
|
310
329
|
});
|
|
@@ -319,15 +338,15 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
319
338
|
to_account_id: user_two.account.id,
|
|
320
339
|
role: grantable_role,
|
|
321
340
|
});
|
|
322
|
-
const res = await
|
|
341
|
+
const res = await rpc_call_for_spec({
|
|
323
342
|
app: test_app.app,
|
|
324
343
|
path: rpc_path,
|
|
325
|
-
|
|
344
|
+
spec: audit_log_permit_history_action_spec,
|
|
345
|
+
params: {},
|
|
326
346
|
headers: test_app.create_session_headers(),
|
|
327
347
|
});
|
|
328
348
|
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');
|
|
349
|
+
assert.ok(res.result.events.length >= 1, 'Expected at least 1 permit history event');
|
|
331
350
|
});
|
|
332
351
|
});
|
|
333
352
|
// --- 6. Admin audit trail ---
|
|
@@ -343,86 +362,83 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
343
362
|
granted_by: test_app.backend.actor.id,
|
|
344
363
|
});
|
|
345
364
|
// Revoke via RPC
|
|
346
|
-
const revoke_res = await
|
|
365
|
+
const revoke_res = await rpc_call_for_spec({
|
|
347
366
|
app: test_app.app,
|
|
348
367
|
path: rpc_path,
|
|
349
|
-
|
|
368
|
+
spec: permit_revoke_action_spec,
|
|
350
369
|
params: { actor_id: target_actor.id, permit_id: permit.id },
|
|
351
370
|
headers: test_app.create_session_headers(),
|
|
352
371
|
});
|
|
353
372
|
assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
354
373
|
// Check audit log for permit_revoke event
|
|
355
|
-
const audit_res = await
|
|
374
|
+
const audit_res = await rpc_call_for_spec({
|
|
356
375
|
app: test_app.app,
|
|
357
376
|
path: rpc_path,
|
|
358
|
-
|
|
377
|
+
spec: audit_log_list_action_spec,
|
|
359
378
|
params: { event_type: 'permit_revoke' },
|
|
360
379
|
headers: test_app.create_session_headers(),
|
|
361
380
|
});
|
|
362
381
|
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');
|
|
382
|
+
assert.ok(audit_res.result.events.length >= 1, 'Expected permit_revoke audit event');
|
|
383
|
+
assert.strictEqual(audit_res.result.events[0].event_type, 'permit_revoke');
|
|
366
384
|
});
|
|
367
385
|
test('admin session revoke-all creates audit event', async () => {
|
|
368
386
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
369
387
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
370
388
|
// Revoke all sessions for user_two via RPC
|
|
371
|
-
const revoke_res = await
|
|
389
|
+
const revoke_res = await rpc_call_for_spec({
|
|
372
390
|
app: test_app.app,
|
|
373
391
|
path: rpc_path,
|
|
374
|
-
|
|
392
|
+
spec: admin_session_revoke_all_action_spec,
|
|
375
393
|
params: { account_id: user_two.account.id },
|
|
376
394
|
headers: test_app.create_session_headers(),
|
|
377
395
|
});
|
|
378
396
|
assert.ok(revoke_res.ok, `admin_session_revoke_all failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
379
397
|
// Check audit log
|
|
380
|
-
const audit_res = await
|
|
398
|
+
const audit_res = await rpc_call_for_spec({
|
|
381
399
|
app: test_app.app,
|
|
382
400
|
path: rpc_path,
|
|
383
|
-
|
|
401
|
+
spec: audit_log_list_action_spec,
|
|
384
402
|
params: { event_type: 'session_revoke_all' },
|
|
385
403
|
headers: test_app.create_session_headers(),
|
|
386
404
|
});
|
|
387
405
|
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');
|
|
406
|
+
assert.ok(audit_res.result.events.length >= 1, 'Expected session_revoke_all audit event');
|
|
407
|
+
assert.strictEqual(audit_res.result.events[0].event_type, 'session_revoke_all');
|
|
391
408
|
});
|
|
392
409
|
test('admin token revoke-all creates audit event', async () => {
|
|
393
410
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
394
411
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
395
412
|
// Revoke all tokens for user_two via RPC
|
|
396
|
-
const revoke_res = await
|
|
413
|
+
const revoke_res = await rpc_call_for_spec({
|
|
397
414
|
app: test_app.app,
|
|
398
415
|
path: rpc_path,
|
|
399
|
-
|
|
416
|
+
spec: admin_token_revoke_all_action_spec,
|
|
400
417
|
params: { account_id: user_two.account.id },
|
|
401
418
|
headers: test_app.create_session_headers(),
|
|
402
419
|
});
|
|
403
420
|
assert.ok(revoke_res.ok, `admin_token_revoke_all failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
404
421
|
// Check audit log
|
|
405
|
-
const audit_res = await
|
|
422
|
+
const audit_res = await rpc_call_for_spec({
|
|
406
423
|
app: test_app.app,
|
|
407
424
|
path: rpc_path,
|
|
408
|
-
|
|
425
|
+
spec: audit_log_list_action_spec,
|
|
409
426
|
params: { event_type: 'token_revoke_all' },
|
|
410
427
|
headers: test_app.create_session_headers(),
|
|
411
428
|
});
|
|
412
429
|
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');
|
|
430
|
+
assert.ok(audit_res.result.events.length >= 1, 'Expected token_revoke_all audit event');
|
|
431
|
+
assert.strictEqual(audit_res.result.events[0].event_type, 'token_revoke_all');
|
|
416
432
|
});
|
|
417
433
|
test('admin session revoke-all 404 emits failure audit', async () => {
|
|
418
434
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
419
435
|
// `Uuid = z.uuid()` is v4-strict; use a valid v4 shape so we hit the
|
|
420
436
|
// handler's account lookup rather than failing at param validation.
|
|
421
437
|
const missing_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaa01';
|
|
422
|
-
const res = await
|
|
438
|
+
const res = await rpc_call_for_spec({
|
|
423
439
|
app: test_app.app,
|
|
424
440
|
path: rpc_path,
|
|
425
|
-
|
|
441
|
+
spec: admin_session_revoke_all_action_spec,
|
|
426
442
|
params: { account_id: missing_id },
|
|
427
443
|
headers: test_app.create_session_headers(),
|
|
428
444
|
});
|
|
@@ -432,48 +448,48 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
432
448
|
// Failure audit row should be visible on the audit-log feed.
|
|
433
449
|
// `target_account_id` is null (FK prevents referencing a missing id)
|
|
434
450
|
// — the probed id is preserved under `metadata.attempted_account_id`.
|
|
435
|
-
const audit_res = await
|
|
451
|
+
const audit_res = await rpc_call_for_spec({
|
|
436
452
|
app: test_app.app,
|
|
437
453
|
path: rpc_path,
|
|
438
|
-
|
|
454
|
+
spec: audit_log_list_action_spec,
|
|
439
455
|
params: { event_type: 'session_revoke_all' },
|
|
440
456
|
headers: test_app.create_session_headers(),
|
|
441
457
|
});
|
|
442
458
|
assert.ok(audit_res.ok, 'audit_log_list should succeed');
|
|
443
|
-
const
|
|
444
|
-
const failure = audit_body.events.find((e) => e.outcome === 'failure');
|
|
459
|
+
const failure = audit_res.result.events.find((e) => e.outcome === 'failure');
|
|
445
460
|
assert.ok(failure, 'Expected a failure-outcome session_revoke_all audit event');
|
|
446
461
|
assert.strictEqual(failure.target_account_id, null);
|
|
447
|
-
|
|
448
|
-
assert.strictEqual(
|
|
462
|
+
const failure_meta = failure.metadata;
|
|
463
|
+
assert.strictEqual(failure_meta.reason, 'account_not_found');
|
|
464
|
+
assert.strictEqual(failure_meta.attempted_account_id, missing_id);
|
|
449
465
|
});
|
|
450
466
|
test('admin token revoke-all 404 emits failure audit', async () => {
|
|
451
467
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
452
468
|
const missing_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaa02';
|
|
453
|
-
const res = await
|
|
469
|
+
const res = await rpc_call_for_spec({
|
|
454
470
|
app: test_app.app,
|
|
455
471
|
path: rpc_path,
|
|
456
|
-
|
|
472
|
+
spec: admin_token_revoke_all_action_spec,
|
|
457
473
|
params: { account_id: missing_id },
|
|
458
474
|
headers: test_app.create_session_headers(),
|
|
459
475
|
});
|
|
460
476
|
assert.ok(!res.ok, 'Expected 404 for missing account');
|
|
461
477
|
assert.strictEqual(res.status, 404);
|
|
462
478
|
assert.strictEqual(res.error.data.reason, 'account_not_found');
|
|
463
|
-
const audit_res = await
|
|
479
|
+
const audit_res = await rpc_call_for_spec({
|
|
464
480
|
app: test_app.app,
|
|
465
481
|
path: rpc_path,
|
|
466
|
-
|
|
482
|
+
spec: audit_log_list_action_spec,
|
|
467
483
|
params: { event_type: 'token_revoke_all' },
|
|
468
484
|
headers: test_app.create_session_headers(),
|
|
469
485
|
});
|
|
470
486
|
assert.ok(audit_res.ok, 'audit_log_list should succeed');
|
|
471
|
-
const
|
|
472
|
-
const failure = audit_body.events.find((e) => e.outcome === 'failure');
|
|
487
|
+
const failure = audit_res.result.events.find((e) => e.outcome === 'failure');
|
|
473
488
|
assert.ok(failure, 'Expected a failure-outcome token_revoke_all audit event');
|
|
474
489
|
assert.strictEqual(failure.target_account_id, null);
|
|
475
|
-
|
|
476
|
-
assert.strictEqual(
|
|
490
|
+
const failure_meta = failure.metadata;
|
|
491
|
+
assert.strictEqual(failure_meta.reason, 'account_not_found');
|
|
492
|
+
assert.strictEqual(failure_meta.attempted_account_id, missing_id);
|
|
477
493
|
});
|
|
478
494
|
});
|
|
479
495
|
// --- 7. Audit log completeness ---
|
|
@@ -527,19 +543,19 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
527
543
|
// 4. revoke permit (RPC)
|
|
528
544
|
const target_actor = await query_actor_by_account({ db: get_db() }, user_two.account.id);
|
|
529
545
|
assert.ok(target_actor);
|
|
530
|
-
const revoke_res = await
|
|
546
|
+
const revoke_res = await rpc_call_for_spec({
|
|
531
547
|
app: test_app.app,
|
|
532
548
|
path: rpc_path,
|
|
533
|
-
|
|
549
|
+
spec: permit_revoke_action_spec,
|
|
534
550
|
params: { actor_id: target_actor.id, permit_id },
|
|
535
551
|
headers: test_app.create_session_headers(),
|
|
536
552
|
});
|
|
537
553
|
assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
538
554
|
// 5. create token (RPC)
|
|
539
|
-
const token_res = await
|
|
555
|
+
const token_res = await rpc_call_for_spec({
|
|
540
556
|
app: test_app.app,
|
|
541
557
|
path: rpc_path,
|
|
542
|
-
|
|
558
|
+
spec: account_token_create_action_spec,
|
|
543
559
|
params: { name: 'audit-test-token' },
|
|
544
560
|
headers: test_app.create_session_headers(),
|
|
545
561
|
});
|
|
@@ -578,15 +594,15 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
578
594
|
origin: 'http://localhost:5173',
|
|
579
595
|
cookie: `${cookie_name}=${relogin_match[1]}`,
|
|
580
596
|
};
|
|
581
|
-
const audit_res = await
|
|
597
|
+
const audit_res = await rpc_call_for_spec({
|
|
582
598
|
app: test_app.app,
|
|
583
599
|
path: rpc_path,
|
|
584
|
-
|
|
600
|
+
spec: audit_log_list_action_spec,
|
|
601
|
+
params: {},
|
|
585
602
|
headers: relogin_headers,
|
|
586
603
|
});
|
|
587
604
|
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;
|
|
605
|
+
const events = audit_res.result.events;
|
|
590
606
|
// check that each operation produced at least one event.
|
|
591
607
|
// `permit_offer_create` fires on the admin RPC; `permit_grant`
|
|
592
608
|
// fires when the recipient accepts (driven by offer_and_accept).
|
|
@@ -625,16 +641,15 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
625
641
|
granted_by: test_app.backend.actor.id,
|
|
626
642
|
});
|
|
627
643
|
// Admin B revokes their own permit via RPC — should succeed
|
|
628
|
-
const revoke_res = await
|
|
644
|
+
const revoke_res = await rpc_call_for_spec({
|
|
629
645
|
app: test_app.app,
|
|
630
646
|
path: rpc_path,
|
|
631
|
-
|
|
647
|
+
spec: permit_revoke_action_spec,
|
|
632
648
|
params: { actor_id: admin_b.actor.id, permit_id: permit.id },
|
|
633
649
|
headers: create_headers(admin_b.session_cookie),
|
|
634
650
|
});
|
|
635
651
|
assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
636
|
-
|
|
637
|
-
assert.strictEqual(result.revoked, true);
|
|
652
|
+
assert.strictEqual(revoke_res.result.revoked, true);
|
|
638
653
|
});
|
|
639
654
|
test('admin revoke-all sessions for another admin works', async () => {
|
|
640
655
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
@@ -643,17 +658,16 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
643
658
|
roles: ['admin'],
|
|
644
659
|
});
|
|
645
660
|
// Admin A revokes all of admin B's sessions via RPC
|
|
646
|
-
const res = await
|
|
661
|
+
const res = await rpc_call_for_spec({
|
|
647
662
|
app: test_app.app,
|
|
648
663
|
path: rpc_path,
|
|
649
|
-
|
|
664
|
+
spec: admin_session_revoke_all_action_spec,
|
|
650
665
|
params: { account_id: admin_b.account.id },
|
|
651
666
|
headers: create_headers(test_app.backend.session_cookie),
|
|
652
667
|
});
|
|
653
668
|
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');
|
|
669
|
+
assert.ok(typeof res.result.count === 'number', 'Expected count field in response');
|
|
670
|
+
assert.ok(res.result.count >= 1, 'Expected at least 1 session revoked');
|
|
657
671
|
});
|
|
658
672
|
test('admin revoke-all tokens for another admin works', async () => {
|
|
659
673
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
@@ -662,36 +676,36 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
662
676
|
roles: ['admin'],
|
|
663
677
|
});
|
|
664
678
|
// Admin B creates an API token via RPC
|
|
665
|
-
const token_res = await
|
|
679
|
+
const token_res = await rpc_call_for_spec({
|
|
666
680
|
app: test_app.app,
|
|
667
681
|
path: rpc_path,
|
|
668
|
-
|
|
682
|
+
spec: account_token_create_action_spec,
|
|
669
683
|
params: { name: 'admin-b-token' },
|
|
670
684
|
headers: create_headers(admin_b.session_cookie),
|
|
671
685
|
});
|
|
672
686
|
assert.ok(token_res.ok, `account_token_create failed: ${token_res.ok ? '' : JSON.stringify(token_res.error)}`);
|
|
673
687
|
// Admin A revokes all of admin B's tokens via RPC
|
|
674
|
-
const res = await
|
|
688
|
+
const res = await rpc_call_for_spec({
|
|
675
689
|
app: test_app.app,
|
|
676
690
|
path: rpc_path,
|
|
677
|
-
|
|
691
|
+
spec: admin_token_revoke_all_action_spec,
|
|
678
692
|
params: { account_id: admin_b.account.id },
|
|
679
693
|
headers: create_headers(test_app.backend.session_cookie),
|
|
680
694
|
});
|
|
681
695
|
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');
|
|
696
|
+
assert.ok(typeof res.result.count === 'number', 'Expected count field in response');
|
|
684
697
|
// Token was created above, so revoke should evict at least one.
|
|
685
|
-
assert.ok(result.count >= 1, 'Expected at least 1 token revoked');
|
|
698
|
+
assert.ok(res.result.count >= 1, 'Expected at least 1 token revoked');
|
|
686
699
|
});
|
|
687
700
|
test('non-admin cannot access admin routes for another account', async () => {
|
|
688
701
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
689
702
|
const regular_user = await test_app.create_account({ username: 'regular_user_iso' });
|
|
690
703
|
// Regular user tries to list accounts via the admin RPC — should 403
|
|
691
|
-
const res = await
|
|
704
|
+
const res = await rpc_call_for_spec({
|
|
692
705
|
app: test_app.app,
|
|
693
706
|
path: rpc_path,
|
|
694
|
-
|
|
707
|
+
spec: admin_account_list_action_spec,
|
|
708
|
+
params: null,
|
|
695
709
|
headers: create_headers(regular_user.session_cookie),
|
|
696
710
|
});
|
|
697
711
|
assert.ok(!res.ok, 'Expected admin_account_list to fail for non-admin');
|
|
@@ -703,10 +717,16 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
703
717
|
test('exercises 401/403 on admin routes for error coverage', async () => {
|
|
704
718
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
705
719
|
captured_route_specs ??= test_app.route_specs;
|
|
720
|
+
// Post-RPC migration, `/api/admin` is nearly empty — admin reads
|
|
721
|
+
// and mutations live on the RPC endpoint behind spec-level role
|
|
722
|
+
// auth. The path-prefix carve is still the right scope here
|
|
723
|
+
// because error coverage is tracked against REST `RouteSpec`s,
|
|
724
|
+
// not RPC method specs (`describe_rpc_round_trip_tests` covers
|
|
725
|
+
// the admin RPC surface separately).
|
|
706
726
|
const prefix = options.admin_prefix ?? '/api/admin';
|
|
707
727
|
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
|
|
728
|
+
// Hit admin routes without auth to exercise 401 error schemas.
|
|
729
|
+
for (const route of admin_routes) {
|
|
710
730
|
const res = await test_app.app.request(route.path, {
|
|
711
731
|
method: route.method,
|
|
712
732
|
headers: { host: 'localhost' },
|
|
@@ -717,24 +737,5 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
717
737
|
}
|
|
718
738
|
});
|
|
719
739
|
});
|
|
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
740
|
});
|
|
740
741
|
};
|