@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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/dist/abort.d.ts +3 -0
  4. package/dist/abort.d.ts.map +1 -0
  5. package/dist/abort.js +30 -0
  6. package/dist/binary.d.ts +8 -0
  7. package/dist/binary.d.ts.map +1 -0
  8. package/dist/binary.js +9 -0
  9. package/dist/builder.d.ts +31 -10
  10. package/dist/builder.d.ts.map +1 -1
  11. package/dist/builder.js +804 -203
  12. package/dist/constants.d.ts +38 -0
  13. package/dist/constants.d.ts.map +1 -1
  14. package/dist/constants.js +123 -2
  15. package/dist/errors.d.ts +14 -3
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +41 -6
  18. package/dist/helpers.d.ts +9 -0
  19. package/dist/helpers.d.ts.map +1 -1
  20. package/dist/helpers.js +18 -2
  21. package/dist/http-helpers.d.ts +52 -5
  22. package/dist/http-helpers.d.ts.map +1 -1
  23. package/dist/http-helpers.js +153 -37
  24. package/dist/index.d.ts +381 -60
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +9 -3
  27. package/dist/interceptor.d.ts +11 -1
  28. package/dist/interceptor.d.ts.map +1 -1
  29. package/dist/interceptor.js +354 -156
  30. package/dist/parser.d.ts.map +1 -1
  31. package/dist/parser.js +12 -3
  32. package/dist/plugin-pipeline.d.ts +11 -4
  33. package/dist/plugin-pipeline.d.ts.map +1 -1
  34. package/dist/plugin-pipeline.js +117 -39
  35. package/dist/response-normalizer.d.ts +16 -0
  36. package/dist/response-normalizer.d.ts.map +1 -0
  37. package/dist/response-normalizer.js +316 -0
  38. package/dist/response-parser.d.ts.map +1 -1
  39. package/dist/response-parser.js +85 -18
  40. package/dist/route-matcher.d.ts +3 -0
  41. package/dist/route-matcher.d.ts.map +1 -1
  42. package/dist/route-matcher.js +12 -6
  43. package/dist/types.d.ts +7 -2
  44. package/dist/types.d.ts.map +1 -1
  45. package/package.json +18 -8
  46. package/src/audit-core-builder.test.ts +0 -218
  47. package/src/audit-response-guard.test.ts +0 -38
  48. package/src/builder.test.ts +0 -289
  49. package/src/builder.ts +0 -701
  50. package/src/constants.test.ts +0 -99
  51. package/src/constants.ts +0 -73
  52. package/src/debug.test.ts +0 -241
  53. package/src/delay.test.ts +0 -319
  54. package/src/dist-shape.test.ts +0 -35
  55. package/src/errors.test.ts +0 -223
  56. package/src/errors.ts +0 -130
  57. package/src/factory.test.ts +0 -133
  58. package/src/helpers.test.ts +0 -147
  59. package/src/helpers.ts +0 -58
  60. package/src/http-helpers.ts +0 -113
  61. package/src/index.ts +0 -151
  62. package/src/interceptor.test.ts +0 -291
  63. package/src/interceptor.ts +0 -320
  64. package/src/namespace.test.ts +0 -274
  65. package/src/parser.property.test.ts +0 -594
  66. package/src/parser.test.ts +0 -148
  67. package/src/parser.ts +0 -64
  68. package/src/plugin-pipeline.ts +0 -103
  69. package/src/plugin-system.test.ts +0 -602
  70. package/src/response-parser.ts +0 -75
  71. package/src/response-parsing.test.ts +0 -333
  72. package/src/route-matcher.ts +0 -69
  73. package/src/route-matching.test.ts +0 -394
  74. package/src/server.test.ts +0 -165
  75. package/src/smart-defaults.test.ts +0 -361
  76. package/src/steps/async-support.steps.ts +0 -400
  77. package/src/steps/audit-core-builder.steps.ts +0 -188
  78. package/src/steps/audit-onerror-tuple.steps.ts +0 -99
  79. package/src/steps/basic-usage.steps.ts +0 -245
  80. package/src/steps/developer-experience.steps.ts +0 -204
  81. package/src/steps/error-handling.steps.ts +0 -345
  82. package/src/steps/fluent-api.steps.ts +0 -255
  83. package/src/steps/http-methods.steps.ts +0 -331
  84. package/src/steps/interceptor.steps.ts +0 -260
  85. package/src/steps/lifecycle-events.steps.ts +0 -142
  86. package/src/steps/performance-reliability.steps.ts +0 -423
  87. package/src/steps/plugin-integration.steps.ts +0 -280
  88. package/src/steps/request-history.steps.ts +0 -276
  89. package/src/steps/response-delay.steps.ts +0 -88
  90. package/src/steps/route-key-format.steps.ts +0 -99
  91. package/src/steps/standalone-server.steps.ts +0 -233
  92. package/src/steps/state-concurrency.steps.ts +0 -739
  93. package/src/steps/stateful-workflows.steps.ts +0 -353
  94. package/src/types.ts +0 -36
@@ -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
- return {
21
- status: result.status,
22
- body: result.body,
23
- headers: result.headers || {},
24
- };
69
+ status = result.status;
70
+ body = result.body;
71
+ headers = toOwnHeaderRecord(result.headers);
72
+ tupleFormat = true;
25
73
  }
26
- // Handle tuple response format [status, body, headers?]
27
- if (isStatusTuple(result)) {
28
- [status, body, headers = {}] = result;
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
- if (!headers["content-type"] && routeConfig.contentType && !tupleFormat) {
102
+ let appliedRouteContentType = false;
103
+ if (!hasContentType(headers) && routeConfig.contentType && !tupleFormat) {
42
104
  headers["content-type"] = routeConfig.contentType;
43
- // Handle special conversion cases when contentType is explicitly set
44
- if (routeConfig.contentType === "text/plain" && body !== undefined) {
45
- if (typeof body === "object" && !Buffer.isBuffer(body)) {
46
- body = JSON.stringify(body);
47
- }
48
- else if (typeof body !== "string") {
49
- body = String(body);
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 {
@@ -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;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,qBAAqB,EAC5B,IAAI,EAAE,MAAM,GACX,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUxB"}
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"}
@@ -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
- const params = {};
35
- route.params.forEach((param, index) => {
36
- params[param] = match[index + 1];
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;
@@ -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,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,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"}
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.2.3",
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
- "build": "bun build:lib && bun build:types",
22
- "build:lib": "bun build --minify --target node --outdir=dist src/index.ts",
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
- });