@fuzdev/fuz_app 0.12.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/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_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 -0
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +31 -0
- 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/package.json +1 -1
|
@@ -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
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAsB7B,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;AAGrD,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAgBjB;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C,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,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;AAeD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mCAAmC,GAC/C,SAAS,8BAA8B,KACrC,
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAsB7B,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;AAGrD,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,SAAS,CAAC;AAgBjB;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C,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,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;AAeD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mCAAmC,GAC/C,SAAS,8BAA8B,KACrC,IAm/CF,CAAC"}
|
|
@@ -127,9 +127,9 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
127
127
|
}),
|
|
128
128
|
});
|
|
129
129
|
assert.strictEqual(res.status, 401);
|
|
130
|
-
|
|
131
|
-
const body = await res.json();
|
|
130
|
+
const body = await res.clone().json();
|
|
132
131
|
assert.strictEqual(body.error, 'invalid_credentials');
|
|
132
|
+
await error_collector.assert_and_record(test_app.route_specs, 'POST', login_route.path, res);
|
|
133
133
|
});
|
|
134
134
|
test('login with nonexistent user returns 401', async () => {
|
|
135
135
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
@@ -148,9 +148,9 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
148
148
|
}),
|
|
149
149
|
});
|
|
150
150
|
assert.strictEqual(res.status, 401);
|
|
151
|
-
|
|
152
|
-
const body = await res.json();
|
|
151
|
+
const body = await res.clone().json();
|
|
153
152
|
assert.strictEqual(body.error, 'invalid_credentials');
|
|
153
|
+
await error_collector.assert_and_record(test_app.route_specs, 'POST', login_route.path, res);
|
|
154
154
|
});
|
|
155
155
|
test('login trims whitespace from username', async () => {
|
|
156
156
|
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
@@ -821,9 +821,9 @@ export const describe_standard_integration_tests = (options) => {
|
|
|
821
821
|
// third attempt should be rate-limited
|
|
822
822
|
const limited_res = await make_bad_login();
|
|
823
823
|
assert.strictEqual(limited_res.status, 429, 'Expected 429 after exceeding rate limit');
|
|
824
|
-
|
|
825
|
-
const limited_body = await limited_res.json();
|
|
824
|
+
const limited_body = await limited_res.clone().json();
|
|
826
825
|
assert.strictEqual(limited_body.error, 'rate_limit_exceeded');
|
|
826
|
+
await error_collector.assert_and_record(test_app.route_specs, 'POST', login_route.path, limited_res);
|
|
827
827
|
// Retry-After header present
|
|
828
828
|
const retry_after = limited_res.headers.get('Retry-After');
|
|
829
829
|
assert.ok(retry_after, 'Expected Retry-After header on 429 response');
|
|
@@ -2,6 +2,7 @@ import './assert_dev_env.js';
|
|
|
2
2
|
import type { RouteSpec, RouteMethod } from '../http/route_spec.js';
|
|
3
3
|
import type { Keyring } from '../auth/keyring.js';
|
|
4
4
|
import { type SessionOptions } from '../auth/session_cookie.js';
|
|
5
|
+
import type { TestApp, TestAccount } from './app_server.js';
|
|
5
6
|
/**
|
|
6
7
|
* Find a route spec matching the given method and path.
|
|
7
8
|
*
|
|
@@ -99,4 +100,20 @@ export declare const collect_json_keys_recursive: (value: unknown) => Set<string
|
|
|
99
100
|
* @param context - description for error messages
|
|
100
101
|
*/
|
|
101
102
|
export declare const assert_no_sensitive_fields_in_json: (body: unknown, blocklist: ReadonlyArray<string>, context: string) => void;
|
|
103
|
+
/**
|
|
104
|
+
* Pick request headers matching a route spec's auth requirement.
|
|
105
|
+
*
|
|
106
|
+
* Maps `RouteAuth` onto a test account's credentials:
|
|
107
|
+
* - `none` — origin headers only
|
|
108
|
+
* - `authenticated` — the authed account's session cookie
|
|
109
|
+
* - `role: admin` — the admin account's session cookie
|
|
110
|
+
* - `role: <other>` — the test app's bootstrapped keeper session
|
|
111
|
+
* - `keeper` — the test app's daemon token
|
|
112
|
+
*
|
|
113
|
+
* @param spec - route spec to inspect
|
|
114
|
+
* @param test_app - the assembled test app (for bootstrapped credentials)
|
|
115
|
+
* @param authed_account - an account with no roles (for `authenticated` auth)
|
|
116
|
+
* @param admin_account - an account with `admin` role (for role-gated routes)
|
|
117
|
+
*/
|
|
118
|
+
export declare const pick_auth_headers: (spec: RouteSpec, test_app: TestApp, authed_account: TestAccount, admin_account: TestAccount) => Record<string, string>;
|
|
102
119
|
//# sourceMappingURL=integration_helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAU7B,OAAO,KAAK,EAAC,SAAS,EAAE,WAAW,EAAC,MAAM,uBAAuB,CAAC;AAElE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAA8B,KAAK,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAE3F;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAC3B,OAAO,KAAK,CAAC,SAAS,CAAC,EACvB,QAAQ,MAAM,EACd,MAAM,MAAM,KACV,SAAS,GAAG,SAad,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,GAC3B,OAAO,KAAK,CAAC,SAAS,CAAC,EACvB,QAAQ,MAAM,EACd,QAAQ,WAAW,KACjB,SAAS,GAAG,SAEd,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,GACxC,aAAa,KAAK,CAAC,SAAS,CAAC,EAC7B,QAAQ,MAAM,EACd,MAAM,MAAM,EACZ,UAAU,QAAQ,KAChB,OAAO,CAAC,IAAI,CAmDd,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,GACtC,SAAS,OAAO,EAChB,iBAAiB,cAAc,CAAC,MAAM,CAAC,KACrC,OAAO,CAAC,MAAM,CAGhB,CAAC;AAuCF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,KAAK,CAAC,MAAM,CAQvF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,SAAS,MAAM,KACb,IAkBF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oCAAoC,GAChD,UAAU,QAAQ,EAClB,MAAM;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC,KACzB,IAUF,CAAC;AAIF,oEAAoE;AACpE,eAAO,MAAM,yBAAyB,EAAE,aAAa,CAAC,MAAM,CAAmC,CAAC;AAEhG,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,EAAE,aAAa,CAAC,MAAM,CAAgC,CAAC;AAE9F;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,GAAI,OAAO,OAAO,KAAG,GAAG,CAAC,MAAM,CAetE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,kCAAkC,GAC9C,MAAM,OAAO,EACb,WAAW,aAAa,CAAC,MAAM,CAAC,EAChC,SAAS,MAAM,KACb,IAKF,CAAC"}
|
|
1
|
+
{"version":3,"file":"integration_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAU7B,OAAO,KAAK,EAAC,SAAS,EAAE,WAAW,EAAC,MAAM,uBAAuB,CAAC;AAElE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAA8B,KAAK,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAE3F,OAAO,KAAK,EAAC,OAAO,EAAE,WAAW,EAAC,MAAM,iBAAiB,CAAC;AAE1D;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAC3B,OAAO,KAAK,CAAC,SAAS,CAAC,EACvB,QAAQ,MAAM,EACd,MAAM,MAAM,KACV,SAAS,GAAG,SAad,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,GAC3B,OAAO,KAAK,CAAC,SAAS,CAAC,EACvB,QAAQ,MAAM,EACd,QAAQ,WAAW,KACjB,SAAS,GAAG,SAEd,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,GACxC,aAAa,KAAK,CAAC,SAAS,CAAC,EAC7B,QAAQ,MAAM,EACd,MAAM,MAAM,EACZ,UAAU,QAAQ,KAChB,OAAO,CAAC,IAAI,CAmDd,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,GACtC,SAAS,OAAO,EAChB,iBAAiB,cAAc,CAAC,MAAM,CAAC,KACrC,OAAO,CAAC,MAAM,CAGhB,CAAC;AAuCF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,KAAK,CAAC,MAAM,CAQvF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,SAAS,MAAM,KACb,IAkBF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oCAAoC,GAChD,UAAU,QAAQ,EAClB,MAAM;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC,KACzB,IAUF,CAAC;AAIF,oEAAoE;AACpE,eAAO,MAAM,yBAAyB,EAAE,aAAa,CAAC,MAAM,CAAmC,CAAC;AAEhG,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,EAAE,aAAa,CAAC,MAAM,CAAgC,CAAC;AAE9F;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,GAAI,OAAO,OAAO,KAAG,GAAG,CAAC,MAAM,CAetE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,kCAAkC,GAC9C,MAAM,OAAO,EACb,WAAW,aAAa,CAAC,MAAM,CAAC,EAChC,SAAS,MAAM,KACb,IAKF,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,SAAS,EACf,UAAU,OAAO,EACjB,gBAAgB,WAAW,EAC3B,eAAe,WAAW,KACxB,MAAM,CAAC,MAAM,EAAE,MAAM,CAcvB,CAAC"}
|
|
@@ -7,6 +7,7 @@ import './assert_dev_env.js';
|
|
|
7
7
|
import { assert } from 'vitest';
|
|
8
8
|
import { is_null_schema, merge_error_schemas } from '../http/schema_helpers.js';
|
|
9
9
|
import { create_session_cookie_value } from '../auth/session_cookie.js';
|
|
10
|
+
import { ROLE_ADMIN } from '../auth/role_schema.js';
|
|
10
11
|
/**
|
|
11
12
|
* Find a route spec matching the given method and path.
|
|
12
13
|
*
|
|
@@ -249,3 +250,33 @@ export const assert_no_sensitive_fields_in_json = (body, blocklist, context) =>
|
|
|
249
250
|
assert.ok(!keys.has(field), `${context}: response contains blocklisted field '${field}'`);
|
|
250
251
|
}
|
|
251
252
|
};
|
|
253
|
+
/**
|
|
254
|
+
* Pick request headers matching a route spec's auth requirement.
|
|
255
|
+
*
|
|
256
|
+
* Maps `RouteAuth` onto a test account's credentials:
|
|
257
|
+
* - `none` — origin headers only
|
|
258
|
+
* - `authenticated` — the authed account's session cookie
|
|
259
|
+
* - `role: admin` — the admin account's session cookie
|
|
260
|
+
* - `role: <other>` — the test app's bootstrapped keeper session
|
|
261
|
+
* - `keeper` — the test app's daemon token
|
|
262
|
+
*
|
|
263
|
+
* @param spec - route spec to inspect
|
|
264
|
+
* @param test_app - the assembled test app (for bootstrapped credentials)
|
|
265
|
+
* @param authed_account - an account with no roles (for `authenticated` auth)
|
|
266
|
+
* @param admin_account - an account with `admin` role (for role-gated routes)
|
|
267
|
+
*/
|
|
268
|
+
export const pick_auth_headers = (spec, test_app, authed_account, admin_account) => {
|
|
269
|
+
switch (spec.auth.type) {
|
|
270
|
+
case 'none':
|
|
271
|
+
return { host: 'localhost', origin: 'http://localhost:5173' };
|
|
272
|
+
case 'authenticated':
|
|
273
|
+
return authed_account.create_session_headers();
|
|
274
|
+
case 'role':
|
|
275
|
+
if (spec.auth.role === ROLE_ADMIN) {
|
|
276
|
+
return admin_account.create_session_headers();
|
|
277
|
+
}
|
|
278
|
+
return test_app.create_session_headers();
|
|
279
|
+
case 'keeper':
|
|
280
|
+
return test_app.create_daemon_token_headers();
|
|
281
|
+
}
|
|
282
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAc7B,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":"round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAc7B,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;AAQ9D,oDAAoD;AACpD,MAAM,WAAW,oBAAoB;IACpC,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,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;IAC5B,+EAA+E;IAC/E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,8BAA8B,GAAI,SAAS,oBAAoB,KAAG,IA6F9E,CAAC"}
|