@schmock/core 2.2.3 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/binary.d.ts +8 -0
  2. package/dist/binary.d.ts.map +1 -0
  3. package/dist/binary.js +9 -0
  4. package/dist/builder.d.ts.map +1 -1
  5. package/dist/builder.js +60 -29
  6. package/dist/http-helpers.d.ts +15 -3
  7. package/dist/http-helpers.d.ts.map +1 -1
  8. package/dist/http-helpers.js +24 -6
  9. package/dist/index.d.ts +90 -27
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/interceptor.d.ts.map +1 -1
  13. package/dist/interceptor.js +71 -20
  14. package/dist/plugin-pipeline.d.ts +11 -4
  15. package/dist/plugin-pipeline.d.ts.map +1 -1
  16. package/dist/plugin-pipeline.js +98 -35
  17. package/dist/response-parser.d.ts.map +1 -1
  18. package/dist/response-parser.js +47 -17
  19. package/package.json +5 -3
  20. package/src/binary.test.ts +126 -0
  21. package/src/binary.ts +11 -0
  22. package/src/builder.ts +82 -35
  23. package/src/http-helpers.test.ts +39 -0
  24. package/src/http-helpers.ts +46 -10
  25. package/src/index.ts +1 -0
  26. package/src/interceptor.test.ts +197 -0
  27. package/src/interceptor.ts +215 -128
  28. package/src/parser.property.test.ts +91 -55
  29. package/src/plugin-pipeline.ts +150 -45
  30. package/src/plugin-system.test.ts +95 -0
  31. package/src/response-parser.ts +58 -19
  32. package/src/server.test.ts +81 -12
  33. package/src/steps/async-support.steps.ts +29 -8
  34. package/src/steps/audit-onerror-tuple.steps.ts +14 -30
  35. package/src/steps/error-handling.steps.ts +463 -253
  36. package/src/steps/fetch-interceptor.steps.ts +457 -0
  37. package/src/steps/http-methods.steps.ts +19 -8
  38. package/src/steps/lifecycle-events.steps.ts +50 -43
  39. package/src/steps/performance-reliability.steps.ts +78 -411
  40. package/src/steps/plugin-integration.steps.ts +54 -0
  41. package/src/steps/request-history.steps.ts +412 -230
  42. package/src/steps/standalone-server.steps.ts +7 -13
  43. package/src/steps/state-concurrency.steps.ts +142 -714
  44. package/src/steps/interceptor.steps.ts +0 -260
  45. package/src/steps/stateful-workflows.steps.ts +0 -353
@@ -1,345 +1,555 @@
1
1
  import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
2
2
  import { expect } from "vitest";
3
+ import { schmock, toHttpMethod, toRouteKey } from "../index";
3
4
  import type { CallableMockInstance, Plugin } from "../types";
4
- import { schmock } from "../index";
5
5
 
6
6
  const feature = await loadFeature("../../features/error-handling.feature");
