@schmock/core 1.13.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/builder.d.ts +2 -0
  2. package/dist/builder.d.ts.map +1 -1
  3. package/dist/builder.js +13 -0
  4. package/dist/constants.d.ts +8 -0
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/constants.js +12 -0
  7. package/dist/helpers.d.ts +9 -0
  8. package/dist/helpers.d.ts.map +1 -0
  9. package/dist/helpers.js +37 -0
  10. package/dist/index.d.ts +4 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +7 -1
  13. package/dist/interceptor.d.ts +5 -0
  14. package/dist/interceptor.d.ts.map +1 -0
  15. package/dist/interceptor.js +213 -0
  16. package/dist/plugin-pipeline.js +1 -1
  17. package/dist/types.d.ts +5 -0
  18. package/dist/types.d.ts.map +1 -1
  19. package/package.json +1 -1
  20. package/src/builder.ts +23 -0
  21. package/src/constants.test.ts +40 -0
  22. package/src/constants.ts +18 -0
  23. package/src/helpers.test.ts +147 -0
  24. package/src/helpers.ts +58 -0
  25. package/src/index.ts +21 -0
  26. package/src/interceptor.test.ts +291 -0
  27. package/src/interceptor.ts +272 -0
  28. package/src/parser.property.test.ts +101 -0
  29. package/src/plugin-pipeline.ts +1 -1
  30. package/src/response-parsing.test.ts +74 -0
  31. package/src/server.test.ts +49 -0
  32. package/src/steps/async-support.steps.ts +0 -35
  33. package/src/steps/basic-usage.steps.ts +0 -84
  34. package/src/steps/developer-experience.steps.ts +0 -269
  35. package/src/steps/error-handling.steps.ts +0 -66
  36. package/src/steps/http-methods.steps.ts +0 -66
  37. package/src/steps/interceptor.steps.ts +206 -0
  38. package/src/steps/request-history.steps.ts +0 -75
  39. package/src/steps/route-key-format.steps.ts +0 -19
  40. package/src/types.ts +5 -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
