@schmock/core 2.2.3 → 2.3.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/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/abort.d.ts +3 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +30 -0
- package/dist/binary.d.ts +8 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +9 -0
- package/dist/builder.d.ts +31 -10
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +804 -203
- package/dist/constants.d.ts +38 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +123 -2
- package/dist/errors.d.ts +14 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +41 -6
- package/dist/helpers.d.ts +9 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +18 -2
- package/dist/http-helpers.d.ts +52 -5
- package/dist/http-helpers.d.ts.map +1 -1
- package/dist/http-helpers.js +153 -37
- package/dist/index.d.ts +381 -60
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/interceptor.d.ts +11 -1
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/interceptor.js +354 -156
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +12 -3
- package/dist/plugin-pipeline.d.ts +11 -4
- package/dist/plugin-pipeline.d.ts.map +1 -1
- package/dist/plugin-pipeline.js +117 -39
- package/dist/response-normalizer.d.ts +16 -0
- package/dist/response-normalizer.d.ts.map +1 -0
- package/dist/response-normalizer.js +316 -0
- package/dist/response-parser.d.ts.map +1 -1
- package/dist/response-parser.js +85 -18
- package/dist/route-matcher.d.ts +3 -0
- package/dist/route-matcher.d.ts.map +1 -1
- package/dist/route-matcher.js +12 -6
- package/dist/types.d.ts +7 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +18 -8
- package/src/audit-core-builder.test.ts +0 -218
- package/src/audit-response-guard.test.ts +0 -38
- package/src/builder.test.ts +0 -289
- package/src/builder.ts +0 -701
- package/src/constants.test.ts +0 -99
- package/src/constants.ts +0 -73
- package/src/debug.test.ts +0 -241
- package/src/delay.test.ts +0 -319
- package/src/dist-shape.test.ts +0 -35
- package/src/errors.test.ts +0 -223
- package/src/errors.ts +0 -130
- package/src/factory.test.ts +0 -133
- package/src/helpers.test.ts +0 -147
- package/src/helpers.ts +0 -58
- package/src/http-helpers.ts +0 -113
- package/src/index.ts +0 -151
- package/src/interceptor.test.ts +0 -291
- package/src/interceptor.ts +0 -320
- package/src/namespace.test.ts +0 -274
- package/src/parser.property.test.ts +0 -594
- package/src/parser.test.ts +0 -148
- package/src/parser.ts +0 -64
- package/src/plugin-pipeline.ts +0 -103
- package/src/plugin-system.test.ts +0 -602
- package/src/response-parser.ts +0 -75
- package/src/response-parsing.test.ts +0 -333
- package/src/route-matcher.ts +0 -69
- package/src/route-matching.test.ts +0 -394
- package/src/server.test.ts +0 -165
- package/src/smart-defaults.test.ts +0 -361
- package/src/steps/async-support.steps.ts +0 -400
- package/src/steps/audit-core-builder.steps.ts +0 -188
- package/src/steps/audit-onerror-tuple.steps.ts +0 -99
- package/src/steps/basic-usage.steps.ts +0 -245
- package/src/steps/developer-experience.steps.ts +0 -204
- package/src/steps/error-handling.steps.ts +0 -345
- package/src/steps/fluent-api.steps.ts +0 -255
- package/src/steps/http-methods.steps.ts +0 -331
- package/src/steps/interceptor.steps.ts +0 -260
- package/src/steps/lifecycle-events.steps.ts +0 -142
- package/src/steps/performance-reliability.steps.ts +0 -423
- package/src/steps/plugin-integration.steps.ts +0 -280
- package/src/steps/request-history.steps.ts +0 -276
- package/src/steps/response-delay.steps.ts +0 -88
- package/src/steps/route-key-format.steps.ts +0 -99
- package/src/steps/standalone-server.steps.ts +0 -233
- package/src/steps/state-concurrency.steps.ts +0 -739
- package/src/steps/stateful-workflows.steps.ts +0 -353
- package/src/types.ts +0 -36
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
|
|
2
|
-
import { expect } from "vitest";
|
|
3
|
-
import { schmock } from "../index";
|
|
4
|
-
|
|
5
|
-
const feature = await loadFeature("../../features/lifecycle-events.feature");
|
|
6
|
-
|
|
7
|
-
describeFeature(feature, ({ Scenario }) => {
|
|
8
|
-
let mock: Schmock.CallableMockInstance;
|
|
9
|
-
let events: Array<{ type: string; data: unknown }>;
|
|
10
|
-
let removedFired: boolean;
|
|
11
|
-
|
|
12
|
-
function collectEvent(type: string) {
|
|
13
|
-
return (data: unknown) => {
|
|
14
|
-
events.push({ type, data });
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
Scenario("Events fire at correct times", ({ Given, And, When, Then }) => {
|
|
19
|
-
Given('a mock with a route "GET /items"', () => {
|
|
20
|
-
mock = schmock({ state: {} });
|
|
21
|
-
mock("GET /items", [{ id: 1 }], {});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
And("I register listeners for all events", () => {
|
|
25
|
-
events = [];
|
|
26
|
-
mock.on("request:start", collectEvent("request:start"));
|
|
27
|
-
mock.on("request:match", collectEvent("request:match"));
|
|
28
|
-
mock.on("request:notfound", collectEvent("request:notfound"));
|
|
29
|
-
mock.on("request:end", collectEvent("request:end"));
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
When('I request "GET /items"', async () => {
|
|
33
|
-
await mock.handle("GET", "/items");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
Then('the "request:start" event fired', () => {
|
|
37
|
-
expect(events.some((e) => e.type === "request:start")).toBe(true);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
And('the "request:match" event fired with routePath "/items"', () => {
|
|
41
|
-
const match = events.find((e) => e.type === "request:match");
|
|
42
|
-
expect(match).toBeDefined();
|
|
43
|
-
const data = match?.data as Record<string, unknown>;
|
|
44
|
-
expect(data.routePath).toBe("/items");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
And('the "request:end" event fired with status 200', () => {
|
|
48
|
-
const end = events.find((e) => e.type === "request:end");
|
|
49
|
-
expect(end).toBeDefined();
|
|
50
|
-
const data = end?.data as Record<string, unknown>;
|
|
51
|
-
expect(data.status).toBe(200);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
Scenario("Not found event fires for unmatched routes", ({ Given, And, When, Then }) => {
|
|
56
|
-
Given('a mock with a route "GET /items"', () => {
|
|
57
|
-
mock = schmock({ state: {} });
|
|
58
|
-
mock("GET /items", [{ id: 1 }], {});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
And("I register listeners for all events", () => {
|
|
62
|
-
events = [];
|
|
63
|
-
mock.on("request:start", collectEvent("request:start"));
|
|
64
|
-
mock.on("request:match", collectEvent("request:match"));
|
|
65
|
-
mock.on("request:notfound", collectEvent("request:notfound"));
|
|
66
|
-
mock.on("request:end", collectEvent("request:end"));
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
When('I request "GET /missing"', async () => {
|
|
70
|
-
await mock.handle("GET", "/missing");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
Then('the "request:start" event fired', () => {
|
|
74
|
-
expect(events.some((e) => e.type === "request:start")).toBe(true);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
And('the "request:notfound" event fired', () => {
|
|
78
|
-
expect(events.some((e) => e.type === "request:notfound")).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
And('the "request:end" event fired with status 404', () => {
|
|
82
|
-
const end = events.find((e) => e.type === "request:end");
|
|
83
|
-
expect(end).toBeDefined();
|
|
84
|
-
const data = end?.data as Record<string, unknown>;
|
|
85
|
-
expect(data.status).toBe(404);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
Scenario("Off removes listener", ({ Given, And, When, Then }) => {
|
|
90
|
-
Given('a mock with a route "GET /items"', () => {
|
|
91
|
-
mock = schmock({ state: {} });
|
|
92
|
-
mock("GET /items", [{ id: 1 }], {});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
And("I register and remove a listener", () => {
|
|
96
|
-
removedFired = false;
|
|
97
|
-
const listener = () => {
|
|
98
|
-
removedFired = true;
|
|
99
|
-
};
|
|
100
|
-
mock.on("request:start", listener);
|
|
101
|
-
mock.off("request:start", listener);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
When('I request "GET /items"', async () => {
|
|
105
|
-
await mock.handle("GET", "/items");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
Then("the removed listener did not fire", () => {
|
|
109
|
-
expect(removedFired).toBe(false);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
Scenario("Reset clears all listeners", ({ Given, And, When, Then }) => {
|
|
114
|
-
Given('a mock with a route "GET /items"', () => {
|
|
115
|
-
mock = schmock({ state: {} });
|
|
116
|
-
mock("GET /items", [{ id: 1 }], {});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
And("I register listeners for all events", () => {
|
|
120
|
-
events = [];
|
|
121
|
-
mock.on("request:start", collectEvent("request:start"));
|
|
122
|
-
mock.on("request:end", collectEvent("request:end"));
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
When("I reset the mock", () => {
|
|
126
|
-
mock.reset();
|
|
127
|
-
events = [];
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
And('I add a route "GET /items" again', () => {
|
|
131
|
-
mock("GET /items", [{ id: 1 }], {});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
And('I request "GET /items" after reset', async () => {
|
|
135
|
-
await mock.handle("GET", "/items");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
Then("no events were collected after reset", () => {
|
|
139
|
-
expect(events).toHaveLength(0);
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
});
|
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
|
|
2
|
-
import { expect } from "vitest";
|
|
3
|
-
import { schmock } from "../index";
|
|
4
|
-
import type { CallableMockInstance } from "../types";
|
|
5
|
-
|
|
6
|
-
const feature = await loadFeature("../../features/performance-reliability.feature");
|
|
7
|
-
|
|
8
|
-
describeFeature(feature, ({ Scenario }) => {
|
|
9
|
-
let mock: CallableMockInstance;
|
|
10
|
-
let responses: any[] = [];
|
|
11
|
-
let responsesTimes: number[] = [];
|
|
12
|
-
let expectedResponseCount = 0;
|
|
13
|
-
|
|
14
|
-
Scenario("High-volume concurrent requests", ({ Given, When, Then, And }) => {
|
|
15
|
-
responses = [];
|
|
16
|
-
responsesTimes = [];
|
|
17
|
-
|
|
18
|
-
Given("I create a mock for high-volume load testing", () => {
|
|
19
|
-
mock = schmock();
|
|
20
|
-
mock('GET /api/health', () => ({
|
|
21
|
-
status: 'healthy',
|
|
22
|
-
timestamp: Date.now()
|
|
23
|
-
}));
|
|
24
|
-
mock('GET /api/data/:id', ({ params }) => ({
|
|
25
|
-
id: params.id,
|
|
26
|
-
data: Array.from({ length: 50 }, (_, i) => ({
|
|
27
|
-
index: i,
|
|
28
|
-
value: Math.random()
|
|
29
|
-
})),
|
|
30
|
-
generated_at: new Date().toISOString()
|
|
31
|
-
}));
|
|
32
|
-
mock('POST /api/process', ({ body }) => {
|
|
33
|
-
const items = Array.isArray(body) ? body : [body];
|
|
34
|
-
return {
|
|
35
|
-
processed: items.length,
|
|
36
|
-
results: items.map(item => ({ ...item, processed: true })),
|
|
37
|
-
batch_id: Math.random().toString(36)
|
|
38
|
-
};
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
When("I send {int} concurrent {string} requests", async (_, count: number, request: string) => {
|
|
43
|
-
const [method, path] = request.split(" ");
|
|
44
|
-
responses = [];
|
|
45
|
-
responsesTimes = [];
|
|
46
|
-
expectedResponseCount = count;
|
|
47
|
-
|
|
48
|
-
const promises = Array.from({ length: count }, async () => {
|
|
49
|
-
const startTime = Date.now();
|
|
50
|
-
const response = await mock.handle(method as any, path);
|
|
51
|
-
const elapsed = Date.now() - startTime;
|
|
52
|
-
return { response, elapsed };
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const results = await Promise.all(promises);
|
|
56
|
-
responses = results.map(r => r.response);
|
|
57
|
-
responsesTimes = results.map(r => r.elapsed);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
Then("all concurrent requests should complete successfully", () => {
|
|
61
|
-
expect(responses).toHaveLength(expectedResponseCount);
|
|
62
|
-
for (const response of responses) {
|
|
63
|
-
expect(response.status).toBe(200);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Generous ceiling, not a perf target: in-process handlers are sub-millisecond,
|
|
68
|
-
// so this only guards against a pathological hang. A tight bound (e.g. 50ms)
|
|
69
|
-
// flakes under CI load where 100 concurrent requests contend for the event loop.
|
|
70
|
-
And("the average response time should be under {int}ms", (_, maxTime: number) => {
|
|
71
|
-
const avgTime = responsesTimes.reduce((a, b) => a + b, 0) / responsesTimes.length;
|
|
72
|
-
expect(avgTime).toBeLessThan(maxTime);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
And("no requests should timeout", () => {
|
|
76
|
-
// All requests completed if we got here, so no timeouts
|
|
77
|
-
expect(responses.length).toBeGreaterThan(0);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
When("I send {int} concurrent requests to different {string} endpoints", async (_, count: number, pathPattern: string) => {
|
|
81
|
-
responses = [];
|
|
82
|
-
|
|
83
|
-
const promises = Array.from({ length: count }, async (_, i) => {
|
|
84
|
-
const path = pathPattern.replace(":id", `id${i}`);
|
|
85
|
-
return await mock.handle("GET", path);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
responses = await Promise.all(promises);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
Then("all responses should be unique based on ID", () => {
|
|
92
|
-
const ids = responses.map(r => r.body.id);
|
|
93
|
-
const uniqueIds = new Set(ids);
|
|
94
|
-
expect(uniqueIds.size).toBe(responses.length);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
And("each response should contain {int} data items", (_, expectedCount: number) => {
|
|
98
|
-
for (const response of responses) {
|
|
99
|
-
expect(response.body.data).toHaveLength(expectedCount);
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
When("I send {int} concurrent {string} requests with different payloads", async (_, count: number, request: string) => {
|
|
104
|
-
const [method, path] = request.split(" ");
|
|
105
|
-
responses = [];
|
|
106
|
-
expectedResponseCount = count;
|
|
107
|
-
|
|
108
|
-
const promises = Array.from({ length: count }, async (_, i) => {
|
|
109
|
-
return await mock.handle(method as any, path, {
|
|
110
|
-
body: { id: i, data: `payload-${i}` }
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
responses = await Promise.all(promises);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
And("all concurrent requests should complete successfully", () => {
|
|
118
|
-
expect(responses).toHaveLength(expectedResponseCount);
|
|
119
|
-
for (const response of responses) {
|
|
120
|
-
expect(response.status).toBe(200);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
And("each response should have a unique batch_id", () => {
|
|
125
|
-
const batchIds = responses.map(r => r.body.batch_id);
|
|
126
|
-
const uniqueBatchIds = new Set(batchIds);
|
|
127
|
-
expect(uniqueBatchIds.size).toBe(responses.length);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
Scenario("Memory usage under sustained load", ({ Given, When, Then, And }) => {
|
|
132
|
-
Given("I create a mock with large payload handling", () => {
|
|
133
|
-
mock = schmock();
|
|
134
|
-
mock('POST /api/large-data', ({ body }) => {
|
|
135
|
-
const largeArray = Array.from({ length: 1000 }, (_, i) => ({
|
|
136
|
-
id: i,
|
|
137
|
-
data: 'x'.repeat(100),
|
|
138
|
-
timestamp: Date.now(),
|
|
139
|
-
payload: body
|
|
140
|
-
}));
|
|
141
|
-
return {
|
|
142
|
-
results: largeArray,
|
|
143
|
-
items: largeArray,
|
|
144
|
-
total_size: largeArray.length,
|
|
145
|
-
size: 'large',
|
|
146
|
-
memory_usage: process.memoryUsage ? process.memoryUsage() : null
|
|
147
|
-
};
|
|
148
|
-
});
|
|
149
|
-
mock('GET /api/accumulate/:count', ({ params }) => {
|
|
150
|
-
const count = parseInt(params.count);
|
|
151
|
-
const items = Array.from({ length: count }, (_, i) => ({
|
|
152
|
-
id: i,
|
|
153
|
-
value: Math.random(),
|
|
154
|
-
timestamp: Date.now()
|
|
155
|
-
}));
|
|
156
|
-
return {
|
|
157
|
-
items: items,
|
|
158
|
-
accumulated: items,
|
|
159
|
-
total: items.length,
|
|
160
|
-
count: items.length,
|
|
161
|
-
memory_usage: process.memoryUsage ? process.memoryUsage() : null
|
|
162
|
-
};
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
When("I send {int} requests to {string} with {int}KB payloads", async (_, count: number, request: string, payloadSize: number) => {
|
|
167
|
-
const [method, path] = request.split(" ");
|
|
168
|
-
responses = [];
|
|
169
|
-
|
|
170
|
-
const largePayload = { data: 'x'.repeat(payloadSize * 1024) };
|
|
171
|
-
|
|
172
|
-
for (let i = 0; i < count; i++) {
|
|
173
|
-
const response = await mock.handle(method as any, path, { body: largePayload });
|
|
174
|
-
responses.push(response);
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
Then("all requests should complete without memory errors", () => {
|
|
179
|
-
expect(responses).toHaveLength(20);
|
|
180
|
-
for (const response of responses) {
|
|
181
|
-
expect(response.status).toBe(200);
|
|
182
|
-
expect(response.body.items).toHaveLength(1000);
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
And("each response should confirm large payload processing", () => {
|
|
187
|
-
expect(responses.every(r => r.body.size === 'large')).toBe(true);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
When("I request {string} multiple times", async (_, request: string) => {
|
|
191
|
-
const [method, path] = request.split(" ");
|
|
192
|
-
responses = [];
|
|
193
|
-
|
|
194
|
-
for (let i = 0; i < 5; i++) {
|
|
195
|
-
const response = await mock.handle(method as any, path);
|
|
196
|
-
responses.push(response);
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
Then("each response should contain {int} accumulated items", (_, expectedCount: number) => {
|
|
201
|
-
for (const response of responses) {
|
|
202
|
-
expect(response.body.accumulated).toHaveLength(expectedCount);
|
|
203
|
-
expect(response.body.total).toBe(expectedCount);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
And("all accumulation requests should complete successfully", () => {
|
|
208
|
-
expect(responses).toHaveLength(5);
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
Scenario("Error resilience and recovery", ({ Given, When, Then, And }) => {
|
|
213
|
-
responses = [];
|
|
214
|
-
|
|
215
|
-
Given("I create a mock with intermittent failure simulation", () => {
|
|
216
|
-
mock = schmock();
|
|
217
|
-
let requestCount = 0;
|
|
218
|
-
mock('POST /api/unreliable', ({ body }) => {
|
|
219
|
-
requestCount++;
|
|
220
|
-
if (requestCount % 5 === 0) {
|
|
221
|
-
return [500, { error: 'Simulated server error', request_id: requestCount }];
|
|
222
|
-
}
|
|
223
|
-
return [200, { success: true, data: body, request_id: requestCount }];
|
|
224
|
-
});
|
|
225
|
-
mock('GET /api/flaky', () => {
|
|
226
|
-
requestCount++;
|
|
227
|
-
if (requestCount % 5 === 0) {
|
|
228
|
-
return [500, { error: 'Flaky service error', request_id: requestCount }];
|
|
229
|
-
}
|
|
230
|
-
return [200, { success: true, request_id: requestCount }];
|
|
231
|
-
});
|
|
232
|
-
mock('POST /api/validate-strict', ({ body }) => {
|
|
233
|
-
if (!body || typeof body !== 'object') {
|
|
234
|
-
return [400, { error: 'Request body is required and must be an object', code: 'INVALID_BODY' }];
|
|
235
|
-
}
|
|
236
|
-
const record = body as Record<string, unknown>;
|
|
237
|
-
if (!record.name || typeof record.name !== 'string') {
|
|
238
|
-
return [422, { error: 'Name field is required and must be a string', code: 'INVALID_NAME' }];
|
|
239
|
-
}
|
|
240
|
-
if (!record.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(record.email as string)) {
|
|
241
|
-
return [422, { error: 'Valid email address is required', code: 'INVALID_EMAIL' }];
|
|
242
|
-
}
|
|
243
|
-
return [200, { message: 'Validation successful', data: body }];
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
When("I send {int} requests to {string}", async (_, count: number, request: string) => {
|
|
248
|
-
const [method, path] = request.split(" ");
|
|
249
|
-
responses = [];
|
|
250
|
-
|
|
251
|
-
for (let i = 0; i < count; i++) {
|
|
252
|
-
const response = await mock.handle(method as any, path);
|
|
253
|
-
responses.push(response);
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
Then("some requests should succeed and some should fail", () => {
|
|
258
|
-
const successCount = responses.filter(r => r.status === 200).length;
|
|
259
|
-
const errorCount = responses.filter(r => r.status >= 400).length;
|
|
260
|
-
|
|
261
|
-
expect(successCount).toBeGreaterThan(0);
|
|
262
|
-
expect(errorCount).toBeGreaterThan(0);
|
|
263
|
-
expect(successCount + errorCount).toBe(responses.length);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
And("the success rate should be between {int}% and {int}%", (_, low: number, high: number) => {
|
|
267
|
-
const successCount = responses.filter(r => r.status === 200).length;
|
|
268
|
-
const actualRate = (successCount / responses.length) * 100;
|
|
269
|
-
|
|
270
|
-
expect(actualRate).toBeGreaterThanOrEqual(low);
|
|
271
|
-
expect(actualRate).toBeLessThanOrEqual(high);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
And("error responses should have appropriate status codes", () => {
|
|
275
|
-
const errorResponses = responses.filter(r => r.status >= 400);
|
|
276
|
-
const validErrorCodes = [429, 500, 503];
|
|
277
|
-
|
|
278
|
-
for (const response of errorResponses) {
|
|
279
|
-
expect(validErrorCodes).toContain(response.status);
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
When("I send requests to {string} with various invalid inputs", async (_, request: string) => {
|
|
284
|
-
const [method, path] = request.split(" ");
|
|
285
|
-
responses = [];
|
|
286
|
-
|
|
287
|
-
// Test various invalid scenarios
|
|
288
|
-
const testCases: Schmock.RequestOptions[] = [
|
|
289
|
-
{ headers: {}, body: null }, // No content-type, no body
|
|
290
|
-
{ headers: { 'content-type': 'application/json' }, body: null }, // No body
|
|
291
|
-
{ headers: { 'content-type': 'application/json' }, body: "invalid" }, // Invalid body type
|
|
292
|
-
{ headers: { 'content-type': 'application/json' }, body: {} }, // Missing required field
|
|
293
|
-
];
|
|
294
|
-
|
|
295
|
-
for (const testCase of testCases) {
|
|
296
|
-
const response = await mock.handle(method as any, path, testCase);
|
|
297
|
-
responses.push(response);
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
Then("each error response should have a non-empty error message", () => {
|
|
302
|
-
for (const response of responses) {
|
|
303
|
-
expect(response.status).toBeGreaterThanOrEqual(400);
|
|
304
|
-
expect(response.body.error).toBeDefined();
|
|
305
|
-
expect(typeof response.body.error).toBe('string');
|
|
306
|
-
expect(response.body.error.length).toBeGreaterThan(0);
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
And("each error response should have the correct status code", () => {
|
|
311
|
-
expect(responses[0].status).toBe(400); // No content-type
|
|
312
|
-
expect(responses[1].status).toBe(400); // No body
|
|
313
|
-
expect(responses[2].status).toBe(400); // Invalid body type
|
|
314
|
-
expect(responses[3].status).toBe(422); // Missing required field
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
Scenario("Route matching performance with complex patterns", ({ Given, When, Then, And }) => {
|
|
319
|
-
responses = [];
|
|
320
|
-
|
|
321
|
-
Given("I create a mock with many nested route patterns", () => {
|
|
322
|
-
mock = schmock();
|
|
323
|
-
mock('GET /api/users', () => ({ type: 'users-list' }));
|
|
324
|
-
mock('GET /api/users/:id', ({ params }) => ({ type: 'user', id: params.id }));
|
|
325
|
-
mock('GET /api/users/:userId/posts', ({ params }) => ({ type: 'user-posts', userId: params.userId }));
|
|
326
|
-
mock('GET /api/users/:userId/posts/:postId', ({ params }) => ({
|
|
327
|
-
type: 'user-post',
|
|
328
|
-
userId: params.userId,
|
|
329
|
-
postId: params.postId
|
|
330
|
-
}));
|
|
331
|
-
mock('GET /api/users/:userId/posts/:postId/comments', ({ params }) => ({
|
|
332
|
-
type: 'post-comments',
|
|
333
|
-
userId: params.userId,
|
|
334
|
-
postId: params.postId
|
|
335
|
-
}));
|
|
336
|
-
mock('GET /api/posts', () => ({ type: 'posts-list' }));
|
|
337
|
-
mock('GET /api/posts/:postId', ({ params }) => ({ type: 'post', postId: params.postId }));
|
|
338
|
-
mock('GET /api/posts/:postId/comments/:commentId', ({ params }) => ({
|
|
339
|
-
type: 'comment',
|
|
340
|
-
postId: params.postId,
|
|
341
|
-
commentId: params.commentId
|
|
342
|
-
}));
|
|
343
|
-
mock('GET /static/:category/:file', ({ params }) => ({
|
|
344
|
-
type: 'static',
|
|
345
|
-
category: params.category,
|
|
346
|
-
file: params.file
|
|
347
|
-
}));
|
|
348
|
-
mock('GET /api/v2/users/:userId', ({ params }) => ({
|
|
349
|
-
type: 'versioned-user',
|
|
350
|
-
userId: params.userId,
|
|
351
|
-
version: 'v2'
|
|
352
|
-
}));
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
When("I send requests to all route patterns simultaneously", async () => {
|
|
356
|
-
const testPaths = [
|
|
357
|
-
"GET /api/users",
|
|
358
|
-
"GET /api/users/123",
|
|
359
|
-
"GET /api/users/123/posts",
|
|
360
|
-
"GET /api/users/123/posts/456",
|
|
361
|
-
"GET /api/users/123/posts/456/comments",
|
|
362
|
-
"GET /api/posts",
|
|
363
|
-
"GET /api/posts/789",
|
|
364
|
-
"GET /api/posts/789/comments/101",
|
|
365
|
-
"GET /static/images/logo.png",
|
|
366
|
-
"GET /api/v2/users/456"
|
|
367
|
-
];
|
|
368
|
-
|
|
369
|
-
const promises = testPaths.map(async (request) => {
|
|
370
|
-
const [method, path] = request.split(" ");
|
|
371
|
-
return await mock.handle(method as any, path);
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
responses = await Promise.all(promises);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
Then("each request should match the correct route pattern", () => {
|
|
378
|
-
expect(responses[0].body.type).toBe("users-list");
|
|
379
|
-
expect(responses[1].body.type).toBe("user");
|
|
380
|
-
expect(responses[2].body.type).toBe("user-posts");
|
|
381
|
-
expect(responses[3].body.type).toBe("user-post");
|
|
382
|
-
expect(responses[4].body.type).toBe("post-comments");
|
|
383
|
-
expect(responses[5].body.type).toBe("posts-list");
|
|
384
|
-
expect(responses[6].body.type).toBe("post");
|
|
385
|
-
expect(responses[7].body.type).toBe("comment");
|
|
386
|
-
expect(responses[8].body.type).toBe("static");
|
|
387
|
-
expect(responses[9].body.type).toBe("versioned-user");
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
And("parameter extraction should work correctly for all patterns", () => {
|
|
391
|
-
expect(responses[1].body.id).toBe("123");
|
|
392
|
-
expect(responses[2].body.userId).toBe("123");
|
|
393
|
-
expect(responses[3].body.userId).toBe("123");
|
|
394
|
-
expect(responses[3].body.postId).toBe("456");
|
|
395
|
-
expect(responses[8].body.category).toBe("images");
|
|
396
|
-
expect(responses[8].body.file).toBe("logo.png");
|
|
397
|
-
expect(responses[9].body.version).toBe("v2");
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
When("I send requests to non-matching paths", async () => {
|
|
401
|
-
const invalidPaths = [
|
|
402
|
-
"GET /api/invalid",
|
|
403
|
-
"GET /users", // Missing /api prefix
|
|
404
|
-
"GET /api/users/123/invalid",
|
|
405
|
-
"POST /static/images/test.jpg" // Wrong method
|
|
406
|
-
];
|
|
407
|
-
|
|
408
|
-
responses = [];
|
|
409
|
-
for (const request of invalidPaths) {
|
|
410
|
-
const [method, path] = request.split(" ");
|
|
411
|
-
const response = await mock.handle(method as any, path);
|
|
412
|
-
responses.push(response);
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
Then("they should consistently return {int} responses", (_, expectedStatus: number) => {
|
|
417
|
-
for (const response of responses) {
|
|
418
|
-
expect(response.status).toBe(expectedStatus);
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
});
|