@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,439 @@
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/developer-experience.feature");
7
+
8
+ describeFeature(feature, ({ Scenario }) => {
9
+ let mock: CallableMockInstance;
10
+ let response: any;
11
+ let responses: any[] = [];
12
+ let error: Error | null = null;
13
+
14
+ Scenario("Forgetting to provide response data", ({ Given, When, Then, And }) => {
15
+ Given("I create a mock without response data:", (_, docString: string) => {
16
+ mock = schmock();
17
+ mock('GET /empty');
18
+ });
19
+
20
+ When("I request {string}", async (_, request: string) => {
21
+ const [method, path] = request.split(" ");
22
+ response = await mock.handle(method as any, path);
23
+ });
24
+
25
+ Then("I should receive status {int}", (_, status: number) => {
26
+ expect(response.status).toBe(status);
27
+ });
28
+
29
+ And("the response body should be empty", () => {
30
+ expect(response.body).toBeUndefined();
31
+ });
32
+ });
33
+
34
+ Scenario("Using wrong parameter name in route", ({ Given, When, Then }) => {
35
+ Given("I create a mock with parameter route:", (_, docString: string) => {
36
+ mock = schmock();
37
+ mock('GET /users/:userId', ({ params }) => ({ id: params.id }));
38
+ });
39
+
40
+ When("I request {string}", async (_, request: string) => {
41
+ const [method, path] = request.split(" ");
42
+ response = await mock.handle(method as any, path);
43
+ });
44
+
45
+ Then("the wrong parameter should be undefined", () => {
46
+ expect(response.body).toEqual({ id: undefined });
47
+ });
48
+ });
49
+
50
+ Scenario("Correct parameter usage", ({ Given, When, Then }) => {
51
+ Given("I create a mock with proper parameter usage:", (_, docString: string) => {
52
+ mock = schmock();
53
+ mock('GET /users/:userId', ({ params }) => ({ id: params.userId }));
54
+ });
55
+
56
+ When("I request {string}", async (_, request: string) => {
57
+ const [method, path] = request.split(" ");
58
+ response = await mock.handle(method as any, path);
59
+ });
60
+
61
+ Then("I should receive:", (_, docString: string) => {
62
+ const expected = JSON.parse(docString);
63
+ expect(response.body).toEqual(expected);
64
+ });
65
+ });
66
+
67
+ Scenario("Mixing content types without explicit configuration", ({ Given, When, Then, And }) => {
68
+ Given("I create a mock with mixed content types:", (_, docString: string) => {
69
+ mock = schmock();
70
+ mock('GET /json', { data: 'json' });
71
+ mock('GET /text', 'plain text');
72
+ mock('GET /number', 42);
73
+ mock('GET /boolean', true);
74
+ });
75
+
76
+ When("I test all mixed content type routes", async () => {
77
+ responses = [];
78
+ responses.push(await mock.handle('GET', '/json'));
79
+ responses.push(await mock.handle('GET', '/text'));
80
+ responses.push(await mock.handle('GET', '/number'));
81
+ responses.push(await mock.handle('GET', '/boolean'));
82
+ });
83
+
84
+ Then("JSON route should have content-type {string}", (_, contentType: string) => {
85
+ expect(responses[0].headers?.["content-type"]).toBe(contentType);
86
+ });
87
+
88
+ And("text route should have content-type {string}", (_, contentType: string) => {
89
+ expect(responses[1].headers?.["content-type"]).toBe(contentType);
90
+ });
91
+
92
+ And("number route should have content-type {string}", (_, contentType: string) => {
93
+ expect(responses[2].headers?.["content-type"]).toBe(contentType);
94
+ });
95
+
96
+ And("boolean route should have content-type {string}", (_, contentType: string) => {
97
+ expect(responses[3].headers?.["content-type"]).toBe(contentType);
98
+ });
99
+ });
100
+
101
+ Scenario("Expecting JSON but getting string conversion", ({ Given, When, Then, And }) => {
102
+ Given("I create a mock with number response:", (_, docString: string) => {
103
+ mock = schmock();
104
+ mock('GET /price', 19.99);
105
+ });
106
+
107
+ When("I request {string}", async (_, request: string) => {
108
+ const [method, path] = request.split(" ");
109
+ response = await mock.handle(method as any, path);
110
+ });
111
+
112
+ Then("I should receive text {string}", (_, expectedText: string) => {
113
+ expect(response.body).toBe(expectedText);
114
+ });
115
+
116
+ And("the content-type should be {string}", (_, contentType: string) => {
117
+ expect(response.headers?.["content-type"]).toBe(contentType);
118
+ });
119
+ });
120
+
121
+ Scenario("Forgetting await with async generators", ({ Given, When, Then, And }) => {
122
+ Given("I create a mock with async generator:", (_, docString: string) => {
123
+ mock = schmock();
124
+ mock('GET /data', async () => ({ async: true }));
125
+ });
126
+
127
+ When("I request {string}", async (_, request: string) => {
128
+ const [method, path] = request.split(" ");
129
+ response = await mock.handle(method as any, path);
130
+ });
131
+
132
+ Then("I should receive:", (_, docString: string) => {
133
+ const expected = JSON.parse(docString);
134
+ expect(response.body).toEqual(expected);
135
+ });
136
+
137
+ And("the content-type should be {string}", (_, contentType: string) => {
138
+ expect(response.headers?.["content-type"]).toBe(contentType);
139
+ });
140
+ });
141
+
142
+ Scenario("State confusion between global and local state", ({ Given, When, Then, And }) => {
143
+ Given("I create a mock with state confusion:", (_, docString: string) => {
144
+ mock = schmock({ state: { global: 1 } });
145
+ mock('GET /counter', ({ state }) => {
146
+ state.local = (state.local || 0) + 1;
147
+ return { global: state.global, local: state.local };
148
+ });
149
+ });
150
+
151
+ When("I request {string} twice", async (_, request: string) => {
152
+ const [method, path] = request.split(" ");
153
+ responses = [];
154
+ responses.push(await mock.handle(method as any, path));
155
+ responses.push(await mock.handle(method as any, path));
156
+ });
157
+
158
+ Then("the first response should have:", (_, docString: string) => {
159
+ const expected = JSON.parse(docString);
160
+ expect(responses[0].body).toEqual(expected);
161
+ });
162
+
163
+ And("the second response should have:", (_, docString: string) => {
164
+ const expected = JSON.parse(docString);
165
+ expect(responses[1].body).toEqual(expected);
166
+ });
167
+ });
168
+
169
+ Scenario("Query parameter edge cases", ({ Given, When, Then }) => {
170
+ Given("I create a mock handling query parameters:", (_, docString: string) => {
171
+ mock = schmock();
172
+ mock('GET /search', ({ query }) => ({
173
+ term: query.q,
174
+ page: query.page,
175
+ empty: query.empty
176
+ }));
177
+ });
178
+
179
+ When("I request {string}", async (_, request: string) => {
180
+ const [method, fullPath] = request.split(" ");
181
+ const [path, queryString] = fullPath.split("?");
182
+
183
+ const query: Record<string, string> = {};
184
+ if (queryString) {
185
+ queryString.split("&").forEach((param) => {
186
+ const [key, value] = param.split("=");
187
+ query[key] = value || "";
188
+ });
189
+ }
190
+
191
+ response = await mock.handle(method as any, path, { query });
192
+ });
193
+
194
+ Then("I should receive:", (_, docString: string) => {
195
+ const expected = JSON.parse(docString);
196
+ expect(response.body).toEqual(expected);
197
+ });
198
+ });
199
+
200
+ Scenario("Headers case sensitivity", ({ Given, When, Then }) => {
201
+ Given("I create a mock checking headers:", (_, docString: string) => {
202
+ mock = schmock();
203
+ mock('GET /auth', ({ headers }) => ({
204
+ auth: headers.authorization,
205
+ authUpper: headers.Authorization,
206
+ contentType: headers['content-type']
207
+ }));
208
+ });
209
+
210
+ When("I request {string} with headers:", async (_, request: string, docString: string) => {
211
+ const [method, path] = request.split(" ");
212
+ const headers = JSON.parse(docString);
213
+ response = await mock.handle(method as any, path, { headers });
214
+ });
215
+
216
+ Then("the header case sensitivity should show expected values", () => {
217
+ expect(response.body).toEqual({
218
+ auth: undefined,
219
+ authUpper: "Bearer token",
220
+ contentType: undefined
221
+ });
222
+ });
223
+ });
224
+
225
+ Scenario("Route precedence with similar paths", ({ Given, When, Then, And }) => {
226
+ Given("I create a mock with similar routes:", (_, docString: string) => {
227
+ mock = schmock();
228
+ mock('GET /users/profile', { type: 'profile' });
229
+ mock('GET /users/:id', ({ params }) => ({ type: 'user', id: params.id }));
230
+ });
231
+
232
+ When("I test both route precedence scenarios", async () => {
233
+ responses = [];
234
+ responses.push(await mock.handle('GET', '/users/profile'));
235
+ responses.push(await mock.handle('GET', '/users/123'));
236
+ });
237
+
238
+ Then("the profile route should return exact match:", (_, docString: string) => {
239
+ const expected = JSON.parse(docString);
240
+ expect(responses[0].body).toEqual(expected);
241
+ });
242
+
243
+ And("the parameterized route should match with param:", (_, docString: string) => {
244
+ const expected = JSON.parse(docString);
245
+ expect(responses[1].body).toEqual(expected);
246
+ });
247
+ });
248
+
249
+ Scenario("Plugin order affecting results", ({ Given, When, Then }) => {
250
+ Given("I create a mock with order-dependent plugins:", (_, docString: string) => {
251
+ mock = schmock();
252
+ const plugin1 = {
253
+ name: 'first',
254
+ process: (ctx: any, response: any) => ({
255
+ context: ctx,
256
+ response: { ...response, step: 1 }
257
+ })
258
+ };
259
+ const plugin2 = {
260
+ name: 'second',
261
+ process: (ctx: any, response: any) => ({
262
+ context: ctx,
263
+ response: { ...response, step: 2 }
264
+ })
265
+ };
266
+ mock('GET /order', { original: true })
267
+ .pipe(plugin1)
268
+ .pipe(plugin2);
269
+ });
270
+
271
+ When("I request {string}", async (_, request: string) => {
272
+ const [method, path] = request.split(" ");
273
+ response = await mock.handle(method as any, path);
274
+ });
275
+
276
+ Then("I should receive:", (_, docString: string) => {
277
+ const expected = JSON.parse(docString);
278
+ expect(response.body).toEqual(expected);
279
+ });
280
+ });
281
+
282
+ Scenario("Namespace confusion with absolute paths", ({ Given, When, Then, And }) => {
283
+ Given("I create a mock with namespace:", (_, docString: string) => {
284
+ mock = schmock({ namespace: '/api/v1' });
285
+ mock('GET /users', []);
286
+ });
287
+
288
+ When("I test both namespace scenarios", async () => {
289
+ responses = [];
290
+ responses.push(await mock.handle('GET', '/users'));
291
+ responses.push(await mock.handle('GET', '/api/v1/users'));
292
+ });
293
+
294
+ Then("the wrong namespace should receive status {int}", (_, status: number) => {
295
+ expect(responses[0].status).toBe(status);
296
+ });
297
+
298
+ And("the correct namespace should receive:", (_, docString: string) => {
299
+ const expected = JSON.parse(docString);
300
+ expect(responses[1].body).toEqual(expected);
301
+ });
302
+ });
303
+
304
+ Scenario("Plugin expecting different context structure", ({ Given, When, Then }) => {
305
+ Given("I create a mock with context-dependent plugin:", (_, docString: string) => {
306
+ mock = schmock();
307
+ const plugin = {
308
+ name: 'context-reader',
309
+ process: (ctx: any, response: any) => ({
310
+ context: ctx,
311
+ response: {
312
+ method: ctx.method,
313
+ path: ctx.path,
314
+ hasBody: !!ctx.body,
315
+ hasQuery: !!ctx.query && Object.keys(ctx.query).length > 0
316
+ }
317
+ })
318
+ };
319
+ mock('POST /analyze', null).pipe(plugin);
320
+ });
321
+
322
+ When("I request {string} with body:", async (_, request: string, docString: string) => {
323
+ const [method, fullPath] = request.split(" ");
324
+ const [path, queryString] = fullPath.split("?");
325
+
326
+ const query: Record<string, string> = {};
327
+ if (queryString) {
328
+ queryString.split("&").forEach((param) => {
329
+ const [key, value] = param.split("=");
330
+ query[key] = value || "";
331
+ });
332
+ }
333
+
334
+ const body = JSON.parse(docString);
335
+ response = await mock.handle(method as any, path, { body, query });
336
+ });
337
+
338
+ Then("I should receive:", (_, docString: string) => {
339
+ const expected = JSON.parse(docString);
340
+ expect(response.body).toEqual(expected);
341
+ });
342
+ });
343
+
344
+ Scenario("Response tuple format edge cases", ({ Given, When, Then, And }) => {
345
+ Given("I create a mock with tuple responses:", (_, docString: string) => {
346
+ mock = schmock();
347
+ mock('GET /created', [201]);
348
+ mock('GET /with-headers', [200, { data: true }, { 'x-custom': 'header' }]);
349
+ mock('GET /empty-with-status', [204, null]);
350
+ });
351
+
352
+ When("I test all tuple response formats", async () => {
353
+ responses = [];
354
+ responses.push(await mock.handle('GET', '/created'));
355
+ responses.push(await mock.handle('GET', '/with-headers'));
356
+ responses.push(await mock.handle('GET', '/empty-with-status'));
357
+ });
358
+
359
+ Then("the created endpoint should return status 201 with empty body", () => {
360
+ expect(responses[0].status).toBe(201);
361
+ expect(responses[0].body).toBeUndefined();
362
+ });
363
+
364
+ And("the headers endpoint should return status 200 with data and custom header", () => {
365
+ expect(responses[1].status).toBe(200);
366
+ expect(responses[1].body).toEqual({ data: true });
367
+ expect(responses[1].headers?.['x-custom']).toBe('header');
368
+ });
369
+
370
+ And("the empty endpoint should return status 204 with null body", () => {
371
+ expect(responses[2].status).toBe(204);
372
+ expect(responses[2].body).toBeUndefined();
373
+ });
374
+ });
375
+
376
+ Scenario("Common typos in method names", ({ Given, When, Then, And }) => {
377
+ let errors: Error[] = [];
378
+
379
+ Given("I attempt to create mocks with typo methods:", (_, docString: string) => {
380
+ mock = schmock();
381
+ });
382
+
383
+ When("I test all common method typos", () => {
384
+ errors = [];
385
+ const typos = ['GETS /users', 'post /users', 'GET/users'];
386
+
387
+ for (const typo of typos) {
388
+ try {
389
+ mock(typo, 'test');
390
+ errors.push(new Error('No error thrown')); // Should not happen
391
+ } catch (e) {
392
+ errors.push(e as Error);
393
+ }
394
+ }
395
+ });
396
+
397
+ Then("the wrong method typo should throw RouteParseError", () => {
398
+ expect(errors[0]).not.toBeNull();
399
+ expect(errors[0].constructor.name).toBe('RouteParseError');
400
+ expect(errors[0].message).toContain('Invalid route key format');
401
+ });
402
+
403
+ And("the lowercase method typo should throw RouteParseError", () => {
404
+ expect(errors[1]).not.toBeNull();
405
+ expect(errors[1].constructor.name).toBe('RouteParseError');
406
+ expect(errors[1].message).toContain('Invalid route key format');
407
+ });
408
+
409
+ And("the missing space typo should throw RouteParseError", () => {
410
+ expect(errors[2]).not.toBeNull();
411
+ expect(errors[2].constructor.name).toBe('RouteParseError');
412
+ expect(errors[2].message).toContain('Invalid route key format');
413
+ });
414
+ });
415
+
416
+ Scenario("Plugin returning unexpected structure", ({ Given, When, Then, And }) => {
417
+ Given("I create a mock with malformed plugin:", (_, docString: string) => {
418
+ mock = schmock();
419
+ const badPlugin = {
420
+ name: 'bad-structure',
421
+ process: () => ({ wrong: 'structure' })
422
+ };
423
+ mock('GET /bad', 'original').pipe(badPlugin);
424
+ });
425
+
426
+ When("I request {string}", async (_, request: string) => {
427
+ const [method, path] = request.split(" ");
428
+ response = await mock.handle(method as any, path);
429
+ });
430
+
431
+ Then("I should receive status {int}", (_, status: number) => {
432
+ expect(response.status).toBe(status);
433
+ });
434
+
435
+ And("the response should contain error {string}", (_, errorMessage: string) => {
436
+ expect(response.body.error).toContain(errorMessage);
437
+ });
438
+ });
439
+ });