@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.
Files changed (48) hide show
  1. package/dist/builder.d.ts +62 -0
  2. package/dist/builder.d.ts.map +1 -0
  3. package/dist/builder.js +432 -0
  4. package/dist/errors.d.ts +56 -0
  5. package/dist/errors.d.ts.map +1 -0
  6. package/dist/errors.js +92 -0
  7. package/dist/index.d.ts +27 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +1 -0
  10. package/dist/parser.d.ts +19 -0
  11. package/dist/parser.d.ts.map +1 -0
  12. package/dist/parser.js +40 -0
  13. package/dist/types.d.ts +15 -0
  14. package/dist/types.d.ts.map +1 -0
  15. package/dist/types.js +2 -0
  16. package/package.json +39 -0
  17. package/src/builder.d.ts.map +1 -0
  18. package/src/builder.test.ts +289 -0
  19. package/src/builder.ts +580 -0
  20. package/src/debug.test.ts +241 -0
  21. package/src/delay.test.ts +319 -0
  22. package/src/errors.d.ts.map +1 -0
  23. package/src/errors.test.ts +223 -0
  24. package/src/errors.ts +124 -0
  25. package/src/factory.test.ts +133 -0
  26. package/src/index.d.ts.map +1 -0
  27. package/src/index.ts +80 -0
  28. package/src/namespace.test.ts +273 -0
  29. package/src/parser.d.ts.map +1 -0
  30. package/src/parser.test.ts +131 -0
  31. package/src/parser.ts +61 -0
  32. package/src/plugin-system.test.ts +511 -0
  33. package/src/response-parsing.test.ts +255 -0
  34. package/src/route-matching.test.ts +351 -0
  35. package/src/smart-defaults.test.ts +361 -0
  36. package/src/steps/async-support.steps.ts +427 -0
  37. package/src/steps/basic-usage.steps.ts +316 -0
  38. package/src/steps/developer-experience.steps.ts +439 -0
  39. package/src/steps/error-handling.steps.ts +387 -0
  40. package/src/steps/fluent-api.steps.ts +252 -0
  41. package/src/steps/http-methods.steps.ts +397 -0
  42. package/src/steps/performance-reliability.steps.ts +459 -0
  43. package/src/steps/plugin-integration.steps.ts +279 -0
  44. package/src/steps/route-key-format.steps.ts +118 -0
  45. package/src/steps/state-concurrency.steps.ts +643 -0
  46. package/src/steps/stateful-workflows.steps.ts +351 -0
  47. package/src/types.d.ts.map +1 -0
  48. 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;