7
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
+
8
39
  describeFeature(feature, ({ Scenario }) => {
9
40
  let mock: CallableMockInstance;
10
- let response: any;
41
+ let response: Schmock.Response;
11
42
 
12
43
  Scenario("Route not found returns 404", ({ Given, When, Then, And }) => {
13
- Given("I create a mock with a GET /users route returning a user list", () => {
14
- mock = schmock();
15
- mock("GET /users", [{ id: 1, name: "John" }]);
16
- });
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
+ );
17
51
 
18
52
  When("I request {string}", async (_, request: string) => {
19
- const [method, path] = request.split(" ");
20
- response = await mock.handle(method as any, path);
53
+ const { method, path } = parseRequest(request);
54
+ response = await mock.handle(method, path);
21
55
  });
22
56
 
23
57
  Then("I should receive status {int}", (_, status: number) => {
24
58
  expect(response.status).toBe(status);
25
59
  });
26
60
 
27
- And("the response should contain error {string}", (_, errorMessage: string) => {
28
- expect(response.body.error).toContain(errorMessage);
29
- });
61
+ And(
62
+ "the response should contain error {string}",
63
+ (_, errorMessage: string) => {
64
+ expect(responseBodyString(response, "error")).toContain(errorMessage);
65
+ },
66
+ );
30
67
 
31
- And("the response should have error code {string}", (_, errorCode: string) => {
32
- expect(response.body.code).toBe(errorCode);
33
- });
68
+ And(
69
+ "the response should have error code {string}",
70
+ (_, errorCode: string) => {
71
+ expect(responseBodyString(response, "code")).toBe(errorCode);
72
+ },
73
+ );
34
74
  });
35
75
 
36
76
  Scenario("Wrong HTTP method returns 404", ({ Given, When, Then, And }) => {
37
- Given("I create a mock with a GET /api/data route returning success", () => {
38
- mock = schmock();
39
- mock("GET /api/data", { success: true });
40
- });
41
-
42
- When("I request {string}", async (_, request: string) => {
43
- const [method, path] = request.split(" ");
44
- response = await mock.handle(method as any, path);
45
- });
46
-
47
- Then("I should receive status {int}", (_, status: number) => {
48
- expect(response.status).toBe(status);
49
- });
50
-
51
- And("the response should contain error {string}", (_, errorMessage: string) => {
52
- expect(response.body.error).toContain(errorMessage);
53
- });
54
- });
55
-
56
- Scenario("Plugin throws error returns 500 with PluginError", ({ Given, When, Then, And }) => {
57
- Given("I create a mock with a plugin that throws {string}", (_, errorMsg: string) => {
58
- mock = schmock();
59
- const failingPlugin: Plugin = {
60
- name: "failing-plugin",
61
- process: () => { throw new Error(errorMsg); },
62
- };
63
- mock("GET /test", "original").pipe(failingPlugin);
64
- });
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
+ );
65
84
 
66
85
  When("I request {string}", async (_, request: string) => {
67
- const [method, path] = request.split(" ");
68
- response = await mock.handle(method as any, path);
86
+ const { method, path } = parseRequest(request);
87
+ response = await mock.handle(method, path);
69
88
  });
70
89
 
71
90
  Then("I should receive status {int}", (_, status: number) => {
72
91
  expect(response.status).toBe(status);
73
92
  });
74
93
 
75
- And("the response should contain error {string}", (_, errorMessage: string) => {
76
- expect(response.body.error).toContain(errorMessage);
77
- });
78
-
79
- And("the response should have error code {string}", (_, errorCode: string) => {
80
- expect(response.body.code).toBe(errorCode);
81
- });
94
+ And(
95
+ "the response should contain error {string}",
96
+ (_, errorMessage: string) => {
97
+ expect(responseBodyString(response, "error")).toContain(errorMessage);
98
+ },
99
+ );
82
100
  });
83
101
 
84
- Scenario("Plugin onError hook recovers from failure", ({ Given, When, Then, And }) => {
85
- Given("I create a mock with a recoverable plugin that returns {string}", (_, recoveredBody: string) => {
86
- mock = schmock();
87
- const recoverablePlugin: Plugin = {
88
- name: "recoverable",
89
- process: () => { throw new Error("Initial failure"); },
90
- onError: () => ({ status: 200, body: recoveredBody, headers: {} }),
91
- };
92
- mock("GET /test", "original").pipe(recoverablePlugin);
93
- });
94
-
95
- When("I request {string}", async (_, request: string) => {
96
- const [method, path] = request.split(" ");
97
- response = await mock.handle(method as any, path);
98
- });
99
-
100
- Then("I should receive status {int}", (_, status: number) => {
101
- expect(response.status).toBe(status);
102
- });
103
-
104
- And("I should receive text {string}", (_, expectedText: string) => {
105
- expect(response.body).toBe(expectedText);
106
- });
107
- });
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
+ });
108
123
 
