@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,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
- }