@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,13 +1,115 @@
|
|
|
1
1
|
import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
|
|
2
2
|
import { expect } from "vitest";
|
|
3
|
+
import { schmock, toHttpMethod } from "../index";
|
|
3
4
|
import type { CallableMockInstance } from "../types";
|
|
4
|
-
import { schmock } from "../index";
|
|
5
5
|
|
|
6
6
|
const feature = await loadFeature("../../features/request-history.feature");
|
|
7
7
|
|
|
8
|
+
interface HistoryTableRow {
|
|
9
|
+
field: string;
|
|
10
|
+
value: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseRequest(request: string): {
|
|
14
|
+
method: Schmock.HttpMethod;
|
|
15
|
+
path: string;
|
|
16
|
+
} {
|
|
17
|
+
const separator = request.indexOf(" ");
|
|
18
|
+
const methodText = request.slice(0, separator);
|
|
19
|
+
const path = request.slice(separator + 1);
|
|
20
|
+
if (separator <= 0 || !path.startsWith("/")) {
|
|
21
|
+
throw new Error(`Expected request in METHOD /path format, got: ${request}`);
|
|
22
|
+
}
|
|
23
|
+
return { method: toHttpMethod(methodText), path };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function requireRecord(
|
|
27
|
+
value: unknown,
|
|
28
|
+
description: string,
|
|
29
|
+
): Record<string, unknown> {
|
|
30
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
31
|
+
throw new Error(`Expected ${description} to be an object`);
|
|
32
|
+
}
|
|
33
|
+
return Object.fromEntries(Object.entries(value));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function requireStringRecord(
|
|
37
|
+
value: unknown,
|
|
38
|
+
description: string,
|
|
39
|
+
): Record<string, string> {
|
|
40
|
+
const record = requireRecord(value, description);
|
|
41
|
+
const strings: Record<string, string> = {};
|
|
42
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
43
|
+
if (typeof entry !== "string") {
|
|
44
|
+
throw new Error(`Expected ${description}.${key} to be a string`);
|
|
45
|
+
}
|
|
46
|
+
strings[key] = entry;
|
|
47
|
+
}
|
|
48
|
+
return strings;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseJson(docString: string): unknown {
|
|
52
|
+
const parsed: unknown = JSON.parse(docString);
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseRequestOptions(docString: string): Schmock.RequestOptions {
|
|
57
|
+
const parsed = requireRecord(parseJson(docString), "request options");
|
|
58
|
+
const options: Schmock.RequestOptions = {};
|
|
59
|
+
if (parsed.headers !== undefined) {
|
|
60
|
+
options.headers = requireStringRecord(parsed.headers, "request headers");
|
|
61
|
+
}
|
|
62
|
+
if (parsed.query !== undefined) {
|
|
63
|
+
options.query = requireStringRecord(parsed.query, "request query");
|
|
64
|
+
}
|
|
65
|
+
if ("body" in parsed) {
|
|
66
|
+
options.body = parsed.body;
|
|
67
|
+
}
|
|
68
|
+
return options;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function requireHistoryTable(table: unknown): HistoryTableRow[] {
|
|
72
|
+
if (!Array.isArray(table)) {
|
|
73
|
+
throw new Error("Expected request history table rows");
|
|
74
|
+
}
|
|
75
|
+
return table.map((candidate) => {
|
|
76
|
+
const row = requireRecord(candidate, "request history table row");
|
|
77
|
+
if (typeof row.field !== "string" || typeof row.value !== "string") {
|
|
78
|
+
throw new Error("Expected request history table field and value strings");
|
|
79
|
+
}
|
|
80
|
+
return { field: row.field, value: row.value };
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function requestRecordField(
|
|
85
|
+
record: Schmock.RequestRecord,
|
|
86
|
+
field: string,
|
|
87
|
+
): unknown {
|
|
88
|
+
switch (field) {
|
|
89
|
+
case "method":
|
|
90
|
+
return record.method;
|
|
91
|
+
case "path":
|
|
92
|
+
return record.path;
|
|
93
|
+
case "params":
|
|
94
|
+
return record.params;
|
|
95
|
+
case "query":
|
|
96
|
+
return record.query;
|
|
97
|
+
case "headers":
|
|
98
|
+
return record.headers;
|
|
99
|
+
case "body":
|
|
100
|
+
return record.body;
|
|
101
|
+
case "timestamp":
|
|
102
|
+
return record.timestamp;
|
|
103
|
+
case "response":
|
|
104
|
+
return record.response;
|
|
105
|
+
default:
|
|
106
|
+
throw new Error(`Unknown request record field: ${field}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
8
110
|
describeFeature(feature, ({ Scenario }) => {
|
|
9
111
|
let mock: CallableMockInstance;
|
|
10
|
-
let response:
|
|
112
|
+
let response: Schmock.Response;
|
|
11
113
|
|
|
12
114
|
Scenario("Record multiple requests", ({ Given, When, And, Then }) => {
|
|
13
115
|
Given("I create a mock with GET and POST user routes", () => {
|
|
@@ -17,22 +119,22 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
17
119
|
});
|
|
18
120
|
|
|
19
121
|
When("I request {string}", async (_, request: string) => {
|
|
20
|
-
const
|
|
21
|
-
response = await mock.handle(method
|
|
122
|
+
const { method, path } = parseRequest(request);
|
|
123
|
+
response = await mock.handle(method, path);
|
|
22
124
|
});
|
|
23
125
|
|
|
24
126
|
And(
|
|
25
127
|
"I request {string} with body:",
|
|
26
128
|
async (_, request: string, docString: string) => {
|
|
27
|
-
const
|
|
28
|
-
const body =
|
|
29
|
-
response = await mock.handle(method
|
|
129
|
+
const { method, path } = parseRequest(request);
|
|
130
|
+
const body = parseJson(docString);
|
|
131
|
+
response = await mock.handle(method, path, { body });
|
|
30
132
|
},
|
|
31
133
|
);
|
|
32
134
|
|
|
33
135
|
And("I request {string}", async (_, request: string) => {
|
|
34
|
-
const
|
|
35
|
-
response = await mock.handle(method
|
|
136
|
+
const { method, path } = parseRequest(request);
|
|
137
|
+
response = await mock.handle(method, path);
|
|
36
138
|
});
|
|
37
139
|
|
|
38
140
|
Then("the call count should be 3", () => {
|
|
@@ -42,235 +144,315 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
42
144
|
And(
|
|
43
145
|
"the call count for {string} should be {int}",
|
|
44
146
|
(_, route: string, count: number) => {
|
|
45
|
-
const
|
|
46
|
-
expect(mock.callCount(method
|
|
147
|
+
const { method, path } = parseRequest(route);
|
|
148
|
+
expect(mock.callCount(method, path)).toBe(count);
|
|
47
149
|
},
|
|
48
150
|
);
|
|
49
151
|
|
|
50
152
|
And(
|
|
51
153
|
"the call count for {string} should be {int} request",
|
|
52
154
|
(_, route: string, count: number) => {
|
|
53
|
-
const
|
|
54
|
-
expect(mock.callCount(method
|
|
55
|
-
},
|
|
56
|
-
);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
Scenario("Filter history by method and path", ({ Given, When, And, Then }) => {
|
|
60
|
-
Given("I create a mock with users and posts routes", () => {
|
|
61
|
-
mock = schmock();
|
|
62
|
-
mock("GET /users", []);
|
|
63
|
-
mock("POST /users", ({ body }) => [201, body]);
|
|
64
|
-
mock("GET /posts", []);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
When("I request {string}", async (_, request: string) => {
|
|
68
|
-
const [method, path] = request.split(" ");
|
|
69
|
-
response = await mock.handle(method as any, path);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
And(
|
|
73
|
-
"I request {string} with body:",
|
|
74
|
-
async (_, request: string, docString: string) => {
|
|
75
|
-
const [method, path] = request.split(" ");
|
|
76
|
-
const body = JSON.parse(docString);
|
|
77
|
-
response = await mock.handle(method as any, path, { body });
|
|
78
|
-
},
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
And("I request {string}", async (_, request: string) => {
|
|
82
|
-
const [method, path] = request.split(" ");
|
|
83
|
-
response = await mock.handle(method as any, path);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
Then(
|
|
87
|
-
"the history for {string} should have {int} record",
|
|
88
|
-
(_, route: string, count: number) => {
|
|
89
|
-
const [method, path] = route.split(" ");
|
|
90
|
-
expect(mock.history(method as any, path)).toHaveLength(count);
|
|
91
|
-
},
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
And(
|
|
95
|
-
"the history for {string} should have {int} record",
|
|
96
|
-
(_, route: string, count: number) => {
|
|
97
|
-
const [method, path] = route.split(" ");
|
|
98
|
-
expect(mock.history(method as any, path)).toHaveLength(count);
|
|
99
|
-
},
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
And(
|
|
103
|
-
"the history for {string} should have {int} entry",
|
|
104
|
-
(_, route: string, count: number) => {
|
|
105
|
-
const [method, path] = route.split(" ");
|
|
106
|
-
expect(mock.history(method as any, path)).toHaveLength(count);
|
|
107
|
-
},
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
And(
|
|
111
|
-
"the history for {string} should have {int} entries",
|
|
112
|
-
(_, route: string, count: number) => {
|
|
113
|
-
const [method, path] = route.split(" ");
|
|
114
|
-
expect(mock.history(method as any, path)).toHaveLength(count);
|
|
115
|
-
},
|
|
116
|
-
);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
Scenario("Check if specific route was called", ({ Given, When, Then, And }) => {
|
|
120
|
-
Given("I create a mock with users and posts list routes", () => {
|
|
121
|
-
mock = schmock();
|
|
122
|
-
mock("GET /users", []);
|
|
123
|
-
mock("GET /posts", []);
|
|
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("{string} should have been called", (_, route: string) => {
|
|
132
|
-
const [method, path] = route.split(" ");
|
|
133
|
-
expect(mock.called(method as any, path)).toBe(true);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
And("{string} should not have been called", (_, route: string) => {
|
|
137
|
-
const [method, path] = route.split(" ");
|
|
138
|
-
expect(mock.called(method as any, path)).toBe(false);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
Scenario("Request record captures full details", ({ Given, When, Then, And }) => {
|
|
143
|
-
Given("I create a mock with a parameterized POST route", () => {
|
|
144
|
-
mock = schmock();
|
|
145
|
-
mock("POST /users/:id", ({ params, body }) => [
|
|
146
|
-
200,
|
|
147
|
-
{ ...(body as Record<string, unknown>), id: params.id },
|
|
148
|
-
]);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
When(
|
|
152
|
-
"I request {string} with headers and body:",
|
|
153
|
-
async (_, request: string, docString: string) => {
|
|
154
|
-
const [method, path] = request.split(" ");
|
|
155
|
-
const options = JSON.parse(docString);
|
|
156
|
-
response = await mock.handle(method as any, path, options);
|
|
157
|
-
},
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
Then("the last request should have:", (_, table: any) => {
|
|
161
|
-
const record = mock.lastRequest();
|
|
162
|
-
expect(record).toBeDefined();
|
|
163
|
-
for (const row of table) {
|
|
164
|
-
const field = row.field as keyof typeof record;
|
|
165
|
-
expect(record![field]).toBe(row.value);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
And(
|
|
170
|
-
"the last request params should include {string} = {string}",
|
|
171
|
-
(_, key: string, value: string) => {
|
|
172
|
-
expect(mock.lastRequest()?.params[key]).toBe(value);
|
|
173
|
-
},
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
And(
|
|
177
|
-
"the last request headers should include {string} = {string}",
|
|
178
|
-
(_, key: string, value: string) => {
|
|
179
|
-
expect(mock.lastRequest()?.headers[key]).toBe(value);
|
|
180
|
-
},
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
And(
|
|
184
|
-
"the last request body should have property {string} with value {string}",
|
|
185
|
-
(_, prop: string, value: string) => {
|
|
186
|
-
expect((mock.lastRequest()?.body as any)?.[prop]).toBe(value);
|
|
187
|
-
},
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
And("the last request should have a timestamp", () => {
|
|
191
|
-
const record = mock.lastRequest();
|
|
192
|
-
expect(record?.timestamp).toBeDefined();
|
|
193
|
-
expect(typeof record?.timestamp).toBe("number");
|
|
194
|
-
expect(record!.timestamp).toBeGreaterThan(0);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
And(
|
|
198
|
-
"the last request response status should be {int}",
|
|
199
|
-
(_, status: number) => {
|
|
200
|
-
expect(mock.lastRequest()?.response.status).toBe(status);
|
|
201
|
-
},
|
|
202
|
-
);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
Scenario("Get last request for a specific route", ({ Given, When, And, Then }) => {
|
|
206
|
-
Given("I create a mock echoing POST body at {string}", (_, path: string) => {
|
|
207
|
-
mock = schmock();
|
|
208
|
-
mock(`POST ${path}`, ({ body }) => [201, body]);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
When(
|
|
212
|
-
"I request {string} with body:",
|
|
213
|
-
async (_, request: string, docString: string) => {
|
|
214
|
-
const [method, path] = request.split(" ");
|
|
215
|
-
const body = JSON.parse(docString);
|
|
216
|
-
response = await mock.handle(method as any, path, { body });
|
|
217
|
-
},
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
And(
|
|
221
|
-
"I request {string} with body:",
|
|
222
|
-
async (_, request: string, docString: string) => {
|
|
223
|
-
const [method, path] = request.split(" ");
|
|
224
|
-
const body = JSON.parse(docString);
|
|
225
|
-
response = await mock.handle(method as any, path, { body });
|
|
226
|
-
},
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
Then(
|
|
230
|
-
"the last request for {string} body should have property {string} with value {string}",
|
|
231
|
-
(_, route: string, prop: string, value: string) => {
|
|
232
|
-
const [method, path] = route.split(" ");
|
|
233
|
-
const record = mock.lastRequest(method as any, path);
|
|
234
|
-
expect((record?.body as any)?.[prop]).toBe(value);
|
|
155
|
+
const { method, path } = parseRequest(route);
|
|
156
|
+
expect(mock.callCount(method, path)).toBe(count);
|
|
235
157
|
},
|
|
236
158
|
);
|
|
237
159
|
});
|
|
238
160
|
|
|
239
|
-
Scenario(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
161
|
+
Scenario(
|
|
162
|
+
"Filter history by method and path",
|
|
163
|
+
({ Given, When, And, Then }) => {
|
|
164
|
+
Given("I create a mock with users and posts routes", () => {
|
|
165
|
+
mock = schmock();
|
|
166
|
+
mock("GET /users", []);
|
|
167
|
+
mock("POST /users", ({ body }) => [201, body]);
|
|
168
|
+
mock("GET /posts", []);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
When("I request {string}", async (_, request: string) => {
|
|
172
|
+
const { method, path } = parseRequest(request);
|
|
173
|
+
response = await mock.handle(method, path);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
And(
|
|
177
|
+
"I request {string} with body:",
|
|
178
|
+
async (_, request: string, docString: string) => {
|
|
179
|
+
const { method, path } = parseRequest(request);
|
|
180
|
+
const body = parseJson(docString);
|
|
181
|
+
response = await mock.handle(method, path, { body });
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
And("I request {string}", async (_, request: string) => {
|
|
186
|
+
const { method, path } = parseRequest(request);
|
|
187
|
+
response = await mock.handle(method, path);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
Then(
|
|
191
|
+
"the history for {string} should have {int} record",
|
|
192
|
+
(_, route: string, count: number) => {
|
|
193
|
+
const { method, path } = parseRequest(route);
|
|
194
|
+
expect(mock.history(method, path)).toHaveLength(count);
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
And(
|
|
199
|
+
"the history for {string} should have {int} record",
|
|
200
|
+
(_, route: string, count: number) => {
|
|
201
|
+
const { method, path } = parseRequest(route);
|
|
202
|
+
expect(mock.history(method, path)).toHaveLength(count);
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
And(
|
|
207
|
+
"the history for {string} should have {int} entry",
|
|
208
|
+
(_, route: string, count: number) => {
|
|
209
|
+
const { method, path } = parseRequest(route);
|
|
210
|
+
expect(mock.history(method, path)).toHaveLength(count);
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
And(
|
|
215
|
+
"the history for {string} should have {int} entries",
|
|
216
|
+
(_, route: string, count: number) => {
|
|
217
|
+
const { method, path } = parseRequest(route);
|
|
218
|
+
expect(mock.history(method, path)).toHaveLength(count);
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
Scenario(
|
|
225
|
+
"Check if specific route was called",
|
|
226
|
+
({ Given, When, Then, And }) => {
|
|
227
|
+
Given("I create a mock with users and posts list routes", () => {
|
|
228
|
+
mock = schmock();
|
|
229
|
+
mock("GET /users", []);
|
|
230
|
+
mock("GET /posts", []);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
When("I request {string}", async (_, request: string) => {
|
|
234
|
+
const { method, path } = parseRequest(request);
|
|
235
|
+
response = await mock.handle(method, path);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
Then("{string} should have been called", (_, route: string) => {
|
|
239
|
+
const { method, path } = parseRequest(route);
|
|
240
|
+
expect(mock.called(method, path)).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
And("{string} should not have been called", (_, route: string) => {
|
|
244
|
+
const { method, path } = parseRequest(route);
|
|
245
|
+
expect(mock.called(method, path)).toBe(false);
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
Scenario(
|
|
251
|
+
"Request record captures full details",
|
|
252
|
+
({ Given, When, Then, And }) => {
|
|
253
|
+
Given("I create a mock with a parameterized POST route", () => {
|
|
254
|
+
mock = schmock();
|
|
255
|
+
mock("POST /users/:id", ({ params, body }) => {
|
|
256
|
+
const requestBody = requireRecord(body, "request body");
|
|
257
|
+
return [200, { ...requestBody, id: params.id }];
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
When(
|
|
262
|
+
"I request {string} with headers and body:",
|
|
263
|
+
async (_, request: string, docString: string) => {
|
|
264
|
+
const { method, path } = parseRequest(request);
|
|
265
|
+
const options = parseRequestOptions(docString);
|
|
266
|
+
response = await mock.handle(method, path, options);
|
|
267
|
+
},
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
Then("the last request should have:", (_, table: unknown) => {
|
|
271
|
+
const record = mock.lastRequest();
|
|
272
|
+
if (!record) {
|
|
273
|
+
throw new Error("Expected a recorded request");
|
|
274
|
+
}
|
|
275
|
+
for (const row of requireHistoryTable(table)) {
|
|
276
|
+
expect(requestRecordField(record, row.field)).toBe(row.value);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
And(
|
|
281
|
+
"the last request params should include {string} = {string}",
|
|
282
|
+
(_, key: string, value: string) => {
|
|
283
|
+
expect(mock.lastRequest()?.params[key]).toBe(value);
|
|
284
|
+
},
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
And(
|
|
288
|
+
"the last request headers should include {string} = {string}",
|
|
289
|
+
(_, key: string, value: string) => {
|
|
290
|
+
expect(mock.lastRequest()?.headers[key]).toBe(value);
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
And(
|
|
295
|
+
"the last request body should have property {string} with value {string}",
|
|
296
|
+
(_, prop: string, value: string) => {
|
|
297
|
+
const record = mock.lastRequest();
|
|
298
|
+
if (!record) {
|
|
299
|
+
throw new Error("Expected a recorded request");
|
|
300
|
+
}
|
|
301
|
+
expect(requireRecord(record.body, "request body")[prop]).toBe(value);
|
|
302
|
+
},
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
And("the last request should have a timestamp", () => {
|
|
306
|
+
const record = mock.lastRequest();
|
|
307
|
+
if (!record) {
|
|
308
|
+
throw new Error("Expected a recorded request");
|
|
309
|
+
}
|
|
310
|
+
expect(typeof record.timestamp).toBe("number");
|
|
311
|
+
expect(record.timestamp).toBeGreaterThan(0);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
And(
|
|
315
|
+
"the last request response status should be {int}",
|
|
316
|
+
(_, status: number) => {
|
|
317
|
+
expect(mock.lastRequest()?.response.status).toBe(status);
|
|
318
|
+
},
|
|
319
|
+
);
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
Scenario(
|
|
324
|
+
"Get last request for a specific route",
|
|
325
|
+
({ Given, When, And, Then }) => {
|
|
326
|
+
Given(
|
|
327
|
+
"I create a mock echoing POST body at {string}",
|
|
328
|
+
(_, path: string) => {
|
|
329
|
+
mock = schmock();
|
|
330
|
+
mock(`POST ${path}`, ({ body }) => [201, body]);
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
When(
|
|
335
|
+
"I request {string} with body:",
|
|
336
|
+
async (_, request: string, docString: string) => {
|
|
337
|
+
const { method, path } = parseRequest(request);
|
|
338
|
+
const body = parseJson(docString);
|
|
339
|
+
response = await mock.handle(method, path, { body });
|
|
340
|
+
},
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
And(
|
|
344
|
+
"I request {string} with body:",
|
|
345
|
+
async (_, request: string, docString: string) => {
|
|
346
|
+
const { method, path } = parseRequest(request);
|
|
347
|
+
const body = parseJson(docString);
|
|
348
|
+
response = await mock.handle(method, path, { body });
|
|
349
|
+
},
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
Then(
|
|
353
|
+
"the last request for {string} body should have property {string} with value {string}",
|
|
354
|
+
(_, route: string, prop: string, value: string) => {
|
|
355
|
+
const { method, path } = parseRequest(route);
|
|
356
|
+
const record = mock.lastRequest(method, path);
|
|
357
|
+
if (!record) {
|
|
358
|
+
throw new Error(`Expected a recorded request for ${route}`);
|
|
359
|
+
}
|
|
360
|
+
expect(requireRecord(record.body, "request body")[prop]).toBe(value);
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
},
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
Scenario(
|
|
367
|
+
"404 requests are not recorded in history",
|
|
368
|
+
({ Given, When, Then, And }) => {
|
|
369
|
+
Given("I create a mock with only a users route", () => {
|
|
370
|
+
mock = schmock();
|
|
371
|
+
mock("GET /users", []);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
When("I request {string}", async (_, request: string) => {
|
|
375
|
+
const { method, path } = parseRequest(request);
|
|
376
|
+
response = await mock.handle(method, path);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
Then("the mock should not have been called", () => {
|
|
380
|
+
expect(mock.called()).toBe(false);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
And("the call count should be 0", () => {
|
|
384
|
+
expect(mock.callCount()).toBe(0);
|
|
385
|
+
});
|
|
386
|
+
},
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
Scenario(
|
|
390
|
+
"maxHistorySize bounds the history with FIFO eviction",
|
|
391
|
+
({ Given, When, Then, And }) => {
|
|
392
|
+
Given(
|
|
393
|
+
"I create a mock with maxHistorySize {int} and a users route",
|
|
394
|
+
(_, maxHistorySize: number) => {
|
|
395
|
+
mock = schmock({ maxHistorySize });
|
|
396
|
+
mock("GET /users", []);
|
|
397
|
+
},
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
When(
|
|
401
|
+
"I issue {int} sequenced requests to {string}",
|
|
402
|
+
async (_, count: number, request: string) => {
|
|
403
|
+
const { method, path } = parseRequest(request);
|
|
404
|
+
for (let i = 0; i < count; i++) {
|
|
405
|
+
await mock.handle(method, path, {
|
|
406
|
+
query: { sequence: String(i + 1) },
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
Then("the call count should be {int}", (_, expected: number) => {
|
|
413
|
+
expect(mock.callCount()).toBe(expected);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
And(
|
|
417
|
+
"the retained request sequence should be {string}",
|
|
418
|
+
(_, sequence: string) => {
|
|
419
|
+
expect(mock.history().map((record) => record.query.sequence)).toEqual(
|
|
420
|
+
sequence.split(","),
|
|
421
|
+
);
|
|
422
|
+
},
|
|
423
|
+
);
|
|
424
|
+
},
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
Scenario(
|
|
428
|
+
"Resetting history preserves routes and shared state",
|
|
429
|
+
({ Given, When, Then, And }) => {
|
|
430
|
+
Given("I create a stateful mock and record a request", async () => {
|
|
431
|
+
mock = schmock({ state: { marker: "preserved" } });
|
|
432
|
+
mock("GET /stateful", ({ state }) => ({ marker: state.marker }));
|
|
433
|
+
await mock.handle("GET", "/stateful");
|
|
434
|
+
expect(mock.callCount()).toBe(1);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
When("I reset only the request history", () => {
|
|
438
|
+
mock.resetHistory();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
Then("the call count should be {int}", (_, expected: number) => {
|
|
442
|
+
expect(mock.callCount()).toBe(expected);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
And("the registered route should still respond", async () => {
|
|
446
|
+
response = await mock.handle("GET", "/stateful");
|
|
447
|
+
expect(response.status).toBe(200);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
And(
|
|
451
|
+
"the shared state marker should still be {string}",
|
|
452
|
+
(_, marker: string) => {
|
|
453
|
+
expect(mock.getState().marker).toBe(marker);
|
|
454
|
+
},
|
|
455
|
+
);
|
|
456
|
+
},
|
|
457
|
+
);
|
|
276
458
|
});
|