@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,100 +1,100 @@
1
1
  import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
2
2
  import { expect } from "vitest";
3
3
  import { schmock } from "../index";
4
- import type { MockInstance } from "../types";
4
+ import type { CallableMockInstance } from "../types";
5
5
 
6
6
  const feature = await loadFeature("../../features/plugin-integration.feature");
7
7
 
8
8
  describeFeature(feature, ({ Scenario }) => {
9
- let mock: MockInstance<any>;
9
+ let mock: CallableMockInstance;
10
10
  let requestResponses: any[] = [];
11
11
 
12
12
  Scenario("Plugin state sharing with pipeline", ({ Given, When, Then, And }) => {
13
13
  requestResponses = [];
14
14
 
15
- Given("I create a mock with stateful plugin:", (_, docString: string) => {
16
- // Create mock with stateful plugin that persists state across requests
17
- mock = schmock({});
18
-
19
- // We need persistent state per route, so let's use a closure
20
- let routeState = { request_count: 0 };
21
-
22
- mock('GET /counter', null, {})
23
- .pipe({
24
- name: "counter-plugin",
25
- process: (ctx, response) => {
26
- // Update shared route state
27
- routeState.request_count = (routeState.request_count || 0) + 1;
28
-
29
- // Generate response if none exists
30
- if (!response) {
31
- return {
32
- context: ctx,
33
- response: {
34
- request_number: routeState.request_count,
35
- path: ctx.path,
36
- processed_at: new Date().toISOString()
37
- }
38
- };
39
- }
40
-
41
- // Pass through existing response
42
- return { context: ctx, response };
15
+ Given("I create a mock with a counter plugin using route state", () => {
16
+ mock = schmock({ state: {} });
17
+ mock("GET /counter", null, { contentType: "application/json" }).pipe({
18
+ name: "counter-plugin",
19
+ process: (ctx, response) => {
20
+ const routeState = ctx.routeState!;
21
+ routeState.request_count =
22
+ ((routeState.request_count as number) || 0) + 1;
23
+
24
+ if (!response) {
25
+ return {
26
+ context: ctx,
27
+ response: {
28
+ request_number: routeState.request_count,
29
+ path: ctx.path,
30
+ processed_at: new Date().toISOString(),
31
+ },
32
+ };
43
33
  }
44
- });
34
+
35
+ return { context: ctx, response };
36
+ },
37
+ });
45
38
  });
46
39
 
47
40
  When("I request {string} three times", async (_, request: string) => {
48
41
  const [method, path] = request.split(" ");
49
42
  requestResponses = [];
50
-
43
+
51
44
  for (let i = 0; i < 3; i++) {
52
45
  const response = await mock.handle(method as any, path);
53
46
  requestResponses.push(response);
54
47
  }
55
48
  });
56
49
 
57
- Then("each response should have incrementing {string} values", (_, property: string) => {
58
- expect(requestResponses).toHaveLength(3);
59
-
60
- for (let i = 0; i < requestResponses.length; i++) {
61
- expect(requestResponses[i].body[property]).toBe(i + 1);
62
- }
63
- });
50
+ Then(
51
+ "each response should have incrementing {string} values",
52
+ (_, property: string) => {
53
+ expect(requestResponses).toHaveLength(3);
64
54
 
65
- And("each response should have a {string} timestamp", (_, property: string) => {
66
- for (const response of requestResponses) {
67
- expect(response.body[property]).toBeDefined();
68
- expect(typeof response.body[property]).toBe('string');
69
- expect(new Date(response.body[property]).getTime()).toBeGreaterThan(0);
70
- }
71
- });
55
+ for (let i = 0; i < requestResponses.length; i++) {
56
+ expect(requestResponses[i].body[property]).toBe(i + 1);
57
+ }
58
+ },
59
+ );
60
+
61
+ And(
62
+ "each response should have a {string} timestamp",
63
+ (_, property: string) => {
64
+ for (const response of requestResponses) {
65
+ expect(response.body[property]).toBeDefined();
66
+ expect(typeof response.body[property]).toBe("string");
67
+ expect(new Date(response.body[property]).getTime()).toBeGreaterThan(0);
68
+ }
69
+ },
70
+ );
72
71
 
73
72
  And("the route state should persist across requests", () => {
74
- // Actually testing that state persists across requests for shared plugin state
75
- // The name is misleading but the test expects [1, 2, 3] which shows shared state
76
- const requestNumbers = requestResponses.map(r => r.body.request_number);
73
+ const requestNumbers = requestResponses.map(
74
+ (r) => r.body.request_number,
75
+ );
77
76
  expect(requestNumbers).toEqual([1, 2, 3]);
78
77
  });
79
78
  });
80
79
 
81
80
  Scenario("Multiple plugins in pipeline", ({ Given, When, Then }) => {
82
- Given("I create a mock with multiple plugins:", (_, docString: string) => {
83
- // Create mock with multiple plugins in pipeline
81
+ Given("I create a mock with auth and wrapper plugins", () => {
84
82
  mock = schmock({});
85
- mock('GET /users', () => [{ id: 1, name: 'John' }], {})
83
+ mock("GET /users", () => [{ id: 1, name: "John" }], {
84
+ contentType: "application/json",
85
+ })
86
86
  .pipe({
87
87
  name: "auth-plugin",
88
88
  process: (ctx, response) => {
89
89
  if (!ctx.headers.authorization) {
90
90
  throw new Error("Missing authorization");
91
91
  }
92
- ctx.state.set('user', { id: 1, name: 'Admin' });
92
+ ctx.state.set("user", { id: 1, name: "Admin" });
93
93
  return { context: ctx, response };
94
- }
94
+ },
95
95
  })
96
96
  .pipe({
97
- name: "wrapper-plugin",
97
+ name: "wrapper-plugin",
98
98
  process: (ctx, response) => {
99
99
  if (response) {
100
100
  return {
@@ -102,22 +102,27 @@ describeFeature(feature, ({ Scenario }) => {
102
102
  response: {
103
103
  data: response,
104
104
  meta: {
105
- user: ctx.state.get('user'),
106
- timestamp: "2025-01-31T10:15:30.123Z" // Fixed timestamp for test consistency
107
- }
108
- }
105
+ user: ctx.state.get("user"),
106
+ timestamp: "2025-01-31T10:15:30.123Z",
107
+ },
108
+ },
109
109
  };
110
110
  }
111
111
  return { context: ctx, response };
112
- }
112
+ },
113
113
  });
114
114
  });
115
115
 
116
- When("I request {string} with headers:", async (_, request: string, docString: string) => {
117
- const [method, path] = request.split(" ");
118
- const headers = JSON.parse(docString);
119
- requestResponses = [await mock.handle(method as any, path, { headers })];
120
- });
116
+ When(
117
+ "I request {string} with headers:",
118
+ async (_, request: string, docString: string) => {
119
+ const [method, path] = request.split(" ");
120
+ const headers = JSON.parse(docString);
121
+ requestResponses = [
122
+ await mock.handle(method as any, path, { headers }),
123
+ ];
124
+ },
125
+ );
121
126
 
122
127
  Then("I should receive:", (_, docString: string) => {
123
128
  const expected = JSON.parse(docString);
@@ -126,29 +131,31 @@ describeFeature(feature, ({ Scenario }) => {
126
131
  });
127
132
 
128
133
  Scenario("Plugin error handling", ({ Given, When, Then, And }) => {
129
- Given("I create a mock with error handling plugin:", (_, docString: string) => {
130
- // Create mock with error handling plugin
134
+ Given("I create a mock with an auth guard plugin", () => {
131
135
  mock = schmock({});
132
- mock('GET /protected', () => ({ secret: 'data' }), {})
133
- .pipe({
134
- name: "auth-plugin",
135
- process: (ctx, response) => {
136
- if (!ctx.headers.authorization) {
137
- // Return error response directly instead of throwing
138
- return {
139
- context: ctx,
140
- response: [401, { error: "Unauthorized", code: "AUTH_REQUIRED" }]
141
- };
142
- }
143
- return { context: ctx, response };
136
+ mock("GET /protected", () => ({ secret: "data" }), {
137
+ contentType: "application/json",
138
+ }).pipe({
139
+ name: "auth-plugin",
140
+ process: (ctx, response) => {
141
+ if (!ctx.headers.authorization) {
142
+ return {
143
+ context: ctx,
144
+ response: [401, { error: "Unauthorized", code: "AUTH_REQUIRED" }],
145
+ };
144
146
  }
145
- });
147
+ return { context: ctx, response };
148
+ },
149
+ });
146
150
  });
147
151
 
148
- When("I request {string} without authorization", async (_, request: string) => {
149
- const [method, path] = request.split(" ");
150
- requestResponses = [await mock.handle(method as any, path)];
151
- });
152
+ When(
153
+ "I request {string} without authorization",
154
+ async (_, request: string) => {
155
+ const [method, path] = request.split(" ");
156
+ requestResponses = [await mock.handle(method as any, path)];
157
+ },
158
+ );
152
159
 
153
160
  Then("the status should be {int}", (_, status: number) => {
154
161
  expect(requestResponses[0].status).toBe(status);
@@ -160,101 +167,91 @@ describeFeature(feature, ({ Scenario }) => {
160
167
  });
161
168
  });
162
169
 
163
- Scenario("Pipeline order and response transformation", ({ Given, When, Then }) => {
164
- Given("I create a mock with ordered plugins:", (_, docString: string) => {
165
- // Create mock with ordered plugins that transform response
166
- mock = schmock({});
167
- mock('GET /data', () => ({ value: 42 }), {})
168
- .pipe({
169
- name: "step-1",
170
- process: (ctx, response) => {
171
- ctx.state.set('step1', 'processed');
172
- // Transform the response by adding step1 property
173
- if (response) {
174
- return {
175
- context: ctx,
176
- response: { ...response, step1: 'processed' }
177
- };
178
- }
179
- return { context: ctx, response };
180
- }
181
- })
182
- .pipe({
183
- name: "step-2",
184
- process: (ctx, response) => {
185
- if (response) {
186
- return {
187
- context: ctx,
188
- response: { ...response, step2: 'processed' }
189
- };
190
- }
191
- return { context: ctx, response };
192
- }
170
+ Scenario(
171
+ "Pipeline order and response transformation",
172
+ ({ Given, When, Then }) => {
173
+ Given("I create a mock with three ordered step plugins", () => {
174
+ mock = schmock({});
175
+ mock("GET /data", () => ({ value: 42 }), {
176
+ contentType: "application/json",
193
177
  })
194
- .pipe({
195
- name: "step-3",
196
- process: (ctx, response) => {
197
- if (response) {
198
- return {
199
- context: ctx,
200
- response: { ...response, step3: 'processed' }
201
- };
202
- }
203
- return { context: ctx, response };
204
- }
205
- });
206
- });
178
+ .pipe({
179
+ name: "step-1",
180
+ process: (ctx, response) => {
181
+ ctx.state.set("step1", "processed");
182
+ if (response) {
183
+ return {
184
+ context: ctx,
185
+ response: { ...response, step1: "processed" },
186
+ };
187
+ }
188
+ return { context: ctx, response };
189
+ },
190
+ })
191
+ .pipe({
192
+ name: "step-2",
193
+ process: (ctx, response) => {
194
+ if (response) {
195
+ return {
196
+ context: ctx,
197
+ response: { ...response, step2: "processed" },
198
+ };
199
+ }
200
+ return { context: ctx, response };
201
+ },
202
+ })
203
+ .pipe({
204
+ name: "step-3",
205
+ process: (ctx, response) => {
206
+ if (response) {
207
+ return {
208
+ context: ctx,
209
+ response: { ...response, step3: "processed" },
210
+ };
211
+ }
212
+ return { context: ctx, response };
213
+ },
214
+ });
215
+ });
207
216
 
208
- When("I request {string}", async (_, request: string) => {
209
- const [method, path] = request.split(" ");
210
- requestResponses = [await mock.handle(method as any, path)];
211
- });
217
+ When("I request {string}", async (_, request: string) => {
218
+ const [method, path] = request.split(" ");
219
+ requestResponses = [await mock.handle(method as any, path)];
220
+ });
212
221
 
213
- Then("I should receive:", (_, docString: string) => {
214
- const expected = JSON.parse(docString);
215
- expect(requestResponses[0].body).toEqual(expected);
216
- });
217
- });
222
+ Then("I should receive:", (_, docString: string) => {
223
+ const expected = JSON.parse(docString);
224
+ expect(requestResponses[0].body).toEqual(expected);
225
+ });
226
+ },
227
+ );
218
228
 
219
229
  Scenario("Schema plugin in pipeline", ({ Given, When, Then, And }) => {
220
- Given("I create a mock with schema plugin:", (_, docString: string) => {
221
- // Create mock with schema plugin that generates data
230
+ Given("I create a mock with a metadata wrapper plugin", () => {
222
231
  mock = schmock({});
223
- mock('GET /users', (ctx) => {
224
- // Schema-based generator function
225
- const schema = {
226
- type: 'array',
227
- items: {
228
- type: 'object',
229
- properties: {
230
- id: { type: 'integer' },
231
- name: { type: 'string', faker: 'person.fullName' },
232
- email: { type: 'string', format: 'email' }
233
- }
234
- }
235
- };
236
- // Generate mock data from schema (simplified)
237
- return [
232
+ mock(
233
+ "GET /users",
234
+ () => [
238
235
  { id: 1, name: "John Doe", email: "john@example.com" },
239
- { id: 2, name: "Jane Smith", email: "jane@example.com" }
240
- ];
241
- }, {})
242
- .pipe({
243
- name: "add-metadata",
244
- process: (ctx, response) => {
245
- if (response && Array.isArray(response)) {
246
- return {
247
- context: ctx,
248
- response: {
249
- users: response,
250
- count: response.length,
251
- generated_at: new Date().toISOString()
252
- }
253
- };
254
- }
255
- return { context: ctx, response };
236
+ { id: 2, name: "Jane Smith", email: "jane@example.com" },
237
+ ],
238
+ { contentType: "application/json" },
239
+ ).pipe({
240
+ name: "add-metadata",
241
+ process: (ctx, response) => {
242
+ if (response && Array.isArray(response)) {
243
+ return {
244
+ context: ctx,
245
+ response: {
246
+ users: response,
247
+ count: response.length,
248
+ generated_at: new Date().toISOString(),
249
+ },
250
+ };
256
251
  }
257
- });
252
+ return { context: ctx, response };
253
+ },
254
+ });
258
255
  });
259
256
 
260
257
  When("I request {string}", async (_, request: string) => {
@@ -271,9 +268,12 @@ describeFeature(feature, ({ Scenario }) => {
271
268
  expect(requestResponses[0].body).toHaveProperty(property);
272
269
  });
273
270
 
274
- And("the response should have a {string} timestamp", (_, property: string) => {
275
- expect(requestResponses[0].body).toHaveProperty(property);
276
- expect(typeof requestResponses[0].body[property]).toBe('string');
277
- });
271
+ And(
272
+ "the response should have a {string} timestamp",
273
+ (_, property: string) => {
274
+ expect(requestResponses[0].body).toHaveProperty(property);
275
+ expect(typeof requestResponses[0].body[property]).toBe("string");
276
+ },
277
+ );
278
278
  });
279
- });
279
+ });