@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.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/abort.d.ts +3 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +30 -0
- package/dist/builder.d.ts +31 -10
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +760 -190
- package/dist/constants.d.ts +38 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +123 -2
- package/dist/errors.d.ts +14 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +41 -6
- package/dist/helpers.d.ts +9 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +18 -2
- package/dist/http-helpers.d.ts +39 -4
- package/dist/http-helpers.d.ts.map +1 -1
- package/dist/http-helpers.js +147 -49
- package/dist/index.d.ts +292 -34
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/interceptor.d.ts +11 -1
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/interceptor.js +316 -169
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +12 -3
- package/dist/plugin-pipeline.d.ts +3 -3
- package/dist/plugin-pipeline.d.ts.map +1 -1
- package/dist/plugin-pipeline.js +29 -14
- package/dist/response-normalizer.d.ts +16 -0
- package/dist/response-normalizer.d.ts.map +1 -0
- package/dist/response-normalizer.js +316 -0
- package/dist/response-parser.d.ts.map +1 -1
- package/dist/response-parser.js +39 -2
- package/dist/route-matcher.d.ts +3 -0
- package/dist/route-matcher.d.ts.map +1 -1
- package/dist/route-matcher.js +12 -6
- package/dist/types.d.ts +7 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +14 -6
- package/src/audit-core-builder.test.ts +0 -218
- package/src/audit-response-guard.test.ts +0 -38
- package/src/binary.test.ts +0 -126
- package/src/binary.ts +0 -11
- package/src/builder.test.ts +0 -289
- package/src/builder.ts +0 -748
- package/src/constants.test.ts +0 -99
- package/src/constants.ts +0 -73
- package/src/debug.test.ts +0 -241
- package/src/delay.test.ts +0 -319
- package/src/dist-shape.test.ts +0 -35
- package/src/errors.test.ts +0 -223
- package/src/errors.ts +0 -130
- package/src/factory.test.ts +0 -133
- package/src/helpers.test.ts +0 -147
- package/src/helpers.ts +0 -58
- package/src/http-helpers.test.ts +0 -39
- package/src/http-helpers.ts +0 -149
- package/src/index.ts +0 -152
- package/src/interceptor.test.ts +0 -488
- package/src/interceptor.ts +0 -407
- package/src/namespace.test.ts +0 -274
- package/src/parser.property.test.ts +0 -630
- package/src/parser.test.ts +0 -148
- package/src/parser.ts +0 -64
- package/src/plugin-pipeline.ts +0 -208
- package/src/plugin-system.test.ts +0 -697
- package/src/response-parser.ts +0 -114
- package/src/response-parsing.test.ts +0 -333
- package/src/route-matcher.ts +0 -69
- package/src/route-matching.test.ts +0 -394
- package/src/server.test.ts +0 -234
- package/src/smart-defaults.test.ts +0 -361
- package/src/steps/async-support.steps.ts +0 -421
- package/src/steps/audit-core-builder.steps.ts +0 -188
- package/src/steps/audit-onerror-tuple.steps.ts +0 -83
- package/src/steps/basic-usage.steps.ts +0 -245
- package/src/steps/developer-experience.steps.ts +0 -204
- package/src/steps/error-handling.steps.ts +0 -555
- package/src/steps/fetch-interceptor.steps.ts +0 -457
- package/src/steps/fluent-api.steps.ts +0 -255
- package/src/steps/http-methods.steps.ts +0 -342
- package/src/steps/lifecycle-events.steps.ts +0 -149
- package/src/steps/performance-reliability.steps.ts +0 -90
- package/src/steps/plugin-integration.steps.ts +0 -334
- package/src/steps/request-history.steps.ts +0 -458
- package/src/steps/response-delay.steps.ts +0 -88
- package/src/steps/route-key-format.steps.ts +0 -99
- package/src/steps/standalone-server.steps.ts +0 -227
- package/src/steps/state-concurrency.steps.ts +0 -167
- package/src/types.ts +0 -36
package/src/response-parser.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { isBinaryBody } from "./binary.js";
|
|
2
|
-
import { isStatusTuple } from "./constants.js";
|
|
3
|
-
|
|
4
|
-
const BINARY_CONTENT_TYPE = "application/octet-stream";
|
|
5
|
-
|
|
6
|
-
function hasContentType(headers: Record<string, string>): boolean {
|
|
7
|
-
return Object.keys(headers).some(
|
|
8
|
-
(header) => header.toLowerCase() === "content-type",
|
|
9
|
-
);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function isResponseObject(value: unknown): value is {
|
|
13
|
-
status: number;
|
|
14
|
-
body: unknown;
|
|
15
|
-
headers?: Record<string, string>;
|
|
16
|
-
} {
|
|
17
|
-
return (
|
|
18
|
-
typeof value === "object" &&
|
|
19
|
-
value !== null &&
|
|
20
|
-
"status" in value &&
|
|
21
|
-
typeof value.status === "number" &&
|
|
22
|
-
"body" in value &&
|
|
23
|
-
(!("headers" in value) ||
|
|
24
|
-
value.headers === undefined ||
|
|
25
|
-
isStringRecord(value.headers))
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function isStringRecord(value: unknown): value is Record<string, string> {
|
|
30
|
-
return (
|
|
31
|
-
typeof value === "object" &&
|
|
32
|
-
value !== null &&
|
|
33
|
-
Object.values(value).every((entry) => typeof entry === "string")
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Parse and normalize response result into Response object
|
|
39
|
-
* Handles tuple format [status, body, headers], direct values, and response objects
|
|
40
|
-
*/
|
|
41
|
-
export function parseResponse(
|
|
42
|
-
result: unknown,
|
|
43
|
-
routeConfig: Schmock.RouteConfig,
|
|
44
|
-
): Schmock.Response {
|
|
45
|
-
let status = 200;
|
|
46
|
-
let body: unknown = result;
|
|
47
|
-
let headers: Record<string, string> = {};
|
|
48
|
-
|
|
49
|
-
let tupleFormat = false;
|
|
50
|
-
|
|
51
|
-
// Handle already-formed response objects (from plugin error recovery)
|
|
52
|
-
if (isResponseObject(result)) {
|
|
53
|
-
status = result.status;
|
|
54
|
-
body = result.body;
|
|
55
|
-
headers = result.headers ?? {};
|
|
56
|
-
tupleFormat = true;
|
|
57
|
-
} else if (isStatusTuple(result)) {
|
|
58
|
-
// Handle tuple response format [status, body, headers?]
|
|
59
|
-
[status, body, headers = {}] = result;
|
|
60
|
-
tupleFormat = true;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Handle null/undefined responses with 204 No Content
|
|
64
|
-
// But don't auto-convert if tuple format was used (status was explicitly provided)
|
|
65
|
-
if (body === null || body === undefined) {
|
|
66
|
-
if (!tupleFormat) {
|
|
67
|
-
status = status === 200 ? 204 : status; // Only change to 204 if status wasn't explicitly set via tuple
|
|
68
|
-
}
|
|
69
|
-
body = undefined; // Ensure body is undefined for null responses
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const binaryBody = isBinaryBody(body);
|
|
73
|
-
|
|
74
|
-
// Binary response values need a transport-safe MIME type. Tuple headers still
|
|
75
|
-
// take precedence, while a non-JSON route override (for example image/png)
|
|
76
|
-
// remains authoritative for non-tuple responses.
|
|
77
|
-
if (!hasContentType(headers) && binaryBody) {
|
|
78
|
-
headers["content-type"] =
|
|
79
|
-
!tupleFormat &&
|
|
80
|
-
routeConfig.contentType &&
|
|
81
|
-
routeConfig.contentType !== "application/json"
|
|
82
|
-
? routeConfig.contentType
|
|
83
|
-
: BINARY_CONTENT_TYPE;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Add content-type header from route config if it exists and headers don't already have it
|
|
87
|
-
// But only if this isn't a tuple response (where headers are explicitly controlled)
|
|
88
|
-
let appliedRouteContentType = false;
|
|
89
|
-
if (!hasContentType(headers) && routeConfig.contentType && !tupleFormat) {
|
|
90
|
-
headers["content-type"] = routeConfig.contentType;
|
|
91
|
-
appliedRouteContentType = true;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Handle special conversion cases when contentType is explicitly set. A
|
|
95
|
-
// binary body keeps its bytes even when a custom MIME type is configured.
|
|
96
|
-
if (
|
|
97
|
-
appliedRouteContentType &&
|
|
98
|
-
routeConfig.contentType === "text/plain" &&
|
|
99
|
-
body !== undefined &&
|
|
100
|
-
!binaryBody
|
|
101
|
-
) {
|
|
102
|
-
if (typeof body === "object") {
|
|
103
|
-
body = JSON.stringify(body);
|
|
104
|
-
} else if (typeof body !== "string") {
|
|
105
|
-
body = String(body);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
status,
|
|
111
|
-
body,
|
|
112
|
-
headers,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { schmock } from "./index";
|
|
3
|
-
|
|
4
|
-
describe("response parsing", () => {
|
|
5
|
-
describe("tuple response formats", () => {
|
|
6
|
-
it("treats single-element array [status] as data, not tuple", 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(200);
|
|
13
|
-
expect(response.body).toEqual([204]);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("handles [status, body] tuple", async () => {
|
|
17
|
-
const mock = schmock();
|
|
18
|
-
mock("POST /create", () => [201, { id: 123, created: true }]);
|
|
19
|
-
|
|
20
|
-
const response = await mock.handle("POST", "/create");
|
|
21
|
-
|
|
22
|
-
expect(response.status).toBe(201);
|
|
23
|
-
expect(response.body).toEqual({ id: 123, created: true });
|
|
24
|
-
expect(response.headers).toEqual({});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("handles [status, body, headers] tuple", async () => {
|
|
28
|
-
const mock = schmock();
|
|
29
|
-
mock(
|
|
30
|
-
"POST /upload",
|
|
31
|
-
() =>
|
|
32
|
-
[
|
|
33
|
-
201,
|
|
34
|
-
{ fileId: "abc123" },
|
|
35
|
-
{
|
|
36
|
-
Location: "/files/abc123",
|
|
37
|
-
"Content-Type": "application/json",
|
|
38
|
-
},
|
|
39
|
-
] as [number, any, Record<string, string>],
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
const response = await mock.handle("POST", "/upload");
|
|
43
|
-
|
|
44
|
-
expect(response.status).toBe(201);
|
|
45
|
-
expect(response.body).toEqual({ fileId: "abc123" });
|
|
46
|
-
expect(response.headers).toEqual({
|
|
47
|
-
Location: "/files/abc123",
|
|
48
|
-
"Content-Type": "application/json",
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("handles empty headers object in tuple", async () => {
|
|
53
|
-
const mock = schmock();
|
|
54
|
-
mock("GET /test", () => [200, "OK", {}]);
|
|
55
|
-
|
|
56
|
-
const response = await mock.handle("GET", "/test");
|
|
57
|
-
|
|
58
|
-
expect(response.status).toBe(200);
|
|
59
|
-
expect(response.body).toBe("OK");
|
|
60
|
-
expect(response.headers).toEqual({});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("treats arrays with more than 3 elements as data, not tuple", async () => {
|
|
64
|
-
const mock = schmock();
|
|
65
|
-
mock(
|
|
66
|
-
"GET /extra",
|
|
67
|
-
() => [200, "data", {}, "ignored", "also-ignored"] as any,
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
const response = await mock.handle("GET", "/extra");
|
|
71
|
-
|
|
72
|
-
expect(response.status).toBe(200);
|
|
73
|
-
expect(response.body).toEqual([
|
|
74
|
-
200,
|
|
75
|
-
"data",
|
|
76
|
-
{},
|
|
77
|
-
"ignored",
|
|
78
|
-
"also-ignored",
|
|
79
|
-
]);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("treats non-numeric first element as body, not status", async () => {
|
|
83
|
-
const mock = schmock();
|
|
84
|
-
mock("GET /array-body", () => ["item1", "item2", "item3"]);
|
|
85
|
-
|
|
86
|
-
const response = await mock.handle("GET", "/array-body");
|
|
87
|
-
|
|
88
|
-
expect(response.status).toBe(200);
|
|
89
|
-
expect(response.body).toEqual(["item1", "item2", "item3"]);
|
|
90
|
-
expect(response.headers).toEqual({ "content-type": "application/json" });
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe("various response types", () => {
|
|
95
|
-
it("handles string responses", async () => {
|
|
96
|
-
const mock = schmock();
|
|
97
|
-
mock("GET /text", () => "Simple text response");
|
|
98
|
-
|
|
99
|
-
const response = await mock.handle("GET", "/text");
|
|
100
|
-
|
|
101
|
-
expect(response.status).toBe(200);
|
|
102
|
-
expect(response.body).toBe("Simple text response");
|
|
103
|
-
expect(response.headers).toEqual({ "content-type": "application/json" });
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("handles number responses", async () => {
|
|
107
|
-
const mock = schmock();
|
|
108
|
-
mock("GET /number", () => 42);
|
|
109
|
-
|
|
110
|
-
const response = await mock.handle("GET", "/number");
|
|
111
|
-
|
|
112
|
-
expect(response.status).toBe(200);
|
|
113
|
-
expect(response.body).toBe(42);
|
|
114
|
-
expect(response.headers).toEqual({ "content-type": "application/json" });
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("handles boolean responses", async () => {
|
|
118
|
-
const mock = schmock();
|
|
119
|
-
mock("GET /bool", () => true);
|
|
120
|
-
|
|
121
|
-
const response = await mock.handle("GET", "/bool");
|
|
122
|
-
|
|
123
|
-
expect(response.status).toBe(200);
|
|
124
|
-
expect(response.body).toBe(true);
|
|
125
|
-
expect(response.headers).toEqual({ "content-type": "application/json" });
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("handles null responses", async () => {
|
|
129
|
-
const mock = schmock();
|
|
130
|
-
mock("GET /null", () => null);
|
|
131
|
-
|
|
132
|
-
const response = await mock.handle("GET", "/null");
|
|
133
|
-
|
|
134
|
-
expect(response.status).toBe(204);
|
|
135
|
-
expect(response.body).toBeUndefined();
|
|
136
|
-
expect(response.headers).toEqual({ "content-type": "application/json" });
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("handles undefined responses", async () => {
|
|
140
|
-
const mock = schmock();
|
|
141
|
-
mock("GET /undefined", () => undefined);
|
|
142
|
-
|
|
143
|
-
const response = await mock.handle("GET", "/undefined");
|
|
144
|
-
|
|
145
|
-
expect(response.status).toBe(204);
|
|
146
|
-
expect(response.body).toBeUndefined();
|
|
147
|
-
expect(response.headers).toEqual({ "content-type": "application/json" });
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("handles complex object responses", async () => {
|
|
151
|
-
const complexObject = {
|
|
152
|
-
data: {
|
|
153
|
-
users: [
|
|
154
|
-
{ id: 1, name: "Alice", tags: ["admin", "active"] },
|
|
155
|
-
{ id: 2, name: "Bob", meta: { lastLogin: "2023-01-01" } },
|
|
156
|
-
],
|
|
157
|
-
pagination: {
|
|
158
|
-
page: 1,
|
|
159
|
-
limit: 10,
|
|
160
|
-
total: 2,
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
timestamp: new Date("2023-01-01T00:00:00Z"),
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const mock = schmock();
|
|
167
|
-
mock("GET /complex", () => complexObject);
|
|
168
|
-
|
|
169
|
-
const response = await mock.handle("GET", "/complex");
|
|
170
|
-
|
|
171
|
-
expect(response.status).toBe(200);
|
|
172
|
-
expect(response.body).toEqual(complexObject);
|
|
173
|
-
expect(response.headers).toEqual({ "content-type": "application/json" });
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("handles empty array responses", async () => {
|
|
177
|
-
const mock = schmock();
|
|
178
|
-
mock("GET /empty-array", () => []);
|
|
179
|
-
|
|
180
|
-
const response = await mock.handle("GET", "/empty-array");
|
|
181
|
-
|
|
182
|
-
expect(response.status).toBe(200);
|
|
183
|
-
expect(response.body).toEqual([]);
|
|
184
|
-
expect(response.headers).toEqual({ "content-type": "application/json" });
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("handles empty object responses", async () => {
|
|
188
|
-
const mock = schmock();
|
|
189
|
-
mock("GET /empty-object", () => ({}));
|
|
190
|
-
|
|
191
|
-
const response = await mock.handle("GET", "/empty-object");
|
|
192
|
-
|
|
193
|
-
expect(response.status).toBe(200);
|
|
194
|
-
expect(response.body).toEqual({});
|
|
195
|
-
expect(response.headers).toEqual({ "content-type": "application/json" });
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
describe("async response functions", () => {
|
|
200
|
-
it("handles async response functions", async () => {
|
|
201
|
-
const mock = schmock();
|
|
202
|
-
mock("GET /async", async () => {
|
|
203
|
-
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
204
|
-
return { async: true };
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
const response = await mock.handle("GET", "/async");
|
|
208
|
-
|
|
209
|
-
expect(response.status).toBe(200);
|
|
210
|
-
expect(response.body).toEqual({ async: true });
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it("handles async tuple responses", async () => {
|
|
214
|
-
const mock = schmock();
|
|
215
|
-
mock("POST /async-create", async () => {
|
|
216
|
-
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
217
|
-
return [201, { created: true }, { "X-Async": "true" }];
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const response = await mock.handle("POST", "/async-create");
|
|
221
|
-
|
|
222
|
-
expect(response.status).toBe(201);
|
|
223
|
-
expect(response.body).toEqual({ created: true });
|
|
224
|
-
expect(response.headers).toEqual({ "X-Async": "true" });
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
describe("status code boundaries", () => {
|
|
229
|
-
it("status code 100 (lower boundary) is accepted as tuple", async () => {
|
|
230
|
-
const mock = schmock();
|
|
231
|
-
mock("GET /continue", () => [100, "Continue"]);
|
|
232
|
-
|
|
233
|
-
const response = await mock.handle("GET", "/continue");
|
|
234
|
-
expect(response.status).toBe(100);
|
|
235
|
-
expect(response.body).toBe("Continue");
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it("status code 599 (upper boundary) is accepted as tuple", async () => {
|
|
239
|
-
const mock = schmock();
|
|
240
|
-
mock("GET /custom", () => [599, { message: "custom status" }]);
|
|
241
|
-
|
|
242
|
-
const response = await mock.handle("GET", "/custom");
|
|
243
|
-
expect(response.status).toBe(599);
|
|
244
|
-
expect(response.body).toEqual({ message: "custom status" });
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it("float status like 200.5 is accepted as tuple (number check)", async () => {
|
|
248
|
-
const mock = schmock();
|
|
249
|
-
mock("GET /float", () => [200.5, { ok: true }]);
|
|
250
|
-
|
|
251
|
-
const response = await mock.handle("GET", "/float");
|
|
252
|
-
// isStatusTuple checks typeof === 'number' and 100 <= n <= 599
|
|
253
|
-
// 200.5 passes both checks, so it's treated as a tuple
|
|
254
|
-
expect(response.status).toBe(200.5);
|
|
255
|
-
expect(response.body).toEqual({ ok: true });
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it("array [201, null, undefined] — tuple with undefined headers defaults to empty", async () => {
|
|
259
|
-
const mock = schmock();
|
|
260
|
-
mock("GET /null-headers", () => [201, null, undefined] as any);
|
|
261
|
-
|
|
262
|
-
const response = await mock.handle("GET", "/null-headers");
|
|
263
|
-
// isStatusTuple: length 3, first element 201 (number, 100-599) -> true
|
|
264
|
-
// destructure: [status=201, body=null, headers=undefined]
|
|
265
|
-
// headers defaults to {} via `headers = {}`
|
|
266
|
-
expect(response.status).toBe(201);
|
|
267
|
-
expect(response.body).toBeUndefined(); // null body becomes undefined
|
|
268
|
-
expect(response.headers).toEqual({});
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("non-tuple array [1, 2, 3, 4] with length > 3 treated as body", async () => {
|
|
272
|
-
const mock = schmock();
|
|
273
|
-
mock("GET /long-array", () => [1, 2, 3, 4] as any);
|
|
274
|
-
|
|
275
|
-
const response = await mock.handle("GET", "/long-array");
|
|
276
|
-
// isStatusTuple requires length 2 or 3, so length 4 => not a tuple => body
|
|
277
|
-
expect(response.status).toBe(200);
|
|
278
|
-
expect(response.body).toEqual([1, 2, 3, 4]);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it("status 99 (below 100) is NOT treated as a tuple", async () => {
|
|
282
|
-
const mock = schmock();
|
|
283
|
-
mock("GET /below-range", () => [99, { data: true }]);
|
|
284
|
-
|
|
285
|
-
const response = await mock.handle("GET", "/below-range");
|
|
286
|
-
// isStatusTuple requires value[0] >= 100, so 99 fails => treated as body
|
|
287
|
-
expect(response.status).toBe(200);
|
|
288
|
-
expect(response.body).toEqual([99, { data: true }]);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("status 600 (above 599) is NOT treated as a tuple", async () => {
|
|
292
|
-
const mock = schmock();
|
|
293
|
-
mock("GET /above-range", () => [600, { data: true }]);
|
|
294
|
-
|
|
295
|
-
const response = await mock.handle("GET", "/above-range");
|
|
296
|
-
// isStatusTuple requires value[0] <= 599, so 600 fails => treated as body
|
|
297
|
-
expect(response.status).toBe(200);
|
|
298
|
-
expect(response.body).toEqual([600, { data: true }]);
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
describe("edge cases", () => {
|
|
303
|
-
it("handles response with circular references gracefully", async () => {
|
|
304
|
-
const mock = schmock();
|
|
305
|
-
mock("GET /circular", () => {
|
|
306
|
-
const obj: any = { name: "test" };
|
|
307
|
-
obj.self = obj; // Create circular reference
|
|
308
|
-
return obj;
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
const response = await mock.handle("GET", "/circular");
|
|
312
|
-
|
|
313
|
-
expect(response.status).toBe(200);
|
|
314
|
-
expect(response.body).toHaveProperty("name", "test");
|
|
315
|
-
expect(response.body).toHaveProperty("self");
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it("preserves functions in response objects", async () => {
|
|
319
|
-
const mock = schmock();
|
|
320
|
-
mock("GET /with-functions", () => ({
|
|
321
|
-
data: "test",
|
|
322
|
-
fn: () => "function result",
|
|
323
|
-
}));
|
|
324
|
-
|
|
325
|
-
const response = await mock.handle("GET", "/with-functions");
|
|
326
|
-
|
|
327
|
-
expect(response.status).toBe(200);
|
|
328
|
-
expect(response.body.data).toBe("test");
|
|
329
|
-
expect(typeof response.body.fn).toBe("function");
|
|
330
|
-
expect(response.body.fn()).toBe("function result");
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
});
|
package/src/route-matcher.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { normalizePath } from "./constants.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Compiled callable route with pattern matching
|
|
5
|
-
*/
|
|
6
|
-
export interface CompiledCallableRoute {
|
|
7
|
-
pattern: RegExp;
|
|
8
|
-
params: string[];
|
|
9
|
-
method: Schmock.HttpMethod;
|
|
10
|
-
path: string;
|
|
11
|
-
generator: Schmock.Generator;
|
|
12
|
-
config: Schmock.RouteConfig;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function isGeneratorFunction(
|
|
16
|
-
gen: Schmock.Generator,
|
|
17
|
-
): gen is Schmock.GeneratorFunction {
|
|
18
|
-
return typeof gen === "function";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Find a route that matches the given method and path
|
|
23
|
-
* Uses two-pass matching: static routes first, then parameterized routes
|
|
24
|
-
* Matches routes in registration order (first registered wins)
|
|
25
|
-
*/
|
|
26
|
-
export function findRoute(
|
|
27
|
-
method: Schmock.HttpMethod,
|
|
28
|
-
path: string,
|
|
29
|
-
staticRoutes: Map<string, CompiledCallableRoute>,
|
|
30
|
-
routes: CompiledCallableRoute[],
|
|
31
|
-
): CompiledCallableRoute | undefined {
|
|
32
|
-
// O(1) lookup for static routes
|
|
33
|
-
const staticMatch = staticRoutes.get(`${method} ${normalizePath(path)}`);
|
|
34
|
-
if (staticMatch) {
|
|
35
|
-
return staticMatch;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Fall through to parameterized route scan
|
|
39
|
-
for (const route of routes) {
|
|
40
|
-
if (
|
|
41
|
-
route.method === method &&
|
|
42
|
-
route.params.length > 0 &&
|
|
43
|
-
route.pattern.test(path)
|
|
44
|
-
) {
|
|
45
|
-
return route;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Extract parameter values from path based on route pattern
|
|
54
|
-
* Maps capture groups from regex match to parameter names
|
|
55
|
-
*/
|
|
56
|
-
export function extractParams(
|
|
57
|
-
route: CompiledCallableRoute,
|
|
58
|
-
path: string,
|
|
59
|
-
): Record<string, string> {
|
|
60
|
-
const match = path.match(route.pattern);
|
|
61
|
-
if (!match) return {};
|
|
62
|
-
|
|
63
|
-
const params: Record<string, string> = {};
|
|
64
|
-
route.params.forEach((param, index) => {
|
|
65
|
-
params[param] = match[index + 1];
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
return params;
|
|
69
|
-
}
|