@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,397 @@
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/http-methods.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("GET method with query parameters", ({ Given, When, Then }) => {
15
+ Given("I create a mock with GET endpoint:", (_, docString: string) => {
16
+ mock = schmock();
17
+ mock('GET /search', ({ query }) => ({
18
+ results: [],
19
+ query: query.q,
20
+ page: parseInt(query.page || '1'),
21
+ limit: parseInt(query.limit || '10')
22
+ }));
23
+ });
24
+
25
+ When("I make a GET request to {string}", async (_, path: string) => {
26
+ const [pathname, queryString] = path.split("?");
27
+ const query: Record<string, string> = {};
28
+ if (queryString) {
29
+ queryString.split("&").forEach((param) => {
30
+ const [key, value] = param.split("=");
31
+ query[key] = value;
32
+ });
33
+ }
34
+ response = await mock.handle('GET', pathname, { query });
35
+ });
36
+
37
+ Then("I should receive GET method response:", (_, docString: string) => {
38
+ const expected = JSON.parse(docString);
39
+ expect(response.body).toEqual(expected);
40
+ });
41
+ });
42
+
43
+ Scenario("POST method with JSON body", ({ Given, When, Then, And }) => {
44
+ Given("I create a mock with POST endpoint:", (_, docString: string) => {
45
+ mock = schmock();
46
+ mock('POST /users', ({ body }) => [201, {
47
+ id: 123,
48
+ ...body,
49
+ createdAt: '2023-01-01T00:00:00Z'
50
+ }]);
51
+ });
52
+
53
+ When("I make a POST request to {string} with JSON body:", async (_, path: string, docString: string) => {
54
+ const body = JSON.parse(docString);
55
+ response = await mock.handle('POST', path, { body });
56
+ });
57
+
58
+ Then("I should receive status {int}", (_, status: number) => {
59
+ expect(response.status).toBe(status);
60
+ });
61
+
62
+ And("I should receive POST method response:", (_, docString: string) => {
63
+ const expected = JSON.parse(docString);
64
+ expect(response.body).toEqual(expected);
65
+ });
66
+ });
67
+
68
+ Scenario("PUT method for resource updates", ({ Given, When, Then }) => {
69
+ Given("I create a mock with PUT endpoint:", (_, docString: string) => {
70
+ mock = schmock();
71
+ mock('PUT /users/:id', ({ params, body }) => ({
72
+ id: parseInt(params.id),
73
+ ...body,
74
+ updatedAt: '2023-01-01T00:00:00Z'
75
+ }));
76
+ });
77
+
78
+ When("I make a PUT request to {string} with JSON body:", async (_, path: string, docString: string) => {
79
+ const body = JSON.parse(docString);
80
+ response = await mock.handle('PUT', path, { body });
81
+ });
82
+
83
+ Then("I should receive PUT method response:", (_, docString: string) => {
84
+ const expected = JSON.parse(docString);
85
+ expect(response.body).toEqual(expected);
86
+ });
87
+ });
88
+
89
+ Scenario("DELETE method with confirmation", ({ Given, When, Then, And }) => {
90
+ Given("I create a mock with DELETE endpoint:", (_, docString: string) => {
91
+ mock = schmock();
92
+ mock('DELETE /users/:id', ({ params }) => [204, null]);
93
+ });
94
+
95
+ When("I make a DELETE request to {string}", async (_, path: string) => {
96
+ response = await mock.handle('DELETE', path);
97
+ });
98
+
99
+ Then("I should receive status {int}", (_, status: number) => {
100
+ expect(response.status).toBe(status);
101
+ });
102
+
103
+ And("the DELETE response body should be empty", () => {
104
+ expect(response.body).toBeUndefined();
105
+ });
106
+ });
107
+
108
+ Scenario("PATCH method for partial updates", ({ Given, When, Then }) => {
109
+ Given("I create a mock with PATCH endpoint:", (_, docString: string) => {
110
+ mock = schmock();
111
+ mock('PATCH /users/:id', ({ params, body }) => ({
112
+ id: parseInt(params.id),
113
+ email: 'existing@example.com',
114
+ ...body,
115
+ updatedAt: '2023-01-01T00:00:00Z'
116
+ }));
117
+ });
118
+
119
+ When("I make a PATCH request to {string} with JSON body:", async (_, path: string, docString: string) => {
120
+ const body = JSON.parse(docString);
121
+ response = await mock.handle('PATCH', path, { body });
122
+ });
123
+
124
+ Then("I should receive PATCH method response:", (_, docString: string) => {
125
+ const expected = JSON.parse(docString);
126
+ expect(response.body).toEqual(expected);
127
+ });
128
+ });
129
+
130
+ Scenario("HEAD method returns headers only", ({ Given, When, Then, And }) => {
131
+ Given("I create a mock with HEAD endpoint:", (_, docString: string) => {
132
+ mock = schmock();
133
+ mock('HEAD /users/:id', ({ params }) => [200, null, {
134
+ 'Content-Type': 'application/json',
135
+ 'Last-Modified': 'Wed, 01 Jan 2023 00:00:00 GMT',
136
+ 'Content-Length': '156'
137
+ }]);
138
+ });
139
+
140
+ When("I make a HEAD request to {string}", async (_, path: string) => {
141
+ response = await mock.handle('HEAD', path);
142
+ });
143
+
144
+ Then("I should receive status {int}", (_, status: number) => {
145
+ expect(response.status).toBe(status);
146
+ });
147
+
148
+ And("the HEAD response body should be empty", () => {
149
+ expect(response.body).toBeUndefined();
150
+ });
151
+
152
+ And("the HEAD response should have proper headers set", () => {
153
+ expect(response.headers?.['Content-Type']).toBe('application/json');
154
+ expect(response.headers?.['Last-Modified']).toBe('Wed, 01 Jan 2023 00:00:00 GMT');
155
+ expect(response.headers?.['Content-Length']).toBe('156');
156
+ });
157
+ });
158
+
159
+ Scenario("OPTIONS method for CORS preflight", ({ Given, When, Then, And }) => {
160
+ Given("I create a mock with OPTIONS endpoint:", (_, docString: string) => {
161
+ mock = schmock();
162
+ mock('OPTIONS /api/users', () => [200, null, {
163
+ 'Access-Control-Allow-Origin': '*',
164
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
165
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization'
166
+ }]);
167
+ });
168
+
169
+ When("I make an OPTIONS request to {string}", async (_, path: string) => {
170
+ response = await mock.handle('OPTIONS', path);
171
+ });
172
+
173
+ Then("I should receive status {int}", (_, status: number) => {
174
+ expect(response.status).toBe(status);
175
+ });
176
+
177
+ And("the OPTIONS response body should be empty", () => {
178
+ expect(response.body).toBeUndefined();
179
+ });
180
+
181
+ And("the OPTIONS response should have header {string} with value {string}", (_, headerName: string, headerValue: string) => {
182
+ expect(response.headers?.[headerName]).toBe(headerValue);
183
+ });
184
+ });
185
+
186
+ Scenario("Multiple methods on same path", ({ Given, When, Then, And }) => {
187
+ Given("I create a mock with multiple methods on same path:", (_, docString: string) => {
188
+ mock = schmock();
189
+ mock('GET /resource', { action: 'read' });
190
+ mock('POST /resource', { action: 'create' });
191
+ mock('PUT /resource', { action: 'update' });
192
+ mock('DELETE /resource', { action: 'delete' });
193
+ });
194
+
195
+ When("I test all methods on {string}", async (_, path: string) => {
196
+ responses = [];
197
+ responses.push(await mock.handle('GET', path));
198
+ responses.push(await mock.handle('POST', path));
199
+ responses.push(await mock.handle('PUT', path));
200
+ responses.push(await mock.handle('DELETE', path));
201
+ });
202
+
203
+ Then("the GET method should return:", (_, docString: string) => {
204
+ const expected = JSON.parse(docString);
205
+ expect(responses[0].body).toEqual(expected);
206
+ });
207
+
208
+ And("the POST method should return:", (_, docString: string) => {
209
+ const expected = JSON.parse(docString);
210
+ expect(responses[1].body).toEqual(expected);
211
+ });
212
+
213
+ And("the PUT method should return:", (_, docString: string) => {
214
+ const expected = JSON.parse(docString);
215
+ expect(responses[2].body).toEqual(expected);
216
+ });
217
+
218
+ And("the DELETE method should return:", (_, docString: string) => {
219
+ const expected = JSON.parse(docString);
220
+ expect(responses[3].body).toEqual(expected);
221
+ });
222
+ });
223
+
224
+ Scenario("Method-specific content types", ({ Given, When, Then, And }) => {
225
+ Given("I create a mock with method-specific content types:", (_, docString: string) => {
226
+ mock = schmock();
227
+ mock('GET /data.json', { data: 'json' });
228
+ mock('GET /data.xml', '<data>xml</data>', { contentType: 'application/xml' });
229
+ mock('GET /data.txt', 'plain text data');
230
+ mock('POST /upload', 'File uploaded successfully', { contentType: 'text/plain' });
231
+ });
232
+
233
+ When("I test method-specific content types", async () => {
234
+ responses = [];
235
+ responses.push(await mock.handle('GET', '/data.json'));
236
+ responses.push(await mock.handle('GET', '/data.xml'));
237
+ responses.push(await mock.handle('GET', '/data.txt'));
238
+ responses.push(await mock.handle('POST', '/upload'));
239
+ });
240
+
241
+ Then("the JSON endpoint should have content-type {string}", (_, contentType: string) => {
242
+ expect(responses[0].headers?.["content-type"]).toBe(contentType);
243
+ });
244
+
245
+ And("the XML endpoint should have content-type {string}", (_, contentType: string) => {
246
+ expect(responses[1].headers?.["content-type"]).toBe(contentType);
247
+ });
248
+
249
+ And("the text endpoint should have content-type {string}", (_, contentType: string) => {
250
+ expect(responses[2].headers?.["content-type"]).toBe(contentType);
251
+ });
252
+
253
+ And("the upload endpoint should have content-type {string}", (_, contentType: string) => {
254
+ expect(responses[3].headers?.["content-type"]).toBe(contentType);
255
+ });
256
+ });
257
+
258
+ Scenario("Method case sensitivity", ({ Given, When, Then }) => {
259
+ error = null;
260
+
261
+ Given("I create a mock with lowercase method:", (_, docString: string) => {
262
+ mock = schmock();
263
+ });
264
+
265
+ When("I attempt to create a mock with lowercase method", () => {
266
+ error = null;
267
+ try {
268
+ mock('get /test', { method: 'get' });
269
+ } catch (e) {
270
+ error = e as Error;
271
+ }
272
+ });
273
+
274
+ Then("it should throw RouteParseError for invalid method case", () => {
275
+ expect(error).not.toBeNull();
276
+ expect(error!.constructor.name).toBe('RouteParseError');
277
+ expect(error!.message).toContain('Invalid route key format');
278
+ });
279
+ });
280
+
281
+ Scenario("Unsupported HTTP methods", ({ Given, When, Then }) => {
282
+ error = null;
283
+
284
+ Given("I create a mock with custom method:", (_, docString: string) => {
285
+ mock = schmock();
286
+ });
287
+
288
+ When("I attempt to create a mock with unsupported method", () => {
289
+ error = null;
290
+ try {
291
+ mock('CUSTOM /endpoint', { custom: true });
292
+ } catch (e) {
293
+ error = e as Error;
294
+ }
295
+ });
296
+
297
+ Then("it should throw RouteParseError for unsupported method", () => {
298
+ expect(error).not.toBeNull();
299
+ expect(error!.constructor.name).toBe('RouteParseError');
300
+ expect(error!.message).toContain('Invalid route key format');
301
+ });
302
+ });
303
+
304
+ Scenario("Method with special characters in path", ({ Given, When, Then }) => {
305
+ Given("I create a mock with special characters:", (_, docString: string) => {
306
+ mock = schmock();
307
+ mock('GET /api/v1/users/:id/posts/:post-id', ({ params }) => ({
308
+ userId: params.id,
309
+ postId: params['post-id']
310
+ }));
311
+ });
312
+
313
+ When("I make a GET request to {string}", async (_, path: string) => {
314
+ response = await mock.handle('GET', path);
315
+ });
316
+
317
+ Then("I should receive special characters response:", (_, docString: string) => {
318
+ const expected = JSON.parse(docString);
319
+ expect(response.body).toEqual(expected);
320
+ });
321
+ });
322
+
323
+ Scenario("Method with request headers validation", ({ Given, When, Then, And }) => {
324
+ Given("I create a mock with header validation:", (_, docString: string) => {
325
+ mock = schmock();
326
+ mock('POST /secure', ({ headers, body }) => {
327
+ if (headers.authorization !== 'Bearer valid-token') {
328
+ return [401, { error: 'Unauthorized' }];
329
+ }
330
+ return [200, { message: 'Success', data: body }];
331
+ });
332
+ });
333
+
334
+ When("I make a POST request with valid headers", async () => {
335
+ response = await mock.handle('POST', '/secure', {
336
+ headers: { authorization: 'Bearer valid-token' },
337
+ body: { test: true }
338
+ });
339
+ });
340
+
341
+ Then("I should receive authorized response:", (_, docString: string) => {
342
+ const expected = JSON.parse(docString);
343
+ expect(response.body).toEqual(expected);
344
+ });
345
+
346
+ When("I make a POST request with invalid headers", async () => {
347
+ response = await mock.handle('POST', '/secure', {
348
+ headers: { authorization: 'Bearer invalid-token' },
349
+ body: { test: true }
350
+ });
351
+ });
352
+
353
+ Then("I should receive status {int}", (_, status: number) => {
354
+ expect(response.status).toBe(status);
355
+ });
356
+
357
+ And("I should receive unauthorized response:", (_, docString: string) => {
358
+ const expected = JSON.parse(docString);
359
+ expect(response.body).toEqual(expected);
360
+ });
361
+ });
362
+
363
+ Scenario("Method chaining with plugins", ({ Given, When, Then, And }) => {
364
+ Given("I create a mock with method-specific plugins:", (_, docString: string) => {
365
+ mock = schmock();
366
+ const loggerPlugin = {
367
+ name: 'method-logger',
368
+ process: (ctx: any, response: any) => ({
369
+ context: ctx,
370
+ response: {
371
+ ...response,
372
+ method: ctx.method,
373
+ logged: true
374
+ }
375
+ })
376
+ };
377
+ mock('GET /logged', { data: 'get' }).pipe(loggerPlugin);
378
+ mock('POST /logged', { data: 'post' }).pipe(loggerPlugin);
379
+ });
380
+
381
+ When("I test method chaining with plugins", async () => {
382
+ responses = [];
383
+ responses.push(await mock.handle('GET', '/logged'));
384
+ responses.push(await mock.handle('POST', '/logged'));
385
+ });
386
+
387
+ Then("the GET with plugin should return:", (_, docString: string) => {
388
+ const expected = JSON.parse(docString);
389
+ expect(responses[0].body).toEqual(expected);
390
+ });
391
+
392
+ And("the POST with plugin should return:", (_, docString: string) => {
393
+ const expected = JSON.parse(docString);
394
+ expect(responses[1].body).toEqual(expected);
395
+ });
396
+ });
397
+ });