@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,459 @@
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/performance-reliability.feature");
7
+
8
+ describeFeature(feature, ({ Scenario }) => {
9
+ let mock: MockInstance<any>;
10
+ let responses: any[] = [];
11
+ let responsesTimes: number[] = [];
12
+
13
+ Scenario("High-volume concurrent requests", ({ Given, When, Then, And }) => {
14
+ responses = [];
15
+ responsesTimes = [];
16
+
17
+ Given("I create a mock for load testing:", (_, docString: string) => {
18
+ // Create mock with new callable API for load testing
19
+ mock = schmock();
20
+
21
+ mock('GET /api/health', () => ({
22
+ status: 'healthy',
23
+ timestamp: Date.now()
24
+ }));
25
+
26
+ mock('GET /api/data/:id', ({ params }) => ({
27
+ id: params.id,
28
+ data: Array.from({ length: 50 }, (_, i) => ({
29
+ index: i,
30
+ value: Math.random()
31
+ })),
32
+ generated_at: new Date().toISOString()
33
+ }));
34
+
35
+ mock('POST /api/process', ({ body }) => {
36
+ // Simulate processing time
37
+ const items = Array.isArray(body) ? body : [body];
38
+ return {
39
+ processed: items.length,
40
+ results: items.map(item => ({ ...item, processed: true })),
41
+ batch_id: Math.random().toString(36)
42
+ };
43
+ });
44
+ });
45
+
46
+ When("I send {int} concurrent {string} requests", async (_, count: number, request: string) => {
47
+ const [method, path] = request.split(" ");
48
+ responses = [];
49
+ responsesTimes = [];
50
+
51
+ const promises = Array.from({ length: count }, async () => {
52
+ const startTime = Date.now();
53
+ const response = await mock.handle(method as any, path);
54
+ const elapsed = Date.now() - startTime;
55
+ return { response, elapsed };
56
+ });
57
+
58
+ const results = await Promise.all(promises);
59
+ responses = results.map(r => r.response);
60
+ responsesTimes = results.map(r => r.elapsed);
61
+ });
62
+
63
+ Then("all concurrent requests should complete successfully", () => {
64
+ const expectedCount = responses.length;
65
+ expect(responses).toHaveLength(expectedCount);
66
+ for (const response of responses) {
67
+ expect(response.status).toBe(200);
68
+ }
69
+ });
70
+
71
+ And("the average response time should be under {int}ms", (_, maxTime: number) => {
72
+ const avgTime = responsesTimes.reduce((a, b) => a + b, 0) / responsesTimes.length;
73
+ expect(avgTime).toBeLessThan(maxTime);
74
+ });
75
+
76
+ And("no requests should timeout", () => {
77
+ // All requests completed if we got here, so no timeouts
78
+ expect(responses.length).toBeGreaterThan(0);
79
+ });
80
+
81
+ When("I send {int} concurrent requests to different {string} endpoints", async (_, count: number, pathPattern: string) => {
82
+ responses = [];
83
+
84
+ const promises = Array.from({ length: count }, async (_, i) => {
85
+ const path = pathPattern.replace(":id", `id${i}`);
86
+ return await mock.handle("GET", path);
87
+ });
88
+
89
+ responses = await Promise.all(promises);
90
+ });
91
+
92
+ Then("all responses should be unique based on ID", () => {
93
+ const ids = responses.map(r => r.body.id);
94
+ const uniqueIds = new Set(ids);
95
+ expect(uniqueIds.size).toBe(responses.length);
96
+ });
97
+
98
+ And("each response should contain {int} data items", (_, expectedCount: number) => {
99
+ for (const response of responses) {
100
+ expect(response.body.data).toHaveLength(expectedCount);
101
+ }
102
+ });
103
+
104
+ When("I send {int} concurrent {string} requests with different payloads", async (_, count: number, request: string) => {
105
+ const [method, path] = request.split(" ");
106
+ responses = [];
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
+ const expectedCount = responses.length;
119
+ expect(responses).toHaveLength(expectedCount);
120
+ for (const response of responses) {
121
+ expect(response.status).toBe(200);
122
+ }
123
+ });
124
+
125
+ And("each response should have a unique batch_id", () => {
126
+ const batchIds = responses.map(r => r.body.batch_id);
127
+ const uniqueBatchIds = new Set(batchIds);
128
+ expect(uniqueBatchIds.size).toBe(responses.length);
129
+ });
130
+ });
131
+
132
+ Scenario("Memory usage under sustained load", ({ Given, When, Then, And }) => {
133
+ Given("I create a mock with potential memory concerns:", (_, docString: string) => {
134
+ // Create mock with routes that handle large data
135
+ mock = schmock();
136
+
137
+ mock('POST /api/large-data', ({ body }) => {
138
+ // Create large response data
139
+ const largeArray = Array.from({ length: 1000 }, (_, i) => ({
140
+ id: i,
141
+ data: 'x'.repeat(100), // 100 chars per item
142
+ timestamp: Date.now(),
143
+ payload: body
144
+ }));
145
+
146
+ return {
147
+ results: largeArray,
148
+ items: largeArray,
149
+ total_size: largeArray.length,
150
+ size: 'large',
151
+ memory_usage: process.memoryUsage ? process.memoryUsage() : null
152
+ };
153
+ });
154
+
155
+ mock('GET /api/accumulate/:count', ({ params }) => {
156
+ const count = parseInt(params.count);
157
+ const items = Array.from({ length: count }, (_, i) => ({
158
+ id: i,
159
+ value: Math.random(),
160
+ timestamp: Date.now()
161
+ }));
162
+
163
+ return {
164
+ items: items,
165
+ accumulated: items,
166
+ total: items.length,
167
+ count: items.length,
168
+ memory_usage: process.memoryUsage ? process.memoryUsage() : null
169
+ };
170
+ });
171
+ });
172
+
173
+ When("I send {int} requests to {string} with {int}KB payloads", async (_, count: number, request: string, payloadSize: number) => {
174
+ const [method, path] = request.split(" ");
175
+ responses = [];
176
+
177
+ const largePayload = { data: 'x'.repeat(payloadSize * 1024) };
178
+
179
+ for (let i = 0; i < count; i++) {
180
+ const response = await mock.handle(method as any, path, { body: largePayload });
181
+ responses.push(response);
182
+ }
183
+ });
184
+
185
+ Then("all requests should complete without memory errors", () => {
186
+ expect(responses).toHaveLength(20);
187
+ for (const response of responses) {
188
+ expect(response.status).toBe(200);
189
+ expect(response.body.items).toHaveLength(1000);
190
+ }
191
+ });
192
+
193
+ And("the mock should handle the load gracefully", () => {
194
+ // If we got here without throwing, the mock handled the load
195
+ expect(responses.every(r => r.body.size === 'large')).toBe(true);
196
+ });
197
+
198
+ When("I request {string} multiple times", async (_, request: string) => {
199
+ const [method, path] = request.split(" ");
200
+ responses = [];
201
+
202
+ for (let i = 0; i < 5; i++) {
203
+ const response = await mock.handle(method as any, path);
204
+ responses.push(response);
205
+ }
206
+ });
207
+
208
+ Then("each response should contain {int} accumulated items", (_, expectedCount: number) => {
209
+ for (const response of responses) {
210
+ expect(response.body.accumulated).toHaveLength(expectedCount);
211
+ expect(response.body.total).toBe(expectedCount);
212
+ }
213
+ });
214
+
215
+ And("the memory usage should remain stable", () => {
216
+ // Memory stability is tested by not crashing during multiple large requests
217
+ expect(responses).toHaveLength(5);
218
+ });
219
+ });
220
+
221
+ Scenario("Error resilience and recovery", ({ Given, When, Then, And }) => {
222
+ responses = [];
223
+
224
+ Given("I create a mock with intermittent failures:", (_, docString: string) => {
225
+ // Create mock with intermittent failure simulation
226
+ mock = schmock();
227
+
228
+ let requestCount = 0;
229
+
230
+ mock('POST /api/unreliable', ({ body }) => {
231
+ requestCount++;
232
+
233
+ // Simulate 20% failure rate
234
+ if (requestCount % 5 === 0) {
235
+ return [500, { error: 'Simulated server error', request_id: requestCount }];
236
+ }
237
+
238
+ return [200, { success: true, data: body, request_id: requestCount }];
239
+ });
240
+
241
+ mock('GET /api/flaky', () => {
242
+ requestCount++;
243
+
244
+ // Simulate 20% failure rate (1 in 5 requests fail)
245
+ if (requestCount % 5 === 0) {
246
+ return [500, { error: 'Flaky service error', request_id: requestCount }];
247
+ }
248
+
249
+ return [200, { success: true, request_id: requestCount }];
250
+ });
251
+
252
+ mock('POST /api/validate-strict', ({ body }) => {
253
+ if (!body || typeof body !== 'object') {
254
+ return [400, { error: 'Request body is required and must be an object', code: 'INVALID_BODY' }];
255
+ }
256
+
257
+ if (!body.name || typeof body.name !== 'string') {
258
+ return [422, { error: 'Name field is required and must be a string', code: 'INVALID_NAME' }];
259
+ }
260
+
261
+ if (!body.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) {
262
+ return [422, { error: 'Valid email address is required', code: 'INVALID_EMAIL' }];
263
+ }
264
+
265
+ return [200, { message: 'Validation successful', data: body }];
266
+ });
267
+ });
268
+
269
+ When("I send {int} requests to {string}", async (_, count: number, request: string) => {
270
+ const [method, path] = request.split(" ");
271
+ responses = [];
272
+
273
+ for (let i = 0; i < count; i++) {
274
+ const response = await mock.handle(method as any, path);
275
+ responses.push(response);
276
+ }
277
+ });
278
+
279
+ Then("some requests should succeed and some should fail", () => {
280
+ const successCount = responses.filter(r => r.status === 200).length;
281
+ const errorCount = responses.filter(r => r.status >= 400).length;
282
+
283
+ expect(successCount).toBeGreaterThan(0);
284
+ expect(errorCount).toBeGreaterThan(0);
285
+ expect(successCount + errorCount).toBe(responses.length);
286
+ });
287
+
288
+ And("the success rate should be approximately {int}%", (_, expectedRate: number) => {
289
+ const successCount = responses.filter(r => r.status === 200).length;
290
+ const actualRate = (successCount / responses.length) * 100;
291
+
292
+ // Allow for some variance due to randomness
293
+ expect(actualRate).toBeGreaterThan(expectedRate - 10);
294
+ expect(actualRate).toBeLessThan(expectedRate + 10);
295
+ });
296
+
297
+ And("error responses should have appropriate status codes", () => {
298
+ const errorResponses = responses.filter(r => r.status >= 400);
299
+ const validErrorCodes = [429, 500, 503];
300
+
301
+ for (const response of errorResponses) {
302
+ expect(validErrorCodes).toContain(response.status);
303
+ }
304
+ });
305
+
306
+ When("I send requests to {string} with various invalid inputs", async (_, request: string) => {
307
+ const [method, path] = request.split(" ");
308
+ responses = [];
309
+
310
+ // Test various invalid scenarios
311
+ const testCases = [
312
+ { headers: {}, body: null }, // No content-type, no body
313
+ { headers: { 'content-type': 'application/json' }, body: null }, // No body
314
+ { headers: { 'content-type': 'application/json' }, body: "invalid" }, // Invalid body type
315
+ { headers: { 'content-type': 'application/json' }, body: {} }, // Missing required field
316
+ ];
317
+
318
+ for (const testCase of testCases) {
319
+ const response = await mock.handle(method as any, path, testCase);
320
+ responses.push(response);
321
+ }
322
+ });
323
+
324
+ Then("each error should have a specific, helpful error message", () => {
325
+ for (const response of responses) {
326
+ expect(response.status).toBeGreaterThanOrEqual(400);
327
+ expect(response.body.error).toBeDefined();
328
+ expect(typeof response.body.error).toBe('string');
329
+ expect(response.body.error.length).toBeGreaterThan(0);
330
+ }
331
+ });
332
+
333
+ And("the error codes should correctly identify the validation issue", () => {
334
+ expect(responses[0].status).toBe(400); // No content-type
335
+ expect(responses[1].status).toBe(400); // No body
336
+ expect(responses[2].status).toBe(400); // Invalid body type
337
+ expect(responses[3].status).toBe(422); // Missing required field
338
+ });
339
+ });
340
+
341
+ Scenario("Route matching performance with complex patterns", ({ Given, When, Then, And }) => {
342
+ responses = [];
343
+
344
+ Given("I create a mock with many route patterns:", (_, docString: string) => {
345
+ // Create mock with many different route patterns for performance testing
346
+ mock = schmock();
347
+
348
+ // Routes that match the test expectations
349
+ mock('GET /api/users', () => ({ type: 'users-list' }));
350
+ mock('GET /api/users/:id', ({ params }) => ({ type: 'user', id: params.id }));
351
+ mock('GET /api/users/:userId/posts', ({ params }) => ({ type: 'user-posts', userId: params.userId }));
352
+ mock('GET /api/users/:userId/posts/:postId', ({ params }) => ({
353
+ type: 'user-post',
354
+ userId: params.userId,
355
+ postId: params.postId
356
+ }));
357
+ mock('GET /api/users/:userId/posts/:postId/comments', ({ params }) => ({
358
+ type: 'post-comments',
359
+ userId: params.userId,
360
+ postId: params.postId
361
+ }));
362
+ mock('GET /api/posts', () => ({ type: 'posts-list' }));
363
+ mock('GET /api/posts/:postId', ({ params }) => ({ type: 'post', postId: params.postId }));
364
+ mock('GET /api/posts/:postId/comments/:commentId', ({ params }) => ({
365
+ type: 'comment',
366
+ postId: params.postId,
367
+ commentId: params.commentId
368
+ }));
369
+ mock('GET /static/:category/:file', ({ params }) => ({
370
+ type: 'static',
371
+ category: params.category,
372
+ file: params.file
373
+ }));
374
+ mock('GET /api/v2/users/:userId', ({ params }) => ({
375
+ type: 'versioned-user',
376
+ userId: params.userId,
377
+ version: 'v2'
378
+ }));
379
+ });
380
+
381
+ When("I send requests to all route patterns simultaneously", async () => {
382
+ const testPaths = [
383
+ "GET /api/users",
384
+ "GET /api/users/123",
385
+ "GET /api/users/123/posts",
386
+ "GET /api/users/123/posts/456",
387
+ "GET /api/users/123/posts/456/comments",
388
+ "GET /api/posts",
389
+ "GET /api/posts/789",
390
+ "GET /api/posts/789/comments/101",
391
+ "GET /static/images/logo.png",
392
+ "GET /api/v2/users/456"
393
+ ];
394
+
395
+ const promises = testPaths.map(async (request) => {
396
+ const [method, path] = request.split(" ");
397
+ return await mock.handle(method as any, path);
398
+ });
399
+
400
+ responses = await Promise.all(promises);
401
+ });
402
+
403
+ Then("each request should match the correct route pattern", () => {
404
+ expect(responses[0].body.type).toBe("users-list");
405
+ expect(responses[1].body.type).toBe("user");
406
+ expect(responses[2].body.type).toBe("user-posts");
407
+ expect(responses[3].body.type).toBe("user-post");
408
+ expect(responses[4].body.type).toBe("post-comments");
409
+ expect(responses[5].body.type).toBe("posts-list");
410
+ expect(responses[6].body.type).toBe("post");
411
+ expect(responses[7].body.type).toBe("comment");
412
+ expect(responses[8].body.type).toBe("static");
413
+ expect(responses[9].body.type).toBe("versioned-user");
414
+ });
415
+
416
+ And("parameter extraction should work correctly for all patterns", () => {
417
+ expect(responses[1].body.id).toBe("123");
418
+ expect(responses[2].body.userId).toBe("123");
419
+ expect(responses[3].body.userId).toBe("123");
420
+ expect(responses[3].body.postId).toBe("456");
421
+ expect(responses[8].body.category).toBe("images");
422
+ expect(responses[8].body.file).toBe("logo.png");
423
+ expect(responses[9].body.version).toBe("v2");
424
+ });
425
+
426
+ And("the route matching should be efficient even with many patterns", () => {
427
+ // All requests completed quickly if we got here
428
+ expect(responses).toHaveLength(10);
429
+ });
430
+
431
+ When("I send requests to non-matching paths", async () => {
432
+ const invalidPaths = [
433
+ "GET /api/invalid",
434
+ "GET /users", // Missing /api prefix
435
+ "GET /api/users/123/invalid",
436
+ "POST /static/images/test.jpg" // Wrong method
437
+ ];
438
+
439
+ responses = [];
440
+ for (const request of invalidPaths) {
441
+ const [method, path] = request.split(" ");
442
+ const response = await mock.handle(method as any, path);
443
+ responses.push(response);
444
+ }
445
+ });
446
+
447
+ Then("they should consistently return {int} responses", (_, expectedStatus: number) => {
448
+ for (const response of responses) {
449
+ expect(response.status).toBe(expectedStatus);
450
+ }
451
+ });
452
+
453
+ And("the {int} responses should be fast", (_, statusCode: number) => {
454
+ // If we got here quickly, the 404 responses were fast
455
+ expect(responses.every(r => r.status === statusCode)).toBe(true);
456
+ });
457
+ });
458
+
459
+ });