@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,9 +1,46 @@
1
1
  import type { HttpMethod } from "./types.js";
2
2
  export declare const ROUTE_NOT_FOUND_CODE: "ROUTE_NOT_FOUND";
3
+ interface ResponseLike {
4
+ status: number;
5
+ body: unknown;
6
+ }
7
+ export declare function markRouteNotFound<T extends ResponseLike>(response: T): T;
8
+ export declare function markResponseException<T extends ResponseLike>(response: T, error: Error): T;
9
+ export declare function getResponseException(response: ResponseLike): Error | undefined;
3
10
  export declare const HTTP_METHODS: readonly HttpMethod[];
4
11
  export declare function isHttpMethod(method: string): method is HttpMethod;
5
12
  export declare function toHttpMethod(method: string): HttpMethod;
6
13
  export declare function normalizePath(path: string): string;
14
+ /**
15
+ * Put a path into the single canonical (percent-encoded) transport form.
16
+ *
17
+ * A path reaches core either as `url.pathname` (already encoded by the URL
18
+ * parser) or as whatever string a `handle()` caller typed, so route paths and
19
+ * request paths must be encoded the same way before they can be compared.
20
+ * Encoding is idempotent: an existing valid `%XX` triplet is normalized to
21
+ * uppercase rather than re-encoded, so applying this at route-parse time and
22
+ * again at request time is safe.
23
+ *
24
+ * This deliberately never throws and never resolves `.`/`..` — an unreachable
25
+ * spelling is preferable to silently rewriting the caller's path.
26
+ */
27
+ export declare function canonicalizePath(path: string): string;
28
+ /**
29
+ * Decode a single captured path parameter.
30
+ *
31
+ * Decoding happens AFTER segmentation, so an encoded separator (`%2F`) stays
32
+ * inside one parameter and reaches the generator as "/" — the same as Express.
33
+ * A malformed sequence (`%ZZ`) is handed back untouched rather than throwing.
34
+ */
35
+ export declare function decodePathSegment(segment: string): string;
36
+ /**
37
+ * Build a route key from a method and a path.
38
+ *
39
+ * `Schmock.RouteKey` requires a leading slash, so a path without one is
40
+ * normalized rather than rejected — callers feed spec paths and pathnames that
41
+ * are already absolute, and silently producing an unreachable route key would
42
+ * be worse than adding the slash.
43
+ */
7
44
  export declare function toRouteKey(method: HttpMethod, path: string): Schmock.RouteKey;
8
45
  /**
9
46
  * Check if a Schmock response is a route-not-found response.
@@ -23,4 +60,5 @@ export declare function isRouteNotFound(response: {
23
60
  * status() helper or return an object response when the data could collide.
24
61
  */
25
62
  export declare function isStatusTuple(value: unknown): value is [number, unknown] | [number, unknown, Record<string, string>];
63
+ export {};
26
64
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAG,iBAA0B,CAAC;AAE/D,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAQpC,CAAC;AAEX,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,UAAU,CAEjE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAMvD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAG7E;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,OAAO,CASV;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAQxE"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAG,iBAA0B,CAAC;AAE/D,UAAU,YAAY;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf;AA+BD,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,YAAY,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAGxE;AAED,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,YAAY,EAC1D,QAAQ,EAAE,CAAC,EACX,KAAK,EAAE,KAAK,GACX,CAAC,CAGH;AAED,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,YAAY,GACrB,KAAK,GAAG,SAAS,CAGnB;AAED,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAQpC,CAAC;AAEX,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,UAAU,CAEjE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAMvD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAwBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA+BrD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMzD;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAK7E;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,OAAO,CAWV;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAQxE"}
package/dist/constants.js CHANGED
@@ -1,4 +1,37 @@
1
1
  export const ROUTE_NOT_FOUND_CODE = "ROUTE_NOT_FOUND";
