@schmock/core 1.0.3 → 1.1.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 (39) hide show
  1. package/dist/builder.d.ts +13 -5
  2. package/dist/builder.d.ts.map +1 -1
  3. package/dist/builder.js +147 -60
  4. package/dist/constants.d.ts +6 -0
  5. package/dist/constants.d.ts.map +1 -0
  6. package/dist/constants.js +20 -0
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +3 -1
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +20 -11
  12. package/dist/parser.d.ts.map +1 -1
  13. package/dist/parser.js +2 -17
  14. package/dist/types.d.ts +17 -210
  15. package/dist/types.d.ts.map +1 -1
  16. package/dist/types.js +1 -0
  17. package/package.json +4 -4
  18. package/src/builder.test.ts +2 -2
  19. package/src/builder.ts +232 -108
  20. package/src/constants.test.ts +59 -0
  21. package/src/constants.ts +25 -0
  22. package/src/errors.ts +3 -1
  23. package/src/index.ts +41 -29
  24. package/src/namespace.test.ts +3 -2
  25. package/src/parser.property.test.ts +495 -0
  26. package/src/parser.ts +2 -20
  27. package/src/route-matching.test.ts +1 -1
  28. package/src/steps/async-support.steps.ts +101 -91
  29. package/src/steps/basic-usage.steps.ts +49 -36
  30. package/src/steps/developer-experience.steps.ts +110 -94
  31. package/src/steps/error-handling.steps.ts +90 -66
  32. package/src/steps/fluent-api.steps.ts +75 -72
  33. package/src/steps/http-methods.steps.ts +33 -33
  34. package/src/steps/performance-reliability.steps.ts +52 -88
  35. package/src/steps/plugin-integration.steps.ts +176 -176
  36. package/src/steps/request-history.steps.ts +333 -0
  37. package/src/steps/state-concurrency.steps.ts +418 -316
  38. package/src/steps/stateful-workflows.steps.ts +138 -136
  39. package/src/types.ts +20 -259
@@ -1,7 +1,7 @@
1
1
  import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
2
2
  import { expect } from "vitest";
3
3
  import { schmock } from "../index";
4
- import type { CallableMockInstance } from "../types";
4
+ import type { CallableMockInstance, Plugin, PluginContext } from "../types";
5
5
 
6
6
  const feature = await loadFeature("../../features/developer-experience.feature");
7
7
 
