@schmock/core 1.13.0 → 2.0.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.
- package/dist/builder.d.ts +2 -0
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +13 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +12 -0
- package/dist/helpers.d.ts +9 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +37 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/interceptor.d.ts +5 -0
- package/dist/interceptor.d.ts.map +1 -0
- package/dist/interceptor.js +213 -0
- package/dist/plugin-pipeline.js +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/builder.ts +23 -0
- package/src/constants.test.ts +40 -0
- package/src/constants.ts +18 -0
- package/src/helpers.test.ts +147 -0
- package/src/helpers.ts +58 -0
- package/src/index.ts +21 -0
- package/src/interceptor.test.ts +291 -0
- package/src/interceptor.ts +272 -0
- package/src/parser.property.test.ts +101 -0
- package/src/plugin-pipeline.ts +1 -1
- package/src/response-parsing.test.ts +74 -0
- package/src/server.test.ts +49 -0
- package/src/steps/async-support.steps.ts +0 -35
- package/src/steps/basic-usage.steps.ts +0 -84
- package/src/steps/developer-experience.steps.ts +0 -269
- package/src/steps/error-handling.steps.ts +0 -66
- package/src/steps/http-methods.steps.ts +0 -66
- package/src/steps/interceptor.steps.ts +206 -0
- package/src/steps/request-history.steps.ts +0 -75
- package/src/steps/route-key-format.steps.ts +0 -19
- package/src/types.ts +5 -0
|
@@ -10,134 +10,6 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
10
10
|
let response: any;
|
|
11
11
|
let responses: any[] = [];
|
|
12
12
|
|
|
13
|
-
Scenario("Forgetting to provide response data", ({ Given, When, Then, And }) => {
|
|
14
|
-
Given("I create a mock with no response data on {string}", (_, route: string) => {
|
|
15
|
-
mock = schmock();
|
|
16
|
-
mock(route as Schmock.RouteKey, undefined);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
When("I request {string}", async (_, request: string) => {
|
|
20
|
-
const [method, path] = request.split(" ");
|
|
21
|
-
response = await mock.handle(method as any, path);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
Then("I should receive status {int}", (_, status: number) => {
|
|
25
|
-
expect(response.status).toBe(status);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
And("the response body should be empty", () => {
|
|
29
|
-
expect(response.body).toBeUndefined();
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
Scenario("Using wrong parameter name in route", ({ Given, When, Then }) => {
|
|
34
|
-
Given("I create a mock with a mismatched parameter name on {string}", (_, route: string) => {
|
|
35
|
-
mock = schmock();
|
|
36
|
-
mock(route as Schmock.RouteKey, ({ params }) => ({ id: params.id }));
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
When("I request {string}", async (_, request: string) => {
|
|
40
|
-
const [method, path] = request.split(" ");
|
|
41
|
-
response = await mock.handle(method as any, path);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
Then("the wrong parameter should be undefined", () => {
|
|
45
|
-
expect(response.body).toEqual({ id: undefined });
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
Scenario("Correct parameter usage", ({ Given, When, Then }) => {
|
|
50
|
-
Given("I create a mock with a matching parameter name on {string}", (_, route: string) => {
|
|
51
|
-
mock = schmock();
|
|
52
|
-
mock(route as Schmock.RouteKey, ({ params }) => ({ id: params.userId }));
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
When("I request {string}", async (_, request: string) => {
|
|
56
|
-
const [method, path] = request.split(" ");
|
|
57
|
-
response = await mock.handle(method as any, path);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
Then("I should receive:", (_, docString: string) => {
|
|
61
|
-
const expected = JSON.parse(docString);
|
|
62
|
-
expect(response.body).toEqual(expected);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
Scenario("Mixing content types without explicit configuration", ({ Given, When, Then, And }) => {
|
|
67
|
-
Given("I create a mock with JSON, text, number, and boolean routes", () => {
|
|
68
|
-
mock = schmock();
|
|
69
|
-
mock("GET /json", { data: "json" });
|
|
70
|
-
mock("GET /text", "plain text");
|
|
71
|
-
mock("GET /number", 42);
|
|
72
|
-
mock("GET /boolean", true);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
When("I test all mixed content type routes", async () => {
|
|
76
|
-
responses = [];
|
|
77
|
-
responses.push(await mock.handle("GET", "/json"));
|
|
78
|
-
responses.push(await mock.handle("GET", "/text"));
|
|
79
|
-
responses.push(await mock.handle("GET", "/number"));
|
|
80
|
-
responses.push(await mock.handle("GET", "/boolean"));
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
Then("JSON route should have content-type {string}", (_, contentType: string) => {
|
|
84
|
-
expect(responses[0].headers?.["content-type"]).toBe(contentType);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
And("text route should have content-type {string}", (_, contentType: string) => {
|
|
88
|
-
expect(responses[1].headers?.["content-type"]).toBe(contentType);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
And("number route should have content-type {string}", (_, contentType: string) => {
|
|
92
|
-
expect(responses[2].headers?.["content-type"]).toBe(contentType);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
And("boolean route should have content-type {string}", (_, contentType: string) => {
|
|
96
|
-
expect(responses[3].headers?.["content-type"]).toBe(contentType);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
Scenario("Expecting JSON but getting string conversion", ({ Given, When, Then, And }) => {
|
|
101
|
-
Given("I create a mock returning a decimal number on {string}", (_, route: string) => {
|
|
102
|
-
mock = schmock();
|
|
103
|
-
mock(route as Schmock.RouteKey, 19.99);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
When("I request {string}", async (_, request: string) => {
|
|
107
|
-
const [method, path] = request.split(" ");
|
|
108
|
-
response = await mock.handle(method as any, path);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
Then("I should receive text {string}", (_, expectedText: string) => {
|
|
112
|
-
expect(response.body).toBe(expectedText);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
And("the content-type should be {string}", (_, contentType: string) => {
|
|
116
|
-
expect(response.headers?.["content-type"]).toBe(contentType);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
Scenario("Forgetting await with async generators", ({ Given, When, Then, And }) => {
|
|
121
|
-
Given("I create a mock with an async handler on {string}", (_, route: string) => {
|
|
122
|
-
mock = schmock();
|
|
123
|
-
mock(route as Schmock.RouteKey, async () => ({ async: true }));
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
When("I request {string}", async (_, request: string) => {
|
|
127
|
-
const [method, path] = request.split(" ");
|
|
128
|
-
response = await mock.handle(method as any, path);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
Then("I should receive:", (_, docString: string) => {
|
|
132
|
-
const expected = JSON.parse(docString);
|
|
133
|
-
expect(response.body).toEqual(expected);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
And("the content-type should be {string}", (_, contentType: string) => {
|
|
137
|
-
expect(response.headers?.["content-type"]).toBe(contentType);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
13
|
Scenario("State confusion between global and local state", ({ Given, When, Then, And }) => {
|
|
142
14
|
Given("I create a mock with global state and a local state counter", () => {
|
|
143
15
|
mock = schmock({ state: { global: 1 } });
|
|
@@ -166,62 +38,6 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
166
38
|
});
|
|
167
39
|
});
|
|
168
40
|
|
|
169
|
-
Scenario("Query parameter edge cases", ({ Given, When, Then }) => {
|
|
170
|
-
Given("I create a mock that echoes query parameters on {string}", (_, route: string) => {
|
|
171
|
-
mock = schmock();
|
|
172
|
-
mock(route as Schmock.RouteKey, ({ query }) => ({
|
|
173
|
-
term: query.q,
|
|
174
|
-
page: query.page,
|
|
175
|
-
empty: query.empty,
|
|
176
|
-
}));
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
When("I request {string}", async (_, request: string) => {
|
|
180
|
-
const [method, fullPath] = request.split(" ");
|
|
181
|
-
const [path, queryString] = fullPath.split("?");
|
|
182
|
-
|
|
183
|
-
const query: Record<string, string> = {};
|
|
184
|
-
if (queryString) {
|
|
185
|
-
queryString.split("&").forEach((param) => {
|
|
186
|
-
const [key, value] = param.split("=");
|
|
187
|
-
query[key] = value || "";
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
response = await mock.handle(method as any, path, { query });
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
Then("I should receive:", (_, docString: string) => {
|
|
195
|
-
const expected = JSON.parse(docString);
|
|
196
|
-
expect(response.body).toEqual(expected);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
Scenario("Headers case sensitivity", ({ Given, When, Then }) => {
|
|
201
|
-
Given("I create a mock that echoes headers on {string}", (_, route: string) => {
|
|
202
|
-
mock = schmock();
|
|
203
|
-
mock(route as Schmock.RouteKey, ({ headers }) => ({
|
|
204
|
-
auth: headers.authorization,
|
|
205
|
-
authUpper: headers.Authorization,
|
|
206
|
-
contentType: headers["content-type"],
|
|
207
|
-
}));
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
When("I request {string} with headers:", async (_, request: string, docString: string) => {
|
|
211
|
-
const [method, path] = request.split(" ");
|
|
212
|
-
const headers = JSON.parse(docString);
|
|
213
|
-
response = await mock.handle(method as any, path, { headers });
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
Then("the header case sensitivity should show expected values", () => {
|
|
217
|
-
expect(response.body).toEqual({
|
|
218
|
-
auth: undefined,
|
|
219
|
-
authUpper: "Bearer token",
|
|
220
|
-
contentType: undefined,
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
41
|
Scenario("Route precedence with similar paths", ({ Given, When, Then, And }) => {
|
|
226
42
|
Given("I create a mock with an exact route and a parameterized route on {string}", (_, basePath: string) => {
|
|
227
43
|
mock = schmock();
|
|
@@ -277,28 +93,6 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
277
93
|
});
|
|
278
94
|
});
|
|
279
95
|
|
|
280
|
-
Scenario("Namespace confusion with absolute paths", ({ Given, When, Then, And }) => {
|
|
281
|
-
Given("I create a mock with namespace {string} and a users route", (_, namespace: string) => {
|
|
282
|
-
mock = schmock({ namespace });
|
|
283
|
-
mock("GET /users", []);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
When("I test both namespace scenarios", async () => {
|
|
287
|
-
responses = [];
|
|
288
|
-
responses.push(await mock.handle("GET", "/users"));
|
|
289
|
-
responses.push(await mock.handle("GET", "/api/v1/users"));
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
Then("the wrong namespace should receive status {int}", (_, status: number) => {
|
|
293
|
-
expect(responses[0].status).toBe(status);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
And("the correct namespace should receive:", (_, docString: string) => {
|
|
297
|
-
const expected = JSON.parse(docString);
|
|
298
|
-
expect(responses[1].body).toEqual(expected);
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
96
|
Scenario("Plugin expecting different context structure", ({ Given, When, Then }) => {
|
|
303
97
|
Given("I create a mock with a plugin that reads context properties", () => {
|
|
304
98
|
mock = schmock();
|
|
@@ -371,46 +165,6 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
371
165
|
});
|
|
372
166
|
});
|
|
373
167
|
|
|
374
|
-
Scenario("Common typos in method names", ({ Given, When, Then, And }) => {
|
|
375
|
-
let errors: Error[] = [];
|
|
376
|
-
|
|
377
|
-
Given("I create an empty mock for testing method typos", () => {
|
|
378
|
-
mock = schmock();
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
When("I test all common method typos", () => {
|
|
382
|
-
errors = [];
|
|
383
|
-
const typos = ["GETS /users", "post /users", "GET/users"];
|
|
384
|
-
|
|
385
|
-
for (const typo of typos) {
|
|
386
|
-
try {
|
|
387
|
-
mock(typo as Schmock.RouteKey, "test");
|
|
388
|
-
errors.push(new Error("No error thrown"));
|
|
389
|
-
} catch (e) {
|
|
390
|
-
errors.push(e as Error);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
Then("the wrong method typo should throw RouteParseError", () => {
|
|
396
|
-
expect(errors[0]).not.toBeNull();
|
|
397
|
-
expect(errors[0].constructor.name).toBe("RouteParseError");
|
|
398
|
-
expect(errors[0].message).toContain("Invalid route key format");
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
And("the lowercase method typo should throw RouteParseError", () => {
|
|
402
|
-
expect(errors[1]).not.toBeNull();
|
|
403
|
-
expect(errors[1].constructor.name).toBe("RouteParseError");
|
|
404
|
-
expect(errors[1].message).toContain("Invalid route key format");
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
And("the missing space typo should throw RouteParseError", () => {
|
|
408
|
-
expect(errors[2]).not.toBeNull();
|
|
409
|
-
expect(errors[2].constructor.name).toBe("RouteParseError");
|
|
410
|
-
expect(errors[2].message).toContain("Invalid route key format");
|
|
411
|
-
});
|
|
412
|
-
});
|
|
413
|
-
|
|
414
168
|
Scenario("Registering duplicate routes first route wins", ({ Given, When, Then }) => {
|
|
415
169
|
Given("I create a mock with two routes on {string} with different data", (_, route: string) => {
|
|
416
170
|
mock = schmock();
|
|
@@ -429,27 +183,4 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
429
183
|
});
|
|
430
184
|
});
|
|
431
185
|
|
|
432
|
-
Scenario("Plugin returning unexpected structure", ({ Given, When, Then, And }) => {
|
|
433
|
-
Given("I create a mock with a plugin that returns an invalid structure", () => {
|
|
434
|
-
mock = schmock();
|
|
435
|
-
const badPlugin = {
|
|
436
|
-
name: "bad-structure",
|
|
437
|
-
process: () => ({ wrong: "structure" }),
|
|
438
|
-
};
|
|
439
|
-
mock("GET /bad", "original").pipe(badPlugin as any);
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
When("I request {string}", async (_, request: string) => {
|
|
443
|
-
const [method, path] = request.split(" ");
|
|
444
|
-
response = await mock.handle(method as any, path);
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
Then("I should receive status {int}", (_, status: number) => {
|
|
448
|
-
expect(response.status).toBe(status);
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
And("the response should contain error {string}", (_, errorMessage: string) => {
|
|
452
|
-
expect(response.body.error).toContain(errorMessage);
|
|
453
|
-
});
|
|
454
|
-
});
|
|
455
186
|
});
|
|
@@ -8,7 +8,6 @@ const feature = await loadFeature("../../features/error-handling.feature");
|
|
|
8
8
|
describeFeature(feature, ({ Scenario }) => {
|
|
9
9
|
let mock: CallableMockInstance;
|
|
10
10
|
let response: any;
|
|
11
|
-
let error: Error | null = null;
|
|
12
11
|
|
|
13
12
|
Scenario("Route not found returns 404", ({ Given, When, Then, And }) => {
|
|
14
13
|
Given("I create a mock with a GET /users route returning a user list", () => {
|
|
@@ -54,48 +53,6 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
54
53
|
});
|
|
55
54
|
});
|
|
56
55
|
|
|
57
|
-
Scenario("Invalid route key throws RouteDefinitionError", ({ Given, Then, And }) => {
|
|
58
|
-
Given("I attempt to register a route with an invalid HTTP method", () => {
|
|
59
|
-
error = null;
|
|
60
|
-
try {
|
|
61
|
-
mock = schmock();
|
|
62
|
-
mock("INVALID_METHOD /path" as any, "response");
|
|
63
|
-
} catch (e) {
|
|
64
|
-
error = e as Error;
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
Then("it should throw a RouteDefinitionError", () => {
|
|
69
|
-
expect(error).not.toBeNull();
|
|
70
|
-
expect(error!.constructor.name).toBe("RouteParseError");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
And("the error message should contain {string}", (_, message: string) => {
|
|
74
|
-
expect(error!.message).toContain("Invalid route key format");
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
Scenario("Empty route path throws RouteDefinitionError", ({ Given, Then, And }) => {
|
|
79
|
-
Given("I attempt to register a route with an empty path", () => {
|
|
80
|
-
error = null;
|
|
81
|
-
try {
|
|
82
|
-
mock = schmock();
|
|
83
|
-
mock("GET " as any, "response");
|
|
84
|
-
} catch (e) {
|
|
85
|
-
error = e as Error;
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
Then("it should throw a RouteDefinitionError", () => {
|
|
90
|
-
expect(error).not.toBeNull();
|
|
91
|
-
expect(error!.constructor.name).toBe("RouteParseError");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
And("the error message should contain {string}", (_, message: string) => {
|
|
95
|
-
expect(error!.message).toContain("Invalid route key format");
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
56
|
Scenario("Plugin throws error returns 500 with PluginError", ({ Given, When, Then, And }) => {
|
|
100
57
|
Given("I create a mock with a plugin that throws {string}", (_, errorMsg: string) => {
|
|
101
58
|
mock = schmock();
|
|
@@ -197,29 +154,6 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
197
154
|
});
|
|
198
155
|
});
|
|
199
156
|
|
|
200
|
-
Scenario("Invalid JSON generator with JSON content-type throws RouteDefinitionError", ({ Given, Then, And }) => {
|
|
201
|
-
Given("I attempt to register a route with a circular reference as JSON", () => {
|
|
202
|
-
error = null;
|
|
203
|
-
try {
|
|
204
|
-
mock = schmock();
|
|
205
|
-
const circularRef: Record<string, unknown> = {};
|
|
206
|
-
circularRef.self = circularRef;
|
|
207
|
-
mock("GET /invalid", circularRef, { contentType: "application/json" });
|
|
208
|
-
} catch (e) {
|
|
209
|
-
error = e as Error;
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
Then("it should throw a RouteDefinitionError", () => {
|
|
214
|
-
expect(error).not.toBeNull();
|
|
215
|
-
expect(error!.constructor.name).toBe("RouteDefinitionError");
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
And("the error message should contain {string}", (_, message: string) => {
|
|
219
|
-
expect(error!.message).toContain(message);
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
157
|
Scenario("Namespace mismatch returns 404", ({ Given, When, Then, And }) => {
|
|
224
158
|
Given("I create a mock with namespace {string} and a GET /users route", (_, namespace: string) => {
|
|
225
159
|
mock = schmock({ namespace });
|
|
@@ -9,7 +9,6 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
9
9
|
let mock: CallableMockInstance;
|
|
10
10
|
let response: any;
|
|
11
11
|
let responses: any[] = [];
|
|
12
|
-
let error: Error | null = null;
|
|
13
12
|
|
|
14
13
|
Scenario("GET method with query parameters", ({ Given, When, Then }) => {
|
|
15
14
|
Given("I create a mock with a GET search endpoint", () => {
|
|
@@ -255,71 +254,6 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
255
254
|
});
|
|
256
255
|
});
|
|
257
256
|
|
|
258
|
-
Scenario("Method case sensitivity", ({ Given, When, Then }) => {
|
|
259
|
-
error = null;
|
|
260
|
-
|
|
261
|
-
Given("I create an empty mock for case sensitivity testing", () => {
|
|
262
|
-
mock = schmock();
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
When("I attempt to create a mock with lowercase method", () => {
|
|
266
|
-
error = null;
|
|
267
|
-
try {
|
|
268
|
-
mock('get /test' as Schmock.RouteKey, { method: 'get' });
|
|
269
|
-
} catch (e) {
|
|
270
|
-
error = e as Error;
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
Then("it should throw RouteParseError for invalid method case", () => {
|
|
275
|
-
expect(error).not.toBeNull();
|
|
276
|
-
expect(error!.constructor.name).toBe('RouteParseError');
|
|
277
|
-
expect(error!.message).toContain('Invalid route key format');
|
|
278
|
-
});
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
Scenario("Unsupported HTTP methods", ({ Given, When, Then }) => {
|
|
282
|
-
error = null;
|
|
283
|
-
|
|
284
|
-
Given("I create an empty mock for unsupported method testing", () => {
|
|
285
|
-
mock = schmock();
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
When("I attempt to create a mock with unsupported method", () => {
|
|
289
|
-
error = null;
|
|
290
|
-
try {
|
|
291
|
-
mock('CUSTOM /endpoint' as Schmock.RouteKey, { custom: true });
|
|
292
|
-
} catch (e) {
|
|
293
|
-
error = e as Error;
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
Then("it should throw RouteParseError for unsupported method", () => {
|
|
298
|
-
expect(error).not.toBeNull();
|
|
299
|
-
expect(error!.constructor.name).toBe('RouteParseError');
|
|
300
|
-
expect(error!.message).toContain('Invalid route key format');
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
Scenario("Method with special characters in path", ({ Given, When, Then }) => {
|
|
305
|
-
Given("I create a mock with nested parameterized path segments", () => {
|
|
306
|
-
mock = schmock();
|
|
307
|
-
mock('GET /api/v1/users/:id/posts/:post-id', ({ params }) => ({
|
|
308
|
-
userId: params.id,
|
|
309
|
-
postId: params['post-id'],
|
|
310
|
-
}));
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
When("I make a GET request to {string}", async (_, path: string) => {
|
|
314
|
-
response = await mock.handle('GET', path);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
Then("I should receive special characters response:", (_, docString: string) => {
|
|
318
|
-
const expected = JSON.parse(docString);
|
|
319
|
-
expect(response.body).toEqual(expected);
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
|
|
323
257
|
Scenario("Method with request headers validation", ({ Given, When, Then, And }) => {
|
|
324
258
|
Given("I create a mock with authorization header checking", () => {
|
|
325
259
|
mock = schmock();
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/// <reference path="../../schmock.d.ts" />
|
|
2
|
+
|
|
3
|
+
import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
|
|
4
|
+
import { expect, 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: ReturnType<typeof vi.fn>;
|
|
14
|
+
let fetchResponse: Response | undefined;
|
|
15
|
+
|
|
16
|
+
function setup() {
|
|
17
|
+
originalFetch = globalThis.fetch;
|
|
18
|
+
savedFetch = vi.fn().mockResolvedValue(new Response("real backend"));
|
|
19
|
+
globalThis.fetch = savedFetch;
|
|
20
|
+
mock = schmock();
|
|
21
|
+
fetchResponse = undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
AfterEachScenario(() => {
|
|
25
|
+
handle?.restore();
|
|
26
|
+
handle = undefined;
|
|
27
|
+
globalThis.fetch = originalFetch;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
Scenario("Intercept a matched fetch request", ({ Given, When, Then, And }) => {
|
|
31
|
+
Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
|
|
32
|
+
setup();
|
|
33
|
+
mock("GET /api/users", [{ id: 1, name: "Alice" }]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
And("fetch is intercepted", () => {
|
|
37
|
+
handle = mock.intercept();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
When("I fetch \"/api/users\"", async () => {
|
|
41
|
+
fetchResponse = await fetch("http://localhost/api/users");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
Then("the fetch response status should be 200", () => {
|
|
45
|
+
expect(fetchResponse?.status).toBe(200);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
And("the fetch response body should be the mocked users", async () => {
|
|
49
|
+
const body = await fetchResponse?.json();
|
|
50
|
+
expect(body).toEqual([{ id: 1, name: "Alice" }]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
Scenario("Passthrough for unmatched routes", ({ Given, When, Then, And }) => {
|
|
55
|
+
Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
|
|
56
|
+
setup();
|
|
57
|
+
mock("GET /api/users", [{ id: 1 }]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
And("fetch is intercepted with passthrough enabled", () => {
|
|
61
|
+
handle = mock.intercept({ passthrough: true });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
When("I fetch \"/api/other\"", async () => {
|
|
65
|
+
fetchResponse = await fetch("http://localhost/api/other");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
Then("the original fetch should have been called", () => {
|
|
69
|
+
expect(savedFetch).toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
Scenario("Passthrough disabled returns 404", ({ Given, When, Then, And }) => {
|
|
74
|
+
Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
|
|
75
|
+
setup();
|
|
76
|
+
mock("GET /api/users", [{ id: 1 }]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
And("fetch is intercepted with passthrough disabled", () => {
|
|
80
|
+
handle = mock.intercept({ passthrough: false });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
When("I fetch \"/api/other\"", async () => {
|
|
84
|
+
fetchResponse = await fetch("http://localhost/api/other");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
Then("the fetch response status should be 404", () => {
|
|
88
|
+
expect(fetchResponse?.status).toBe(404);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
Scenario("Restore puts original fetch back", ({ Given, When, Then, And }) => {
|
|
93
|
+
let savedRef: typeof globalThis.fetch;
|
|
94
|
+
|
|
95
|
+
Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
|
|
96
|
+
setup();
|
|
97
|
+
mock("GET /api/users", [{ id: 1 }]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
And("fetch is intercepted", () => {
|
|
101
|
+
savedRef = globalThis.fetch;
|
|
102
|
+
handle = mock.intercept();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
When("I restore the interceptor", () => {
|
|
106
|
+
handle?.restore();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
Then("globalThis.fetch should be the original function", () => {
|
|
110
|
+
expect(globalThis.fetch).toBe(savedRef);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
Scenario("BaseUrl filters which requests are intercepted", ({ Given, When, Then, And }) => {
|
|
115
|
+
Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
|
|
116
|
+
setup();
|
|
117
|
+
mock("GET /api/users", [{ id: 1 }]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
And("fetch is intercepted with baseUrl \"/api\"", () => {
|
|
121
|
+
handle = mock.intercept({ baseUrl: "/api" });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
When("I fetch \"/other/endpoint\"", async () => {
|
|
125
|
+
await fetch("http://localhost/other/endpoint");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
Then("the original fetch should have been called", () => {
|
|
129
|
+
expect(savedFetch).toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
Scenario("beforeRequest hook modifies the request", ({ Given, When, Then, And }) => {
|
|
134
|
+
Given("a Schmock instance with route \"GET /api/users\" that reads headers", () => {
|
|
135
|
+
setup();
|
|
136
|
+
mock("GET /api/users", ({ headers }) => [200, { token: headers["x-token"] }]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
And("fetch is intercepted with a beforeRequest hook that adds a header", () => {
|
|
140
|
+
handle = mock.intercept({
|
|
141
|
+
beforeRequest: (req) => ({
|
|
142
|
+
...req,
|
|
143
|
+
headers: { ...req.headers, "x-token": "injected" },
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
When("I fetch \"/api/users\"", async () => {
|
|
149
|
+
fetchResponse = await fetch("http://localhost/api/users");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
Then("the response should contain the injected header value", async () => {
|
|
153
|
+
const body = await fetchResponse?.json();
|
|
154
|
+
expect(body).toEqual({ token: "injected" });
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
Scenario("beforeResponse hook modifies the response", ({ Given, When, Then, And }) => {
|
|
159
|
+
Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
|
|
160
|
+
setup();
|
|
161
|
+
mock("GET /api/users", [{ id: 1 }]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
And("fetch is intercepted with a beforeResponse hook that adds a header", () => {
|
|
165
|
+
handle = mock.intercept({
|
|
166
|
+
beforeResponse: (res) => ({
|
|
167
|
+
...res,
|
|
168
|
+
headers: { ...res.headers, "x-mock": "true" },
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
When("I fetch \"/api/users\"", async () => {
|
|
174
|
+
fetchResponse = await fetch("http://localhost/api/users");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
Then("the fetch response should have the injected header", () => {
|
|
178
|
+
expect(fetchResponse?.headers.get("x-mock")).toBe("true");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
Scenario("Double intercept throws an error", ({ Given, When, Then, And }) => {
|
|
183
|
+
let error: Error | undefined;
|
|
184
|
+
|
|
185
|
+
Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
|
|
186
|
+
setup();
|
|
187
|
+
mock("GET /api/users", [{ id: 1 }]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
And("fetch is intercepted", () => {
|
|
191
|
+
handle = mock.intercept();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
When("I try to intercept again", () => {
|
|
195
|
+
try {
|
|
196
|
+
mock.intercept();
|
|
197
|
+
} catch (e) {
|
|
198
|
+
error = e as Error;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
Then("it should throw an error about already intercepting", () => {
|
|
203
|
+
expect(error?.message).toMatch(/already intercepting/i);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|