@schmock/core 2.2.2 → 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,260 +0,0 @@
|
|
|
1
|
-
/// <reference path="../../schmock.d.ts" />
|
|
2
|
-
|
|
3
|
-
import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
|
|
4
|
-
import { expect, vi } from "vitest";
|
|
5
|
-
import { schmock } from "../index.js";
|
|
6
|
-
|
|
7
|
-
const feature = await loadFeature("../../features/fetch-interceptor.feature");
|
|
8
|
-
|
|
9
|
-
describeFeature(feature, ({ Scenario, AfterEachScenario }) => {
|
|
10
|
-
let mock: Schmock.CallableMockInstance;
|
|
11
|
-
let handle: Schmock.InterceptHandle | undefined;
|
|
12
|
-
let originalFetch: typeof globalThis.fetch;
|
|
13
|
-
let savedFetch: ReturnType<typeof vi.fn>;
|
|
14
|
-
let fetchResponse: Response | undefined;
|
|
15
|
-
|
|
16
|
-
function setup() {
|
|
17
|
-
originalFetch = globalThis.fetch;
|
|
18
|
-
savedFetch = vi.fn().mockResolvedValue(new Response("real backend"));
|
|
19
|
-
globalThis.fetch = savedFetch;
|
|
20
|
-
mock = schmock();
|
|
21
|
-
fetchResponse = undefined;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
AfterEachScenario(() => {
|
|
25
|
-
handle?.restore();
|
|
26
|
-
handle = undefined;
|
|
27
|
-
globalThis.fetch = originalFetch;
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
Scenario("Intercept a matched fetch request", ({ Given, When, Then, And }) => {
|
|
31
|
-
Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
|
|
32
|
-
setup();
|
|
33
|
-
mock("GET /api/users", [{ id: 1, name: "Alice" }]);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
And("fetch is intercepted", () => {
|
|
37
|
-
handle = mock.intercept();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
When("I fetch \"/api/users\"", async () => {
|
|
41
|
-
fetchResponse = await fetch("http://localhost/api/users");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
Then("the fetch response status should be 200", () => {
|
|
45
|
-
expect(fetchResponse?.status).toBe(200);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
And("the fetch response body should be the mocked users", async () => {
|
|
49
|
-
const body = await fetchResponse?.json();
|
|
50
|
-
expect(body).toEqual([{ id: 1, name: "Alice" }]);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
Scenario("Passthrough for unmatched routes", ({ Given, When, Then, And }) => {
|
|
55
|
-
Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
|
|
56
|
-
setup();
|
|
57
|
-
mock("GET /api/users", [{ id: 1 }]);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
And("fetch is intercepted with passthrough enabled", () => {
|
|
61
|
-
handle = mock.intercept({ passthrough: true });
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
When("I fetch \"/api/other\"", async () => {
|
|
65
|
-
fetchResponse = await fetch("http://localhost/api/other");
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
Then("the original fetch should have been called", () => {
|
|
69
|
-
expect(savedFetch).toHaveBeenCalled();
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
Scenario("Passthrough disabled returns 404", ({ Given, When, Then, And }) => {
|
|
74
|
-
Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
|
|
75
|
-
setup();
|
|
76
|
-
mock("GET /api/users", [{ id: 1 }]);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
And("fetch is intercepted with passthrough disabled", () => {
|
|
80
|
-
handle = mock.intercept({ passthrough: false });
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
When("I fetch \"/api/other\"", async () => {
|
|
84
|
-
fetchResponse = await fetch("http://localhost/api/other");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
Then("the fetch response status should be 404", () => {
|
|
88
|
-
expect(fetchResponse?.status).toBe(404);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
Scenario("Restore puts original fetch back", ({ Given, When, Then, And }) => {
|
|
93
|
-
let savedRef: typeof globalThis.fetch;
|
|
94
|
-
|
|
95
|
-
Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
|
|
96
|
-
setup();
|
|
97
|
-
mock("GET /api/users", [{ id: 1 }]);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
And("fetch is intercepted", () => {
|
|
101
|
-
savedRef = globalThis.fetch;
|
|
102
|
-
handle = mock.intercept();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
When("I restore the interceptor", () => {
|
|
106
|
-
handle?.restore();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
Then("globalThis.fetch should be the original function", () => {
|
|
110
|
-
expect(globalThis.fetch).toBe(savedRef);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
Scenario("BaseUrl filters which requests are intercepted", ({ Given, When, Then, And }) => {
|
|
115
|
-
Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
|
|
116
|
-
setup();
|
|
117
|
-
mock("GET /api/users", [{ id: 1 }]);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
And("fetch is intercepted with baseUrl \"/api\"", () => {
|
|
121
|
-
handle = mock.intercept({ baseUrl: "/api" });
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
When("I fetch \"/other/endpoint\"", async () => {
|
|
125
|
-
await fetch("http://localhost/other/endpoint");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
Then("the original fetch should have been called", () => {
|
|
129
|
-
expect(savedFetch).toHaveBeenCalled();
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
Scenario("Origin-form baseUrl intercepts matching origin only", ({ Given, When, Then, And }) => {
|
|
134
|
-
Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
|
|
135
|
-
setup();
|
|
136
|
-
mock("GET /api/users", [{ id: 1 }]);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
And("fetch is intercepted with baseUrl \"https://api.example.com\"", () => {
|
|
140
|
-
handle = mock.intercept({ baseUrl: "https://api.example.com" });
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
When("I fetch \"https://api.example.com/api/users\"", async () => {
|
|
144
|
-
fetchResponse = await fetch("https://api.example.com/api/users");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
Then("the response status should be {int}", (_, status: number) => {
|
|
148
|
-
expect(fetchResponse?.status).toBe(status);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
When("I fetch \"https://other.example.com/api/users\"", async () => {
|
|
152
|
-
await fetch("https://other.example.com/api/users");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
Then("the original fetch should have been called", () => {
|
|
156
|
-
expect(savedFetch).toHaveBeenCalled();
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
Scenario("Origin-form baseUrl with a path prefix requires both to match", ({ Given, When, Then, And }) => {
|
|
161
|
-
Given("a Schmock instance with route \"GET /v1/users\" returning users", () => {
|
|
162
|
-
setup();
|
|
163
|
-
mock("GET /v1/users", [{ id: 1 }]);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
And("fetch is intercepted with baseUrl \"https://api.example.com/v1\"", () => {
|
|
167
|
-
handle = mock.intercept({ baseUrl: "https://api.example.com/v1" });
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
When("I fetch \"https://api.example.com/v1/users\"", async () => {
|
|
171
|
-
fetchResponse = await fetch("https://api.example.com/v1/users");
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
Then("the response status should be {int}", (_, status: number) => {
|
|
175
|
-
expect(fetchResponse?.status).toBe(status);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
When("I fetch \"https://api.example.com/users\"", async () => {
|
|
179
|
-
await fetch("https://api.example.com/users");
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
Then("the original fetch should have been called", () => {
|
|
183
|
-
expect(savedFetch).toHaveBeenCalled();
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
Scenario("beforeRequest hook modifies the request", ({ Given, When, Then, And }) => {
|
|
188
|
-
Given("a Schmock instance with route \"GET /api/users\" that reads headers", () => {
|
|
189
|
-
setup();
|
|
190
|
-
mock("GET /api/users", ({ headers }) => [200, { token: headers["x-token"] }]);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
And("fetch is intercepted with a beforeRequest hook that adds a header", () => {
|
|
194
|
-
handle = mock.intercept({
|
|
195
|
-
beforeRequest: (req) => ({
|
|
196
|
-
...req,
|
|
197
|
-
headers: { ...req.headers, "x-token": "injected" },
|
|
198
|
-
}),
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
When("I fetch \"/api/users\"", async () => {
|
|
203
|
-
fetchResponse = await fetch("http://localhost/api/users");
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
Then("the response should contain the injected header value", async () => {
|
|
207
|
-
const body = await fetchResponse?.json();
|
|
208
|
-
expect(body).toEqual({ token: "injected" });
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
Scenario("beforeResponse hook modifies the response", ({ Given, When, Then, And }) => {
|
|
213
|
-
Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
|
|
214
|
-
setup();
|
|
215
|
-
mock("GET /api/users", [{ id: 1 }]);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
And("fetch is intercepted with a beforeResponse hook that adds a header", () => {
|
|
219
|
-
handle = mock.intercept({
|
|
220
|
-
beforeResponse: (res) => ({
|
|
221
|
-
...res,
|
|
222
|
-
headers: { ...res.headers, "x-mock": "true" },
|
|
223
|
-
}),
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
When("I fetch \"/api/users\"", async () => {
|
|
228
|
-
fetchResponse = await fetch("http://localhost/api/users");
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
Then("the fetch response should have the injected header", () => {
|
|
232
|
-
expect(fetchResponse?.headers.get("x-mock")).toBe("true");
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
Scenario("Double intercept throws an error", ({ Given, When, Then, And }) => {
|
|
237
|
-
let error: Error | undefined;
|
|
238
|
-
|
|
239
|
-
Given("a Schmock instance with route \"GET /api/users\" returning users", () => {
|
|
240
|
-
setup();
|
|
241
|
-
mock("GET /api/users", [{ id: 1 }]);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
And("fetch is intercepted", () => {
|
|
245
|
-
handle = mock.intercept();
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
When("I try to intercept again", () => {
|
|
249
|
-
try {
|
|
250
|
-
mock.intercept();
|
|
251
|
-
} catch (e) {
|
|
252
|
-
error = e as Error;
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
Then("it should throw an error about already intercepting", () => {
|
|
257
|
-
expect(error?.message).toMatch(/already intercepting/i);
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
});
|
|
@@ -1,353 +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
|
-
interface CartItem {
|
|
7
|
-
id: number;
|
|
8
|
-
name: string;
|
|
9
|
-
price: number;
|
|
10
|
-
quantity: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface UserRecord {
|
|
14
|
-
username: string;
|
|
15
|
-
password: string;
|
|
16
|
-
profile: { name: string; role: string };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface SessionRecord {
|
|
20
|
-
user: string;
|
|
21
|
-
profile: { name: string; role: string };
|
|
22
|
-
loginTime: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const feature = await loadFeature("../../features/stateful-workflows.feature");
|
|
26
|
-
|
|
27
|
-
describeFeature(feature, ({ Scenario }) => {
|
|
28
|
-
let mock: CallableMockInstance;
|
|
29
|
-
let response: any;
|
|
30
|
-
let sessionToken: string;
|
|
31
|
-
let addedItemIds: number[] = [];
|
|
32
|
-
|
|
33
|
-
Scenario("Shopping cart workflow", ({ Given, When, Then, And }) => {
|
|
34
|
-
addedItemIds = [];
|
|
35
|
-
|
|
36
|
-
Given("I create a stateful shopping cart mock", () => {
|
|
37
|
-
const items: CartItem[] = [];
|
|
38
|
-
let total = 0;
|
|
39
|
-
|
|
40
|
-
mock = schmock();
|
|
41
|
-
mock("GET /cart", () => ({
|
|
42
|
-
items,
|
|
43
|
-
total,
|
|
44
|
-
count: items.length,
|
|
45
|
-
}));
|
|
46
|
-
mock("POST /cart/add", ({ body }) => {
|
|
47
|
-
const b = body as Record<string, unknown>;
|
|
48
|
-
const item: CartItem = {
|
|
49
|
-
id: Date.now(),
|
|
50
|
-
name: b.name as string,
|
|
51
|
-
price: b.price as number,
|
|
52
|
-
quantity: (b.quantity as number) || 1,
|
|
53
|
-
};
|
|
54
|
-
items.push(item);
|
|
55
|
-
total += item.price * item.quantity;
|
|
56
|
-
return {
|
|
57
|
-
message: "Item added to cart",
|
|
58
|
-
added: item,
|
|
59
|
-
cart: {
|
|
60
|
-
items,
|
|
61
|
-
total,
|
|
62
|
-
count: items.length,
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
});
|
|
66
|
-
mock("DELETE /cart/:id", ({ params }) => {
|
|
67
|
-
const itemId = parseInt(params.id);
|
|
68
|
-
const itemIndex = items.findIndex((item) => item.id === itemId);
|
|
69
|
-
if (itemIndex === -1) {
|
|
70
|
-
return [404, { error: "Item not found in cart" }];
|
|
71
|
-
}
|
|
72
|
-
const removedItem = items[itemIndex];
|
|
73
|
-
total -= removedItem.price * removedItem.quantity;
|
|
74
|
-
items.splice(itemIndex, 1);
|
|
75
|
-
return {
|
|
76
|
-
message: "Item removed from cart",
|
|
77
|
-
item: removedItem,
|
|
78
|
-
cart: {
|
|
79
|
-
items,
|
|
80
|
-
total,
|
|
81
|
-
count: items.length,
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
});
|
|
85
|
-
mock("POST /cart/clear", () => {
|
|
86
|
-
items.length = 0;
|
|
87
|
-
total = 0;
|
|
88
|
-
return {
|
|
89
|
-
message: "Cart cleared",
|
|
90
|
-
cart: {
|
|
91
|
-
items,
|
|
92
|
-
total,
|
|
93
|
-
count: items.length,
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
When("I request {string}", async (_, request: string) => {
|
|
100
|
-
const [method, path] = request.split(" ");
|
|
101
|
-
response = await mock.handle(method as any, path);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
Then("the response should show an empty cart with count {int}", (_, expectedCount: number) => {
|
|
105
|
-
expect(response.body.count).toBe(expectedCount);
|
|
106
|
-
expect(response.body.items).toEqual([]);
|
|
107
|
-
expect(response.body.total).toBe(0);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
When("I add item with name {string} and price {string}", async (_, itemName: string, priceStr: string) => {
|
|
111
|
-
const price = parseFloat(priceStr);
|
|
112
|
-
response = await mock.handle("POST", "/cart/add", {
|
|
113
|
-
body: { name: itemName, price }
|
|
114
|
-
});
|
|
115
|
-
addedItemIds.push(response.body.added.id);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
And("I add second item with name {string} and price {string}", async (_, itemName: string, priceStr: string) => {
|
|
119
|
-
const price = parseFloat(priceStr);
|
|
120
|
-
response = await mock.handle("POST", "/cart/add", {
|
|
121
|
-
body: { name: itemName, price }
|
|
122
|
-
});
|
|
123
|
-
addedItemIds.push(response.body.added.id);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
Then("the cart should contain {int} items", async (_, expectedCount: number) => {
|
|
127
|
-
const cartResponse = await mock.handle("GET", "/cart");
|
|
128
|
-
const body = cartResponse.body as { items: CartItem[]; total: number; count: number };
|
|
129
|
-
expect(body.count).toBe(expectedCount);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
Then("the cart should contain {int} item", async (_, expectedCount: number) => {
|
|
133
|
-
const cartResponse = await mock.handle("GET", "/cart");
|
|
134
|
-
const body = cartResponse.body as { items: CartItem[]; total: number; count: number };
|
|
135
|
-
expect(body.count).toBe(expectedCount);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
And("the initial cart total should be {string}", async (_, expectedTotalStr: string) => {
|
|
139
|
-
const expectedTotal = parseFloat(expectedTotalStr);
|
|
140
|
-
const cartResponse = await mock.handle("GET", "/cart");
|
|
141
|
-
const body = cartResponse.body as { items: CartItem[]; total: number; count: number };
|
|
142
|
-
expect(body.total).toBe(expectedTotal);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
And("the final cart total should be {string}", async (_, expectedTotalStr: string) => {
|
|
146
|
-
const expectedTotal = parseFloat(expectedTotalStr);
|
|
147
|
-
const cartResponse = await mock.handle("GET", "/cart");
|
|
148
|
-
const body = cartResponse.body as { items: CartItem[]; total: number; count: number };
|
|
149
|
-
expect(body.total).toBe(expectedTotal);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
When("I remove the first item from the cart", async () => {
|
|
153
|
-
const cartResponse = await mock.handle("GET", "/cart");
|
|
154
|
-
const body = cartResponse.body as { items: CartItem[]; total: number; count: number };
|
|
155
|
-
const firstItemId = body.items[0].id;
|
|
156
|
-
response = await mock.handle("DELETE", `/cart/${firstItemId}`);
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
Scenario("User session simulation", ({ Given, When, Then, And }) => {
|
|
161
|
-
sessionToken = "";
|
|
162
|
-
|
|
163
|
-
Given("I create a session-based authentication mock", () => {
|
|
164
|
-
const users: UserRecord[] = [
|
|
165
|
-
{ username: "admin", password: "secret", profile: { name: "Admin User", role: "administrator" } },
|
|
166
|
-
{ username: "user1", password: "pass123", profile: { name: "Regular User", role: "user" } },
|
|
167
|
-
];
|
|
168
|
-
const sessions: Record<string, SessionRecord> = {};
|
|
169
|
-
|
|
170
|
-
mock = schmock();
|
|
171
|
-
mock("POST /auth/login", ({ body }) => {
|
|
172
|
-
const b = body as Record<string, unknown>;
|
|
173
|
-
const user = users.find((u) => u.username === b.username && u.password === b.password);
|
|
174
|
-
if (!user) {
|
|
175
|
-
return [401, { error: "Invalid credentials" }];
|
|
176
|
-
}
|
|
177
|
-
const token = "token-" + user.username + "-" + Date.now();
|
|
178
|
-
sessions[token] = {
|
|
179
|
-
user: user.username,
|
|
180
|
-
profile: user.profile,
|
|
181
|
-
loginTime: new Date().toISOString(),
|
|
182
|
-
};
|
|
183
|
-
return {
|
|
184
|
-
message: "Login successful",
|
|
185
|
-
token,
|
|
186
|
-
user: user.profile,
|
|
187
|
-
};
|
|
188
|
-
});
|
|
189
|
-
mock("GET /profile", ({ headers }) => {
|
|
190
|
-
const token = headers.authorization ? headers.authorization.replace("Bearer ", "") : "";
|
|
191
|
-
if (!token || !sessions[token]) {
|
|
192
|
-
return [401, { error: "Unauthorized" }];
|
|
193
|
-
}
|
|
194
|
-
const session = sessions[token];
|
|
195
|
-
return {
|
|
196
|
-
user: session.user,
|
|
197
|
-
profile: session.profile,
|
|
198
|
-
loginTime: session.loginTime,
|
|
199
|
-
session: {
|
|
200
|
-
active: true,
|
|
201
|
-
},
|
|
202
|
-
};
|
|
203
|
-
});
|
|
204
|
-
mock("POST /auth/logout", ({ headers }) => {
|
|
205
|
-
const token = headers.authorization ? headers.authorization.replace("Bearer ", "") : "";
|
|
206
|
-
if (!token || !sessions[token]) {
|
|
207
|
-
return [401, { error: "Unauthorized" }];
|
|
208
|
-
}
|
|
209
|
-
delete sessions[token];
|
|
210
|
-
return { message: "Logged out successfully" };
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
When("I login with username {string} and password {string}", async (_, username: string, password: string) => {
|
|
215
|
-
response = await mock.handle("POST", "/auth/login", {
|
|
216
|
-
body: { username, password }
|
|
217
|
-
});
|
|
218
|
-
if (response.body.token) {
|
|
219
|
-
sessionToken = response.body.token;
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
Then("I should receive a session token", () => {
|
|
224
|
-
expect(response.body.token).toBeDefined();
|
|
225
|
-
expect(typeof response.body.token).toBe('string');
|
|
226
|
-
expect(response.body.token.length).toBeGreaterThan(0);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
And("the response should contain user info with role {string}", (_, expectedRole: string) => {
|
|
230
|
-
expect(response.body.user).toBeDefined();
|
|
231
|
-
expect(response.body.user.role).toBe(expectedRole);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
When("I request {string} with the session token", async (_, path: string) => {
|
|
235
|
-
response = await mock.handle("GET", path, {
|
|
236
|
-
headers: { authorization: `Bearer ${sessionToken}` }
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
Then("I should get my profile information", () => {
|
|
241
|
-
expect(response.body.user).toBeDefined();
|
|
242
|
-
expect(response.body.session).toBeDefined();
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
And("the session should be marked as active", () => {
|
|
246
|
-
expect(response.body.session.active).toBe(true);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
When("I logout with the session token", async () => {
|
|
250
|
-
response = await mock.handle("POST", "/auth/logout", {
|
|
251
|
-
headers: { authorization: `Bearer ${sessionToken}` }
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
Then("the logout should be successful", () => {
|
|
256
|
-
expect(response.body.message).toContain("Logged out successfully");
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
When("I request {string} with the same token", async (_, path: string) => {
|
|
260
|
-
response = await mock.handle("GET", path, {
|
|
261
|
-
headers: { authorization: `Bearer ${sessionToken}` }
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
Then("I should get a {int} unauthorized response", (_, expectedStatus: number) => {
|
|
266
|
-
expect(response.status).toBe(expectedStatus);
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
Scenario("Multi-user state isolation", ({ Given, When, Then, And }) => {
|
|
271
|
-
Given("I create a multi-user counter mock", () => {
|
|
272
|
-
const counters: Record<string, number> = {};
|
|
273
|
-
|
|
274
|
-
mock = schmock();
|
|
275
|
-
mock("POST /counter/:userId/increment", ({ params }) => {
|
|
276
|
-
const userId = params.userId;
|
|
277
|
-
if (!counters[userId]) {
|
|
278
|
-
counters[userId] = 0;
|
|
279
|
-
}
|
|
280
|
-
counters[userId]++;
|
|
281
|
-
return {
|
|
282
|
-
userId,
|
|
283
|
-
count: counters[userId],
|
|
284
|
-
message: "Counter incremented for user " + userId,
|
|
285
|
-
};
|
|
286
|
-
});
|
|
287
|
-
mock("GET /counter/:userId", ({ params }) => {
|
|
288
|
-
const userId = params.userId;
|
|
289
|
-
const count = counters[userId] || 0;
|
|
290
|
-
return {
|
|
291
|
-
userId,
|
|
292
|
-
count,
|
|
293
|
-
};
|
|
294
|
-
});
|
|
295
|
-
mock("GET /counters/summary", () => {
|
|
296
|
-
const totalCount = Object.values(counters).reduce((sum, count) => sum + count, 0);
|
|
297
|
-
const userCount = Object.keys(counters).length;
|
|
298
|
-
return {
|
|
299
|
-
totalCounts: totalCount,
|
|
300
|
-
totalUsers: userCount,
|
|
301
|
-
counters,
|
|
302
|
-
};
|
|
303
|
-
});
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
When("user {string} increments their counter {int} times", async (_, userId: string, times: number) => {
|
|
307
|
-
for (let i = 0; i < times; i++) {
|
|
308
|
-
await mock.handle("POST", `/counter/${userId}/increment`);
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
And("user {string} increments their counter {int} times", async (_, userId: string, times: number) => {
|
|
313
|
-
for (let i = 0; i < times; i++) {
|
|
314
|
-
await mock.handle("POST", `/counter/${userId}/increment`);
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
Then("{string}'s counter should be {int}", async (_, userId: string, expectedCount: number) => {
|
|
319
|
-
const response = await mock.handle("GET", `/counter/${userId}`);
|
|
320
|
-
const body = response.body as { userId: string; count: number };
|
|
321
|
-
expect(body.count).toBe(expectedCount);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
And("{string}'s counter should be {int}", async (_, userId: string, expectedCount: number) => {
|
|
325
|
-
const response = await mock.handle("GET", `/counter/${userId}`);
|
|
326
|
-
const body = response.body as { userId: string; count: number };
|
|
327
|
-
expect(body.count).toBe(expectedCount);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
And("the summary should show {int} total users", async (_, expectedUsers: number) => {
|
|
331
|
-
const response = await mock.handle("GET", "/counters/summary");
|
|
332
|
-
const body = response.body as { totalCounts: number; totalUsers: number; counters: Record<string, number> };
|
|
333
|
-
expect(body.totalUsers).toBe(expectedUsers);
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
And("the summary should show total counts of {int}", async (_, expectedTotal: number) => {
|
|
337
|
-
const response = await mock.handle("GET", "/counters/summary");
|
|
338
|
-
const body = response.body as { totalCounts: number; totalUsers: number; counters: Record<string, number> };
|
|
339
|
-
expect(body.totalCounts).toBe(expectedTotal);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
And("each user's state should be independent", async () => {
|
|
343
|
-
const aliceResponse = await mock.handle("GET", "/counter/alice");
|
|
344
|
-
const bobResponse = await mock.handle("GET", "/counter/bob");
|
|
345
|
-
const aliceBody = aliceResponse.body as { userId: string; count: number };
|
|
346
|
-
const bobBody = bobResponse.body as { userId: string; count: number };
|
|
347
|
-
|
|
348
|
-
expect(aliceBody.count).toBe(3);
|
|
349
|
-
expect(bobBody.count).toBe(2);
|
|
350
|
-
expect(aliceBody.count).not.toBe(bobBody.count);
|
|
351
|
-
});
|
|
352
|
-
});
|
|
353
|
-
});
|