@schmock/core 1.13.0 → 2.0.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/builder.d.ts +2 -0
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +10 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +12 -0
- package/dist/helpers.d.ts +9 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +37 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/interceptor.d.ts +5 -0
- package/dist/interceptor.d.ts.map +1 -0
- package/dist/interceptor.js +196 -0
- package/dist/plugin-pipeline.js +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/builder.ts +20 -0
- package/src/constants.test.ts +40 -0
- package/src/constants.ts +18 -0
- package/src/helpers.test.ts +106 -0
- package/src/helpers.ts +58 -0
- package/src/index.ts +21 -0
- package/src/interceptor.test.ts +160 -0
- package/src/interceptor.ts +252 -0
- package/src/plugin-pipeline.ts +1 -1
- package/src/steps/interceptor.steps.ts +206 -0
- package/src/types.ts +4 -0
package/dist/builder.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare class CallableMockInstance {
|
|
|
13
13
|
private callableRef;
|
|
14
14
|
private server;
|
|
15
15
|
private serverInfo;
|
|
16
|
+
private interceptHandle;
|
|
16
17
|
private listeners;
|
|
17
18
|
constructor(globalConfig?: Schmock.GlobalConfig);
|
|
18
19
|
defineRoute(route: Schmock.RouteKey, generator: Schmock.Generator, config: Schmock.RouteConfig): this;
|
|
@@ -32,6 +33,7 @@ export declare class CallableMockInstance {
|
|
|
32
33
|
resetState(): void;
|
|
33
34
|
listen(port?: number, hostname?: string): Promise<Schmock.ServerInfo>;
|
|
34
35
|
close(): void;
|
|
36
|
+
intercept(options?: Schmock.InterceptOptions): Schmock.InterceptHandle;
|
|
35
37
|
handle(method: Schmock.HttpMethod, path: string, options?: Schmock.RequestOptions): Promise<Schmock.Response>;
|
|
36
38
|
/**
|
|
37
39
|
* Apply configured response delay
|
package/dist/builder.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAwDA;;;;GAIG;AACH,qBAAa,oBAAoB;IAanB,OAAO,CAAC,YAAY;IAZhC,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;IACnD,OAAO,CAAC,eAAe,CAAwC;IAE/D,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;IAYb,SAAS,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,gBAAgB,GAAG,OAAO,CAAC,eAAe;IAgBhE,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
|
@@ -2,6 +2,7 @@ import { createServer } from "node:http";
|
|
|
2
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
|
+
import { createFetchInterceptor } from "./interceptor.js";
|
|
5
6
|
import { parseRouteKey } from "./parser.js";
|
|
6
7
|
import { runPluginPipeline } from "./plugin-pipeline.js";
|
|
7
8
|
import { parseResponse } from "./response-parser.js";
|
|
@@ -52,6 +53,7 @@ export class CallableMockInstance {
|
|
|
52
53
|
callableRef;
|
|
53
54
|
server;
|
|
54
55
|
serverInfo;
|
|
56
|
+
interceptHandle = null;
|
|
55
57
|
// biome-ignore lint/complexity/noBannedTypes: internal storage for event listeners with varying signatures
|
|
56
58
|
listeners = new Map();
|
|
57
59
|
constructor(globalConfig = {}) {
|
|
@@ -284,6 +286,14 @@ export class CallableMockInstance {
|
|
|
284
286
|
this.serverInfo = undefined;
|
|
285
287
|
this.logger.log("server", "Server stopped");
|
|
286
288
|
}
|
|
289
|
+
// ===== Fetch Interceptor =====
|
|
290
|
+
intercept(options) {
|
|
291
|
+
if (this.interceptHandle?.active) {
|
|
292
|
+
throw new SchmockError("Already intercepting. Call restore() first.", "ALREADY_INTERCEPTING");
|
|
293
|
+
}
|
|
294
|
+
this.interceptHandle = createFetchInterceptor((method, path, opts) => this.handle(method, path, opts), options);
|
|
295
|
+
return this.interceptHandle;
|
|
296
|
+
}
|
|
287
297
|
async handle(method, path, options) {
|
|
288
298
|
const handleStart = performance.now();
|
|
289
299
|
const requestId = this.globalConfig.debug ? crypto.randomUUID() : "";
|
package/dist/constants.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ export declare function isHttpMethod(method: string): method is HttpMethod;
|
|
|
5
5
|
export declare function toHttpMethod(method: string): HttpMethod;
|
|
6
6
|
export declare function normalizePath(path: string): string;
|
|
7
7
|
export declare function toRouteKey(method: HttpMethod, path: string): Schmock.RouteKey;
|
|
8
|
+
/**
|
|
9
|
+
* Check if a Schmock response is a route-not-found response.
|
|
10
|
+
* Used by adapters to decide whether to pass through to the real backend.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isRouteNotFound(response: {
|
|
13
|
+
status: number;
|
|
14
|
+
body: unknown;
|
|
15
|
+
}): boolean;
|
|
8
16
|
/**
|
|
9
17
|
* Check if a value is a status tuple: [status, body] or [status, body, headers]
|
|
10
18
|
* 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,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"}
|
|
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,eAAe,CAAC,QAAQ,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,OAAO,CASV;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
|
@@ -25,6 +25,18 @@ export function toRouteKey(method, path) {
|
|
|
25
25
|
const key = `${method} ${path}`;
|
|
26
26
|
return key;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if a Schmock response is a route-not-found response.
|
|
30
|
+
* Used by adapters to decide whether to pass through to the real backend.
|
|
31
|
+
*/
|
|
32
|
+
export function isRouteNotFound(response) {
|
|
33
|
+
const { status, body } = response;
|
|
34
|
+
return (status === 404 &&
|
|
35
|
+
body !== null &&
|
|
36
|
+
typeof body === "object" &&
|
|
37
|
+
"code" in body &&
|
|
38
|
+
body.code === ROUTE_NOT_FOUND_CODE);
|
|
39
|
+
}
|
|
28
40
|
/**
|
|
29
41
|
* Check if a value is a status tuple: [status, body] or [status, body, headers]
|
|
30
42
|
* Guards against misinterpreting numeric arrays like [1, 2, 3] as tuples.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function notFound(message?: string | object): [number, object];
|
|
2
|
+
export declare function badRequest(message?: string | object): [number, object];
|
|
3
|
+
export declare function unauthorized(message?: string | object): [number, object];
|
|
4
|
+
export declare function forbidden(message?: string | object): [number, object];
|
|
5
|
+
export declare function serverError(message?: string | object): [number, object];
|
|
6
|
+
export declare function created(body: object): [number, object];
|
|
7
|
+
export declare function noContent(): [number, null];
|
|
8
|
+
export declare function paginate<T>(items: T[], options?: Schmock.PaginateOptions): Schmock.PaginatedResponse<T>;
|
|
9
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAEA,wBAAgB,QAAQ,CACtB,OAAO,GAAE,MAAM,GAAG,MAAoB,GACrC,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,UAAU,CACxB,OAAO,GAAE,MAAM,GAAG,MAAsB,GACvC,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,YAAY,CAC1B,OAAO,GAAE,MAAM,GAAG,MAAuB,GACxC,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,SAAS,CACvB,OAAO,GAAE,MAAM,GAAG,MAAoB,GACrC,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,WAAW,CACzB,OAAO,GAAE,MAAM,GAAG,MAAgC,GACjD,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAEtD;AAED,wBAAgB,SAAS,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAE1C;AAED,wBAAgB,QAAQ,CAAC,CAAC,EACxB,KAAK,EAAE,CAAC,EAAE,EACV,OAAO,GAAE,OAAO,CAAC,eAAoB,GACpC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAS9B"}
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/// <reference path="../schmock.d.ts" />
|
|
2
|
+
export function notFound(message = "Not Found") {
|
|
3
|
+
const body = typeof message === "string" ? { message } : message;
|
|
4
|
+
return [404, body];
|
|
5
|
+
}
|
|
6
|
+
export function badRequest(message = "Bad Request") {
|
|
7
|
+
const body = typeof message === "string" ? { message } : message;
|
|
8
|
+
return [400, body];
|
|
9
|
+
}
|
|
10
|
+
export function unauthorized(message = "Unauthorized") {
|
|
11
|
+
const body = typeof message === "string" ? { message } : message;
|
|
12
|
+
return [401, body];
|
|
13
|
+
}
|
|
14
|
+
export function forbidden(message = "Forbidden") {
|
|
15
|
+
const body = typeof message === "string" ? { message } : message;
|
|
16
|
+
return [403, body];
|
|
17
|
+
}
|
|
18
|
+
export function serverError(message = "Internal Server Error") {
|
|
19
|
+
const body = typeof message === "string" ? { message } : message;
|
|
20
|
+
return [500, body];
|
|
21
|
+
}
|
|
22
|
+
export function created(body) {
|
|
23
|
+
return [201, body];
|
|
24
|
+
}
|
|
25
|
+
export function noContent() {
|
|
26
|
+
return [204, null];
|
|
27
|
+
}
|
|
28
|
+
export function paginate(items, options = {}) {
|
|
29
|
+
const page = options.page || 1;
|
|
30
|
+
const pageSize = options.pageSize || 10;
|
|
31
|
+
const total = items.length;
|
|
32
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
33
|
+
const start = (page - 1) * pageSize;
|
|
34
|
+
const end = start + pageSize;
|
|
35
|
+
const data = items.slice(start, end);
|
|
36
|
+
return { data, page, pageSize, total, totalPages };
|
|
37
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,8 +22,10 @@
|
|
|
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, toRouteKey, } from "./constants.js";
|
|
25
|
+
export { HTTP_METHODS, isHttpMethod, isRouteNotFound, 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
|
+
export { badRequest, created, forbidden, noContent, notFound, paginate, serverError, unauthorized, } from "./helpers.js";
|
|
27
28
|
export { collectBody, parseNodeHeaders, parseNodeQuery, writeSchmockResponse, } from "./http-helpers.js";
|
|
28
|
-
export
|
|
29
|
+
export { createFetchInterceptor } from "./interceptor.js";
|
|
30
|
+
export type { AdapterRequest, AdapterResponse, CallableMockInstance, Generator, GeneratorFunction, GlobalConfig, HttpMethod, InterceptHandle, InterceptOptions, Plugin, PluginContext, PluginResult, RequestContext, RequestOptions, RequestRecord, Response, ResponseBody, ResponseResult, RouteConfig, RouteInfo, RouteKey, ServerInfo, StaticData, } from "./types.js";
|
|
29
31
|
//# sourceMappingURL=index.d.ts.map
|
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,
|
|
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,CAqD9B;AAGD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,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,UAAU,EACV,OAAO,EACP,SAAS,EACT,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,YAAY,GACb,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,YAAY,EACV,cAAc,EACd,eAAe,EACf,oBAAoB,EACpB,SAAS,EACT,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,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
|
@@ -54,13 +54,19 @@ export function schmock(config) {
|
|
|
54
54
|
getState: instance.getState.bind(instance),
|
|
55
55
|
listen: instance.listen.bind(instance),
|
|
56
56
|
close: instance.close.bind(instance),
|
|
57
|
+
intercept: (options) => instance.intercept(options),
|
|
57
58
|
});
|
|
58
59
|
instance.setCallableRef(callableInstance);
|
|
59
60
|
return callableInstance;
|
|
60
61
|
}
|
|
61
62
|
// Re-export constants and utilities
|
|
62
|
-
export { HTTP_METHODS, isHttpMethod, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, toRouteKey, } from "./constants.js";
|
|
63
|
+
export { HTTP_METHODS, isHttpMethod, isRouteNotFound, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, toRouteKey, } from "./constants.js";
|
|
63
64
|
// Re-export errors
|
|
64
65
|
export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors.js";
|
|
66
|
+
// Re-export response helpers
|
|
67
|
+
export { badRequest, created, forbidden, noContent, notFound, paginate, serverError, unauthorized, } from "./helpers.js";
|
|
65
68
|
// Re-export HTTP server helpers
|
|
66
69
|
export { collectBody, parseNodeHeaders, parseNodeQuery, writeSchmockResponse, } from "./http-helpers.js";
|
|
70
|
+
// Re-export types
|
|
71
|
+
// Re-export interceptor
|
|
72
|
+
export { createFetchInterceptor } from "./interceptor.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a fetch interceptor that routes requests through mock.handle().
|
|
3
|
+
*/
|
|
4
|
+
export declare function createFetchInterceptor(handle: (method: Schmock.HttpMethod, path: string, requestOptions?: Schmock.RequestOptions) => Promise<Schmock.Response>, options?: Schmock.InterceptOptions): Schmock.InterceptHandle;
|
|
5
|
+
//# sourceMappingURL=interceptor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../src/interceptor.ts"],"names":[],"mappings":"AAwGA;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,CACN,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,cAAc,CAAC,EAAE,OAAO,CAAC,cAAc,KACpC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,EAC9B,OAAO,GAAE,OAAO,CAAC,gBAAqB,GACrC,OAAO,CAAC,eAAe,CAyIzB"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/// <reference path="../schmock.d.ts" />
|
|
2
|
+
import { isRouteNotFound, toHttpMethod } from "./constants.js";
|
|
3
|
+
/**
|
|
4
|
+
* Extract pathname from a URL string, handling both absolute and relative URLs.
|
|
5
|
+
*/
|
|
6
|
+
function extractPathname(url) {
|
|
7
|
+
const queryStart = url.indexOf("?");
|
|
8
|
+
const urlWithoutQuery = queryStart === -1 ? url : url.slice(0, queryStart);
|
|
9
|
+
if (urlWithoutQuery.includes("://")) {
|
|
10
|
+
try {
|
|
11
|
+
return new URL(urlWithoutQuery).pathname;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Fall through to simple extraction
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (!urlWithoutQuery.startsWith("/")) {
|
|
18
|
+
return `/${urlWithoutQuery}`;
|
|
19
|
+
}
|
|
20
|
+
return urlWithoutQuery;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Extract query parameters from a URL string.
|
|
24
|
+
*/
|
|
25
|
+
function extractQuery(url) {
|
|
26
|
+
const queryStart = url.indexOf("?");
|
|
27
|
+
if (queryStart === -1)
|
|
28
|
+
return {};
|
|
29
|
+
const params = new URLSearchParams(url.slice(queryStart + 1));
|
|
30
|
+
const result = {};
|
|
31
|
+
params.forEach((value, key) => {
|
|
32
|
+
result[key] = value;
|
|
33
|
+
});
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Extract headers from fetch init or Request object.
|
|
38
|
+
*/
|
|
39
|
+
function extractHeaders(input, init) {
|
|
40
|
+
const headers = {};
|
|
41
|
+
const raw = input instanceof Request ? input.headers : init?.headers;
|
|
42
|
+
if (!raw)
|
|
43
|
+
return headers;
|
|
44
|
+
if (raw instanceof Headers) {
|
|
45
|
+
raw.forEach((value, key) => {
|
|
46
|
+
headers[key] = value;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
else if (Array.isArray(raw)) {
|
|
50
|
+
for (const [key, value] of raw) {
|
|
51
|
+
headers[key] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
Object.assign(headers, raw);
|
|
56
|
+
}
|
|
57
|
+
return headers;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Extract body from fetch init, parsing JSON when possible.
|
|
61
|
+
*/
|
|
62
|
+
async function extractBody(input, init) {
|
|
63
|
+
const rawBody = input instanceof Request ? input.body : init?.body;
|
|
64
|
+
if (rawBody === null || rawBody === undefined)
|
|
65
|
+
return undefined;
|
|
66
|
+
// String body — try to parse as JSON
|
|
67
|
+
const bodyInit = init?.body;
|
|
68
|
+
if (typeof bodyInit === "string") {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(bodyInit);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return bodyInit;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Request with body — clone and read
|
|
77
|
+
if (input instanceof Request && input.body) {
|
|
78
|
+
try {
|
|
79
|
+
return await input.clone().json();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
try {
|
|
83
|
+
return await input.clone().text();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Create a fetch interceptor that routes requests through mock.handle().
|
|
94
|
+
*/
|
|
95
|
+
export function createFetchInterceptor(handle, options = {}) {
|
|
96
|
+
const { baseUrl, passthrough = true, beforeRequest, beforeResponse, errorFormatter, } = options;
|
|
97
|
+
const originalFetch = globalThis.fetch;
|
|
98
|
+
let active = true;
|
|
99
|
+
globalThis.fetch = async (input, init) => {
|
|
100
|
+
// Resolve the URL string
|
|
101
|
+
const urlString = input instanceof Request
|
|
102
|
+
? input.url
|
|
103
|
+
: input instanceof URL
|
|
104
|
+
? input.href
|
|
105
|
+
: input;
|
|
106
|
+
const path = extractPathname(urlString);
|
|
107
|
+
// BaseUrl filter — non-matching requests go straight to real fetch
|
|
108
|
+
if (baseUrl && !path.startsWith(baseUrl)) {
|
|
109
|
+
return originalFetch(input, init);
|
|
110
|
+
}
|
|
111
|
+
// Build adapter request
|
|
112
|
+
const method = input instanceof Request ? input.method : (init?.method ?? "GET");
|
|
113
|
+
const headers = extractHeaders(input, init);
|
|
114
|
+
const query = extractQuery(urlString);
|
|
115
|
+
const body = await extractBody(input, init);
|
|
116
|
+
let adapterRequest = {
|
|
117
|
+
method,
|
|
118
|
+
path,
|
|
119
|
+
headers,
|
|
120
|
+
body,
|
|
121
|
+
query,
|
|
122
|
+
};
|
|
123
|
+
try {
|
|
124
|
+
// Apply beforeRequest hook
|
|
125
|
+
if (beforeRequest) {
|
|
126
|
+
const modified = await beforeRequest(adapterRequest);
|
|
127
|
+
if (modified) {
|
|
128
|
+
adapterRequest = modified;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const schmockResponse = await handle(toHttpMethod(adapterRequest.method), adapterRequest.path, {
|
|
132
|
+
headers: adapterRequest.headers,
|
|
133
|
+
body: adapterRequest.body,
|
|
134
|
+
query: adapterRequest.query,
|
|
135
|
+
});
|
|
136
|
+
// Route not found — passthrough or 404
|
|
137
|
+
if (isRouteNotFound(schmockResponse)) {
|
|
138
|
+
if (passthrough) {
|
|
139
|
+
return originalFetch(input, init);
|
|
140
|
+
}
|
|
141
|
+
return new Response(JSON.stringify({
|
|
142
|
+
error: "No matching mock route found",
|
|
143
|
+
code: "ROUTE_NOT_FOUND",
|
|
144
|
+
}), {
|
|
145
|
+
status: 404,
|
|
146
|
+
headers: { "content-type": "application/json" },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// Apply beforeResponse hook
|
|
150
|
+
let response = schmockResponse;
|
|
151
|
+
if (beforeResponse) {
|
|
152
|
+
const modified = await beforeResponse(response, adapterRequest);
|
|
153
|
+
if (modified) {
|
|
154
|
+
response = modified;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Build fetch Response
|
|
158
|
+
const responseHeaders = new Headers(response.headers);
|
|
159
|
+
if (!responseHeaders.has("content-type") &&
|
|
160
|
+
response.body !== null &&
|
|
161
|
+
response.body !== undefined) {
|
|
162
|
+
responseHeaders.set("content-type", "application/json");
|
|
163
|
+
}
|
|
164
|
+
const responseBody = response.body === null || response.body === undefined
|
|
165
|
+
? null
|
|
166
|
+
: typeof response.body === "string"
|
|
167
|
+
? response.body
|
|
168
|
+
: JSON.stringify(response.body);
|
|
169
|
+
return new Response(responseBody, {
|
|
170
|
+
status: response.status,
|
|
171
|
+
headers: responseHeaders,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (errorFormatter) {
|
|
176
|
+
const formatted = errorFormatter(error instanceof Error ? error : new Error(String(error)));
|
|
177
|
+
return new Response(JSON.stringify(formatted), {
|
|
178
|
+
status: 500,
|
|
179
|
+
headers: { "content-type": "application/json" },
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
restore() {
|
|
187
|
+
if (active) {
|
|
188
|
+
globalThis.fetch = originalFetch;
|
|
189
|
+
active = false;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
get active() {
|
|
193
|
+
return active;
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
package/dist/plugin-pipeline.js
CHANGED
|
@@ -12,7 +12,7 @@ export async function runPluginPipeline(plugins, context, initialResponse, logge
|
|
|
12
12
|
logger.log("pipeline", `Processing plugin: ${plugin.name}`);
|
|
13
13
|
try {
|
|
14
14
|
const result = await plugin.process(currentContext, response);
|
|
15
|
-
if (!result
|
|
15
|
+
if (!result?.context) {
|
|
16
16
|
throw new Error(`Plugin ${plugin.name} didn't return valid result`);
|
|
17
17
|
}
|
|
18
18
|
currentContext = result.context;
|
package/dist/types.d.ts
CHANGED
|
@@ -26,4 +26,8 @@ export type SeedSource = Schmock.SeedSource;
|
|
|
26
26
|
export type SeedConfig = Schmock.SeedConfig;
|
|
27
27
|
export type CliOptions = Schmock.CliOptions;
|
|
28
28
|
export type CliServer = Schmock.CliServer;
|
|
29
|
+
export type AdapterRequest = Schmock.AdapterRequest;
|
|
30
|
+
export type AdapterResponse = Schmock.AdapterResponse;
|
|
31
|
+
export type InterceptOptions = Schmock.InterceptOptions;
|
|
32
|
+
export type InterceptHandle = Schmock.InterceptHandle;
|
|
29
33
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;AAC9C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;AAChE,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AACpC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC;AACtE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;AAC5D,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;AAC9C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;AAChE,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AACpC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC;AACtE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;AAC5D,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;AACtD,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;AACxD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC"}
|
package/package.json
CHANGED
package/src/builder.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
parseNodeQuery,
|
|
14
14
|
writeSchmockResponse,
|
|
15
15
|
} from "./http-helpers.js";
|
|
16
|
+
import { createFetchInterceptor } from "./interceptor.js";
|
|
16
17
|
import { parseRouteKey } from "./parser.js";
|
|
17
18
|
import { runPluginPipeline } from "./plugin-pipeline.js";
|
|
18
19
|
import { parseResponse } from "./response-parser.js";
|
|
@@ -67,6 +68,7 @@ export class CallableMockInstance {
|
|
|
67
68
|
private callableRef: Schmock.CallableMockInstance | undefined;
|
|
68
69
|
private server: Server | undefined;
|
|
69
70
|
private serverInfo: Schmock.ServerInfo | undefined;
|
|
71
|
+
private interceptHandle: Schmock.InterceptHandle | null = null;
|
|
70
72
|
// biome-ignore lint/complexity/noBannedTypes: internal storage for event listeners with varying signatures
|
|
71
73
|
private listeners = new Map<string, Set<Function>>();
|
|
72
74
|
|
|
@@ -374,6 +376,24 @@ export class CallableMockInstance {
|
|
|
374
376
|
this.logger.log("server", "Server stopped");
|
|
375
377
|
}
|
|
376
378
|
|
|
379
|
+
// ===== Fetch Interceptor =====
|
|
380
|
+
|
|
381
|
+
intercept(options?: Schmock.InterceptOptions): Schmock.InterceptHandle {
|
|
382
|
+
if (this.interceptHandle?.active) {
|
|
383
|
+
throw new SchmockError(
|
|
384
|
+
"Already intercepting. Call restore() first.",
|
|
385
|
+
"ALREADY_INTERCEPTING",
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
this.interceptHandle = createFetchInterceptor(
|
|
390
|
+
(method, path, opts) => this.handle(method, path, opts),
|
|
391
|
+
options,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
return this.interceptHandle;
|
|
395
|
+
}
|
|
396
|
+
|
|
377
397
|
async handle(
|
|
378
398
|
method: Schmock.HttpMethod,
|
|
379
399
|
path: string,
|
package/src/constants.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
HTTP_METHODS,
|
|
4
4
|
isHttpMethod,
|
|
5
|
+
isRouteNotFound,
|
|
5
6
|
ROUTE_NOT_FOUND_CODE,
|
|
6
7
|
toHttpMethod,
|
|
7
8
|
} from "./constants";
|
|
@@ -57,3 +58,42 @@ describe("toHttpMethod", () => {
|
|
|
57
58
|
expect(() => toHttpMethod("")).toThrow('Invalid HTTP method: ""');
|
|
58
59
|
});
|
|
59
60
|
});
|
|
61
|
+
|
|
62
|
+
describe("isRouteNotFound", () => {
|
|
63
|
+
it("returns true for a route-not-found response", () => {
|
|
64
|
+
const response = {
|
|
65
|
+
status: 404,
|
|
66
|
+
body: { error: "Route not found", code: ROUTE_NOT_FOUND_CODE },
|
|
67
|
+
headers: {},
|
|
68
|
+
};
|
|
69
|
+
expect(isRouteNotFound(response)).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("returns false for a regular 404 response", () => {
|
|
73
|
+
const response = {
|
|
74
|
+
status: 404,
|
|
75
|
+
body: { message: "User not found" },
|
|
76
|
+
headers: {},
|
|
77
|
+
};
|
|
78
|
+
expect(isRouteNotFound(response)).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns false for a non-404 response", () => {
|
|
82
|
+
const response = {
|
|
83
|
+
status: 200,
|
|
84
|
+
body: { code: ROUTE_NOT_FOUND_CODE },
|
|
85
|
+
headers: {},
|
|
86
|
+
};
|
|
87
|
+
expect(isRouteNotFound(response)).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns false when body is null", () => {
|
|
91
|
+
const response = { status: 404, body: null, headers: {} };
|
|
92
|
+
expect(isRouteNotFound(response)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns false when body is a string", () => {
|
|
96
|
+
const response = { status: 404, body: "not found", headers: {} };
|
|
97
|
+
expect(isRouteNotFound(response)).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
package/src/constants.ts
CHANGED
|
@@ -33,6 +33,24 @@ export function toRouteKey(method: HttpMethod, path: string): Schmock.RouteKey {
|
|
|
33
33
|
return key;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Check if a Schmock response is a route-not-found response.
|
|
38
|
+
* Used by adapters to decide whether to pass through to the real backend.
|
|
39
|
+
*/
|
|
40
|
+
export function isRouteNotFound(response: {
|
|
41
|
+
status: number;
|
|
42
|
+
body: unknown;
|
|
43
|
+
}): boolean {
|
|
44
|
+
const { status, body } = response;
|
|
45
|
+
return (
|
|
46
|
+
status === 404 &&
|
|
47
|
+
body !== null &&
|
|
48
|
+
typeof body === "object" &&
|
|
49
|
+
"code" in body &&
|
|
50
|
+
body.code === ROUTE_NOT_FOUND_CODE
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
/**
|
|
37
55
|
* Check if a value is a status tuple: [status, body] or [status, body, headers]
|
|
38
56
|
* Guards against misinterpreting numeric arrays like [1, 2, 3] as tuples.
|