@schmock/core 1.9.1 → 1.9.2
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/builder.d.ts.map +1 -1
- package/dist/builder.js +57 -31
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +7 -0
- package/dist/http-helpers.d.ts +4 -1
- package/dist/http-helpers.d.ts.map +1 -1
- package/dist/http-helpers.js +18 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/route-matcher.d.ts.map +1 -1
- package/dist/route-matcher.js +2 -2
- package/package.json +3 -2
- package/src/builder.ts +63 -38
- package/src/constants.ts +9 -0
- package/src/http-helpers.ts +24 -2
- package/src/index.ts +1 -0
- package/src/route-matcher.ts +3 -3
package/dist/builder.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAuDA;;;;GAIG;AACH,qBAAa,oBAAoB;IAYnB,OAAO,CAAC,YAAY;IAXhC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,WAAW,CAA2C;IAC9D,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,UAAU,CAAiC;IAEnD,OAAO,CAAC,SAAS,CAAoC;gBAEjC,YAAY,GAAE,OAAO,CAAC,YAAiB;IAa3D,WAAW,CACT,KAAK,EAAE,OAAO,CAAC,QAAQ,EACvB,SAAS,EAAE,OAAO,CAAC,SAAS,EAC5B,MAAM,EAAE,OAAO,CAAC,WAAW,GAC1B,IAAI;
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAuDA;;;;GAIG;AACH,qBAAa,oBAAoB;IAYnB,OAAO,CAAC,YAAY;IAXhC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,WAAW,CAA2C;IAC9D,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,UAAU,CAAiC;IAEnD,OAAO,CAAC,SAAS,CAAoC;gBAEjC,YAAY,GAAE,OAAO,CAAC,YAAiB;IAa3D,WAAW,CACT,KAAK,EAAE,OAAO,CAAC,QAAQ,EACvB,SAAS,EAAE,OAAO,CAAC,SAAS,EAC5B,MAAM,EAAE,OAAO,CAAC,WAAW,GAC1B,IAAI;IAiFP,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,oBAAoB,GAAG,IAAI;IAIvD,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI;IAoBlC,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE;IAS5E,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO;IAS3D,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM;IAS7D,WAAW,CACT,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAC3B,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,GAAG,SAAS;IAYpC,SAAS,IAAI,OAAO,CAAC,SAAS,EAAE;IAQhC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAMnC,EAAE,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,EAC/B,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GACnD,IAAI;IAUP,GAAG,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,EAChC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GACnD,IAAI;IAKP,OAAO,CAAC,IAAI;IAWZ,KAAK,IAAI,IAAI;IAeb,YAAY,IAAI,IAAI;IAKpB,UAAU,IAAI,IAAI;IAWlB,MAAM,CAAC,IAAI,SAAI,EAAE,QAAQ,SAAc,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC;IAqDrE,KAAK,IAAI,IAAI;IAUP,MAAM,CACV,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,GAC/B,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;IAoO5B;;;;OAIG;YACW,UAAU;CAezB"}
|
package/dist/builder.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
-
import { toHttpMethod } from "./constants.js";
|
|
2
|
+
import { normalizePath, toHttpMethod } from "./constants.js";
|
|
3
3
|
import { errorMessage, RouteDefinitionError, RouteNotFoundError, SchmockError, } from "./errors.js";
|
|
4
4
|
import { collectBody, parseNodeHeaders, parseNodeQuery, writeSchmockResponse, } from "./http-helpers.js";
|
|
5
5
|
import { parseRouteKey } from "./parser.js";
|
|
@@ -119,10 +119,7 @@ export class CallableMockInstance {
|
|
|
119
119
|
// Store static routes (no params) in Map for O(1) lookup
|
|
120
120
|
// Only store the first registration — "first registration wins" semantics
|
|
121
121
|
if (parsed.params.length === 0) {
|
|
122
|
-
const
|
|
123
|
-
? parsed.path.slice(0, -1)
|
|
124
|
-
: parsed.path;
|
|
125
|
-
const key = `${parsed.method} ${normalizedPath}`;
|
|
122
|
+
const key = `${parsed.method} ${normalizePath(parsed.path)}`;
|
|
126
123
|
if (!this.staticRoutes.has(key)) {
|
|
127
124
|
this.staticRoutes.set(key, compiledRoute);
|
|
128
125
|
}
|
|
@@ -152,26 +149,26 @@ export class CallableMockInstance {
|
|
|
152
149
|
}
|
|
153
150
|
// ===== Request Spy / History API =====
|
|
154
151
|
history(method, path) {
|
|
155
|
-
if (method
|
|
156
|
-
return this.requestHistory.filter((r) => r.method === method && r.path === path);
|
|
152
|
+
if (method || path) {
|
|
153
|
+
return this.requestHistory.filter((r) => (!method || r.method === method) && (!path || r.path === path));
|
|
157
154
|
}
|
|
158
155
|
return [...this.requestHistory];
|
|
159
156
|
}
|
|
160
157
|
called(method, path) {
|
|
161
|
-
if (method
|
|
162
|
-
return this.requestHistory.some((r) => r.method === method && r.path === path);
|
|
158
|
+
if (method || path) {
|
|
159
|
+
return this.requestHistory.some((r) => (!method || r.method === method) && (!path || r.path === path));
|
|
163
160
|
}
|
|
164
161
|
return this.requestHistory.length > 0;
|
|
165
162
|
}
|
|
166
163
|
callCount(method, path) {
|
|
167
|
-
if (method
|
|
168
|
-
return this.requestHistory.filter((r) => r.method === method && r.path === path).length;
|
|
164
|
+
if (method || path) {
|
|
165
|
+
return this.requestHistory.filter((r) => (!method || r.method === method) && (!path || r.path === path)).length;
|
|
169
166
|
}
|
|
170
167
|
return this.requestHistory.length;
|
|
171
168
|
}
|
|
172
169
|
lastRequest(method, path) {
|
|
173
|
-
if (method
|
|
174
|
-
const filtered = this.requestHistory.filter((r) => r.method === method && r.path === path);
|
|
170
|
+
if (method || path) {
|
|
171
|
+
const filtered = this.requestHistory.filter((r) => (!method || r.method === method) && (!path || r.path === path));
|
|
175
172
|
return filtered[filtered.length - 1];
|
|
176
173
|
}
|
|
177
174
|
return this.requestHistory[this.requestHistory.length - 1];
|
|
@@ -185,7 +182,7 @@ export class CallableMockInstance {
|
|
|
185
182
|
}));
|
|
186
183
|
}
|
|
187
184
|
getState() {
|
|
188
|
-
return this.globalConfig.state || {};
|
|
185
|
+
return { ...(this.globalConfig.state || {}) };
|
|
189
186
|
}
|
|
190
187
|
// ===== Lifecycle Events =====
|
|
191
188
|
on(event, listener) {
|
|
@@ -242,14 +239,29 @@ export class CallableMockInstance {
|
|
|
242
239
|
throw new SchmockError("Server is already running", "SERVER_ALREADY_RUNNING");
|
|
243
240
|
}
|
|
244
241
|
const httpServer = createServer((req, res) => {
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
242
|
+
const handleRequest = async () => {
|
|
243
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
244
|
+
const method = toHttpMethod(req.method ?? "GET");
|
|
245
|
+
const path = url.pathname;
|
|
246
|
+
const headers = parseNodeHeaders(req);
|
|
247
|
+
const query = parseNodeQuery(url);
|
|
248
|
+
const body = await collectBody(req, headers);
|
|
249
|
+
const schmockResponse = await this.handle(method, path, {
|
|
250
|
+
headers,
|
|
251
|
+
body,
|
|
252
|
+
query,
|
|
253
|
+
});
|
|
251
254
|
writeSchmockResponse(res, schmockResponse);
|
|
252
|
-
}
|
|
255
|
+
};
|
|
256
|
+
handleRequest().catch((error) => {
|
|
257
|
+
if (!res.headersSent) {
|
|
258
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
259
|
+
}
|
|
260
|
+
res.end(JSON.stringify({
|
|
261
|
+
error: error instanceof Error ? error.message : "Internal Server Error",
|
|
262
|
+
code: "SERVER_ERROR",
|
|
263
|
+
}));
|
|
264
|
+
});
|
|
253
265
|
});
|
|
254
266
|
this.server = httpServer;
|
|
255
267
|
return new Promise((resolve, reject) => {
|
|
@@ -273,18 +285,20 @@ export class CallableMockInstance {
|
|
|
273
285
|
this.logger.log("server", "Server stopped");
|
|
274
286
|
}
|
|
275
287
|
async handle(method, path, options) {
|
|
276
|
-
const requestId = crypto.randomUUID();
|
|
277
288
|
const handleStart = performance.now();
|
|
289
|
+
const requestId = this.globalConfig.debug ? crypto.randomUUID() : "";
|
|
290
|
+
const reqQuery = options?.query || {};
|
|
291
|
+
const reqHeaders = options?.headers || {};
|
|
278
292
|
this.logger.log("request", `[${requestId}] ${method} ${path}`, {
|
|
279
|
-
headers:
|
|
280
|
-
query:
|
|
293
|
+
headers: reqHeaders,
|
|
294
|
+
query: reqQuery,
|
|
281
295
|
bodyType: options?.body ? typeof options.body : "none",
|
|
282
296
|
});
|
|
283
297
|
this.logger.time(`request-${requestId}`);
|
|
284
298
|
this.emit("request:start", {
|
|
285
299
|
method,
|
|
286
300
|
path,
|
|
287
|
-
headers:
|
|
301
|
+
headers: reqHeaders,
|
|
288
302
|
});
|
|
289
303
|
try {
|
|
290
304
|
// Apply namespace if configured
|
|
@@ -307,6 +321,12 @@ export class CallableMockInstance {
|
|
|
307
321
|
body: { error: error.message, code: error.code },
|
|
308
322
|
headers: {},
|
|
309
323
|
};
|
|
324
|
+
this.emit("request:end", {
|
|
325
|
+
method,
|
|
326
|
+
path,
|
|
327
|
+
status: 404,
|
|
328
|
+
duration: performance.now() - handleStart,
|
|
329
|
+
});
|
|
310
330
|
this.logger.timeEnd(`request-${requestId}`);
|
|
311
331
|
return response;
|
|
312
332
|
}
|
|
@@ -348,8 +368,8 @@ export class CallableMockInstance {
|
|
|
348
368
|
method,
|
|
349
369
|
path: requestPath,
|
|
350
370
|
params,
|
|
351
|
-
query:
|
|
352
|
-
headers:
|
|
371
|
+
query: reqQuery,
|
|
372
|
+
headers: reqHeaders,
|
|
353
373
|
body: options?.body,
|
|
354
374
|
state: this.globalConfig.state || {},
|
|
355
375
|
};
|
|
@@ -366,8 +386,8 @@ export class CallableMockInstance {
|
|
|
366
386
|
route: matchedRoute.config,
|
|
367
387
|
method,
|
|
368
388
|
params,
|
|
369
|
-
query:
|
|
370
|
-
headers:
|
|
389
|
+
query: reqQuery,
|
|
390
|
+
headers: reqHeaders,
|
|
371
391
|
body: options?.body,
|
|
372
392
|
state: new Map(),
|
|
373
393
|
routeState: this.globalConfig.state || {},
|
|
@@ -391,8 +411,8 @@ export class CallableMockInstance {
|
|
|
391
411
|
method,
|
|
392
412
|
path: requestPath,
|
|
393
413
|
params,
|
|
394
|
-
query:
|
|
395
|
-
headers:
|
|
414
|
+
query: reqQuery,
|
|
415
|
+
headers: reqHeaders,
|
|
396
416
|
body: options?.body,
|
|
397
417
|
timestamp: Date.now(),
|
|
398
418
|
response: { status: response.status, body: response.body },
|
|
@@ -425,6 +445,12 @@ export class CallableMockInstance {
|
|
|
425
445
|
};
|
|
426
446
|
// Apply delay even for error responses
|
|
427
447
|
await this.applyDelay();
|
|
448
|
+
this.emit("request:end", {
|
|
449
|
+
method,
|
|
450
|
+
path,
|
|
451
|
+
status: 500,
|
|
452
|
+
duration: performance.now() - handleStart,
|
|
453
|
+
});
|
|
428
454
|
this.logger.log("error", `[${requestId}] Returning error response 500`);
|
|
429
455
|
this.logger.timeEnd(`request-${requestId}`);
|
|
430
456
|
return errorResponse;
|
package/dist/constants.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export declare const ROUTE_NOT_FOUND_CODE: "ROUTE_NOT_FOUND";
|
|
|
3
3
|
export declare const HTTP_METHODS: readonly HttpMethod[];
|
|
4
4
|
export declare function isHttpMethod(method: string): method is HttpMethod;
|
|
5
5
|
export declare function toHttpMethod(method: string): HttpMethod;
|
|
6
|
+
export declare function normalizePath(path: string): string;
|
|
7
|
+
export declare function toRouteKey(method: HttpMethod, path: string): Schmock.RouteKey;
|
|
6
8
|
/**
|
|
7
9
|
* Check if a value is a status tuple: [status, body] or [status, body, headers]
|
|
8
10
|
* Guards against misinterpreting numeric arrays like [1, 2, 3] as tuples.
|
package/dist/constants.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAG,iBAA0B,CAAC;AAE/D,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAQpC,CAAC;AAEX,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,UAAU,CAEjE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAMvD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAQxE"}
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAG,iBAA0B,CAAC;AAE/D,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAQpC,CAAC;AAEX,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,UAAU,CAEjE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAMvD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAG7E;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAQxE"}
|
package/dist/constants.js
CHANGED
|
@@ -18,6 +18,13 @@ export function toHttpMethod(method) {
|
|
|
18
18
|
}
|
|
19
19
|
return upper;
|
|
20
20
|
}
|
|
21
|
+
export function normalizePath(path) {
|
|
22
|
+
return path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
|
|
23
|
+
}
|
|
24
|
+
export function toRouteKey(method, path) {
|
|
25
|
+
const key = `${method} ${path}`;
|
|
26
|
+
return key;
|
|
27
|
+
}
|
|
21
28
|
/**
|
|
22
29
|
* Check if a value is a status tuple: [status, body] or [status, body, headers]
|
|
23
30
|
* Guards against misinterpreting numeric arrays like [1, 2, 3] as tuples.
|
package/dist/http-helpers.d.ts
CHANGED
|
@@ -12,8 +12,11 @@ export declare function parseNodeQuery(url: URL): Record<string, string>;
|
|
|
12
12
|
* Collect and parse the request body from a Node.js IncomingMessage.
|
|
13
13
|
* Returns parsed JSON if content-type includes "json", otherwise the raw string.
|
|
14
14
|
* Returns undefined for empty bodies.
|
|
15
|
+
* @param req - Node.js IncomingMessage
|
|
16
|
+
* @param headers - Parsed request headers
|
|
17
|
+
* @param maxBodySize - Maximum body size in bytes (default: 10 MB)
|
|
15
18
|
*/
|
|
16
|
-
export declare function collectBody(req: IncomingMessage, headers: Record<string, string
|
|
19
|
+
export declare function collectBody(req: IncomingMessage, headers: Record<string, string>, maxBodySize?: number): Promise<unknown>;
|
|
17
20
|
/**
|
|
18
21
|
* Write a Schmock Response to a Node.js ServerResponse.
|
|
19
22
|
* Serializes non-string bodies as JSON and sets content-type when missing.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-helpers.d.ts","sourceRoot":"","sources":["../src/http-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ7E;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAM/D;
|
|
1
|
+
{"version":3,"file":"http-helpers.d.ts","sourceRoot":"","sources":["../src/http-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ7E;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAM/D;AAKD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,WAAW,SAAwB,GAClC,OAAO,CAAC,OAAO,CAAC,CAqClB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAC1B,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,IAAI,CAuBN"}
|
package/dist/http-helpers.js
CHANGED
|
@@ -21,15 +21,30 @@ export function parseNodeQuery(url) {
|
|
|
21
21
|
});
|
|
22
22
|
return query;
|
|
23
23
|
}
|
|
24
|
+
/** Default body size limit: 10 MB */
|
|
25
|
+
const DEFAULT_MAX_BODY_SIZE = 10 * 1024 * 1024;
|
|
24
26
|
/**
|
|
25
27
|
* Collect and parse the request body from a Node.js IncomingMessage.
|
|
26
28
|
* Returns parsed JSON if content-type includes "json", otherwise the raw string.
|
|
27
29
|
* Returns undefined for empty bodies.
|
|
30
|
+
* @param req - Node.js IncomingMessage
|
|
31
|
+
* @param headers - Parsed request headers
|
|
32
|
+
* @param maxBodySize - Maximum body size in bytes (default: 10 MB)
|
|
28
33
|
*/
|
|
29
|
-
export function collectBody(req, headers) {
|
|
30
|
-
return new Promise((resolve) => {
|
|
34
|
+
export function collectBody(req, headers, maxBodySize = DEFAULT_MAX_BODY_SIZE) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
31
36
|
const chunks = [];
|
|
32
|
-
|
|
37
|
+
let totalSize = 0;
|
|
38
|
+
req.on("error", reject);
|
|
39
|
+
req.on("data", (chunk) => {
|
|
40
|
+
totalSize += chunk.length;
|
|
41
|
+
if (totalSize > maxBodySize) {
|
|
42
|
+
req.destroy();
|
|
43
|
+
reject(Object.assign(new Error("Request body too large"), { status: 413 }));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
chunks.push(chunk);
|
|
47
|
+
});
|
|
33
48
|
req.on("end", () => {
|
|
34
49
|
const raw = Buffer.concat(chunks).toString();
|
|
35
50
|
if (!raw) {
|
package/dist/index.d.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* @returns A callable mock instance
|
|
23
23
|
*/
|
|
24
24
|
export declare function schmock(config?: Schmock.GlobalConfig): Schmock.CallableMockInstance;
|
|
25
|
-
export { HTTP_METHODS, isHttpMethod, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, } from "./constants.js";
|
|
25
|
+
export { HTTP_METHODS, isHttpMethod, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, toRouteKey, } from "./constants.js";
|
|
26
26
|
export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors.js";
|
|
27
27
|
export { collectBody, parseNodeHeaders, parseNodeQuery, writeSchmockResponse, } from "./http-helpers.js";
|
|
28
28
|
export type { CallableMockInstance, Generator, GeneratorFunction, GlobalConfig, HttpMethod, Plugin, PluginContext, PluginResult, RequestContext, RequestOptions, RequestRecord, Response, ResponseBody, ResponseResult, RouteConfig, RouteInfo, RouteKey, ServerInfo, StaticData, } from "./types.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,OAAO,CACrB,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,GAC5B,OAAO,CAAC,oBAAoB,CAmD9B;AAGD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,OAAO,CACrB,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,GAC5B,OAAO,CAAC,oBAAoB,CAmD9B;AAGD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,YAAY,EACZ,UAAU,GACX,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,GACb,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,oBAAoB,EACpB,SAAS,EACT,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,MAAM,EACN,aAAa,EACb,YAAY,EACZ,cAAc,EACd,cAAc,EACd,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,WAAW,EACX,SAAS,EACT,QAAQ,EACR,UAAU,EACV,UAAU,GACX,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -59,7 +59,7 @@ export function schmock(config) {
|
|
|
59
59
|
return callableInstance;
|
|
60
60
|
}
|
|
61
61
|
// Re-export constants and utilities
|
|
62
|
-
export { HTTP_METHODS, isHttpMethod, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, } from "./constants.js";
|
|
62
|
+
export { HTTP_METHODS, isHttpMethod, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, toRouteKey, } from "./constants.js";
|
|
63
63
|
// Re-export errors
|
|
64
64
|
export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors.js";
|
|
65
65
|
// Re-export HTTP server helpers
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../src/route-matcher.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../src/route-matcher.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC;IAC7B,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC;CAC7B;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,OAAO,CAAC,SAAS,GACrB,GAAG,IAAI,OAAO,CAAC,iBAAiB,CAElC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,qBAAqB,CAAC,EAChD,MAAM,EAAE,qBAAqB,EAAE,GAC9B,qBAAqB,GAAG,SAAS,CAmBnC;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,qBAAqB,EAC5B,IAAI,EAAE,MAAM,GACX,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUxB"}
|
package/dist/route-matcher.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizePath } from "./constants.js";
|
|
1
2
|
export function isGeneratorFunction(gen) {
|
|
2
3
|
return typeof gen === "function";
|
|
3
4
|
}
|
|
@@ -8,8 +9,7 @@ export function isGeneratorFunction(gen) {
|
|
|
8
9
|
*/
|
|
9
10
|
export function findRoute(method, path, staticRoutes, routes) {
|
|
10
11
|
// O(1) lookup for static routes
|
|
11
|
-
const
|
|
12
|
-
const staticMatch = staticRoutes.get(`${method} ${normalizedPath}`);
|
|
12
|
+
const staticMatch = staticRoutes.get(`${method} ${normalizePath(path)}`);
|
|
13
13
|
if (staticMatch) {
|
|
14
14
|
return staticMatch;
|
|
15
15
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schmock/core",
|
|
3
3
|
"description": "Core functionality for Schmock",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@amiceli/vitest-cucumber": "^6.2.0",
|
|
36
|
-
"@types/
|
|
36
|
+
"@types/json-schema": "^7.0.15",
|
|
37
|
+
"@types/node": "^25.3.0",
|
|
37
38
|
"@vitest/ui": "^4.0.18",
|
|
38
39
|
"vitest": "^4.0.18"
|
|
39
40
|
}
|
package/src/builder.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Server } from "node:http";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
|
-
import { toHttpMethod } from "./constants.js";
|
|
3
|
+
import { normalizePath, toHttpMethod } from "./constants.js";
|
|
4
4
|
import {
|
|
5
5
|
errorMessage,
|
|
6
6
|
RouteDefinitionError,
|
|
@@ -153,11 +153,7 @@ export class CallableMockInstance {
|
|
|
153
153
|
// Store static routes (no params) in Map for O(1) lookup
|
|
154
154
|
// Only store the first registration — "first registration wins" semantics
|
|
155
155
|
if (parsed.params.length === 0) {
|
|
156
|
-
const
|
|
157
|
-
parsed.path.endsWith("/") && parsed.path !== "/"
|
|
158
|
-
? parsed.path.slice(0, -1)
|
|
159
|
-
: parsed.path;
|
|
160
|
-
const key = `${parsed.method} ${normalizedPath}`;
|
|
156
|
+
const key = `${parsed.method} ${normalizePath(parsed.path)}`;
|
|
161
157
|
if (!this.staticRoutes.has(key)) {
|
|
162
158
|
this.staticRoutes.set(key, compiledRoute);
|
|
163
159
|
}
|
|
@@ -197,27 +193,27 @@ export class CallableMockInstance {
|
|
|
197
193
|
// ===== Request Spy / History API =====
|
|
198
194
|
|
|
199
195
|
history(method?: Schmock.HttpMethod, path?: string): Schmock.RequestRecord[] {
|
|
200
|
-
if (method
|
|
196
|
+
if (method || path) {
|
|
201
197
|
return this.requestHistory.filter(
|
|
202
|
-
(r) => r.method === method && r.path === path,
|
|
198
|
+
(r) => (!method || r.method === method) && (!path || r.path === path),
|
|
203
199
|
);
|
|
204
200
|
}
|
|
205
201
|
return [...this.requestHistory];
|
|
206
202
|
}
|
|
207
203
|
|
|
208
204
|
called(method?: Schmock.HttpMethod, path?: string): boolean {
|
|
209
|
-
if (method
|
|
205
|
+
if (method || path) {
|
|
210
206
|
return this.requestHistory.some(
|
|
211
|
-
(r) => r.method === method && r.path === path,
|
|
207
|
+
(r) => (!method || r.method === method) && (!path || r.path === path),
|
|
212
208
|
);
|
|
213
209
|
}
|
|
214
210
|
return this.requestHistory.length > 0;
|
|
215
211
|
}
|
|
216
212
|
|
|
217
213
|
callCount(method?: Schmock.HttpMethod, path?: string): number {
|
|
218
|
-
if (method
|
|
214
|
+
if (method || path) {
|
|
219
215
|
return this.requestHistory.filter(
|
|
220
|
-
(r) => r.method === method && r.path === path,
|
|
216
|
+
(r) => (!method || r.method === method) && (!path || r.path === path),
|
|
221
217
|
).length;
|
|
222
218
|
}
|
|
223
219
|
return this.requestHistory.length;
|
|
@@ -227,9 +223,9 @@ export class CallableMockInstance {
|
|
|
227
223
|
method?: Schmock.HttpMethod,
|
|
228
224
|
path?: string,
|
|
229
225
|
): Schmock.RequestRecord | undefined {
|
|
230
|
-
if (method
|
|
226
|
+
if (method || path) {
|
|
231
227
|
const filtered = this.requestHistory.filter(
|
|
232
|
-
(r) => r.method === method && r.path === path,
|
|
228
|
+
(r) => (!method || r.method === method) && (!path || r.path === path),
|
|
233
229
|
);
|
|
234
230
|
return filtered[filtered.length - 1];
|
|
235
231
|
}
|
|
@@ -247,7 +243,7 @@ export class CallableMockInstance {
|
|
|
247
243
|
}
|
|
248
244
|
|
|
249
245
|
getState(): Record<string, unknown> {
|
|
250
|
-
return this.globalConfig.state || {};
|
|
246
|
+
return { ...(this.globalConfig.state || {}) };
|
|
251
247
|
}
|
|
252
248
|
|
|
253
249
|
// ===== Lifecycle Events =====
|
|
@@ -324,19 +320,33 @@ export class CallableMockInstance {
|
|
|
324
320
|
}
|
|
325
321
|
|
|
326
322
|
const httpServer = createServer((req, res) => {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.handle(method, path, {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
)
|
|
339
|
-
|
|
323
|
+
const handleRequest = async () => {
|
|
324
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
325
|
+
const method = toHttpMethod(req.method ?? "GET");
|
|
326
|
+
const path = url.pathname;
|
|
327
|
+
const headers = parseNodeHeaders(req);
|
|
328
|
+
const query = parseNodeQuery(url);
|
|
329
|
+
const body = await collectBody(req, headers);
|
|
330
|
+
const schmockResponse = await this.handle(method, path, {
|
|
331
|
+
headers,
|
|
332
|
+
body,
|
|
333
|
+
query,
|
|
334
|
+
});
|
|
335
|
+
writeSchmockResponse(res, schmockResponse);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
handleRequest().catch((error) => {
|
|
339
|
+
if (!res.headersSent) {
|
|
340
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
341
|
+
}
|
|
342
|
+
res.end(
|
|
343
|
+
JSON.stringify({
|
|
344
|
+
error:
|
|
345
|
+
error instanceof Error ? error.message : "Internal Server Error",
|
|
346
|
+
code: "SERVER_ERROR",
|
|
347
|
+
}),
|
|
348
|
+
);
|
|
349
|
+
});
|
|
340
350
|
});
|
|
341
351
|
|
|
342
352
|
this.server = httpServer;
|
|
@@ -369,11 +379,13 @@ export class CallableMockInstance {
|
|
|
369
379
|
path: string,
|
|
370
380
|
options?: Schmock.RequestOptions,
|
|
371
381
|
): Promise<Schmock.Response> {
|
|
372
|
-
const requestId = crypto.randomUUID();
|
|
373
382
|
const handleStart = performance.now();
|
|
383
|
+
const requestId = this.globalConfig.debug ? crypto.randomUUID() : "";
|
|
384
|
+
const reqQuery = options?.query || {};
|
|
385
|
+
const reqHeaders = options?.headers || {};
|
|
374
386
|
this.logger.log("request", `[${requestId}] ${method} ${path}`, {
|
|
375
|
-
headers:
|
|
376
|
-
query:
|
|
387
|
+
headers: reqHeaders,
|
|
388
|
+
query: reqQuery,
|
|
377
389
|
bodyType: options?.body ? typeof options.body : "none",
|
|
378
390
|
});
|
|
379
391
|
this.logger.time(`request-${requestId}`);
|
|
@@ -381,7 +393,7 @@ export class CallableMockInstance {
|
|
|
381
393
|
this.emit("request:start", {
|
|
382
394
|
method,
|
|
383
395
|
path,
|
|
384
|
-
headers:
|
|
396
|
+
headers: reqHeaders,
|
|
385
397
|
});
|
|
386
398
|
|
|
387
399
|
try {
|
|
@@ -414,6 +426,12 @@ export class CallableMockInstance {
|
|
|
414
426
|
body: { error: error.message, code: error.code },
|
|
415
427
|
headers: {},
|
|
416
428
|
};
|
|
429
|
+
this.emit("request:end", {
|
|
430
|
+
method,
|
|
431
|
+
path,
|
|
432
|
+
status: 404,
|
|
433
|
+
duration: performance.now() - handleStart,
|
|
434
|
+
});
|
|
417
435
|
this.logger.timeEnd(`request-${requestId}`);
|
|
418
436
|
return response;
|
|
419
437
|
}
|
|
@@ -473,8 +491,8 @@ export class CallableMockInstance {
|
|
|
473
491
|
method,
|
|
474
492
|
path: requestPath,
|
|
475
493
|
params,
|
|
476
|
-
query:
|
|
477
|
-
headers:
|
|
494
|
+
query: reqQuery,
|
|
495
|
+
headers: reqHeaders,
|
|
478
496
|
body: options?.body,
|
|
479
497
|
state: this.globalConfig.state || {},
|
|
480
498
|
};
|
|
@@ -492,8 +510,8 @@ export class CallableMockInstance {
|
|
|
492
510
|
route: matchedRoute.config,
|
|
493
511
|
method,
|
|
494
512
|
params,
|
|
495
|
-
query:
|
|
496
|
-
headers:
|
|
513
|
+
query: reqQuery,
|
|
514
|
+
headers: reqHeaders,
|
|
497
515
|
body: options?.body,
|
|
498
516
|
state: new Map(),
|
|
499
517
|
routeState: this.globalConfig.state || {},
|
|
@@ -528,8 +546,8 @@ export class CallableMockInstance {
|
|
|
528
546
|
method,
|
|
529
547
|
path: requestPath,
|
|
530
548
|
params,
|
|
531
|
-
query:
|
|
532
|
-
headers:
|
|
549
|
+
query: reqQuery,
|
|
550
|
+
headers: reqHeaders,
|
|
533
551
|
body: options?.body,
|
|
534
552
|
timestamp: Date.now(),
|
|
535
553
|
response: { status: response.status, body: response.body },
|
|
@@ -575,6 +593,13 @@ export class CallableMockInstance {
|
|
|
575
593
|
// Apply delay even for error responses
|
|
576
594
|
await this.applyDelay();
|
|
577
595
|
|
|
596
|
+
this.emit("request:end", {
|
|
597
|
+
method,
|
|
598
|
+
path,
|
|
599
|
+
status: 500,
|
|
600
|
+
duration: performance.now() - handleStart,
|
|
601
|
+
});
|
|
602
|
+
|
|
578
603
|
this.logger.log("error", `[${requestId}] Returning error response 500`);
|
|
579
604
|
this.logger.timeEnd(`request-${requestId}`);
|
|
580
605
|
return errorResponse;
|
package/src/constants.ts
CHANGED
|
@@ -24,6 +24,15 @@ export function toHttpMethod(method: string): HttpMethod {
|
|
|
24
24
|
return upper;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export function normalizePath(path: string): string {
|
|
28
|
+
return path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function toRouteKey(method: HttpMethod, path: string): Schmock.RouteKey {
|
|
32
|
+
const key: `${HttpMethod} ${string}` = `${method} ${path}`;
|
|
33
|
+
return key;
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
/**
|
|
28
37
|
* Check if a value is a status tuple: [status, body] or [status, body, headers]
|
|
29
38
|
* Guards against misinterpreting numeric arrays like [1, 2, 3] as tuples.
|
package/src/http-helpers.ts
CHANGED
|
@@ -25,18 +25,40 @@ export function parseNodeQuery(url: URL): Record<string, string> {
|
|
|
25
25
|
return query;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Default body size limit: 10 MB */
|
|
29
|
+
const DEFAULT_MAX_BODY_SIZE = 10 * 1024 * 1024;
|
|
30
|
+
|
|
28
31
|
/**
|
|
29
32
|
* Collect and parse the request body from a Node.js IncomingMessage.
|
|
30
33
|
* Returns parsed JSON if content-type includes "json", otherwise the raw string.
|
|
31
34
|
* Returns undefined for empty bodies.
|
|
35
|
+
* @param req - Node.js IncomingMessage
|
|
36
|
+
* @param headers - Parsed request headers
|
|
37
|
+
* @param maxBodySize - Maximum body size in bytes (default: 10 MB)
|
|
32
38
|
*/
|
|
33
39
|
export function collectBody(
|
|
34
40
|
req: IncomingMessage,
|
|
35
41
|
headers: Record<string, string>,
|
|
42
|
+
maxBodySize = DEFAULT_MAX_BODY_SIZE,
|
|
36
43
|
): Promise<unknown> {
|
|
37
|
-
return new Promise((resolve) => {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
38
45
|
const chunks: Buffer[] = [];
|
|
39
|
-
|
|
46
|
+
let totalSize = 0;
|
|
47
|
+
|
|
48
|
+
req.on("error", reject);
|
|
49
|
+
|
|
50
|
+
req.on("data", (chunk: Buffer) => {
|
|
51
|
+
totalSize += chunk.length;
|
|
52
|
+
if (totalSize > maxBodySize) {
|
|
53
|
+
req.destroy();
|
|
54
|
+
reject(
|
|
55
|
+
Object.assign(new Error("Request body too large"), { status: 413 }),
|
|
56
|
+
);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
chunks.push(chunk);
|
|
60
|
+
});
|
|
61
|
+
|
|
40
62
|
req.on("end", () => {
|
|
41
63
|
const raw = Buffer.concat(chunks).toString();
|
|
42
64
|
if (!raw) {
|
package/src/index.ts
CHANGED
package/src/route-matcher.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizePath } from "./constants.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Compiled callable route with pattern matching
|
|
3
5
|
*/
|
|
@@ -28,9 +30,7 @@ export function findRoute(
|
|
|
28
30
|
routes: CompiledCallableRoute[],
|
|
29
31
|
): CompiledCallableRoute | undefined {
|
|
30
32
|
// O(1) lookup for static routes
|
|
31
|
-
const
|
|
32
|
-
path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
|
|
33
|
-
const staticMatch = staticRoutes.get(`${method} ${normalizedPath}`);
|
|
33
|
+
const staticMatch = staticRoutes.get(`${method} ${normalizePath(path)}`);
|
|
34
34
|
if (staticMatch) {
|
|
35
35
|
return staticMatch;
|
|
36
36
|
}
|