@schmock/core 2.3.0 → 2.3.1

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 (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/dist/abort.d.ts +3 -0
  4. package/dist/abort.d.ts.map +1 -0
  5. package/dist/abort.js +30 -0
  6. package/dist/builder.d.ts +31 -10
  7. package/dist/builder.d.ts.map +1 -1
  8. package/dist/builder.js +760 -190
  9. package/dist/constants.d.ts +38 -0
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/constants.js +123 -2
  12. package/dist/errors.d.ts +14 -3
  13. package/dist/errors.d.ts.map +1 -1
  14. package/dist/errors.js +41 -6
  15. package/dist/helpers.d.ts +9 -0
  16. package/dist/helpers.d.ts.map +1 -1
  17. package/dist/helpers.js +18 -2
  18. package/dist/http-helpers.d.ts +39 -4
  19. package/dist/http-helpers.d.ts.map +1 -1
  20. package/dist/http-helpers.js +147 -49
  21. package/dist/index.d.ts +292 -34
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +8 -3
  24. package/dist/interceptor.d.ts +11 -1
  25. package/dist/interceptor.d.ts.map +1 -1
  26. package/dist/interceptor.js +316 -169
  27. package/dist/parser.d.ts.map +1 -1
  28. package/dist/parser.js +12 -3
  29. package/dist/plugin-pipeline.d.ts +3 -3
  30. package/dist/plugin-pipeline.d.ts.map +1 -1
  31. package/dist/plugin-pipeline.js +29 -14
  32. package/dist/response-normalizer.d.ts +16 -0
  33. package/dist/response-normalizer.d.ts.map +1 -0
  34. package/dist/response-normalizer.js +316 -0
  35. package/dist/response-parser.d.ts.map +1 -1
  36. package/dist/response-parser.js +39 -2
  37. package/dist/route-matcher.d.ts +3 -0
  38. package/dist/route-matcher.d.ts.map +1 -1
  39. package/dist/route-matcher.js +12 -6
  40. package/dist/types.d.ts +7 -2
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +14 -6
  43. package/src/audit-core-builder.test.ts +0 -218
  44. package/src/audit-response-guard.test.ts +0 -38
  45. package/src/binary.test.ts +0 -126
  46. package/src/binary.ts +0 -11
  47. package/src/builder.test.ts +0 -289
  48. package/src/builder.ts +0 -748
  49. package/src/constants.test.ts +0 -99
  50. package/src/constants.ts +0 -73
  51. package/src/debug.test.ts +0 -241
  52. package/src/delay.test.ts +0 -319
  53. package/src/dist-shape.test.ts +0 -35
  54. package/src/errors.test.ts +0 -223
  55. package/src/errors.ts +0 -130
  56. package/src/factory.test.ts +0 -133
  57. package/src/helpers.test.ts +0 -147
  58. package/src/helpers.ts +0 -58
  59. package/src/http-helpers.test.ts +0 -39
  60. package/src/http-helpers.ts +0 -149
  61. package/src/index.ts +0 -152
  62. package/src/interceptor.test.ts +0 -488
  63. package/src/interceptor.ts +0 -407
  64. package/src/namespace.test.ts +0 -274
  65. package/src/parser.property.test.ts +0 -630
  66. package/src/parser.test.ts +0 -148
  67. package/src/parser.ts +0 -64
  68. package/src/plugin-pipeline.ts +0 -208
  69. package/src/plugin-system.test.ts +0 -697
  70. package/src/response-parser.ts +0 -114
  71. package/src/response-parsing.test.ts +0 -333
  72. package/src/route-matcher.ts +0 -69
  73. package/src/route-matching.test.ts +0 -394
  74. package/src/server.test.ts +0 -234
  75. package/src/smart-defaults.test.ts +0 -361
  76. package/src/steps/async-support.steps.ts +0 -421
  77. package/src/steps/audit-core-builder.steps.ts +0 -188
  78. package/src/steps/audit-onerror-tuple.steps.ts +0 -83
  79. package/src/steps/basic-usage.steps.ts +0 -245
  80. package/src/steps/developer-experience.steps.ts +0 -204
  81. package/src/steps/error-handling.steps.ts +0 -555
  82. package/src/steps/fetch-interceptor.steps.ts +0 -457
  83. package/src/steps/fluent-api.steps.ts +0 -255
  84. package/src/steps/http-methods.steps.ts +0 -342
  85. package/src/steps/lifecycle-events.steps.ts +0 -149
  86. package/src/steps/performance-reliability.steps.ts +0 -90
  87. package/src/steps/plugin-integration.steps.ts +0 -334
  88. package/src/steps/request-history.steps.ts +0 -458
  89. package/src/steps/response-delay.steps.ts +0 -88
  90. package/src/steps/route-key-format.steps.ts +0 -99
  91. package/src/steps/standalone-server.steps.ts +0 -227
  92. package/src/steps/state-concurrency.steps.ts +0 -167
  93. package/src/types.ts +0 -36
package/package.json CHANGED
@@ -1,13 +1,21 @@
1
1
  {
2
2
  "name": "@schmock/core",
3
3
  "description": "Core functionality for Schmock",
4
- "version": "2.3.0",
4
+ "version": "2.3.1",
5
5
  "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/khalic-lab/schmock.git",
9
+ "directory": "packages/core"
10
+ },
11
+ "homepage": "https://github.com/khalic-lab/schmock/tree/main/packages/core#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/khalic-lab/schmock/issues"
14
+ },
6
15
  "main": "./dist/index.js",