@@ -9,12 +9,11 @@ describeFeature(feature, ({ Scenario }) => {
9
9
  let mock: CallableMockInstance;
10
10
  let response: any;
11
11
  let responses: any[] = [];
12
- let error: Error | null = null;
13
12
 
14
13
  Scenario("Forgetting to provide response data", ({ Given, When, Then, And }) => {
15
- Given("I create a mock without response data:", (_, docString: string) => {
14
+ Given("I create a mock with no response data on {string}", (_, route: string) => {
16
15
  mock = schmock();
17
- mock('GET /empty');
16
+ mock(route as Schmock.RouteKey, undefined);
18
17
  });
19
18
 
20
19
  When("I request {string}", async (_, request: string) => {
@@ -32,9 +31,9 @@ describeFeature(feature, ({ Scenario }) => {
32
31
  });
33
32
 
34
33
  Scenario("Using wrong parameter name in route", ({ Given, When, Then }) => {
35
- Given("I create a mock with parameter route:", (_, docString: string) => {
34
+ Given("I create a mock with a mismatched parameter name on {string}", (_, route: string) => {
36
35
  mock = schmock();
37
- mock('GET /users/:userId', ({ params }) => ({ id: params.id }));
36
+ mock(route as Schmock.RouteKey, ({ params }) => ({ id: params.id }));
38
37
  });
39
38
 
40
39
  When("I request {string}", async (_, request: string) => {
@@ -48,9 +47,9 @@ describeFeature(feature, ({ Scenario }) => {
48
47
  });
49
48
 
50
49
  Scenario("Correct parameter usage", ({ Given, When, Then }) => {
51
- Given("I create a mock with proper parameter usage:", (_, docString: string) => {
50
+ Given("I create a mock with a matching parameter name on {string}", (_, route: string) => {
52
51
  mock = schmock();
53
- mock('GET /users/:userId', ({ params }) => ({ id: params.userId }));
52
+ mock(route as Schmock.RouteKey, ({ params }) => ({ id: params.userId }));
54
53
  });
55
54
 
56
55
  When("I request {string}", async (_, request: string) => {
@@ -65,20 +64,20 @@ describeFeature(feature, ({ Scenario }) => {
65
64
  });
66
65
 
67
66
  Scenario("Mixing content types without explicit configuration", ({ Given, When, Then, And }) => {
68
- Given("I create a mock with mixed content types:", (_, docString: string) => {
67
+ Given("I create a mock with JSON, text, number, and boolean routes", () => {
69
68
  mock = schmock();
70
- mock('GET /json', { data: 'json' });
71
- mock('GET /text', 'plain text');
72
- mock('GET /number', 42);
73
- mock('GET /boolean', true);
69
+ mock("GET /json", { data: "json" });
70
+ mock("GET /text", "plain text");
71
+ mock("GET /number", 42);
72
+ mock("GET /boolean", true);
74
73
  });
75
74
 
76
75
  When("I test all mixed content type routes", async () => {
77
76
  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'));
77
+ responses.push(await mock.handle("GET", "/json"));
78
+ responses.push(await mock.handle("GET", "/text"));
79
+ responses.push(await mock.handle("GET", "/number"));
80
+ responses.push(await mock.handle("GET", "/boolean"));
82
81
  });
83
82
 
84
83
  Then("JSON route should have content-type {string}", (_, contentType: string) => {
@@ -99,9 +98,9 @@ describeFeature(feature, ({ Scenario }) => {
99
98
  });
100
99
 
101
100
  Scenario("Expecting JSON but getting string conversion", ({ Given, When, Then, And }) => {
102
- Given("I create a mock with number response:", (_, docString: string) => {
101
+ Given("I create a mock returning a decimal number on {string}", (_, route: string) => {
103
102
  mock = schmock();
104
- mock('GET /price', 19.99);
103
+ mock(route as Schmock.RouteKey, 19.99);
105
104
  });
106
105
 
107
106
  When("I request {string}", async (_, request: string) => {
@@ -119,9 +118,9 @@ describeFeature(feature, ({ Scenario }) => {
119
118
  });
120
119
 
121
120
  Scenario("Forgetting await with async generators", ({ Given, When, Then, And }) => {
122
- Given("I create a mock with async generator:", (_, docString: string) => {
121
+ Given("I create a mock with an async handler on {string}", (_, route: string) => {
123
122
  mock = schmock();
124
- mock('GET /data', async () => ({ async: true }));
123
+ mock(route as Schmock.RouteKey, async () => ({ async: true }));
125
124
  });
126
125
 
127
126
  When("I request {string}", async (_, request: string) => {
@@ -140,10 +139,11 @@ describeFeature(feature, ({ Scenario }) => {
140
139
  });
141
140
 
142
141
  Scenario("State confusion between global and local state", ({ Given, When, Then, And }) => {
143
- Given("I create a mock with state confusion:", (_, docString: string) => {
142
+ Given("I create a mock with global state and a local state counter", () => {
144
143
  mock = schmock({ state: { global: 1 } });
145
- mock('GET /counter', ({ state }) => {
146
- state.local = (state.local || 0) + 1;
144
+ mock("GET /counter", ({ state }) => {
145
+ const current = (state.local as number | undefined) || 0;
146
+ state.local = current + 1;
147
147
  return { global: state.global, local: state.local };
148
148
  });
149
149
  });
@@ -167,19 +167,19 @@ describeFeature(feature, ({ Scenario }) => {
167
167
  });
168
168
 
169
169
  Scenario("Query parameter edge cases", ({ Given, When, Then }) => {
170
- Given("I create a mock handling query parameters:", (_, docString: string) => {
170
+ Given("I create a mock that echoes query parameters on {string}", (_, route: string) => {
171
171
  mock = schmock();
172
- mock('GET /search', ({ query }) => ({
172
+ mock(route as Schmock.RouteKey, ({ query }) => ({
173
173
  term: query.q,
174
174
  page: query.page,
175
- empty: query.empty
175
+ empty: query.empty,
176
176
  }));
177
177
  });
178
178
 
179
179
  When("I request {string}", async (_, request: string) => {
180
180
  const [method, fullPath] = request.split(" ");
181
181
  const [path, queryString] = fullPath.split("?");
182
-
182
+
183
183
  const query: Record<string, string> = {};
184
184
  if (queryString) {
185
185
  queryString.split("&").forEach((param) => {
@@ -187,7 +187,7 @@ describeFeature(feature, ({ Scenario }) => {
187
187
  query[key] = value || "";
188
188
  });
189
189
  }
190
-
190
+
191
191
  response = await mock.handle(method as any, path, { query });
192
192
  });
193
193
 
@@ -198,12 +198,12 @@ describeFeature(feature, ({ Scenario }) => {
198
198
  });
199
199
 
200
200
  Scenario("Headers case sensitivity", ({ Given, When, Then }) => {
201
- Given("I create a mock checking headers:", (_, docString: string) => {
201
+ Given("I create a mock that echoes headers on {string}", (_, route: string) => {
202
202
  mock = schmock();
203
- mock('GET /auth', ({ headers }) => ({
203
+ mock(route as Schmock.RouteKey, ({ headers }) => ({
204
204
  auth: headers.authorization,
205
205
  authUpper: headers.Authorization,
206
- contentType: headers['content-type']
206
+ contentType: headers["content-type"],
207
207
  }));
208
208
  });
209
209
 
@@ -214,25 +214,25 @@ describeFeature(feature, ({ Scenario }) => {
214
214
  });
215
215
 
216
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
217
+ expect(response.body).toEqual({
218
+ auth: undefined,
219
+ authUpper: "Bearer token",
220
+ contentType: undefined,
221
221
  });
222
222
  });
223
223
  });
224
224
 
225
225
  Scenario("Route precedence with similar paths", ({ Given, When, Then, And }) => {
226
- Given("I create a mock with similar routes:", (_, docString: string) => {
226
+ Given("I create a mock with an exact route and a parameterized route on {string}", (_, basePath: string) => {
227
227
  mock = schmock();
228
- mock('GET /users/profile', { type: 'profile' });
229
- mock('GET /users/:id', ({ params }) => ({ type: 'user', id: params.id }));
228
+ mock(`GET ${basePath}/profile`, { type: "profile" });
229
+ mock(`GET ${basePath}/:id`, ({ params }) => ({ type: "user", id: params.id }));
230
230
  });
231
231
 
232
232
  When("I test both route precedence scenarios", async () => {
233
233
  responses = [];
234
- responses.push(await mock.handle('GET', '/users/profile'));
235
- responses.push(await mock.handle('GET', '/users/123'));
234
+ responses.push(await mock.handle("GET", "/users/profile"));
235
+ responses.push(await mock.handle("GET", "/users/123"));
236
236
  });
237
237
 
238
238
  Then("the profile route should return exact match:", (_, docString: string) => {
@@ -247,25 +247,23 @@ describeFeature(feature, ({ Scenario }) => {
247
247
  });
248
248
 
249
249
  Scenario("Plugin order affecting results", ({ Given, When, Then }) => {
250
- Given("I create a mock with order-dependent plugins:", (_, docString: string) => {
250
+ Given("I create a mock with two plugins that each set a step number", () => {
251
251
  mock = schmock();
252
- const plugin1 = {
253
- name: 'first',
254
- process: (ctx: any, response: any) => ({
252
+ const plugin1: Plugin = {
253
+ name: "first",
254
+ process: (ctx: PluginContext, response: unknown) => ({
255
255
  context: ctx,
256
- response: { ...response, step: 1 }
257
- })
256
+ response: { ...(response as Record<string, unknown>), step: 1 },
257
+ }),
258
258
  };
259
- const plugin2 = {
260
- name: 'second',
261
- process: (ctx: any, response: any) => ({
259
+ const plugin2: Plugin = {
260
+ name: "second",
261
+ process: (ctx: PluginContext, response: unknown) => ({
262
262
  context: ctx,
263
- response: { ...response, step: 2 }
264
- })
263
+ response: { ...(response as Record<string, unknown>), step: 2 },
264
+ }),
265
265
  };
266
- mock('GET /order', { original: true })
267
- .pipe(plugin1)
268
- .pipe(plugin2);
266
+ mock("GET /order", { original: true }).pipe(plugin1).pipe(plugin2);
269
267
  });
270
268
 
271
269
  When("I request {string}", async (_, request: string) => {
@@ -280,15 +278,15 @@ describeFeature(feature, ({ Scenario }) => {
280
278
  });
281
279
 
282
280
  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', []);
281
+ Given("I create a mock with namespace {string} and a users route", (_, namespace: string) => {
282
+ mock = schmock({ namespace });
283
+ mock("GET /users", []);
286
284
  });
287
285
 
288
286
  When("I test both namespace scenarios", async () => {
289
287
  responses = [];
290
- responses.push(await mock.handle('GET', '/users'));
291
- responses.push(await mock.handle('GET', '/api/v1/users'));
288
+ responses.push(await mock.handle("GET", "/users"));
289
+ responses.push(await mock.handle("GET", "/api/v1/users"));
292
290
  });
293
291
 
294
292
  Then("the wrong namespace should receive status {int}", (_, status: number) => {
@@ -302,27 +300,27 @@ describeFeature(feature, ({ Scenario }) => {
302
300
  });
303
301
 
304
302
  Scenario("Plugin expecting different context structure", ({ Given, When, Then }) => {
305
- Given("I create a mock with context-dependent plugin:", (_, docString: string) => {
303
+ Given("I create a mock with a plugin that reads context properties", () => {
306
304
  mock = schmock();
307
- const plugin = {
308
- name: 'context-reader',
309
- process: (ctx: any, response: any) => ({
305
+ const plugin: Plugin = {
306
+ name: "context-reader",
307
+ process: (ctx: PluginContext, _response: unknown) => ({
310
308
  context: ctx,
311
309
  response: {
312
310
  method: ctx.method,
313
311
  path: ctx.path,
314
312
  hasBody: !!ctx.body,
315
- hasQuery: !!ctx.query && Object.keys(ctx.query).length > 0
316
- }
317
- })
313
+ hasQuery: !!ctx.query && Object.keys(ctx.query).length > 0,
314
+ },
315
+ }),
318
316
  };
319
- mock('POST /analyze', null).pipe(plugin);
317
+ mock("POST /analyze", null).pipe(plugin);
320
318
  });
321
319
 
322
320
  When("I request {string} with body:", async (_, request: string, docString: string) => {
323
321
  const [method, fullPath] = request.split(" ");
324
322
  const [path, queryString] = fullPath.split("?");
325
-
323
+
326
324
  const query: Record<string, string> = {};
327
325
  if (queryString) {
328
326
  queryString.split("&").forEach((param) => {
@@ -330,7 +328,7 @@ describeFeature(feature, ({ Scenario }) => {
330
328
  query[key] = value || "";
331
329
  });
332
330
  }
333
-
331
+
334
332
  const body = JSON.parse(docString);
335
333
  response = await mock.handle(method as any, path, { body, query });
336
334
  });
@@ -342,18 +340,18 @@ describeFeature(feature, ({ Scenario }) => {
342
340
  });
343
341
 
344
342
  Scenario("Response tuple format edge cases", ({ Given, When, Then, And }) => {
345
- Given("I create a mock with tuple responses:", (_, docString: string) => {
343
+ Given("I create a mock with three tuple response routes", () => {
346
344
  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]);
345
+ mock("GET /created", [201]);
346
+ mock("GET /with-headers", [200, { data: true }, { "x-custom": "header" }]);
347
+ mock("GET /empty-with-status", [204, null]);
350
348
  });
351
349
 
352
350
  When("I test all tuple response formats", async () => {
353
351
  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'));
352
+ responses.push(await mock.handle("GET", "/created"));
353
+ responses.push(await mock.handle("GET", "/with-headers"));
354
+ responses.push(await mock.handle("GET", "/empty-with-status"));
357
355
  });
358
356
 
359
357
  Then("the created endpoint should return status 201 with empty body", () => {
@@ -364,7 +362,7 @@ describeFeature(feature, ({ Scenario }) => {
364
362
  And("the headers endpoint should return status 200 with data and custom header", () => {
365
363
  expect(responses[1].status).toBe(200);
366
364
  expect(responses[1].body).toEqual({ data: true });
367
- expect(responses[1].headers?.['x-custom']).toBe('header');
365
+ expect(responses[1].headers?.["x-custom"]).toBe("header");
368
366
  });
369
367
 
370
368
  And("the empty endpoint should return status 204 with null body", () => {
@@ -376,18 +374,18 @@ describeFeature(feature, ({ Scenario }) => {
376
374
  Scenario("Common typos in method names", ({ Given, When, Then, And }) => {
377
375
  let errors: Error[] = [];
378
376
 
379
- Given("I attempt to create mocks with typo methods:", (_, docString: string) => {
377
+ Given("I create an empty mock for testing method typos", () => {
380
378
  mock = schmock();
381
379
  });
382
380
 
383
381
  When("I test all common method typos", () => {
384
382
  errors = [];
385
- const typos = ['GETS /users', 'post /users', 'GET/users'];
386
-
383
+ const typos = ["GETS /users", "post /users", "GET/users"];
384
+
387
385
  for (const typo of typos) {
388
386
  try {
389
- mock(typo, 'test');
390
- errors.push(new Error('No error thrown')); // Should not happen
387
+ mock(typo as Schmock.RouteKey, "test");
388
+ errors.push(new Error("No error thrown"));
391
389
  } catch (e) {
392
390
  errors.push(e as Error);
393
391
  }
@@ -396,31 +394,49 @@ describeFeature(feature, ({ Scenario }) => {
396
394
 
397
395
  Then("the wrong method typo should throw RouteParseError", () => {
398
396
  expect(errors[0]).not.toBeNull();
399
- expect(errors[0].constructor.name).toBe('RouteParseError');
400
- expect(errors[0].message).toContain('Invalid route key format');
397
+ expect(errors[0].constructor.name).toBe("RouteParseError");
398
+ expect(errors[0].message).toContain("Invalid route key format");
401
399
  });
402
400
 
403
401
  And("the lowercase method typo should throw RouteParseError", () => {
404
402
  expect(errors[1]).not.toBeNull();
405
- expect(errors[1].constructor.name).toBe('RouteParseError');
406
- expect(errors[1].message).toContain('Invalid route key format');
403
+ expect(errors[1].constructor.name).toBe("RouteParseError");
404
+ expect(errors[1].message).toContain("Invalid route key format");
407
405
  });
408
406
 
409
407
  And("the missing space typo should throw RouteParseError", () => {
410
408
  expect(errors[2]).not.toBeNull();
411
- expect(errors[2].constructor.name).toBe('RouteParseError');
412
- expect(errors[2].message).toContain('Invalid route key format');
409
+ expect(errors[2].constructor.name).toBe("RouteParseError");
410
+ expect(errors[2].message).toContain("Invalid route key format");
411
+ });
412
+ });
413
+
414
+ Scenario("Registering duplicate routes first route wins", ({ Given, When, Then }) => {
415
+ Given("I create a mock with two routes on {string} with different data", (_, route: string) => {
416
+ mock = schmock();
417
+ mock(route as Schmock.RouteKey, [{ id: 1 }]);
418
+ mock(route as Schmock.RouteKey, [{ id: 2 }]);
419
+ });
420
+
421
+ When("I request {string}", async (_, request: string) => {
422
+ const [method, path] = request.split(" ");
423
+ response = await mock.handle(method as any, path);
424
+ });
425
+
426
+ Then("the first route response should win:", (_, docString: string) => {
427
+ const expected = JSON.parse(docString);
428
+ expect(response.body).toEqual(expected);
413
429
  });
414
430
  });
415
431
 
416
432
  Scenario("Plugin returning unexpected structure", ({ Given, When, Then, And }) => {
417
- Given("I create a mock with malformed plugin:", (_, docString: string) => {
433
+ Given("I create a mock with a plugin that returns an invalid structure", () => {
418
434
  mock = schmock();
419
435
  const badPlugin = {
420
- name: 'bad-structure',
421
- process: () => ({ wrong: 'structure' })
436
+ name: "bad-structure",
437
+ process: () => ({ wrong: "structure" }),
422
438
  };
423
- mock('GET /bad', 'original').pipe(badPlugin);
439
+ mock("GET /bad", "original").pipe(badPlugin as any);
424
440
  });
425
441
 
426
442
  When("I request {string}", async (_, request: string) => {
@@ -436,4 +452,4 @@ describeFeature(feature, ({ Scenario }) => {
436
452
  expect(response.body.error).toContain(errorMessage);
437
453
  });
438
454
  });
439
- });
455
+ });