109
- Scenario("Plugin returns invalid result structure", ({ Given, When, Then, And }) => {
110
- Given("I create a mock with a plugin returning an invalid result", () => {
111
- mock = schmock();
112
- const invalidPlugin: Plugin = {
113
- name: "invalid",
114
- process: () => ({ wrongStructure: true }) as any,
115
- };
116
- mock("GET /test", "original").pipe(invalidPlugin);
117
- });
124
+ Then("I should receive status {int}", (_, status: number) => {
125
+ expect(response.status).toBe(status);
126
+ });
118
127
 
119
- When("I request {string}", async (_, request: string) => {
120
- const [method, path] = request.split(" ");
121
- response = await mock.handle(method as any, path);
122
- });
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
+ });
123
166
 
124
- Then("I should receive status {int}", (_, status: number) => {
125
- expect(response.status).toBe(status);
126
- });
167
+ Then("I should receive status {int}", (_, status: number) => {
168
+ expect(response.status).toBe(status);
169
+ });
127
170
 
128
- And("the response should contain error {string}", (_, errorMessage: string) => {
129
- expect(response.body.error).toContain(errorMessage);
130
- });
131
- });
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
+ });
132
191
 
133
- Scenario("Function generator throws error returns 500", ({ Given, When, Then, And }) => {
134
- Given("I create a mock with a generator that throws {string}", (_, errorMsg: string) => {
135
- mock = schmock();
136
- mock("GET /fail", () => { throw new Error(errorMsg); });
137
- });
192
+ When("I request {string}", async (_, request: string) => {
193
+ const { method, path } = parseRequest(request);
194
+ response = await mock.handle(method, path);
195
+ });
138
196
 
139
- When("I request {string}", async (_, request: string) => {
140
- const [method, path] = request.split(" ");
141
- response = await mock.handle(method as any, path);
142
- });
197
+ Then("I should receive status {int}", (_, status: number) => {
198
+ expect(response.status).toBe(status);
199
+ });
143
200
 
144
- Then("I should receive status {int}", (_, status: number) => {
145
- expect(response.status).toBe(status);
146
- });
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
+ });
147
227
 
148
- And("the response should contain error {string}", (_, errorMessage: string) => {
149
- expect(response.body.error).toContain(errorMessage);
150
- });
228
+ Then("I should receive status {int}", (_, status: number) => {
229
+ expect(response.status).toBe(status);
230
+ });
151
231
 
152
- And("the response should have error code {string}", (_, errorCode: string) => {
153
- expect(response.body.code).toBe(errorCode);
154
- });
155
- });
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
+ );
156
247
 
157
248
  Scenario("Namespace mismatch returns 404", ({ Given, When, Then, And }) => {
158
- Given("I create a mock with namespace {string} and a GET /users route", (_, namespace: string) => {
159
- mock = schmock({ namespace });
160
- mock("GET /users", [{ id: 1 }]);
161
- });
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
+ );
162
256
 
163
257
  When("I request {string}", async (_, request: string) => {
164
- const [method, path] = request.split(" ");
165
- response = await mock.handle(method as any, path);
258
+ const { method, path } = parseRequest(request);
259
+ response = await mock.handle(method, path);
166
260
  });
167
261
 
168
262
  Then("I should receive status {int}", (_, status: number) => {
169
263
  expect(response.status).toBe(status);
170
264
  });
171
265
 
172
- And("the response should contain error {string}", (_, errorMessage: string) => {
173
- expect(response.body.error).toContain(errorMessage);
174
- });
266
+ And(
267
+ "the response should contain error {string}",
268
+ (_, errorMessage: string) => {
269
+ expect(responseBodyString(response, "error")).toContain(errorMessage);
270
+ },
271
+ );
175
272
  });
176
273
 
177
- Scenario("Multiple plugin failures cascade properly", ({ Given, When, Then, And }) => {
178
- Given("I create a mock with two failing plugins piped in sequence", () => {
179
- mock = schmock();
180
- const plugin1: Plugin = {
181
- name: "first-fail",
182
- process: () => { throw new Error("First error"); },
183
- };
184
- const plugin2: Plugin = {
185
- name: "second-fail",
186
- process: () => { throw new Error("Second error"); },
187
- };
188
- mock("GET /cascade", "original").pipe(plugin1).pipe(plugin2);
189
- });
190
-
191
- When("I request {string}", async (_, request: string) => {
192
- const [method, path] = request.split(" ");
193
- response = await mock.handle(method as any, path);
194
- });
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
+ });
195
301
 
