@fuzdev/fuz_app 0.6.0 → 0.7.1
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/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +21 -7
- package/dist/testing/app_server.d.ts +2 -1
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +1 -0
- package/dist/testing/data_exposure.js +2 -3
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +2 -3
- package/dist/testing/rpc_attack_surface.d.ts +23 -0
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -0
- package/dist/testing/rpc_attack_surface.js +376 -0
- package/dist/testing/rpc_helpers.d.ts +44 -0
- package/dist/testing/rpc_helpers.d.ts.map +1 -0
- package/dist/testing/rpc_helpers.js +74 -0
- package/dist/testing/rpc_round_trip.d.ts +41 -0
- package/dist/testing/rpc_round_trip.d.ts.map +1 -0
- package/dist/testing/rpc_round_trip.js +162 -0
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +30 -2
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/schema_helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAuB,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAC,MAAM,oBAAoB,CAAC;AAEnG;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAAsC,CAAC;AAE1F;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OACe,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,
|
|
1
|
+
{"version":3,"file":"schema_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/schema_helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAuB,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAC,MAAM,oBAAoB,CAAC;AAEnG;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAAsC,CAAC;AAE1F;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OACe,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAQrD,CAAC;AAoBF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,EAAE,YAAY,MAAM,KAAG,OAQxE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM;IACL,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACpB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC3B,EACD,oBAAoB,iBAAiB,GAAG,IAAI,KAC1C,iBAAiB,GAAG,IAUtB,CAAC"}
|
|
@@ -34,18 +34,32 @@ export const schema_to_surface = (schema) => {
|
|
|
34
34
|
return null;
|
|
35
35
|
try {
|
|
36
36
|
const json_schema = z.toJSONSchema(schema);
|
|
37
|
-
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
39
|
-
if (typeof json_schema === 'object' && json_schema !== null && '$schema' in json_schema) {
|
|
40
|
-
const { $schema: _, ...rest } = json_schema;
|
|
41
|
-
return rest;
|
|
42
|
-
}
|
|
43
|
-
return json_schema;
|
|
37
|
+
return strip_json_schema_noise(json_schema);
|
|
44
38
|
}
|
|
45
39
|
catch {
|
|
46
40
|
return null;
|
|
47
41
|
}
|
|
48
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* Recursively strip `$schema` and `default` from a JSON Schema value.
|
|
45
|
+
*
|
|
46
|
+
* `$schema` is noise for snapshots. `default` can be non-deterministic
|
|
47
|
+
* when schemas use function defaults (e.g. `z.string().default(() => new Date().toISOString())`),
|
|
48
|
+
* and defaults are runtime behavior, not attack surface structure.
|
|
49
|
+
*/
|
|
50
|
+
const strip_json_schema_noise = (value) => {
|
|
51
|
+
if (typeof value !== 'object' || value === null)
|
|
52
|
+
return value;
|
|
53
|
+
if (Array.isArray(value))
|
|
54
|
+
return value.map(strip_json_schema_noise);
|
|
55
|
+
const result = {};
|
|
56
|
+
for (const [k, v] of Object.entries(value)) {
|
|
57
|
+
if (k === '$schema' || k === 'default')
|
|
58
|
+
continue;
|
|
59
|
+
result[k] = strip_json_schema_noise(v);
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
};
|
|
49
63
|
/**
|
|
50
64
|
* Check if a middleware path pattern applies to a route path.
|
|
51
65
|
*
|
|
@@ -19,7 +19,7 @@ import type { PasswordHashDeps } from '../auth/password.js';
|
|
|
19
19
|
import { type SessionOptions } from '../auth/session_cookie.js';
|
|
20
20
|
import type { AppBackend } from '../server/app_backend.js';
|
|
21
21
|
import { type AppServerOptions, type AppServerContext } from '../server/app_server.js';
|
|
22
|
-
import type { AppSurface } from '../http/surface.js';
|
|
22
|
+
import type { AppSurface, AppSurfaceSpec } from '../http/surface.js';
|
|
23
23
|
import type { RouteSpec } from '../http/route_spec.js';
|
|
24
24
|
/**
|
|
25
25
|
* Fast password stub for tests that don't exercise login/password flows.
|
|
@@ -137,6 +137,7 @@ export interface TestAccount {
|
|
|
137
137
|
export interface TestApp {
|
|
138
138
|
app: Hono;
|
|
139
139
|
backend: TestAppServer;
|
|
140
|
+
surface_spec: AppSurfaceSpec;
|
|
140
141
|
surface: AppSurface;
|
|
141
142
|
route_specs: Array<RouteSpec>;
|
|
142
143
|
/** Build request headers with the bootstrapped session cookie. */
|
|
@@ -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,EAAC,MAAM,oBAAoB,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,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;AAKrD;;;;;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;CACtB;AAqBD,eAAO,MAAM,sBAAsB,GAClC,SAAS,oBAAoB,KAC3B,OAAO,CAAC,aAAa,CAsFvB,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,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,CAgFpF,CAAC"}
|
|
@@ -135,9 +135,8 @@ const describe_data_exposure_runtime_tests = (options) => {
|
|
|
135
135
|
};
|
|
136
136
|
const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
|
|
137
137
|
for (const factory of factories) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return;
|
|
138
|
+
const describe_fn = factory.skip ? describe.skip : describe;
|
|
139
|
+
describe_fn(`data exposure — runtime (${factory.name})`, () => {
|
|
141
140
|
let test_app;
|
|
142
141
|
let authed_account;
|
|
143
142
|
let admin_account;
|
|
@@ -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;AAO9D,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,
|
|
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;AAO9D,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,IAqF9E,CAAC"}
|
|
@@ -38,9 +38,8 @@ export const describe_round_trip_validation = (options) => {
|
|
|
38
38
|
};
|
|
39
39
|
const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
|
|
40
40
|
for (const factory of factories) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return;
|
|
41
|
+
const describe_fn = factory.skip ? describe.skip : describe;
|
|
42
|
+
describe_fn(`round-trip validation (${factory.name})`, () => {
|
|
44
43
|
let test_app;
|
|
45
44
|
let authed_account;
|
|
46
45
|
let admin_account;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
import type { AppSurfaceSpec } from '../http/surface.js';
|
|
3
|
+
/** Options for `describe_rpc_attack_surface_tests`. */
|
|
4
|
+
export interface RpcAttackSurfaceOptions {
|
|
5
|
+
/** Build the app surface bundle (surface + route specs + middleware specs + rpc_endpoints). */
|
|
6
|
+
build: () => AppSurfaceSpec;
|
|
7
|
+
/** All roles in the app (e.g. `['admin', 'keeper']`). */
|
|
8
|
+
roles: Array<string>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Run the standard RPC attack surface test suite.
|
|
12
|
+
*
|
|
13
|
+
* Generates 3 test groups:
|
|
14
|
+
* 1. Auth enforcement — per-method auth checks via JSON-RPC envelopes
|
|
15
|
+
* 2. Adversarial envelopes — malformed JSON-RPC requests
|
|
16
|
+
* 3. Adversarial params — schema-invalid params per method
|
|
17
|
+
*
|
|
18
|
+
* Skips silently when `surface.rpc_endpoints` is empty.
|
|
19
|
+
*
|
|
20
|
+
* @param options - the test configuration
|
|
21
|
+
*/
|
|
22
|
+
export declare const describe_rpc_attack_surface_tests: (options: RpcAttackSurfaceOptions) => void;
|
|
23
|
+
//# sourceMappingURL=rpc_attack_surface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc_attack_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_attack_surface.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAkB7B,OAAO,KAAK,EAA6C,cAAc,EAAC,MAAM,oBAAoB,CAAC;AAenG,uDAAuD;AACvD,MAAM,WAAW,uBAAuB;IACvC,+FAA+F;IAC/F,KAAK,EAAE,MAAM,cAAc,CAAC;IAC5B,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACrB;AAkaD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iCAAiC,GAAI,SAAS,uBAAuB,KAAG,IAOpF,CAAC"}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Composable RPC attack surface test suite.
|
|
4
|
+
*
|
|
5
|
+
* Three test groups for JSON-RPC 2.0 endpoints:
|
|
6
|
+
* 1. **Auth enforcement** — per-method auth inside the dispatcher
|
|
7
|
+
* 2. **Adversarial envelopes** — malformed JSON-RPC requests
|
|
8
|
+
* 3. **Adversarial params** — schema-invalid params per method
|
|
9
|
+
*
|
|
10
|
+
* Uses the same `{build, roles}` config as `describe_adversarial_auth`
|
|
11
|
+
* and `describe_adversarial_input`. No DB needed — uses stub deps.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { test, assert, describe } from 'vitest';
|
|
16
|
+
import { JSONRPC_ERROR_CODES } from '../http/jsonrpc_errors.js';
|
|
17
|
+
import { create_auth_test_apps, select_auth_app } from './auth_apps.js';
|
|
18
|
+
import { generate_input_test_cases } from './adversarial_input.js';
|
|
19
|
+
import { ERROR_INVALID_JSON_BODY } from '../http/error_schemas.js';
|
|
20
|
+
import { create_rpc_post_init, create_rpc_get_url, assert_jsonrpc_error_response, } from './rpc_helpers.js';
|
|
21
|
+
// --- Helpers ---
|
|
22
|
+
/** Filter RPC methods that require any form of authentication. */
|
|
23
|
+
const filter_protected_rpc_methods = (endpoint) => endpoint.methods.filter((m) => m.auth.type !== 'none');
|
|
24
|
+
/** Filter RPC methods that require a specific role. */
|
|
25
|
+
const filter_role_rpc_methods = (endpoint) => endpoint.methods.filter((m) => m.auth.type === 'role');
|
|
26
|
+
/** Find the `RpcAction` source spec for a surface method. */
|
|
27
|
+
const find_rpc_action = (rpc_endpoint_specs, endpoint_path, method_name) => {
|
|
28
|
+
const ep = rpc_endpoint_specs.find((e) => e.path === endpoint_path);
|
|
29
|
+
return ep?.actions.find((a) => a.spec.method === method_name);
|
|
30
|
+
};
|
|
31
|
+
// --- Auth enforcement ---
|
|
32
|
+
/**
|
|
33
|
+
* Generate adversarial auth enforcement tests for RPC endpoints.
|
|
34
|
+
*
|
|
35
|
+
* For each endpoint, iterates methods with auth requirements and fires
|
|
36
|
+
* JSON-RPC envelopes with wrong/missing credentials. Auth errors are
|
|
37
|
+
* JSON-RPC format: `{jsonrpc, id, error: {code, message}}`.
|
|
38
|
+
*
|
|
39
|
+
* Describe blocks:
|
|
40
|
+
* - unauthenticated → error code -32001 — every protected method
|
|
41
|
+
* - wrong role → error code -32002 — every role method with non-matching roles
|
|
42
|
+
* - authenticated without role → -32002 — every role method, no-role context
|
|
43
|
+
* - keeper routes reject session credential → -32002
|
|
44
|
+
* - correct auth passes — every protected method, assert not 401/403
|
|
45
|
+
*/
|
|
46
|
+
const describe_rpc_auth = (options) => {
|
|
47
|
+
const { build, roles } = options;
|
|
48
|
+
const { surface, route_specs } = build();
|
|
49
|
+
if (surface.rpc_endpoints.length === 0)
|
|
50
|
+
return;
|
|
51
|
+
const apps = create_auth_test_apps(route_specs, roles);
|
|
52
|
+
describe('RPC auth enforcement', () => {
|
|
53
|
+
for (const endpoint of surface.rpc_endpoints) {
|
|
54
|
+
const protected_methods = filter_protected_rpc_methods(endpoint);
|
|
55
|
+
if (protected_methods.length === 0)
|
|
56
|
+
continue;
|
|
57
|
+
const role_methods = filter_role_rpc_methods(endpoint);
|
|
58
|
+
describe(endpoint.path, () => {
|
|
59
|
+
describe('unauthenticated → JSON-RPC error', () => {
|
|
60
|
+
for (const method of protected_methods) {
|
|
61
|
+
test(`${method.name} (${format_auth(method.auth)})`, async () => {
|
|
62
|
+
const res = await apps.public.request(endpoint.path, create_rpc_post_init(method.name));
|
|
63
|
+
assert.strictEqual(res.status, 401, `${method.name} should return 401`);
|
|
64
|
+
const body = await res.json();
|
|
65
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.unauthenticated);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
if (role_methods.length > 0) {
|
|
70
|
+
describe('wrong role → forbidden', () => {
|
|
71
|
+
for (const method of role_methods) {
|
|
72
|
+
const wrong_roles = roles.filter((r) => r !== method.auth.role);
|
|
73
|
+
for (const wrong_role of wrong_roles) {
|
|
74
|
+
test(`${method.name} (${wrong_role} instead of ${method.auth.role})`, async () => {
|
|
75
|
+
const app = apps.by_role.get(wrong_role);
|
|
76
|
+
if (!app)
|
|
77
|
+
throw new Error(`No test app for role '${wrong_role}'`);
|
|
78
|
+
const res = await app.request(endpoint.path, create_rpc_post_init(method.name));
|
|
79
|
+
assert.strictEqual(res.status, 403, `${method.name} should return 403`);
|
|
80
|
+
const body = await res.json();
|
|
81
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.forbidden);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
describe('authenticated without role → forbidden', () => {
|
|
87
|
+
for (const method of role_methods) {
|
|
88
|
+
test(`${method.name} (${method.auth.role})`, async () => {
|
|
89
|
+
const res = await apps.authed.request(endpoint.path, create_rpc_post_init(method.name));
|
|
90
|
+
assert.strictEqual(res.status, 403, `${method.name} should return 403`);
|
|
91
|
+
const body = await res.json();
|
|
92
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.forbidden);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// NOTE: no "keeper rejects session credential" test for RPC — the RPC
|
|
98
|
+
// dispatcher's check_action_auth only checks role, not credential type.
|
|
99
|
+
// Credential type enforcement is a REST middleware concern (require_keeper).
|
|
100
|
+
describe('correct auth passes', () => {
|
|
101
|
+
for (const method of protected_methods) {
|
|
102
|
+
test(method.name, async () => {
|
|
103
|
+
const app = select_auth_app(apps, method.auth);
|
|
104
|
+
const res = await app.request(endpoint.path, create_rpc_post_init(method.name));
|
|
105
|
+
// handler may error (500, 404 from stub deps) — that's fine
|
|
106
|
+
assert.notStrictEqual(res.status, 401, 'should not be 401');
|
|
107
|
+
assert.notStrictEqual(res.status, 403, 'should not be 403');
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
// also test GET for read methods with auth
|
|
112
|
+
const protected_reads = protected_methods.filter((m) => !m.side_effects);
|
|
113
|
+
if (protected_reads.length > 0) {
|
|
114
|
+
describe('GET unauthenticated → JSON-RPC error', () => {
|
|
115
|
+
for (const method of protected_reads) {
|
|
116
|
+
test(`${method.name} (GET)`, async () => {
|
|
117
|
+
const url = create_rpc_get_url(endpoint.path, method.name);
|
|
118
|
+
const res = await apps.public.request(url);
|
|
119
|
+
assert.strictEqual(res.status, 401, `GET ${method.name} should return 401`);
|
|
120
|
+
const body = await res.json();
|
|
121
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.unauthenticated);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
// --- Adversarial envelopes ---
|
|
131
|
+
/**
|
|
132
|
+
* Generate adversarial envelope tests for RPC endpoints.
|
|
133
|
+
*
|
|
134
|
+
* Fixed set of malformation cases that exercise the dispatcher's
|
|
135
|
+
* envelope parsing (step 1) and method lookup (step 2).
|
|
136
|
+
*/
|
|
137
|
+
const describe_rpc_adversarial_envelopes = (options) => {
|
|
138
|
+
const { build, roles } = options;
|
|
139
|
+
const { surface, route_specs } = build();
|
|
140
|
+
if (surface.rpc_endpoints.length === 0)
|
|
141
|
+
return;
|
|
142
|
+
// public app for envelope errors (happen before auth checks)
|
|
143
|
+
const apps = create_auth_test_apps(route_specs, []);
|
|
144
|
+
// authed apps for the GET mutation test (needs correct auth to reach the side_effects check)
|
|
145
|
+
const authed_apps = create_auth_test_apps(route_specs, roles);
|
|
146
|
+
describe('RPC adversarial envelopes', () => {
|
|
147
|
+
for (const endpoint of surface.rpc_endpoints) {
|
|
148
|
+
// find a mutation method for GET-restriction testing
|
|
149
|
+
const mutation_method = endpoint.methods.find((m) => m.side_effects);
|
|
150
|
+
describe(endpoint.path, () => {
|
|
151
|
+
// --- POST envelope malformation ---
|
|
152
|
+
test('non-JSON body → parse_error', async () => {
|
|
153
|
+
const res = await apps.public.request(endpoint.path, {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: { 'Content-Type': 'application/json' },
|
|
156
|
+
body: 'not-json',
|
|
157
|
+
});
|
|
158
|
+
assert.strictEqual(res.status, 400);
|
|
159
|
+
const body = await res.json();
|
|
160
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.parse_error);
|
|
161
|
+
});
|
|
162
|
+
test('wrong jsonrpc version → invalid_request', async () => {
|
|
163
|
+
const res = await apps.public.request(endpoint.path, {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: { 'Content-Type': 'application/json' },
|
|
166
|
+
body: JSON.stringify({
|
|
167
|
+
jsonrpc: '1.0',
|
|
168
|
+
id: 'test',
|
|
169
|
+
method: endpoint.methods[0]?.name ?? 'any',
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
assert.strictEqual(res.status, 400);
|
|
173
|
+
const body = await res.json();
|
|
174
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_request);
|
|
175
|
+
});
|
|
176
|
+
test('missing jsonrpc field → invalid_request', async () => {
|
|
177
|
+
const res = await apps.public.request(endpoint.path, {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers: { 'Content-Type': 'application/json' },
|
|
180
|
+
body: JSON.stringify({
|
|
181
|
+
id: 'test',
|
|
182
|
+
method: endpoint.methods[0]?.name ?? 'any',
|
|
183
|
+
}),
|
|
184
|
+
});
|
|
185
|
+
assert.strictEqual(res.status, 400);
|
|
186
|
+
const body = await res.json();
|
|
187
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_request);
|
|
188
|
+
});
|
|
189
|
+
test('missing method field → invalid_request', async () => {
|
|
190
|
+
const res = await apps.public.request(endpoint.path, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
headers: { 'Content-Type': 'application/json' },
|
|
193
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 'test' }),
|
|
194
|
+
});
|
|
195
|
+
assert.strictEqual(res.status, 400);
|
|
196
|
+
const body = await res.json();
|
|
197
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_request);
|
|
198
|
+
});
|
|
199
|
+
test('missing id field → invalid_request', async () => {
|
|
200
|
+
const res = await apps.public.request(endpoint.path, {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: { 'Content-Type': 'application/json' },
|
|
203
|
+
body: JSON.stringify({
|
|
204
|
+
jsonrpc: '2.0',
|
|
205
|
+
method: endpoint.methods[0]?.name ?? 'any',
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
assert.strictEqual(res.status, 400);
|
|
209
|
+
const body = await res.json();
|
|
210
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_request);
|
|
211
|
+
});
|
|
212
|
+
test('batch (array) body → invalid_request', async () => {
|
|
213
|
+
const res = await apps.public.request(endpoint.path, {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
headers: { 'Content-Type': 'application/json' },
|
|
216
|
+
body: JSON.stringify([
|
|
217
|
+
{ jsonrpc: '2.0', id: '1', method: endpoint.methods[0]?.name ?? 'any' },
|
|
218
|
+
]),
|
|
219
|
+
});
|
|
220
|
+
assert.strictEqual(res.status, 400);
|
|
221
|
+
const body = await res.json();
|
|
222
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_request);
|
|
223
|
+
assert.strictEqual(body.id, null, 'batch has no extractable id');
|
|
224
|
+
});
|
|
225
|
+
test('unknown method name → method_not_found', async () => {
|
|
226
|
+
const res = await apps.public.request(endpoint.path, create_rpc_post_init('__nonexistent_method__'));
|
|
227
|
+
assert.strictEqual(res.status, 404);
|
|
228
|
+
const body = await res.json();
|
|
229
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.method_not_found);
|
|
230
|
+
});
|
|
231
|
+
// --- GET envelope malformation ---
|
|
232
|
+
test('GET missing method → invalid_request', async () => {
|
|
233
|
+
const res = await apps.public.request(`${endpoint.path}?id=test`);
|
|
234
|
+
assert.strictEqual(res.status, 400);
|
|
235
|
+
const body = await res.json();
|
|
236
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_request);
|
|
237
|
+
});
|
|
238
|
+
test('GET missing id → invalid_request', async () => {
|
|
239
|
+
const first_method = endpoint.methods[0]?.name ?? 'any';
|
|
240
|
+
const res = await apps.public.request(`${endpoint.path}?method=${first_method}`);
|
|
241
|
+
assert.strictEqual(res.status, 400);
|
|
242
|
+
const body = await res.json();
|
|
243
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_request);
|
|
244
|
+
});
|
|
245
|
+
test('GET invalid JSON params → invalid_params', async () => {
|
|
246
|
+
const read_method = endpoint.methods.find((m) => !m.side_effects);
|
|
247
|
+
// skip if no read methods exist
|
|
248
|
+
if (!read_method)
|
|
249
|
+
return;
|
|
250
|
+
const res = await apps.public.request(`${endpoint.path}?method=${read_method.name}&id=test¶ms=not-json`);
|
|
251
|
+
assert.strictEqual(res.status, 400);
|
|
252
|
+
const body = await res.json();
|
|
253
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_params);
|
|
254
|
+
});
|
|
255
|
+
test('GET non-object params → error', async () => {
|
|
256
|
+
const read_method = endpoint.methods.find((m) => !m.side_effects);
|
|
257
|
+
// skip if no read methods exist
|
|
258
|
+
if (!read_method)
|
|
259
|
+
return;
|
|
260
|
+
// valid JSON but not an object — hits dispatcher's params validation
|
|
261
|
+
const res = await apps.public.request(`${endpoint.path}?method=${read_method.name}&id=test¶ms=42`);
|
|
262
|
+
// should reject: either invalid_params (step 4) or auth error (step 3)
|
|
263
|
+
assert.ok(res.status >= 400, `expected error status for non-object params, got ${res.status}`);
|
|
264
|
+
const body = await res.json();
|
|
265
|
+
assert_jsonrpc_error_response(body);
|
|
266
|
+
});
|
|
267
|
+
if (mutation_method) {
|
|
268
|
+
test('GET mutation method → invalid_request (side effects)', async () => {
|
|
269
|
+
const url = create_rpc_get_url(endpoint.path, mutation_method.name);
|
|
270
|
+
// need correct auth to reach the side_effects check
|
|
271
|
+
const app = select_auth_app(authed_apps, mutation_method.auth);
|
|
272
|
+
const res = await app.request(url);
|
|
273
|
+
assert.strictEqual(res.status, 400);
|
|
274
|
+
const body = await res.json();
|
|
275
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_request);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
// --- Adversarial params ---
|
|
283
|
+
/**
|
|
284
|
+
* Generate adversarial params validation tests for RPC endpoints.
|
|
285
|
+
*
|
|
286
|
+
* For each method with a non-null input schema, generates test cases
|
|
287
|
+
* from the schema (wrong types, missing fields, format violations)
|
|
288
|
+
* and wraps them in valid JSON-RPC envelopes. Reuses
|
|
289
|
+
* `generate_input_test_cases` from `adversarial_input.ts`.
|
|
290
|
+
*/
|
|
291
|
+
const describe_rpc_adversarial_params = (options) => {
|
|
292
|
+
const { build, roles } = options;
|
|
293
|
+
const { surface, route_specs, rpc_endpoints: rpc_endpoint_specs } = build();
|
|
294
|
+
if (surface.rpc_endpoints.length === 0)
|
|
295
|
+
return;
|
|
296
|
+
const apps = create_auth_test_apps(route_specs, roles);
|
|
297
|
+
let total_cases = 0;
|
|
298
|
+
describe('RPC adversarial params', () => {
|
|
299
|
+
for (const endpoint of surface.rpc_endpoints) {
|
|
300
|
+
const methods_with_input = endpoint.methods.filter((m) => m.input_schema !== null);
|
|
301
|
+
if (methods_with_input.length === 0)
|
|
302
|
+
continue;
|
|
303
|
+
describe(endpoint.path, () => {
|
|
304
|
+
for (const method of methods_with_input) {
|
|
305
|
+
// look up the source RpcAction for the Zod schema
|
|
306
|
+
const action = find_rpc_action(rpc_endpoint_specs, endpoint.path, method.name);
|
|
307
|
+
if (!action) {
|
|
308
|
+
test(`${method.name} — missing RpcAction source spec`, () => {
|
|
309
|
+
assert.fail(`surface has method '${method.name}' but no matching RpcAction in rpc_endpoints`);
|
|
310
|
+
});
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
// filter out structural cases (non-object body) — those fail at
|
|
314
|
+
// envelope validation (invalid_request) not params validation (invalid_params).
|
|
315
|
+
// Envelope-level structural errors are covered by adversarial envelopes.
|
|
316
|
+
const test_cases = generate_input_test_cases(action.spec.input).filter((tc) => tc.expected_error !== ERROR_INVALID_JSON_BODY);
|
|
317
|
+
if (test_cases.length === 0)
|
|
318
|
+
continue;
|
|
319
|
+
total_cases += test_cases.length;
|
|
320
|
+
const app = select_auth_app(apps, method.auth);
|
|
321
|
+
describe(method.name, () => {
|
|
322
|
+
for (const tc of test_cases) {
|
|
323
|
+
test(tc.label, async () => {
|
|
324
|
+
const res = await app.request(endpoint.path, create_rpc_post_init(method.name, tc.body));
|
|
325
|
+
assert.strictEqual(res.status, 400, `Expected 400 for ${method.name} [${tc.label}], got ${res.status}`);
|
|
326
|
+
const body = await res.json();
|
|
327
|
+
assert_jsonrpc_error_response(body, JSONRPC_ERROR_CODES.invalid_params);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
test('generated RPC params test cases', () => {
|
|
335
|
+
// soft check — methods with only null-input schemas produce 0 cases
|
|
336
|
+
if (surface.rpc_endpoints.some((ep) => ep.methods.some((m) => m.input_schema !== null))) {
|
|
337
|
+
assert.ok(total_cases > 0, 'No RPC params test cases generated — schema walking may be broken');
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
// --- Helpers (formatting) ---
|
|
343
|
+
/** Format a `RouteAuth` as a human-readable label. */
|
|
344
|
+
const format_auth = (auth) => {
|
|
345
|
+
switch (auth.type) {
|
|
346
|
+
case 'none':
|
|
347
|
+
return 'public';
|
|
348
|
+
case 'authenticated':
|
|
349
|
+
return 'authenticated';
|
|
350
|
+
case 'role':
|
|
351
|
+
return `role: ${auth.role}`;
|
|
352
|
+
case 'keeper':
|
|
353
|
+
return 'keeper';
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
// --- Public API ---
|
|
357
|
+
/**
|
|
358
|
+
* Run the standard RPC attack surface test suite.
|
|
359
|
+
*
|
|
360
|
+
* Generates 3 test groups:
|
|
361
|
+
* 1. Auth enforcement — per-method auth checks via JSON-RPC envelopes
|
|
362
|
+
* 2. Adversarial envelopes — malformed JSON-RPC requests
|
|
363
|
+
* 3. Adversarial params — schema-invalid params per method
|
|
364
|
+
*
|
|
365
|
+
* Skips silently when `surface.rpc_endpoints` is empty.
|
|
366
|
+
*
|
|
367
|
+
* @param options - the test configuration
|
|
368
|
+
*/
|
|
369
|
+
export const describe_rpc_attack_surface_tests = (options) => {
|
|
370
|
+
const { surface } = options.build();
|
|
371
|
+
if (surface.rpc_endpoints.length === 0)
|
|
372
|
+
return;
|
|
373
|
+
describe_rpc_auth(options);
|
|
374
|
+
describe_rpc_adversarial_envelopes(options);
|
|
375
|
+
describe_rpc_adversarial_params(options);
|
|
376
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { JsonrpcErrorCode } from '../http/jsonrpc_errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* Create a `RequestInit` for a JSON-RPC POST request.
|
|
6
|
+
*
|
|
7
|
+
* @param method - JSON-RPC method name
|
|
8
|
+
* @param params - params object (omit for null-input methods)
|
|
9
|
+
* @param id - request id (default `'test'`)
|
|
10
|
+
* @returns a `RequestInit` with the JSON-RPC envelope as body
|
|
11
|
+
*/
|
|
12
|
+
export declare const create_rpc_post_init: (method: string, params?: unknown, id?: string | number) => RequestInit;
|
|
13
|
+
/**
|
|
14
|
+
* Build a GET URL with JSON-RPC query parameters.
|
|
15
|
+
*
|
|
16
|
+
* @param endpoint_path - the RPC endpoint path (e.g., `/api/rpc`)
|
|
17
|
+
* @param method - JSON-RPC method name
|
|
18
|
+
* @param params - params object (omit for null-input methods)
|
|
19
|
+
* @param id - request id (default `'test'`)
|
|
20
|
+
* @returns the full URL with query string
|
|
21
|
+
*/
|
|
22
|
+
export declare const create_rpc_get_url: (endpoint_path: string, method: string, params?: unknown, id?: string | number) => string;
|
|
23
|
+
/**
|
|
24
|
+
* Assert that a response body is a valid JSON-RPC error response.
|
|
25
|
+
*
|
|
26
|
+
* Validates the structure matches `JsonrpcErrorResponse` and optionally
|
|
27
|
+
* checks the error code.
|
|
28
|
+
*
|
|
29
|
+
* @param body - parsed response body
|
|
30
|
+
* @param expected_code - optional error code to assert
|
|
31
|
+
*/
|
|
32
|
+
export declare const assert_jsonrpc_error_response: (body: unknown, expected_code?: JsonrpcErrorCode) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Assert that a response body is a valid JSON-RPC success response.
|
|
35
|
+
*
|
|
36
|
+
* Validates the structure matches `JsonrpcResponse`. When `output_schema`
|
|
37
|
+
* is provided, also validates the `result` field against the declared
|
|
38
|
+
* output schema — matching the REST round-trip's `assert_response_matches_spec`.
|
|
39
|
+
*
|
|
40
|
+
* @param body - parsed response body
|
|
41
|
+
* @param output_schema - optional Zod schema to validate the `result` field against
|
|
42
|
+
*/
|
|
43
|
+
export declare const assert_jsonrpc_success_response: (body: unknown, output_schema?: z.ZodType) => void;
|
|
44
|
+
//# sourceMappingURL=rpc_helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAW7B,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AAEhE;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAChC,QAAQ,MAAM,EACd,SAAS,OAAO,EAChB,KAAI,MAAM,GAAG,MAAe,KAC1B,WAID,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC9B,eAAe,MAAM,EACrB,QAAQ,MAAM,EACd,SAAS,OAAO,EAChB,KAAI,MAAM,GAAG,MAAe,KAC1B,MAMF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,OAAO,EACb,gBAAgB,gBAAgB,KAC9B,IAUF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,+BAA+B,GAAI,MAAM,OAAO,EAAE,gBAAgB,CAAC,CAAC,OAAO,KAAG,IAU1F,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* JSON-RPC request construction and response assertion helpers.
|
|
4
|
+
*
|
|
5
|
+
* Shared by `rpc_attack_surface.ts` and `rpc_round_trip.ts`.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { assert } from 'vitest';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { JSONRPC_VERSION, JsonrpcErrorResponse, JsonrpcResponse } from '../http/jsonrpc.js';
|
|
12
|
+
/**
|
|
13
|
+
* Create a `RequestInit` for a JSON-RPC POST request.
|
|
14
|
+
*
|
|
15
|
+
* @param method - JSON-RPC method name
|
|
16
|
+
* @param params - params object (omit for null-input methods)
|
|
17
|
+
* @param id - request id (default `'test'`)
|
|
18
|
+
* @returns a `RequestInit` with the JSON-RPC envelope as body
|
|
19
|
+
*/
|
|
20
|
+
export const create_rpc_post_init = (method, params, id = 'test') => ({
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
body: JSON.stringify({ jsonrpc: JSONRPC_VERSION, method, params, id }),
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Build a GET URL with JSON-RPC query parameters.
|
|
27
|
+
*
|
|
28
|
+
* @param endpoint_path - the RPC endpoint path (e.g., `/api/rpc`)
|
|
29
|
+
* @param method - JSON-RPC method name
|
|
30
|
+
* @param params - params object (omit for null-input methods)
|
|
31
|
+
* @param id - request id (default `'test'`)
|
|
32
|
+
* @returns the full URL with query string
|
|
33
|
+
*/
|
|
34
|
+
export const create_rpc_get_url = (endpoint_path, method, params, id = 'test') => {
|
|
35
|
+
const search = new URLSearchParams({ method, id: String(id) });
|
|
36
|
+
if (params !== undefined && params !== null) {
|
|
37
|
+
search.set('params', JSON.stringify(params));
|
|
38
|
+
}
|
|
39
|
+
return `${endpoint_path}?${search.toString()}`;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Assert that a response body is a valid JSON-RPC error response.
|
|
43
|
+
*
|
|
44
|
+
* Validates the structure matches `JsonrpcErrorResponse` and optionally
|
|
45
|
+
* checks the error code.
|
|
46
|
+
*
|
|
47
|
+
* @param body - parsed response body
|
|
48
|
+
* @param expected_code - optional error code to assert
|
|
49
|
+
*/
|
|
50
|
+
export const assert_jsonrpc_error_response = (body, expected_code) => {
|
|
51
|
+
const result = JsonrpcErrorResponse.safeParse(body);
|
|
52
|
+
assert.ok(result.success, `not a valid JSON-RPC error response: ${JSON.stringify(body)}`);
|
|
53
|
+
if (expected_code !== undefined) {
|
|
54
|
+
assert.strictEqual(result.data.error.code, expected_code, `expected error code ${expected_code}, got ${result.data.error.code}`);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Assert that a response body is a valid JSON-RPC success response.
|
|
59
|
+
*
|
|
60
|
+
* Validates the structure matches `JsonrpcResponse`. When `output_schema`
|
|
61
|
+
* is provided, also validates the `result` field against the declared
|
|
62
|
+
* output schema — matching the REST round-trip's `assert_response_matches_spec`.
|
|
63
|
+
*
|
|
64
|
+
* @param body - parsed response body
|
|
65
|
+
* @param output_schema - optional Zod schema to validate the `result` field against
|
|
66
|
+
*/
|
|
67
|
+
export const assert_jsonrpc_success_response = (body, output_schema) => {
|
|
68
|
+
const result = JsonrpcResponse.safeParse(body);
|
|
69
|
+
assert.ok(result.success, `not a valid JSON-RPC success response: ${JSON.stringify(body)}`);
|
|
70
|
+
if (output_schema) {
|
|
71
|
+
const output_result = output_schema.safeParse(result.data.result);
|
|
72
|
+
assert.ok(output_result.success, `JSON-RPC result does not match output schema: ${JSON.stringify(output_result.error?.issues)}`);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
import type { RouteSpec } from '../http/route_spec.js';
|
|
3
|
+
import type { AppServerContext, AppServerOptions } from '../server/app_server.js';
|
|
4
|
+
import type { SessionOptions } from '../auth/session_cookie.js';
|
|
5
|
+
import { type DbFactory } from './db.js';
|
|
6
|
+
import type { RpcEndpointSpec } from '../http/surface.js';
|
|
7
|
+
/** Options for `describe_rpc_round_trip_tests`. */
|
|
8
|
+
export interface RpcRoundTripTestOptions {
|
|
9
|
+
/** Session config for cookie-based auth. */
|
|
10
|
+
session_options: SessionOptions<string>;
|
|
11
|
+
/** Route spec factory — same one used in production. */
|
|
12
|
+
create_route_specs: (ctx: AppServerContext) => Array<RouteSpec>;
|
|
13
|
+
/** RPC endpoint specs — the source `RpcAction` arrays for params generation. */
|
|
14
|
+
rpc_endpoints: Array<RpcEndpointSpec>;
|
|
15
|
+
/** Optional overrides for `AppServerOptions`. */
|
|
16
|
+
app_options?: Partial<Omit<AppServerOptions, 'backend' | 'session_options' | 'create_route_specs'>>;
|
|
17
|
+
/** Database factories to run tests against. Default: pglite only. */
|
|
18
|
+
db_factories?: Array<DbFactory>;
|
|
19
|
+
/** Methods to skip, by name (e.g., `'tx_plan'`). */
|
|
20
|
+
skip_methods?: Array<string>;
|
|
21
|
+
/** Override generated params for specific methods (method name → params). */
|
|
22
|
+
input_overrides?: Map<string, Record<string, unknown>>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Run schema-driven round-trip validation for RPC endpoints.
|
|
26
|
+
*
|
|
27
|
+
* For each method:
|
|
28
|
+
* 1. Generate valid params from the action's input schema
|
|
29
|
+
* 2. Fire a POST request with JSON-RPC envelope
|
|
30
|
+
* 3. For `side_effects: false` methods, also fire a GET request
|
|
31
|
+
* 4. Validate response is well-formed JSON-RPC; successful responses are
|
|
32
|
+
* also validated against the method's declared output schema
|
|
33
|
+
*
|
|
34
|
+
* Error responses (from missing DB state, etc.) are expected and validated
|
|
35
|
+
* as well-formed JSON-RPC errors. Successful responses are validated against
|
|
36
|
+
* `action.spec.output`.
|
|
37
|
+
*
|
|
38
|
+
* @param options - round-trip test configuration
|
|
39
|
+
*/
|
|
40
|
+
export declare const describe_rpc_round_trip_tests: (options: RpcRoundTripTestOptions) => void;
|
|
41
|
+
//# sourceMappingURL=rpc_round_trip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/rpc_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAe7B,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;AAK9D,OAAO,KAAK,EAAC,eAAe,EAAsB,MAAM,oBAAoB,CAAC;AAQ7E,mDAAmD;AACnD,MAAM,WAAW,uBAAuB;IACvC,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,gFAAgF;IAChF,aAAa,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;IACtC,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,oDAAoD;IACpD,YAAY,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,6EAA6E;IAC7E,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AA2BD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,6BAA6B,GAAI,SAAS,uBAAuB,KAAG,IAqIhF,CAAC"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Schema-driven round-trip validation for RPC endpoints.
|
|
4
|
+
*
|
|
5
|
+
* For every RPC method, generates valid params and fires JSON-RPC requests
|
|
6
|
+
* (POST for all methods, GET for reads), validating that responses are
|
|
7
|
+
* well-formed JSON-RPC. Successful responses are validated against the
|
|
8
|
+
* method's declared output schema. DB-backed via `create_test_app`.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { describe, test, beforeAll, afterAll } from 'vitest';
|
|
13
|
+
import { ROLE_ADMIN } from '../auth/role_schema.js';
|
|
14
|
+
import { create_test_app } from './app_server.js';
|
|
15
|
+
import { create_pglite_factory } from './db.js';
|
|
16
|
+
import { generate_valid_body } from './schema_generators.js';
|
|
17
|
+
import { run_migrations } from '../db/migrate.js';
|
|
18
|
+
import { AUTH_MIGRATION_NS } from '../auth/migrations.js';
|
|
19
|
+
import { create_rpc_post_init, create_rpc_get_url, assert_jsonrpc_error_response, assert_jsonrpc_success_response, } from './rpc_helpers.js';
|
|
20
|
+
/**
|
|
21
|
+
* Pick auth headers matching an RPC method's auth requirement.
|
|
22
|
+
*/
|
|
23
|
+
const pick_rpc_auth_headers = (method, test_app, authed_account, admin_account) => {
|
|
24
|
+
switch (method.auth.type) {
|
|
25
|
+
case 'none':
|
|
26
|
+
return { host: 'localhost', origin: 'http://localhost:5173' };
|
|
27
|
+
case 'authenticated':
|
|
28
|
+
return authed_account.create_session_headers();
|
|
29
|
+
case 'role':
|
|
30
|
+
if (method.auth.role === ROLE_ADMIN) {
|
|
31
|
+
return admin_account.create_session_headers();
|
|
32
|
+
}
|
|
33
|
+
// keeper role uses the bootstrapped account
|
|
34
|
+
return test_app.create_session_headers();
|
|
35
|
+
case 'keeper':
|
|
36
|
+
return test_app.create_bearer_headers();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Run schema-driven round-trip validation for RPC endpoints.
|
|
41
|
+
*
|
|
42
|
+
* For each method:
|
|
43
|
+
* 1. Generate valid params from the action's input schema
|
|
44
|
+
* 2. Fire a POST request with JSON-RPC envelope
|
|
45
|
+
* 3. For `side_effects: false` methods, also fire a GET request
|
|
46
|
+
* 4. Validate response is well-formed JSON-RPC; successful responses are
|
|
47
|
+
* also validated against the method's declared output schema
|
|
48
|
+
*
|
|
49
|
+
* Error responses (from missing DB state, etc.) are expected and validated
|
|
50
|
+
* as well-formed JSON-RPC errors. Successful responses are validated against
|
|
51
|
+
* `action.spec.output`.
|
|
52
|
+
*
|
|
53
|
+
* @param options - round-trip test configuration
|
|
54
|
+
*/
|
|
55
|
+
export const describe_rpc_round_trip_tests = (options) => {
|
|
56
|
+
const skip_set = new Set(options.skip_methods);
|
|
57
|
+
const init_schema = async (db) => {
|
|
58
|
+
await run_migrations(db, [AUTH_MIGRATION_NS]);
|
|
59
|
+
};
|
|
60
|
+
const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
|
|
61
|
+
for (const factory of factories) {
|
|
62
|
+
const describe_fn = factory.skip ? describe.skip : describe;
|
|
63
|
+
describe_fn(`RPC round-trip validation (${factory.name})`, () => {
|
|
64
|
+
let test_app;
|
|
65
|
+
let authed_account;
|
|
66
|
+
let admin_account;
|
|
67
|
+
let db;
|
|
68
|
+
beforeAll(async () => {
|
|
69
|
+
db = await factory.create();
|
|
70
|
+
test_app = await create_test_app({
|
|
71
|
+
session_options: options.session_options,
|
|
72
|
+
create_route_specs: options.create_route_specs,
|
|
73
|
+
db,
|
|
74
|
+
app_options: {
|
|
75
|
+
rpc_endpoints: options.rpc_endpoints,
|
|
76
|
+
...options.app_options,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
authed_account = await test_app.create_account({
|
|
80
|
+
username: 'rpc_round_trip_authed',
|
|
81
|
+
roles: [],
|
|
82
|
+
});
|
|
83
|
+
admin_account = await test_app.create_account({
|
|
84
|
+
username: 'rpc_round_trip_admin',
|
|
85
|
+
roles: [ROLE_ADMIN],
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
afterAll(async () => {
|
|
89
|
+
await test_app.cleanup();
|
|
90
|
+
await factory.close(db);
|
|
91
|
+
});
|
|
92
|
+
test('all RPC methods produce valid JSON-RPC responses (POST)', async () => {
|
|
93
|
+
for (const ep_spec of options.rpc_endpoints) {
|
|
94
|
+
const surface_ep = test_app.surface_spec.surface.rpc_endpoints.find((e) => e.path === ep_spec.path);
|
|
95
|
+
if (!surface_ep)
|
|
96
|
+
continue;
|
|
97
|
+
for (const action of ep_spec.actions) {
|
|
98
|
+
if (skip_set.has(action.spec.method))
|
|
99
|
+
continue;
|
|
100
|
+
const surface_method = surface_ep.methods.find((m) => m.name === action.spec.method);
|
|
101
|
+
if (!surface_method)
|
|
102
|
+
continue;
|
|
103
|
+
// generate or override params
|
|
104
|
+
const override = options.input_overrides?.get(action.spec.method);
|
|
105
|
+
const params = override ?? generate_valid_body(action.spec.input) ?? null;
|
|
106
|
+
// pick auth
|
|
107
|
+
const headers = pick_rpc_auth_headers(surface_method, test_app, authed_account, admin_account);
|
|
108
|
+
const init = create_rpc_post_init(action.spec.method, params);
|
|
109
|
+
// merge auth headers into init
|
|
110
|
+
Object.assign(init.headers, headers);
|
|
111
|
+
const res = await test_app.app.request(ep_spec.path, init); // eslint-disable-line no-await-in-loop
|
|
112
|
+
const body = await res.json(); // eslint-disable-line no-await-in-loop
|
|
113
|
+
// validate well-formed JSON-RPC; successful responses also checked against output schema
|
|
114
|
+
try {
|
|
115
|
+
if (res.ok) {
|
|
116
|
+
assert_jsonrpc_success_response(body, action.spec.output);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
assert_jsonrpc_error_response(body);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
throw new Error(`RPC round-trip POST failed for ${action.spec.method} (status ${res.status}): ${e.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
test('all read RPC methods produce valid JSON-RPC responses (GET)', async () => {
|
|
129
|
+
for (const ep_spec of options.rpc_endpoints) {
|
|
130
|
+
const surface_ep = test_app.surface_spec.surface.rpc_endpoints.find((e) => e.path === ep_spec.path);
|
|
131
|
+
if (!surface_ep)
|
|
132
|
+
continue;
|
|
133
|
+
const read_actions = ep_spec.actions.filter((a) => !a.spec.side_effects);
|
|
134
|
+
for (const action of read_actions) {
|
|
135
|
+
if (skip_set.has(action.spec.method))
|
|
136
|
+
continue;
|
|
137
|
+
const surface_method = surface_ep.methods.find((m) => m.name === action.spec.method);
|
|
138
|
+
if (!surface_method)
|
|
139
|
+
continue;
|
|
140
|
+
const override = options.input_overrides?.get(action.spec.method);
|
|
141
|
+
const params = override ?? generate_valid_body(action.spec.input) ?? undefined;
|
|
142
|
+
const headers = pick_rpc_auth_headers(surface_method, test_app, authed_account, admin_account);
|
|
143
|
+
const url = create_rpc_get_url(ep_spec.path, action.spec.method, params);
|
|
144
|
+
const res = await test_app.app.request(url, { headers }); // eslint-disable-line no-await-in-loop
|
|
145
|
+
const body = await res.json(); // eslint-disable-line no-await-in-loop
|
|
146
|
+
try {
|
|
147
|
+
if (res.ok) {
|
|
148
|
+
assert_jsonrpc_success_response(body, action.spec.output);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
assert_jsonrpc_error_response(body);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
throw new Error(`RPC round-trip GET failed for ${action.spec.method} (status ${res.status}): ${e.message}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema_generators.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_generators.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AACtB,OAAO,EAKN,KAAK,YAAY,EACjB,MAAM,yBAAyB,CAAC;AAIjC;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,cAAc,CAAC,CAAC,OAAO,KAAG,MAAM,GAAG,IAShE,CAAC;
|
|
1
|
+
{"version":3,"file":"schema_generators.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_generators.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AACtB,OAAO,EAKN,KAAK,YAAY,EACjB,MAAM,yBAAyB,CAAC;AAIjC;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,cAAc,CAAC,CAAC,OAAO,KAAG,MAAM,GAAG,IAShE,CAAC;AA+BF,qEAAqE;AACrE,eAAO,MAAM,oBAAoB,GAAI,OAAO,YAAY,EAAE,cAAc,CAAC,CAAC,OAAO,KAAG,OAkDnF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,gBAAgB,CAAC,CAAC,SAAS,KAAG,MAa9E,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC/B,cAAc,CAAC,CAAC,OAAO,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAkB5B,CAAC"}
|
|
@@ -43,7 +43,20 @@ const generate_valid_string = (field_schema) => {
|
|
|
43
43
|
// no constraints
|
|
44
44
|
}
|
|
45
45
|
const target = Math.max(min_length, Math.min(10, max_length));
|
|
46
|
-
|
|
46
|
+
const base = 'x'.repeat(target) || 'test_value';
|
|
47
|
+
// Validate against the full schema (including refinements/brands).
|
|
48
|
+
// If the base string fails, try common patterns before giving up.
|
|
49
|
+
if (field_schema.safeParse(base).success)
|
|
50
|
+
return base;
|
|
51
|
+
// Absolute path refinement (e.g. DiskfilePath)
|
|
52
|
+
const with_slash = '/' + base;
|
|
53
|
+
if (field_schema.safeParse(with_slash).success)
|
|
54
|
+
return with_slash;
|
|
55
|
+
// URL refinement
|
|
56
|
+
const as_url = 'https://example.com/' + base;
|
|
57
|
+
if (field_schema.safeParse(as_url).success)
|
|
58
|
+
return as_url;
|
|
59
|
+
return base; // fall through — generate_valid_body will report the failure
|
|
47
60
|
};
|
|
48
61
|
/** Generate a valid-ish value for a field based on its base type. */
|
|
49
62
|
export const generate_valid_value = (field, field_schema) => {
|
|
@@ -58,6 +71,8 @@ export const generate_valid_value = (field, field_schema) => {
|
|
|
58
71
|
return '00000000-0000-0000-0000-000000000000';
|
|
59
72
|
if (format === 'email')
|
|
60
73
|
return 'test@example.com';
|
|
74
|
+
if (format === 'date-time')
|
|
75
|
+
return '2020-01-01T00:00:00.000Z';
|
|
61
76
|
return generate_valid_string(field_schema);
|
|
62
77
|
case 'number':
|
|
63
78
|
case 'int':
|
|
@@ -66,8 +81,21 @@ export const generate_valid_value = (field, field_schema) => {
|
|
|
66
81
|
return true;
|
|
67
82
|
case 'array':
|
|
68
83
|
return [];
|
|
69
|
-
case 'object':
|
|
84
|
+
case 'object': {
|
|
85
|
+
// Recursively generate valid nested objects
|
|
86
|
+
const nested_schema = zod_unwrap_to_object(field_schema);
|
|
87
|
+
if (nested_schema) {
|
|
88
|
+
const nested_fields = zod_extract_fields(nested_schema);
|
|
89
|
+
const nested = {};
|
|
90
|
+
for (const nf of nested_fields) {
|
|
91
|
+
if (!nf.required && !nf.has_default)
|
|
92
|
+
continue;
|
|
93
|
+
nested[nf.name] = generate_valid_value(nf, nested_schema.shape[nf.name]);
|
|
94
|
+
}
|
|
95
|
+
return nested;
|
|
96
|
+
}
|
|
70
97
|
return {};
|
|
98
|
+
}
|
|
71
99
|
case 'null':
|
|
72
100
|
return null;
|
|
73
101
|
case 'enum': {
|