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