@schmock/core 2.2.3 → 2.3.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/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/abort.d.ts +3 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +30 -0
- package/dist/binary.d.ts +8 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +9 -0
- package/dist/builder.d.ts +31 -10
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +804 -203
- package/dist/constants.d.ts +38 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +123 -2
- package/dist/errors.d.ts +14 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +41 -6
- package/dist/helpers.d.ts +9 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +18 -2
- package/dist/http-helpers.d.ts +52 -5
- package/dist/http-helpers.d.ts.map +1 -1
- package/dist/http-helpers.js +153 -37
- package/dist/index.d.ts +381 -60
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/interceptor.d.ts +11 -1
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/interceptor.js +354 -156
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +12 -3
- package/dist/plugin-pipeline.d.ts +11 -4
- package/dist/plugin-pipeline.d.ts.map +1 -1
- package/dist/plugin-pipeline.js +117 -39
- package/dist/response-normalizer.d.ts +16 -0
- package/dist/response-normalizer.d.ts.map +1 -0
- package/dist/response-normalizer.js +316 -0
- package/dist/response-parser.d.ts.map +1 -1
- package/dist/response-parser.js +85 -18
- package/dist/route-matcher.d.ts +3 -0
- package/dist/route-matcher.d.ts.map +1 -1
- package/dist/route-matcher.js +12 -6
- package/dist/types.d.ts +7 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +18 -8
- package/src/audit-core-builder.test.ts +0 -218
- package/src/audit-response-guard.test.ts +0 -38
- package/src/builder.test.ts +0 -289
- package/src/builder.ts +0 -701
- package/src/constants.test.ts +0 -99
- package/src/constants.ts +0 -73
- package/src/debug.test.ts +0 -241
- package/src/delay.test.ts +0 -319
- package/src/dist-shape.test.ts +0 -35
- package/src/errors.test.ts +0 -223
- package/src/errors.ts +0 -130
- package/src/factory.test.ts +0 -133
- package/src/helpers.test.ts +0 -147
- package/src/helpers.ts +0 -58
- package/src/http-helpers.ts +0 -113
- package/src/index.ts +0 -151
- package/src/interceptor.test.ts +0 -291
- package/src/interceptor.ts +0 -320
- package/src/namespace.test.ts +0 -274
- package/src/parser.property.test.ts +0 -594
- package/src/parser.test.ts +0 -148
- package/src/parser.ts +0 -64
- package/src/plugin-pipeline.ts +0 -103
- package/src/plugin-system.test.ts +0 -602
- package/src/response-parser.ts +0 -75
- package/src/response-parsing.test.ts +0 -333
- package/src/route-matcher.ts +0 -69
- package/src/route-matching.test.ts +0 -394
- package/src/server.test.ts +0 -165
- package/src/smart-defaults.test.ts +0 -361
- package/src/steps/async-support.steps.ts +0 -400
- package/src/steps/audit-core-builder.steps.ts +0 -188
- package/src/steps/audit-onerror-tuple.steps.ts +0 -99
- package/src/steps/basic-usage.steps.ts +0 -245
- package/src/steps/developer-experience.steps.ts +0 -204
- package/src/steps/error-handling.steps.ts +0 -345
- package/src/steps/fluent-api.steps.ts +0 -255
- package/src/steps/http-methods.steps.ts +0 -331
- package/src/steps/interceptor.steps.ts +0 -260
- package/src/steps/lifecycle-events.steps.ts +0 -142
- package/src/steps/performance-reliability.steps.ts +0 -423
- package/src/steps/plugin-integration.steps.ts +0 -280
- package/src/steps/request-history.steps.ts +0 -276
- package/src/steps/response-delay.steps.ts +0 -88
- package/src/steps/route-key-format.steps.ts +0 -99
- package/src/steps/standalone-server.steps.ts +0 -233
- package/src/steps/state-concurrency.steps.ts +0 -739
- package/src/steps/stateful-workflows.steps.ts +0 -353
- package/src/types.ts +0 -36
package/dist/response-parser.js
CHANGED
|
@@ -1,10 +1,59 @@
|
|
|
1
|
+
import { isBinaryBody } from "./binary.js";
|
|
1
2
|
import { isStatusTuple } from "./constants.js";
|
|
3
|
+
import { InvalidResponseError } from "./errors.js";
|
|
4
|
+
const BINARY_CONTENT_TYPE = "application/octet-stream";
|
|
5
|
+
/**
|
|
6
|
+
* Take ownership of caller-supplied response headers.
|
|
7
|
+
*
|
|
8
|
+
* Parsing injects a content type into the header record, so the caller's object
|
|
9
|
+
* must never be aliased — a generator reusing a module-level header object would
|
|
10
|
+
* otherwise leak the content type of one response into the next. Only the shape
|
|
11
|
+
* is checked here; per-value type checks stay in `normalizeResponse` so its
|
|
12
|
+
* distinct messages ("header names/values must be strings") are preserved.
|
|
13
|
+
*/
|
|
14
|
+
function toOwnHeaderRecord(value) {
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
return {};
|
|
17
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
18
|
+
throw new InvalidResponseError("headers must be a string record");
|
|
19
|
+
}
|
|
20
|
+
// Per-value types are `normalizeResponse`'s job (it keeps its own distinct
|
|
21
|
+
// message), so a non-string value is carried through here rather than
|
|
22
|
+
// rejected; this pass only shape-checks and detaches the caller's object.
|
|
23
|
+
const record = {};
|
|
24
|
+
Object.assign(record, value);
|
|
25
|
+
return record;
|
|
26
|
+
}
|
|
27
|
+
function hasContentType(headers) {
|
|
28
|
+
return Object.keys(headers).some((header) => header.toLowerCase() === "content-type");
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Detect the object response envelope `{ status, body, headers? }`.
|
|
32
|
+
*
|
|
33
|
+
* Detection is by shape, so a legitimate domain object carrying a numeric
|
|
34
|
+
* `status` next to a `body` is unwrapped instead of being delivered as the
|
|
35
|
+
* payload. Callers who need to return such a shape as data should nest it or
|
|
36
|
+
* use an explicit `[status, body]` tuple for the envelope. An object whose
|
|
37
|
+
* `headers` is present but not a string record is deliberately NOT an envelope
|
|
38
|
+
* and is delivered whole — plugins that inspect responses must use this same
|
|
39
|
+
* rule (see `@schmock/validation`) or they will judge an undelivered payload.
|
|
40
|
+
*/
|
|
2
41
|
function isResponseObject(value) {
|
|
3
42
|
return (typeof value === "object" &&
|
|
4
43
|
value !== null &&
|
|
44
|
+
!Array.isArray(value) &&
|
|
5
45
|
"status" in value &&
|
|
6
46
|
typeof value.status === "number" &&
|
|
7
|
-
"body" in value
|
|
47
|
+
"body" in value &&
|
|
48
|
+
(!("headers" in value) ||
|
|
49
|
+
value.headers === undefined ||
|
|
50
|
+
isStringRecord(value.headers)));
|
|
51
|
+
}
|
|
52
|
+
function isStringRecord(value) {
|
|
53
|
+
return (typeof value === "object" &&
|
|
54
|
+
value !== null &&
|
|
55
|
+
!Array.isArray(value) &&
|
|
56
|
+
Object.values(value).every((entry) => typeof entry === "string"));
|
|
8
57
|
}
|
|
9
58
|
/**
|
|
10
59
|
* Parse and normalize response result into Response object
|
|
@@ -17,15 +66,15 @@ export function parseResponse(result, routeConfig) {
|
|
|
17
66
|
let tupleFormat = false;
|
|
18
67
|
// Handle already-formed response objects (from plugin error recovery)
|
|
19
68
|
if (isResponseObject(result)) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
};
|
|
69
|
+
status = result.status;
|
|
70
|
+
body = result.body;
|
|
71
|
+
headers = toOwnHeaderRecord(result.headers);
|
|
72
|
+
tupleFormat = true;
|
|
25
73
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
[status, body
|
|
74
|
+
else if (isStatusTuple(result)) {
|
|
75
|
+
// Handle tuple response format [status, body, headers?]
|
|
76
|
+
[status, body] = result;
|
|
77
|
+
headers = toOwnHeaderRecord(result[2]);
|
|
29
78
|
tupleFormat = true;
|
|
30
79
|
}
|
|
31
80
|
// Handle null/undefined responses with 204 No Content
|
|
@@ -36,18 +85,36 @@ export function parseResponse(result, routeConfig) {
|
|
|
36
85
|
}
|
|
37
86
|
body = undefined; // Ensure body is undefined for null responses
|
|
38
87
|
}
|
|
88
|
+
const binaryBody = isBinaryBody(body);
|
|
89
|
+
// Binary response values need a transport-safe MIME type. Tuple headers still
|
|
90
|
+
// take precedence, while a non-JSON route override (for example image/png)
|
|
91
|
+
// remains authoritative for non-tuple responses.
|
|
92
|
+
if (!hasContentType(headers) && binaryBody) {
|
|
93
|
+
headers["content-type"] =
|
|
94
|
+
!tupleFormat &&
|
|
95
|
+
routeConfig.contentType &&
|
|
96
|
+
routeConfig.contentType !== "application/json"
|
|
97
|
+
? routeConfig.contentType
|
|
98
|
+
: BINARY_CONTENT_TYPE;
|
|
99
|
+
}
|
|
39
100
|
// Add content-type header from route config if it exists and headers don't already have it
|
|
40
101
|
// But only if this isn't a tuple response (where headers are explicitly controlled)
|
|
41
|
-
|
|
102
|
+
let appliedRouteContentType = false;
|
|
103
|
+
if (!hasContentType(headers) && routeConfig.contentType && !tupleFormat) {
|
|
42
104
|
headers["content-type"] = routeConfig.contentType;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
105
|
+
appliedRouteContentType = true;
|
|
106
|
+
}
|
|
107
|
+
// Handle special conversion cases when contentType is explicitly set. A
|
|
108
|
+
// binary body keeps its bytes even when a custom MIME type is configured.
|
|
109
|
+
if (appliedRouteContentType &&
|
|
110
|
+
routeConfig.contentType === "text/plain" &&
|
|
111
|
+
body !== undefined &&
|
|
112
|
+
!binaryBody) {
|
|
113
|
+
if (typeof body === "object") {
|
|
114
|
+
body = JSON.stringify(body);
|
|
115
|
+
}
|
|
116
|
+
else if (typeof body !== "string") {
|
|
117
|
+
body = String(body);
|
|
51
118
|
}
|
|
52
119
|
}
|
|
53
120
|
return {
|
package/dist/route-matcher.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ export declare function findRoute(method: Schmock.HttpMethod, path: string, stat
|
|
|
19
19
|
/**
|
|
20
20
|
* Extract parameter values from path based on route pattern
|
|
21
21
|
* Maps capture groups from regex match to parameter names
|
|
22
|
+
*
|
|
23
|
+
* Matching runs on the ENCODED path so `%2F` cannot act as a separator;
|
|
24
|
+
* captures are decoded afterwards, so a generator receives readable values.
|
|
22
25
|
*/
|
|
23
26
|
export declare function extractParams(route: CompiledCallableRoute, path: string): Record<string, string>;
|
|
24
27
|
//# sourceMappingURL=route-matcher.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../src/route-matcher.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC;IAC7B,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC;CAC7B;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,OAAO,CAAC,SAAS,GACrB,GAAG,IAAI,OAAO,CAAC,iBAAiB,CAElC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,qBAAqB,CAAC,EAChD,MAAM,EAAE,qBAAqB,EAAE,GAC9B,qBAAqB,GAAG,SAAS,CAmBnC;AAED
|
|
1
|
+
{"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../src/route-matcher.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC;IAC7B,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC;CAC7B;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,OAAO,CAAC,SAAS,GACrB,GAAG,IAAI,OAAO,CAAC,iBAAiB,CAElC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,qBAAqB,CAAC,EAChD,MAAM,EAAE,qBAAqB,EAAE,GAC9B,qBAAqB,GAAG,SAAS,CAmBnC;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,qBAAqB,EAC5B,IAAI,EAAE,MAAM,GACX,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAcxB"}
|
package/dist/route-matcher.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizePath } from "./constants.js";
|
|
1
|
+
import { decodePathSegment, normalizePath } from "./constants.js";
|
|
2
2
|
export function isGeneratorFunction(gen) {
|
|
3
3
|
return typeof gen === "function";
|
|
4
4
|
}
|
|
@@ -26,14 +26,20 @@ export function findRoute(method, path, staticRoutes, routes) {
|
|
|
26
26
|
/**
|
|
27
27
|
* Extract parameter values from path based on route pattern
|
|
28
28
|
* Maps capture groups from regex match to parameter names
|
|
29
|
+
*
|
|
30
|
+
* Matching runs on the ENCODED path so `%2F` cannot act as a separator;
|
|
31
|
+
* captures are decoded afterwards, so a generator receives readable values.
|
|
29
32
|
*/
|
|
30
33
|
export function extractParams(route, path) {
|
|
31
34
|
const match = path.match(route.pattern);
|
|
32
35
|
if (!match)
|
|
33
36
|
return {};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return params
|
|
37
|
+
// Object.fromEntries defines own properties, so a parameter named
|
|
38
|
+
// `__proto__` (or any other Object.prototype key) survives instead of being
|
|
39
|
+
// swallowed by the prototype setter. The prototype itself is retained so
|
|
40
|
+
// generators can still call params.hasOwnProperty(...).
|
|
41
|
+
return Object.fromEntries(route.params.map((param, index) => [
|
|
42
|
+
param,
|
|
43
|
+
decodePathSegment(match[index + 1]),
|
|
44
|
+
]));
|
|
39
45
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type Schema = Schmock.Schema;
|
|
2
|
+
export type SchemaDefinition = Schmock.SchemaDefinition;
|
|
1
3
|
export type HttpMethod = Schmock.HttpMethod;
|
|
2
4
|
export type RouteKey = Schmock.RouteKey;
|
|
3
5
|
export type ResponseBody = Schmock.ResponseBody;
|
|
@@ -17,15 +19,18 @@ export type StaticData = Schmock.StaticData;
|
|
|
17
19
|
export type RequestRecord = Schmock.RequestRecord;
|
|
18
20
|
export type ServerInfo = Schmock.ServerInfo;
|
|
19
21
|
export type RouteInfo = Schmock.RouteInfo;
|
|
22
|
+
export type ResourceOverride = Schmock.ResourceOverride;
|
|
23
|
+
export type ResponseHeaderDef = Schmock.ResponseHeaderDef;
|
|
24
|
+
export type CrudOperationMeta = Schmock.CrudOperationMeta;
|
|
20
25
|
export type SchemaGenerationContext = Schmock.SchemaGenerationContext;
|
|
21
26
|
export type FakerPluginOptions = Schmock.FakerPluginOptions;
|
|
22
27
|
export type ExpressAdapterOptions = Schmock.ExpressAdapterOptions;
|
|
23
28
|
export type AngularAdapterOptions = Schmock.AngularAdapterOptions;
|
|
24
29
|
export type OpenApiOptions = Schmock.OpenApiOptions;
|
|
30
|
+
export type OpenApiCallbackRequest = Schmock.OpenApiCallbackRequest;
|
|
31
|
+
export type OpenApiCallbackOptions = Schmock.OpenApiCallbackOptions;
|
|
25
32
|
export type SeedSource = Schmock.SeedSource;
|
|
26
33
|
export type SeedConfig = Schmock.SeedConfig;
|
|
27
|
-
export type CliOptions = Schmock.CliOptions;
|
|
28
|
-
export type CliServer = Schmock.CliServer;
|
|
29
34
|
export type AdapterRequest = Schmock.AdapterRequest;
|
|
30
35
|
export type AdapterResponse = Schmock.AdapterResponse;
|
|
31
36
|
export type InterceptOptions = Schmock.InterceptOptions;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;AAC9C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;AAChE,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AACpC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC;AACtE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;AAC5D,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AACpC,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;AACxD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;AAC9C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;AAChE,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AACpC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;AACxD,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC;AACtE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;AAC5D,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC;AACpE,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC;AACpE,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;AACtD,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;AACxD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;AACtD,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schmock/core",
|
|
3
3
|
"description": "Core functionality for Schmock",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.1",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/khalic-lab/schmock.git",
|
|
9
|
+
"directory": "packages/core"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/khalic-lab/schmock/tree/main/packages/core#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/khalic-lab/schmock/issues"
|
|
14
|
+
},
|
|
6
15
|
"main": "./dist/index.js",
|
|
7
16
|
"types": "./dist/index.d.ts",
|
|
8
17
|
"files": [
|
|
9
|
-
"dist"
|
|
10
|
-
"src"
|
|
18
|
+
"dist"
|
|
11
19
|
],
|
|
12
20
|
"exports": {
|
|
13
21
|
".": {
|
|
@@ -18,11 +26,12 @@
|
|
|
18
26
|
"./package.json": "./package.json"
|
|
19
27
|
},
|
|
20
28
|
"scripts": {
|
|
21
|
-
"
|
|
22
|
-
"build
|
|
29
|
+
"clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo",
|
|
30
|
+
"build": "bun run clean && bun run build:lib && bun run build:types",
|
|
31
|
+
"build:lib": "tsc -p tsconfig.runtime.json",
|
|
23
32
|
"build:types": "rm -f tsconfig.tsbuildinfo && tsc --build && node scripts/embed-ambient-types.js",
|
|
24
33
|
"pretest": "rm -f src/*.js src/*.d.ts || true",
|
|
25
|
-
"test": "vitest",
|
|
34
|
+
"test": "vitest run",
|
|
26
35
|
"test:watch": "vitest --watch",
|
|
27
36
|
"pretest:bdd": "rm -f src/*.js src/*.d.ts || true",
|
|
28
37
|
"test:bdd": "vitest run --config vitest.config.bdd.ts",
|
|
@@ -31,11 +40,12 @@
|
|
|
31
40
|
"check:publish": "publint && attw --pack --ignore-rules cjs-resolves-to-esm"
|
|
32
41
|
},
|
|
33
42
|
"license": "MIT",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@types/json-schema": "^7.0.15"
|
|
45
|
+
},
|
|
34
46
|
"devDependencies": {
|
|
35
47
|
"@amiceli/vitest-cucumber": "^7.0.0",
|
|
36
|
-
"@types/json-schema": "^7.0.15",
|
|
37
48
|
"@types/node": "^26.1.0",
|
|
38
|
-
"@vitest/ui": "^4.1.9",
|
|
39
49
|
"vitest": "^4.1.9"
|
|
40
50
|
}
|
|
41
51
|
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { schmock } from "./index";
|
|
3
|
-
|
|
4
|
-
// ── FIX 1.2: defineRoute must shallow-clone the config ────────────────────────
|
|
5
|
-
|
|
6
|
-
describe("FIX 1.2 — defineRoute must not mutate the caller's config", () => {
|
|
7
|
-
it("does not add contentType to the caller's config object", () => {
|
|
8
|
-
const sharedConfig: Schmock.RouteConfig = {};
|
|
9
|
-
const mock = schmock();
|
|
10
|
-
mock("GET /a", { x: 1 }, sharedConfig);
|
|
11
|
-
// sharedConfig must remain empty — defineRoute must work on a clone
|
|
12
|
-
expect(Object.keys(sharedConfig)).toHaveLength(0);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("second route registered with same config is not polluted by first route detection", () => {
|
|
16
|
-
const sharedConfig: Schmock.RouteConfig = {};
|
|
17
|
-
const mock = schmock();
|
|
18
|
-
mock("GET /first", { x: 1 }, sharedConfig);
|
|
19
|
-
mock("GET /second", "hello", sharedConfig);
|
|
20
|
-
// sharedConfig must still be clean — neither call should have written to it
|
|
21
|
-
expect(Object.keys(sharedConfig)).toHaveLength(0);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("mutating caller's config after registration does not change the registered route response", async () => {
|
|
25
|
-
const config: Schmock.RouteConfig = {};
|
|
26
|
-
const mock = schmock();
|
|
27
|
-
mock("GET /item", { value: 99 }, config);
|
|
28
|
-
|
|
29
|
-
// After registration, mutate the caller's config
|
|
30
|
-
config.status = 503;
|
|
31
|
-
config.contentType = "text/html";
|
|
32
|
-
|
|
33
|
-
const response = await mock.handle("GET", "/item");
|
|
34
|
-
// The registered route must use its own cloned config — not 503
|
|
35
|
-
expect(response.status).toBe(200);
|
|
36
|
-
const ct =
|
|
37
|
-
response.headers?.["content-type"] ?? response.headers?.["Content-Type"];
|
|
38
|
-
expect(ct).toContain("application/json");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("each route keeps its own detected contentType even when sharing the same config object", async () => {
|
|
42
|
-
const sharedConfig: Schmock.RouteConfig = {};
|
|
43
|
-
const mock = schmock();
|
|
44
|
-
// First route: object → application/json
|
|
45
|
-
mock("GET /obj", { value: 1 }, sharedConfig);
|
|
46
|
-
// Second route: primitive string → text/plain
|
|
47
|
-
mock("GET /str", "hello", sharedConfig);
|
|
48
|
-
|
|
49
|
-
const objResp = await mock.handle("GET", "/obj");
|
|
50
|
-
const strResp = await mock.handle("GET", "/str");
|
|
51
|
-
|
|
52
|
-
const ctObj =
|
|
53
|
-
objResp.headers?.["content-type"] ?? objResp.headers?.["Content-Type"];
|
|
54
|
-
const ctStr =
|
|
55
|
-
strResp.headers?.["content-type"] ?? strResp.headers?.["Content-Type"];
|
|
56
|
-
|
|
57
|
-
expect(ctObj).toContain("application/json");
|
|
58
|
-
expect(ctStr).toContain("text/plain");
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// ── FIX 2.2: duplicate route detection must normalize trailing slash ──────────
|
|
63
|
-
|
|
64
|
-
describe("FIX 2.2 — duplicate-route detection uses normalized paths", () => {
|
|
65
|
-
it("GET /users/ is treated as duplicate of GET /users", () => {
|
|
66
|
-
const mock = schmock();
|
|
67
|
-
mock("GET /users", [{ id: 1 }], {});
|
|
68
|
-
// Registering with trailing slash must not add a second route
|
|
69
|
-
mock("GET /users/", [{ id: 2 }], {});
|
|
70
|
-
|
|
71
|
-
const routes = mock.getRoutes();
|
|
72
|
-
const userRoutes = routes.filter(
|
|
73
|
-
(r) =>
|
|
74
|
-
r.method === "GET" && (r.path === "/users" || r.path === "/users/"),
|
|
75
|
-
);
|
|
76
|
-
expect(userRoutes).toHaveLength(1);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("first registration wins — trailing-slash variant does not overwrite", async () => {
|
|
80
|
-
const mock = schmock();
|
|
81
|
-
mock("GET /items", [{ id: 1 }], {});
|
|
82
|
-
mock("GET /items/", [{ id: 999 }], {});
|
|
83
|
-
|
|
84
|
-
// Should still get the first registration's data
|
|
85
|
-
const resp = await mock.handle("GET", "/items");
|
|
86
|
-
expect(resp.status).toBe(200);
|
|
87
|
-
expect(Array.isArray(resp.body)).toBe(true);
|
|
88
|
-
expect((resp.body as { id: number }[])[0].id).toBe(1);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("registering verbatim duplicate also produces only one route", () => {
|
|
92
|
-
const mock = schmock();
|
|
93
|
-
mock("GET /same", { ok: true }, {});
|
|
94
|
-
mock("GET /same", { ok: false }, {});
|
|
95
|
-
|
|
96
|
-
const routes = mock.getRoutes();
|
|
97
|
-
const sameRoutes = routes.filter(
|
|
98
|
-
(r) => r.method === "GET" && r.path === "/same",
|
|
99
|
-
);
|
|
100
|
-
expect(sameRoutes).toHaveLength(1);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// ── FIX 3.3: reset()/resetState() must reassign, not delete keys ─────────────
|
|
105
|
-
|
|
106
|
-
describe("FIX 3.3 — reset()/resetState() must not mutate caller's state", () => {
|
|
107
|
-
it("reset() leaves the external state object untouched", () => {
|
|
108
|
-
const st = { a: 1 };
|
|
109
|
-
const mock = schmock({ state: st });
|
|
110
|
-
mock.reset();
|
|
111
|
-
expect(st).toEqual({ a: 1 });
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("resetState() leaves the external state object untouched", () => {
|
|
115
|
-
const st = { b: 2 };
|
|
116
|
-
const mock = schmock({ state: st });
|
|
117
|
-
mock.resetState();
|
|
118
|
-
expect(st).toEqual({ b: 2 });
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("mock.getState() is empty after reset()", () => {
|
|
122
|
-
const st = { x: 10 };
|
|
123
|
-
const mock = schmock({ state: st });
|
|
124
|
-
mock.reset();
|
|
125
|
-
expect(mock.getState()).toEqual({});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("mock.getState() is empty after resetState()", () => {
|
|
129
|
-
const st = { y: 20 };
|
|
130
|
-
const mock = schmock({ state: st });
|
|
131
|
-
mock.resetState();
|
|
132
|
-
expect(mock.getState()).toEqual({});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("generator sees empty state after resetState", async () => {
|
|
136
|
-
const st: Record<string, unknown> = { counter: 0 };
|
|
137
|
-
const mock = schmock({ state: st });
|
|
138
|
-
mock("GET /stateful", (ctx: Schmock.RequestContext) => {
|
|
139
|
-
return { stateKeys: Object.keys(ctx.state) };
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
mock.resetState();
|
|
143
|
-
|
|
144
|
-
const resp = await mock.handle("GET", "/stateful");
|
|
145
|
-
expect(resp.status).toBe(200);
|
|
146
|
-
// After resetState, the generator should see an empty state
|
|
147
|
-
expect((resp.body as { stateKeys: string[] }).stateKeys).toHaveLength(0);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// ── FIX 2.3: history()/lastRequest() must return deep clones ─────────────────
|
|
152
|
-
|
|
153
|
-
describe("FIX 2.3 — history() and lastRequest() return deep clones", () => {
|
|
154
|
-
it("mutating a history() record's nested body does not corrupt internal history", async () => {
|
|
155
|
-
const mock = schmock();
|
|
156
|
-
mock("GET /nested", { deep: { value: "original" } }, {});
|
|
157
|
-
|
|
158
|
-
await mock.handle("GET", "/nested");
|
|
159
|
-
|
|
160
|
-
// First read: mutate the returned record's body
|
|
161
|
-
const records1 = mock.history();
|
|
162
|
-
const firstRecord = records1[0];
|
|
163
|
-
(firstRecord.response.body as { deep: { value: string } }).deep.value =
|
|
164
|
-
"MUTATED";
|
|
165
|
-
|
|
166
|
-
// Second read: internal history must be unchanged
|
|
167
|
-
const records2 = mock.history();
|
|
168
|
-
expect(
|
|
169
|
-
(records2[0].response.body as { deep: { value: string } }).deep.value,
|
|
170
|
-
).toBe("original");
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("mutating filtered history() record does not corrupt internal history", async () => {
|
|
174
|
-
const mock = schmock();
|
|
175
|
-
mock("GET /data", { info: { count: 42 } }, {});
|
|
176
|
-
await mock.handle("GET", "/data");
|
|
177
|
-
|
|
178
|
-
const filtered = mock.history("GET", "/data");
|
|
179
|
-
(filtered[0].response.body as { info: { count: number } }).info.count = 0;
|
|
180
|
-
|
|
181
|
-
const again = mock.history("GET", "/data");
|
|
182
|
-
expect(
|
|
183
|
-
(again[0].response.body as { info: { count: number } }).info.count,
|
|
184
|
-
).toBe(42);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("mutating lastRequest() record does not corrupt internal history", async () => {
|
|
188
|
-
const mock = schmock();
|
|
189
|
-
mock("GET /last", { meta: { tag: "safe" } }, {});
|
|
190
|
-
await mock.handle("GET", "/last");
|
|
191
|
-
|
|
192
|
-
const last = mock.lastRequest();
|
|
193
|
-
if (!last) throw new Error("expected a record");
|
|
194
|
-
(last.response.body as { meta: { tag: string } }).meta.tag = "corrupted";
|
|
195
|
-
|
|
196
|
-
const again = mock.lastRequest();
|
|
197
|
-
if (!again) throw new Error("expected a record");
|
|
198
|
-
expect((again.response.body as { meta: { tag: string } }).meta.tag).toBe(
|
|
199
|
-
"safe",
|
|
200
|
-
);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("mutating filtered lastRequest() record does not corrupt internal history", async () => {
|
|
204
|
-
const mock = schmock();
|
|
205
|
-
mock("POST /echo", { msg: { text: "hello" } }, {});
|
|
206
|
-
await mock.handle("POST", "/echo");
|
|
207
|
-
|
|
208
|
-
const last = mock.lastRequest("POST", "/echo");
|
|
209
|
-
if (!last) throw new Error("expected a record");
|
|
210
|
-
(last.response.body as { msg: { text: string } }).msg.text = "CHANGED";
|
|
211
|
-
|
|
212
|
-
const again = mock.lastRequest("POST", "/echo");
|
|
213
|
-
if (!again) throw new Error("expected a record");
|
|
214
|
-
expect((again.response.body as { msg: { text: string } }).msg.text).toBe(
|
|
215
|
-
"hello",
|
|
216
|
-
);
|
|
217
|
-
});
|
|
218
|
-
});
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { schmock } from "./index";
|
|
3
|
-
|
|
4
|
-
describe("isResponseObject guard — status type check", () => {
|
|
5
|
-
describe("non-numeric status must NOT be treated as response envelope", () => {
|
|
6
|
-
it("returns status 200 (default) when generator returns { status: 'ok', body: 'hello' }", async () => {
|
|
7
|
-
const mock = schmock();
|
|
8
|
-
mock("GET /bad-envelope", () => ({ status: "ok", body: "hello" }));
|
|
9
|
-
|
|
10
|
-
const response = await mock.handle("GET", "/bad-envelope");
|
|
11
|
-
|
|
12
|
-
// The string status must never leak into response.status
|
|
13
|
-
expect(response.status).toBe(200);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("returns the whole object as body when generator returns { status: 'ok', body: 'hello' }", async () => {
|
|
17
|
-
const mock = schmock();
|
|
18
|
-
mock("GET /bad-envelope-body", () => ({ status: "ok", body: "hello" }));
|
|
19
|
-
|
|
20
|
-
const response = await mock.handle("GET", "/bad-envelope-body");
|
|
21
|
-
|
|
22
|
-
// The original object must be treated as plain body, not unwrapped
|
|
23
|
-
expect(response.body).toEqual({ status: "ok", body: "hello" });
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe("numeric status IS correctly treated as response envelope", () => {
|
|
28
|
-
it("returns status 201 and unwrapped body when generator returns { status: 201, body: { id: 1 } }", async () => {
|
|
29
|
-
const mock = schmock();
|
|
30
|
-
mock("POST /create", () => ({ status: 201, body: { id: 1 } }));
|
|
31
|
-
|
|
32
|
-
const response = await mock.handle("POST", "/create");
|
|
33
|
-
|
|
34
|
-
expect(response.status).toBe(201);
|
|
35
|
-
expect(response.body).toEqual({ id: 1 });
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
});
|