@schmock/core 1.0.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/builder.d.ts +62 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +432 -0
- package/dist/errors.d.ts +56 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +92 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/parser.d.ts +19 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +40 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/package.json +39 -0
- package/src/builder.d.ts.map +1 -0
- package/src/builder.test.ts +289 -0
- package/src/builder.ts +580 -0
- package/src/debug.test.ts +241 -0
- package/src/delay.test.ts +319 -0
- package/src/errors.d.ts.map +1 -0
- package/src/errors.test.ts +223 -0
- package/src/errors.ts +124 -0
- package/src/factory.test.ts +133 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.ts +80 -0
- package/src/namespace.test.ts +273 -0
- package/src/parser.d.ts.map +1 -0
- package/src/parser.test.ts +131 -0
- package/src/parser.ts +61 -0
- package/src/plugin-system.test.ts +511 -0
- package/src/response-parsing.test.ts +255 -0
- package/src/route-matching.test.ts +351 -0
- package/src/smart-defaults.test.ts +361 -0
- package/src/steps/async-support.steps.ts +427 -0
- package/src/steps/basic-usage.steps.ts +316 -0
- package/src/steps/developer-experience.steps.ts +439 -0
- package/src/steps/error-handling.steps.ts +387 -0
- package/src/steps/fluent-api.steps.ts +252 -0
- package/src/steps/http-methods.steps.ts +397 -0
- package/src/steps/performance-reliability.steps.ts +459 -0
- package/src/steps/plugin-integration.steps.ts +279 -0
- package/src/steps/route-key-format.steps.ts +118 -0
- package/src/steps/state-concurrency.steps.ts +643 -0
- package/src/steps/stateful-workflows.steps.ts +351 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.ts +17 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
|
|
2
|
+
import { expect } from "vitest";
|
|
3
|
+
import { schmock } from "../index";
|
|
4
|
+
import type { MockInstance } from "../types";
|
|
5
|
+
|
|
6
|
+
const feature = await loadFeature("../../features/stateful-workflows.feature");
|
|
7
|
+
|
|
8
|
+
describeFeature(feature, ({ Scenario }) => {
|
|
9
|
+
let mock: MockInstance<any>;
|
|
10
|
+
let response: any;
|
|
11
|
+
let sessionToken: string;
|
|
12
|
+
let addedItemIds: number[] = [];
|
|
13
|
+
|
|
14
|
+
Scenario("Shopping cart workflow", ({ Given, When, Then, And }) => {
|
|
15
|
+
addedItemIds = [];
|
|
16
|
+
|
|
17
|
+
Given("I create a stateful shopping cart mock:", (_, docString: string) => {
|
|
18
|
+
// Create stateful shopping cart mock with new callable API
|
|
19
|
+
const sharedState = { items: [], total: 0 };
|
|
20
|
+
mock = schmock({ state: sharedState });
|
|
21
|
+
|
|
22
|
+
mock('GET /cart', ({ state }) => ({
|
|
23
|
+
items: state.items,
|
|
24
|
+
total: state.total,
|
|
25
|
+
count: state.items.length
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
mock('POST /cart/add', ({ state, body }) => {
|
|
29
|
+
const item = {
|
|
30
|
+
id: Date.now(),
|
|
31
|
+
name: body.name,
|
|
32
|
+
price: body.price,
|
|
33
|
+
quantity: body.quantity || 1
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
state.items.push(item);
|
|
37
|
+
state.total += item.price * item.quantity;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
message: 'Item added to cart',
|
|
41
|
+
added: item,
|
|
42
|
+
cart: {
|
|
43
|
+
items: state.items,
|
|
44
|
+
total: state.total,
|
|
45
|
+
count: state.items.length
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
mock('DELETE /cart/:id', ({ state, params }) => {
|
|
51
|
+
const itemId = parseInt(params.id);
|
|
52
|
+
const itemIndex = state.items.findIndex(item => item.id === itemId);
|
|
53
|
+
|
|
54
|
+
if (itemIndex === -1) {
|
|
55
|
+
return [404, { error: 'Item not found in cart' }];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const removedItem = state.items[itemIndex];
|
|
59
|
+
state.total -= removedItem.price * removedItem.quantity;
|
|
60
|
+
state.items.splice(itemIndex, 1);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
message: 'Item removed from cart',
|
|
64
|
+
item: removedItem,
|
|
65
|
+
cart: {
|
|
66
|
+
items: state.items,
|
|
67
|
+
total: state.total,
|
|
68
|
+
count: state.items.length
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
mock('POST /cart/clear', ({ state }) => {
|
|
74
|
+
state.items = [];
|
|
75
|
+
state.total = 0;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
message: 'Cart cleared',
|
|
79
|
+
cart: {
|
|
80
|
+
items: state.items,
|
|
81
|
+
total: state.total,
|
|
82
|
+
count: state.items.length
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
When("I request {string}", async (_, request: string) => {
|
|
89
|
+
const [method, path] = request.split(" ");
|
|
90
|
+
response = await mock.handle(method as any, path);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
Then("the response should show an empty cart with count {int}", (_, expectedCount: number) => {
|
|
94
|
+
expect(response.body.count).toBe(expectedCount);
|
|
95
|
+
expect(response.body.items).toEqual([]);
|
|
96
|
+
expect(response.body.total).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
When("I add item with name {string} and price {string}", async (_, itemName: string, priceStr: string) => {
|
|
100
|
+
const price = parseFloat(priceStr);
|
|
101
|
+
response = await mock.handle("POST", "/cart/add", {
|
|
102
|
+
body: { name: itemName, price }
|
|
103
|
+
});
|
|
104
|
+
addedItemIds.push(response.body.added.id);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
And("I add second item with name {string} and price {string}", async (_, itemName: string, priceStr: string) => {
|
|
108
|
+
const price = parseFloat(priceStr);
|
|
109
|
+
response = await mock.handle("POST", "/cart/add", {
|
|
110
|
+
body: { name: itemName, price }
|
|
111
|
+
});
|
|
112
|
+
addedItemIds.push(response.body.added.id);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
Then("the cart should contain {int} items", async (_, expectedCount: number) => {
|
|
116
|
+
const cartResponse = await mock.handle("GET", "/cart");
|
|
117
|
+
expect(cartResponse.body.count).toBe(expectedCount);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
Then("the cart should contain {int} item", async (_, expectedCount: number) => {
|
|
121
|
+
const cartResponse = await mock.handle("GET", "/cart");
|
|
122
|
+
expect(cartResponse.body.count).toBe(expectedCount);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
And("the initial cart total should be {string}", async (_, expectedTotalStr: string) => {
|
|
126
|
+
const expectedTotal = parseFloat(expectedTotalStr);
|
|
127
|
+
const cartResponse = await mock.handle("GET", "/cart");
|
|
128
|
+
expect(cartResponse.body.total).toBe(expectedTotal);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
And("the final cart total should be {string}", async (_, expectedTotalStr: string) => {
|
|
132
|
+
const expectedTotal = parseFloat(expectedTotalStr);
|
|
133
|
+
const cartResponse = await mock.handle("GET", "/cart");
|
|
134
|
+
expect(cartResponse.body.total).toBe(expectedTotal);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
When("I remove the first item from the cart", async () => {
|
|
138
|
+
const cartResponse = await mock.handle("GET", "/cart");
|
|
139
|
+
const firstItemId = cartResponse.body.items[0].id;
|
|
140
|
+
response = await mock.handle("DELETE", `/cart/${firstItemId}`);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
Scenario("User session simulation", ({ Given, When, Then, And }) => {
|
|
145
|
+
sessionToken = "";
|
|
146
|
+
|
|
147
|
+
Given("I create a session-based mock:", (_, docString: string) => {
|
|
148
|
+
// Create session-based mock with new callable API
|
|
149
|
+
const sharedState = {
|
|
150
|
+
users: [
|
|
151
|
+
{ username: 'admin', password: 'secret', profile: { name: 'Admin User', role: 'administrator' } },
|
|
152
|
+
{ username: 'user1', password: 'pass123', profile: { name: 'Regular User', role: 'user' } }
|
|
153
|
+
],
|
|
154
|
+
sessions: {}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
mock = schmock({ state: sharedState });
|
|
158
|
+
|
|
159
|
+
mock('POST /auth/login', ({ state, body }) => {
|
|
160
|
+
const user = state.users.find(u => u.username === body.username && u.password === body.password);
|
|
161
|
+
|
|
162
|
+
if (!user) {
|
|
163
|
+
return [401, { error: 'Invalid credentials' }];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const token = `token-${user.username}-${Date.now()}`;
|
|
167
|
+
state.sessions[token] = {
|
|
168
|
+
user: user.username,
|
|
169
|
+
profile: user.profile,
|
|
170
|
+
loginTime: new Date().toISOString()
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
message: 'Login successful',
|
|
175
|
+
token: token,
|
|
176
|
+
user: user.profile
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
mock('GET /profile', ({ state, headers }) => {
|
|
181
|
+
const token = headers.authorization?.replace('Bearer ', '');
|
|
182
|
+
|
|
183
|
+
if (!token || !state.sessions[token]) {
|
|
184
|
+
return [401, { error: 'Unauthorized' }];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const session = state.sessions[token];
|
|
188
|
+
return {
|
|
189
|
+
user: session.user,
|
|
190
|
+
profile: session.profile,
|
|
191
|
+
loginTime: session.loginTime,
|
|
192
|
+
session: {
|
|
193
|
+
active: true
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
mock('POST /auth/logout', ({ state, headers }) => {
|
|
199
|
+
const token = headers.authorization?.replace('Bearer ', '');
|
|
200
|
+
|
|
201
|
+
if (!token || !state.sessions[token]) {
|
|
202
|
+
return [401, { error: 'Unauthorized' }];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
delete state.sessions[token];
|
|
206
|
+
return { message: 'Logged out successfully' };
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
When("I login with username {string} and password {string}", async (_, username: string, password: string) => {
|
|
211
|
+
response = await mock.handle("POST", "/auth/login", {
|
|
212
|
+
body: { username, password }
|
|
213
|
+
});
|
|
214
|
+
if (response.body.token) {
|
|
215
|
+
sessionToken = response.body.token;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
Then("I should receive a session token", () => {
|
|
220
|
+
expect(response.body.token).toBeDefined();
|
|
221
|
+
expect(typeof response.body.token).toBe('string');
|
|
222
|
+
expect(response.body.token.length).toBeGreaterThan(0);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
And("the response should contain user info with role {string}", (_, expectedRole: string) => {
|
|
226
|
+
expect(response.body.user).toBeDefined();
|
|
227
|
+
expect(response.body.user.role).toBe(expectedRole);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
When("I request {string} with the session token", async (_, path: string) => {
|
|
231
|
+
response = await mock.handle("GET", path, {
|
|
232
|
+
headers: { authorization: `Bearer ${sessionToken}` }
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
Then("I should get my profile information", () => {
|
|
237
|
+
expect(response.body.user).toBeDefined();
|
|
238
|
+
expect(response.body.session).toBeDefined();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
And("the session should be marked as active", () => {
|
|
242
|
+
expect(response.body.session.active).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
When("I logout with the session token", async () => {
|
|
246
|
+
response = await mock.handle("POST", "/auth/logout", {
|
|
247
|
+
headers: { authorization: `Bearer ${sessionToken}` }
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
Then("the logout should be successful", () => {
|
|
252
|
+
expect(response.body.message).toContain("Logged out successfully");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
When("I request {string} with the same token", async (_, path: string) => {
|
|
256
|
+
response = await mock.handle("GET", path, {
|
|
257
|
+
headers: { authorization: `Bearer ${sessionToken}` }
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
Then("I should get a {int} unauthorized response", (_, expectedStatus: number) => {
|
|
262
|
+
expect(response.status).toBe(expectedStatus);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
Scenario("Multi-user state isolation", ({ Given, When, Then, And }) => {
|
|
267
|
+
Given("I create a multi-user counter mock:", (_, docString: string) => {
|
|
268
|
+
// Create multi-user counter mock with new callable API
|
|
269
|
+
const sharedState = { counters: {} };
|
|
270
|
+
mock = schmock({ state: sharedState });
|
|
271
|
+
|
|
272
|
+
mock('POST /counter/:userId/increment', ({ state, params }) => {
|
|
273
|
+
const userId = params.userId;
|
|
274
|
+
|
|
275
|
+
if (!state.counters[userId]) {
|
|
276
|
+
state.counters[userId] = 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
state.counters[userId]++;
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
userId: userId,
|
|
283
|
+
count: state.counters[userId],
|
|
284
|
+
message: `Counter incremented for user ${userId}`
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
mock('GET /counter/:userId', ({ state, params }) => {
|
|
289
|
+
const userId = params.userId;
|
|
290
|
+
const count = state.counters[userId] || 0;
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
userId: userId,
|
|
294
|
+
count: count
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
mock('GET /counters/summary', ({ state }) => {
|
|
299
|
+
const totalCount = Object.values(state.counters).reduce((sum: number, count: number) => sum + count, 0);
|
|
300
|
+
const userCount = Object.keys(state.counters).length;
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
totalCounts: totalCount,
|
|
304
|
+
totalUsers: userCount,
|
|
305
|
+
counters: state.counters
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
When("user {string} increments their counter {int} times", async (_, userId: string, times: number) => {
|
|
311
|
+
for (let i = 0; i < times; i++) {
|
|
312
|
+
await mock.handle("POST", `/counter/${userId}/increment`);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
And("user {string} increments their counter {int} times", async (_, userId: string, times: number) => {
|
|
317
|
+
for (let i = 0; i < times; i++) {
|
|
318
|
+
await mock.handle("POST", `/counter/${userId}/increment`);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
Then("{string}'s counter should be {int}", async (_, userId: string, expectedCount: number) => {
|
|
323
|
+
const response = await mock.handle("GET", `/counter/${userId}`);
|
|
324
|
+
expect(response.body.count).toBe(expectedCount);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
And("{string}'s counter should be {int}", async (_, userId: string, expectedCount: number) => {
|
|
328
|
+
const response = await mock.handle("GET", `/counter/${userId}`);
|
|
329
|
+
expect(response.body.count).toBe(expectedCount);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
And("the summary should show {int} total users", async (_, expectedUsers: number) => {
|
|
333
|
+
const response = await mock.handle("GET", "/counters/summary");
|
|
334
|
+
expect(response.body.totalUsers).toBe(expectedUsers);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
And("the summary should show total counts of {int}", async (_, expectedTotal: number) => {
|
|
338
|
+
const response = await mock.handle("GET", "/counters/summary");
|
|
339
|
+
expect(response.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
|
+
|
|
346
|
+
expect(aliceResponse.body.count).toBe(3);
|
|
347
|
+
expect(bobResponse.body.count).toBe(2);
|
|
348
|
+
expect(aliceResponse.body.count).not.toBe(bobResponse.body.count);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,eAAe,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;AACtE,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,gBAAgB,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;AACxE,MAAM,MAAM,eAAe,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;AACtE,MAAM,MAAM,MAAM,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACpD,MAAM,MAAM,OAAO,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AACtD,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC"}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/// <reference path="../../../types/schmock.d.ts" />
|
|
2
|
+
|
|
3
|
+
// Re-export types for internal use
|
|
4
|
+
export type HttpMethod = Schmock.HttpMethod;
|
|
5
|
+
export type RouteKey = Schmock.RouteKey;
|
|
6
|
+
export type ResponseResult = Schmock.ResponseResult;
|
|
7
|
+
export type RequestContext = Schmock.RequestContext;
|
|
8
|
+
export type Response = Schmock.Response;
|
|
9
|
+
export type RequestOptions = Schmock.RequestOptions;
|
|
10
|
+
export type GlobalConfig = Schmock.GlobalConfig;
|
|
11
|
+
export type RouteConfig = Schmock.RouteConfig;
|
|
12
|
+
export type Generator = Schmock.Generator;
|
|
13
|
+
export type GeneratorFunction = Schmock.GeneratorFunction;
|
|
14
|
+
export type CallableMockInstance = Schmock.CallableMockInstance;
|
|
15
|
+
export type Plugin = Schmock.Plugin;
|
|
16
|
+
export type PluginContext = Schmock.PluginContext;
|
|
17
|
+
export type PluginResult = Schmock.PluginResult;
|