7
16
  "types": "./dist/index.d.ts",
8
17
  "files": [
9
- "dist",
10
- "src"
18
+ "dist"
11
19
  ],
12
20
  "exports": {
13
21
  ".": {
@@ -18,8 +26,9 @@
18
26
  "./package.json": "./package.json"
19
27
  },
20
28
  "scripts": {
21
- "build": "bun build:lib && bun build:types",
22
- "build:lib": "bun build --minify --target node --outdir=dist src/index.ts",
29
+ "clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo",
30
+ "build": "bun run clean && bun run build:lib && bun run build:types",
31
+ "build:lib": "tsc -p tsconfig.runtime.json",
23
32
  "build:types": "rm -f tsconfig.tsbuildinfo && tsc --build && node scripts/embed-ambient-types.js",
24
33
  "pretest": "rm -f src/*.js src/*.d.ts || true",
25
34
  "test": "vitest run",
@@ -37,7 +46,6 @@
37
46
  "devDependencies": {
38
47
  "@amiceli/vitest-cucumber": "^7.0.0",
39
48
  "@types/node": "^26.1.0",
40
- "@vitest/ui": "^4.1.9",
41
49
  "vitest": "^4.1.9"
42
50
  }
43
51
  }
@@ -1,218 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { schmock } from "./index";
3
-
4
- // ── FIX 1.2: defineRoute must shallow-clone the config ────────────────────────
5
-
6
- describe("FIX 1.2 — defineRoute must not mutate the caller's config", () => {
7
- it("does not add contentType to the caller's config object", () => {
8
- const sharedConfig: Schmock.RouteConfig = {};
9
- const mock = schmock();
10
- mock("GET /a", { x: 1 }, sharedConfig);
11
- // sharedConfig must remain empty — defineRoute must work on a clone
12
- expect(Object.keys(sharedConfig)).toHaveLength(0);
13
- });
14
-
15
- it("second route registered with same config is not polluted by first route detection", () => {
16
- const sharedConfig: Schmock.RouteConfig = {};
17
- const mock = schmock();
18
- mock("GET /first", { x: 1 }, sharedConfig);
19
- mock("GET /second", "hello", sharedConfig);
20
- // sharedConfig must still be clean — neither call should have written to it
21
- expect(Object.keys(sharedConfig)).toHaveLength(0);
22
- });
23
-
24
- it("mutating caller's config after registration does not change the registered route response", async () => {
25
- const config: Schmock.RouteConfig = {};
26
- const mock = schmock();
27
- mock("GET /item", { value: 99 }, config);
28
-
29
- // After registration, mutate the caller's config
30
- config.status = 503;
31
- config.contentType = "text/html";
32
-
33
- const response = await mock.handle("GET", "/item");
34
- // The registered route must use its own cloned config — not 503
35
- expect(response.status).toBe(200);
36
- const ct =
37
- response.headers?.["content-type"] ?? response.headers?.["Content-Type"];
38
- expect(ct).toContain("application/json");
39
- });
40
-
41
- it("each route keeps its own detected contentType even when sharing the same config object", async () => {
42
- const sharedConfig: Schmock.RouteConfig = {};
43
- const mock = schmock();
44
- // First route: object → application/json
45
- mock("GET /obj", { value: 1 }, sharedConfig);
46
- // Second route: primitive string → text/plain
47
- mock("GET /str", "hello", sharedConfig);
48
-
49
- const objResp = await mock.handle("GET", "/obj");
50
- const strResp = await mock.handle("GET", "/str");
51
-
52
- const ctObj =
53
- objResp.headers?.["content-type"] ?? objResp.headers?.["Content-Type"];
54
- const ctStr =
55
- strResp.headers?.["content-type"] ?? strResp.headers?.["Content-Type"];
56
-
57
- expect(ctObj).toContain("application/json");
58
- expect(ctStr).toContain("text/plain");
59
- });
60
- });
61
-
62
- // ── FIX 2.2: duplicate route detection must normalize trailing slash ──────────
63
-
64
- describe("FIX 2.2 — duplicate-route detection uses normalized paths", () => {
65
- it("GET /users/ is treated as duplicate of GET /users", () => {
66
- const mock = schmock();
67
- mock("GET /users", [{ id: 1 }], {});
68
- // Registering with trailing slash must not add a second route
69
- mock("GET /users/", [{ id: 2 }], {});
70
-
71
- const routes = mock.getRoutes();
72
- const userRoutes = routes.filter(
73
- (r) =>
74
- r.method === "GET" && (r.path === "/users" || r.path === "/users/"),
75
- );
76
- expect(userRoutes).toHaveLength(1);
77
- });
78
-
79
- it("first registration wins — trailing-slash variant does not overwrite", async () => {
80
- const mock = schmock();
81
- mock("GET /items", [{ id: 1 }], {});
82
- mock("GET /items/", [{ id: 999 }], {});
83
-
84
- // Should still get the first registration's data
85
- const resp = await mock.handle("GET", "/items");
86
- expect(resp.status).toBe(200);
87
- expect(Array.isArray(resp.body)).toBe(true);
88
- expect((resp.body as { id: number }[])[0].id).toBe(1);
89
- });
90
-
91
- it("registering verbatim duplicate also produces only one route", () => {
92
- const mock = schmock();
93
- mock("GET /same", { ok: true }, {});
94
- mock("GET /same", { ok: false }, {});
95
-
96
- const routes = mock.getRoutes();
97
- const sameRoutes = routes.filter(
98
- (r) => r.method === "GET" && r.path === "/same",
99
- );
100
- expect(sameRoutes).toHaveLength(1);
101
- });
102
- });
103
-
104
- // ── FIX 3.3: reset()/resetState() must reassign, not delete keys ─────────────
105
-
106
- describe("FIX 3.3 — reset()/resetState() must not mutate caller's state", () => {
107
- it("reset() leaves the external state object untouched", () => {
108
- const st = { a: 1 };
109
- const mock = schmock({ state: st });
110
- mock.reset();
111
- expect(st).toEqual({ a: 1 });
112
- });
113
-
114
- it("resetState() leaves the external state object untouched", () => {
115
- const st = { b: 2 };
116
- const mock = schmock({ state: st });
117
- mock.resetState();
118
- expect(st).toEqual({ b: 2 });
119
- });
120
-
121
- it("mock.getState() is empty after reset()", () => {
122
- const st = { x: 10 };
123
- const mock = schmock({ state: st });
124
- mock.reset();
125
- expect(mock.getState()).toEqual({});
126
- });
127
-
128
- it("mock.getState() is empty after resetState()", () => {
129
- const st = { y: 20 };
130
- const mock = schmock({ state: st });
131
- mock.resetState();
132
- expect(mock.getState()).toEqual({});
133
- });
134
-
135
- it("generator sees empty state after resetState", async () => {
136
- const st: Record<string, unknown> = { counter: 0 };
137
- const mock = schmock({ state: st });
138
- mock("GET /stateful", (ctx: Schmock.RequestContext) => {
139
- return { stateKeys: Object.keys(ctx.state) };
140
- });
141
-
142
- mock.resetState();
143
-
144
- const resp = await mock.handle("GET", "/stateful");
145
- expect(resp.status).toBe(200);
146
- // After resetState, the generator should see an empty state
147
- expect((resp.body as { stateKeys: string[] }).stateKeys).toHaveLength(0);
148
- });
149
- });
150
-
151
- // ── FIX 2.3: history()/lastRequest() must return deep clones ─────────────────
152
-
153
- describe("FIX 2.3 — history() and lastRequest() return deep clones", () => {
154
- it("mutating a history() record's nested body does not corrupt internal history", async () => {
155
- const mock = schmock();
156
- mock("GET /nested", { deep: { value: "original" } }, {});
157
-
158
- await mock.handle("GET", "/nested");
159
-
160
- // First read: mutate the returned record's body
161
- const records1 = mock.history();
162
- const firstRecord = records1[0];
163
- (firstRecord.response.body as { deep: { value: string } }).deep.value =
164
- "MUTATED";
165
-
166
- // Second read: internal history must be unchanged
167
- const records2 = mock.history();
168
- expect(
169
- (records2[0].response.body as { deep: { value: string } }).deep.value,
170
- ).toBe("original");
171
- });
172
-
173
- it("mutating filtered history() record does not corrupt internal history", async () => {
174
- const mock = schmock();
175
- mock("GET /data", { info: { count: 42 } }, {});
176
- await mock.handle("GET", "/data");
177
-
178
- const filtered = mock.history("GET", "/data");
179
- (filtered[0].response.body as { info: { count: number } }).info.count = 0;
180
-
181
- const again = mock.history("GET", "/data");
182
- expect(
183
- (again[0].response.body as { info: { count: number } }).info.count,
184
- ).toBe(42);
185
- });
186
-
187
- it("mutating lastRequest() record does not corrupt internal history", async () => {
188
- const mock = schmock();
189
- mock("GET /last", { meta: { tag: "safe" } }, {});
190
- await mock.handle("GET", "/last");
191
-
192
- const last = mock.lastRequest();
193
- if (!last) throw new Error("expected a record");
194
- (last.response.body as { meta: { tag: string } }).meta.tag = "corrupted";
195
-
196
- const again = mock.lastRequest();
197
- if (!again) throw new Error("expected a record");
198
- expect((again.response.body as { meta: { tag: string } }).meta.tag).toBe(
199
- "safe",
200
- );
201
- });
202
-
203
- it("mutating filtered lastRequest() record does not corrupt internal history", async () => {
204
- const mock = schmock();
205
- mock("POST /echo", { msg: { text: "hello" } }, {});
206
- await mock.handle("POST", "/echo");
207
-
208
- const last = mock.lastRequest("POST", "/echo");
209
- if (!last) throw new Error("expected a record");
210
- (last.response.body as { msg: { text: string } }).msg.text = "CHANGED";
211
-
212
- const again = mock.lastRequest("POST", "/echo");
213
- if (!again) throw new Error("expected a record");
214
- expect((again.response.body as { msg: { text: string } }).msg.text).toBe(
215
- "hello",
216
- );
217
- });
218
- });
@@ -1,38 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { schmock } from "./index";
3
-
4
- describe("isResponseObject guard — status type check", () => {
5
- describe("non-numeric status must NOT be treated as response envelope", () => {
6
- it("returns status 200 (default) when generator returns { status: 'ok', body: 'hello' }", async () => {
7
- const mock = schmock();
8
- mock("GET /bad-envelope", () => ({ status: "ok", body: "hello" }));
9
-
10
- const response = await mock.handle("GET", "/bad-envelope");
11
-
12
- // The string status must never leak into response.status
13
- expect(response.status).toBe(200);
14
- });
15
-
16
- it("returns the whole object as body when generator returns { status: 'ok', body: 'hello' }", async () => {
17
- const mock = schmock();
18
- mock("GET /bad-envelope-body", () => ({ status: "ok", body: "hello" }));
19
-
20
- const response = await mock.handle("GET", "/bad-envelope-body");
21
-
22
- // The original object must be treated as plain body, not unwrapped
23
- expect(response.body).toEqual({ status: "ok", body: "hello" });
24
- });
25
- });
26
-
27
- describe("numeric status IS correctly treated as response envelope", () => {
28
- it("returns status 201 and unwrapped body when generator returns { status: 201, body: { id: 1 } }", async () => {
29
- const mock = schmock();
30
- mock("POST /create", () => ({ status: 201, body: { id: 1 } }));
31
-
32
- const response = await mock.handle("POST", "/create");
33
-
34
- expect(response.status).toBe(201);
35
- expect(response.body).toEqual({ id: 1 });
36
- });
37
- });
38
- });
@@ -1,126 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { isBinaryBody } from "./binary.js";
3
- import { schmock } from "./index.js";
4
- import { parseResponse } from "./response-parser.js";
5
-
6
- function binaryBytes(value: unknown): number[] {
7
- if (value instanceof ArrayBuffer) {
8
- return [...new Uint8Array(value)];
9
- }
10
- if (ArrayBuffer.isView(value)) {
11
- return [
12
- ...new Uint8Array(value.buffer, value.byteOffset, value.byteLength),
13
- ];
14
- }
15
- throw new Error("Expected a binary response body");
16
- }
17
-
18
- describe("isBinaryBody", () => {
19
- it("recognizes browser binary values without a Node Buffer global", () => {
20
- expect(isBinaryBody(new Uint8Array([1, 2, 3]))).toBe(true);
21
- expect(isBinaryBody(new ArrayBuffer(3))).toBe(true);
22
- expect(isBinaryBody(new DataView(new SharedArrayBuffer(3)))).toBe(true);
23
- expect(isBinaryBody({ 0: 1, length: 1 })).toBe(false);
24
- });
25
- });
26
-
27
- describe("parseResponse binary handling", () => {
28
- it("stringifies text objects when Buffer is not available", () => {
29
- const originalBuffer = globalThis.Buffer;
30
- Reflect.deleteProperty(globalThis, "Buffer");
31
-
32
- try {
33
- const response = parseResponse(
34
- { message: "browser" },
35
- { contentType: "text/plain" },
36
- );
37
-
38
- expect(response.body).toBe('{"message":"browser"}');
39
- } finally {
40
- globalThis.Buffer = originalBuffer;
41
- }
42
- });
43
-
44
- it("detects binary returned by a dynamic generator", async () => {
45
- const mock = schmock();
46
- mock("GET /dynamic-binary", () => new Uint8Array([1, 2, 3]));
47
-
48
- const response = await mock.handle("GET", "/dynamic-binary");
49
-
50
- expect(response.headers["content-type"]).toBe("application/octet-stream");
51
- expect(binaryBytes(response.body)).toEqual([1, 2, 3]);
52
- });
53
-
54
- it("detects binary in a status tuple", async () => {
55
- const mock = schmock();
56
- const bytes = new Uint8Array([4, 5, 6]);
57
- mock(
58
- "GET /tuple-binary",
59
- () => [206, new DataView(bytes.buffer)] satisfies [number, unknown],
60
- );
61
-
62
- const response = await mock.handle("GET", "/tuple-binary");
63
-
64
- expect(response.status).toBe(206);
65
- expect(response.headers["content-type"]).toBe("application/octet-stream");
66
- expect(binaryBytes(response.body)).toEqual([4, 5, 6]);
67
- });
68
-
69
- it("detects binary in a response object", () => {
70
- const response = parseResponse(
71
- { status: 206, body: new Uint8Array([7, 8]), headers: {} },
72
- {},
73
- );
74
-
75
- expect(response.status).toBe(206);
76
- expect(response.headers["content-type"]).toBe("application/octet-stream");
77
- expect(binaryBytes(response.body)).toEqual([7, 8]);
78
- });
79
-
80
- it("preserves an explicit binary content type", async () => {
81
- const mock = schmock();
82
- mock("GET /image", () => new Uint8Array([137, 80, 78, 71]), {
83
- contentType: "image/png",
84
- });
85
-
86
- const response = await mock.handle("GET", "/image");
87
-
88
- expect(response.headers["content-type"]).toBe("image/png");
89
- expect(binaryBytes(response.body)).toEqual([137, 80, 78, 71]);
90
- });
91
-
92
- it("preserves a tuple content-type header without adding a duplicate", () => {
93
- const response = parseResponse(
94
- [
95
- 200,
96
- new Uint8Array([1]),
97
- { "Content-Type": "application/custom-binary" },
98
- ],
99
- { contentType: "application/json" },
100
- );
101
-
102
- expect(response.headers).toEqual({
103
- "Content-Type": "application/custom-binary",
104
- });
105
- expect(binaryBytes(response.body)).toEqual([1]);
106
- });
107
- });
108
-
109
- describe("browser runtime compatibility", () => {
110
- it("defines and serves object routes when Buffer is not available", async () => {
111
- const originalBuffer = globalThis.Buffer;
112
- Reflect.deleteProperty(globalThis, "Buffer");
113
-
114
- try {
115
- const mock = schmock();
116
- mock("GET /browser", { runtime: "browser" });
117
-
118
- const response = await mock.handle("GET", "/browser");
119
- expect(response.status).toBe(200);
120
- expect(response.body).toEqual({ runtime: "browser" });
121
- expect(response.headers["content-type"]).toBe("application/json");
122
- } finally {
123
- globalThis.Buffer = originalBuffer;
124
- }
125
- });
126
- });
package/src/binary.ts DELETED
@@ -1,11 +0,0 @@
1
- /**
2
- * Browser-safe binary body detection.
3
- *
4
- * Every ArrayBuffer view is accepted, including DataView, typed arrays backed
5
- * by SharedArrayBuffer, and Node.js Buffer without referencing its global.
6
- */
7
- export function isBinaryBody(
8
- value: unknown,
9
- ): value is ArrayBuffer | ArrayBufferView {
10
- return value instanceof ArrayBuffer || ArrayBuffer.isView(value);
11
- }