@schmock/core 2.2.2 → 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.
- package/dist/binary.d.ts +8 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +9 -0
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +60 -29
- package/dist/http-helpers.d.ts +15 -3
- package/dist/http-helpers.d.ts.map +1 -1
- package/dist/http-helpers.js +24 -6
- package/dist/index.d.ts +90 -27
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/interceptor.js +71 -20
- package/dist/plugin-pipeline.d.ts +11 -4
- package/dist/plugin-pipeline.d.ts.map +1 -1
- package/dist/plugin-pipeline.js +98 -35
- package/dist/response-parser.d.ts.map +1 -1
- package/dist/response-parser.js +47 -17
- package/package.json +5 -3
- package/src/binary.test.ts +126 -0
- package/src/binary.ts +11 -0
- package/src/builder.ts +82 -35
- package/src/http-helpers.test.ts +39 -0
- package/src/http-helpers.ts +46 -10
- package/src/index.ts +1 -0
- package/src/interceptor.test.ts +197 -0
- package/src/interceptor.ts +215 -128
- package/src/parser.property.test.ts +91 -55
- package/src/plugin-pipeline.ts +150 -45
- package/src/plugin-system.test.ts +95 -0
- package/src/response-parser.ts +58 -19
- package/src/server.test.ts +81 -12
- package/src/steps/async-support.steps.ts +29 -8
- package/src/steps/audit-onerror-tuple.steps.ts +14 -30
- package/src/steps/error-handling.steps.ts +463 -253
- package/src/steps/fetch-interceptor.steps.ts +457 -0
- package/src/steps/http-methods.steps.ts +19 -8
- package/src/steps/lifecycle-events.steps.ts +50 -43
- package/src/steps/performance-reliability.steps.ts +78 -411
- package/src/steps/plugin-integration.steps.ts +54 -0
- package/src/steps/request-history.steps.ts +412 -230
- package/src/steps/standalone-server.steps.ts +7 -13
- package/src/steps/state-concurrency.steps.ts +142 -714
- package/src/steps/interceptor.steps.ts +0 -260
- package/src/steps/stateful-workflows.steps.ts +0 -353
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/// <reference path="../../schmock.d.ts" />
|
|
2
|
+
|
|
3
|
+
import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
|
|
4
|
+
import { expect, type Mock, vi } from "vitest";
|
|
5
|
+
import { schmock } from "../index.js";
|
|
6
|
+
|
|
7
|
+
const feature = await loadFeature("../../features/fetch-interceptor.feature");
|
|
8
|
+
|
|
9
|
+
describeFeature(feature, ({ Scenario, AfterEachScenario }) => {
|
|
10
|
+
let mock: Schmock.CallableMockInstance;
|
|
11
|
+
let handle: Schmock.InterceptHandle | undefined;
|
|
12
|
+
let originalFetch: typeof globalThis.fetch;
|
|
13
|
+
let savedFetch: Mock<typeof globalThis.fetch>;
|
|
14
|
+
let fetchResponse: Response | undefined;
|
|
15
|
+
let additionalHandles: Schmock.InterceptHandle[] = [];
|
|
16
|
+
|
|
17
|
+
function setup() {
|
|
18
|
+
originalFetch = globalThis.fetch;
|
|
19
|
+
savedFetch = vi
|
|
20
|
+
.fn<typeof globalThis.fetch>()
|
|
21
|
+
.mockResolvedValue(new Response("real backend"));
|
|
22
|
+
globalThis.fetch = savedFetch;
|
|
23
|
+
mock = schmock();
|
|
24
|
+
fetchResponse = undefined;
|
|
25
|
+
additionalHandles = [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
AfterEachScenario(() => {
|
|
29
|
+
handle?.restore();
|
|
30
|
+
for (const additionalHandle of additionalHandles) {
|
|
31
|
+
additionalHandle.restore();
|
|
32
|
+
}
|
|
33
|
+
handle = undefined;
|
|
34
|
+
additionalHandles = [];
|
|
35
|
+
globalThis.fetch = originalFetch;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
Scenario(
|
|
39
|
+
"Intercept a matched fetch request",
|
|
40
|
+
({ Given, When, Then, And }) => {
|
|
41
|
+
Given(
|
|
42
|
+
'a Schmock instance with route "GET /api/users" returning users',
|
|
43
|
+
() => {
|
|
44
|
+
setup();
|
|
45
|
+
mock("GET /api/users", [{ id: 1, name: "Alice" }]);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
And("fetch is intercepted", () => {
|
|
50
|
+
handle = mock.intercept();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
When('I fetch "/api/users"', async () => {
|
|
54
|
+
fetchResponse = await fetch("http://localhost/api/users");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
Then("the fetch response status should be 200", () => {
|
|
58
|
+
expect(fetchResponse?.status).toBe(200);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
And("the fetch response body should be the mocked users", async () => {
|
|
62
|
+
const body = await fetchResponse?.json();
|
|
63
|
+
expect(body).toEqual([{ id: 1, name: "Alice" }]);
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
Scenario("Passthrough for unmatched routes", ({ Given, When, Then, And }) => {
|
|
69
|
+
Given(
|
|
70
|
+
'a Schmock instance with route "GET /api/users" returning users',
|
|
71
|
+
() => {
|
|
72
|
+
setup();
|
|
73
|
+
mock("GET /api/users", [{ id: 1 }]);
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
And("fetch is intercepted with passthrough enabled", () => {
|
|
78
|
+
handle = mock.intercept({ passthrough: true });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
When('I fetch "/api/other"', async () => {
|
|
82
|
+
fetchResponse = await fetch("http://localhost/api/other");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
Then("the original fetch should have been called", () => {
|
|
86
|
+
expect(savedFetch).toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
Scenario("Passthrough disabled returns 404", ({ Given, When, Then, And }) => {
|
|
91
|
+
Given(
|
|
92
|
+
'a Schmock instance with route "GET /api/users" returning users',
|
|
93
|
+
() => {
|
|
94
|
+
setup();
|
|
95
|
+
mock("GET /api/users", [{ id: 1 }]);
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
And("fetch is intercepted with passthrough disabled", () => {
|
|
100
|
+
handle = mock.intercept({ passthrough: false });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
When('I fetch "/api/other"', async () => {
|
|
104
|
+
fetchResponse = await fetch("http://localhost/api/other");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
Then("the fetch response status should be 404", () => {
|
|
108
|
+
expect(fetchResponse?.status).toBe(404);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
Scenario("Restore puts original fetch back", ({ Given, When, Then, And }) => {
|
|
113
|
+
let savedRef: typeof globalThis.fetch;
|
|
114
|
+
|
|
115
|
+
Given(
|
|
116
|
+
'a Schmock instance with route "GET /api/users" returning users',
|
|
117
|
+
() => {
|
|
118
|
+
setup();
|
|
119
|
+
mock("GET /api/users", [{ id: 1 }]);
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
And("fetch is intercepted", () => {
|
|
124
|
+
savedRef = globalThis.fetch;
|
|
125
|
+
handle = mock.intercept();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
When("I restore the interceptor", () => {
|
|
129
|
+
handle?.restore();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
Then("globalThis.fetch should be the original function", () => {
|
|
133
|
+
expect(globalThis.fetch).toBe(savedRef);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
Scenario(
|
|
138
|
+
"BaseUrl filters which requests are intercepted",
|
|
139
|
+
({ Given, When, Then, And }) => {
|
|
140
|
+
Given(
|
|
141
|
+
'a Schmock instance with route "GET /api/users" returning users',
|
|
142
|
+
() => {
|
|
143
|
+
setup();
|
|
144
|
+
mock("GET /api/users", [{ id: 1 }]);
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
And('fetch is intercepted with baseUrl "/api"', () => {
|
|
149
|
+
handle = mock.intercept({ baseUrl: "/api" });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
When('I fetch "/other/endpoint"', async () => {
|
|
153
|
+
await fetch("http://localhost/other/endpoint");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
Then("the original fetch should have been called", () => {
|
|
157
|
+
expect(savedFetch).toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
Scenario(
|
|
163
|
+
"Origin-form baseUrl intercepts matching origin only",
|
|
164
|
+
({ Given, When, Then, And }) => {
|
|
165
|
+
Given(
|
|
166
|
+
'a Schmock instance with route "GET /api/users" returning users',
|
|
167
|
+
() => {
|
|
168
|
+
setup();
|
|
169
|
+
mock("GET /api/users", [{ id: 1 }]);
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
And('fetch is intercepted with baseUrl "https://api.example.com"', () => {
|
|
174
|
+
handle = mock.intercept({ baseUrl: "https://api.example.com" });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
When('I fetch "https://api.example.com/api/users"', async () => {
|
|
178
|
+
fetchResponse = await fetch("https://api.example.com/api/users");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
Then("the response status should be {int}", (_, status: number) => {
|
|
182
|
+
expect(fetchResponse?.status).toBe(status);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
And("the fetch response body should be the mocked users", async () => {
|
|
186
|
+
expect(await fetchResponse?.json()).toEqual([{ id: 1 }]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
And("the original fetch should not have been called", () => {
|
|
190
|
+
expect(savedFetch).not.toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
When('I fetch "https://other.example.com/api/users"', async () => {
|
|
194
|
+
await fetch("https://other.example.com/api/users");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
Then(
|
|
198
|
+
"the original fetch should have been called exactly once for {string}",
|
|
199
|
+
(_, url: string) => {
|
|
200
|
+
expect(savedFetch.mock.calls).toEqual([[url, undefined]]);
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
Scenario(
|
|
207
|
+
"Origin-form baseUrl with a path prefix requires both to match",
|
|
208
|
+
({ Given, When, Then, And }) => {
|
|
209
|
+
Given(
|
|
210
|
+
'a Schmock instance with route "GET /v1/users" returning users',
|
|
211
|
+
() => {
|
|
212
|
+
setup();
|
|
213
|
+
mock("GET /v1/users", [{ id: 1 }]);
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
And(
|
|
218
|
+
'fetch is intercepted with baseUrl "https://api.example.com/v1"',
|
|
219
|
+
() => {
|
|
220
|
+
handle = mock.intercept({ baseUrl: "https://api.example.com/v1" });
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
When('I fetch "https://api.example.com/v1/users"', async () => {
|
|
225
|
+
fetchResponse = await fetch("https://api.example.com/v1/users");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
Then("the response status should be {int}", (_, status: number) => {
|
|
229
|
+
expect(fetchResponse?.status).toBe(status);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
And("the fetch response body should be the mocked users", async () => {
|
|
233
|
+
expect(await fetchResponse?.json()).toEqual([{ id: 1 }]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
And("the original fetch should not have been called", () => {
|
|
237
|
+
expect(savedFetch).not.toHaveBeenCalled();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
When('I fetch "https://api.example.com/users"', async () => {
|
|
241
|
+
await fetch("https://api.example.com/users");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
Then(
|
|
245
|
+
"the original fetch should have been called exactly once for {string}",
|
|
246
|
+
(_, url: string) => {
|
|
247
|
+
expect(savedFetch.mock.calls).toEqual([[url, undefined]]);
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
Scenario(
|
|
254
|
+
"beforeRequest hook modifies the request",
|
|
255
|
+
({ Given, When, Then, And }) => {
|
|
256
|
+
Given(
|
|
257
|
+
'a Schmock instance with route "GET /api/users" that reads headers',
|
|
258
|
+
() => {
|
|
259
|
+
setup();
|
|
260
|
+
mock("GET /api/users", ({ headers }) => [
|
|
261
|
+
200,
|
|
262
|
+
{ token: headers["x-token"] },
|
|
263
|
+
]);
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
And(
|
|
268
|
+
"fetch is intercepted with a beforeRequest hook that adds a header",
|
|
269
|
+
() => {
|
|
270
|
+
handle = mock.intercept({
|
|
271
|
+
beforeRequest: (req) => ({
|
|
272
|
+
...req,
|
|
273
|
+
headers: { ...req.headers, "x-token": "injected" },
|
|
274
|
+
}),
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
When('I fetch "/api/users"', async () => {
|
|
280
|
+
fetchResponse = await fetch("http://localhost/api/users");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
Then(
|
|
284
|
+
"the response should contain the injected header value",
|
|
285
|
+
async () => {
|
|
286
|
+
const body = await fetchResponse?.json();
|
|
287
|
+
expect(body).toEqual({ token: "injected" });
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
Scenario(
|
|
294
|
+
"beforeResponse hook modifies the response",
|
|
295
|
+
({ Given, When, Then, And }) => {
|
|
296
|
+
Given(
|
|
297
|
+
'a Schmock instance with route "GET /api/users" returning users',
|
|
298
|
+
() => {
|
|
299
|
+
setup();
|
|
300
|
+
mock("GET /api/users", [{ id: 1 }]);
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
And(
|
|
305
|
+
"fetch is intercepted with a beforeResponse hook that adds a header",
|
|
306
|
+
() => {
|
|
307
|
+
handle = mock.intercept({
|
|
308
|
+
beforeResponse: (res) => ({
|
|
309
|
+
...res,
|
|
310
|
+
headers: { ...res.headers, "x-mock": "true" },
|
|
311
|
+
}),
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
When('I fetch "/api/users"', async () => {
|
|
317
|
+
fetchResponse = await fetch("http://localhost/api/users");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
Then("the fetch response should have the injected header", () => {
|
|
321
|
+
expect(fetchResponse?.headers.get("x-mock")).toBe("true");
|
|
322
|
+
});
|
|
323
|
+
},
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
Scenario("Double intercept throws an error", ({ Given, When, Then, And }) => {
|
|
327
|
+
let error: Error | undefined;
|
|
328
|
+
|
|
329
|
+
Given(
|
|
330
|
+
'a Schmock instance with route "GET /api/users" returning users',
|
|
331
|
+
() => {
|
|
332
|
+
setup();
|
|
333
|
+
mock("GET /api/users", [{ id: 1 }]);
|
|
334
|
+
},
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
And("fetch is intercepted", () => {
|
|
338
|
+
handle = mock.intercept();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
When("I try to intercept again", () => {
|
|
342
|
+
try {
|
|
343
|
+
mock.intercept();
|
|
344
|
+
} catch (caught) {
|
|
345
|
+
error = caught instanceof Error ? caught : new Error(String(caught));
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
Then("it should throw an error about already intercepting", () => {
|
|
350
|
+
expect(error?.message).toMatch(/already intercepting/i);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
Scenario(
|
|
355
|
+
"Multiple mocks compose from newest to oldest",
|
|
356
|
+
({ Given, When, Then, And }) => {
|
|
357
|
+
let newerMock: Schmock.CallableMockInstance;
|
|
358
|
+
let newerHandle: Schmock.InterceptHandle | undefined;
|
|
359
|
+
let sharedResponse: Response | undefined;
|
|
360
|
+
let olderResponse: Response | undefined;
|
|
361
|
+
|
|
362
|
+
Given(
|
|
363
|
+
'an older mock for both routes and a newer mock for "GET /api/shared"',
|
|
364
|
+
() => {
|
|
365
|
+
setup();
|
|
366
|
+
mock("GET /api/older", { source: "older" });
|
|
367
|
+
mock("GET /api/shared", { source: "older" });
|
|
368
|
+
newerMock = schmock();
|
|
369
|
+
newerMock("GET /api/shared", { source: "newer" });
|
|
370
|
+
},
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
And("both mocks intercept fetch in that order", () => {
|
|
374
|
+
handle = mock.intercept();
|
|
375
|
+
newerHandle = newerMock.intercept();
|
|
376
|
+
additionalHandles.push(newerHandle);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
When("I fetch the shared and older routes", async () => {
|
|
380
|
+
sharedResponse = await fetch("http://localhost/api/shared");
|
|
381
|
+
olderResponse = await fetch("http://localhost/api/older");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
Then("the shared route should use the newer mock", async () => {
|
|
385
|
+
expect(await sharedResponse?.json()).toEqual({ source: "newer" });
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
And("the older route should fall through to the older mock", async () => {
|
|
389
|
+
expect(await olderResponse?.json()).toEqual({ source: "older" });
|
|
390
|
+
});
|
|
391
|
+
},
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
Scenario(
|
|
395
|
+
"Interceptors can be restored out of order",
|
|
396
|
+
({ Given, When, Then }) => {
|
|
397
|
+
let newerMock: Schmock.CallableMockInstance;
|
|
398
|
+
let newerHandle: Schmock.InterceptHandle | undefined;
|
|
399
|
+
let newerResponse: Response | undefined;
|
|
400
|
+
|
|
401
|
+
Given("an older mock and a newer mock both intercepting fetch", () => {
|
|
402
|
+
setup();
|
|
403
|
+
mock("GET /api/older", { source: "older" });
|
|
404
|
+
newerMock = schmock();
|
|
405
|
+
newerMock("GET /api/newer", { source: "newer" });
|
|
406
|
+
handle = mock.intercept();
|
|
407
|
+
newerHandle = newerMock.intercept();
|
|
408
|
+
additionalHandles.push(newerHandle);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
When("I restore the older interceptor first", () => {
|
|
412
|
+
handle?.restore();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
Then("the newer interceptor should remain active", async () => {
|
|
416
|
+
newerResponse = await fetch("http://localhost/api/newer");
|
|
417
|
+
expect(newerHandle?.active).toBe(true);
|
|
418
|
+
expect(await newerResponse.json()).toEqual({ source: "newer" });
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
When("I restore the newer interceptor", () => {
|
|
422
|
+
newerHandle?.restore();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
Then("globalThis.fetch should be the original function", () => {
|
|
426
|
+
expect(globalThis.fetch).toBe(savedFetch);
|
|
427
|
+
});
|
|
428
|
+
},
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
Scenario(
|
|
432
|
+
"Restore preserves a third-party fetch replacement",
|
|
433
|
+
({ Given, When, Then, And }) => {
|
|
434
|
+
let replacementFetch: Mock<typeof globalThis.fetch>;
|
|
435
|
+
|
|
436
|
+
Given("fetch is intercepted", () => {
|
|
437
|
+
setup();
|
|
438
|
+
handle = mock.intercept();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
When("another library replaces globalThis.fetch", () => {
|
|
442
|
+
replacementFetch = vi
|
|
443
|
+
.fn<typeof globalThis.fetch>()
|
|
444
|
+
.mockResolvedValue(new Response("third-party backend"));
|
|
445
|
+
globalThis.fetch = replacementFetch;
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
And("I restore the interceptor", () => {
|
|
449
|
+
handle?.restore();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
Then("the third-party fetch replacement should remain installed", () => {
|
|
453
|
+
expect(globalThis.fetch).toBe(replacementFetch);
|
|
454
|
+
});
|
|
455
|
+
},
|
|
456
|
+
);
|
|
457
|
+
});
|
|
@@ -5,6 +5,10 @@ import type { CallableMockInstance, Plugin } from "../types";
|
|
|
5
5
|
|
|
6
6
|
const feature = await loadFeature("../../features/http-methods.feature");
|
|
7
7
|
|
|
8
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
8
12
|
describeFeature(feature, ({ Scenario }) => {
|
|
9
13
|
let mock: CallableMockInstance;
|
|
10
14
|
let response: any;
|
|
@@ -299,14 +303,21 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
299
303
|
mock = schmock();
|
|
300
304
|
const loggerPlugin: Plugin = {
|
|
301
305
|
name: 'method-logger',
|
|
302
|
-
process: (ctx,
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
306
|
+
process: (ctx, pluginResponse) => {
|
|
307
|
+
if (!isRecord(pluginResponse)) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
"Expected the logger plugin response to be an object",
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
context: ctx,
|
|
314
|
+
response: {
|
|
315
|
+
...pluginResponse,
|
|
316
|
+
method: ctx.method,
|
|
317
|
+
logged: true,
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
},
|
|
310
321
|
};
|
|
311
322
|
mock('GET /logged', { data: 'get' }).pipe(loggerPlugin);
|
|
312
323
|
mock('POST /logged', { data: 'post' }).pipe(loggerPlugin);
|
|
@@ -15,6 +15,19 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
19
|
+
return typeof value === "object" && value !== null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function eventData(type: string): Record<string, unknown> {
|
|
23
|
+
const event = events.find((candidate) => candidate.type === type);
|
|
24
|
+
expect(event).toBeDefined();
|
|
25
|
+
if (!event || !isRecord(event.data)) {
|
|
26
|
+
throw new Error(`Expected object payload for ${type}`);
|
|
27
|
+
}
|
|
28
|
+
return event.data;
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
Scenario("Events fire at correct times", ({ Given, And, When, Then }) => {
|
|
19
32
|
Given('a mock with a route "GET /items"', () => {
|
|
20
33
|
mock = schmock({ state: {} });
|
|
@@ -33,58 +46,52 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
33
46
|
await mock.handle("GET", "/items");
|
|
34
47
|
});
|
|
35
48
|
|
|
36
|
-
Then(
|
|
37
|
-
expect(events.
|
|
49
|
+
Then("the event order should be {string}", (_, expectedOrder: string) => {
|
|
50
|
+
expect(events.map((event) => event.type)).toEqual(
|
|
51
|
+
expectedOrder.split(","),
|
|
52
|
+
);
|
|
38
53
|
});
|
|
39
54
|
|
|
40
55
|
And('the "request:match" event fired with routePath "/items"', () => {
|
|
41
|
-
|
|
42
|
-
expect(match).toBeDefined();
|
|
43
|
-
const data = match?.data as Record<string, unknown>;
|
|
44
|
-
expect(data.routePath).toBe("/items");
|
|
56
|
+
expect(eventData("request:match").routePath).toBe("/items");
|
|
45
57
|
});
|
|
46
58
|
|
|
47
59
|
And('the "request:end" event fired with status 200', () => {
|
|
48
|
-
|
|
49
|
-
expect(end).toBeDefined();
|
|
50
|
-
const data = end?.data as Record<string, unknown>;
|
|
51
|
-
expect(data.status).toBe(200);
|
|
60
|
+
expect(eventData("request:end").status).toBe(200);
|
|
52
61
|
});
|
|
53
62
|
});
|
|
54
63
|
|
|
55
|
-
Scenario(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
mock
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
});
|
|
87
|
-
});
|
|
64
|
+
Scenario(
|
|
65
|
+
"Not found event fires for unmatched routes",
|
|
66
|
+
({ Given, And, When, Then }) => {
|
|
67
|
+
Given('a mock with a route "GET /items"', () => {
|
|
68
|
+
mock = schmock({ state: {} });
|
|
69
|
+
mock("GET /items", [{ id: 1 }], {});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
And("I register listeners for all events", () => {
|
|
73
|
+
events = [];
|
|
74
|
+
mock.on("request:start", collectEvent("request:start"));
|
|
75
|
+
mock.on("request:match", collectEvent("request:match"));
|
|
76
|
+
mock.on("request:notfound", collectEvent("request:notfound"));
|
|
77
|
+
mock.on("request:end", collectEvent("request:end"));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
When('I request "GET /missing"', async () => {
|
|
81
|
+
await mock.handle("GET", "/missing");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
Then("the event order should be {string}", (_, expectedOrder: string) => {
|
|
85
|
+
expect(events.map((event) => event.type)).toEqual(
|
|
86
|
+
expectedOrder.split(","),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
And('the "request:end" event fired with status 404', () => {
|
|
91
|
+
expect(eventData("request:end").status).toBe(404);
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
);
|
|
88
95
|
|
|
89
96
|
Scenario("Off removes listener", ({ Given, And, When, Then }) => {
|
|
90
97
|
Given('a mock with a route "GET /items"', () => {
|