@fuzdev/fuz_app 0.11.0 → 0.13.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_codegen.d.ts.map +1 -1
- package/dist/actions/action_event.d.ts.map +1 -1
- package/dist/actions/action_event.js +6 -0
- package/dist/actions/action_event_data.d.ts.map +1 -1
- package/dist/actions/action_event_helpers.d.ts +1 -1
- package/dist/actions/action_event_helpers.d.ts.map +1 -1
- package/dist/actions/action_event_types.d.ts.map +1 -1
- package/dist/actions/action_peer.d.ts.map +1 -1
- package/dist/actions/request_tracker.svelte.d.ts.map +1 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports_http.d.ts.map +1 -1
- package/dist/actions/transports_ws.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/auth/account_routes.d.ts +30 -0
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +44 -9
- package/dist/auth/admin_routes.d.ts.map +1 -1
- package/dist/auth/admin_routes.js +33 -2
- package/dist/auth/audit_log_routes.d.ts +2 -1
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +11 -2
- package/dist/auth/audit_log_schema.d.ts +1 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +3 -1
- package/dist/auth/permit_queries.d.ts +19 -0
- package/dist/auth/permit_queries.d.ts.map +1 -1
- package/dist/auth/permit_queries.js +21 -0
- package/dist/auth/request_context.d.ts +10 -0
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +14 -0
- package/dist/hono_context.d.ts +7 -0
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/realtime/sse.d.ts +0 -2
- package/dist/realtime/sse.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.d.ts +23 -3
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +38 -2
- package/dist/realtime/subscriber_registry.d.ts +62 -17
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +64 -21
- package/dist/server/validate_nginx.d.ts.map +1 -1
- package/dist/server/validate_nginx.js +61 -7
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +8 -8
- package/dist/testing/app_server.d.ts +9 -0
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +4 -3
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +1 -20
- package/dist/testing/error_coverage.d.ts +93 -27
- package/dist/testing/error_coverage.d.ts.map +1 -1
- package/dist/testing/error_coverage.js +160 -67
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +6 -6
- package/dist/testing/integration_helpers.d.ts +17 -5
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +37 -1
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +41 -55
- package/dist/testing/sse_round_trip.d.ts +64 -0
- package/dist/testing/sse_round_trip.d.ts.map +1 -0
- package/dist/testing/sse_round_trip.js +241 -0
- package/dist/ui/AdminOverview.svelte +1 -0
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin_integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/admin_integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAiB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAA0B,KAAK,gBAAgB,EAAC,MAAM,wBAAwB,CAAC;AAGtF,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAUjB;;GAEG;AACH,MAAM,WAAW,mCAAmC;IACnD,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,4GAA4G;IAC5G,KAAK,EAAE,gBAAgB,CAAC;IACxB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAChC;AAgDD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,mCAAmC,KAC1C,
|
|
1
|
+
{"version":3,"file":"admin_integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/admin_integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAiB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAA0B,KAAK,gBAAgB,EAAC,MAAM,wBAAwB,CAAC;AAGtF,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAUjB;;GAEG;AACH,MAAM,WAAW,mCAAmC;IACnD,4CAA4C;IAC5C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,wDAAwD;IACxD,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,4GAA4G;IAC5G,KAAK,EAAE,gBAAgB,CAAC;IACxB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAChC;AAgDD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,mCAAmC,KAC1C,IAwnCF,CAAC"}
|
|
@@ -130,9 +130,9 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
130
130
|
headers: test_app.create_session_headers(),
|
|
131
131
|
});
|
|
132
132
|
assert.strictEqual(res.status, 403);
|
|
133
|
-
|
|
134
|
-
const body = await res.json();
|
|
133
|
+
const body = await res.clone().json();
|
|
135
134
|
assert.strictEqual(body.error, 'insufficient_permissions');
|
|
135
|
+
await error_collector.assert_and_record(test_app.route_specs, 'GET', accounts_route.path, res);
|
|
136
136
|
});
|
|
137
137
|
});
|
|
138
138
|
// --- 2. Permit grant lifecycle ---
|
|
@@ -167,9 +167,9 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
167
167
|
body: JSON.stringify({ role: ROLE_KEEPER }),
|
|
168
168
|
});
|
|
169
169
|
assert.strictEqual(res.status, 403);
|
|
170
|
-
|
|
171
|
-
const body = await res.json();
|
|
170
|
+
const body = await res.clone().json();
|
|
172
171
|
assert.strictEqual(body.error, 'role_not_web_grantable');
|
|
172
|
+
await error_collector.assert_and_record(test_app.route_specs, 'POST', grant_route.path, res);
|
|
173
173
|
});
|
|
174
174
|
test('granting same role twice is idempotent (returns same permit)', async () => {
|
|
175
175
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
@@ -227,9 +227,9 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
227
227
|
body: JSON.stringify({ role: grantable_role }),
|
|
228
228
|
});
|
|
229
229
|
assert.strictEqual(res.status, 404);
|
|
230
|
-
|
|
231
|
-
const body = await res.json();
|
|
230
|
+
const body = await res.clone().json();
|
|
232
231
|
assert.strictEqual(body.error, 'account_not_found');
|
|
232
|
+
await error_collector.assert_and_record(test_app.route_specs, 'POST', grant_route.path, res);
|
|
233
233
|
});
|
|
234
234
|
test('admin can revoke a permit', async () => {
|
|
235
235
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
@@ -309,9 +309,9 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
309
309
|
headers: test_app.create_session_headers(),
|
|
310
310
|
});
|
|
311
311
|
assert.strictEqual(second.status, 404);
|
|
312
|
-
|
|
313
|
-
const body = await second.json();
|
|
312
|
+
const body = await second.clone().json();
|
|
314
313
|
assert.strictEqual(body.error, 'permit_not_found');
|
|
314
|
+
await error_collector.assert_and_record(test_app.route_specs, 'POST', revoke_route.path, second);
|
|
315
315
|
});
|
|
316
316
|
});
|
|
317
317
|
// --- 3. Admin session management ---
|
|
@@ -17,6 +17,7 @@ import { type Keyring } from '../auth/keyring.js';
|
|
|
17
17
|
import type { Db, DbType } from '../db/db.js';
|
|
18
18
|
import type { PasswordHashDeps } from '../auth/password.js';
|
|
19
19
|
import { type SessionOptions } from '../auth/session_cookie.js';
|
|
20
|
+
import type { AuditLogEvent } from '../auth/audit_log_schema.js';
|
|
20
21
|
import type { AppBackend } from '../server/app_backend.js';
|
|
21
22
|
import { type AppServerOptions, type AppServerContext } from '../server/app_server.js';
|
|
22
23
|
import type { AppSurface, AppSurfaceSpec } from '../http/surface.js';
|
|
@@ -100,6 +101,14 @@ export interface TestAppServerOptions {
|
|
|
100
101
|
password_value?: string;
|
|
101
102
|
/** Roles to grant. Default: `[ROLE_KEEPER]`. */
|
|
102
103
|
roles?: Array<string>;
|
|
104
|
+
/**
|
|
105
|
+
* Backend audit event callback — wired into `backend.deps.on_audit_event`.
|
|
106
|
+
* When `audit_log_sse: true` is passed to `create_app_server`, this runs
|
|
107
|
+
* after the audit SSE broadcast (composed downstream by app_server).
|
|
108
|
+
* Use to wire consumer SSE auth guards in tests.
|
|
109
|
+
* Default: no-op.
|
|
110
|
+
*/
|
|
111
|
+
on_audit_event?: (event: AuditLogEvent) => void;
|
|
103
112
|
}
|
|
104
113
|
export declare const create_test_app_server: (options: TestAppServerOptions) => Promise<TestAppServer>;
|
|
105
114
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/app_server.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,MAAM,CAAC;AAK/B,OAAO,EAA2B,KAAK,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAE1E,OAAO,KAAK,EAAC,EAAE,EAAE,MAAM,EAAC,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,qBAAqB,CAAC;AAU1D,OAAO,EAA8B,KAAK,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAG3F,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAEN,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAC,UAAU,EAAE,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAUrD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,EAAE,gBAIhC,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,kBAAkB,QAAiB,CAAC;AASjD;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC3C,EAAE,EAAE,EAAE,CAAC;IACP,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,GAClC,SAAS,2BAA2B,KAClC,OAAO,CAAC;IACV,OAAO,EAAE;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC;IACxC,KAAK,EAAE;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACvB,CAyCA,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,UAAU;IAChD,gCAAgC;IAChC,OAAO,EAAE;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC;IACxC,uCAAuC;IACvC,KAAK,EAAE;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,CAAC;IACpB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAC;IACvB,+FAA+F;IAC/F,OAAO,EAAE,OAAO,CAAC;IACjB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,mDAAmD;IACnD,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,kGAAkG;IAClG,EAAE,CAAC,EAAE,EAAE,CAAC;IACR,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yHAAyH;IACzH,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/app_server.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,MAAM,CAAC;AAK/B,OAAO,EAA2B,KAAK,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAE1E,OAAO,KAAK,EAAC,EAAE,EAAE,MAAM,EAAC,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,qBAAqB,CAAC;AAU1D,OAAO,EAA8B,KAAK,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAG3F,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAEN,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAC,UAAU,EAAE,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAUrD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,EAAE,gBAIhC,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,kBAAkB,QAAiB,CAAC;AASjD;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC3C,EAAE,EAAE,EAAE,CAAC;IACP,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,GAClC,SAAS,2BAA2B,KAClC,OAAO,CAAC;IACV,OAAO,EAAE;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC;IACxC,KAAK,EAAE;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACvB,CAyCA,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,UAAU;IAChD,gCAAgC;IAChC,OAAO,EAAE;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC;IACxC,uCAAuC;IACvC,KAAK,EAAE;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,CAAC;IACpB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAC;IACvB,+FAA+F;IAC/F,OAAO,EAAE,OAAO,CAAC;IACjB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,mDAAmD;IACnD,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,kGAAkG;IAClG,EAAE,CAAC,EAAE,EAAE,CAAC;IACR,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yHAAyH;IACzH,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACtB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;CAChD;AAqBD,eAAO,MAAM,sBAAsB,GAClC,SAAS,oBAAoB,KAC3B,OAAO,CAAC,aAAa,CAuFvB,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,oBAAqB,SAAQ,oBAAoB;IACjE,yEAAyE;IACzE,kBAAkB,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IACpE,gHAAgH;IAChH,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;CACF;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAAC;IACxC,KAAK,EAAE;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,CAAC;IACpB,mCAAmC;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,sBAAsB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnF,8DAA8D;IAC9D,qBAAqB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClF;AAED;;GAEG;AACH,MAAM,WAAW,OAAO;IACvB,GAAG,EAAE,IAAI,CAAC;IACV,OAAO,EAAE,aAAa,CAAC;IACvB,YAAY,EAAE,cAAc,CAAC;IAC7B,OAAO,EAAE,UAAU,CAAC;IACpB,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAC9B,kEAAkE;IAClE,sBAAsB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnF,gEAAgE;IAChE,qBAAqB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClF,iEAAiE;IACjE,2BAA2B,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxF,qDAAqD;IACrD,cAAc,EAAE,CAAC,OAAO,CAAC,EAAE;QAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;KACtB,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IAC3B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,GAAU,SAAS,oBAAoB,KAAG,OAAO,CAAC,OAAO,CAkGpF,CAAC"}
|
|
@@ -88,7 +88,8 @@ export const bootstrap_test_account = async (options) => {
|
|
|
88
88
|
/** Silent logger for tests — suppresses all output. */
|
|
89
89
|
const test_log = new Logger('test', { level: 'off' });
|
|
90
90
|
export const create_test_app_server = async (options) => {
|
|
91
|
-
const { session_options, db: existing_db, db_type = 'pglite-memory', password = stub_password_deps, username = 'keeper', password_value = 'test-password-123', roles = [ROLE_KEEPER],
|
|
91
|
+
const { session_options, db: existing_db, db_type = 'pglite-memory', password = stub_password_deps, username = 'keeper', password_value = 'test-password-123', roles = [ROLE_KEEPER], on_audit_event = () => { }, // eslint-disable-line @typescript-eslint/no-empty-function
|
|
92
|
+
} = options;
|
|
92
93
|
// Keyring from test secret
|
|
93
94
|
const keyring_result = create_validated_keyring(TEST_COOKIE_SECRET);
|
|
94
95
|
if (!keyring_result.ok) {
|
|
@@ -117,7 +118,7 @@ export const create_test_app_server = async (options) => {
|
|
|
117
118
|
password,
|
|
118
119
|
db: existing_db,
|
|
119
120
|
log: test_log,
|
|
120
|
-
on_audit_event
|
|
121
|
+
on_audit_event,
|
|
121
122
|
...fs_stubs,
|
|
122
123
|
},
|
|
123
124
|
};
|
|
@@ -137,7 +138,7 @@ export const create_test_app_server = async (options) => {
|
|
|
137
138
|
password,
|
|
138
139
|
db,
|
|
139
140
|
log: test_log,
|
|
140
|
-
on_audit_event
|
|
141
|
+
on_audit_event,
|
|
141
142
|
...fs_stubs,
|
|
142
143
|
},
|
|
143
144
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"data_exposure.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/data_exposure.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAgB7B,OAAO,KAAK,EAAC,UAAU,EAAE,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAG9D,OAAO,EAAwB,KAAK,SAAS,EAAC,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"data_exposure.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/data_exposure.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAgB7B,OAAO,KAAK,EAAC,UAAU,EAAE,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAG9D,OAAO,EAAwB,KAAK,SAAS,EAAC,MAAM,SAAS,CAAC;AAe9D;;;;;;;;GAQG;AACH,eAAO,MAAM,kCAAkC,GAAI,QAAQ,OAAO,KAAG,GAAG,CAAC,MAAM,CAuB9E,CAAC;AAIF;;;;;GAKG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,UAAU,EACnB,mBAAkB,aAAa,CAAC,MAAM,CAA6B,KACjE,IAWF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,GACpD,SAAS,UAAU,EACnB,oBAAmB,aAAa,CAAC,MAAM,CAA8B,KACnE,IAcF,CAAC;AAIF,kDAAkD;AAClD,MAAM,WAAW,uBAAuB;IACvC,4DAA4D;IAC5D,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,wCAAwC;IACxC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,4CAA4C;IAC5C,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,2FAA2F;IAC3F,gBAAgB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACzC,iGAAiG;IACjG,iBAAiB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC1C,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CACpB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,iBAAiB,GAAG,oBAAoB,CAAC,CAC5E,CAAC;IACF,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAChC,kDAAkD;IAClD,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GAAI,SAAS,uBAAuB,KAAG,IAmC/E,CAAC"}
|
|
@@ -18,7 +18,7 @@ import { resolve_valid_path, generate_valid_body } from './schema_generators.js'
|
|
|
18
18
|
import { run_migrations } from '../db/migrate.js';
|
|
19
19
|
import { AUTH_MIGRATION_NS } from '../auth/migrations.js';
|
|
20
20
|
import { is_null_schema, is_strict_object_schema } from '../http/schema_helpers.js';
|
|
21
|
-
import { SENSITIVE_FIELD_BLOCKLIST, ADMIN_ONLY_FIELD_BLOCKLIST, assert_no_sensitive_fields_in_json, } from './integration_helpers.js';
|
|
21
|
+
import { SENSITIVE_FIELD_BLOCKLIST, ADMIN_ONLY_FIELD_BLOCKLIST, assert_no_sensitive_fields_in_json, pick_auth_headers, } from './integration_helpers.js';
|
|
22
22
|
// --- Schema introspection ---
|
|
23
23
|
/**
|
|
24
24
|
* Recursively collect all property names from a JSON Schema.
|
|
@@ -275,22 +275,3 @@ const describe_data_exposure_runtime_tests = (options) => {
|
|
|
275
275
|
});
|
|
276
276
|
}
|
|
277
277
|
};
|
|
278
|
-
/**
|
|
279
|
-
* Pick auth headers matching a route spec's auth requirement.
|
|
280
|
-
*/
|
|
281
|
-
const pick_auth_headers = (spec, test_app, authed_account, admin_account) => {
|
|
282
|
-
switch (spec.auth.type) {
|
|
283
|
-
case 'none':
|
|
284
|
-
return { host: 'localhost', origin: 'http://localhost:5173' };
|
|
285
|
-
case 'authenticated':
|
|
286
|
-
return authed_account.create_session_headers();
|
|
287
|
-
case 'role':
|
|
288
|
-
if (spec.auth.role === ROLE_ADMIN) {
|
|
289
|
-
return admin_account.create_session_headers();
|
|
290
|
-
}
|
|
291
|
-
// keeper role uses the bootstrapped account
|
|
292
|
-
return test_app.create_session_headers();
|
|
293
|
-
case 'keeper':
|
|
294
|
-
return test_app.create_daemon_token_headers();
|
|
295
|
-
}
|
|
296
|
-
};
|
|
@@ -1,52 +1,116 @@
|
|
|
1
1
|
import './assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Error reachability coverage tracking.
|
|
4
|
+
*
|
|
5
|
+
* Tracks which declared error statuses (and specific error codes) are
|
|
6
|
+
* actually exercised in tests. `ErrorCoverageCollector` records status
|
|
7
|
+
* codes (optionally with body `error` codes) observed during test runs,
|
|
8
|
+
* then `assert_error_coverage` compares against declared error schemas
|
|
9
|
+
* to find uncovered error paths — reporting per-code when the declared
|
|
10
|
+
* schema is a literal or enum, per-status otherwise.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod';
|
|
2
15
|
import type { RouteSpec } from '../http/route_spec.js';
|
|
3
16
|
/**
|
|
4
|
-
*
|
|
17
|
+
* Extract declared error code values from an error response schema.
|
|
18
|
+
*
|
|
19
|
+
* Recognizes schemas shaped like `z.object({error: z.literal(...)})` or
|
|
20
|
+
* `z.object({error: z.enum([...])})` (incl. `looseObject`/`strictObject`).
|
|
21
|
+
* Returns the set of declared code values, or `null` if the schema doesn't
|
|
22
|
+
* expose a literal/enum `error` field (e.g., bare `ApiError` with `z.string()`).
|
|
5
23
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
24
|
+
* Used by coverage reporting to split a single declared status into per-code
|
|
25
|
+
* rows when the route's error schema names specific codes.
|
|
26
|
+
*/
|
|
27
|
+
export declare const extract_declared_error_codes: (schema: z.ZodType) => Array<string> | null;
|
|
28
|
+
/** Uncovered entry — either a status-level row (no `code`) or a specific-code row. */
|
|
29
|
+
export interface UncoveredEntry {
|
|
30
|
+
method: string;
|
|
31
|
+
path: string;
|
|
32
|
+
status: number;
|
|
33
|
+
/** Declared code value missing, when the status's error schema names specific codes. */
|
|
34
|
+
code?: string;
|
|
35
|
+
}
|
|
36
|
+
/** Options controlling which routes/statuses are considered for coverage. */
|
|
37
|
+
export interface CoverageFilterOptions {
|
|
38
|
+
/** Routes to skip, in `'METHOD /path'` format. */
|
|
39
|
+
ignore_routes?: Array<string>;
|
|
40
|
+
/** HTTP status codes to skip. */
|
|
41
|
+
ignore_statuses?: Array<number>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Tracks which route × status (and route × status × code) combinations have
|
|
45
|
+
* been exercised in tests.
|
|
46
|
+
*
|
|
47
|
+
* Use `record()` to log an observed status (optionally with the body's `error`
|
|
48
|
+
* code), or `assert_and_record()` to combine response validation with tracking
|
|
49
|
+
* (auto-extracts `body.error` from the response when present).
|
|
50
|
+
* After all tests, call `uncovered()` to find declared error paths never
|
|
51
|
+
* exercised.
|
|
52
|
+
*
|
|
53
|
+
* An observation recorded without a code still satisfies "any-code" coverage
|
|
54
|
+
* requirements for the same status — i.e., if a caller records just the status,
|
|
55
|
+
* all declared codes for that status are considered covered. Per-code tracking
|
|
56
|
+
* is additive: callers who know the body's `error` value should pass it to get
|
|
57
|
+
* precise per-code gap reporting on routes with literal/enum error schemas.
|
|
9
58
|
*/
|
|
10
59
|
export declare class ErrorCoverageCollector {
|
|
11
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Observed keys: `"METHOD /spec-path:STATUS"` or `"METHOD /spec-path:STATUS:CODE"`.
|
|
62
|
+
*
|
|
63
|
+
* Both shapes coexist — the code-less key marks the status as covered at any
|
|
64
|
+
* code; a code-bearing key adds per-code precision.
|
|
65
|
+
*/
|
|
12
66
|
readonly observed: Set<string>;
|
|
13
67
|
/**
|
|
14
|
-
* Record an observed error status for a route.
|
|
68
|
+
* Record an observed error status (optionally with the body's `error` code) for a route.
|
|
15
69
|
*
|
|
16
70
|
* Resolves the concrete request path back to the spec template path
|
|
17
|
-
* (e.g., `/api/accounts/abc` → `/api/accounts/:id`).
|
|
71
|
+
* (e.g., `/api/accounts/abc` → `/api/accounts/:id`). When `code` is provided,
|
|
72
|
+
* it is stored alongside the status for per-code coverage tracking.
|
|
18
73
|
*
|
|
19
74
|
* @param route_specs - route specs for path resolution
|
|
20
75
|
* @param method - HTTP method
|
|
21
76
|
* @param path - request path (may be concrete)
|
|
22
77
|
* @param status - observed HTTP status code
|
|
78
|
+
* @param code - observed body `error` code (pass when the route's error
|
|
79
|
+
* schema declares specific codes via `z.literal` or `z.enum`)
|
|
23
80
|
*/
|
|
24
|
-
record(route_specs: Array<RouteSpec>, method: string, path: string, status: number): void;
|
|
81
|
+
record(route_specs: Array<RouteSpec>, method: string, path: string, status: number, code?: string): void;
|
|
25
82
|
/**
|
|
26
83
|
* Validate a response against its route spec and record the status.
|
|
27
84
|
*
|
|
28
|
-
* Wraps `assert_response_matches_spec` and records the status code.
|
|
85
|
+
* Wraps `assert_response_matches_spec` and records the status code. For
|
|
86
|
+
* error responses, auto-extracts `body.error` from the JSON body (via a
|
|
87
|
+
* cloned response, so the original stream stays usable) and records it
|
|
88
|
+
* for per-code coverage. Pass an explicit `code` to override the
|
|
89
|
+
* auto-extracted value or when the body was already consumed.
|
|
29
90
|
*
|
|
30
91
|
* @param route_specs - route specs for schema lookup and path resolution
|
|
31
92
|
* @param method - HTTP method
|
|
32
93
|
* @param path - request path
|
|
33
94
|
* @param response - the Response to validate and record
|
|
95
|
+
* @param code - observed body `error` code (override; if omitted and the
|
|
96
|
+
* response body is a JSON object with a string `error` field, that value
|
|
97
|
+
* is auto-extracted)
|
|
34
98
|
*/
|
|
35
|
-
assert_and_record(route_specs: Array<RouteSpec>, method: string, path: string, response: Response): Promise<void>;
|
|
99
|
+
assert_and_record(route_specs: Array<RouteSpec>, method: string, path: string, response: Response, code?: string): Promise<void>;
|
|
36
100
|
/**
|
|
37
|
-
* Find declared error
|
|
101
|
+
* Find declared error paths that were never observed.
|
|
38
102
|
*
|
|
39
|
-
* Computes the declared set from `merge_error_schemas` for each route spec
|
|
40
|
-
*
|
|
103
|
+
* Computes the declared set from `merge_error_schemas` for each route spec.
|
|
104
|
+
* For statuses whose error schema names specific codes (via `z.literal` or
|
|
105
|
+
* `z.enum`), reports per-code rows; otherwise reports one row per status.
|
|
106
|
+
* A status-only observation (no code) satisfies all declared codes for that
|
|
107
|
+
* status — the "any-code" rule.
|
|
41
108
|
*
|
|
42
109
|
* @param route_specs - route specs to check coverage against
|
|
43
|
-
* @
|
|
110
|
+
* @param options - exclusion configuration (skip routes or statuses)
|
|
111
|
+
* @returns uncovered entries with method, path, status, and optional code
|
|
44
112
|
*/
|
|
45
|
-
uncovered(route_specs: Array<RouteSpec
|
|
46
|
-
method: string;
|
|
47
|
-
path: string;
|
|
48
|
-
status: number;
|
|
49
|
-
}>;
|
|
113
|
+
uncovered(route_specs: Array<RouteSpec>, options?: CoverageFilterOptions): Array<UncoveredEntry>;
|
|
50
114
|
}
|
|
51
115
|
/**
|
|
52
116
|
* Default minimum error coverage threshold for the standard integration
|
|
@@ -55,20 +119,22 @@ export declare class ErrorCoverageCollector {
|
|
|
55
119
|
*/
|
|
56
120
|
export declare const DEFAULT_INTEGRATION_ERROR_COVERAGE = 0.2;
|
|
57
121
|
/** Options for `assert_error_coverage`. */
|
|
58
|
-
export interface ErrorCoverageOptions {
|
|
122
|
+
export interface ErrorCoverageOptions extends CoverageFilterOptions {
|
|
59
123
|
/** Minimum coverage ratio (0–1). Default `0` (informational only). */
|
|
60
124
|
min_coverage?: number;
|
|
61
|
-
/** Routes to skip, in `'METHOD /path'` format. */
|
|
62
|
-
ignore_routes?: Array<string>;
|
|
63
|
-
/** HTTP status codes to skip. */
|
|
64
|
-
ignore_statuses?: Array<number>;
|
|
65
125
|
}
|
|
66
126
|
/**
|
|
67
127
|
* Assert error coverage meets a minimum threshold.
|
|
68
128
|
*
|
|
69
|
-
* Computes the ratio of exercised error
|
|
70
|
-
*
|
|
71
|
-
*
|
|
129
|
+
* Computes the ratio of exercised error paths to total declared error paths.
|
|
130
|
+
* For routes whose status error schema names specific codes (`z.literal` or
|
|
131
|
+
* `z.enum`), each declared code counts as one coverage path; for schemas
|
|
132
|
+
* without declared codes (`ApiError`/`z.string()`), the status counts as one
|
|
133
|
+
* path. A status-only observation covers all declared codes for that status
|
|
134
|
+
* (the "any-code" rule).
|
|
135
|
+
*
|
|
136
|
+
* When `min_coverage` is 0 (default), logs coverage info without failing.
|
|
137
|
+
* When > 0, fails if coverage is below the threshold.
|
|
72
138
|
*
|
|
73
139
|
* @param collector - the coverage collector with recorded observations
|
|
74
140
|
* @param route_specs - route specs to check coverage against
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error_coverage.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/error_coverage.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"error_coverage.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/error_coverage.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;GAWG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAIrD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,KAAK,CAAC,MAAM,CAAC,GAAG,IAWhF,CAAC;AAEF,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,wFAAwF;IACxF,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,6EAA6E;AAC7E,MAAM,WAAW,qBAAqB;IACrC,kDAAkD;IAClD,aAAa,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC9B,iCAAiC;IACjC,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAChC;AAqDD;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,sBAAsB;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAa;IAE3C;;;;;;;;;;;;;OAaG;IACH,MAAM,CACL,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,EAC7B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM,GACX,IAAI;IAUP;;;;;;;;;;;;;;;;OAgBG;IACG,iBAAiB,CACtB,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,EAC7B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,EAClB,IAAI,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC;IAgBhB;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC,cAAc,CAAC;CAKhG;AAED;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,MAAM,CAAC;AAEtD,2CAA2C;AAC3C,MAAM,WAAW,oBAAqB,SAAQ,qBAAqB;IAClE,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAaD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,qBAAqB,GACjC,WAAW,sBAAsB,EACjC,aAAa,KAAK,CAAC,SAAS,CAAC,EAC7B,UAAU,oBAAoB,KAC5B,IAqBF,CAAC"}
|
|
@@ -2,80 +2,183 @@ import './assert_dev_env.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Error reachability coverage tracking.
|
|
4
4
|
*
|
|
5
|
-
* Tracks which declared error statuses
|
|
6
|
-
* `ErrorCoverageCollector` records status
|
|
5
|
+
* Tracks which declared error statuses (and specific error codes) are
|
|
6
|
+
* actually exercised in tests. `ErrorCoverageCollector` records status
|
|
7
|
+
* codes (optionally with body `error` codes) observed during test runs,
|
|
7
8
|
* then `assert_error_coverage` compares against declared error schemas
|
|
8
|
-
* to find uncovered error paths
|
|
9
|
+
* to find uncovered error paths — reporting per-code when the declared
|
|
10
|
+
* schema is a literal or enum, per-status otherwise.
|
|
9
11
|
*
|
|
10
12
|
* @module
|
|
11
13
|
*/
|
|
14
|
+
import { z } from 'zod';
|
|
12
15
|
import { assert } from 'vitest';
|
|
13
16
|
import { merge_error_schemas } from '../http/schema_helpers.js';
|
|
14
17
|
import { find_route_spec, assert_response_matches_spec } from './integration_helpers.js';
|
|
15
18
|
/**
|
|
16
|
-
*
|
|
19
|
+
* Extract declared error code values from an error response schema.
|
|
17
20
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
+
* Recognizes schemas shaped like `z.object({error: z.literal(...)})` or
|
|
22
|
+
* `z.object({error: z.enum([...])})` (incl. `looseObject`/`strictObject`).
|
|
23
|
+
* Returns the set of declared code values, or `null` if the schema doesn't
|
|
24
|
+
* expose a literal/enum `error` field (e.g., bare `ApiError` with `z.string()`).
|
|
25
|
+
*
|
|
26
|
+
* Used by coverage reporting to split a single declared status into per-code
|
|
27
|
+
* rows when the route's error schema names specific codes.
|
|
28
|
+
*/
|
|
29
|
+
export const extract_declared_error_codes = (schema) => {
|
|
30
|
+
if (!(schema instanceof z.ZodObject))
|
|
31
|
+
return null;
|
|
32
|
+
const error_field = schema.shape.error;
|
|
33
|
+
if (!error_field)
|
|
34
|
+
return null;
|
|
35
|
+
if (error_field instanceof z.ZodLiteral) {
|
|
36
|
+
return [...error_field.values].map(String);
|
|
37
|
+
}
|
|
38
|
+
if (error_field instanceof z.ZodEnum) {
|
|
39
|
+
return error_field.options.map(String);
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Shared walk over declared error paths.
|
|
45
|
+
*
|
|
46
|
+
* Single source of truth for the route → status → code traversal used by
|
|
47
|
+
* both `uncovered()` and `assert_error_coverage`. Yields one entry per
|
|
48
|
+
* declared coverage path with a `covered` flag, applying the "any-code"
|
|
49
|
+
* rule (status-only observation covers all declared codes).
|
|
50
|
+
*/
|
|
51
|
+
const walk_coverage = (collector, route_specs, options) => {
|
|
52
|
+
const ignore_routes = new Set(options?.ignore_routes);
|
|
53
|
+
const ignore_statuses = new Set(options?.ignore_statuses);
|
|
54
|
+
const entries = [];
|
|
55
|
+
for (const spec of route_specs) {
|
|
56
|
+
const route_key = `${spec.method} ${spec.path}`;
|
|
57
|
+
if (ignore_routes.has(route_key))
|
|
58
|
+
continue;
|
|
59
|
+
const merged = merge_error_schemas(spec);
|
|
60
|
+
if (!merged)
|
|
61
|
+
continue;
|
|
62
|
+
for (const status_str of Object.keys(merged)) {
|
|
63
|
+
const status = Number(status_str);
|
|
64
|
+
if (ignore_statuses.has(status))
|
|
65
|
+
continue;
|
|
66
|
+
const error_schema = merged[status];
|
|
67
|
+
if (!error_schema)
|
|
68
|
+
continue;
|
|
69
|
+
const status_key = `${spec.method} ${spec.path}:${status}`;
|
|
70
|
+
const status_observed = collector.observed.has(status_key);
|
|
71
|
+
const codes = extract_declared_error_codes(error_schema);
|
|
72
|
+
if (codes && codes.length > 0) {
|
|
73
|
+
for (const code of codes) {
|
|
74
|
+
const covered = status_observed || collector.observed.has(`${status_key}:${code}`);
|
|
75
|
+
entries.push({ method: spec.method, path: spec.path, status, code, covered });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
entries.push({ method: spec.method, path: spec.path, status, covered: status_observed });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return entries;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Tracks which route × status (and route × status × code) combinations have
|
|
87
|
+
* been exercised in tests.
|
|
88
|
+
*
|
|
89
|
+
* Use `record()` to log an observed status (optionally with the body's `error`
|
|
90
|
+
* code), or `assert_and_record()` to combine response validation with tracking
|
|
91
|
+
* (auto-extracts `body.error` from the response when present).
|
|
92
|
+
* After all tests, call `uncovered()` to find declared error paths never
|
|
93
|
+
* exercised.
|
|
94
|
+
*
|
|
95
|
+
* An observation recorded without a code still satisfies "any-code" coverage
|
|
96
|
+
* requirements for the same status — i.e., if a caller records just the status,
|
|
97
|
+
* all declared codes for that status are considered covered. Per-code tracking
|
|
98
|
+
* is additive: callers who know the body's `error` value should pass it to get
|
|
99
|
+
* precise per-code gap reporting on routes with literal/enum error schemas.
|
|
21
100
|
*/
|
|
22
101
|
export class ErrorCoverageCollector {
|
|
23
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Observed keys: `"METHOD /spec-path:STATUS"` or `"METHOD /spec-path:STATUS:CODE"`.
|
|
104
|
+
*
|
|
105
|
+
* Both shapes coexist — the code-less key marks the status as covered at any
|
|
106
|
+
* code; a code-bearing key adds per-code precision.
|
|
107
|
+
*/
|
|
24
108
|
observed = new Set();
|
|
25
109
|
/**
|
|
26
|
-
* Record an observed error status for a route.
|
|
110
|
+
* Record an observed error status (optionally with the body's `error` code) for a route.
|
|
27
111
|
*
|
|
28
112
|
* Resolves the concrete request path back to the spec template path
|
|
29
|
-
* (e.g., `/api/accounts/abc` → `/api/accounts/:id`).
|
|
113
|
+
* (e.g., `/api/accounts/abc` → `/api/accounts/:id`). When `code` is provided,
|
|
114
|
+
* it is stored alongside the status for per-code coverage tracking.
|
|
30
115
|
*
|
|
31
116
|
* @param route_specs - route specs for path resolution
|
|
32
117
|
* @param method - HTTP method
|
|
33
118
|
* @param path - request path (may be concrete)
|
|
34
119
|
* @param status - observed HTTP status code
|
|
120
|
+
* @param code - observed body `error` code (pass when the route's error
|
|
121
|
+
* schema declares specific codes via `z.literal` or `z.enum`)
|
|
35
122
|
*/
|
|
36
|
-
record(route_specs, method, path, status) {
|
|
123
|
+
record(route_specs, method, path, status, code) {
|
|
37
124
|
const spec = find_route_spec(route_specs, method, path);
|
|
38
125
|
const spec_path = spec ? spec.path : path;
|
|
39
|
-
|
|
126
|
+
const base_key = `${method} ${spec_path}:${status}`;
|
|
127
|
+
this.observed.add(base_key);
|
|
128
|
+
if (code !== undefined) {
|
|
129
|
+
this.observed.add(`${base_key}:${code}`);
|
|
130
|
+
}
|
|
40
131
|
}
|
|
41
132
|
/**
|
|
42
133
|
* Validate a response against its route spec and record the status.
|
|
43
134
|
*
|
|
44
|
-
* Wraps `assert_response_matches_spec` and records the status code.
|
|
135
|
+
* Wraps `assert_response_matches_spec` and records the status code. For
|
|
136
|
+
* error responses, auto-extracts `body.error` from the JSON body (via a
|
|
137
|
+
* cloned response, so the original stream stays usable) and records it
|
|
138
|
+
* for per-code coverage. Pass an explicit `code` to override the
|
|
139
|
+
* auto-extracted value or when the body was already consumed.
|
|
45
140
|
*
|
|
46
141
|
* @param route_specs - route specs for schema lookup and path resolution
|
|
47
142
|
* @param method - HTTP method
|
|
48
143
|
* @param path - request path
|
|
49
144
|
* @param response - the Response to validate and record
|
|
145
|
+
* @param code - observed body `error` code (override; if omitted and the
|
|
146
|
+
* response body is a JSON object with a string `error` field, that value
|
|
147
|
+
* is auto-extracted)
|
|
50
148
|
*/
|
|
51
|
-
async assert_and_record(route_specs, method, path, response) {
|
|
149
|
+
async assert_and_record(route_specs, method, path, response, code) {
|
|
52
150
|
await assert_response_matches_spec(route_specs, method, path, response);
|
|
53
|
-
|
|
151
|
+
let resolved_code = code;
|
|
152
|
+
if (resolved_code === undefined && !response.ok && !response.bodyUsed) {
|
|
153
|
+
try {
|
|
154
|
+
const body = await response.clone().json();
|
|
155
|
+
if (body && typeof body.error === 'string') {
|
|
156
|
+
resolved_code = body.error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// non-JSON body — no code to extract
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
this.record(route_specs, method, path, response.status, resolved_code);
|
|
54
164
|
}
|
|
55
165
|
/**
|
|
56
|
-
* Find declared error
|
|
166
|
+
* Find declared error paths that were never observed.
|
|
57
167
|
*
|
|
58
|
-
* Computes the declared set from `merge_error_schemas` for each route spec
|
|
59
|
-
*
|
|
168
|
+
* Computes the declared set from `merge_error_schemas` for each route spec.
|
|
169
|
+
* For statuses whose error schema names specific codes (via `z.literal` or
|
|
170
|
+
* `z.enum`), reports per-code rows; otherwise reports one row per status.
|
|
171
|
+
* A status-only observation (no code) satisfies all declared codes for that
|
|
172
|
+
* status — the "any-code" rule.
|
|
60
173
|
*
|
|
61
174
|
* @param route_specs - route specs to check coverage against
|
|
62
|
-
* @
|
|
175
|
+
* @param options - exclusion configuration (skip routes or statuses)
|
|
176
|
+
* @returns uncovered entries with method, path, status, and optional code
|
|
63
177
|
*/
|
|
64
|
-
uncovered(route_specs) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!merged)
|
|
69
|
-
continue;
|
|
70
|
-
for (const status_str of Object.keys(merged)) {
|
|
71
|
-
const status = Number(status_str);
|
|
72
|
-
const key = `${spec.method} ${spec.path}:${status}`;
|
|
73
|
-
if (!this.observed.has(key)) {
|
|
74
|
-
missing.push({ method: spec.method, path: spec.path, status });
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return missing;
|
|
178
|
+
uncovered(route_specs, options) {
|
|
179
|
+
return walk_coverage(this, route_specs, options)
|
|
180
|
+
.filter((entry) => !entry.covered)
|
|
181
|
+
.map(({ method, path, status, code }) => ({ method, path, status, ...(code && { code }) }));
|
|
79
182
|
}
|
|
80
183
|
}
|
|
81
184
|
/**
|
|
@@ -84,12 +187,25 @@ export class ErrorCoverageCollector {
|
|
|
84
187
|
* in the composable suites. Consumers should increase as their test suites mature.
|
|
85
188
|
*/
|
|
86
189
|
export const DEFAULT_INTEGRATION_ERROR_COVERAGE = 0.2;
|
|
190
|
+
/**
|
|
191
|
+
* Format an uncovered entry for human-readable log output.
|
|
192
|
+
*
|
|
193
|
+
* Uses `status (code)` — spaces around the code make `:` unambiguous as
|
|
194
|
+
* the route_key / status separator.
|
|
195
|
+
*/
|
|
196
|
+
const format_uncovered = (entry) => `${entry.method} ${entry.path} → ${entry.status}${entry.code ? ` (${entry.code})` : ''}`;
|
|
87
197
|
/**
|
|
88
198
|
* Assert error coverage meets a minimum threshold.
|
|
89
199
|
*
|
|
90
|
-
* Computes the ratio of exercised error
|
|
91
|
-
*
|
|
92
|
-
*
|
|
200
|
+
* Computes the ratio of exercised error paths to total declared error paths.
|
|
201
|
+
* For routes whose status error schema names specific codes (`z.literal` or
|
|
202
|
+
* `z.enum`), each declared code counts as one coverage path; for schemas
|
|
203
|
+
* without declared codes (`ApiError`/`z.string()`), the status counts as one
|
|
204
|
+
* path. A status-only observation covers all declared codes for that status
|
|
205
|
+
* (the "any-code" rule).
|
|
206
|
+
*
|
|
207
|
+
* When `min_coverage` is 0 (default), logs coverage info without failing.
|
|
208
|
+
* When > 0, fails if coverage is below the threshold.
|
|
93
209
|
*
|
|
94
210
|
* @param collector - the coverage collector with recorded observations
|
|
95
211
|
* @param route_specs - route specs to check coverage against
|
|
@@ -97,39 +213,16 @@ export const DEFAULT_INTEGRATION_ERROR_COVERAGE = 0.2;
|
|
|
97
213
|
*/
|
|
98
214
|
export const assert_error_coverage = (collector, route_specs, options) => {
|
|
99
215
|
const min_coverage = options?.min_coverage ?? 0;
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
for (const spec of route_specs) {
|
|
106
|
-
const route_key = `${spec.method} ${spec.path}`;
|
|
107
|
-
if (ignore_routes.has(route_key))
|
|
108
|
-
continue;
|
|
109
|
-
const merged = merge_error_schemas(spec);
|
|
110
|
-
if (!merged)
|
|
111
|
-
continue;
|
|
112
|
-
for (const status_str of Object.keys(merged)) {
|
|
113
|
-
const status = Number(status_str);
|
|
114
|
-
if (ignore_statuses.has(status))
|
|
115
|
-
continue;
|
|
116
|
-
total++;
|
|
117
|
-
const key = `${spec.method} ${spec.path}:${status}`;
|
|
118
|
-
if (collector.observed.has(key)) {
|
|
119
|
-
covered++;
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
uncovered_entries.push(`${route_key} → ${status}`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
216
|
+
const entries = walk_coverage(collector, route_specs, options);
|
|
217
|
+
const total = entries.length;
|
|
218
|
+
const uncovered_entries = entries.filter((e) => !e.covered);
|
|
219
|
+
const covered = total - uncovered_entries.length;
|
|
220
|
+
const uncovered_lines = uncovered_entries.map(format_uncovered);
|
|
126
221
|
const ratio = total > 0 ? covered / total : 1;
|
|
127
222
|
console.log(`[error coverage] ${covered}/${total} (${(ratio * 100).toFixed(1)}%)` +
|
|
128
|
-
(
|
|
129
|
-
? `\n uncovered:\n ${uncovered_entries.join('\n ')}`
|
|
130
|
-
: ''));
|
|
223
|
+
(uncovered_lines.length > 0 ? `\n uncovered:\n ${uncovered_lines.join('\n ')}` : ''));
|
|
131
224
|
if (min_coverage > 0) {
|
|
132
225
|
assert.ok(ratio >= min_coverage, `Error coverage ${(ratio * 100).toFixed(1)}% below threshold ${(min_coverage * 100).toFixed(1)}%` +
|
|
133
|
-
`\n uncovered:\n ${
|
|
226
|
+
`\n uncovered:\n ${uncovered_lines.join('\n ')}`);
|
|
134
227
|
}
|
|
135
228
|
};
|