@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.
- 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
|
@@ -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:
|
|
41
|
+
let response: Schmock.Response;
|
|
11
42
|
|
|
12
43
|
Scenario("Route not found returns 404", ({ Given, When, Then, And }) => {
|
|
13
|
-
Given(
|
|
14
|
-
mock
|
|
15
|
-
|
|
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
|
|
20
|
-
response = await mock.handle(method
|
|
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(
|
|
28
|
-
|
|
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(
|
|
32
|
-
|
|
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(
|
|
38
|
-
mock
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
68
|
-
response = await mock.handle(method
|
|
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(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
167
|
+
Then("I should receive status {int}", (_, status: number) => {
|
|
168
|
+
expect(response.status).toBe(status);
|
|
169
|
+
});
|
|
127
170
|
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
197
|
+
Then("I should receive status {int}", (_, status: number) => {
|
|
198
|
+
expect(response.status).toBe(status);
|
|
199
|
+
});
|
|
143
200
|
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
228
|
+
Then("I should receive status {int}", (_, status: number) => {
|
|
229
|
+
expect(response.status).toBe(status);
|
|
230
|
+
});
|
|
151
231
|
|
|
152
|
-
|
|
153
|
-
|
|
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(
|
|
159
|
-
mock
|
|
160
|
-
|
|
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
|
|
165
|
-
response = await mock.handle(method
|
|
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(
|
|
173
|
-
|
|
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(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
302
|
+
Then("I should receive status {int}", (_, status: number) => {
|
|
303
|
+
expect(response.status).toBe(status);
|
|
304
|
+
});
|
|
199
305
|
|
|
200
|
-
|
|
201
|
-
|
|
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(
|
|
207
|
-
mock
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
218
|
-
response = await mock.handle(method
|
|
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(
|
|
226
|
-
|
|
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(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
413
|
+
Then("I should receive status {int}", (_, status: number) => {
|
|
414
|
+
expect(response.status).toBe(status);
|
|
415
|
+
});
|
|
267
416
|
|
|
268
|
-
|
|
269
|
-
|
|
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("
|
|
274
|
-
Given(
|
|
275
|
-
mock
|
|
276
|
-
|
|
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
|
|
281
|
-
response = await mock.handle(method
|
|
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(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
});
|
|
473
|
+
Then("I should receive status {int}", (_, status: number) => {
|
|
474
|
+
expect(response.status).toBe(status);
|
|
475
|
+
});
|
|
312
476
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
477
|
+
And("the content-type should be {string}", (_, contentType: string) => {
|
|
478
|
+
expect(response.headers["content-type"]).toBe(contentType);
|
|
479
|
+
});
|
|
316
480
|
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
338
|
-
|
|
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
|
-
|
|
342
|
-
|
|
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
|
});
|