@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,255 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { schmock } from "./index";
3
+
4
+ describe("response parsing", () => {
5
+ describe("tuple response formats", () => {
6
+ it("handles status-only tuple [status]", async () => {
7
+ const mock = schmock();
8
+ mock("GET /status-only", () => [204] as [number]);
9
+
10
+ const response = await mock.handle("GET", "/status-only");
11
+
12
+ expect(response.status).toBe(204);
13
+ expect(response.body).toBeUndefined();
14
+ expect(response.headers).toEqual({});
15
+ });
16
+
17
+ it("handles [status, body] tuple", async () => {
18
+ const mock = schmock();
19
+ mock("POST /create", () => [201, { id: 123, created: true }]);
20
+
21
+ const response = await mock.handle("POST", "/create");
22
+
23
+ expect(response.status).toBe(201);
24
+ expect(response.body).toEqual({ id: 123, created: true });
25
+ expect(response.headers).toEqual({});
26
+ });
27
+
28
+ it("handles [status, body, headers] tuple", async () => {
29
+ const mock = schmock();
30
+ mock(
31
+ "POST /upload",
32
+ () =>
33
+ [
34
+ 201,
35
+ { fileId: "abc123" },
36
+ {
37
+ Location: "/files/abc123",
38
+ "Content-Type": "application/json",
39
+ },
40
+ ] as [number, any, Record<string, string>],
41
+ );
42
+
43
+ const response = await mock.handle("POST", "/upload");
44
+
45
+ expect(response.status).toBe(201);
46
+ expect(response.body).toEqual({ fileId: "abc123" });
47
+ expect(response.headers).toEqual({
48
+ Location: "/files/abc123",
49
+ "Content-Type": "application/json",
50
+ });
51
+ });
52
+
53
+ it("handles empty headers object in tuple", async () => {
54
+ const mock = schmock();
55
+ mock("GET /test", () => [200, "OK", {}]);
56
+
57
+ const response = await mock.handle("GET", "/test");
58
+
59
+ expect(response.status).toBe(200);
60
+ expect(response.body).toBe("OK");
61
+ expect(response.headers).toEqual({});
62
+ });
63
+
64
+ it("ignores extra tuple elements beyond [status, body, headers]", async () => {
65
+ const mock = schmock();
66
+ mock(
67
+ "GET /extra",
68
+ () => [200, "data", {}, "ignored", "also-ignored"] as any,
69
+ );
70
+
71
+ const response = await mock.handle("GET", "/extra");
72
+
73
+ expect(response.status).toBe(200);
74
+ expect(response.body).toBe("data");
75
+ expect(response.headers).toEqual({});
76
+ });
77
+
78
+ it("treats non-numeric first element as body, not status", async () => {
79
+ const mock = schmock();
80
+ mock("GET /array-body", () => ["item1", "item2", "item3"]);
81
+
82
+ const response = await mock.handle("GET", "/array-body");
83
+
84
+ expect(response.status).toBe(200);
85
+ expect(response.body).toEqual(["item1", "item2", "item3"]);
86
+ expect(response.headers).toEqual({ "content-type": "application/json" });
87
+ });
88
+ });
89
+
90
+ describe("various response types", () => {
91
+ it("handles string responses", async () => {
92
+ const mock = schmock();
93
+ mock("GET /text", () => "Simple text response");
94
+
95
+ const response = await mock.handle("GET", "/text");
96
+
97
+ expect(response.status).toBe(200);
98
+ expect(response.body).toBe("Simple text response");
99
+ expect(response.headers).toEqual({ "content-type": "application/json" });
100
+ });
101
+
102
+ it("handles number responses", async () => {
103
+ const mock = schmock();
104
+ mock("GET /number", () => 42);
105
+
106
+ const response = await mock.handle("GET", "/number");
107
+
108
+ expect(response.status).toBe(200);
109
+ expect(response.body).toBe(42);
110
+ expect(response.headers).toEqual({ "content-type": "application/json" });
111
+ });
112
+
113
+ it("handles boolean responses", async () => {
114
+ const mock = schmock();
115
+ mock("GET /bool", () => true);
116
+
117
+ const response = await mock.handle("GET", "/bool");
118
+
119
+ expect(response.status).toBe(200);
120
+ expect(response.body).toBe(true);
121
+ expect(response.headers).toEqual({ "content-type": "application/json" });
122
+ });
123
+
124
+ it("handles null responses", async () => {
125
+ const mock = schmock();
126
+ mock("GET /null", () => null);
127
+
128
+ const response = await mock.handle("GET", "/null");
129
+
130
+ expect(response.status).toBe(204);
131
+ expect(response.body).toBeUndefined();
132
+ expect(response.headers).toEqual({ "content-type": "application/json" });
133
+ });
134
+
135
+ it("handles undefined responses", async () => {
136
+ const mock = schmock();
137
+ mock("GET /undefined", () => undefined);
138
+
139
+ const response = await mock.handle("GET", "/undefined");
140
+
141
+ expect(response.status).toBe(204);
142
+ expect(response.body).toBeUndefined();
143
+ expect(response.headers).toEqual({ "content-type": "application/json" });
144
+ });
145
+
146
+ it("handles complex object responses", async () => {
147
+ const complexObject = {
148
+ data: {
149
+ users: [
150
+ { id: 1, name: "Alice", tags: ["admin", "active"] },
151
+ { id: 2, name: "Bob", meta: { lastLogin: "2023-01-01" } },
152
+ ],
153
+ pagination: {
154
+ page: 1,
155
+ limit: 10,
156
+ total: 2,
157
+ },
158
+ },
159
+ timestamp: new Date("2023-01-01T00:00:00Z"),
160
+ };
161
+
162
+ const mock = schmock();
163
+ mock("GET /complex", () => complexObject);
164
+
165
+ const response = await mock.handle("GET", "/complex");
166
+
167
+ expect(response.status).toBe(200);
168
+ expect(response.body).toEqual(complexObject);
169
+ expect(response.headers).toEqual({ "content-type": "application/json" });
170
+ });
171
+
172
+ it("handles empty array responses", async () => {
173
+ const mock = schmock();
174
+ mock("GET /empty-array", () => []);
175
+
176
+ const response = await mock.handle("GET", "/empty-array");
177
+
178
+ expect(response.status).toBe(200);
179
+ expect(response.body).toEqual([]);
180
+ expect(response.headers).toEqual({ "content-type": "application/json" });
181
+ });
182
+
183
+ it("handles empty object responses", async () => {
184
+ const mock = schmock();
185
+ mock("GET /empty-object", () => ({}));
186
+
187
+ const response = await mock.handle("GET", "/empty-object");
188
+
189
+ expect(response.status).toBe(200);
190
+ expect(response.body).toEqual({});
191
+ expect(response.headers).toEqual({ "content-type": "application/json" });
192
+ });
193
+ });
194
+
195
+ describe("async response functions", () => {
196
+ it("handles async response functions", async () => {
197
+ const mock = schmock();
198
+ mock("GET /async", async () => {
199
+ await new Promise((resolve) => setTimeout(resolve, 1));
200
+ return { async: true };
201
+ });
202
+
203
+ const response = await mock.handle("GET", "/async");
204
+
205
+ expect(response.status).toBe(200);
206
+ expect(response.body).toEqual({ async: true });
207
+ });
208
+
209
+ it("handles async tuple responses", async () => {
210
+ const mock = schmock();
211
+ mock("POST /async-create", async () => {
212
+ await new Promise((resolve) => setTimeout(resolve, 1));
213
+ return [201, { created: true }, { "X-Async": "true" }];
214
+ });
215
+
216
+ const response = await mock.handle("POST", "/async-create");
217
+
218
+ expect(response.status).toBe(201);
219
+ expect(response.body).toEqual({ created: true });
220
+ expect(response.headers).toEqual({ "X-Async": "true" });
221
+ });
222
+ });
223
+
224
+ describe("edge cases", () => {
225
+ it("handles response with circular references gracefully", async () => {
226
+ const mock = schmock();
227
+ mock("GET /circular", () => {
228
+ const obj: any = { name: "test" };
229
+ obj.self = obj; // Create circular reference
230
+ return obj;
231
+ });
232
+
233
+ const response = await mock.handle("GET", "/circular");
234
+
235
+ expect(response.status).toBe(200);
236
+ expect(response.body).toHaveProperty("name", "test");
237
+ expect(response.body).toHaveProperty("self");
238
+ });
239
+
240
+ it("preserves functions in response objects", async () => {
241
+ const mock = schmock();
242
+ mock("GET /with-functions", () => ({
243
+ data: "test",
244
+ fn: () => "function result",
245
+ }));
246
+
247
+ const response = await mock.handle("GET", "/with-functions");
248
+
249
+ expect(response.status).toBe(200);
250
+ expect(response.body.data).toBe("test");
251
+ expect(typeof response.body.fn).toBe("function");
252
+ expect(response.body.fn()).toBe("function result");
253
+ });
254
+ });
255
+ });
@@ -0,0 +1,351 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { schmock } from "./index";
3
+
4
+ describe("route matching", () => {
5
+ describe("static routes", () => {
6
+ it("matches exact static routes", async () => {
7
+ const mock = schmock();
8
+ mock("GET /users", "users");
9
+ mock("GET /posts", "posts");
10
+
11
+ const users = await mock.handle("GET", "/users");
12
+ const posts = await mock.handle("GET", "/posts");
13
+
14
+ expect(users.body).toBe("users");
15
+ expect(posts.body).toBe("posts");
16
+ });
17
+
18
+ it("differentiates between HTTP methods", async () => {
19
+ const mock = schmock();
20
+ mock("GET /resource", "get-response");
21
+ mock("POST /resource", "post-response");
22
+ mock("PUT /resource", "put-response");
23
+ mock("DELETE /resource", "delete-response");
24
+
25
+ const getRes = await mock.handle("GET", "/resource");
26
+ const postRes = await mock.handle("POST", "/resource");
27
+ const putRes = await mock.handle("PUT", "/resource");
28
+ const deleteRes = await mock.handle("DELETE", "/resource");
29
+
30
+ expect(getRes.body).toBe("get-response");
31
+ expect(postRes.body).toBe("post-response");
32
+ expect(putRes.body).toBe("put-response");
33
+ expect(deleteRes.body).toBe("delete-response");
34
+ });
35
+
36
+ it("handles root path correctly", async () => {
37
+ const mock = schmock();
38
+ mock("GET /", "root");
39
+
40
+ const response = await mock.handle("GET", "/");
41
+ expect(response.body).toBe("root");
42
+ });
43
+
44
+ it("case sensitive path matching", async () => {
45
+ const mock = schmock();
46
+ mock("GET /Users", "capital-users");
47
+
48
+ const response1 = await mock.handle("GET", "/Users");
49
+ const response2 = await mock.handle("GET", "/users");
50
+
51
+ expect(response1.body).toBe("capital-users");
52
+ expect(response2.status).toBe(404);
53
+ });
54
+ });
55
+
56
+ describe("parameterized routes", () => {
57
+ it("matches single parameter routes", async () => {
58
+ const mock = schmock();
59
+ mock("GET /users/:id", ({ params }) => ({ userId: params.id }));
60
+
61
+ const response = await mock.handle("GET", "/users/123");
62
+
63
+ expect(response.body).toEqual({ userId: "123" });
64
+ });
65
+
66
+ it("matches multiple parameter routes", async () => {
67
+ const mock = schmock();
68
+ mock("GET /users/:userId/posts/:postId", ({ params }) => ({
69
+ user: params.userId,
70
+ post: params.postId,
71
+ }));
72
+
73
+ const response = await mock.handle("GET", "/users/456/posts/789");
74
+
75
+ expect(response.body).toEqual({ user: "456", post: "789" });
76
+ });
77
+
78
+ it("handles parameters with special characters", async () => {
79
+ const mock = schmock();
80
+ mock("GET /files/:filename", ({ params }) => ({ file: params.filename }));
81
+
82
+ const response = await mock.handle("GET", "/files/test-file.txt");
83
+
84
+ expect(response.body).toEqual({ file: "test-file.txt" });
85
+ });
86
+
87
+ it("handles parameters with numbers", async () => {
88
+ const mock = schmock();
89
+ mock("GET /items/:id", ({ params }) => ({ itemId: params.id }));
90
+
91
+ const response = await mock.handle("GET", "/items/12345");
92
+
93
+ expect(response.body).toEqual({ itemId: "12345" });
94
+ });
95
+
96
+ it("handles parameters with underscores and hyphens", async () => {
97
+ const mock = schmock();
98
+ mock("GET /api/:snake_case/:kebab-case", ({ params }) => params);
99
+
100
+ const response = await mock.handle(
101
+ "GET",
102
+ "/api/test_value/another-value",
103
+ );
104
+
105
+ expect(response.body).toEqual({
106
+ snake_case: "test_value",
107
+ "kebab-case": "another-value",
108
+ });
109
+ });
110
+
111
+ it("doesn't match if parameter segment is empty", async () => {
112
+ const mock = schmock();
113
+ mock("GET /users/:id", "found");
114
+
115
+ const response = await mock.handle("GET", "/users/");
116
+
117
+ expect(response.status).toBe(404);
118
+ });
119
+
120
+ it("doesn't match parameters across path segments", async () => {
121
+ const mock = schmock();
122
+ mock("GET /users/:id", "found");
123
+
124
+ const response = await mock.handle("GET", "/users/123/extra");
125
+
126
+ expect(response.status).toBe(404);
127
+ });
128
+ });
129
+
130
+ describe("route precedence and conflicts", () => {
131
+ it("matches most recently defined route when patterns overlap", async () => {
132
+ const mock = schmock();
133
+ mock("GET /users/:id", "parameterized");
134
+ mock("GET /users/special", "static");
135
+
136
+ const paramResponse = await mock.handle("GET", "/users/123");
137
+ const staticResponse = await mock.handle("GET", "/users/special");
138
+
139
+ // The static route should be matched since it was defined later
140
+ expect(paramResponse.body).toBe("parameterized");
141
+ expect(staticResponse.body).toBe("static");
142
+ });
143
+
144
+ it("handles exact vs parameterized route conflicts", async () => {
145
+ const mock = schmock();
146
+ mock("GET /api/:version/users", "versioned");
147
+ mock("GET /api/v1/users", "v1-specific");
148
+
149
+ const versionedResponse = await mock.handle("GET", "/api/v2/users");
150
+ const v1Response = await mock.handle("GET", "/api/v1/users");
151
+
152
+ expect(versionedResponse.body).toBe("versioned");
153
+ expect(v1Response.body).toBe("v1-specific");
154
+ });
155
+
156
+ it("matches first matching route in definition order", async () => {
157
+ const mock = schmock();
158
+ mock("GET /:type/items", "first");
159
+ mock("GET /shop/:category", "second");
160
+
161
+ const response = await mock.handle("GET", "/shop/items");
162
+
163
+ // Both routes match, but with reverse order search, the second route should win
164
+ // /:type/items matches with type="shop"
165
+ // /shop/:category matches with category="items"
166
+ // Since we search in reverse, /shop/:category (more specific) should match
167
+ expect(response.body).toBe("second");
168
+ });
169
+ });
170
+
171
+ describe("complex path patterns", () => {
172
+ it("handles deeply nested parameterized routes", async () => {
173
+ const mock = schmock();
174
+ mock(
175
+ "GET /api/:version/users/:userId/posts/:postId/comments/:commentId",
176
+ ({ params }) => params,
177
+ );
178
+
179
+ const response = await mock.handle(
180
+ "GET",
181
+ "/api/v1/users/123/posts/456/comments/789",
182
+ );
183
+
184
+ expect(response.body).toEqual({
185
+ version: "v1",
186
+ userId: "123",
187
+ postId: "456",
188
+ commentId: "789",
189
+ });
190
+ });
191
+
192
+ it("handles mixed static and parameterized segments", async () => {
193
+ const mock = schmock();
194
+ mock(
195
+ "GET /api/v1/users/:userId/profile/settings/:setting",
196
+ ({ params }) => params,
197
+ );
198
+
199
+ const response = await mock.handle(
200
+ "GET",
201
+ "/api/v1/users/789/profile/settings/privacy",
202
+ );
203
+
204
+ expect(response.body).toEqual({
205
+ userId: "789",
206
+ setting: "privacy",
207
+ });
208
+ });
209
+
210
+ it("handles paths with similar prefixes", async () => {
211
+ const mock = schmock();
212
+ mock("GET /user", "single-user");
213
+ mock("GET /users", "all-users");
214
+ mock("GET /users/:id", "specific-user");
215
+
216
+ const singleResponse = await mock.handle("GET", "/user");
217
+ const allResponse = await mock.handle("GET", "/users");
218
+ const specificResponse = await mock.handle("GET", "/users/123");
219
+
220
+ expect(singleResponse.body).toBe("single-user");
221
+ expect(allResponse.body).toBe("all-users");
222
+ expect(specificResponse.body).toBe("specific-user");
223
+ });
224
+ });
225
+
226
+ describe("edge cases", () => {
227
+ it("handles empty parameter values correctly", async () => {
228
+ const mock = schmock();
229
+ mock("GET /search/:query", ({ params }) => ({ query: params.query }));
230
+
231
+ // This should not match because parameter is empty
232
+ const response = await mock.handle("GET", "/search/");
233
+
234
+ expect(response.status).toBe(404);
235
+ });
236
+
237
+ it("handles special characters in paths", async () => {
238
+ const mock = schmock();
239
+ mock("GET /files/:filename", ({ params }) => ({ file: params.filename }));
240
+
241
+ const response = await mock.handle("GET", "/files/my-file.test.json");
242
+
243
+ expect(response.body).toEqual({ file: "my-file.test.json" });
244
+ });
245
+
246
+ it("handles URL encoded characters in parameters", async () => {
247
+ const mock = schmock();
248
+ mock("GET /search/:query", ({ params }) => ({ query: params.query }));
249
+
250
+ // Note: This tests the raw parameter, URL decoding would happen at HTTP layer
251
+ const response = await mock.handle("GET", "/search/hello%20world");
252
+
253
+ expect(response.body).toEqual({ query: "hello%20world" });
254
+ });
255
+
256
+ it("handles very long parameter values", async () => {
257
+ const mock = schmock();
258
+ mock("GET /data/:id", ({ params }) => ({ length: params.id.length }));
259
+
260
+ const longId = "a".repeat(1000);
261
+ const response = await mock.handle("GET", `/data/${longId}`);
262
+
263
+ expect(response.body).toEqual({ length: 1000 });
264
+ });
265
+
266
+ it("handles numeric parameter values", async () => {
267
+ const mock = schmock();
268
+ mock("GET /items/:id", ({ params }) => ({
269
+ id: params.id,
270
+ type: typeof params.id,
271
+ parsed: Number.parseInt(params.id),
272
+ }));
273
+
274
+ const response = await mock.handle("GET", "/items/12345");
275
+
276
+ expect(response.body).toEqual({
277
+ id: "12345",
278
+ type: "string",
279
+ parsed: 12345,
280
+ });
281
+ });
282
+ });
283
+
284
+ describe("no route found scenarios", () => {
285
+ it("returns 404 for completely unmatched paths", async () => {
286
+ const mock = schmock();
287
+ mock("GET /existing", "found");
288
+
289
+ const response = await mock.handle("GET", "/nonexistent");
290
+
291
+ expect(response.status).toBe(404);
292
+ expect(response.body.error).toContain("Route not found");
293
+ expect(response.body.code).toBe("ROUTE_NOT_FOUND");
294
+ });
295
+
296
+ it("returns 404 for wrong HTTP method", async () => {
297
+ const mock = schmock();
298
+ mock("GET /resource", "get-only");
299
+
300
+ const response = await mock.handle("POST", "/resource");
301
+
302
+ expect(response.status).toBe(404);
303
+ expect(response.body.error).toContain("POST /resource");
304
+ });
305
+
306
+ it("returns 404 for partial path matches", async () => {
307
+ const mock = schmock();
308
+ mock("GET /api/users", "found");
309
+
310
+ const response1 = await mock.handle("GET", "/api");
311
+ const response2 = await mock.handle("GET", "/api/users/extra");
312
+
313
+ expect(response1.status).toBe(404);
314
+ expect(response2.status).toBe(404);
315
+ });
316
+ });
317
+
318
+ describe("regex pattern validation", () => {
319
+ it("escapes special regex characters in static paths", async () => {
320
+ const mock = schmock();
321
+ mock("GET /api/test.json", "json-file");
322
+ mock("GET /api/test*json", "wildcard-file");
323
+
324
+ const jsonResponse = await mock.handle("GET", "/api/test.json");
325
+ const wildcardResponse = await mock.handle("GET", "/api/test*json");
326
+ const dotResponse = await mock.handle("GET", "/api/testXjson"); // Should not match
327
+
328
+ expect(jsonResponse.body).toBe("json-file");
329
+ expect(wildcardResponse.body).toBe("wildcard-file");
330
+ expect(dotResponse.status).toBe(404);
331
+ });
332
+
333
+ it("handles paths with parentheses", async () => {
334
+ const mock = schmock();
335
+ mock("GET /api/(v1)/users", "versioned");
336
+
337
+ const response = await mock.handle("GET", "/api/(v1)/users");
338
+
339
+ expect(response.body).toBe("versioned");
340
+ });
341
+
342
+ it("handles paths with square brackets", async () => {
343
+ const mock = schmock();
344
+ mock("GET /api/[admin]/users", "admin-users");
345
+
346
+ const response = await mock.handle("GET", "/api/[admin]/users");
347
+
348
+ expect(response.body).toBe("admin-users");
349
+ });
350
+ });
351
+ });