@schmock/core 2.3.0 → 2.4.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/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 +18 -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/interceptor.ts
DELETED
|
@@ -1,407 +0,0 @@
|
|
|
1
|
-
/// <reference path="../schmock.d.ts" />
|
|
2
|
-
|
|
3
|
-
import { isBinaryBody } from "./binary.js";
|
|
4
|
-
import { isRouteNotFound, toHttpMethod } from "./constants.js";
|
|
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
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Extract pathname from a URL string, handling both absolute and relative URLs.
|
|
100
|
-
*/
|
|
101
|
-
function extractPathname(url: string): string {
|
|
102
|
-
const queryStart = url.indexOf("?");
|
|
103
|
-
const urlWithoutQuery = queryStart === -1 ? url : url.slice(0, queryStart);
|
|
104
|
-
|
|
105
|
-
if (urlWithoutQuery.includes("://")) {
|
|
106
|
-
try {
|
|
107
|
-
return new URL(urlWithoutQuery).pathname;
|
|
108
|
-
} catch {
|
|
109
|
-
// Fall through to simple extraction
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (!urlWithoutQuery.startsWith("/")) {
|
|
114
|
-
return `/${urlWithoutQuery}`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return urlWithoutQuery;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Extract origin (scheme + host + port) from a URL string, or null for
|
|
122
|
-
* relative URLs that don't carry one.
|
|
123
|
-
*/
|
|
124
|
-
function extractOrigin(url: string): string | null {
|
|
125
|
-
if (!url.includes("://")) return null;
|
|
126
|
-
try {
|
|
127
|
-
return new URL(url).origin;
|
|
128
|
-
} catch {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Parse the user-supplied baseUrl option into its origin and path parts.
|
|
135
|
-
* - "/api" → { origin: null, path: "/api" }
|
|
136
|
-
* - "https://x.com/api/v1" → { origin: "https://x.com", path: "/api/v1" }
|
|
137
|
-
* - "https://x.com" → { origin: "https://x.com", path: "" }
|
|
138
|
-
*
|
|
139
|
-
* Trailing slash is stripped from the path so the segment-boundary check
|
|
140
|
-
* works the same way for "/api" and "/api/".
|
|
141
|
-
*/
|
|
142
|
-
function parseBaseUrl(baseUrl: string): {
|
|
143
|
-
origin: string | null;
|
|
144
|
-
path: string;
|
|
145
|
-
} {
|
|
146
|
-
if (baseUrl.includes("://")) {
|
|
147
|
-
try {
|
|
148
|
-
const parsed = new URL(baseUrl);
|
|
149
|
-
const rawPath = parsed.pathname;
|
|
150
|
-
const path = rawPath === "/" ? "" : rawPath.replace(/\/$/, "");
|
|
151
|
-
return { origin: parsed.origin, path };
|
|
152
|
-
} catch {
|
|
153
|
-
// Fall through to path-only handling
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return { origin: null, path: baseUrl.replace(/\/$/, "") };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Extract query parameters from a URL string.
|
|
161
|
-
*/
|
|
162
|
-
function extractQuery(url: string): Record<string, string> {
|
|
163
|
-
const queryStart = url.indexOf("?");
|
|
164
|
-
if (queryStart === -1) return {};
|
|
165
|
-
|
|
166
|
-
const params = new URLSearchParams(url.slice(queryStart + 1));
|
|
167
|
-
const result: Record<string, string> = {};
|
|
168
|
-
params.forEach((value, key) => {
|
|
169
|
-
result[key] = value;
|
|
170
|
-
});
|
|
171
|
-
return result;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Extract headers from fetch init or Request object.
|
|
176
|
-
*/
|
|
177
|
-
function extractHeaders(
|
|
178
|
-
input: RequestInfo | URL,
|
|
179
|
-
init?: RequestInit,
|
|
180
|
-
): Record<string, string> {
|
|
181
|
-
const headers: Record<string, string> = {};
|
|
182
|
-
|
|
183
|
-
const raw =
|
|
184
|
-
init?.headers ?? (input instanceof Request ? input.headers : undefined);
|
|
185
|
-
if (!raw) return headers;
|
|
186
|
-
|
|
187
|
-
if (raw instanceof Headers) {
|
|
188
|
-
raw.forEach((value, key) => {
|
|
189
|
-
headers[key.toLowerCase()] = value;
|
|
190
|
-
});
|
|
191
|
-
} else if (Array.isArray(raw)) {
|
|
192
|
-
for (const [key, value] of raw) {
|
|
193
|
-
headers[key.toLowerCase()] = value;
|
|
194
|
-
}
|
|
195
|
-
} else {
|
|
196
|
-
for (const key of Object.keys(raw)) {
|
|
197
|
-
headers[key.toLowerCase()] = (raw as Record<string, string>)[key];
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return headers;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Extract body from fetch init, parsing JSON when possible.
|
|
206
|
-
*/
|
|
207
|
-
async function extractBody(
|
|
208
|
-
input: RequestInfo | URL,
|
|
209
|
-
init?: RequestInit,
|
|
210
|
-
): Promise<unknown> {
|
|
211
|
-
// Per Fetch spec, init.body overrides Request.body when both are present
|
|
212
|
-
const bodyInit = init?.body ?? (input instanceof Request ? input.body : null);
|
|
213
|
-
if (bodyInit === null || bodyInit === undefined) return undefined;
|
|
214
|
-
|
|
215
|
-
// String body — try to parse as JSON
|
|
216
|
-
if (typeof bodyInit === "string") {
|
|
217
|
-
try {
|
|
218
|
-
return JSON.parse(bodyInit);
|
|
219
|
-
} catch {
|
|
220
|
-
return bodyInit;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// URLSearchParams — convert to key/value object
|
|
225
|
-
if (bodyInit instanceof URLSearchParams) {
|
|
226
|
-
const result: Record<string, string> = {};
|
|
227
|
-
bodyInit.forEach((value, key) => {
|
|
228
|
-
result[key] = value;
|
|
229
|
-
});
|
|
230
|
-
return result;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Request with body — clone and read
|
|
234
|
-
if (input instanceof Request && !init?.body && input.body) {
|
|
235
|
-
try {
|
|
236
|
-
return await input.clone().json();
|
|
237
|
-
} catch {
|
|
238
|
-
try {
|
|
239
|
-
return await input.clone().text();
|
|
240
|
-
} catch {
|
|
241
|
-
return undefined;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return undefined;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Create a fetch interceptor that routes requests through mock.handle().
|
|
251
|
-
*/
|
|
252
|
-
export function createFetchInterceptor(
|
|
253
|
-
handle: (
|
|
254
|
-
method: Schmock.HttpMethod,
|
|
255
|
-
path: string,
|
|
256
|
-
requestOptions?: Schmock.RequestOptions,
|
|
257
|
-
) => Promise<Schmock.Response>,
|
|
258
|
-
options: Schmock.InterceptOptions = {},
|
|
259
|
-
): Schmock.InterceptHandle {
|
|
260
|
-
const {
|
|
261
|
-
baseUrl,
|
|
262
|
-
passthrough = true,
|
|
263
|
-
beforeRequest,
|
|
264
|
-
beforeResponse,
|
|
265
|
-
errorFormatter,
|
|
266
|
-
} = options;
|
|
267
|
-
|
|
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
|
-
}
|
|
296
|
-
}
|
|
297
|
-
if (basePath) {
|
|
298
|
-
const isMatch = path === basePath || path.startsWith(`${basePath}/`);
|
|
299
|
-
if (!isMatch) {
|
|
300
|
-
return PASSTHROUGH;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
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
|
-
};
|
|
319
|
-
|
|
320
|
-
try {
|
|
321
|
-
// Apply beforeRequest hook
|
|
322
|
-
if (beforeRequest) {
|
|
323
|
-
const modified = await beforeRequest(adapterRequest);
|
|
324
|
-
if (modified) {
|
|
325
|
-
adapterRequest = modified;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const schmockResponse = await handle(
|
|
330
|
-
toHttpMethod(adapterRequest.method),
|
|
331
|
-
adapterRequest.path,
|
|
332
|
-
{
|
|
333
|
-
headers: adapterRequest.headers,
|
|
334
|
-
body: adapterRequest.body,
|
|
335
|
-
query: adapterRequest.query,
|
|
336
|
-
},
|
|
337
|
-
);
|
|
338
|
-
|
|
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
|
-
);
|
|
354
|
-
}
|
|
355
|
-
|
|
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
|
-
}
|
|
364
|
-
|
|
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
|
-
}
|
|
379
|
-
|
|
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;
|
|
404
|
-
}
|
|
405
|
-
},
|
|
406
|
-
);
|
|
407
|
-
}
|
package/src/namespace.test.ts
DELETED
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { schmock } from "./index";
|
|
3
|
-
|
|
4
|
-
describe("namespace functionality", () => {
|
|
5
|
-
describe("basic namespace behavior", () => {
|
|
6
|
-
it("applies namespace to all routes", async () => {
|
|
7
|
-
const mock = schmock({ namespace: "/api" });
|
|
8
|
-
mock("GET /users", "users");
|
|
9
|
-
mock("POST /users", "create-user");
|
|
10
|
-
|
|
11
|
-
const getResponse = await mock.handle("GET", "/api/users");
|
|
12
|
-
const postResponse = await mock.handle("POST", "/api/users");
|
|
13
|
-
|
|
14
|
-
expect(getResponse.body).toBe("users");
|
|
15
|
-
expect(postResponse.body).toBe("create-user");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("returns 404 for requests without namespace prefix", async () => {
|
|
19
|
-
const mock = schmock({ namespace: "/api" });
|
|
20
|
-
mock("GET /users", "users");
|
|
21
|
-
|
|
22
|
-
const response = await mock.handle("GET", "/users");
|
|
23
|
-
|
|
24
|
-
expect(response.status).toBe(404);
|
|
25
|
-
expect(response.body.error).toContain("Route not found: GET /users");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("handles nested namespaces", async () => {
|
|
29
|
-
const mock = schmock({ namespace: "/api/v1" });
|
|
30
|
-
mock("GET /users", "v1-users");
|
|
31
|
-
|
|
32
|
-
const response = await mock.handle("GET", "/api/v1/users");
|
|
33
|
-
|
|
34
|
-
expect(response.body).toBe("v1-users");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("works with root namespace", async () => {
|
|
38
|
-
const mock = schmock({ namespace: "/" });
|
|
39
|
-
mock("GET /users", "users");
|
|
40
|
-
|
|
41
|
-
const response = await mock.handle("GET", "/users");
|
|
42
|
-
|
|
43
|
-
expect(response.body).toBe("users");
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe("namespace with parameters", () => {
|
|
48
|
-
it("works with parameterized routes under namespace", async () => {
|
|
49
|
-
const mock = schmock({ namespace: "/api" });
|
|
50
|
-
mock("GET /users/:id", ({ params }) => ({ userId: params.id }));
|
|
51
|
-
|
|
52
|
-
const response = await mock.handle("GET", "/api/users/123");
|
|
53
|
-
|
|
54
|
-
expect(response.body).toEqual({ userId: "123" });
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("extracts parameters correctly after namespace removal", async () => {
|
|
58
|
-
const mock = schmock({ namespace: "/api/v1" });
|
|
59
|
-
mock("GET /users/:userId/posts/:postId", ({ params, path }) => ({
|
|
60
|
-
params,
|
|
61
|
-
processedPath: path,
|
|
62
|
-
}));
|
|
63
|
-
|
|
64
|
-
const response = await mock.handle("GET", "/api/v1/users/456/posts/789");
|
|
65
|
-
|
|
66
|
-
expect(response.body).toEqual({
|
|
67
|
-
params: { userId: "456", postId: "789" },
|
|
68
|
-
processedPath: "/users/456/posts/789",
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("passes correct path to context after namespace stripping", async () => {
|
|
73
|
-
const mock = schmock({ namespace: "/api" });
|
|
74
|
-
mock("GET /test/:param", ({ path, params }) => ({
|
|
75
|
-
contextPath: path,
|
|
76
|
-
params,
|
|
77
|
-
}));
|
|
78
|
-
|
|
79
|
-
const response = await mock.handle("GET", "/api/test/value");
|
|
80
|
-
|
|
81
|
-
expect(response.body).toEqual({
|
|
82
|
-
contextPath: "/test/value",
|
|
83
|
-
params: { param: "value" },
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe("namespace edge cases", () => {
|
|
89
|
-
it("handles namespace with trailing slash", async () => {
|
|
90
|
-
const mock = schmock({ namespace: "/api/" });
|
|
91
|
-
mock("GET /users", "users");
|
|
92
|
-
|
|
93
|
-
// Should work with or without the trailing slash in the request
|
|
94
|
-
const response1 = await mock.handle("GET", "/api/users");
|
|
95
|
-
const response2 = await mock.handle("GET", "/api//users");
|
|
96
|
-
|
|
97
|
-
expect(response1.body).toBe("users");
|
|
98
|
-
// New logic gracefully handles double slashes by stripping the full namespace
|
|
99
|
-
expect(response2.status).toBe(200);
|
|
100
|
-
expect(response2.body).toBe("users");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("handles empty namespace", async () => {
|
|
104
|
-
const mock = schmock({ namespace: "" });
|
|
105
|
-
mock("GET /users", "users");
|
|
106
|
-
|
|
107
|
-
const response = await mock.handle("GET", "/users");
|
|
108
|
-
|
|
109
|
-
expect(response.body).toBe("users");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("handles namespace without leading slash", async () => {
|
|
113
|
-
const mock = schmock({ namespace: "api" });
|
|
114
|
-
mock("GET /users", "users");
|
|
115
|
-
|
|
116
|
-
const response = await mock.handle("GET", "api/users");
|
|
117
|
-
|
|
118
|
-
expect(response.body).toBe("users");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("rejects requests that partially match namespace", async () => {
|
|
122
|
-
const mock = schmock({ namespace: "/api" });
|
|
123
|
-
mock("GET /users", "users");
|
|
124
|
-
|
|
125
|
-
const response1 = await mock.handle("GET", "/ap/users");
|
|
126
|
-
const response2 = await mock.handle("GET", "/apiextra/users");
|
|
127
|
-
|
|
128
|
-
expect(response1.status).toBe(404);
|
|
129
|
-
expect(response2.status).toBe(404);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("handles very long namespaces", async () => {
|
|
133
|
-
const longNamespace =
|
|
134
|
-
"/api/v1/internal/microservice/health/monitoring/endpoints";
|
|
135
|
-
const mock = schmock({ namespace: longNamespace });
|
|
136
|
-
mock("GET /status", "healthy");
|
|
137
|
-
|
|
138
|
-
const response = await mock.handle("GET", `${longNamespace}/status`);
|
|
139
|
-
|
|
140
|
-
expect(response.body).toBe("healthy");
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe("namespace without routes", () => {
|
|
145
|
-
it("returns 404 when no routes defined", async () => {
|
|
146
|
-
const mock = schmock({ namespace: "/api" });
|
|
147
|
-
|
|
148
|
-
const response = await mock.handle("GET", "/api/anything");
|
|
149
|
-
|
|
150
|
-
expect(response.status).toBe(404);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("returns 404 for namespace root when no root route", async () => {
|
|
154
|
-
const mock = schmock({ namespace: "/api" });
|
|
155
|
-
mock("GET /users", "users");
|
|
156
|
-
|
|
157
|
-
const response = await mock.handle("GET", "/api");
|
|
158
|
-
|
|
159
|
-
expect(response.status).toBe(404);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("supports root route under namespace", async () => {
|
|
163
|
-
const mock = schmock({ namespace: "/api" });
|
|
164
|
-
mock("GET /", "api-root");
|
|
165
|
-
|
|
166
|
-
const response = await mock.handle("GET", "/api/");
|
|
167
|
-
|
|
168
|
-
expect(response.body).toBe("api-root");
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
describe("namespace with global state", () => {
|
|
173
|
-
it("maintains global state across namespaced routes", async () => {
|
|
174
|
-
const mock = schmock({
|
|
175
|
-
namespace: "/api",
|
|
176
|
-
state: { counter: 0 },
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
mock("POST /increment", ({ state }) => {
|
|
180
|
-
state.counter++;
|
|
181
|
-
return { counter: state.counter };
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
mock("GET /count", ({ state }) => ({ counter: state.counter }));
|
|
185
|
-
|
|
186
|
-
await mock.handle("POST", "/api/increment");
|
|
187
|
-
await mock.handle("POST", "/api/increment");
|
|
188
|
-
const response = await mock.handle("GET", "/api/count");
|
|
189
|
-
|
|
190
|
-
expect(response.body).toEqual({ counter: 2 });
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
describe("namespace with plugins", () => {
|
|
195
|
-
it("works correctly with plugin pipeline", async () => {
|
|
196
|
-
const mock = schmock({ namespace: "/api" });
|
|
197
|
-
|
|
198
|
-
const plugin = {
|
|
199
|
-
name: "namespace-plugin",
|
|
200
|
-
process: (ctx: any, res: any) => {
|
|
201
|
-
return {
|
|
202
|
-
context: ctx,
|
|
203
|
-
response: {
|
|
204
|
-
...res,
|
|
205
|
-
namespacedPath: ctx.path,
|
|
206
|
-
},
|
|
207
|
-
};
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
mock("GET /users", { users: [] }).pipe(plugin);
|
|
212
|
-
|
|
213
|
-
const response = await mock.handle("GET", "/api/users");
|
|
214
|
-
|
|
215
|
-
expect(response.body).toEqual({
|
|
216
|
-
users: [],
|
|
217
|
-
namespacedPath: "/users", // Should be the path after namespace removal
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
describe("special characters in namespace", () => {
|
|
223
|
-
it("handles namespace with special characters", async () => {
|
|
224
|
-
const mock = schmock({ namespace: "/api-v1.2" });
|
|
225
|
-
mock("GET /users", "users");
|
|
226
|
-
|
|
227
|
-
const response = await mock.handle("GET", "/api-v1.2/users");
|
|
228
|
-
|
|
229
|
-
expect(response.body).toBe("users");
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("handles namespace with underscores", async () => {
|
|
233
|
-
const mock = schmock({ namespace: "/my_api" });
|
|
234
|
-
mock("GET /test", "test");
|
|
235
|
-
|
|
236
|
-
const response = await mock.handle("GET", "/my_api/test");
|
|
237
|
-
|
|
238
|
-
expect(response.body).toBe("test");
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it("handles namespace with numbers", async () => {
|
|
242
|
-
const mock = schmock({ namespace: "/api2" });
|
|
243
|
-
mock("GET /version", "v2");
|
|
244
|
-
|
|
245
|
-
const response = await mock.handle("GET", "/api2/version");
|
|
246
|
-
|
|
247
|
-
expect(response.body).toBe("v2");
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
describe("namespace error messages", () => {
|
|
252
|
-
it("provides clear error message for namespace mismatch", async () => {
|
|
253
|
-
const mock = schmock({ namespace: "/api/v1" });
|
|
254
|
-
mock("GET /users", "users");
|
|
255
|
-
|
|
256
|
-
const response = await mock.handle("GET", "/api/v2/users");
|
|
257
|
-
|
|
258
|
-
expect(response.status).toBe(404);
|
|
259
|
-
expect(response.body.error).toContain(
|
|
260
|
-
"Route not found: GET /api/v2/users",
|
|
261
|
-
);
|
|
262
|
-
expect(response.body.code).toBe("ROUTE_NOT_FOUND");
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("includes original requested path in error", async () => {
|
|
266
|
-
const mock = schmock({ namespace: "/api" });
|
|
267
|
-
mock("GET /users", "users");
|
|
268
|
-
|
|
269
|
-
const response = await mock.handle("GET", "/wrong/path");
|
|
270
|
-
|
|
271
|
-
expect(response.body.error).toContain("/wrong/path");
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
});
|