2
+ const RESPONSE_ORIGIN = Symbol.for("@schmock/core.response-origin");
3
+ function setResponseOrigin(response, origin) {
4
+ Object.defineProperty(response, RESPONSE_ORIGIN, {
5
+ configurable: true,
6
+ value: origin,
7
+ });
8
+ }
9
+ function getResponseOrigin(response) {
10
+ const origin = Reflect.get(response, RESPONSE_ORIGIN);
11
+ if (typeof origin !== "object" || origin === null || !("kind" in origin)) {
12
+ return undefined;
13
+ }
14
+ if (origin.kind === "route-not-found")
15
+ return { kind: "route-not-found" };
16
+ if (origin.kind === "exception" &&
17
+ "error" in origin &&
18
+ origin.error instanceof Error) {
19
+ return { kind: "exception", error: origin.error };
20
+ }
21
+ return undefined;
22
+ }
23
+ export function markRouteNotFound(response) {
24
+ setResponseOrigin(response, { kind: "route-not-found" });
25
+ return response;
26
+ }
27
+ export function markResponseException(response, error) {
28
+ setResponseOrigin(response, { kind: "exception", error });
29
+ return response;
30
+ }
31
+ export function getResponseException(response) {
32
+ const origin = getResponseOrigin(response);
33
+ return origin?.kind === "exception" ? origin.error : undefined;
34
+ }
2
35
  export const HTTP_METHODS = [
3
36
  "GET",
4
37
  "POST",
@@ -21,15 +54,103 @@ export function toHttpMethod(method) {
21
54
  export function normalizePath(path) {
22
55
  return path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
23
56
  }
57
+ /**
58
+ * ASCII characters the URL parser percent-encodes inside a path even though
59
+ * they are printable. `?` and `#` are delimiters there rather than encoded, but
60
+ * encoding them here is what makes a route containing one reachable at all.
61
+ */
62
+ const PATH_ENCODED_ASCII = new Set([
63
+ " ",
64
+ '"',
65
+ "#",
66
+ "<",
67
+ ">",
68
+ "?",
69
+ "^",
70
+ "`",
71
+ "{",
72
+ "}",
73
+ ]);
74
+ const PERCENT_TRIPLET = /^%[0-9A-Fa-f]{2}$/;
75
+ /** UTF-8 for U+FFFD, the URL parser's substitute for a lone surrogate. */
76
+ const ENCODED_REPLACEMENT_CHARACTER = "%EF%BF%BD";
77
+ /**
78
+ * Put a path into the single canonical (percent-encoded) transport form.
79
+ *
80
+ * A path reaches core either as `url.pathname` (already encoded by the URL
81
+ * parser) or as whatever string a `handle()` caller typed, so route paths and
82
+ * request paths must be encoded the same way before they can be compared.
83
+ * Encoding is idempotent: an existing valid `%XX` triplet is normalized to
84
+ * uppercase rather than re-encoded, so applying this at route-parse time and
85
+ * again at request time is safe.
86
+ *
87
+ * This deliberately never throws and never resolves `.`/`..` — an unreachable
88
+ * spelling is preferable to silently rewriting the caller's path.
89
+ */
90
+ export function canonicalizePath(path) {
91
+ let result = "";
92
+ for (let index = 0; index < path.length;) {
93
+ if (path[index] === "%" &&
94
+ PERCENT_TRIPLET.test(path.slice(index, index + 3))) {
95
+ result += path.slice(index, index + 3).toUpperCase();
96
+ index += 3;
97
+ continue;
98
+ }
99
+ const codePoint = path.codePointAt(index) ?? 0;
100
+ const character = String.fromCodePoint(codePoint);
101
+ index += character.length;
102
+ if (codePoint <= 0x1f ||
103
+ codePoint > 0x7e ||
104
+ PATH_ENCODED_ASCII.has(character)) {
105
+ try {
106
+ result += encodeURIComponent(character);
107
+ }
108
+ catch {
109
+ result += ENCODED_REPLACEMENT_CHARACTER;
110
+ }
111
+ }
112
+ else {
113
+ result += character;
114
+ }
115
+ }
116
+ return result;
117
+ }
118
+ /**
119
+ * Decode a single captured path parameter.
120
+ *
121
+ * Decoding happens AFTER segmentation, so an encoded separator (`%2F`) stays
122
+ * inside one parameter and reaches the generator as "/" — the same as Express.
123
+ * A malformed sequence (`%ZZ`) is handed back untouched rather than throwing.
124
+ */
125
+ export function decodePathSegment(segment) {
126
+ try {
127
+ return decodeURIComponent(segment);
128
+ }
129
+ catch {
130
+ return segment;
131
+ }
132
+ }
133
+ /**
134
+ * Build a route key from a method and a path.
135
+ *
136
+ * `Schmock.RouteKey` requires a leading slash, so a path without one is
137
+ * normalized rather than rejected — callers feed spec paths and pathnames that
138
+ * are already absolute, and silently producing an unreachable route key would
139
+ * be worse than adding the slash.
140
+ */
24
141
  export function toRouteKey(method, path) {
25
- const key = `${method} ${path}`;
26
- return key;
142
+ // Built from the slash rather than concatenated onto it, so the template
143
+ // literal proves the leading slash `RouteKey` requires without an assertion.
144
+ const relativePath = path.startsWith("/") ? path.slice(1) : path;
145
+ return `${method} /${relativePath}`;
27
146
  }
28
147
  /**
29
148
  * Check if a Schmock response is a route-not-found response.
30
149
  * Used by adapters to decide whether to pass through to the real backend.
31
150
  */
32
151
  export function isRouteNotFound(response) {
152
+ if (getResponseOrigin(response)?.kind === "route-not-found")
153
+ return true;
33
154
  const { status, body } = response;
34
155
  return (status === 404 &&
35
156
  body !== null &&
package/dist/errors.d.ts CHANGED
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Describe a thrown value.
3
+ *
4
+ * Non-Error throws keep whatever they carry — a bare string, a primitive or the
5
+ * JSON of a plain object — because "Unknown error" is useless while debugging a
6
+ * mock. Only `null`/`undefined` (which carry nothing) and values that cannot be
7
+ * stringified safely fall back to "Unknown error"; a throwing `toString`, a
8
+ * throwing getter or a circular reference must never turn the error path into a
9
+ * second exception. Long stringifications are truncated so 500 bodies and debug
10
+ * logs stay bounded.
11
+ */
1
12
  export declare function errorMessage(error: unknown): string;
2
13
  /**
3
14
  * Base error class for all Schmock errors
@@ -20,10 +31,10 @@ export declare class RouteParseError extends SchmockError {
20
31
  constructor(routeKey: string, reason: string);
21
32
  }
22
33
  /**
23
- * Error thrown when response generation fails
34
+ * Error thrown when a response cannot be represented safely by transports
24
35
  */
25
- export declare class ResponseGenerationError extends SchmockError {
26
- constructor(route: string, error: Error);
36
+ export declare class InvalidResponseError extends SchmockError {
37
+ constructor(reason: string, context?: Record<string, unknown>);
27
38
  }
28
39
  /**
29
40
  * Error thrown when a plugin fails
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEnD;AAED;;GAEG;AACH,qBAAa,YAAa,SAAQ,KAAK;aAGnB,IAAI,EAAE,MAAM;aACZ,OAAO,CAAC,EAAE,OAAO;gBAFjC,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,YAAA;CAQpC;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAOzC;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,YAAY;gBACnC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,YAAY;gBAC3C,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAQxC;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,YAAY;gBAC/B,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAO7C;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;gBACxC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAQnE;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,OAAO;CAQ1D;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAQ7D"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AASA;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAanD;AAED;;GAEG;AACH,qBAAa,YAAa,SAAQ,KAAK;aAGnB,IAAI,EAAE,MAAM;aACZ,OAAO,CAAC,EAAE,OAAO;gBAFjC,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,YAAA;CAQpC;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAOzC;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,YAAY;gBACnC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;gBACxC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;CAOlE;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,YAAY;gBAC/B,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAO7C;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;gBACxC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAQnE;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,OAAO;CAQ1D;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAQ7D"}
package/dist/errors.js CHANGED
@@ -1,5 +1,37 @@
1
+ /** Longest stringification of a non-Error throw kept in a message. */
2
+ const MAX_THROWN_VALUE_LENGTH = 200;
3
+ function truncateThrownValue(text) {
4
+ return text.length > MAX_THROWN_VALUE_LENGTH
5
+ ? `${text.slice(0, MAX_THROWN_VALUE_LENGTH)}…`
6
+ : text;
7
+ }
8
+ /**
9
+ * Describe a thrown value.
10
+ *
11
+ * Non-Error throws keep whatever they carry — a bare string, a primitive or the
12
+ * JSON of a plain object — because "Unknown error" is useless while debugging a
13
+ * mock. Only `null`/`undefined` (which carry nothing) and values that cannot be
14
+ * stringified safely fall back to "Unknown error"; a throwing `toString`, a
15
+ * throwing getter or a circular reference must never turn the error path into a
16
+ * second exception. Long stringifications are truncated so 500 bodies and debug
17
+ * logs stay bounded.
18
+ */
1
19
  export function errorMessage(error) {
2
- return error instanceof Error ? error.message : "Unknown error";
20
+ if (error instanceof Error)
21
+ return error.message;
22
+ if (error === null || error === undefined)
23
+ return "Unknown error";
24
+ if (typeof error === "string")
25
+ return truncateThrownValue(error);
26
+ try {
27
+ const text = typeof error === "object" ? JSON.stringify(error) : String(error);
28
+ if (typeof text !== "string" || text.length === 0)
29
+ return "Unknown error";
30
+ return truncateThrownValue(text);
31
+ }
32
+ catch {
33
+ return "Unknown error";
34
+ }
3
35
  }
4
36
  /**
5
37
  * Base error class for all Schmock errors
@@ -39,12 +71,15 @@ export class RouteParseError extends SchmockError {
39
71
  }
40
72
  }
41
73
  /**
42
- * Error thrown when response generation fails
74
+ * Error thrown when a response cannot be represented safely by transports
43
75
  */
44
- export class ResponseGenerationError extends SchmockError {
45
- constructor(route, error) {
46
- super(`Failed to generate response for route ${route}: ${error.message}`, "RESPONSE_GENERATION_ERROR", { route, originalError: error });
47
- this.name = "ResponseGenerationError";
76
+ export class InvalidResponseError extends SchmockError {
77
+ constructor(reason, context = {}) {
78
+ super(`Invalid response: ${reason}`, "INVALID_RESPONSE", {
79
+ ...context,
80
+ reason,
81
+ });
82
+ this.name = "InvalidResponseError";
48
83
  }
49
84
  }
50
85
  /**
package/dist/helpers.d.ts CHANGED
@@ -5,5 +5,14 @@ export declare function forbidden(message?: string | object): [number, object];
5
5
  export declare function serverError(message?: string | object): [number, object];
6
6
  export declare function created(body: object): [number, object];
7
7
  export declare function noContent(): [number, null];
8
+ /**
9
+ * Slice `items` into a page envelope.
10
+ *
11
+ * `page` and `pageSize` are normalized to positive integers (falling back to
12
+ * page 1 and a page size of 10) so a fractional, negative, NaN or infinite
13
+ * option can never produce a nonsensical slice or a negative `totalPages`. The
14
+ * returned envelope always echoes the NORMALIZED values, so it is internally
15
+ * consistent with `data`.
16
+ */
8
17
  export declare function paginate<T>(items: T[], options?: Schmock.PaginateOptions): Schmock.PaginatedResponse<T>;
9
18
  //# sourceMappingURL=helpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAEA,wBAAgB,QAAQ,CACtB,OAAO,GAAE,MAAM,GAAG,MAAoB,GACrC,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,UAAU,CACxB,OAAO,GAAE,MAAM,GAAG,MAAsB,GACvC,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,YAAY,CAC1B,OAAO,GAAE,MAAM,GAAG,MAAuB,GACxC,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,SAAS,CACvB,OAAO,GAAE,MAAM,GAAG,MAAoB,GACrC,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,WAAW,CACzB,OAAO,GAAE,MAAM,GAAG,MAAgC,GACjD,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAEtD;AAED,wBAAgB,SAAS,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAE1C;AAED,wBAAgB,QAAQ,CAAC,CAAC,EACxB,KAAK,EAAE,CAAC,EAAE,EACV,OAAO,GAAE,OAAO,CAAC,eAAoB,GACpC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAS9B"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAEA,wBAAgB,QAAQ,CACtB,OAAO,GAAE,MAAM,GAAG,MAAoB,GACrC,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,UAAU,CACxB,OAAO,GAAE,MAAM,GAAG,MAAsB,GACvC,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,YAAY,CAC1B,OAAO,GAAE,MAAM,GAAG,MAAuB,GACxC,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,SAAS,CACvB,OAAO,GAAE,MAAM,GAAG,MAAoB,GACrC,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,WAAW,CACzB,OAAO,GAAE,MAAM,GAAG,MAAgC,GACjD,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAEtD;AAED,wBAAgB,SAAS,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAE1C;AAWD;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACxB,KAAK,EAAE,CAAC,EAAE,EACV,OAAO,GAAE,OAAO,CAAC,eAAoB,GACpC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAS9B"}
package/dist/helpers.js CHANGED
@@ -25,9 +25,25 @@ export function created(body) {
25
25
  export function noContent() {
26
26
  return [204, null];
27
27
  }
28
+ /** Default page size used when `pageSize` is absent or not a positive integer. */
29
+ const DEFAULT_PAGE_SIZE = 10;
30
+ function positiveInteger(value, fallback) {
31
+ return typeof value === "number" && Number.isInteger(value) && value >= 1
32
+ ? value
33
+ : fallback;
34
+ }
35
+ /**
36
+ * Slice `items` into a page envelope.
37
+ *
38
+ * `page` and `pageSize` are normalized to positive integers (falling back to
39
+ * page 1 and a page size of 10) so a fractional, negative, NaN or infinite
40
+ * option can never produce a nonsensical slice or a negative `totalPages`. The
41
+ * returned envelope always echoes the NORMALIZED values, so it is internally
42
+ * consistent with `data`.
43
+ */
28
44
  export function paginate(items, options = {}) {
29
- const page = options.page || 1;
30
- const pageSize = options.pageSize || 10;
45
+ const page = positiveInteger(options.page, 1);
46
+ const pageSize = positiveInteger(options.pageSize, DEFAULT_PAGE_SIZE);
31
47
  const total = items.length;
32
48
  const totalPages = Math.ceil(total / pageSize);
33
49
  const start = (page - 1) * pageSize;
@@ -1,25 +1,72 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
1
+ interface RequestWithHeaders {
2
+ readonly headers: {
3
+ readonly [header: string]: string | string[] | undefined;
4
+ };
5
+ }
6
+ interface BodyReadable {
7
+ on(event: "aborted", listener: () => void): this;
8
+ on(event: "close", listener: () => void): this;
9
+ on(event: "error", listener: (error: Error) => void): this;
10
+ on(event: "data", listener: (chunk: Uint8Array) => void): this;
11
+ on(event: "end", listener: () => void): this;
12
+ destroy(error?: Error): this;
13
+ }
14
+ interface ResponseWritable {
15
+ writeHead(status: number, headers: Record<string, string>): this;
16
+ end(body?: string | Uint8Array): this;
17
+ }
18
+ export type HttpIngressErrorCode = "MALFORMED_JSON" | "PAYLOAD_TOO_LARGE";
19
+ /** An HTTP client error raised while collecting an incoming request body. */
20
+ export declare class HttpIngressError extends Error {
21
+ readonly status: 400 | 413;
22
+ readonly code: HttpIngressErrorCode;
23
+ constructor(status: 400 | 413, code: HttpIngressErrorCode, message: string);
24
+ }
2
25
  /**
3
26
  * Convert Node.js IncomingMessage headers to a flat Record<string, string>.
4
27
  * Drops array-valued headers (keeps only string values).
5
28
  */
6
- export declare function parseNodeHeaders(req: IncomingMessage): Record<string, string>;
29
+ export declare function parseNodeHeaders(req: RequestWithHeaders): Record<string, string>;
7
30
  /**
8
31
  * Extract query parameters from a URL as a flat Record<string, string>.
9
32
  */
10
33
  export declare function parseNodeQuery(url: URL): Record<string, string>;
11
34
  /**
12
35
  * Collect and parse the request body from a Node.js IncomingMessage.
13
- * Returns parsed JSON if content-type includes "json", otherwise the raw string.
36
+ * Returns parsed JSON for application/json and +json media types, otherwise the
37
+ * raw string.
14
38
  * Returns undefined for empty bodies.
15
39
  * @param req - Node.js IncomingMessage
16
40
  * @param headers - Parsed request headers
17
41
  * @param maxBodySize - Maximum body size in bytes (default: 10 MB)
18
42
  */
19
- export declare function collectBody(req: IncomingMessage, headers: Record<string, string>, maxBodySize?: number): Promise<unknown>;
43
+ export declare function collectBody(req: BodyReadable, headers: Record<string, string>, maxBodySize?: number): Promise<unknown>;
44
+ interface RejectedRequestReadable {
45
+ resume(): unknown;
46
+ on(event: "data", listener: (chunk: Uint8Array) => void): unknown;
47
+ off(event: "data", listener: (chunk: Uint8Array) => void): unknown;
48
+ once(event: "end" | "close", listener: () => void): unknown;
49
+ }
50
+ interface RejectedResponseWritable extends ResponseWritable {
51
+ readonly writableEnded: boolean;
52
+ write(chunk: string | Uint8Array): unknown;
53
+ once(event: "close", listener: () => void): unknown;
54
+ }
20
55
  /**
21
56
  * Write a Schmock Response to a Node.js ServerResponse.
22
57
  * Serializes non-string bodies as JSON and sets content-type when missing.
23
58
  */
24
- export declare function writeSchmockResponse(res: ServerResponse, response: Schmock.Response, extraHeaders?: Record<string, string>): void;
59
+ export declare function writeSchmockResponse(res: ResponseWritable, response: Schmock.Response, extraHeaders?: Record<string, string>): void;
60
+ /**
61
+ * Write a rejection (e.g. 413) while the client may still be uploading.
62
+ *
63
+ * Ending the response immediately makes Node tear the socket down while
64
+ * request bytes are in flight; the resulting TCP reset discards the
65
+ * already-written error from the client's receive buffer, so the client
66
+ * observes ECONNRESET instead of the response. Instead the response body is
67
+ * flushed right away and the end is deferred — a lingering close — until the
68
+ * request finishes, goes idle, or exhausts the grace cap.
69
+ */
70
+ export declare function writeRejectedSchmockResponse(req: RejectedRequestReadable, res: RejectedResponseWritable, response: Schmock.Response, extraHeaders?: Record<string, string>): void;
71
+ export {};
25
72
  //# sourceMappingURL=http-helpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"http-helpers.d.ts","sourceRoot":"","sources":["../src/http-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ7E;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAM/D;AAKD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,WAAW,SAAwB,GAClC,OAAO,CAAC,OAAO,CAAC,CAqClB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAC1B,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,IAAI,CAuBN"}
1
+ {"version":3,"file":"http-helpers.d.ts","sourceRoot":"","sources":["../src/http-helpers.ts"],"names":[],"mappings":"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"}