@schmock/core 2.3.0 → 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 (93) 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/builder.d.ts +31 -10
  7. package/dist/builder.d.ts.map +1 -1
  8. package/dist/builder.js +760 -190
  9. package/dist/constants.d.ts +38 -0
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/constants.js +123 -2
  12. package/dist/errors.d.ts +14 -3
  13. package/dist/errors.d.ts.map +1 -1
  14. package/dist/errors.js +41 -6
  15. package/dist/helpers.d.ts +9 -0
  16. package/dist/helpers.d.ts.map +1 -1
  17. package/dist/helpers.js +18 -2
  18. package/dist/http-helpers.d.ts +39 -4
  19. package/dist/http-helpers.d.ts.map +1 -1
  20. package/dist/http-helpers.js +147 -49
  21. package/dist/index.d.ts +292 -34
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +8 -3
  24. package/dist/interceptor.d.ts +11 -1
  25. package/dist/interceptor.d.ts.map +1 -1
  26. package/dist/interceptor.js +316 -169
  27. package/dist/parser.d.ts.map +1 -1
  28. package/dist/parser.js +12 -3
  29. package/dist/plugin-pipeline.d.ts +3 -3
  30. package/dist/plugin-pipeline.d.ts.map +1 -1
  31. package/dist/plugin-pipeline.js +29 -14
  32. package/dist/response-normalizer.d.ts +16 -0
  33. package/dist/response-normalizer.d.ts.map +1 -0
  34. package/dist/response-normalizer.js +316 -0
  35. package/dist/response-parser.d.ts.map +1 -1
  36. package/dist/response-parser.js +39 -2
  37. package/dist/route-matcher.d.ts +3 -0
  38. package/dist/route-matcher.d.ts.map +1 -1
  39. package/dist/route-matcher.js +12 -6
  40. package/dist/types.d.ts +7 -2
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +14 -6
  43. package/src/audit-core-builder.test.ts +0 -218
  44. package/src/audit-response-guard.test.ts +0 -38
  45. package/src/binary.test.ts +0 -126
  46. package/src/binary.ts +0 -11
  47. package/src/builder.test.ts +0 -289
  48. package/src/builder.ts +0 -748
  49. package/src/constants.test.ts +0 -99
  50. package/src/constants.ts +0 -73
  51. package/src/debug.test.ts +0 -241
  52. package/src/delay.test.ts +0 -319
  53. package/src/dist-shape.test.ts +0 -35
  54. package/src/errors.test.ts +0 -223
  55. package/src/errors.ts +0 -130
  56. package/src/factory.test.ts +0 -133
  57. package/src/helpers.test.ts +0 -147
  58. package/src/helpers.ts +0 -58
  59. package/src/http-helpers.test.ts +0 -39
  60. package/src/http-helpers.ts +0 -149
  61. package/src/index.ts +0 -152
  62. package/src/interceptor.test.ts +0 -488
  63. package/src/interceptor.ts +0 -407
  64. package/src/namespace.test.ts +0 -274
  65. package/src/parser.property.test.ts +0 -630
  66. package/src/parser.test.ts +0 -148
  67. package/src/parser.ts +0 -64
  68. package/src/plugin-pipeline.ts +0 -208
  69. package/src/plugin-system.test.ts +0 -697
  70. package/src/response-parser.ts +0 -114
  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 -234
  75. package/src/smart-defaults.test.ts +0 -361
  76. package/src/steps/async-support.steps.ts +0 -421
  77. package/src/steps/audit-core-builder.steps.ts +0 -188
  78. package/src/steps/audit-onerror-tuple.steps.ts +0 -83
  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 -555
  82. package/src/steps/fetch-interceptor.steps.ts +0 -457
  83. package/src/steps/fluent-api.steps.ts +0 -255
  84. package/src/steps/http-methods.steps.ts +0 -342
  85. package/src/steps/lifecycle-events.steps.ts +0 -149
  86. package/src/steps/performance-reliability.steps.ts +0 -90
  87. package/src/steps/plugin-integration.steps.ts +0 -334
  88. package/src/steps/request-history.steps.ts +0 -458
  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 -227
  92. package/src/steps/state-concurrency.steps.ts +0 -167
  93. 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,15 +1,27 @@
1
- import type { ServerResponse } from "node:http";
2
1
  interface RequestWithHeaders {
3
2
  readonly headers: {
4
3
  readonly [header: string]: string | string[] | undefined;
5
4
  };
6
5
  }
7
6
  interface BodyReadable {
7
+ on(event: "aborted", listener: () => void): this;
8
+ on(event: "close", listener: () => void): this;
8
9
  on(event: "error", listener: (error: Error) => void): this;
9
- on(event: "data", listener: (chunk: Buffer) => void): this;
10
+ on(event: "data", listener: (chunk: Uint8Array) => void): this;
10
11
  on(event: "end", listener: () => void): this;
11
12
  destroy(error?: Error): this;
12
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
+ }
13
25
  /**
14
26
  * Convert Node.js IncomingMessage headers to a flat Record<string, string>.
15
27
  * Drops array-valued headers (keeps only string values).
@@ -21,17 +33,40 @@ export declare function parseNodeHeaders(req: RequestWithHeaders): Record<string
21
33
  export declare function parseNodeQuery(url: URL): Record<string, string>;
22
34
  /**
23
35
  * Collect and parse the request body from a Node.js IncomingMessage.
24
- * 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.
25
38
  * Returns undefined for empty bodies.
26
39
  * @param req - Node.js IncomingMessage
27
40
  * @param headers - Parsed request headers
28
41
  * @param maxBodySize - Maximum body size in bytes (default: 10 MB)
29
42
  */
30
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
+ }
31
55
  /**
32
56
  * Write a Schmock Response to a Node.js ServerResponse.
33
57
  * Serializes non-string bodies as JSON and sets content-type when missing.
34
58
  */
35
- 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;
36
71
  export {};
37
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,cAAc,EAAE,MAAM,WAAW,CAAC;AAGhD,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,OAAO,EAAE;QAChB,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;KAC1D,CAAC;CACH;AAED,UAAU,YAAY;IACpB,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IAC3D,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC3D,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC7C,OAAO,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,kBAAkB,GACtB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;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,YAAY,EACjB,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,CA2CN"}
1
+ {"version":3,"file":"http-helpers.d.ts","sourceRoot":"","sources":["../src/http-helpers.ts"],"names":[],"mappings":"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"}