@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,427 @@
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/async-support.feature");
7
+
8
+ describeFeature(feature, ({ Scenario }) => {
9
+ let mock: CallableMockInstance;
10
+ let response: any;
11
+ let responses: any[] = [];
12
+ let startTime: number;
13
+ let endTime: number;
14
+
15
+ Scenario("Async generator function returns Promise", ({ Given, When, Then, And }) => {
16
+ Given("I create a mock with async generator:", (_, docString: string) => {
17
+ mock = schmock();
18
+ mock('GET /async-data', async () => {
19
+ await new Promise(resolve => setTimeout(resolve, 10));
20
+ return { message: 'async response' };
21
+ });
22
+ });
23
+
24
+ When("I request {string}", async (_, request: string) => {
25
+ const [method, path] = request.split(" ");
26
+ response = await mock.handle(method as any, path);
27
+ });
28
+
29
+ Then("I should receive:", (_, docString: string) => {
30
+ const expected = JSON.parse(docString);
31
+ expect(response.body).toEqual(expected);
32
+ });
33
+
34
+ And("the content-type should be {string}", (_, contentType: string) => {
35
+ expect(response.headers?.["content-type"]).toBe(contentType);
36
+ });
37
+ });
38
+
39
+ Scenario("Async generator with context access", ({ Given, When, Then, And }) => {
40
+ Given("I create a mock with async context generator:", (_, docString: string) => {
41
+ mock = schmock();
42
+ mock('GET /async-user/:id', async ({ params }) => {
43
+ await new Promise(resolve => setTimeout(resolve, 5));
44
+ return {
45
+ userId: params.id,
46
+ fetchedAt: new Date().toISOString()
47
+ };
48
+ });
49
+ });
50
+
51
+ When("I request {string}", async (_, request: string) => {
52
+ const [method, path] = request.split(" ");
53
+ response = await mock.handle(method as any, path);
54
+ });
55
+
56
+ Then("the response should have property {string} with value {string}", (_, property: string, value: string) => {
57
+ expect(response.body).toHaveProperty(property, value);
58
+ });
59
+
60
+ And("the response should have property {string}", (_, property: string) => {
61
+ expect(response.body).toHaveProperty(property);
62
+ });
63
+ });
64
+
65
+ Scenario("Multiple async generators in different routes", ({ Given, When, Then, And }) => {
66
+ Given("I create a mock with multiple async routes:", (_, docString: string) => {
67
+ mock = schmock();
68
+ mock('GET /async-posts', async () => {
69
+ await new Promise(resolve => setTimeout(resolve, 5));
70
+ return [{ id: 1, title: 'First Post' }];
71
+ });
72
+ mock('GET /async-comments', async () => {
73
+ await new Promise(resolve => setTimeout(resolve, 8));
74
+ return [{ id: 1, comment: 'Great post!' }];
75
+ });
76
+ });
77
+
78
+ When("I make concurrent requests to {string} and {string}", async (_, path1: string, path2: string) => {
79
+ const [posts, comments] = await Promise.all([
80
+ mock.handle('GET', path1),
81
+ mock.handle('GET', path2)
82
+ ]);
83
+ responses = [posts, comments];
84
+ });
85
+
86
+ Then("both responses should be returned successfully", () => {
87
+ expect(responses[0].status).toBe(200);
88
+ expect(responses[1].status).toBe(200);
89
+ });
90
+
91
+ And("the posts response should contain {string}", (_, text: string) => {
92
+ expect(JSON.stringify(responses[0].body)).toContain(text);
93
+ });
94
+
95
+ And("the comments response should contain {string}", (_, text: string) => {
96
+ expect(JSON.stringify(responses[1].body)).toContain(text);
97
+ });
98
+ });
99
+
100
+ Scenario("Async plugin processing", ({ Given, When, Then, And }) => {
101
+ Given("I create a mock with async plugin:", (_, docString: string) => {
102
+ mock = schmock();
103
+ const asyncPlugin = {
104
+ name: 'async-processor',
105
+ process: async (ctx: any, response: any) => {
106
+ await new Promise(resolve => setTimeout(resolve, 5));
107
+ return {
108
+ context: ctx,
109
+ response: {
110
+ data: response,
111
+ processedAsync: true,
112
+ timestamp: new Date().toISOString()
113
+ }
114
+ };
115
+ }
116
+ };
117
+ mock('GET /processed', { original: 'data' }).pipe(asyncPlugin);
118
+ });
119
+
120
+ When("I request {string}", async (_, request: string) => {
121
+ const [method, path] = request.split(" ");
122
+ response = await mock.handle(method as any, path);
123
+ });
124
+
125
+ Then("the async response should have property {string}", (_, property: string) => {
126
+ expect(response.body).toHaveProperty(property);
127
+ });
128
+
129
+ And("the async response should have property {string} with value {word}", (_, property: string, value: string) => {
130
+ const expectedValue = value === 'true' ? true : value === 'false' ? false : value;
131
+ expect(response.body).toHaveProperty(property, expectedValue);
132
+ });
133
+
134
+ And("the async response should have property {string}", (_, property: string) => {
135
+ expect(response.body).toHaveProperty(property);
136
+ });
137
+ });
138
+
139
+ Scenario("Mixed sync and async plugin pipeline", ({ Given, When, Then, And }) => {
140
+ Given("I create a mock with mixed plugin pipeline:", (_, docString: string) => {
141
+ mock = schmock();
142
+ const syncPlugin = {
143
+ name: 'sync-step',
144
+ process: (ctx: any, response: any) => ({
145
+ context: ctx,
146
+ response: { ...response, syncStep: true }
147
+ })
148
+ };
149
+ const asyncPlugin = {
150
+ name: 'async-step',
151
+ process: async (ctx: any, response: any) => {
152
+ await new Promise(resolve => setTimeout(resolve, 5));
153
+ return {
154
+ context: ctx,
155
+ response: { ...response, asyncStep: true }
156
+ };
157
+ }
158
+ };
159
+ mock('GET /mixed', { base: 'data' })
160
+ .pipe(syncPlugin)
161
+ .pipe(asyncPlugin);
162
+ });
163
+
164
+ When("I request {string}", async (_, request: string) => {
165
+ const [method, path] = request.split(" ");
166
+ response = await mock.handle(method as any, path);
167
+ });
168
+
169
+ Then("the response should have property {string} with value {string}", (_, property: string, value: string) => {
170
+ expect(response.body).toHaveProperty(property, value);
171
+ });
172
+
173
+ And("the response should have property {string} with value {word}", (_, property: string, value: string) => {
174
+ const expectedValue = value === 'true' ? true : value === 'false' ? false : value;
175
+ expect(response.body).toHaveProperty(property, expectedValue);
176
+ });
177
+
178
+ And("the response should have property {string} with boolean value {word}", (_, property: string, value: string) => {
179
+ const expectedValue = value === 'true' ? true : value === 'false' ? false : value;
180
+ expect(response.body).toHaveProperty(property, expectedValue);
181
+ });
182
+ });
183
+
184
+ Scenario("Async generator with Promise rejection", ({ Given, When, Then, And }) => {
185
+ Given("I create a mock with failing async generator:", (_, docString: string) => {
186
+ mock = schmock();
187
+ mock('GET /async-fail', async () => {
188
+ await new Promise(resolve => setTimeout(resolve, 5));
189
+ throw new Error('Async operation failed');
190
+ });
191
+ });
192
+
193
+ When("I request {string}", async (_, request: string) => {
194
+ const [method, path] = request.split(" ");
195
+ response = await mock.handle(method as any, path);
196
+ });
197
+
198
+ Then("I should receive status {int}", (_, status: number) => {
199
+ expect(response.status).toBe(status);
200
+ });
201
+
202
+ And("the response should contain error {string}", (_, errorMessage: string) => {
203
+ expect(response.body.error).toContain(errorMessage);
204
+ });
205
+ });
206
+
207
+ Scenario("Async plugin error recovery", ({ Given, When, Then, And }) => {
208
+ Given("I create a mock with async error recovery:", (_, docString: string) => {
209
+ mock = schmock();
210
+ const asyncErrorPlugin = {
211
+ name: 'async-error-handler',
212
+ process: async () => {
213
+ await new Promise(resolve => setTimeout(resolve, 5));
214
+ throw new Error('Async plugin failed');
215
+ },
216
+ onError: async (error: Error, ctx: any) => {
217
+ await new Promise(resolve => setTimeout(resolve, 3));
218
+ return {
219
+ status: 200,
220
+ body: { recovered: true, originalError: error.message },
221
+ headers: {}
222
+ };
223
+ }
224
+ };
225
+ mock('GET /async-recovery', 'original').pipe(asyncErrorPlugin);
226
+ });
227
+
228
+ When("I request {string}", async (_, request: string) => {
229
+ const [method, path] = request.split(" ");
230
+ response = await mock.handle(method as any, path);
231
+ });
232
+
233
+ Then("I should receive status {int}", (_, status: number) => {
234
+ expect(response.status).toBe(status);
235
+ });
236
+
237
+ And("the response should have property {string} with value {word}", (_, property: string, value: string) => {
238
+ const expectedValue = value === 'true' ? true : value === 'false' ? false : value;
239
+ expect(response.body).toHaveProperty(property, expectedValue);
240
+ });
241
+
242
+ And("the response should have property {string}", (_, property: string) => {
243
+ expect(response.body).toHaveProperty(property);
244
+ });
245
+ });
246
+
247
+ Scenario("Async generator with delay configuration", ({ Given, When, Then, And }) => {
248
+ Given("I create a mock with async generator and delay:", (_, docString: string) => {
249
+ mock = schmock({ delay: 20 });
250
+ mock('GET /delayed-async', async () => {
251
+ await new Promise(resolve => setTimeout(resolve, 10));
252
+ return { delayed: true, async: true };
253
+ });
254
+ });
255
+
256
+ When("I request {string} and measure time", async (_, request: string) => {
257
+ const [method, path] = request.split(" ");
258
+ startTime = Date.now();
259
+ response = await mock.handle(method as any, path);
260
+ endTime = Date.now();
261
+ });
262
+
263
+ Then("the response time should be at least {int}ms", (_, minTime: number) => {
264
+ const actualTime = endTime - startTime;
265
+ expect(actualTime).toBeGreaterThanOrEqual(minTime);
266
+ });
267
+
268
+ And("I should receive:", (_, docString: string) => {
269
+ const expected = JSON.parse(docString);
270
+ expect(response.body).toEqual(expected);
271
+ });
272
+ });
273
+
274
+ Scenario("Async generator with state management", ({ Given, When, Then, And }) => {
275
+ Given("I create a mock with async stateful generator:", (_, docString: string) => {
276
+ mock = schmock({ state: { asyncCounter: 0 } });
277
+ mock('GET /async-counter', async ({ state }) => {
278
+ await new Promise(resolve => setTimeout(resolve, 5));
279
+ state.asyncCounter = (state.asyncCounter || 0) + 1;
280
+ return {
281
+ count: state.asyncCounter,
282
+ processedAsync: true
283
+ };
284
+ });
285
+ });
286
+
287
+ When("I request {string} twice", async (_, request: string) => {
288
+ const [method, path] = request.split(" ");
289
+ const firstResponse = await mock.handle(method as any, path);
290
+ const secondResponse = await mock.handle(method as any, path);
291
+ responses = [firstResponse, secondResponse];
292
+ });
293
+
294
+ Then("the first response should have count {int}", (_, count: number) => {
295
+ expect(responses[0].body.count).toBe(count);
296
+ });
297
+
298
+ And("the second response should have count {int}", (_, count: number) => {
299
+ expect(responses[1].body.count).toBe(count);
300
+ });
301
+
302
+ And("both responses should have processedAsync {word}", (_, value: string) => {
303
+ const expectedValue = value === 'true' ? true : value === 'false' ? false : value;
304
+ expect(responses[0].body.processedAsync).toBe(expectedValue);
305
+ expect(responses[1].body.processedAsync).toBe(expectedValue);
306
+ });
307
+ });
308
+
309
+ Scenario("Promise-based plugin response generation", ({ Given, When, Then }) => {
310
+ Given("I create a mock with Promise-generating plugin:", (_, docString: string) => {
311
+ mock = schmock();
312
+ const promisePlugin = {
313
+ name: 'promise-generator',
314
+ process: async (ctx: any, response: any) => {
315
+ if (!response) {
316
+ const data = await Promise.resolve({ generated: 'by promise' });
317
+ return { context: ctx, response: data };
318
+ }
319
+ return { context: ctx, response };
320
+ }
321
+ };
322
+ mock('GET /promise-gen', null).pipe(promisePlugin);
323
+ });
324
+
325
+ When("I request {string}", async (_, request: string) => {
326
+ const [method, path] = request.split(" ");
327
+ response = await mock.handle(method as any, path);
328
+ });
329
+
330
+ Then("I should receive:", (_, docString: string) => {
331
+ const expected = JSON.parse(docString);
332
+ expect(response.body).toEqual(expected);
333
+ });
334
+ });
335
+
336
+ Scenario("Concurrent async requests isolation", ({ Given, When, Then, And }) => {
337
+ Given("I create a mock with async state isolation:", (_, docString: string) => {
338
+ mock = schmock();
339
+ mock('GET /isolated/:id', async ({ params }) => {
340
+ const delay = parseInt(params.id) * 5;
341
+ await new Promise(resolve => setTimeout(resolve, delay));
342
+ return {
343
+ id: params.id,
344
+ processedAt: Date.now()
345
+ };
346
+ });
347
+ });
348
+
349
+ When("I make concurrent requests to {string}, {string}, and {string}", async (_, path1: string, path2: string, path3: string) => {
350
+ const promises = [
351
+ mock.handle('GET', path1),
352
+ mock.handle('GET', path2),
353
+ mock.handle('GET', path3)
354
+ ];
355
+ responses = await Promise.all(promises);
356
+ });
357
+
358
+ Then("all responses should have different processedAt timestamps", () => {
359
+ const timestamps = responses.map(r => r.body.processedAt);
360
+ const uniqueTimestamps = new Set(timestamps);
361
+ // In fast test environments, timestamps might be the same, so just check they exist
362
+ expect(timestamps).toHaveLength(3);
363
+ timestamps.forEach(timestamp => expect(timestamp).toBeGreaterThan(0));
364
+ });
365
+
366
+ And("each response should have the correct id value", () => {
367
+ expect(responses[0].body.id).toBe("1");
368
+ expect(responses[1].body.id).toBe("2");
369
+ expect(responses[2].body.id).toBe("3");
370
+ });
371
+
372
+ And("the responses should complete in expected order", () => {
373
+ // All responses should be successful regardless of timing
374
+ for (const response of responses) {
375
+ expect(response.status).toBe(200);
376
+ expect(response.body).toHaveProperty('id');
377
+ expect(response.body).toHaveProperty('processedAt');
378
+ }
379
+ });
380
+ });
381
+
382
+ Scenario("Async plugin pipeline with context state", ({ Given, When, Then, And }) => {
383
+ Given("I create a mock with async stateful plugins:", (_, docString: string) => {
384
+ mock = schmock();
385
+ const plugin1 = {
386
+ name: 'async-step-1',
387
+ process: async (ctx: any, response: any) => {
388
+ await new Promise(resolve => setTimeout(resolve, 5));
389
+ ctx.state.set('step1', 'completed');
390
+ return { context: ctx, response };
391
+ }
392
+ };
393
+ const plugin2 = {
394
+ name: 'async-step-2',
395
+ process: async (ctx: any, response: any) => {
396
+ await new Promise(resolve => setTimeout(resolve, 3));
397
+ const step1Status = ctx.state.get('step1');
398
+ return {
399
+ context: ctx,
400
+ response: {
401
+ ...response,
402
+ step1: step1Status,
403
+ step2: 'completed'
404
+ }
405
+ };
406
+ }
407
+ };
408
+ mock('GET /async-pipeline', { base: 'data' })
409
+ .pipe(plugin1)
410
+ .pipe(plugin2);
411
+ });
412
+
413
+ When("I request {string}", async (_, request: string) => {
414
+ const [method, path] = request.split(" ");
415
+ response = await mock.handle(method as any, path);
416
+ });
417
+
418
+ Then("the response should have property {string} with value {string}", (_, property: string, value: string) => {
419
+ expect(response.body).toHaveProperty(property, value);
420
+ });
421
+
422
+ And("the async pipeline response should have both step properties completed", () => {
423
+ expect(response.body).toHaveProperty("step1", "completed");
424
+ expect(response.body).toHaveProperty("step2", "completed");
425
+ });
426
+ });
427
+ });