@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,421 +0,0 @@
1
- import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
2
- import { expect } from "vitest";
3
- import { schmock } from "../index";
4
- import type { CallableMockInstance, Plugin } from "../types";
5
-
6
- const feature = await loadFeature("../../features/async-support.feature");
7
-
8
- function isRecord(value: unknown): value is Record<string, unknown> {
9
- return typeof value === "object" && value !== null && !Array.isArray(value);
10
- }
11
-
12
- describeFeature(feature, ({ Scenario }) => {
13
- let mock: CallableMockInstance;
14
- let response: any;
15
- let responses: any[] = [];
16
- let startTime: number;
17
- let endTime: number;
18
-
19
- Scenario("Async generator function returns Promise", ({ Given, When, Then, And }) => {
20
- Given("I create a mock with an async generator at {string}", (_, route: string) => {
21
- const [method, path] = route.split(" ");
22
- mock = schmock();
23
- mock(`${method} ${path}` as any, async () => {
24
- await new Promise(resolve => setTimeout(resolve, 10));
25
- return { message: "async response" };
26
- });
27
- });
28
-
29
- When("I request {string}", async (_, request: string) => {
30
- const [method, path] = request.split(" ");
31
- response = await mock.handle(method as any, path);
32
- });
33
-
34
- Then("I should receive:", (_, docString: string) => {
35
- const expected = JSON.parse(docString);
36
- expect(response.body).toEqual(expected);
37
- });
38
-
39
- And("the content-type should be {string}", (_, contentType: string) => {
40
- expect(response.headers?.["content-type"]).toBe(contentType);
41
- });
42
- });
43
-
44
- Scenario("Async generator with context access", ({ Given, When, Then, And }) => {
45
- Given("I create a mock with an async param-based generator at {string}", (_, route: string) => {
46
- const [method, path] = route.split(" ");
47
- mock = schmock();
48
- mock(`${method} ${path}` as any, async ({ params }: any) => {
49
- await new Promise(resolve => setTimeout(resolve, 5));
50
- return {
51
- userId: params.id,
52
- fetchedAt: new Date().toISOString(),
53
- };
54
- });
55
- });
56
-
57
- When("I request {string}", async (_, request: string) => {
58
- const [method, path] = request.split(" ");
59
- response = await mock.handle(method as any, path);
60
- });
61
-
62
- Then("the response should have property {string} with value {string}", (_, property: string, value: string) => {
63
- expect(response.body).toHaveProperty(property, value);
64
- });
65
-
66
- And("the response should have property {string}", (_, property: string) => {
67
- expect(response.body).toHaveProperty(property);
68
- });
69
- });
70
-
71
- Scenario("Async plugin processing", ({ Given, When, Then, And }) => {
72
- Given("I create a mock with an async processing plugin at {string}", (_, route: string) => {
73
- const [method, path] = route.split(" ");
74
- mock = schmock();
75
- const asyncPlugin: Plugin = {
76
- name: "async-processor",
77
- process: async (ctx, response) => {
78
- await new Promise(resolve => setTimeout(resolve, 5));
79
- return {
80
- context: ctx,
81
- response: {
82
- data: response,
83
- processedAsync: true,
84
- timestamp: new Date().toISOString(),
85
- },
86
- };
87
- },
88
- };
89
- mock(`${method} ${path}` as any, { original: "data" }).pipe(asyncPlugin);
90
- });
91
-
92
- When("I request {string}", async (_, request: string) => {
93
- const [method, path] = request.split(" ");
94
- response = await mock.handle(method as any, path);
95
- });
96
-
97
- Then("the async response should have property {string}", (_, property: string) => {
98
- expect(response.body).toHaveProperty(property);
99
- });
100
-
101
- And("the async response should have property {string} with value {word}", (_, property: string, value: string) => {
102
- const expectedValue = value === "true" ? true : value === "false" ? false : value;
103
- expect(response.body).toHaveProperty(property, expectedValue);
104
- });
105
-
106
- And("the async response should have property {string}", (_, property: string) => {
107
- expect(response.body).toHaveProperty(property);
108
- });
109
- });
110
-
111
- Scenario("Mixed sync and async plugin pipeline", ({ Given, When, Then, And }) => {
112
- Given("I create a mock with sync and async plugins at {string}", (_, route: string) => {
113
- const [method, path] = route.split(" ");
114
- mock = schmock();
115
- const syncPlugin: Plugin = {
116
- name: "sync-step",
117
- process: (ctx, pluginResponse) => {
118
- if (!isRecord(pluginResponse)) {
119
- throw new Error(
120
- "Expected the sync plugin response to be an object",
121
- );
122
- }
123
- return {
124
- context: ctx,
125
- response: { ...pluginResponse, syncStep: true },
126
- };
127
- },
128
- };
129
- const asyncPlugin: Plugin = {
130
- name: "async-step",
131
- process: async (ctx, pluginResponse) => {
132
- await new Promise(resolve => setTimeout(resolve, 5));
133
- if (!isRecord(pluginResponse)) {
134
- throw new Error(
135
- "Expected the async plugin response to be an object",
136
- );
137
- }
138
- return {
139
- context: ctx,
140
- response: { ...pluginResponse, asyncStep: true },
141
- };
142
- },
143
- };
144
- mock(`${method} ${path}` as any, { base: "data" })
145
- .pipe(syncPlugin)
146
- .pipe(asyncPlugin);
147
- });
148
-
149
- When("I request {string}", async (_, request: string) => {
150
- const [method, path] = request.split(" ");
151
- response = await mock.handle(method as any, path);
152
- });
153
-
154
- Then("the response should have property {string} with value {string}", (_, property: string, value: string) => {
155
- expect(response.body).toHaveProperty(property, value);
156
- });
157
-
158
- And("the response should have property {string} with value {word}", (_, property: string, value: string) => {
159
- const expectedValue = value === "true" ? true : value === "false" ? false : value;
160
- expect(response.body).toHaveProperty(property, expectedValue);
161
- });
162
-
163
- And("the response should have property {string} with boolean value {word}", (_, property: string, value: string) => {
164
- const expectedValue = value === "true" ? true : value === "false" ? false : value;
165
- expect(response.body).toHaveProperty(property, expectedValue);
166
- });
167
- });
168
-
169
- Scenario("Async generator with Promise rejection", ({ Given, When, Then, And }) => {
170
- Given("I create a mock with an async generator that throws at {string}", (_, route: string) => {
171
- const [method, path] = route.split(" ");
172
- mock = schmock();
173
- mock(`${method} ${path}` as any, async () => {
174
- await new Promise(resolve => setTimeout(resolve, 5));
175
- throw new Error("Async operation failed");
176
- });
177
- });
178
-
179
- When("I request {string}", async (_, request: string) => {
180
- const [method, path] = request.split(" ");
181
- response = await mock.handle(method as any, path);
182
- });
183
-
184
- Then("I should receive status {int}", (_, status: number) => {
185
- expect(response.status).toBe(status);
186
- });
187
-
188
- And("the response should contain error {string}", (_, errorMessage: string) => {
189
- expect(response.body.error).toContain(errorMessage);
190
- });
191
- });
192
-
193
- Scenario("Async plugin error recovery", ({ Given, When, Then, And }) => {
194
- Given("I create a mock with an async error-recovery plugin at {string}", (_, route: string) => {
195
- const [method, path] = route.split(" ");
196
- mock = schmock();
197
- const asyncErrorPlugin: Plugin = {
198
- name: "async-error-handler",
199
- process: async () => {
200
- await new Promise(resolve => setTimeout(resolve, 5));
201
- throw new Error("Async plugin failed");
202
- },
203
- onError: async (error) => {
204
- await new Promise(resolve => setTimeout(resolve, 3));
205
- return {
206
- status: 200,
207
- body: { recovered: true, originalError: error.message },
208
- headers: {},
209
- };
210
- },
211
- };
212
- mock(`${method} ${path}` as any, "original").pipe(asyncErrorPlugin);
213
- });
214
-
215
- When("I request {string}", async (_, request: string) => {
216
- const [method, path] = request.split(" ");
217
- response = await mock.handle(method as any, path);
218
- });
219
-
220
- Then("I should receive status {int}", (_, status: number) => {
221
- expect(response.status).toBe(status);
222
- });
223
-
224
- And("the response should have property {string} with value {word}", (_, property: string, value: string) => {
225
- const expectedValue = value === "true" ? true : value === "false" ? false : value;
226
- expect(response.body).toHaveProperty(property, expectedValue);
227
- });
228
-
229
- And("the response should have property {string}", (_, property: string) => {
230
- expect(response.body).toHaveProperty(property);
231
- });
232
- });
233
-
234
- Scenario("Async generator with delay configuration", ({ Given, When, Then, And }) => {
235
- Given("I create a mock with delay 20ms and async generator at {string}", (_, route: string) => {
236
- const [method, path] = route.split(" ");
237
- mock = schmock({ delay: 20 });
238
- mock(`${method} ${path}` as any, async () => {
239
- await new Promise(resolve => setTimeout(resolve, 10));
240
- return { delayed: true, async: true };
241
- });
242
- });
243
-
244
- When("I request {string} and measure time", async (_, request: string) => {
245
- const [method, path] = request.split(" ");
246
- startTime = Date.now();
247
- response = await mock.handle(method as any, path);
248
- endTime = Date.now();
249
- });
250
-
251
- Then("the response time should be at least {int}ms", (_, minTime: number) => {
252
- const actualTime = endTime - startTime;
253
- expect(actualTime).toBeGreaterThanOrEqual(minTime);
254
- });
255
-
256
- And("I should receive:", (_, docString: string) => {
257
- const expected = JSON.parse(docString);
258
- expect(response.body).toEqual(expected);
259
- });
260
- });
261
-
262
- Scenario("Async generator with state management", ({ Given, When, Then, And }) => {
263
- Given("I create a mock with async stateful counter at {string}", (_, route: string) => {
264
- const [method, path] = route.split(" ");
265
- mock = schmock({ state: { asyncCounter: 0 } });
266
- mock(`${method} ${path}` as any, async ({ state }: any) => {
267
- await new Promise(resolve => setTimeout(resolve, 5));
268
- state.asyncCounter = (state.asyncCounter || 0) + 1;
269
- return {
270
- count: state.asyncCounter,
271
- processedAsync: true,
272
- };
273
- });
274
- });
275
-
276
- When("I request {string} twice", async (_, request: string) => {
277
- const [method, path] = request.split(" ");
278
- const firstResponse = await mock.handle(method as any, path);
279
- const secondResponse = await mock.handle(method as any, path);
280
- responses = [firstResponse, secondResponse];
281
- });
282
-
283
- Then("the first response should have count {int}", (_, count: number) => {
284
- expect(responses[0].body.count).toBe(count);
285
- });
286
-
287
- And("the second response should have count {int}", (_, count: number) => {
288
- expect(responses[1].body.count).toBe(count);
289
- });
290
-
291
- And("both responses should have processedAsync {word}", (_, value: string) => {
292
- const expectedValue = value === "true" ? true : value === "false" ? false : value;
293
- expect(responses[0].body.processedAsync).toBe(expectedValue);
294
- expect(responses[1].body.processedAsync).toBe(expectedValue);
295
- });
296
- });
297
-
298
- Scenario("Promise-based plugin response generation", ({ Given, When, Then }) => {
299
- Given("I create a mock with a Promise-generating plugin at {string}", (_, route: string) => {
300
- const [method, path] = route.split(" ");
301
- mock = schmock();
302
- const promisePlugin: Plugin = {
303
- name: "promise-generator",
304
- process: async (ctx, response) => {
305
- if (!response) {
306
- const data = await Promise.resolve({ generated: "by promise" });
307
- return { context: ctx, response: data };
308
- }
309
- return { context: ctx, response };
310
- },
311
- };
312
- mock(`${method} ${path}` as any, null).pipe(promisePlugin);
313
- });
314
-
315
- When("I request {string}", async (_, request: string) => {
316
- const [method, path] = request.split(" ");
317
- response = await mock.handle(method as any, path);
318
- });
319
-
320
- Then("I should receive:", (_, docString: string) => {
321
- const expected = JSON.parse(docString);
322
- expect(response.body).toEqual(expected);
323
- });
324
- });
325
-
326
- Scenario("Concurrent async requests isolation", ({ Given, When, Then, And }) => {
327
- Given("I create a mock with async delay per id at {string}", (_, route: string) => {
328
- const [method, path] = route.split(" ");
329
- mock = schmock();
330
- mock(`${method} ${path}` as any, async ({ params }: any) => {
331
- const delay = parseInt(params.id) * 5;
332
- await new Promise(resolve => setTimeout(resolve, delay));
333
- return {
334
- id: params.id,
335
- processedAt: Date.now(),
336
- };
337
- });
338
- });
339
-
340
- When("I make concurrent requests to {string}, {string}, and {string}", async (_, path1: string, path2: string, path3: string) => {
341
- const promises = [
342
- mock.handle("GET", path1),
343
- mock.handle("GET", path2),
344
- mock.handle("GET", path3),
345
- ];
346
- responses = await Promise.all(promises);
347
- });
348
-
349
- Then("all responses should have valid processedAt timestamps", () => {
350
- const timestamps = responses.map(r => r.body.processedAt);
351
- expect(timestamps).toHaveLength(3);
352
- timestamps.forEach((timestamp: number) => expect(timestamp).toBeGreaterThan(0));
353
- });
354
-
355
- And("each response should have the correct id value", () => {
356
- expect(responses[0].body.id).toBe("1");
357
- expect(responses[1].body.id).toBe("2");
358
- expect(responses[2].body.id).toBe("3");
359
- });
360
-
361
- And("all responses should complete with status 200", () => {
362
- for (const response of responses) {
363
- expect(response.status).toBe(200);
364
- expect(response.body).toHaveProperty("id");
365
- expect(response.body).toHaveProperty("processedAt");
366
- }
367
- });
368
- });
369
-
370
- Scenario("Async plugin pipeline with context state", ({ Given, When, Then, And }) => {
371
- Given("I create a mock with two async stateful plugins at {string}", (_, route: string) => {
372
- const [method, path] = route.split(" ");
373
- mock = schmock();
374
- const plugin1: Plugin = {
375
- name: "async-step-1",
376
- process: async (ctx, response) => {
377
- await new Promise(resolve => setTimeout(resolve, 5));
378
- ctx.state.set("step1", "completed");
379
- return { context: ctx, response };
380
- },
381
- };
382
- const plugin2: Plugin = {
383
- name: "async-step-2",
384
- process: async (ctx, pluginResponse) => {
385
- await new Promise(resolve => setTimeout(resolve, 3));
386
- const step1Status = ctx.state.get("step1");
387
- if (!isRecord(pluginResponse)) {
388
- throw new Error(
389
- "Expected the stateful plugin response to be an object",
390
- );
391
- }
392
- return {
393
- context: ctx,
394
- response: {
395
- ...pluginResponse,
396
- step1: step1Status,
397
- step2: "completed",
398
- },
399
- };
400
- },
401
- };
402
- mock(`${method} ${path}` as any, { base: "data" })
403
- .pipe(plugin1)
404
- .pipe(plugin2);
405
- });
406
-
407
- When("I request {string}", async (_, request: string) => {
408
- const [method, path] = request.split(" ");
409
- response = await mock.handle(method as any, path);
410
- });
411
-
412
- Then("the response should have property {string} with value {string}", (_, property: string, value: string) => {
413
- expect(response.body).toHaveProperty(property, value);
414
- });
415
-
416
- And("the async pipeline response should have both step properties completed", () => {
417
- expect(response.body).toHaveProperty("step1", "completed");
418
- expect(response.body).toHaveProperty("step2", "completed");
419
- });
420
- });
421
- });
@@ -1,188 +0,0 @@
1
- import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
2
- import { expect } from "vitest";
3
- import { schmock } from "../index";
4
-
5
- const feature = await loadFeature("../../features/audit-core-builder.feature");
6
-
7
- describeFeature(feature, ({ Scenario }) => {
8
- // ── FIX 1.2: defineRoute must shallow-clone the config ───────────────────
9
-
10
- Scenario(
11
- "defineRoute does not mutate the caller's config object",
12
- ({ Given, When, Then }) => {
13
- let sharedConfig: Schmock.RouteConfig;
14
- let mock: Schmock.CallableMockInstance;
15
-
16
- Given("a shared config object with no contentType", () => {
17
- sharedConfig = {};
18
- });
19
-
20
- When("I register two routes with the same config object", () => {
21
- mock = schmock();
22
- mock("GET /route1", { name: "route1" }, sharedConfig);
23
- mock("GET /route2", { name: "route2" }, sharedConfig);
24
- });
25
-
26
- Then(
27
- "the shared config object remains unchanged after registration",
28
- () => {
29
- expect(Object.keys(sharedConfig)).toHaveLength(0);
30
- },
31
- );
32
- },
33
- );
34
-
35
- Scenario(
36
- "Mutating a returned config does not affect the registered route",
37
- ({ Given, When, Then, And }) => {
38
- let singleConfig: Schmock.RouteConfig;
39
- let mock: Schmock.CallableMockInstance;
40
-
41
- Given("a single config object with no contentType", () => {
42
- singleConfig = {};
43
- });
44
-
45
- When("I register one route with that config object", () => {
46
- mock = schmock();
47
- mock("GET /item", { value: 99 }, singleConfig);
48
- });
49
-
50
- And("I mutate the config object after registration", () => {
51
- singleConfig.status = 503;
52
- singleConfig.contentType = "text/html";
53
- });
54
-
55
- Then(
56
- "the registered route still responds with application/json content type",
57
- async () => {
58
- const response = await mock.handle("GET", "/item");
59
- expect(response.status).toBe(200);
60
- const ct =
61
- response.headers?.["content-type"] ??
62
- response.headers?.["Content-Type"];
63
- expect(ct).toContain("application/json");
64
- },
65
- );
66
- },
67
- );
68
-
69
- // ── FIX 2.2: duplicate route detection must normalize trailing slash ──────
70
-
71
- Scenario(
72
- "Trailing-slash path is treated as duplicate",
73
- ({ Given, And, Then }) => {
74
- let mock: Schmock.CallableMockInstance;
75
-
76
- Given("a fresh mock instance", () => {
77
- mock = schmock();
78
- });
79
-
80
- And("I register GET /users without trailing slash", () => {
81
- mock("GET /users", [{ id: 1 }], {});
82
- });
83
-
84
- And("I register GET /users with trailing slash", () => {
85
- mock("GET /users/", [{ id: 2 }], {});
86
- });
87
-
88
- Then("only one route exists for GET /users", () => {
89
- const routes = mock.getRoutes();
90
- const userRoutes = routes.filter(
91
- (r) =>
92
- r.method === "GET" && (r.path === "/users" || r.path === "/users/"),
93
- );
94
- expect(userRoutes).toHaveLength(1);
95
- });
96
- },
97
- );
98
-
99
- // ── FIX 3.3: reset()/resetState() must not mutate caller's state ─────────
100
-
101
- Scenario(
102
- "reset() does not mutate the caller's state object",
103
- ({ Given, When, Then, And }) => {
104
- let externalState: Record<string, unknown>;
105
- let mock: Schmock.CallableMockInstance;
106
-
107
- Given('a mock with external state containing key "a" equal to 1', () => {
108
- externalState = { a: 1 };
109
- mock = schmock({ state: externalState });
110
- });
111
-
112
- When("I call mock reset", () => {
113
- mock.reset();
114
- });
115
-
116
- Then('the external state still has key "a" equal to 1', () => {
117
- expect(externalState).toEqual({ a: 1 });
118
- });
119
-
120
- And("the mock internal state is empty", () => {
121
- expect(mock.getState()).toEqual({});
122
- });
123
- },
124
- );
125
-
126
- Scenario(
127
- "resetState() does not mutate the caller's state object",
128
- ({ Given, When, Then, And }) => {
129
- let externalState: Record<string, unknown>;
130
- let mock: Schmock.CallableMockInstance;
131
-
132
- Given('a mock with external state containing key "b" equal to 2', () => {
133
- externalState = { b: 2 };
134
- mock = schmock({ state: externalState });
135
- });
136
-
137
- When("I call mock resetState", () => {
138
- mock.resetState();
139
- });
140
-
141
- Then('the external state still has key "b" equal to 2', () => {
142
- expect(externalState).toEqual({ b: 2 });
143
- });
144
-
145
- And("the mock internal state is empty after resetState", () => {
146
- expect(mock.getState()).toEqual({});
147
- });
148
- },
149
- );
150
-
151
- // ── FIX 2.3: history()/lastRequest() must return deep clones ─────────────
152
-
153
- Scenario(
154
- "history() returns deep clones of request records",
155
- ({ Given, When, Then, And }) => {
156
- let mock: Schmock.CallableMockInstance;
157
-
158
- Given("a fresh mock with a route returning a nested body", () => {
159
- mock = schmock();
160
- mock("GET /data", { nested: { value: "original" } }, {});
161
- });
162
-
163
- When("I handle that route once", async () => {
164
- await mock.handle("GET", "/data");
165
- });
166
-
167
- And("I mutate the response body of the first history record", () => {
168
- const records = mock.history();
169
- expect(records).toHaveLength(1);
170
- const body = records[0].response.body as { nested: { value: string } };
171
- body.nested.value = "MUTATED";
172
- });
173
-
174
- Then("history returns the original body unchanged", () => {
175
- const records = mock.history();
176
- const body = records[0].response.body as { nested: { value: string } };
177
- expect(body.nested.value).toBe("original");
178
- });
179
-
180
- And("lastRequest returns the original body unchanged", () => {
181
- const last = mock.lastRequest();
182
- if (!last) throw new Error("expected a record");
183
- const body = last.response.body as { nested: { value: string } };
184
- expect(body.nested.value).toBe("original");
185
- });
186
- },
187
- );
188
- });
@@ -1,83 +0,0 @@
1
- import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
2
- import { expect } from "vitest";
3
- import { schmock } from "../index";
4
-
5
- const feature = await loadFeature("../../features/audit-onerror-tuple.feature");
6
-
7
- describeFeature(feature, ({ Scenario }) => {
8
- let mock: Schmock.CallableMockInstance;
9
- let response: Awaited<ReturnType<Schmock.CallableMockInstance["handle"]>>;
10
-
11
- Scenario(
12
- "2-element tuple from onError recovers with correct status and body",
13
- ({ Given, When, Then, And }) => {
14
- Given(
15
- "a mock with a plugin whose process throws and onError returns a 2-element tuple",
16
- () => {
17
- mock = schmock({ state: {} });
18
- mock("GET /fail", null, {}).pipe({
19
- name: "failing-plugin",
20
- process: (_ctx, _response) => {
21
- throw new Error("simulated failure");
22
- },
23
- onError: () => [503, { error: "recovered" }],
24
- });
25
- },
26
- );
27
-
28
- When('I handle a request to "GET /fail"', async () => {
29
- response = await mock.handle("GET", "/fail");
30
- });
31
-
32
- Then("the response status is 503", () => {
33
- expect(response.status).toBe(503);
34
- });
35
-
36
- And('the response body is \'{"error":"recovered"}\'', () => {
37
- expect(response.body).toEqual({ error: "recovered" });
38
- });
39
- },
40
- );
41
-
42
- Scenario(
43
- "3-element tuple from onError recovers and includes custom headers",
44
- ({ Given, When, Then, And }) => {
45
- Given(
46
- "a mock with a plugin whose process throws and onError returns a 3-element tuple with headers",
47
- () => {
48
- mock = schmock({ state: {} });
49
- mock("GET /fail-with-headers", null, {}).pipe({
50
- name: "failing-plugin-headers",
51
- process: (_ctx, _response) => {
52
- throw new Error("simulated failure with headers");
53
- },
54
- onError: () => [
55
- 503,
56
- { error: "recovered with headers" },
57
- { "x-recovery": "true" },
58
- ],
59
- });
60
- },
61
- );
62
-
63
- When('I handle a request to "GET /fail-with-headers"', async () => {
64
- response = await mock.handle("GET", "/fail-with-headers");
65
- });
66
-
67
- Then("the response status is 503", () => {
68
- expect(response.status).toBe(503);
69
- });
70
-
71
- And('the response body is \'{"error":"recovered with headers"}\'', () => {
72
- expect(response.body).toEqual({ error: "recovered with headers" });
73
- });
74
-
75
- And(
76
- "the response header {string} is {string}",
77
- (_, header: string, value: string) => {
78
- expect(response.headers[header]).toBe(value);
79
- },
80
- );
81
- },
82
- );
83
- });