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