@schmock/core 1.0.4 → 1.2.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 +15 -5
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +148 -60
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +11 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -12
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +2 -17
- package/dist/types.d.ts +17 -214
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/package.json +4 -4
- package/src/builder.test.ts +2 -2
- package/src/builder.ts +226 -108
- package/src/constants.ts +17 -1
- package/src/index.ts +34 -28
- package/src/namespace.test.ts +3 -2
- package/src/parser.property.test.ts +493 -0
- package/src/parser.ts +2 -20
- package/src/plugin-system.test.ts +91 -0
- package/src/response-parsing.test.ts +11 -7
- package/src/steps/async-support.steps.ts +101 -91
- package/src/steps/basic-usage.steps.ts +49 -36
- package/src/steps/developer-experience.steps.ts +95 -97
- package/src/steps/error-handling.steps.ts +71 -72
- package/src/steps/fluent-api.steps.ts +75 -72
- package/src/steps/http-methods.steps.ts +33 -33
- package/src/steps/performance-reliability.steps.ts +52 -88
- package/src/steps/plugin-integration.steps.ts +176 -176
- package/src/steps/request-history.steps.ts +333 -0
- package/src/steps/state-concurrency.steps.ts +418 -316
- package/src/steps/stateful-workflows.steps.ts +138 -136
- package/src/types.ts +20 -271
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
|
|
2
2
|
import { expect } from "vitest";
|
|
3
3
|
import { schmock } from "../index";
|
|
4
|
-
import type { CallableMockInstance } from "../types";
|
|
4
|
+
import type { CallableMockInstance, Plugin } from "../types";
|
|
5
5
|
|
|
6
6
|
const feature = await loadFeature("../../features/http-methods.feature");
|
|
7
7
|
|
|
@@ -12,13 +12,13 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
12
12
|
let error: Error | null = null;
|
|
13
13
|
|
|
14
14
|
Scenario("GET method with query parameters", ({ Given, When, Then }) => {
|
|
15
|
-
Given("I create a mock with GET endpoint
|
|
15
|
+
Given("I create a mock with a GET search endpoint", () => {
|
|
16
16
|
mock = schmock();
|
|
17
17
|
mock('GET /search', ({ query }) => ({
|
|
18
18
|
results: [],
|
|
19
19
|
query: query.q,
|
|
20
20
|
page: parseInt(query.page || '1'),
|
|
21
|
-
limit: parseInt(query.limit || '10')
|
|
21
|
+
limit: parseInt(query.limit || '10'),
|
|
22
22
|
}));
|
|
23
23
|
});
|
|
24
24
|
|
|
@@ -41,12 +41,12 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
Scenario("POST method with JSON body", ({ Given, When, Then, And }) => {
|
|
44
|
-
Given("I create a mock with POST endpoint
|
|
44
|
+
Given("I create a mock with a POST users endpoint", () => {
|
|
45
45
|
mock = schmock();
|
|
46
46
|
mock('POST /users', ({ body }) => [201, {
|
|
47
47
|
id: 123,
|
|
48
|
-
...body,
|
|
49
|
-
createdAt: '2023-01-01T00:00:00Z'
|
|
48
|
+
...(body as Record<string, unknown>),
|
|
49
|
+
createdAt: '2023-01-01T00:00:00Z',
|
|
50
50
|
}]);
|
|
51
51
|
});
|
|
52
52
|
|
|
@@ -66,12 +66,12 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
Scenario("PUT method for resource updates", ({ Given, When, Then }) => {
|
|
69
|
-
Given("I create a mock with PUT endpoint
|
|
69
|
+
Given("I create a mock with a PUT users endpoint", () => {
|
|
70
70
|
mock = schmock();
|
|
71
71
|
mock('PUT /users/:id', ({ params, body }) => ({
|
|
72
72
|
id: parseInt(params.id),
|
|
73
|
-
...body,
|
|
74
|
-
updatedAt: '2023-01-01T00:00:00Z'
|
|
73
|
+
...(body as Record<string, unknown>),
|
|
74
|
+
updatedAt: '2023-01-01T00:00:00Z',
|
|
75
75
|
}));
|
|
76
76
|
});
|
|
77
77
|
|
|
@@ -87,7 +87,7 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
Scenario("DELETE method with confirmation", ({ Given, When, Then, And }) => {
|
|
90
|
-
Given("I create a mock with DELETE endpoint
|
|
90
|
+
Given("I create a mock with a DELETE users endpoint", () => {
|
|
91
91
|
mock = schmock();
|
|
92
92
|
mock('DELETE /users/:id', ({ params }) => [204, null]);
|
|
93
93
|
});
|
|
@@ -106,13 +106,13 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
Scenario("PATCH method for partial updates", ({ Given, When, Then }) => {
|
|
109
|
-
Given("I create a mock with PATCH endpoint
|
|
109
|
+
Given("I create a mock with a PATCH users endpoint", () => {
|
|
110
110
|
mock = schmock();
|
|
111
111
|
mock('PATCH /users/:id', ({ params, body }) => ({
|
|
112
112
|
id: parseInt(params.id),
|
|
113
113
|
email: 'existing@example.com',
|
|
114
|
-
...body,
|
|
115
|
-
updatedAt: '2023-01-01T00:00:00Z'
|
|
114
|
+
...(body as Record<string, unknown>),
|
|
115
|
+
updatedAt: '2023-01-01T00:00:00Z',
|
|
116
116
|
}));
|
|
117
117
|
});
|
|
118
118
|
|
|
@@ -128,12 +128,12 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
Scenario("HEAD method returns headers only", ({ Given, When, Then, And }) => {
|
|
131
|
-
Given("I create a mock with HEAD endpoint
|
|
131
|
+
Given("I create a mock with a HEAD users endpoint", () => {
|
|
132
132
|
mock = schmock();
|
|
133
133
|
mock('HEAD /users/:id', ({ params }) => [200, null, {
|
|
134
134
|
'Content-Type': 'application/json',
|
|
135
135
|
'Last-Modified': 'Wed, 01 Jan 2023 00:00:00 GMT',
|
|
136
|
-
'Content-Length': '156'
|
|
136
|
+
'Content-Length': '156',
|
|
137
137
|
}]);
|
|
138
138
|
});
|
|
139
139
|
|
|
@@ -157,12 +157,12 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
Scenario("OPTIONS method for CORS preflight", ({ Given, When, Then, And }) => {
|
|
160
|
-
Given("I create a mock with OPTIONS endpoint
|
|
160
|
+
Given("I create a mock with an OPTIONS users endpoint", () => {
|
|
161
161
|
mock = schmock();
|
|
162
162
|
mock('OPTIONS /api/users', () => [200, null, {
|
|
163
163
|
'Access-Control-Allow-Origin': '*',
|
|
164
164
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
|
165
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
|
165
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
166
166
|
}]);
|
|
167
167
|
});
|
|
168
168
|
|
|
@@ -184,7 +184,7 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
184
184
|
});
|
|
185
185
|
|
|
186
186
|
Scenario("Multiple methods on same path", ({ Given, When, Then, And }) => {
|
|
187
|
-
Given("I create a mock with
|
|
187
|
+
Given("I create a mock with GET, POST, PUT, and DELETE on the same path", () => {
|
|
188
188
|
mock = schmock();
|
|
189
189
|
mock('GET /resource', { action: 'read' });
|
|
190
190
|
mock('POST /resource', { action: 'create' });
|
|
@@ -222,7 +222,7 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
222
222
|
});
|
|
223
223
|
|
|
224
224
|
Scenario("Method-specific content types", ({ Given, When, Then, And }) => {
|
|
225
|
-
Given("I create a mock with
|
|
225
|
+
Given("I create a mock with JSON, XML, text, and upload endpoints", () => {
|
|
226
226
|
mock = schmock();
|
|
227
227
|
mock('GET /data.json', { data: 'json' });
|
|
228
228
|
mock('GET /data.xml', '<data>xml</data>', { contentType: 'application/xml' });
|
|
@@ -258,14 +258,14 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
258
258
|
Scenario("Method case sensitivity", ({ Given, When, Then }) => {
|
|
259
259
|
error = null;
|
|
260
260
|
|
|
261
|
-
Given("I create
|
|
261
|
+
Given("I create an empty mock for case sensitivity testing", () => {
|
|
262
262
|
mock = schmock();
|
|
263
263
|
});
|
|
264
264
|
|
|
265
265
|
When("I attempt to create a mock with lowercase method", () => {
|
|
266
266
|
error = null;
|
|
267
267
|
try {
|
|
268
|
-
mock('get /test', { method: 'get' });
|
|
268
|
+
mock('get /test' as Schmock.RouteKey, { method: 'get' });
|
|
269
269
|
} catch (e) {
|
|
270
270
|
error = e as Error;
|
|
271
271
|
}
|
|
@@ -281,14 +281,14 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
281
281
|
Scenario("Unsupported HTTP methods", ({ Given, When, Then }) => {
|
|
282
282
|
error = null;
|
|
283
283
|
|
|
284
|
-
Given("I create
|
|
284
|
+
Given("I create an empty mock for unsupported method testing", () => {
|
|
285
285
|
mock = schmock();
|
|
286
286
|
});
|
|
287
287
|
|
|
288
288
|
When("I attempt to create a mock with unsupported method", () => {
|
|
289
289
|
error = null;
|
|
290
290
|
try {
|
|
291
|
-
mock('CUSTOM /endpoint', { custom: true });
|
|
291
|
+
mock('CUSTOM /endpoint' as Schmock.RouteKey, { custom: true });
|
|
292
292
|
} catch (e) {
|
|
293
293
|
error = e as Error;
|
|
294
294
|
}
|
|
@@ -302,11 +302,11 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
302
302
|
});
|
|
303
303
|
|
|
304
304
|
Scenario("Method with special characters in path", ({ Given, When, Then }) => {
|
|
305
|
-
Given("I create a mock with
|
|
305
|
+
Given("I create a mock with nested parameterized path segments", () => {
|
|
306
306
|
mock = schmock();
|
|
307
307
|
mock('GET /api/v1/users/:id/posts/:post-id', ({ params }) => ({
|
|
308
308
|
userId: params.id,
|
|
309
|
-
postId: params['post-id']
|
|
309
|
+
postId: params['post-id'],
|
|
310
310
|
}));
|
|
311
311
|
});
|
|
312
312
|
|
|
@@ -321,7 +321,7 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
321
321
|
});
|
|
322
322
|
|
|
323
323
|
Scenario("Method with request headers validation", ({ Given, When, Then, And }) => {
|
|
324
|
-
Given("I create a mock with header
|
|
324
|
+
Given("I create a mock with authorization header checking", () => {
|
|
325
325
|
mock = schmock();
|
|
326
326
|
mock('POST /secure', ({ headers, body }) => {
|
|
327
327
|
if (headers.authorization !== 'Bearer valid-token') {
|
|
@@ -361,18 +361,18 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
361
361
|
});
|
|
362
362
|
|
|
363
363
|
Scenario("Method chaining with plugins", ({ Given, When, Then, And }) => {
|
|
364
|
-
Given("I create a mock with
|
|
364
|
+
Given("I create a mock with a logger plugin on GET and POST", () => {
|
|
365
365
|
mock = schmock();
|
|
366
|
-
const loggerPlugin = {
|
|
366
|
+
const loggerPlugin: Plugin = {
|
|
367
367
|
name: 'method-logger',
|
|
368
|
-
process: (ctx
|
|
368
|
+
process: (ctx, response) => ({
|
|
369
369
|
context: ctx,
|
|
370
370
|
response: {
|
|
371
371
|
...response,
|
|
372
372
|
method: ctx.method,
|
|
373
|
-
logged: true
|
|
374
|
-
}
|
|
375
|
-
})
|
|
373
|
+
logged: true,
|
|
374
|
+
},
|
|
375
|
+
}),
|
|
376
376
|
};
|
|
377
377
|
mock('GET /logged', { data: 'get' }).pipe(loggerPlugin);
|
|
378
378
|
mock('POST /logged', { data: 'post' }).pipe(loggerPlugin);
|
|
@@ -394,4 +394,4 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
394
394
|
expect(responses[1].body).toEqual(expected);
|
|
395
395
|
});
|
|
396
396
|
});
|
|
397
|
-
});
|
|
397
|
+
});
|
|
@@ -1,39 +1,35 @@
|
|
|
1
1
|
import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
|
|
2
2
|
import { expect } from "vitest";
|
|
3
3
|
import { schmock } from "../index";
|
|
4
|
-
import type {
|
|
4
|
+
import type { CallableMockInstance } from "../types";
|
|
5
5
|
|
|
6
6
|
const feature = await loadFeature("../../features/performance-reliability.feature");
|
|
7
7
|
|
|
8
8
|
describeFeature(feature, ({ Scenario }) => {
|
|
9
|
-
let mock:
|
|
9
|
+
let mock: CallableMockInstance;
|
|
10
10
|
let responses: any[] = [];
|
|
11
11
|
let responsesTimes: number[] = [];
|
|
12
|
+
let expectedResponseCount = 0;
|
|
12
13
|
|
|
13
14
|
Scenario("High-volume concurrent requests", ({ Given, When, Then, And }) => {
|
|
14
15
|
responses = [];
|
|
15
16
|
responsesTimes = [];
|
|
16
17
|
|
|
17
|
-
Given("I create a mock for load testing
|
|
18
|
-
// Create mock with new callable API for load testing
|
|
18
|
+
Given("I create a mock for high-volume load testing", () => {
|
|
19
19
|
mock = schmock();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
timestamp: Date.now()
|
|
20
|
+
mock('GET /api/health', () => ({
|
|
21
|
+
status: 'healthy',
|
|
22
|
+
timestamp: Date.now()
|
|
24
23
|
}));
|
|
25
|
-
|
|
26
24
|
mock('GET /api/data/:id', ({ params }) => ({
|
|
27
25
|
id: params.id,
|
|
28
|
-
data: Array.from({ length: 50 }, (_, i) => ({
|
|
29
|
-
index: i,
|
|
30
|
-
value: Math.random()
|
|
26
|
+
data: Array.from({ length: 50 }, (_, i) => ({
|
|
27
|
+
index: i,
|
|
28
|
+
value: Math.random()
|
|
31
29
|
})),
|
|
32
30
|
generated_at: new Date().toISOString()
|
|
33
31
|
}));
|
|
34
|
-
|
|
35
32
|
mock('POST /api/process', ({ body }) => {
|
|
36
|
-
// Simulate processing time
|
|
37
33
|
const items = Array.isArray(body) ? body : [body];
|
|
38
34
|
return {
|
|
39
35
|
processed: items.length,
|
|
@@ -47,6 +43,7 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
47
43
|
const [method, path] = request.split(" ");
|
|
48
44
|
responses = [];
|
|
49
45
|
responsesTimes = [];
|
|
46
|
+
expectedResponseCount = count;
|
|
50
47
|
|
|
51
48
|
const promises = Array.from({ length: count }, async () => {
|
|
52
49
|
const startTime = Date.now();
|
|
@@ -61,8 +58,7 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
61
58
|
});
|
|
62
59
|
|
|
63
60
|
Then("all concurrent requests should complete successfully", () => {
|
|
64
|
-
|
|
65
|
-
expect(responses).toHaveLength(expectedCount);
|
|
61
|
+
expect(responses).toHaveLength(expectedResponseCount);
|
|
66
62
|
for (const response of responses) {
|
|
67
63
|
expect(response.status).toBe(200);
|
|
68
64
|
}
|
|
@@ -80,7 +76,7 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
80
76
|
|
|
81
77
|
When("I send {int} concurrent requests to different {string} endpoints", async (_, count: number, pathPattern: string) => {
|
|
82
78
|
responses = [];
|
|
83
|
-
|
|
79
|
+
|
|
84
80
|
const promises = Array.from({ length: count }, async (_, i) => {
|
|
85
81
|
const path = pathPattern.replace(":id", `id${i}`);
|
|
86
82
|
return await mock.handle("GET", path);
|
|
@@ -104,6 +100,7 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
104
100
|
When("I send {int} concurrent {string} requests with different payloads", async (_, count: number, request: string) => {
|
|
105
101
|
const [method, path] = request.split(" ");
|
|
106
102
|
responses = [];
|
|
103
|
+
expectedResponseCount = count;
|
|
107
104
|
|
|
108
105
|
const promises = Array.from({ length: count }, async (_, i) => {
|
|
109
106
|
return await mock.handle(method as any, path, {
|
|
@@ -115,8 +112,7 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
115
112
|
});
|
|
116
113
|
|
|
117
114
|
And("all concurrent requests should complete successfully", () => {
|
|
118
|
-
|
|
119
|
-
expect(responses).toHaveLength(expectedCount);
|
|
115
|
+
expect(responses).toHaveLength(expectedResponseCount);
|
|
120
116
|
for (const response of responses) {
|
|
121
117
|
expect(response.status).toBe(200);
|
|
122
118
|
}
|
|
@@ -130,19 +126,15 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
130
126
|
});
|
|
131
127
|
|
|
132
128
|
Scenario("Memory usage under sustained load", ({ Given, When, Then, And }) => {
|
|
133
|
-
Given("I create a mock with
|
|
134
|
-
// Create mock with routes that handle large data
|
|
129
|
+
Given("I create a mock with large payload handling", () => {
|
|
135
130
|
mock = schmock();
|
|
136
|
-
|
|
137
131
|
mock('POST /api/large-data', ({ body }) => {
|
|
138
|
-
// Create large response data
|
|
139
132
|
const largeArray = Array.from({ length: 1000 }, (_, i) => ({
|
|
140
133
|
id: i,
|
|
141
|
-
data: 'x'.repeat(100),
|
|
134
|
+
data: 'x'.repeat(100),
|
|
142
135
|
timestamp: Date.now(),
|
|
143
136
|
payload: body
|
|
144
137
|
}));
|
|
145
|
-
|
|
146
138
|
return {
|
|
147
139
|
results: largeArray,
|
|
148
140
|
items: largeArray,
|
|
@@ -151,7 +143,6 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
151
143
|
memory_usage: process.memoryUsage ? process.memoryUsage() : null
|
|
152
144
|
};
|
|
153
145
|
});
|
|
154
|
-
|
|
155
146
|
mock('GET /api/accumulate/:count', ({ params }) => {
|
|
156
147
|
const count = parseInt(params.count);
|
|
157
148
|
const items = Array.from({ length: count }, (_, i) => ({
|
|
@@ -159,7 +150,6 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
159
150
|
value: Math.random(),
|
|
160
151
|
timestamp: Date.now()
|
|
161
152
|
}));
|
|
162
|
-
|
|
163
153
|
return {
|
|
164
154
|
items: items,
|
|
165
155
|
accumulated: items,
|
|
@@ -173,7 +163,7 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
173
163
|
When("I send {int} requests to {string} with {int}KB payloads", async (_, count: number, request: string, payloadSize: number) => {
|
|
174
164
|
const [method, path] = request.split(" ");
|
|
175
165
|
responses = [];
|
|
176
|
-
|
|
166
|
+
|
|
177
167
|
const largePayload = { data: 'x'.repeat(payloadSize * 1024) };
|
|
178
168
|
|
|
179
169
|
for (let i = 0; i < count; i++) {
|
|
@@ -198,7 +188,7 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
198
188
|
When("I request {string} multiple times", async (_, request: string) => {
|
|
199
189
|
const [method, path] = request.split(" ");
|
|
200
190
|
responses = [];
|
|
201
|
-
|
|
191
|
+
|
|
202
192
|
for (let i = 0; i < 5; i++) {
|
|
203
193
|
const response = await mock.handle(method as any, path);
|
|
204
194
|
responses.push(response);
|
|
@@ -221,47 +211,34 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
221
211
|
Scenario("Error resilience and recovery", ({ Given, When, Then, And }) => {
|
|
222
212
|
responses = [];
|
|
223
213
|
|
|
224
|
-
Given("I create a mock with intermittent
|
|
225
|
-
// Create mock with intermittent failure simulation
|
|
214
|
+
Given("I create a mock with intermittent failure simulation", () => {
|
|
226
215
|
mock = schmock();
|
|
227
|
-
|
|
228
216
|
let requestCount = 0;
|
|
229
|
-
|
|
230
217
|
mock('POST /api/unreliable', ({ body }) => {
|
|
231
218
|
requestCount++;
|
|
232
|
-
|
|
233
|
-
// Simulate 20% failure rate
|
|
234
219
|
if (requestCount % 5 === 0) {
|
|
235
220
|
return [500, { error: 'Simulated server error', request_id: requestCount }];
|
|
236
221
|
}
|
|
237
|
-
|
|
238
222
|
return [200, { success: true, data: body, request_id: requestCount }];
|
|
239
223
|
});
|
|
240
|
-
|
|
241
224
|
mock('GET /api/flaky', () => {
|
|
242
225
|
requestCount++;
|
|
243
|
-
|
|
244
|
-
// Simulate 20% failure rate (1 in 5 requests fail)
|
|
245
226
|
if (requestCount % 5 === 0) {
|
|
246
227
|
return [500, { error: 'Flaky service error', request_id: requestCount }];
|
|
247
228
|
}
|
|
248
|
-
|
|
249
229
|
return [200, { success: true, request_id: requestCount }];
|
|
250
230
|
});
|
|
251
|
-
|
|
252
231
|
mock('POST /api/validate-strict', ({ body }) => {
|
|
253
232
|
if (!body || typeof body !== 'object') {
|
|
254
233
|
return [400, { error: 'Request body is required and must be an object', code: 'INVALID_BODY' }];
|
|
255
234
|
}
|
|
256
|
-
|
|
257
|
-
if (!
|
|
235
|
+
const record = body as Record<string, unknown>;
|
|
236
|
+
if (!record.name || typeof record.name !== 'string') {
|
|
258
237
|
return [422, { error: 'Name field is required and must be a string', code: 'INVALID_NAME' }];
|
|
259
238
|
}
|
|
260
|
-
|
|
261
|
-
if (!body.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) {
|
|
239
|
+
if (!record.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(record.email as string)) {
|
|
262
240
|
return [422, { error: 'Valid email address is required', code: 'INVALID_EMAIL' }];
|
|
263
241
|
}
|
|
264
|
-
|
|
265
242
|
return [200, { message: 'Validation successful', data: body }];
|
|
266
243
|
});
|
|
267
244
|
});
|
|
@@ -279,7 +256,7 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
279
256
|
Then("some requests should succeed and some should fail", () => {
|
|
280
257
|
const successCount = responses.filter(r => r.status === 200).length;
|
|
281
258
|
const errorCount = responses.filter(r => r.status >= 400).length;
|
|
282
|
-
|
|
259
|
+
|
|
283
260
|
expect(successCount).toBeGreaterThan(0);
|
|
284
261
|
expect(errorCount).toBeGreaterThan(0);
|
|
285
262
|
expect(successCount + errorCount).toBe(responses.length);
|
|
@@ -288,7 +265,7 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
288
265
|
And("the success rate should be approximately {int}%", (_, expectedRate: number) => {
|
|
289
266
|
const successCount = responses.filter(r => r.status === 200).length;
|
|
290
267
|
const actualRate = (successCount / responses.length) * 100;
|
|
291
|
-
|
|
268
|
+
|
|
292
269
|
// Allow for some variance due to randomness
|
|
293
270
|
expect(actualRate).toBeGreaterThan(expectedRate - 10);
|
|
294
271
|
expect(actualRate).toBeLessThan(expectedRate + 10);
|
|
@@ -297,18 +274,18 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
297
274
|
And("error responses should have appropriate status codes", () => {
|
|
298
275
|
const errorResponses = responses.filter(r => r.status >= 400);
|
|
299
276
|
const validErrorCodes = [429, 500, 503];
|
|
300
|
-
|
|
277
|
+
|
|
301
278
|
for (const response of errorResponses) {
|
|
302
279
|
expect(validErrorCodes).toContain(response.status);
|
|
303
280
|
}
|
|
304
281
|
});
|
|
305
282
|
|
|
306
283
|
When("I send requests to {string} with various invalid inputs", async (_, request: string) => {
|
|
307
|
-
const [method, path] = request.split(" ");
|
|
284
|
+
const [method, path] = request.split(" ");
|
|
308
285
|
responses = [];
|
|
309
286
|
|
|
310
287
|
// Test various invalid scenarios
|
|
311
|
-
const testCases = [
|
|
288
|
+
const testCases: Schmock.RequestOptions[] = [
|
|
312
289
|
{ headers: {}, body: null }, // No content-type, no body
|
|
313
290
|
{ headers: { 'content-type': 'application/json' }, body: null }, // No body
|
|
314
291
|
{ headers: { 'content-type': 'application/json' }, body: "invalid" }, // Invalid body type
|
|
@@ -332,7 +309,7 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
332
309
|
|
|
333
310
|
And("the error codes should correctly identify the validation issue", () => {
|
|
334
311
|
expect(responses[0].status).toBe(400); // No content-type
|
|
335
|
-
expect(responses[1].status).toBe(400); // No body
|
|
312
|
+
expect(responses[1].status).toBe(400); // No body
|
|
336
313
|
expect(responses[2].status).toBe(400); // Invalid body type
|
|
337
314
|
expect(responses[3].status).toBe(422); // Missing required field
|
|
338
315
|
});
|
|
@@ -341,47 +318,44 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
341
318
|
Scenario("Route matching performance with complex patterns", ({ Given, When, Then, And }) => {
|
|
342
319
|
responses = [];
|
|
343
320
|
|
|
344
|
-
Given("I create a mock with many route patterns
|
|
345
|
-
// Create mock with many different route patterns for performance testing
|
|
321
|
+
Given("I create a mock with many nested route patterns", () => {
|
|
346
322
|
mock = schmock();
|
|
347
|
-
|
|
348
|
-
// Routes that match the test expectations
|
|
349
323
|
mock('GET /api/users', () => ({ type: 'users-list' }));
|
|
350
324
|
mock('GET /api/users/:id', ({ params }) => ({ type: 'user', id: params.id }));
|
|
351
325
|
mock('GET /api/users/:userId/posts', ({ params }) => ({ type: 'user-posts', userId: params.userId }));
|
|
352
|
-
mock('GET /api/users/:userId/posts/:postId', ({ params }) => ({
|
|
353
|
-
type: 'user-post',
|
|
354
|
-
userId: params.userId,
|
|
355
|
-
postId: params.postId
|
|
326
|
+
mock('GET /api/users/:userId/posts/:postId', ({ params }) => ({
|
|
327
|
+
type: 'user-post',
|
|
328
|
+
userId: params.userId,
|
|
329
|
+
postId: params.postId
|
|
356
330
|
}));
|
|
357
|
-
mock('GET /api/users/:userId/posts/:postId/comments', ({ params }) => ({
|
|
358
|
-
type: 'post-comments',
|
|
359
|
-
userId: params.userId,
|
|
360
|
-
postId: params.postId
|
|
331
|
+
mock('GET /api/users/:userId/posts/:postId/comments', ({ params }) => ({
|
|
332
|
+
type: 'post-comments',
|
|
333
|
+
userId: params.userId,
|
|
334
|
+
postId: params.postId
|
|
361
335
|
}));
|
|
362
336
|
mock('GET /api/posts', () => ({ type: 'posts-list' }));
|
|
363
337
|
mock('GET /api/posts/:postId', ({ params }) => ({ type: 'post', postId: params.postId }));
|
|
364
|
-
mock('GET /api/posts/:postId/comments/:commentId', ({ params }) => ({
|
|
365
|
-
type: 'comment',
|
|
366
|
-
postId: params.postId,
|
|
367
|
-
commentId: params.commentId
|
|
338
|
+
mock('GET /api/posts/:postId/comments/:commentId', ({ params }) => ({
|
|
339
|
+
type: 'comment',
|
|
340
|
+
postId: params.postId,
|
|
341
|
+
commentId: params.commentId
|
|
368
342
|
}));
|
|
369
|
-
mock('GET /static/:category/:file', ({ params }) => ({
|
|
370
|
-
type: 'static',
|
|
371
|
-
category: params.category,
|
|
372
|
-
file: params.file
|
|
343
|
+
mock('GET /static/:category/:file', ({ params }) => ({
|
|
344
|
+
type: 'static',
|
|
345
|
+
category: params.category,
|
|
346
|
+
file: params.file
|
|
373
347
|
}));
|
|
374
|
-
mock('GET /api/v2/users/:userId', ({ params }) => ({
|
|
375
|
-
type: 'versioned-user',
|
|
376
|
-
userId: params.userId,
|
|
377
|
-
version: 'v2'
|
|
348
|
+
mock('GET /api/v2/users/:userId', ({ params }) => ({
|
|
349
|
+
type: 'versioned-user',
|
|
350
|
+
userId: params.userId,
|
|
351
|
+
version: 'v2'
|
|
378
352
|
}));
|
|
379
353
|
});
|
|
380
354
|
|
|
381
355
|
When("I send requests to all route patterns simultaneously", async () => {
|
|
382
356
|
const testPaths = [
|
|
383
357
|
"GET /api/users",
|
|
384
|
-
"GET /api/users/123",
|
|
358
|
+
"GET /api/users/123",
|
|
385
359
|
"GET /api/users/123/posts",
|
|
386
360
|
"GET /api/users/123/posts/456",
|
|
387
361
|
"GET /api/users/123/posts/456/comments",
|
|
@@ -423,11 +397,6 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
423
397
|
expect(responses[9].body.version).toBe("v2");
|
|
424
398
|
});
|
|
425
399
|
|
|
426
|
-
And("the route matching should be efficient even with many patterns", () => {
|
|
427
|
-
// All requests completed quickly if we got here
|
|
428
|
-
expect(responses).toHaveLength(10);
|
|
429
|
-
});
|
|
430
|
-
|
|
431
400
|
When("I send requests to non-matching paths", async () => {
|
|
432
401
|
const invalidPaths = [
|
|
433
402
|
"GET /api/invalid",
|
|
@@ -449,11 +418,6 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
449
418
|
expect(response.status).toBe(expectedStatus);
|
|
450
419
|
}
|
|
451
420
|
});
|
|
452
|
-
|
|
453
|
-
And("the {int} responses should be fast", (_, statusCode: number) => {
|
|
454
|
-
// If we got here quickly, the 404 responses were fast
|
|
455
|
-
expect(responses.every(r => r.status === statusCode)).toBe(true);
|
|
456
|
-
});
|
|
457
421
|
});
|
|
458
422
|
|
|
459
|
-
});
|
|
423
|
+
});
|