@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,555 +0,0 @@
1
- import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
2
- import { expect } from "vitest";
3
- import { schmock, toHttpMethod, toRouteKey } from "../index";
4
- import type { CallableMockInstance, Plugin } from "../types";
5
-
6
- const feature = await loadFeature("../../features/error-handling.feature");
7
-
8
- function parseRequest(request: string): {
9
- method: Schmock.HttpMethod;
10
- path: string;
11
- } {
12
- const separator = request.indexOf(" ");
13
- const methodText = request.slice(0, separator);
14
- const path = request.slice(separator + 1);
15
- if (separator <= 0 || !path.startsWith("/")) {
16
- throw new Error(`Expected request in METHOD /path format, got: ${request}`);
17
- }
18
- return { method: toHttpMethod(methodText), path };
19
- }
20
-
21
- function requireRecord(
22
- value: unknown,
23
- description: string,
24
- ): Record<string, unknown> {
25
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
26
- throw new Error(`Expected ${description} to be an object`);
27
- }
28
- return Object.fromEntries(Object.entries(value));
29
- }
30
-
31
- function responseBodyString(response: Schmock.Response, field: string): string {
32
- const value = requireRecord(response.body, "response body")[field];
33
- if (typeof value !== "string") {
34
- throw new Error(`Expected response body field ${field} to be a string`);
35
- }
36
- return value;
37
- }
38
-
39
- describeFeature(feature, ({ Scenario }) => {
40
- let mock: CallableMockInstance;
41
- let response: Schmock.Response;
42
-
43
- Scenario("Route not found returns 404", ({ Given, When, Then, And }) => {
44
- Given(
45
- "I create a mock with a GET /users route returning a user list",
46
- () => {
47
- mock = schmock();
48
- mock("GET /users", [{ id: 1, name: "John" }]);
49
- },
50
- );
51
-
52
- When("I request {string}", async (_, request: string) => {
53
- const { method, path } = parseRequest(request);
54
- response = await mock.handle(method, path);
55
- });
56
-
57
- Then("I should receive status {int}", (_, status: number) => {
58
- expect(response.status).toBe(status);
59
- });
60
-
61
- And(
62
- "the response should contain error {string}",
63
- (_, errorMessage: string) => {
64
- expect(responseBodyString(response, "error")).toContain(errorMessage);
65
- },
66
- );
67
-
68
- And(
69
- "the response should have error code {string}",
70
- (_, errorCode: string) => {
71
- expect(responseBodyString(response, "code")).toBe(errorCode);
72
- },
73
- );
74
- });
75
-
76
- Scenario("Wrong HTTP method returns 404", ({ Given, When, Then, And }) => {
77
- Given(
78
- "I create a mock with a GET /api/data route returning success",
79
- () => {
80
- mock = schmock();
81
- mock("GET /api/data", { success: true });
82
- },
83
- );
84
-
85
- When("I request {string}", async (_, request: string) => {
86
- const { method, path } = parseRequest(request);
87
- response = await mock.handle(method, path);
88
- });
89
-
90
- Then("I should receive status {int}", (_, status: number) => {
91
- expect(response.status).toBe(status);
92
- });
93
-
94
- And(
95
- "the response should contain error {string}",
96
- (_, errorMessage: string) => {
97
- expect(responseBodyString(response, "error")).toContain(errorMessage);
98
- },
99
- );
100
- });
101
-
102
- Scenario(
103
- "Plugin throws error returns 500 with PluginError",
104
- ({ Given, When, Then, And }) => {
105
- Given(
106
- "I create a mock with a plugin that throws {string}",
107
- (_, errorMsg: string) => {
108
- mock = schmock();
109
- const failingPlugin: Plugin = {
110
- name: "failing-plugin",
111
- process: () => {
112
- throw new Error(errorMsg);
113
- },
114
- };
115
- mock("GET /test", "original").pipe(failingPlugin);
116
- },
117
- );
118
-
119
- When("I request {string}", async (_, request: string) => {
120
- const { method, path } = parseRequest(request);
121
- response = await mock.handle(method, path);
122
- });
123
-
124
- Then("I should receive status {int}", (_, status: number) => {
125
- expect(response.status).toBe(status);
126
- });
127
-
128
- And(
129
- "the response should contain error {string}",
130
- (_, errorMessage: string) => {
131
- expect(responseBodyString(response, "error")).toContain(errorMessage);
132
- },
133
- );
134
-
135
- And(
136
- "the response should have error code {string}",
137
- (_, errorCode: string) => {
138
- expect(responseBodyString(response, "code")).toBe(errorCode);
139
- },
140
- );
141
- },
142
- );
143
-
144
- Scenario(
145
- "Plugin onError hook recovers from failure",
146
- ({ Given, When, Then, And }) => {
147
- Given(
148
- "I create a mock with a recoverable plugin that returns {string}",
149
- (_, recoveredBody: string) => {
150
- mock = schmock();
151
- const recoverablePlugin: Plugin = {
152
- name: "recoverable",
153
- process: () => {
154
- throw new Error("Initial failure");
155
- },
156
- onError: () => ({ status: 200, body: recoveredBody, headers: {} }),
157
- };
158
- mock("GET /test", "original").pipe(recoverablePlugin);
159
- },
160
- );
161
-
162
- When("I request {string}", async (_, request: string) => {
163
- const { method, path } = parseRequest(request);
164
- response = await mock.handle(method, path);
165
- });
166
-
167
- Then("I should receive status {int}", (_, status: number) => {
168
- expect(response.status).toBe(status);
169
- });
170
-
171
- And("I should receive text {string}", (_, expectedText: string) => {
172
- expect(response.body).toBe(expectedText);
173
- });
174
- },
175
- );
176
-
177
- Scenario(
178
- "Plugin returns invalid result structure",
179
- ({ Given, When, Then, And }) => {
180
- Given("I create a mock with a plugin returning an invalid result", () => {
181
- mock = schmock();
182
- const invalidPlugin: Plugin = {
183
- name: "invalid",
184
- process: (context) => ({ context }),
185
- };
186
- Object.defineProperty(invalidPlugin, "process", {
187
- value: () => ({ wrongStructure: true }),
188
- });
189
- mock("GET /test", "original").pipe(invalidPlugin);
190
- });
191
-
192
- When("I request {string}", async (_, request: string) => {
193
- const { method, path } = parseRequest(request);
194
- response = await mock.handle(method, path);
195
- });
196
-
197
- Then("I should receive status {int}", (_, status: number) => {
198
- expect(response.status).toBe(status);
199
- });
200
-
201
- And(
202
- "the response should contain error {string}",
203
- (_, errorMessage: string) => {
204
- expect(responseBodyString(response, "error")).toContain(errorMessage);
205
- },
206
- );
207
- },
208
- );
209
-
210
- Scenario(
211
- "Function generator throws error returns 500",
212
- ({ Given, When, Then, And }) => {
213
- Given(
214
- "I create a mock with a generator that throws {string}",
215
- (_, errorMsg: string) => {
216
- mock = schmock();
217
- mock("GET /fail", () => {
218
- throw new Error(errorMsg);
219
- });
220
- },
221
- );
222
-
223
- When("I request {string}", async (_, request: string) => {
224
- const { method, path } = parseRequest(request);
225
- response = await mock.handle(method, path);
226
- });
227
-
228
- Then("I should receive status {int}", (_, status: number) => {
229
- expect(response.status).toBe(status);
230
- });
231
-
232
- And(
233
- "the response should contain error {string}",
234
- (_, errorMessage: string) => {
235
- expect(responseBodyString(response, "error")).toContain(errorMessage);
236
- },
237
- );
238
-
239
- And(
240
- "the response should have error code {string}",
241
- (_, errorCode: string) => {
242
- expect(responseBodyString(response, "code")).toBe(errorCode);
243
- },
244
- );
245
- },
246
- );
247
-
248
- Scenario("Namespace mismatch returns 404", ({ Given, When, Then, And }) => {
249
- Given(
250
- "I create a mock with namespace {string} and a GET /users route",
251
- (_, namespace: string) => {
252
- mock = schmock({ namespace });
253
- mock("GET /users", [{ id: 1 }]);
254
- },
255
- );
256
-
257
- When("I request {string}", async (_, request: string) => {
258
- const { method, path } = parseRequest(request);
259
- response = await mock.handle(method, path);
260
- });
261
-
262
- Then("I should receive status {int}", (_, status: number) => {
263
- expect(response.status).toBe(status);
264
- });
265
-
266
- And(
267
- "the response should contain error {string}",
268
- (_, errorMessage: string) => {
269
- expect(responseBodyString(response, "error")).toContain(errorMessage);
270
- },
271
- );
272
- });
273
-
274
- Scenario(
275
- "Multiple plugin failures cascade properly",
276
- ({ Given, When, Then, And }) => {
277
- Given(
278
- "I create a mock with two failing plugins piped in sequence",
279
- () => {
280
- mock = schmock();
281
- const plugin1: Plugin = {
282
- name: "first-fail",
283
- process: () => {
284
- throw new Error("First error");
285
- },
286
- };
287
- const plugin2: Plugin = {
288
- name: "second-fail",
289
- process: () => {
290
- throw new Error("Second error");
291
- },
292
- };
293
- mock("GET /cascade", "original").pipe(plugin1).pipe(plugin2);
294
- },
295
- );
296
-
297
- When("I request {string}", async (_, request: string) => {
298
- const { method, path } = parseRequest(request);
299
- response = await mock.handle(method, path);
300
- });
301
-
302
- Then("I should receive status {int}", (_, status: number) => {
303
- expect(response.status).toBe(status);
304
- });
305
-
306
- And(
307
- "the response should contain error {string}",
308
- (_, errorMessage: string) => {
309
- expect(responseBodyString(response, "error")).toContain(errorMessage);
310
- },
311
- );
312
- },
313
- );
314
-
315
- Scenario("Plugin onError hook also fails", ({ Given, When, Then, And }) => {
316
- Given(
317
- "I create a mock with a plugin whose error handler also throws",
318
- () => {
319
- mock = schmock({ debug: true });
320
- const brokenPlugin: Plugin = {
321
- name: "broken-handler",
322
- process: () => {
323
- throw new Error("Process failed");
324
- },
325
- onError: () => {
326
- throw new Error("Handler failed");
327
- },
328
- };
329
- mock("GET /broken", "original").pipe(brokenPlugin);
330
- },
331
- );
332
-
333
- When("I request {string}", async (_, request: string) => {
334
- const { method, path } = parseRequest(request);
335
- response = await mock.handle(method, path);
336
- });
337
-
338
- Then("I should receive status {int}", (_, status: number) => {
339
- expect(response.status).toBe(status);
340
- });
341
-
342
- And(
343
- "the response should contain error {string}",
344
- (_, errorMessage: string) => {
345
- expect(responseBodyString(response, "error")).toContain(errorMessage);
346
- },
347
- );
348
- });
349
-
350
- Scenario(
351
- "Downstream error handler recovers an earlier plugin failure",
352
- ({ Given, When, Then, And }) => {
353
- Given("I create a failing plugin followed by a recovery plugin", () => {
354
- mock = schmock();
355
- mock("GET /downstream-recovery", "original")
356
- .pipe({
357
- name: "upstream-failure",
358
- process() {
359
- throw new Error("upstream failed");
360
- },
361
- })
362
- .pipe({
363
- name: "downstream-recovery",
364
- process(context, currentResponse) {
365
- return { context, response: currentResponse };
366
- },
367
- onError() {
368
- return [
369
- 503,
370
- {
371
- error: "Recovered by downstream plugin",
372
- code: "RECOVERED_DOWNSTREAM",
373
- },
374
- ];
375
- },
376
- });
377
- });
378
-
379
- When("I request {string}", async (_, request: string) => {
380
- const { method, path } = parseRequest(request);
381
- response = await mock.handle(method, path);
382
- });
383
-
384
- Then("I should receive status {int}", (_, status: number) => {
385
- expect(response.status).toBe(status);
386
- });
387
-
388
- And("the response should have error code {string}", (_, code: string) => {
389
- expect(responseBodyString(response, "code")).toBe(code);
390
- });
391
- },
392
- );
393
-
394
- Scenario(
395
- "Empty parameter in route returns 404",
396
- ({ Given, When, Then, And }) => {
397
- Given(
398
- "I create a mock with a parameterized route {string}",
399
- (_, route: string) => {
400
- mock = schmock();
401
- const { method, path } = parseRequest(route);
402
- mock(toRouteKey(method, path), ({ params }) => ({
403
- userId: params.id,
404
- }));
405
- },
406
- );
407
-
408
- When("I request {string}", async (_, request: string) => {
409
- const { method, path } = parseRequest(request);
410
- response = await mock.handle(method, path);
411
- });
412
-
413
- Then("I should receive status {int}", (_, status: number) => {
414
- expect(response.status).toBe(status);
415
- });
416
-
417
- And(
418
- "the response should contain error {string}",
419
- (_, errorMessage: string) => {
420
- expect(responseBodyString(response, "error")).toContain(errorMessage);
421
- },
422
- );
423
- },
424
- );
425
-
426
- Scenario("Async generator error handling", ({ Given, When, Then, And }) => {
427
- Given(
428
- "I create a mock with an async generator that throws {string}",
429
- (_, errorMsg: string) => {
430
- mock = schmock();
431
- mock("GET /async-fail", async () => {
432
- await new Promise((resolve) => setTimeout(resolve, 10));
433
- throw new Error(errorMsg);
434
- });
435
- },
436
- );
437
-
438
- When("I request {string}", async (_, request: string) => {
439
- const { method, path } = parseRequest(request);
440
- response = await mock.handle(method, path);
441
- });
442
-
443
- Then("I should receive status {int}", (_, status: number) => {
444
- expect(response.status).toBe(status);
445
- });
446
-
447
- And(
448
- "the response should contain error {string}",
449
- (_, errorMessage: string) => {
450
- expect(responseBodyString(response, "error")).toContain(errorMessage);
451
- },
452
- );
453
- });
454
-
455
- Scenario(
456
- "Error responses include proper headers",
457
- ({ Given, When, Then, And }) => {
458
- Given(
459
- "I create a mock with a generator that throws {string}",
460
- (_, errorMsg: string) => {
461
- mock = schmock();
462
- mock("GET /error", () => {
463
- throw new Error(errorMsg);
464
- });
465
- },
466
- );
467
-
468
- When("I request {string}", async (_, request: string) => {
469
- const { method, path } = parseRequest(request);
470
- response = await mock.handle(method, path);
471
- });
472
-
473
- Then("I should receive status {int}", (_, status: number) => {
474
- expect(response.status).toBe(status);
475
- });
476
-
477
- And("the content-type should be {string}", (_, contentType: string) => {
478
- expect(response.headers["content-type"]).toBe(contentType);
479
- });
480
-
481
- And(
482
- "the response body should be a structured {string} error for {string}",
483
- (_, code: string, message: string) => {
484
- expect(response.body).toEqual({ error: message, code });
485
- },
486
- );
487
- },
488
- );
489
-
490
- Scenario(
491
- "Plugin onError returns response with status 0",
492
- ({ Given, When, Then, And }) => {
493
- Given(
494
- "I create a mock with a plugin whose error handler returns status 0",
495
- () => {
496
- mock = schmock();
497
- const plugin: Plugin = {
498
- name: "zero-status",
499
- process: () => {
500
- throw new Error("fail");
501
- },
502
- onError: () => ({ status: 0, body: "zero status", headers: {} }),
503
- };
504
- mock("GET /zero", "original").pipe(plugin);
505
- },
506
- );
507
-
508
- When("I request {string}", async (_, request: string) => {
509
- const { method, path } = parseRequest(request);
510
- response = await mock.handle(method, path);
511
- });
512
-
513
- Then("I should receive status {int}", (_, status: number) => {
514
- expect(response.status).toBe(status);
515
- });
516
-
517
- And("I should receive text {string}", (_, expectedText: string) => {
518
- expect(response.body).toBe(expectedText);
519
- });
520
- },
521
- );
522
-
523
- Scenario(
524
- "Plugin null/undefined return handling",
525
- ({ Given, When, Then, And }) => {
526
- Given("I create a mock with a plugin that returns null", () => {
527
- mock = schmock();
528
- const nullPlugin: Plugin = {
529
- name: "null-plugin",
530
- process: (context) => ({ context }),
531
- };
532
- Object.defineProperty(nullPlugin, "process", {
533
- value: () => null,
534
- });
535
- mock("GET /null", "original").pipe(nullPlugin);
536
- });
537
-
538
- When("I request {string}", async (_, request: string) => {
539
- const { method, path } = parseRequest(request);
540
- response = await mock.handle(method, path);
541
- });
542
-
543
- Then("I should receive status {int}", (_, status: number) => {
544
- expect(response.status).toBe(status);
545
- });
546
-
547
- And(
548
- "the response should contain error {string}",
549
- (_, errorMessage: string) => {
550
- expect(responseBodyString(response, "error")).toContain(errorMessage);
551
- },
552
- );
553
- },
554
- );
555
- });