@schmock/core 2.2.3 → 2.3.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.
- package/dist/binary.d.ts +8 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +9 -0
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +60 -29
- package/dist/http-helpers.d.ts +15 -3
- package/dist/http-helpers.d.ts.map +1 -1
- package/dist/http-helpers.js +24 -6
- package/dist/index.d.ts +90 -27
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/interceptor.js +71 -20
- package/dist/plugin-pipeline.d.ts +11 -4
- package/dist/plugin-pipeline.d.ts.map +1 -1
- package/dist/plugin-pipeline.js +98 -35
- package/dist/response-parser.d.ts.map +1 -1
- package/dist/response-parser.js +47 -17
- package/package.json +5 -3
- package/src/binary.test.ts +126 -0
- package/src/binary.ts +11 -0
- package/src/builder.ts +82 -35
- package/src/http-helpers.test.ts +39 -0
- package/src/http-helpers.ts +46 -10
- package/src/index.ts +1 -0
- package/src/interceptor.test.ts +197 -0
- package/src/interceptor.ts +215 -128
- package/src/parser.property.test.ts +91 -55
- package/src/plugin-pipeline.ts +150 -45
- package/src/plugin-system.test.ts +95 -0
- package/src/response-parser.ts +58 -19
- package/src/server.test.ts +81 -12
- package/src/steps/async-support.steps.ts +29 -8
- package/src/steps/audit-onerror-tuple.steps.ts +14 -30
- package/src/steps/error-handling.steps.ts +463 -253
- package/src/steps/fetch-interceptor.steps.ts +457 -0
- package/src/steps/http-methods.steps.ts +19 -8
- package/src/steps/lifecycle-events.steps.ts +50 -43
- package/src/steps/performance-reliability.steps.ts +78 -411
- package/src/steps/plugin-integration.steps.ts +54 -0
- package/src/steps/request-history.steps.ts +412 -230
- package/src/steps/standalone-server.steps.ts +7 -13
- package/src/steps/state-concurrency.steps.ts +142 -714
- package/src/steps/interceptor.steps.ts +0 -260
- package/src/steps/stateful-workflows.steps.ts +0 -353
package/src/interceptor.ts
CHANGED
|
@@ -1,7 +1,100 @@
|
|
|
1
1
|
/// <reference path="../schmock.d.ts" />
|
|
2
2
|
|
|
3
|
+
import { isBinaryBody } from "./binary.js";
|
|
3
4
|
import { isRouteNotFound, toHttpMethod } from "./constants.js";
|
|
4
5
|
|
|
6
|
+
const PASSTHROUGH = Symbol("schmock.fetch.passthrough");
|
|
7
|
+
|
|
8
|
+
type InterceptorResult = Response | typeof PASSTHROUGH;
|
|
9
|
+
|
|
10
|
+
function toFetchBinaryBody(
|
|
11
|
+
body: ArrayBuffer | ArrayBufferView,
|
|
12
|
+
): ArrayBuffer | Uint8Array<ArrayBuffer> {
|
|
13
|
+
if (body instanceof ArrayBuffer) return body;
|
|
14
|
+
|
|
15
|
+
const bytes = new Uint8Array(body.byteLength);
|
|
16
|
+
bytes.set(new Uint8Array(body.buffer, body.byteOffset, body.byteLength));
|
|
17
|
+
return bytes;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface RegisteredInterceptor {
|
|
21
|
+
token: symbol;
|
|
22
|
+
intercept: (
|
|
23
|
+
input: RequestInfo | URL,
|
|
24
|
+
init?: RequestInit,
|
|
25
|
+
) => Promise<InterceptorResult>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface InterceptorSession {
|
|
29
|
+
baselineFetch: typeof globalThis.fetch;
|
|
30
|
+
dispatchFetch: typeof globalThis.fetch;
|
|
31
|
+
interceptors: RegisteredInterceptor[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let activeSession: InterceptorSession | undefined;
|
|
35
|
+
|
|
36
|
+
function createInterceptorSession(): InterceptorSession {
|
|
37
|
+
const baselineFetch = globalThis.fetch;
|
|
38
|
+
const interceptors: RegisteredInterceptor[] = [];
|
|
39
|
+
const dispatchFetch: typeof globalThis.fetch = async (input, init) => {
|
|
40
|
+
const snapshot = interceptors.slice();
|
|
41
|
+
|
|
42
|
+
for (let index = snapshot.length - 1; index >= 0; index -= 1) {
|
|
43
|
+
const result = await snapshot[index].intercept(input, init);
|
|
44
|
+
if (result !== PASSTHROUGH) {
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return baselineFetch(input, init);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return { baselineFetch, dispatchFetch, interceptors };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function registerInterceptor(
|
|
56
|
+
intercept: RegisteredInterceptor["intercept"],
|
|
57
|
+
): Schmock.InterceptHandle {
|
|
58
|
+
let session = activeSession;
|
|
59
|
+
if (!session || globalThis.fetch !== session.dispatchFetch) {
|
|
60
|
+
session = createInterceptorSession();
|
|
61
|
+
activeSession = session;
|
|
62
|
+
globalThis.fetch = session.dispatchFetch;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const token = Symbol("schmock.fetch.interceptor");
|
|
66
|
+
session.interceptors.push({ token, intercept });
|
|
67
|
+
let active = true;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
restore() {
|
|
71
|
+
if (!active) return;
|
|
72
|
+
|
|
73
|
+
active = false;
|
|
74
|
+
const index = session.interceptors.findIndex(
|
|
75
|
+
(entry) => entry.token === token,
|
|
76
|
+
);
|
|
77
|
+
if (index !== -1) {
|
|
78
|
+
session.interceptors.splice(index, 1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (session.interceptors.length !== 0) return;
|
|
82
|
+
if (activeSession === session) {
|
|
83
|
+
activeSession = undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A library may have installed its own fetch wrapper after Schmock. Its
|
|
87
|
+
// replacement is now the current owner and must not be overwritten.
|
|
88
|
+
if (globalThis.fetch === session.dispatchFetch) {
|
|
89
|
+
globalThis.fetch = session.baselineFetch;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
get active() {
|
|
93
|
+
return active;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
5
98
|
/**
|
|
6
99
|
* Extract pathname from a URL string, handling both absolute and relative URLs.
|
|
7
100
|
*/
|
|
@@ -172,149 +265,143 @@ export function createFetchInterceptor(
|
|
|
172
265
|
errorFormatter,
|
|
173
266
|
} = options;
|
|
174
267
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return originalFetch(input, init);
|
|
268
|
+
return registerInterceptor(
|
|
269
|
+
async (
|
|
270
|
+
input: RequestInfo | URL,
|
|
271
|
+
init?: RequestInit,
|
|
272
|
+
): Promise<InterceptorResult> => {
|
|
273
|
+
// Resolve the URL string
|
|
274
|
+
const urlString =
|
|
275
|
+
input instanceof Request
|
|
276
|
+
? input.url
|
|
277
|
+
: input instanceof URL
|
|
278
|
+
? input.href
|
|
279
|
+
: input;
|
|
280
|
+
|
|
281
|
+
const path = extractPathname(urlString);
|
|
282
|
+
|
|
283
|
+
// BaseUrl filter — non-matching requests go straight to real fetch.
|
|
284
|
+
// Two modes:
|
|
285
|
+
// - origin form ("https://api.example.com/v1"): require matching
|
|
286
|
+
// origin AND matching path prefix.
|
|
287
|
+
// - path form ("/api"): match pathname prefix only.
|
|
288
|
+
// Both enforce a segment boundary so "/api" doesn't match "/apiv2".
|
|
289
|
+
if (baseUrl) {
|
|
290
|
+
const { origin: baseOrigin, path: basePath } = parseBaseUrl(baseUrl);
|
|
291
|
+
if (baseOrigin) {
|
|
292
|
+
const reqOrigin = extractOrigin(urlString);
|
|
293
|
+
if (reqOrigin !== baseOrigin) {
|
|
294
|
+
return PASSTHROUGH;
|
|
295
|
+
}
|
|
204
296
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
297
|
+
if (basePath) {
|
|
298
|
+
const isMatch = path === basePath || path.startsWith(`${basePath}/`);
|
|
299
|
+
if (!isMatch) {
|
|
300
|
+
return PASSTHROUGH;
|
|
301
|
+
}
|
|
210
302
|
}
|
|
211
303
|
}
|
|
212
|
-
}
|
|
213
304
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
305
|
+
// Build adapter request
|
|
306
|
+
const method =
|
|
307
|
+
input instanceof Request ? input.method : (init?.method ?? "GET");
|
|
308
|
+
const headers = extractHeaders(input, init);
|
|
309
|
+
const query = extractQuery(urlString);
|
|
310
|
+
const body = await extractBody(input, init);
|
|
311
|
+
|
|
312
|
+
let adapterRequest: Schmock.AdapterRequest = {
|
|
313
|
+
method,
|
|
314
|
+
path,
|
|
315
|
+
headers,
|
|
316
|
+
body,
|
|
317
|
+
query,
|
|
318
|
+
};
|
|
228
319
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
320
|
+
try {
|
|
321
|
+
// Apply beforeRequest hook
|
|
322
|
+
if (beforeRequest) {
|
|
323
|
+
const modified = await beforeRequest(adapterRequest);
|
|
324
|
+
if (modified) {
|
|
325
|
+
adapterRequest = modified;
|
|
326
|
+
}
|
|
235
327
|
}
|
|
236
|
-
}
|
|
237
328
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
{
|
|
242
|
-
headers: adapterRequest.headers,
|
|
243
|
-
body: adapterRequest.body,
|
|
244
|
-
query: adapterRequest.query,
|
|
245
|
-
},
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
// Route not found — passthrough or 404
|
|
249
|
-
if (isRouteNotFound(schmockResponse)) {
|
|
250
|
-
if (passthrough) {
|
|
251
|
-
return originalFetch(input, init);
|
|
252
|
-
}
|
|
253
|
-
return new Response(
|
|
254
|
-
JSON.stringify({
|
|
255
|
-
error: "No matching mock route found",
|
|
256
|
-
code: "ROUTE_NOT_FOUND",
|
|
257
|
-
}),
|
|
329
|
+
const schmockResponse = await handle(
|
|
330
|
+
toHttpMethod(adapterRequest.method),
|
|
331
|
+
adapterRequest.path,
|
|
258
332
|
{
|
|
259
|
-
|
|
260
|
-
|
|
333
|
+
headers: adapterRequest.headers,
|
|
334
|
+
body: adapterRequest.body,
|
|
335
|
+
query: adapterRequest.query,
|
|
261
336
|
},
|
|
262
337
|
);
|
|
263
|
-
}
|
|
264
338
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
339
|
+
// Route not found — passthrough or 404
|
|
340
|
+
if (isRouteNotFound(schmockResponse)) {
|
|
341
|
+
if (passthrough) {
|
|
342
|
+
return PASSTHROUGH;
|
|
343
|
+
}
|
|
344
|
+
return new Response(
|
|
345
|
+
JSON.stringify({
|
|
346
|
+
error: "No matching mock route found",
|
|
347
|
+
code: "ROUTE_NOT_FOUND",
|
|
348
|
+
}),
|
|
349
|
+
{
|
|
350
|
+
status: 404,
|
|
351
|
+
headers: { "content-type": "application/json" },
|
|
352
|
+
},
|
|
353
|
+
);
|
|
271
354
|
}
|
|
272
|
-
}
|
|
273
355
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
356
|
+
// Apply beforeResponse hook
|
|
357
|
+
let response: Schmock.AdapterResponse = schmockResponse;
|
|
358
|
+
if (beforeResponse) {
|
|
359
|
+
const modified = await beforeResponse(response, adapterRequest);
|
|
360
|
+
if (modified) {
|
|
361
|
+
response = modified;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
283
364
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
299
|
-
);
|
|
300
|
-
return new Response(JSON.stringify(formatted), {
|
|
301
|
-
status: 500,
|
|
302
|
-
headers: { "content-type": "application/json" },
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
throw error;
|
|
306
|
-
}
|
|
307
|
-
};
|
|
365
|
+
// Build fetch Response
|
|
366
|
+
const responseHeaders = new Headers(response.headers);
|
|
367
|
+
if (
|
|
368
|
+
!responseHeaders.has("content-type") &&
|
|
369
|
+
response.body !== null &&
|
|
370
|
+
response.body !== undefined
|
|
371
|
+
) {
|
|
372
|
+
responseHeaders.set(
|
|
373
|
+
"content-type",
|
|
374
|
+
isBinaryBody(response.body)
|
|
375
|
+
? "application/octet-stream"
|
|
376
|
+
: "application/json",
|
|
377
|
+
);
|
|
378
|
+
}
|
|
308
379
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
380
|
+
const responseBody =
|
|
381
|
+
response.body === null || response.body === undefined
|
|
382
|
+
? null
|
|
383
|
+
: typeof response.body === "string"
|
|
384
|
+
? response.body
|
|
385
|
+
: isBinaryBody(response.body)
|
|
386
|
+
? toFetchBinaryBody(response.body)
|
|
387
|
+
: JSON.stringify(response.body);
|
|
388
|
+
|
|
389
|
+
return new Response(responseBody, {
|
|
390
|
+
status: response.status,
|
|
391
|
+
headers: responseHeaders,
|
|
392
|
+
});
|
|
393
|
+
} catch (error) {
|
|
394
|
+
if (errorFormatter) {
|
|
395
|
+
const formatted = errorFormatter(
|
|
396
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
397
|
+
);
|
|
398
|
+
return new Response(JSON.stringify(formatted), {
|
|
399
|
+
status: 500,
|
|
400
|
+
headers: { "content-type": "application/json" },
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
throw error;
|
|
314
404
|
}
|
|
315
405
|
},
|
|
316
|
-
|
|
317
|
-
return active;
|
|
318
|
-
},
|
|
319
|
-
};
|
|
406
|
+
);
|
|
320
407
|
}
|
|
@@ -83,8 +83,6 @@ const doubleSlashPath = fc
|
|
|
83
83
|
.array(safeSeg, { minLength: 2, maxLength: 4 })
|
|
84
84
|
.map((segs) => `/${segs.join("//")}`);
|
|
85
85
|
|
|
86
|
-
const trailingSlashPath = safePath.map((p) => `${p}/`);
|
|
87
|
-
|
|
88
86
|
const deepPath = fc
|
|
89
87
|
.array(safeSeg, { minLength: 10, maxLength: 30 })
|
|
90
88
|
.map((segs) => `/${segs.join("/")}`);
|
|
@@ -106,14 +104,55 @@ const paramRouteKey = fc
|
|
|
106
104
|
return `${m} /${parts.join("/")}`;
|
|
107
105
|
});
|
|
108
106
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
const parameterizedRouteCase = fc
|
|
108
|
+
.tuple(
|
|
109
|
+
method,
|
|
110
|
+
fc.uniqueArray(
|
|
111
|
+
fc.record({
|
|
112
|
+
literal: safeSeg,
|
|
113
|
+
parameter: safeSeg,
|
|
114
|
+
value: safeSeg,
|
|
115
|
+
}),
|
|
116
|
+
{
|
|
117
|
+
minLength: 1,
|
|
118
|
+
maxLength: 4,
|
|
119
|
+
selector: (part) => part.parameter,
|
|
120
|
+
},
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
.map(([httpMethod, parts]) => ({
|
|
124
|
+
key: `${httpMethod} /${parts
|
|
125
|
+
.map((part) => `${part.literal}/:${part.parameter}`)
|
|
126
|
+
.join("/")}`,
|
|
127
|
+
concretePath: `/${parts
|
|
128
|
+
.map((part) => `${part.literal}/${part.value}`)
|
|
129
|
+
.join("/")}`,
|
|
130
|
+
parameterNames: parts.map((part) => part.parameter),
|
|
131
|
+
parameterValues: parts.map((part) => part.value),
|
|
132
|
+
}));
|
|
113
133
|
|
|
114
134
|
// --- Arbitrary inputs ---
|
|
115
135
|
|
|
116
|
-
const
|
|
136
|
+
const asciiString = fc
|
|
137
|
+
.array(fc.integer({ min: 0, max: 0x7f }), {
|
|
138
|
+
minLength: 0,
|
|
139
|
+
maxLength: 200,
|
|
140
|
+
})
|
|
141
|
+
.map((codePoints) => String.fromCharCode(...codePoints));
|
|
142
|
+
const unicodeString = fc.string({ minLength: 0, maxLength: 200 });
|
|
143
|
+
|
|
144
|
+
function expectParseResultOrRouteError(input: string): void {
|
|
145
|
+
let result: ReturnType<typeof parseRouteKey>;
|
|
146
|
+
try {
|
|
147
|
+
result = parseRouteKey(input);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
expect(error).toBeInstanceOf(RouteParseError);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
expect(`${result.method} ${result.path}`).toBe(input);
|
|
154
|
+
expect(result.pattern.test(result.path)).toBe(true);
|
|
155
|
+
}
|
|
117
156
|
|
|
118
157
|
// --- Namespace generators ---
|
|
119
158
|
|
|
@@ -133,42 +172,35 @@ const namespace = fc.oneof(
|
|
|
133
172
|
|
|
134
173
|
describe("parseRouteKey — fuzz", () => {
|
|
135
174
|
it("never throws an unhandled error on arbitrary ASCII input", () => {
|
|
136
|
-
fc.assert(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
parseRouteKey(input);
|
|
140
|
-
} catch (e) {
|
|
141
|
-
expect(e).toBeInstanceOf(RouteParseError);
|
|
142
|
-
}
|
|
143
|
-
}),
|
|
144
|
-
{ numRuns: 1000 },
|
|
145
|
-
);
|
|
175
|
+
fc.assert(fc.property(asciiString, expectParseResultOrRouteError), {
|
|
176
|
+
numRuns: 1000,
|
|
177
|
+
});
|
|
146
178
|
});
|
|
147
179
|
|
|
148
180
|
it("never throws an unhandled error on arbitrary unicode input", () => {
|
|
149
|
-
fc.assert(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
parseRouteKey(input);
|
|
153
|
-
} catch (e) {
|
|
154
|
-
expect(e).toBeInstanceOf(RouteParseError);
|
|
155
|
-
}
|
|
156
|
-
}),
|
|
157
|
-
{ numRuns: 1000 },
|
|
158
|
-
);
|
|
181
|
+
fc.assert(fc.property(unicodeString, expectParseResultOrRouteError), {
|
|
182
|
+
numRuns: 1000,
|
|
183
|
+
});
|
|
159
184
|
});
|
|
160
185
|
|
|
161
|
-
it("
|
|
186
|
+
it("escapes regex metacharacters in literal path segments", () => {
|
|
162
187
|
fc.assert(
|
|
163
|
-
fc.property(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
188
|
+
fc.property(
|
|
189
|
+
method,
|
|
190
|
+
safeSeg,
|
|
191
|
+
regexMetaSeg,
|
|
192
|
+
safeSeg,
|
|
193
|
+
(httpMethod, prefix, literal, suffix) => {
|
|
194
|
+
const result = parseRouteKey(
|
|
195
|
+
`${httpMethod} /${prefix}/${literal}/${suffix}`,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
expect(result.pattern.test(result.path)).toBe(true);
|
|
199
|
+
expect(result.pattern.test(`/${prefix}/literal/${suffix}`)).toBe(
|
|
200
|
+
false,
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
),
|
|
172
204
|
{ numRuns: 500 },
|
|
173
205
|
);
|
|
174
206
|
});
|
|
@@ -195,36 +227,40 @@ describe("parseRouteKey — fuzz", () => {
|
|
|
195
227
|
);
|
|
196
228
|
});
|
|
197
229
|
|
|
198
|
-
it("
|
|
230
|
+
it("matches concrete parameterized paths and captures every value", () => {
|
|
199
231
|
fc.assert(
|
|
200
|
-
fc.property(
|
|
201
|
-
const result = parseRouteKey(key);
|
|
202
|
-
const
|
|
203
|
-
|
|
232
|
+
fc.property(parameterizedRouteCase, (routeCase) => {
|
|
233
|
+
const result = parseRouteKey(routeCase.key);
|
|
234
|
+
const match = result.pattern.exec(routeCase.concretePath);
|
|
235
|
+
|
|
236
|
+
expect(match).not.toBeNull();
|
|
237
|
+
expect(match?.slice(1)).toEqual(routeCase.parameterValues);
|
|
238
|
+
expect(result.params).toEqual(routeCase.parameterNames);
|
|
204
239
|
}),
|
|
205
240
|
{ numRuns: 1000 },
|
|
206
241
|
);
|
|
207
242
|
});
|
|
208
243
|
|
|
209
|
-
it("
|
|
244
|
+
it("rejects slash-delimited near misses for parameterized paths", () => {
|
|
210
245
|
fc.assert(
|
|
211
|
-
fc.property(
|
|
212
|
-
const result = parseRouteKey(key);
|
|
213
|
-
const
|
|
214
|
-
|
|
246
|
+
fc.property(parameterizedRouteCase, safeSeg, (routeCase, extra) => {
|
|
247
|
+
const result = parseRouteKey(routeCase.key);
|
|
248
|
+
const nearMiss = `${routeCase.concretePath}/${extra}`;
|
|
249
|
+
|
|
250
|
+
expect(result.pattern.test(nearMiss)).toBe(false);
|
|
215
251
|
}),
|
|
216
|
-
{ numRuns:
|
|
252
|
+
{ numRuns: 500 },
|
|
217
253
|
);
|
|
218
254
|
});
|
|
219
255
|
|
|
220
|
-
it("
|
|
256
|
+
it("does not normalize malformed separators while matching", () => {
|
|
221
257
|
fc.assert(
|
|
222
|
-
fc.property(routeKey,
|
|
258
|
+
fc.property(routeKey, (key) => {
|
|
223
259
|
const result = parseRouteKey(key);
|
|
224
|
-
const
|
|
225
|
-
expect(
|
|
260
|
+
const doubledLeadingSlash = `/${result.path}`;
|
|
261
|
+
expect(result.pattern.test(doubledLeadingSlash)).toBe(false);
|
|
226
262
|
}),
|
|
227
|
-
{ numRuns:
|
|
263
|
+
{ numRuns: 500 },
|
|
228
264
|
);
|
|
229
265
|
});
|
|
230
266
|
|
|
@@ -287,7 +323,7 @@ describe("handle() — fuzz", () => {
|
|
|
287
323
|
mock("DELETE /items/:id/comments/:cid", () => ({ deleted: true }));
|
|
288
324
|
|
|
289
325
|
fc.assert(
|
|
290
|
-
fc.asyncProperty(method,
|
|
326
|
+
fc.asyncProperty(method, asciiString, async (m, reqPath) => {
|
|
291
327
|
const response = await mock.handle(m, reqPath);
|
|
292
328
|
expect(response).toBeDefined();
|
|
293
329
|
expect(typeof response.status).toBe("number");
|
|
@@ -302,7 +338,7 @@ describe("handle() — fuzz", () => {
|
|
|
302
338
|
mock("GET /data/:key", () => ({ ok: true }));
|
|
303
339
|
|
|
304
340
|
fc.assert(
|
|
305
|
-
fc.asyncProperty(
|
|
341
|
+
fc.asyncProperty(unicodeString, async (reqPath) => {
|
|
306
342
|
const response = await mock.handle("GET", reqPath);
|
|
307
343
|
expect(response).toBeDefined();
|
|
308
344
|
expect(typeof response.status).toBe("number");
|