@schmock/core 2.3.0 → 2.4.0

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 +18 -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,114 +0,0 @@
1
- import { isBinaryBody } from "./binary.js";
2
- import { isStatusTuple } from "./constants.js";
3
-
4
- const BINARY_CONTENT_TYPE = "application/octet-stream";
5
-
6
- function hasContentType(headers: Record<string, string>): boolean {
7
- return Object.keys(headers).some(
8
- (header) => header.toLowerCase() === "content-type",
9
- );
10
- }
11
-
12
- function isResponseObject(value: unknown): value is {
13
- status: number;
14
- body: unknown;
15
- headers?: Record<string, string>;
16
- } {
17
- return (
18
- typeof value === "object" &&
19
- value !== null &&
20
- "status" in value &&
21
- typeof value.status === "number" &&
22
- "body" in value &&
23
- (!("headers" in value) ||
24
- value.headers === undefined ||
25
- isStringRecord(value.headers))
26
- );
27
- }
28
-
29
- function isStringRecord(value: unknown): value is Record<string, string> {
30
- return (
31
- typeof value === "object" &&
32
- value !== null &&
33
- Object.values(value).every((entry) => typeof entry === "string")
34
- );
35
- }
36
-
37
- /**
38
- * Parse and normalize response result into Response object
39
- * Handles tuple format [status, body, headers], direct values, and response objects
40
- */
41
- export function parseResponse(
42
- result: unknown,
43
- routeConfig: Schmock.RouteConfig,
44
- ): Schmock.Response {
45
- let status = 200;
46
- let body: unknown = result;
47
- let headers: Record<string, string> = {};
48
-
49
- let tupleFormat = false;
50
-
51
- // Handle already-formed response objects (from plugin error recovery)
52
- if (isResponseObject(result)) {
53
- status = result.status;
54
- body = result.body;
55
- headers = result.headers ?? {};
56
- tupleFormat = true;
57
- } else if (isStatusTuple(result)) {
58
- // Handle tuple response format [status, body, headers?]
59
- [status, body, headers = {}] = result;
60
- tupleFormat = true;
61
- }
62
-
63
- // Handle null/undefined responses with 204 No Content
64
- // But don't auto-convert if tuple format was used (status was explicitly provided)
65
- if (body === null || body === undefined) {
66
- if (!tupleFormat) {
67
- status = status === 200 ? 204 : status; // Only change to 204 if status wasn't explicitly set via tuple
68
- }
69
- body = undefined; // Ensure body is undefined for null responses
70
- }
71
-
72
- const binaryBody = isBinaryBody(body);
73
-
74
- // Binary response values need a transport-safe MIME type. Tuple headers still
75
- // take precedence, while a non-JSON route override (for example image/png)
76
- // remains authoritative for non-tuple responses.
77
- if (!hasContentType(headers) && binaryBody) {
78
- headers["content-type"] =
79
- !tupleFormat &&
80
- routeConfig.contentType &&
81
- routeConfig.contentType !== "application/json"
82
- ? routeConfig.contentType
83
- : BINARY_CONTENT_TYPE;
84
- }
85
-
86
- // Add content-type header from route config if it exists and headers don't already have it
87
- // But only if this isn't a tuple response (where headers are explicitly controlled)
88
- let appliedRouteContentType = false;
89
- if (!hasContentType(headers) && routeConfig.contentType && !tupleFormat) {
90
- headers["content-type"] = routeConfig.contentType;
91
- appliedRouteContentType = true;
92
- }
93
-
94
- // Handle special conversion cases when contentType is explicitly set. A
95
- // binary body keeps its bytes even when a custom MIME type is configured.
96
- if (
97
- appliedRouteContentType &&
98
- routeConfig.contentType === "text/plain" &&
99
- body !== undefined &&
100
- !binaryBody
101
- ) {
102
- if (typeof body === "object") {
103
- body = JSON.stringify(body);
104
- } else if (typeof body !== "string") {
105
- body = String(body);
106
- }
107
- }
108
-
109
- return {
110
- status,
111
- body,
112
- headers,
113
- };
114
- }
@@ -1,333 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { schmock } from "./index";
3
-
4
- describe("response parsing", () => {
5
- describe("tuple response formats", () => {
6
- it("treats single-element array [status] as data, not tuple", async () => {
7
- const mock = schmock();
8
- mock("GET /status-only", () => [204] as [number]);
9
-
10
- const response = await mock.handle("GET", "/status-only");
11
-
12
- expect(response.status).toBe(200);
13
- expect(response.body).toEqual([204]);
14
- });
15
-
16
- it("handles [status, body] tuple", async () => {
17
- const mock = schmock();
18
- mock("POST /create", () => [201, { id: 123, created: true }]);
19
-
20
- const response = await mock.handle("POST", "/create");
21
-
22
- expect(response.status).toBe(201);
23
- expect(response.body).toEqual({ id: 123, created: true });
24
- expect(response.headers).toEqual({});
25
- });
26
-
27
- it("handles [status, body, headers] tuple", async () => {
28
- const mock = schmock();
29
- mock(
30
- "POST /upload",
31
- () =>
32
- [
33
- 201,
34
- { fileId: "abc123" },
35
- {
36
- Location: "/files/abc123",
37
- "Content-Type": "application/json",
38
- },
39
- ] as [number, any, Record<string, string>],
40
- );
41
-
42
- const response = await mock.handle("POST", "/upload");
43
-
44
- expect(response.status).toBe(201);
45
- expect(response.body).toEqual({ fileId: "abc123" });
46
- expect(response.headers).toEqual({
47
- Location: "/files/abc123",
48
- "Content-Type": "application/json",
49
- });
50
- });
51
-
52
- it("handles empty headers object in tuple", async () => {
53
- const mock = schmock();
54
- mock("GET /test", () => [200, "OK", {}]);
55
-
56
- const response = await mock.handle("GET", "/test");
57
-
58
- expect(response.status).toBe(200);
59
- expect(response.body).toBe("OK");
60
- expect(response.headers).toEqual({});
61
- });
62
-
63
- it("treats arrays with more than 3 elements as data, not tuple", async () => {
64
- const mock = schmock();
65
- mock(
66
- "GET /extra",
67
- () => [200, "data", {}, "ignored", "also-ignored"] as any,
68
- );
69
-
70
- const response = await mock.handle("GET", "/extra");
71
-
72
- expect(response.status).toBe(200);
73
- expect(response.body).toEqual([
74
- 200,
75
- "data",
76
- {},
77
- "ignored",
78
- "also-ignored",
79
- ]);
80
- });
81
-
82
- it("treats non-numeric first element as body, not status", async () => {
83
- const mock = schmock();
84
- mock("GET /array-body", () => ["item1", "item2", "item3"]);
85
-
86
- const response = await mock.handle("GET", "/array-body");
87
-
88
- expect(response.status).toBe(200);
89
- expect(response.body).toEqual(["item1", "item2", "item3"]);
90
- expect(response.headers).toEqual({ "content-type": "application/json" });
91
- });
92
- });
93
-
94
- describe("various response types", () => {
95
- it("handles string responses", async () => {
96
- const mock = schmock();
97
- mock("GET /text", () => "Simple text response");
98
-
99
- const response = await mock.handle("GET", "/text");
100
-
101
- expect(response.status).toBe(200);
102
- expect(response.body).toBe("Simple text response");
103
- expect(response.headers).toEqual({ "content-type": "application/json" });
104
- });
105
-
106
- it("handles number responses", async () => {
107
- const mock = schmock();
108
- mock("GET /number", () => 42);
109
-
110
- const response = await mock.handle("GET", "/number");
111
-
112
- expect(response.status).toBe(200);
113
- expect(response.body).toBe(42);
114
- expect(response.headers).toEqual({ "content-type": "application/json" });
115
- });
116
-
117
- it("handles boolean responses", async () => {
118
- const mock = schmock();
119
- mock("GET /bool", () => true);
120
-
121
- const response = await mock.handle("GET", "/bool");
122
-
123
- expect(response.status).toBe(200);
124
- expect(response.body).toBe(true);
125
- expect(response.headers).toEqual({ "content-type": "application/json" });
126
- });
127
-
128
- it("handles null responses", async () => {
129
- const mock = schmock();
130
- mock("GET /null", () => null);
131
-
132
- const response = await mock.handle("GET", "/null");
133
-
134
- expect(response.status).toBe(204);
135
- expect(response.body).toBeUndefined();
136
- expect(response.headers).toEqual({ "content-type": "application/json" });
137
- });
138
-
139
- it("handles undefined responses", async () => {
140
- const mock = schmock();
141
- mock("GET /undefined", () => undefined);
142
-
143
- const response = await mock.handle("GET", "/undefined");
144
-
145
- expect(response.status).toBe(204);
146
- expect(response.body).toBeUndefined();
147
- expect(response.headers).toEqual({ "content-type": "application/json" });
148
- });
149
-
150
- it("handles complex object responses", async () => {
151
- const complexObject = {
152
- data: {
153
- users: [
154
- { id: 1, name: "Alice", tags: ["admin", "active"] },
155
- { id: 2, name: "Bob", meta: { lastLogin: "2023-01-01" } },
156
- ],
157
- pagination: {
158
- page: 1,
159
- limit: 10,
160
- total: 2,
161
- },
162
- },
163
- timestamp: new Date("2023-01-01T00:00:00Z"),
164
- };
165
-
166
- const mock = schmock();
167
- mock("GET /complex", () => complexObject);
168
-
169
- const response = await mock.handle("GET", "/complex");
170
-
171
- expect(response.status).toBe(200);
172
- expect(response.body).toEqual(complexObject);
173
- expect(response.headers).toEqual({ "content-type": "application/json" });
174
- });
175
-
176
- it("handles empty array responses", async () => {
177
- const mock = schmock();
178
- mock("GET /empty-array", () => []);
179
-
180
- const response = await mock.handle("GET", "/empty-array");
181
-
182
- expect(response.status).toBe(200);
183
- expect(response.body).toEqual([]);
184
- expect(response.headers).toEqual({ "content-type": "application/json" });
185
- });
186
-
187
- it("handles empty object responses", async () => {
188
- const mock = schmock();
189
- mock("GET /empty-object", () => ({}));
190
-
191
- const response = await mock.handle("GET", "/empty-object");
192
-
193
- expect(response.status).toBe(200);
194
- expect(response.body).toEqual({});
195
- expect(response.headers).toEqual({ "content-type": "application/json" });
196
- });
197
- });
198
-
199
- describe("async response functions", () => {
200
- it("handles async response functions", async () => {
201
- const mock = schmock();
202
- mock("GET /async", async () => {
203
- await new Promise((resolve) => setTimeout(resolve, 1));
204
- return { async: true };
205
- });
206
-
207
- const response = await mock.handle("GET", "/async");
208
-
209
- expect(response.status).toBe(200);
210
- expect(response.body).toEqual({ async: true });
211
- });
212
-
213
- it("handles async tuple responses", async () => {
214
- const mock = schmock();
215
- mock("POST /async-create", async () => {
216
- await new Promise((resolve) => setTimeout(resolve, 1));
217
- return [201, { created: true }, { "X-Async": "true" }];
218
- });
219
-
220
- const response = await mock.handle("POST", "/async-create");
221
-
222
- expect(response.status).toBe(201);
223
- expect(response.body).toEqual({ created: true });
224
- expect(response.headers).toEqual({ "X-Async": "true" });
225
- });
226
- });
227
-
228
- describe("status code boundaries", () => {
229
- it("status code 100 (lower boundary) is accepted as tuple", async () => {
230
- const mock = schmock();
231
- mock("GET /continue", () => [100, "Continue"]);
232
-
233
- const response = await mock.handle("GET", "/continue");
234
- expect(response.status).toBe(100);
235
- expect(response.body).toBe("Continue");
236
- });
237
-
238
- it("status code 599 (upper boundary) is accepted as tuple", async () => {
239
- const mock = schmock();
240
- mock("GET /custom", () => [599, { message: "custom status" }]);
241
-
242
- const response = await mock.handle("GET", "/custom");
243
- expect(response.status).toBe(599);
244
- expect(response.body).toEqual({ message: "custom status" });
245
- });
246
-
247
- it("float status like 200.5 is accepted as tuple (number check)", async () => {
248
- const mock = schmock();
249
- mock("GET /float", () => [200.5, { ok: true }]);
250
-
251
- const response = await mock.handle("GET", "/float");
252
- // isStatusTuple checks typeof === 'number' and 100 <= n <= 599
253
- // 200.5 passes both checks, so it's treated as a tuple
254
- expect(response.status).toBe(200.5);
255
- expect(response.body).toEqual({ ok: true });
256
- });
257
-
258
- it("array [201, null, undefined] — tuple with undefined headers defaults to empty", async () => {
259
- const mock = schmock();
260
- mock("GET /null-headers", () => [201, null, undefined] as any);
261
-
262
- const response = await mock.handle("GET", "/null-headers");
263
- // isStatusTuple: length 3, first element 201 (number, 100-599) -> true
264
- // destructure: [status=201, body=null, headers=undefined]
265
- // headers defaults to {} via `headers = {}`
266
- expect(response.status).toBe(201);
267
- expect(response.body).toBeUndefined(); // null body becomes undefined
268
- expect(response.headers).toEqual({});
269
- });
270
-
271
- it("non-tuple array [1, 2, 3, 4] with length > 3 treated as body", async () => {
272
- const mock = schmock();
273
- mock("GET /long-array", () => [1, 2, 3, 4] as any);
274
-
275
- const response = await mock.handle("GET", "/long-array");
276
- // isStatusTuple requires length 2 or 3, so length 4 => not a tuple => body
277
- expect(response.status).toBe(200);
278
- expect(response.body).toEqual([1, 2, 3, 4]);
279
- });
280
-
281
- it("status 99 (below 100) is NOT treated as a tuple", async () => {
282
- const mock = schmock();
283
- mock("GET /below-range", () => [99, { data: true }]);
284
-
285
- const response = await mock.handle("GET", "/below-range");
286
- // isStatusTuple requires value[0] >= 100, so 99 fails => treated as body
287
- expect(response.status).toBe(200);
288
- expect(response.body).toEqual([99, { data: true }]);
289
- });
290
-
291
- it("status 600 (above 599) is NOT treated as a tuple", async () => {
292
- const mock = schmock();
293
- mock("GET /above-range", () => [600, { data: true }]);
294
-
295
- const response = await mock.handle("GET", "/above-range");
296
- // isStatusTuple requires value[0] <= 599, so 600 fails => treated as body
297
- expect(response.status).toBe(200);
298
- expect(response.body).toEqual([600, { data: true }]);
299
- });
300
- });
301
-
302
- describe("edge cases", () => {
303
- it("handles response with circular references gracefully", async () => {
304
- const mock = schmock();
305
- mock("GET /circular", () => {
306
- const obj: any = { name: "test" };
307
- obj.self = obj; // Create circular reference
308
- return obj;
309
- });
310
-
311
- const response = await mock.handle("GET", "/circular");
312
-
313
- expect(response.status).toBe(200);
314
- expect(response.body).toHaveProperty("name", "test");
315
- expect(response.body).toHaveProperty("self");
316
- });
317
-
318
- it("preserves functions in response objects", async () => {
319
- const mock = schmock();
320
- mock("GET /with-functions", () => ({
321
- data: "test",
322
- fn: () => "function result",
323
- }));
324
-
325
- const response = await mock.handle("GET", "/with-functions");
326
-
327
- expect(response.status).toBe(200);
328
- expect(response.body.data).toBe("test");
329
- expect(typeof response.body.fn).toBe("function");
330
- expect(response.body.fn()).toBe("function result");
331
- });
332
- });
333
- });
@@ -1,69 +0,0 @@
1
- import { normalizePath } from "./constants.js";
2
-
3
- /**
4
- * Compiled callable route with pattern matching
5
- */
6
- export interface CompiledCallableRoute {
7
- pattern: RegExp;
8
- params: string[];
9
- method: Schmock.HttpMethod;
10
- path: string;
11
- generator: Schmock.Generator;
12
- config: Schmock.RouteConfig;
13
- }
14
-
15
- export function isGeneratorFunction(
16
- gen: Schmock.Generator,
17
- ): gen is Schmock.GeneratorFunction {
18
- return typeof gen === "function";
19
- }
20
-
21
- /**
22
- * Find a route that matches the given method and path
23
- * Uses two-pass matching: static routes first, then parameterized routes
24
- * Matches routes in registration order (first registered wins)
25
- */
26
- export function findRoute(
27
- method: Schmock.HttpMethod,
28
- path: string,
29
- staticRoutes: Map<string, CompiledCallableRoute>,
30
- routes: CompiledCallableRoute[],
31
- ): CompiledCallableRoute | undefined {
32
- // O(1) lookup for static routes
33
- const staticMatch = staticRoutes.get(`${method} ${normalizePath(path)}`);
34
- if (staticMatch) {
35
- return staticMatch;
36
- }
37
-
38
- // Fall through to parameterized route scan
39
- for (const route of routes) {
40
- if (
41
- route.method === method &&
42
- route.params.length > 0 &&
43
- route.pattern.test(path)
44
- ) {
45
- return route;
46
- }
47
- }
48
-
49
- return undefined;
50
- }
51
-
52
- /**
53
- * Extract parameter values from path based on route pattern
54
- * Maps capture groups from regex match to parameter names
55
- */
56
- export function extractParams(
57
- route: CompiledCallableRoute,
58
- path: string,
59
- ): Record<string, string> {
60
- const match = path.match(route.pattern);
61
- if (!match) return {};
62
-
63
- const params: Record<string, string> = {};
64
- route.params.forEach((param, index) => {
65
- params[param] = match[index + 1];
66
- });
67
-
68
- return params;
69
- }