@@ -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;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"}
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;IAiBb,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;IAab,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 = {}) {
@@ -208,6 +210,8 @@ export class CallableMockInstance {
208
210
  }
209
211
  // ===== Reset / Lifecycle =====
210
212
  reset() {
213
+ this.interceptHandle?.restore();
214
+ this.interceptHandle = null;
211
215
  this.close();
212
216
  this.routes = [];
213
217
  this.staticRoutes.clear();
@@ -279,11 +283,20 @@ export class CallableMockInstance {
279
283
  if (!this.server) {
280
284
  return;
281
285
  }
286
+ this.server.closeAllConnections();
282
287
  this.server.close();
283
288
  this.server = undefined;
284
289
  this.serverInfo = undefined;
285
290
  this.logger.log("server", "Server stopped");
286
291
  }
292
+ // ===== Fetch Interceptor =====
293
+ intercept(options) {
294
+ if (this.interceptHandle?.active) {
295
+ throw new SchmockError("Already intercepting. Call restore() first.", "ALREADY_INTERCEPTING");
296
+ }
297
+ this.interceptHandle = createFetchInterceptor((method, path, opts) => this.handle(method, path, opts), options);
298
+ return this.interceptHandle;
299
+ }
287
300
  async handle(method, path, options) {
288
301
  const handleStart = performance.now();
289
302
  const requestId = this.globalConfig.debug ? crypto.randomUUID() : "";
@@ -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.
@@ -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"}
@@ -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 type { CallableMockInstance, Generator, GeneratorFunction, GlobalConfig, HttpMethod, Plugin, PluginContext, PluginResult, RequestContext, RequestOptions, RequestRecord, Response, ResponseBody, ResponseResult, RouteConfig, RouteInfo, RouteKey, ServerInfo, StaticData, } from "./types.js";
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
@@ -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,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"}
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":"AAoHA;;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,CAiJzB"}
@@ -0,0 +1,213 @@
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 = init?.headers ?? (input instanceof Request ? input.headers : undefined);
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.toLowerCase()] = value;
52
+ }
53
+ }
54
+ else {
55
+ for (const key of Object.keys(raw)) {
56
+ headers[key.toLowerCase()] = raw[key];
57
+ }
58
+ }
59
+ return headers;
60
+ }
61
+ /**
62
+ * Extract body from fetch init, parsing JSON when possible.
63
+ */
64
+ async function extractBody(input, init) {
65
+ // Per Fetch spec, init.body overrides Request.body when both are present
66
+ const bodyInit = init?.body ?? (input instanceof Request ? input.body : null);
67
+ if (bodyInit === null || bodyInit === undefined)
68
+ return undefined;
69
+ // String body — try to parse as JSON
70
+ if (typeof bodyInit === "string") {
71
+ try {
72
+ return JSON.parse(bodyInit);
73
+ }
74
+ catch {
75
+ return bodyInit;
76
+ }
77
+ }
78
+ // URLSearchParams — convert to key/value object
79
+ if (bodyInit instanceof URLSearchParams) {
80
+ const result = {};
81
+ bodyInit.forEach((value, key) => {
82
+ result[key] = value;
83
+ });
84
+ return result;
85
+ }
86
+ // Request with body — clone and read
87
+ if (input instanceof Request && !init?.body && input.body) {
88
+ try {
89
+ return await input.clone().json();
90
+ }
91
+ catch {
92
+ try {
93
+ return await input.clone().text();
94
+ }
95
+ catch {
96
+ return undefined;
97
+ }
98
+ }
99
+ }
100
+ return undefined;
101
+ }
102
+ /**
103
+ * Create a fetch interceptor that routes requests through mock.handle().
104
+ */
105
+ export function createFetchInterceptor(handle, options = {}) {
106
+ const { baseUrl, passthrough = true, beforeRequest, beforeResponse, errorFormatter, } = options;
107
+ const originalFetch = globalThis.fetch;
108
+ let active = true;
109
+ globalThis.fetch = async (input, init) => {
110
+ // Resolve the URL string
111
+ const urlString = input instanceof Request
112
+ ? input.url
113
+ : input instanceof URL
114
+ ? input.href
115
+ : input;
116
+ const path = extractPathname(urlString);
117
+ // BaseUrl filter — non-matching requests go straight to real fetch
118
+ // Enforce segment boundary: /api must not match /apiv2
119
+ if (baseUrl) {
120
+ const normalizedBase = baseUrl.endsWith("/")
121
+ ? baseUrl.slice(0, -1)
122
+ : baseUrl;
123
+ const isMatch = path === normalizedBase || path.startsWith(`${normalizedBase}/`);
124
+ if (!isMatch) {
125
+ return originalFetch(input, init);
126
+ }
127
+ }
128
+ // Build adapter request
129
+ const method = input instanceof Request ? input.method : (init?.method ?? "GET");
130
+ const headers = extractHeaders(input, init);
131
+ const query = extractQuery(urlString);
132
+ const body = await extractBody(input, init);
133
+ let adapterRequest = {
134
+ method,
135
+ path,
136
+ headers,
137
+ body,
138
+ query,
139
+ };
140
+ try {
141
+ // Apply beforeRequest hook
142
+ if (beforeRequest) {
143
+ const modified = await beforeRequest(adapterRequest);
144
+ if (modified) {
145
+ adapterRequest = modified;
146
+ }
147
+ }
148
+ const schmockResponse = await handle(toHttpMethod(adapterRequest.method), adapterRequest.path, {
149
+ headers: adapterRequest.headers,
150
+ body: adapterRequest.body,
151
+ query: adapterRequest.query,
152
+ });
153
+ // Route not found — passthrough or 404
154
+ if (isRouteNotFound(schmockResponse)) {
155
+ if (passthrough) {
156
+ return originalFetch(input, init);
157
+ }
158
+ return new Response(JSON.stringify({
159
+ error: "No matching mock route found",
160
+ code: "ROUTE_NOT_FOUND",
161
+ }), {
162
+ status: 404,
163
+ headers: { "content-type": "application/json" },
164
+ });
165
+ }
166
+ // Apply beforeResponse hook
167
+ let response = schmockResponse;
168
+ if (beforeResponse) {
169
+ const modified = await beforeResponse(response, adapterRequest);
170
+ if (modified) {
171
+ response = modified;
172
+ }
173
+ }
174
+ // Build fetch Response
175
+ const responseHeaders = new Headers(response.headers);
176
+ if (!responseHeaders.has("content-type") &&
177
+ response.body !== null &&
178
+ response.body !== undefined) {
179
+ responseHeaders.set("content-type", "application/json");
180
+ }
181
+ const responseBody = response.body === null || response.body === undefined
182
+ ? null
183
+ : typeof response.body === "string"
184
+ ? response.body
185
+ : JSON.stringify(response.body);
186
+ return new Response(responseBody, {
187
+ status: response.status,
188
+ headers: responseHeaders,
189
+ });
190
+ }
191
+ catch (error) {
192
+ if (errorFormatter) {
193
+ const formatted = errorFormatter(error instanceof Error ? error : new Error(String(error)));
194
+ return new Response(JSON.stringify(formatted), {
195
+ status: 500,
196
+ headers: { "content-type": "application/json" },
197
+ });
198
+ }
199
+ throw error;
200
+ }
201
+ };
202
+ return {
203
+ restore() {
204
+ if (active) {
205
+ globalThis.fetch = originalFetch;
206
+ active = false;
207
+ }
208
+ },
209
+ get active() {
210
+ return active;
211
+ },
212
+ };
213
+ }
@@ -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 || !result.context) {
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,9 @@ 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;
33
+ export type AdapterRequestOverride = Schmock.AdapterRequestOverride;
29
34
  //# sourceMappingURL=types.d.ts.map
@@ -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;AACtD,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC"}
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.13.0",
4
+ "version": "2.0.1",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
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
 
@@ -281,6 +283,8 @@ export class CallableMockInstance {
281
283
  // ===== Reset / Lifecycle =====
282
284
 
283
285
  reset(): void {
286
+ this.interceptHandle?.restore();
287
+ this.interceptHandle = null;
284
288
  this.close();
285
289
  this.routes = [];
286
290
  this.staticRoutes.clear();
@@ -368,12 +372,31 @@ export class CallableMockInstance {
368
372
  if (!this.server) {
369
373
  return;
370
374
  }
375
+ this.server.closeAllConnections();
371
376
  this.server.close();
372
377
  this.server = undefined;
373
378
  this.serverInfo = undefined;
374
379
  this.logger.log("server", "Server stopped");
375
380
  }
376
381
 
382
+ // ===== Fetch Interceptor =====
383
+
384
+ intercept(options?: Schmock.InterceptOptions): Schmock.InterceptHandle {
385
+ if (this.interceptHandle?.active) {
386
+ throw new SchmockError(
387
+ "Already intercepting. Call restore() first.",
388
+ "ALREADY_INTERCEPTING",
389
+ );
390
+ }
391
+
392
+ this.interceptHandle = createFetchInterceptor(
393
+ (method, path, opts) => this.handle(method, path, opts),
394
+ options,
395
+ );
396
+
397
+ return this.interceptHandle;
398
+ }
399
+
377
400
  async handle(
378
401
  method: Schmock.HttpMethod,
379
402
  path: string,
@@ -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.