196
- Then("I should receive status {int}", (_, status: number) => {
197
- expect(response.status).toBe(status);
198
- });
302
+ Then("I should receive status {int}", (_, status: number) => {
303
+ expect(response.status).toBe(status);
304
+ });
199
305
 
200
- And("the response should contain error {string}", (_, errorMessage: string) => {
201
- expect(response.body.error).toContain(errorMessage);
202
- });
203
- });
306
+ And(
307
+ "the response should contain error {string}",
308
+ (_, errorMessage: string) => {
309
+ expect(responseBodyString(response, "error")).toContain(errorMessage);
310
+ },
311
+ );
312
+ },
313
+ );
204
314
 
205
315
  Scenario("Plugin onError hook also fails", ({ Given, When, Then, And }) => {
206
- Given("I create a mock with a plugin whose error handler also throws", () => {
207
- mock = schmock({ debug: true });
208
- const brokenPlugin: Plugin = {
209
- name: "broken-handler",
210
- process: () => { throw new Error("Process failed"); },
211
- onError: () => { throw new Error("Handler failed"); },
212
- };
213
- mock("GET /broken", "original").pipe(brokenPlugin);
214
- });
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
+ );
215
332
 
216
333
  When("I request {string}", async (_, request: string) => {
217
- const [method, path] = request.split(" ");
218
- response = await mock.handle(method as any, path);
334
+ const { method, path } = parseRequest(request);
335
+ response = await mock.handle(method, path);
219
336
  });
220
337
 
221
338
  Then("I should receive status {int}", (_, status: number) => {
222
339
  expect(response.status).toBe(status);
223
340
  });
224
341
 
225
- And("the response should contain error {string}", (_, errorMessage: string) => {
226
- expect(response.body.error).toContain(errorMessage);
227
- });
342
+ And(
343
+ "the response should contain error {string}",
344
+ (_, errorMessage: string) => {
345
+ expect(responseBodyString(response, "error")).toContain(errorMessage);
346
+ },
347
+ );
228
348
  });
229
349
 
230
- Scenario("Empty parameter in route returns 404", ({ Given, When, Then, And }) => {
231
- Given("I create a mock with a parameterized route {string}", (_, route: string) => {
232
- mock = schmock();
233
- mock(route as any, ({ params }: any) => ({ userId: params.id }));
234
- });
235
-
236
- When("I request {string}", async (_, request: string) => {
237
- const [method, path] = request.split(" ");
238
- response = await mock.handle(method as any, path);
239
- });
240
-
241
- Then("I should receive status {int}", (_, status: number) => {
242
- expect(response.status).toBe(status);
243
- });
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
+ });
244
378
 
245
- And("the response should contain error {string}", (_, errorMessage: string) => {
246
- expect(response.body.error).toContain(errorMessage);
247
- });
248
- });
379
+ When("I request {string}", async (_, request: string) => {
380
+ const { method, path } = parseRequest(request);
381
+ response = await mock.handle(method, path);
382
+ });
249
383
 
250
- Scenario("Async generator error handling", ({ Given, When, Then, And }) => {
251
- Given("I create a mock with an async generator that throws {string}", (_, errorMsg: string) => {
252
- mock = schmock();
253
- mock("GET /async-fail", async () => {
254
- await new Promise(resolve => setTimeout(resolve, 10));
255
- throw new Error(errorMsg);
384
+ Then("I should receive status {int}", (_, status: number) => {
385
+ expect(response.status).toBe(status);
256
386
  });
257
- });
258
387
 
259
- When("I request {string}", async (_, request: string) => {
260
- const [method, path] = request.split(" ");
261
- response = await mock.handle(method as any, path);
262
- });
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
+ });
263
412
 
264
- Then("I should receive status {int}", (_, status: number) => {
265
- expect(response.status).toBe(status);
266
- });
413
+ Then("I should receive status {int}", (_, status: number) => {
414
+ expect(response.status).toBe(status);
415
+ });
267
416
 
268
- And("the response should contain error {string}", (_, errorMessage: string) => {
269
- expect(response.body.error).toContain(errorMessage);
270
- });
271
- });
417
+ And(
418
+ "the response should contain error {string}",
419
+ (_, errorMessage: string) => {
420
+ expect(responseBodyString(response, "error")).toContain(errorMessage);
421
+ },
422
+ );
423
+ },
424
+ );
272
425
 
