@schmock/core 2.3.0 → 2.4.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/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/abort.d.ts +3 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +30 -0
- package/dist/builder.d.ts +31 -10
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +760 -190
- package/dist/constants.d.ts +38 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +123 -2
- package/dist/errors.d.ts +14 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +41 -6
- package/dist/helpers.d.ts +9 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +18 -2
- package/dist/http-helpers.d.ts +39 -4
- package/dist/http-helpers.d.ts.map +1 -1
- package/dist/http-helpers.js +147 -49
- package/dist/index.d.ts +292 -34
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/interceptor.d.ts +11 -1
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/interceptor.js +316 -169
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +12 -3
- package/dist/plugin-pipeline.d.ts +3 -3
- package/dist/plugin-pipeline.d.ts.map +1 -1
- package/dist/plugin-pipeline.js +29 -14
- package/dist/response-normalizer.d.ts +16 -0
- package/dist/response-normalizer.d.ts.map +1 -0
- package/dist/response-normalizer.js +316 -0
- package/dist/response-parser.d.ts.map +1 -1
- package/dist/response-parser.js +39 -2
- package/dist/route-matcher.d.ts +3 -0
- package/dist/route-matcher.d.ts.map +1 -1
- package/dist/route-matcher.js +12 -6
- package/dist/types.d.ts +7 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +18 -6
- package/src/audit-core-builder.test.ts +0 -218
- package/src/audit-response-guard.test.ts +0 -38
- package/src/binary.test.ts +0 -126
- package/src/binary.ts +0 -11
- package/src/builder.test.ts +0 -289
- package/src/builder.ts +0 -748
- package/src/constants.test.ts +0 -99
- package/src/constants.ts +0 -73
- package/src/debug.test.ts +0 -241
- package/src/delay.test.ts +0 -319
- package/src/dist-shape.test.ts +0 -35
- package/src/errors.test.ts +0 -223
- package/src/errors.ts +0 -130
- package/src/factory.test.ts +0 -133
- package/src/helpers.test.ts +0 -147
- package/src/helpers.ts +0 -58
- package/src/http-helpers.test.ts +0 -39
- package/src/http-helpers.ts +0 -149
- package/src/index.ts +0 -152
- package/src/interceptor.test.ts +0 -488
- package/src/interceptor.ts +0 -407
- package/src/namespace.test.ts +0 -274
- package/src/parser.property.test.ts +0 -630
- package/src/parser.test.ts +0 -148
- package/src/parser.ts +0 -64
- package/src/plugin-pipeline.ts +0 -208
- package/src/plugin-system.test.ts +0 -697
- package/src/response-parser.ts +0 -114
- package/src/response-parsing.test.ts +0 -333
- package/src/route-matcher.ts +0 -69
- package/src/route-matching.test.ts +0 -394
- package/src/server.test.ts +0 -234
- package/src/smart-defaults.test.ts +0 -361
- package/src/steps/async-support.steps.ts +0 -421
- package/src/steps/audit-core-builder.steps.ts +0 -188
- package/src/steps/audit-onerror-tuple.steps.ts +0 -83
- package/src/steps/basic-usage.steps.ts +0 -245
- package/src/steps/developer-experience.steps.ts +0 -204
- package/src/steps/error-handling.steps.ts +0 -555
- package/src/steps/fetch-interceptor.steps.ts +0 -457
- package/src/steps/fluent-api.steps.ts +0 -255
- package/src/steps/http-methods.steps.ts +0 -342
- package/src/steps/lifecycle-events.steps.ts +0 -149
- package/src/steps/performance-reliability.steps.ts +0 -90
- package/src/steps/plugin-integration.steps.ts +0 -334
- package/src/steps/request-history.steps.ts +0 -458
- package/src/steps/response-delay.steps.ts +0 -88
- package/src/steps/route-key-format.steps.ts +0 -99
- package/src/steps/standalone-server.steps.ts +0 -227
- package/src/steps/state-concurrency.steps.ts +0 -167
- package/src/types.ts +0 -36
|
@@ -1,342 +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/http-methods.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
|
-
|
|
17
|
-
Scenario("GET method with query parameters", ({ Given, When, Then }) => {
|
|
18
|
-
Given("I create a mock with a GET search endpoint", () => {
|
|
19
|
-
mock = schmock();
|
|
20
|
-
mock('GET /search', ({ query }) => ({
|
|
21
|
-
results: [],
|
|
22
|
-
query: query.q,
|
|
23
|
-
page: parseInt(query.page || '1'),
|
|
24
|
-
limit: parseInt(query.limit || '10'),
|
|
25
|
-
}));
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
When("I make a GET request to {string}", async (_, path: string) => {
|
|
29
|
-
const [pathname, queryString] = path.split("?");
|
|
30
|
-
const query: Record<string, string> = {};
|
|
31
|
-
if (queryString) {
|
|
32
|
-
queryString.split("&").forEach((param) => {
|
|
33
|
-
const [key, value] = param.split("=");
|
|
34
|
-
query[key] = value;
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
response = await mock.handle('GET', pathname, { query });
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
Then("I should receive GET method response:", (_, docString: string) => {
|
|
41
|
-
const expected = JSON.parse(docString);
|
|
42
|
-
expect(response.body).toEqual(expected);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
Scenario("POST method with JSON body", ({ Given, When, Then, And }) => {
|
|
47
|
-
Given("I create a mock with a POST users endpoint", () => {
|
|
48
|
-
mock = schmock();
|
|
49
|
-
mock('POST /users', ({ body }) => [201, {
|
|
50
|
-
id: 123,
|
|
51
|
-
...(body as Record<string, unknown>),
|
|
52
|
-
createdAt: '2023-01-01T00:00:00Z',
|
|
53
|
-
}]);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
When("I make a POST request to {string} with JSON body:", async (_, path: string, docString: string) => {
|
|
57
|
-
const body = JSON.parse(docString);
|
|
58
|
-
response = await mock.handle('POST', path, { body });
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
Then("I should receive status {int}", (_, status: number) => {
|
|
62
|
-
expect(response.status).toBe(status);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
And("I should receive POST method response:", (_, docString: string) => {
|
|
66
|
-
const expected = JSON.parse(docString);
|
|
67
|
-
expect(response.body).toEqual(expected);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
Scenario("PUT method for resource updates", ({ Given, When, Then }) => {
|
|
72
|
-
Given("I create a mock with a PUT users endpoint", () => {
|
|
73
|
-
mock = schmock();
|
|
74
|
-
mock('PUT /users/:id', ({ params, body }) => ({
|
|
75
|
-
id: parseInt(params.id),
|
|
76
|
-
...(body as Record<string, unknown>),
|
|
77
|
-
updatedAt: '2023-01-01T00:00:00Z',
|
|
78
|
-
}));
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
When("I make a PUT request to {string} with JSON body:", async (_, path: string, docString: string) => {
|
|
82
|
-
const body = JSON.parse(docString);
|
|
83
|
-
response = await mock.handle('PUT', path, { body });
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
Then("I should receive PUT method response:", (_, docString: string) => {
|
|
87
|
-
const expected = JSON.parse(docString);
|
|
88
|
-
expect(response.body).toEqual(expected);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
Scenario("DELETE method with confirmation", ({ Given, When, Then, And }) => {
|
|
93
|
-
Given("I create a mock with a DELETE users endpoint", () => {
|
|
94
|
-
mock = schmock();
|
|
95
|
-
mock('DELETE /users/:id', ({ params }) => [204, null]);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
When("I make a DELETE request to {string}", async (_, path: string) => {
|
|
99
|
-
response = await mock.handle('DELETE', path);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
Then("I should receive status {int}", (_, status: number) => {
|
|
103
|
-
expect(response.status).toBe(status);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
And("the DELETE response body should be empty", () => {
|
|
107
|
-
expect(response.body).toBeUndefined();
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
Scenario("PATCH method for partial updates", ({ Given, When, Then }) => {
|
|
112
|
-
Given("I create a mock with a PATCH users endpoint", () => {
|
|
113
|
-
mock = schmock();
|
|
114
|
-
mock('PATCH /users/:id', ({ params, body }) => ({
|
|
115
|
-
id: parseInt(params.id),
|
|
116
|
-
email: 'existing@example.com',
|
|
117
|
-
...(body as Record<string, unknown>),
|
|
118
|
-
updatedAt: '2023-01-01T00:00:00Z',
|
|
119
|
-
}));
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
When("I make a PATCH request to {string} with JSON body:", async (_, path: string, docString: string) => {
|
|
123
|
-
const body = JSON.parse(docString);
|
|
124
|
-
response = await mock.handle('PATCH', path, { body });
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
Then("I should receive PATCH method response:", (_, docString: string) => {
|
|
128
|
-
const expected = JSON.parse(docString);
|
|
129
|
-
expect(response.body).toEqual(expected);
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
Scenario("HEAD method returns headers only", ({ Given, When, Then, And }) => {
|
|
134
|
-
Given("I create a mock with a HEAD users endpoint", () => {
|
|
135
|
-
mock = schmock();
|
|
136
|
-
mock('HEAD /users/:id', ({ params }) => [200, null, {
|
|
137
|
-
'Content-Type': 'application/json',
|
|
138
|
-
'Last-Modified': 'Wed, 01 Jan 2023 00:00:00 GMT',
|
|
139
|
-
'Content-Length': '156',
|
|
140
|
-
}]);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
When("I make a HEAD request to {string}", async (_, path: string) => {
|
|
144
|
-
response = await mock.handle('HEAD', path);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
Then("I should receive status {int}", (_, status: number) => {
|
|
148
|
-
expect(response.status).toBe(status);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
And("the HEAD response body should be empty", () => {
|
|
152
|
-
expect(response.body).toBeUndefined();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
And("the HEAD response should have proper headers set", () => {
|
|
156
|
-
expect(response.headers?.['Content-Type']).toBe('application/json');
|
|
157
|
-
expect(response.headers?.['Last-Modified']).toBe('Wed, 01 Jan 2023 00:00:00 GMT');
|
|
158
|
-
expect(response.headers?.['Content-Length']).toBe('156');
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
Scenario("OPTIONS method for CORS preflight", ({ Given, When, Then, And }) => {
|
|
163
|
-
Given("I create a mock with an OPTIONS users endpoint", () => {
|
|
164
|
-
mock = schmock();
|
|
165
|
-
mock('OPTIONS /api/users', () => [200, null, {
|
|
166
|
-
'Access-Control-Allow-Origin': '*',
|
|
167
|
-
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
|
168
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
169
|
-
}]);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
When("I make an OPTIONS request to {string}", async (_, path: string) => {
|
|
173
|
-
response = await mock.handle('OPTIONS', path);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
Then("I should receive status {int}", (_, status: number) => {
|
|
177
|
-
expect(response.status).toBe(status);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
And("the OPTIONS response body should be empty", () => {
|
|
181
|
-
expect(response.body).toBeUndefined();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
And("the OPTIONS response should have header {string} with value {string}", (_, headerName: string, headerValue: string) => {
|
|
185
|
-
expect(response.headers?.[headerName]).toBe(headerValue);
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
Scenario("Multiple methods on same path", ({ Given, When, Then, And }) => {
|
|
190
|
-
Given("I create a mock with GET, POST, PUT, and DELETE on the same path", () => {
|
|
191
|
-
mock = schmock();
|
|
192
|
-
mock('GET /resource', { action: 'read' });
|
|
193
|
-
mock('POST /resource', { action: 'create' });
|
|
194
|
-
mock('PUT /resource', { action: 'update' });
|
|
195
|
-
mock('DELETE /resource', { action: 'delete' });
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
When("I test all methods on {string}", async (_, path: string) => {
|
|
199
|
-
responses = [];
|
|
200
|
-
responses.push(await mock.handle('GET', path));
|
|
201
|
-
responses.push(await mock.handle('POST', path));
|
|
202
|
-
responses.push(await mock.handle('PUT', path));
|
|
203
|
-
responses.push(await mock.handle('DELETE', path));
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
Then("the GET method should return:", (_, docString: string) => {
|
|
207
|
-
const expected = JSON.parse(docString);
|
|
208
|
-
expect(responses[0].body).toEqual(expected);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
And("the POST method should return:", (_, docString: string) => {
|
|
212
|
-
const expected = JSON.parse(docString);
|
|
213
|
-
expect(responses[1].body).toEqual(expected);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
And("the PUT method should return:", (_, docString: string) => {
|
|
217
|
-
const expected = JSON.parse(docString);
|
|
218
|
-
expect(responses[2].body).toEqual(expected);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
And("the DELETE method should return:", (_, docString: string) => {
|
|
222
|
-
const expected = JSON.parse(docString);
|
|
223
|
-
expect(responses[3].body).toEqual(expected);
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
Scenario("Method-specific content types", ({ Given, When, Then, And }) => {
|
|
228
|
-
Given("I create a mock with JSON, XML, text, and upload endpoints", () => {
|
|
229
|
-
mock = schmock();
|
|
230
|
-
mock('GET /data.json', { data: 'json' });
|
|
231
|
-
mock('GET /data.xml', '<data>xml</data>', { contentType: 'application/xml' });
|
|
232
|
-
mock('GET /data.txt', 'plain text data');
|
|
233
|
-
mock('POST /upload', 'File uploaded successfully', { contentType: 'text/plain' });
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
When("I test method-specific content types", async () => {
|
|
237
|
-
responses = [];
|
|
238
|
-
responses.push(await mock.handle('GET', '/data.json'));
|
|
239
|
-
responses.push(await mock.handle('GET', '/data.xml'));
|
|
240
|
-
responses.push(await mock.handle('GET', '/data.txt'));
|
|
241
|
-
responses.push(await mock.handle('POST', '/upload'));
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
Then("the JSON endpoint should have content-type {string}", (_, contentType: string) => {
|
|
245
|
-
expect(responses[0].headers?.["content-type"]).toBe(contentType);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
And("the XML endpoint should have content-type {string}", (_, contentType: string) => {
|
|
249
|
-
expect(responses[1].headers?.["content-type"]).toBe(contentType);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
And("the text endpoint should have content-type {string}", (_, contentType: string) => {
|
|
253
|
-
expect(responses[2].headers?.["content-type"]).toBe(contentType);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
And("the upload endpoint should have content-type {string}", (_, contentType: string) => {
|
|
257
|
-
expect(responses[3].headers?.["content-type"]).toBe(contentType);
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
Scenario("Method with request headers validation", ({ Given, When, Then, And }) => {
|
|
262
|
-
Given("I create a mock with authorization header checking", () => {
|
|
263
|
-
mock = schmock();
|
|
264
|
-
mock('POST /secure', ({ headers, body }) => {
|
|
265
|
-
if (headers.authorization !== 'Bearer valid-token') {
|
|
266
|
-
return [401, { error: 'Unauthorized' }];
|
|
267
|
-
}
|
|
268
|
-
return [200, { message: 'Success', data: body }];
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
When("I make a POST request with valid headers", async () => {
|
|
273
|
-
response = await mock.handle('POST', '/secure', {
|
|
274
|
-
headers: { authorization: 'Bearer valid-token' },
|
|
275
|
-
body: { test: true }
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
Then("I should receive authorized response:", (_, docString: string) => {
|
|
280
|
-
const expected = JSON.parse(docString);
|
|
281
|
-
expect(response.body).toEqual(expected);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
When("I make a POST request with invalid headers", async () => {
|
|
285
|
-
response = await mock.handle('POST', '/secure', {
|
|
286
|
-
headers: { authorization: 'Bearer invalid-token' },
|
|
287
|
-
body: { test: true }
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
Then("I should receive status {int}", (_, status: number) => {
|
|
292
|
-
expect(response.status).toBe(status);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
And("I should receive unauthorized response:", (_, docString: string) => {
|
|
296
|
-
const expected = JSON.parse(docString);
|
|
297
|
-
expect(response.body).toEqual(expected);
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
Scenario("Method chaining with plugins", ({ Given, When, Then, And }) => {
|
|
302
|
-
Given("I create a mock with a logger plugin on GET and POST", () => {
|
|
303
|
-
mock = schmock();
|
|
304
|
-
const loggerPlugin: Plugin = {
|
|
305
|
-
name: 'method-logger',
|
|
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
|
-
},
|
|
321
|
-
};
|
|
322
|
-
mock('GET /logged', { data: 'get' }).pipe(loggerPlugin);
|
|
323
|
-
mock('POST /logged', { data: 'post' }).pipe(loggerPlugin);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
When("I test method chaining with plugins", async () => {
|
|
327
|
-
responses = [];
|
|
328
|
-
responses.push(await mock.handle('GET', '/logged'));
|
|
329
|
-
responses.push(await mock.handle('POST', '/logged'));
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
Then("the GET with plugin should return:", (_, docString: string) => {
|
|
333
|
-
const expected = JSON.parse(docString);
|
|
334
|
-
expect(responses[0].body).toEqual(expected);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
And("the POST with plugin should return:", (_, docString: string) => {
|
|
338
|
-
const expected = JSON.parse(docString);
|
|
339
|
-
expect(responses[1].body).toEqual(expected);
|
|
340
|
-
});
|
|
341
|
-
});
|
|
342
|
-
});
|
|
@@ -1,149 +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/lifecycle-events.feature");
|
|
6
|
-
|
|
7
|
-
describeFeature(feature, ({ Scenario }) => {
|
|
8
|
-
let mock: Schmock.CallableMockInstance;
|
|
9
|
-
let events: Array<{ type: string; data: unknown }>;
|
|
10
|
-
let removedFired: boolean;
|
|
11
|
-
|
|
12
|
-
function collectEvent(type: string) {
|
|
13
|
-
return (data: unknown) => {
|
|
14
|
-
events.push({ type, data });
|
|
15
|
-
};
|
|
16
|
-
}
|
|
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
|
-
|
|
31
|
-
Scenario("Events fire at correct times", ({ Given, And, When, Then }) => {
|
|
32
|
-
Given('a mock with a route "GET /items"', () => {
|
|
33
|
-
mock = schmock({ state: {} });
|
|
34
|
-
mock("GET /items", [{ id: 1 }], {});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
And("I register listeners for all events", () => {
|
|
38
|
-
events = [];
|
|
39
|
-
mock.on("request:start", collectEvent("request:start"));
|
|
40
|
-
mock.on("request:match", collectEvent("request:match"));
|
|
41
|
-
mock.on("request:notfound", collectEvent("request:notfound"));
|
|
42
|
-
mock.on("request:end", collectEvent("request:end"));
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
When('I request "GET /items"', async () => {
|
|
46
|
-
await mock.handle("GET", "/items");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
Then("the event order should be {string}", (_, expectedOrder: string) => {
|
|
50
|
-
expect(events.map((event) => event.type)).toEqual(
|
|
51
|
-
expectedOrder.split(","),
|
|
52
|
-
);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
And('the "request:match" event fired with routePath "/items"', () => {
|
|
56
|
-
expect(eventData("request:match").routePath).toBe("/items");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
And('the "request:end" event fired with status 200', () => {
|
|
60
|
-
expect(eventData("request:end").status).toBe(200);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
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
|
-
);
|
|
95
|
-
|
|
96
|
-
Scenario("Off removes listener", ({ Given, And, When, Then }) => {
|
|
97
|
-
Given('a mock with a route "GET /items"', () => {
|
|
98
|
-
mock = schmock({ state: {} });
|
|
99
|
-
mock("GET /items", [{ id: 1 }], {});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
And("I register and remove a listener", () => {
|
|
103
|
-
removedFired = false;
|
|
104
|
-
const listener = () => {
|
|
105
|
-
removedFired = true;
|
|
106
|
-
};
|
|
107
|
-
mock.on("request:start", listener);
|
|
108
|
-
mock.off("request:start", listener);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
When('I request "GET /items"', async () => {
|
|
112
|
-
await mock.handle("GET", "/items");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
Then("the removed listener did not fire", () => {
|
|
116
|
-
expect(removedFired).toBe(false);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
Scenario("Reset clears all listeners", ({ Given, And, When, Then }) => {
|
|
121
|
-
Given('a mock with a route "GET /items"', () => {
|
|
122
|
-
mock = schmock({ state: {} });
|
|
123
|
-
mock("GET /items", [{ id: 1 }], {});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
And("I register listeners for all events", () => {
|
|
127
|
-
events = [];
|
|
128
|
-
mock.on("request:start", collectEvent("request:start"));
|
|
129
|
-
mock.on("request:end", collectEvent("request:end"));
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
When("I reset the mock", () => {
|
|
133
|
-
mock.reset();
|
|
134
|
-
events = [];
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
And('I add a route "GET /items" again', () => {
|
|
138
|
-
mock("GET /items", [{ id: 1 }], {});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
And('I request "GET /items" after reset', async () => {
|
|
142
|
-
await mock.handle("GET", "/items");
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
Then("no events were collected after reset", () => {
|
|
146
|
-
expect(events).toHaveLength(0);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
});
|
|
@@ -1,90 +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(
|
|
6
|
-
"../../features/performance-reliability.feature",
|
|
7
|
-
);
|
|
8
|
-
|
|
9
|
-
function withTimeout(
|
|
10
|
-
promise: Promise<void>,
|
|
11
|
-
timeoutMs: number,
|
|
12
|
-
message: string,
|
|
13
|
-
): Promise<void> {
|
|
14
|
-
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
15
|
-
const timeout = new Promise<never>((_, reject) => {
|
|
16
|
-
timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
return Promise.race([promise, timeout]).finally(() => {
|
|
20
|
-
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describeFeature(feature, ({ Scenario }) => {
|
|
25
|
-
let mock: Schmock.CallableMockInstance;
|
|
26
|
-
let releaseBarrier: (() => void) | undefined;
|
|
27
|
-
let boundedBarrier: Promise<void> | undefined;
|
|
28
|
-
let arrivals = 0;
|
|
29
|
-
let responses: Schmock.Response[] = [];
|
|
30
|
-
let expectedIds: string[] = [];
|
|
31
|
-
|
|
32
|
-
Scenario(
|
|
33
|
-
"Concurrent request contexts remain isolated",
|
|
34
|
-
({ Given, When, Then, And }) => {
|
|
35
|
-
Given(
|
|
36
|
-
"I create an asynchronous mock that waits until every request reaches a shared rendezvous",
|
|
37
|
-
() => {
|
|
38
|
-
mock = schmock();
|
|
39
|
-
arrivals = 0;
|
|
40
|
-
mock("GET /items/:id", async ({ params }) => {
|
|
41
|
-
arrivals += 1;
|
|
42
|
-
if (arrivals === expectedIds.length) releaseBarrier?.();
|
|
43
|
-
if (!boundedBarrier) {
|
|
44
|
-
throw new Error("Concurrent rendezvous was not initialized");
|
|
45
|
-
}
|
|
46
|
-
await boundedBarrier;
|
|
47
|
-
return { id: params.id };
|
|
48
|
-
});
|
|
49
|
-
},
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
When(
|
|
53
|
-
"I issue {int} concurrent requests with distinct route IDs",
|
|
54
|
-
async (_scenario, count: number) => {
|
|
55
|
-
expectedIds = Array.from({ length: count }, (_, index) =>
|
|
56
|
-
String(index),
|
|
57
|
-
);
|
|
58
|
-
const barrier = new Promise<void>((resolve) => {
|
|
59
|
-
releaseBarrier = resolve;
|
|
60
|
-
});
|
|
61
|
-
boundedBarrier = withTimeout(
|
|
62
|
-
barrier,
|
|
63
|
-
2_000,
|
|
64
|
-
`Timed out waiting for ${count} requests to reach the shared rendezvous`,
|
|
65
|
-
);
|
|
66
|
-
const pending = expectedIds.map((id) =>
|
|
67
|
-
mock.handle("GET", `/items/${id}`),
|
|
68
|
-
);
|
|
69
|
-
responses = await Promise.all(pending);
|
|
70
|
-
},
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
Then("every response should contain its corresponding route ID", () => {
|
|
74
|
-
expect(arrivals).toBe(expectedIds.length);
|
|
75
|
-
expect(responses).toHaveLength(expectedIds.length);
|
|
76
|
-
expect(responses.map((response) => response.body)).toEqual(
|
|
77
|
-
expectedIds.map((id) => ({ id })),
|
|
78
|
-
);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
And("the history should contain each route ID exactly once", () => {
|
|
82
|
-
const retainedIds = mock
|
|
83
|
-
.history()
|
|
84
|
-
.map((record) => record.params.id)
|
|
85
|
-
.sort();
|
|
86
|
-
expect(retainedIds).toEqual([...expectedIds].sort());
|
|
87
|
-
});
|
|
88
|
-
},
|
|
89
|
-
);
|
|
90
|
-
});
|