273
- Scenario("Error responses include proper headers", ({ Given, When, Then, And }) => {
274
- Given("I create a mock with a generator that throws {string}", (_, errorMsg: string) => {
275
- mock = schmock();
276
- mock("GET /error", () => { throw new Error(errorMsg); });
277
- });
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
+ );
278
437
 
279
438
  When("I request {string}", async (_, request: string) => {
280
- const [method, path] = request.split(" ");
281
- response = await mock.handle(method as any, path);
439
+ const { method, path } = parseRequest(request);
440
+ response = await mock.handle(method, path);
282
441
  });
283
442
 
284
443
  Then("I should receive status {int}", (_, status: number) => {
285
444
  expect(response.status).toBe(status);
286
445
  });
287
446
 
288
- And("the content-type should be {string}", (_, contentType: string) => {
289
- expect(response.headers).toBeDefined();
290
- });
291
-
292
- And("the response body should be valid JSON", () => {
293
- expect(() => JSON.parse(JSON.stringify(response.body))).not.toThrow();
294
- });
447
+ And(
448
+ "the response should contain error {string}",
449
+ (_, errorMessage: string) => {
450
+ expect(responseBodyString(response, "error")).toContain(errorMessage);
451
+ },
452
+ );
295
453
  });
296
454
 
297
- Scenario("Plugin onError returns response with status 0", ({ Given, When, Then, And }) => {
298
- Given("I create a mock with a plugin whose error handler returns status 0", () => {
299
- mock = schmock();
300
- const plugin: Plugin = {
301
- name: "zero-status",
302
- process: () => { throw new Error("fail"); },
303
- onError: () => ({ status: 0, body: "zero status", headers: {} }),
304
- };
305
- mock("GET /zero", "original").pipe(plugin);
306
- });
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
+ });
307
472
 
308
- When("I request {string}", async (_, request: string) => {
309
- const [method, path] = request.split(" ");
310
- response = await mock.handle(method as any, path);
311
- });
473
+ Then("I should receive status {int}", (_, status: number) => {
474
+ expect(response.status).toBe(status);
475
+ });
312
476
 
313
- Then("I should receive status {int}", (_, status: number) => {
314
- expect(response.status).toBe(status);
315
- });
477
+ And("the content-type should be {string}", (_, contentType: string) => {
478
+ expect(response.headers["content-type"]).toBe(contentType);
479
+ });
316
480
 
317
- And("I should receive text {string}", (_, expectedText: string) => {
318
- expect(response.body).toBe(expectedText);
319
- });
320
- });
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
+ });
321
512
 
322
- Scenario("Plugin null/undefined return handling", ({ Given, When, Then, And }) => {
323
- Given("I create a mock with a plugin that returns null", () => {
324
- mock = schmock();
325
- const nullPlugin: Plugin = {
326
- name: "null-plugin",
327
- process: () => null as any,
328
- };
329
- mock("GET /null", "original").pipe(nullPlugin);
330
- });
513
+ Then("I should receive status {int}", (_, status: number) => {
514
+ expect(response.status).toBe(status);
515
+ });
331
516
 
332
- When("I request {string}", async (_, request: string) => {
333
- const [method, path] = request.split(" ");
334
- response = await mock.handle(method as any, path);
335
- });
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
+ });
336
537
 
337
- Then("I should receive status {int}", (_, status: number) => {
338
- expect(response.status).toBe(status);
339
- });
538
+ When("I request {string}", async (_, request: string) => {
539
+ const { method, path } = parseRequest(request);
540
+ response = await mock.handle(method, path);
541
+ });
340
542
 
341
- And("the response should contain error {string}", (_, errorMessage: string) => {
342
- expect(response.body.error).toContain(errorMessage);
343
- });
344
- });
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
+ );
345
555
  });