@schmock/core 2.2.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/binary.d.ts +8 -0
  2. package/dist/binary.d.ts.map +1 -0
  3. package/dist/binary.js +9 -0
  4. package/dist/builder.d.ts.map +1 -1
  5. package/dist/builder.js +60 -29
  6. package/dist/http-helpers.d.ts +15 -3
  7. package/dist/http-helpers.d.ts.map +1 -1
  8. package/dist/http-helpers.js +24 -6
  9. package/dist/index.d.ts +90 -27
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/interceptor.d.ts.map +1 -1
  13. package/dist/interceptor.js +71 -20
  14. package/dist/plugin-pipeline.d.ts +11 -4
  15. package/dist/plugin-pipeline.d.ts.map +1 -1
  16. package/dist/plugin-pipeline.js +98 -35
  17. package/dist/response-parser.d.ts.map +1 -1
  18. package/dist/response-parser.js +47 -17
  19. package/package.json +5 -3
  20. package/src/binary.test.ts +126 -0
  21. package/src/binary.ts +11 -0
  22. package/src/builder.ts +82 -35
  23. package/src/http-helpers.test.ts +39 -0
  24. package/src/http-helpers.ts +46 -10
  25. package/src/index.ts +1 -0
  26. package/src/interceptor.test.ts +197 -0
  27. package/src/interceptor.ts +215 -128
  28. package/src/parser.property.test.ts +91 -55
  29. package/src/plugin-pipeline.ts +150 -45
  30. package/src/plugin-system.test.ts +95 -0
  31. package/src/response-parser.ts +58 -19
  32. package/src/server.test.ts +81 -12
  33. package/src/steps/async-support.steps.ts +29 -8
  34. package/src/steps/audit-onerror-tuple.steps.ts +14 -30
  35. package/src/steps/error-handling.steps.ts +463 -253
  36. package/src/steps/fetch-interceptor.steps.ts +457 -0
  37. package/src/steps/http-methods.steps.ts +19 -8
  38. package/src/steps/lifecycle-events.steps.ts +50 -43
  39. package/src/steps/performance-reliability.steps.ts +78 -411
  40. package/src/steps/plugin-integration.steps.ts +54 -0
  41. package/src/steps/request-history.steps.ts +412 -230
  42. package/src/steps/standalone-server.steps.ts +7 -13
  43. package/src/steps/state-concurrency.steps.ts +142 -714
  44. package/src/steps/interceptor.steps.ts +0 -260
  45. package/src/steps/stateful-workflows.steps.ts +0 -353
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Browser-safe binary body detection.
3
+ *
4
+ * Every ArrayBuffer view is accepted, including DataView, typed arrays backed
5
+ * by SharedArrayBuffer, and Node.js Buffer without referencing its global.
6
+ */
7
+ export declare function isBinaryBody(value: unknown): value is ArrayBuffer | ArrayBufferView;
8
+ //# sourceMappingURL=binary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binary.d.ts","sourceRoot":"","sources":["../src/binary.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,WAAW,GAAG,eAAe,CAExC"}
package/dist/binary.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Browser-safe binary body detection.
3
+ *
4
+ * Every ArrayBuffer view is accepted, including DataView, typed arrays backed
5
+ * by SharedArrayBuffer, and Node.js Buffer without referencing its global.
6
+ */
7
+ export function isBinaryBody(value) {
8
+ return value instanceof ArrayBuffer || ArrayBuffer.isView(value);
9
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAuDA;;;;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;IAsFP,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,oBAAoB,GAAG,IAAI;IAIvD,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI;IAoBlC;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE;IAW5E,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;IAgBpC,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;IAgErE,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;IA+O5B;;;;OAIG;YACW,UAAU;CAezB"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AA4DA;;;;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;IAsFP,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,oBAAoB,GAAG,IAAI;IAIvD,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI;IAoBlC;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE;IAW5E,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;IAgBpC,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;IAwErE,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;IAgR5B;;;;OAIG;YACW,UAAU;CAgBzB"}
package/dist/builder.js CHANGED
@@ -1,9 +1,10 @@
1
+ import { isBinaryBody } from "./binary.js";
1
2
  import { normalizePath, toHttpMethod } from "./constants.js";
2
3
  import { errorMessage, RouteDefinitionError, RouteNotFoundError, SchmockError, } from "./errors.js";
3
4
  import { collectBody, parseNodeHeaders, parseNodeQuery, writeSchmockResponse, } from "./http-helpers.js";
4
5
  import { createFetchInterceptor } from "./interceptor.js";
5
6
  import { parseRouteKey } from "./parser.js";
6
- import { runPluginPipeline } from "./plugin-pipeline.js";
7
+ import { recoverGeneratorError, runPluginBeforeRequest, runPluginPipeline, } from "./plugin-pipeline.js";
7
8
  import { parseResponse } from "./response-parser.js";
8
9
  import { extractParams, findRoute, isGeneratorFunction, } from "./route-matcher.js";
9
10
  /**
@@ -83,8 +84,8 @@ export class CallableMockInstance {
83
84
  // Default to plain text for primitives
84
85
  routeConfig.contentType = "text/plain";
85
86
  }
86
- else if (Buffer.isBuffer(generator)) {
87
- // Default to octet-stream for buffers
87
+ else if (isBinaryBody(generator)) {
88
+ // Default to octet-stream for browser and Node binary values
88
89
  routeConfig.contentType = "application/octet-stream";
89
90
  }
90
91
  else {
@@ -298,8 +299,16 @@ export class CallableMockInstance {
298
299
  });
299
300
  this.server = httpServer;
300
301
  return new Promise((resolve, reject) => {
301
- httpServer.on("error", reject);
302
+ const handleStartupError = (error) => {
303
+ if (this.server === httpServer) {
304
+ this.server = undefined;
305
+ this.serverInfo = undefined;
306
+ }
307
+ reject(error);
308
+ };
309
+ httpServer.once("error", handleStartupError);
302
310
  httpServer.listen(port, hostname, () => {
311
+ httpServer.off("error", handleStartupError);
303
312
  const addr = httpServer.address();
304
313
  const actualPort = addr !== null && typeof addr === "object" ? addr.port : port;
305
314
  this.serverInfo = { port: actualPort, hostname };
@@ -361,7 +370,7 @@ export class CallableMockInstance {
361
370
  const response = {
362
371
  status: 404,
363
372
  body: { error: error.message, code: error.code },
364
- headers: {},
373
+ headers: { "content-type": "application/json" },
365
374
  };
366
375
  this.emit("request:end", {
367
376
  method,
@@ -385,7 +394,7 @@ export class CallableMockInstance {
385
394
  const response = {
386
395
  status: 404,
387
396
  body: { error: error.message, code: error.code },
388
- headers: {},
397
+ headers: { "content-type": "application/json" },
389
398
  };
390
399
  this.emit("request:end", {
391
400
  method,
@@ -405,24 +414,8 @@ export class CallableMockInstance {
405
414
  routePath: matchedRoute.path,
406
415
  params,
407
416
  });
408
- // Generate initial response from route handler
409
- const context = {
410
- method,
411
- path: requestPath,
412
- params,
413
- query: reqQuery,
414
- headers: reqHeaders,
415
- body: options?.body,
416
- state: this.globalConfig.state || {},
417
- };
418
- let result;
419
- if (isGeneratorFunction(matchedRoute.generator)) {
420
- result = await matchedRoute.generator(context);
421
- }
422
- else {
423
- result = matchedRoute.generator;
424
- }
425
- // Build plugin context
417
+ // Build plugin context before route code so request guards can reject
418
+ // invalid or unauthorized requests without triggering side effects.
426
419
  let pluginContext = {
427
420
  path: requestPath,
428
421
  route: matchedRoute.config,
@@ -434,11 +427,48 @@ export class CallableMockInstance {
434
427
  state: new Map(),
435
428
  routeState: this.globalConfig.state || {},
436
429
  };
430
+ const preflightResult = await runPluginBeforeRequest(this.plugins, pluginContext, this.logger);
431
+ pluginContext = preflightResult.context;
432
+ if (preflightResult.requestShortCircuited === true) {
433
+ pluginContext = { ...pluginContext, requestShortCircuited: true };
434
+ }
435
+ let result = preflightResult.response;
436
+ let skipPostProcessing = preflightResult.recoveredFromError === true;
437
+ if (result === undefined) {
438
+ const context = {
439
+ method: pluginContext.method,
440
+ path: pluginContext.path,
441
+ params: pluginContext.params,
442
+ query: pluginContext.query,
443
+ headers: pluginContext.headers,
444
+ body: pluginContext.body,
445
+ state: pluginContext.routeState ?? this.globalConfig.state ?? {},
446
+ };
447
+ try {
448
+ if (isGeneratorFunction(matchedRoute.generator)) {
449
+ result = await matchedRoute.generator(context);
450
+ }
451
+ else {
452
+ result = matchedRoute.generator;
453
+ }
454
+ }
455
+ catch (error) {
456
+ const recovery = await recoverGeneratorError(this.plugins, pluginContext, error, this.logger);
457
+ pluginContext = recovery.context;
458
+ result = recovery.response;
459
+ skipPostProcessing = recovery.recoveredFromError === true;
460
+ }
461
+ }
437
462
  // Run plugin pipeline to transform the response
438
463
  try {
439
- const pipelineResult = await runPluginPipeline(this.plugins, pluginContext, result, this.logger);
440
- pluginContext = pipelineResult.context;
441
- result = pipelineResult.response;
464
+ if (skipPostProcessing) {
465
+ this.logger.log("pipeline", "Skipping response processors after error recovery");
466
+ }
467
+ else {
468
+ const pipelineResult = await runPluginPipeline(this.plugins, pluginContext, result, this.logger);
469
+ pluginContext = pipelineResult.context;
470
+ result = pipelineResult.response;
471
+ }
442
472
  }
443
473
  catch (error) {
444
474
  this.logger.log("error", `[${requestId}] Plugin pipeline error: ${errorMessage(error)}`);
@@ -489,7 +519,7 @@ export class CallableMockInstance {
489
519
  error: errorMessage(error),
490
520
  code: error instanceof SchmockError ? error.code : "INTERNAL_ERROR",
491
521
  },
492
- headers: {},
522
+ headers: { "content-type": "application/json" },
493
523
  };
494
524
  // Apply delay even for error responses
495
525
  await this.applyDelay();
@@ -514,10 +544,11 @@ export class CallableMockInstance {
514
544
  if (!effectiveDelay) {
515
545
  return;
516
546
  }
517
- const ms = Array.isArray(effectiveDelay)
547
+ const configuredMs = Array.isArray(effectiveDelay)
518
548
  ? Math.random() * (effectiveDelay[1] - effectiveDelay[0]) +
519
549
  effectiveDelay[0]
520
550
  : effectiveDelay;
551
+ const ms = Math.max(0, configuredMs);
521
552
  await new Promise((resolve) => setTimeout(resolve, ms));
522
553
  }
523
554
  }
@@ -1,9 +1,20 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
1
+ import type { ServerResponse } from "node:http";
2
+ interface RequestWithHeaders {
3
+ readonly headers: {
4
+ readonly [header: string]: string | string[] | undefined;
5
+ };
6
+ }
7
+ interface BodyReadable {
8
+ on(event: "error", listener: (error: Error) => void): this;
9
+ on(event: "data", listener: (chunk: Buffer) => void): this;
10
+ on(event: "end", listener: () => void): this;
11
+ destroy(error?: Error): this;
12
+ }
2
13
  /**
3
14
  * Convert Node.js IncomingMessage headers to a flat Record<string, string>.
4
15
  * Drops array-valued headers (keeps only string values).
5
16
  */
6
- export declare function parseNodeHeaders(req: IncomingMessage): Record<string, string>;
17
+ export declare function parseNodeHeaders(req: RequestWithHeaders): Record<string, string>;
7
18
  /**
8
19
  * Extract query parameters from a URL as a flat Record<string, string>.
9
20
  */
@@ -16,10 +27,11 @@ export declare function parseNodeQuery(url: URL): Record<string, string>;
16
27
  * @param headers - Parsed request headers
17
28
  * @param maxBodySize - Maximum body size in bytes (default: 10 MB)
18
29
  */
19
- export declare function collectBody(req: IncomingMessage, headers: Record<string, string>, maxBodySize?: number): Promise<unknown>;
30
+ export declare function collectBody(req: BodyReadable, headers: Record<string, string>, maxBodySize?: number): Promise<unknown>;
20
31
  /**
21
32
  * Write a Schmock Response to a Node.js ServerResponse.
22
33
  * Serializes non-string bodies as JSON and sets content-type when missing.
23
34
  */
24
35
  export declare function writeSchmockResponse(res: ServerResponse, response: Schmock.Response, extraHeaders?: Record<string, string>): void;
36
+ export {};
25
37
  //# sourceMappingURL=http-helpers.d.ts.map
@@ -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;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"}
1
+ {"version":3,"file":"http-helpers.d.ts","sourceRoot":"","sources":["../src/http-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGhD,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,OAAO,EAAE;QAChB,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;KAC1D,CAAC;CACH;AAED,UAAU,YAAY;IACpB,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IAC3D,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC3D,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC7C,OAAO,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,kBAAkB,GACtB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;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,YAAY,EACjB,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,CA2CN"}
@@ -1,3 +1,4 @@
1
+ import { isBinaryBody } from "./binary.js";
1
2
  /**
2
3
  * Convert Node.js IncomingMessage headers to a flat Record<string, string>.
3
4
  * Drops array-valued headers (keeps only string values).
@@ -75,16 +76,33 @@ export function writeSchmockResponse(res, response, extraHeaders) {
75
76
  ...response.headers,
76
77
  ...extraHeaders,
77
78
  };
78
- if (!responseHeaders["content-type"] &&
79
+ const hasContentType = Object.keys(responseHeaders).some((header) => header.toLowerCase() === "content-type");
80
+ if (!hasContentType &&
81
+ response.body !== undefined &&
82
+ isBinaryBody(response.body)) {
83
+ responseHeaders["content-type"] = "application/octet-stream";
84
+ }
85
+ else if (!hasContentType &&
79
86
  response.body !== undefined &&
80
87
  typeof response.body !== "string") {
81
88
  responseHeaders["content-type"] = "application/json";
82
89
  }
83
- const responseBody = response.body === undefined
84
- ? undefined
85
- : typeof response.body === "string"
86
- ? response.body
87
- : JSON.stringify(response.body);
90
+ let responseBody;
91
+ if (response.body === undefined) {
92
+ responseBody = undefined;
93
+ }
94
+ else if (typeof response.body === "string") {
95
+ responseBody = response.body;
96
+ }
97
+ else if (response.body instanceof ArrayBuffer) {
98
+ responseBody = new Uint8Array(response.body);
99
+ }
100
+ else if (ArrayBuffer.isView(response.body)) {
101
+ responseBody = new Uint8Array(response.body.buffer, response.body.byteOffset, response.body.byteLength);
102
+ }
103
+ else {
104
+ responseBody = JSON.stringify(response.body);
105
+ }
88
106
  res.writeHead(response.status, responseHeaders);
89
107
  res.end(responseBody);
90
108
  }
package/dist/index.d.ts CHANGED
@@ -28,7 +28,7 @@ namespace Schmock {
28
28
  * 'DELETE /api/posts/:postId/comments/:commentId'
29
29
  */
30
30
  type RouteKey = `${HttpMethod} ${string}`;
31
-
31
+
32
32
  /**
33
33
  * Plugin interface for extending Schmock functionality
34
34
  */
@@ -45,6 +45,15 @@ namespace Schmock {
45
45
  */
46
46
  install?(instance: CallableMockInstance): void;
47
47
 
48
+ /**
49
+ * Inspect or transform a request before its route generator executes.
50
+ * Returning a response short-circuits the generator, while returning only
51
+ * a context allows request changes to flow into the generator.
52
+ */
53
+ beforeRequest?(
54
+ context: PluginContext,
55
+ ): PluginResult | void | Promise<PluginResult | void>;
56
+
48
57
  /**
49
58
  * Process the request through this plugin
50
59
  * First plugin to set response becomes the generator, others transform
@@ -52,16 +61,22 @@ namespace Schmock {
52
61
  * @param response - Response from previous plugin (if any)
53
62
  * @returns Updated context and response
54
63
  */
55
- process(context: PluginContext, response?: unknown): PluginResult | Promise<PluginResult>;
64
+ process(
65
+ context: PluginContext,
66
+ response?: unknown,
67
+ ): PluginResult | Promise<PluginResult>;
56
68
 
57
69
  /**
58
- * Called when an error occurs
59
- * Can handle, transform, or suppress errors
70
+ * Called when this plugin or an earlier pipeline stage fails. If this hook
71
+ * does not recover, downstream handlers are tried in registration order.
60
72
  * @param error - The error that occurred
61
73
  * @param context - Plugin context
62
74
  * @returns Modified error, response data, or void to continue error propagation
63
75
  */
64
- onError?(error: Error, context: PluginContext): Error | ResponseResult | void | Promise<Error | ResponseResult | void>;
76
+ onError?(
77
+ error: Error,
78
+ context: PluginContext,
79
+ ): Error | ResponseResult | void | Promise<Error | ResponseResult | void>;
65
80
  }
66
81
 
67
82
  /**
@@ -99,6 +114,8 @@ namespace Schmock {
99
114
  body?: unknown;
100
115
  /** Shared state between plugins for this request */
101
116
  state: Map<string, unknown>;
117
+ /** True when a beforeRequest hook supplied the response instead of the route generator. */
118
+ requestShortCircuited?: boolean;
102
119
  /** Route-specific state */
103
120
  routeState?: Record<string, unknown>;
104
121
  }
@@ -148,20 +165,28 @@ namespace Schmock {
148
165
  /**
149
166
  * Generator types that can be passed to route definitions
150
167
  */
151
- type Generator =
152
- | GeneratorFunction
153
- | StaticData
154
- | JSONSchema7;
168
+ type Generator = GeneratorFunction | StaticData | JSONSchema7;
155
169
 
156
170
  /**
157
171
  * Function that generates responses
158
172
  */
159
- type GeneratorFunction = (context: RequestContext) => ResponseResult | Promise<ResponseResult>;
173
+ type GeneratorFunction = (
174
+ context: RequestContext,
175
+ ) => ResponseResult | Promise<ResponseResult>;
160
176
 
161
177
  /**
162
178
  * Static data (non-function) that gets returned as-is
163
179
  */
164
- type StaticData = string | number | boolean | null | undefined | Record<string, unknown> | unknown[];
180
+ type StaticData =
181
+ | string
182
+ | number
183
+ | boolean
184
+ | null
185
+ | undefined
186
+ | Record<string, unknown>
187
+ | unknown[]
188
+ | ArrayBuffer
189
+ | ArrayBufferView;
165
190
 
166
191
  /**
167
192
  * Context passed to generator functions
@@ -240,12 +265,12 @@ namespace Schmock {
240
265
  interface CallableMockInstance {
241
266
  /**
242
267
  * Define a route by calling the instance directly
243
- *
268
+ *
244
269
  * @param route - Route pattern in format 'METHOD /path'
245
270
  * @param generator - Response generator (function, static data, or schema)
246
271
  * @param config - Route-specific configuration
247
272
  * @returns The same instance for method chaining
248
- *
273
+ *
249
274
  * @example
250
275
  * ```typescript
251
276
  * const mock = schmock()
@@ -253,19 +278,22 @@ namespace Schmock {
253
278
  * mock('POST /users', userData, { contentType: 'application/json' })
254
279
  * ```
255
280
  */
256
- (route: RouteKey, generator: Generator, config?: RouteConfig): CallableMockInstance;
281
+ (
282
+ route: RouteKey,
283
+ generator: Generator,
284
+ config?: RouteConfig,
285
+ ): CallableMockInstance;
257
286
 
258
287
  /**
259
288
  * Add a plugin to the pipeline
260
- *
289
+ *
261
290
  * @param plugin - Plugin to add to the pipeline
262
291
  * @returns The same instance for method chaining
263
- *
292
+ *
264
293
  * @example
265
294
  * ```typescript
295
+ * mock.pipe(authPlugin()).pipe(corsPlugin())
266
296
  * mock('GET /users', generator, config)
267
- * .pipe(authPlugin())
268
- * .pipe(corsPlugin())
269
297
  * ```
270
298
  */
271
299
  pipe(plugin: Plugin): CallableMockInstance;
@@ -285,7 +313,11 @@ namespace Schmock {
285
313
  * })
286
314
  * ```
287
315
  */
288
- handle(method: HttpMethod, path: string, options?: RequestOptions): Promise<Response>;
316
+ handle(
317
+ method: HttpMethod,
318
+ path: string,
319
+ options?: RequestOptions,
320
+ ): Promise<Response>;
289
321
 
290
322
  // ===== Request Spy / History API =====
291
323
 
@@ -347,12 +379,18 @@ namespace Schmock {
347
379
  /**
348
380
  * Register an event listener
349
381
  */
350
- on<E extends SchmockEvent>(event: E, listener: (data: SchmockEventMap[E]) => void): CallableMockInstance;
382
+ on<E extends SchmockEvent>(
383
+ event: E,
384
+ listener: (data: SchmockEventMap[E]) => void,
385
+ ): CallableMockInstance;
351
386
 
352
387
  /**
353
388
  * Remove an event listener
354
389
  */
355
- off<E extends SchmockEvent>(event: E, listener: (data: SchmockEventMap[E]) => void): CallableMockInstance;
390
+ off<E extends SchmockEvent>(
391
+ event: E,
392
+ listener: (data: SchmockEventMap[E]) => void,
393
+ ): CallableMockInstance;
356
394
 
357
395
  // ===== Introspection =====
358
396
 
@@ -555,6 +593,8 @@ namespace Schmock {
555
593
  interface CrudOperationMeta {
556
594
  /** Full success response schema (wrapper + items) */
557
595
  responseSchema?: JSONSchema7;
596
+ /** Concrete success status selected from the operation contract. */
597
+ responseStatus?: number;
558
598
  /** Response headers from spec */
559
599
  responseHeaders?: Record<string, ResponseHeaderDef>;
560
600
  /** Error response schemas keyed by status code */
@@ -627,14 +667,24 @@ namespace Schmock {
627
667
  passthrough?: boolean;
628
668
  errorFormatter?: (error: Error, request: unknown) => unknown;
629
669
  transformRequest?: (request: unknown) => AdapterRequestOverride;
630
- transformResponse?: (
631
- response: Response,
632
- request: unknown,
633
- ) => Response;
670
+ transformResponse?: (response: Response, request: unknown) => Response;
634
671
  }
635
672
 
636
673
  // ===== OpenAPI Plugin Options =====
637
674
 
675
+ /** A callback request resolved from an OpenAPI callback expression. */
676
+ interface OpenApiCallbackRequest {
677
+ url: string;
678
+ method: HttpMethod;
679
+ headers: Record<string, string>;
680
+ body?: unknown;
681
+ }
682
+
683
+ /** Explicit application-owned delivery for OpenAPI callbacks. */
684
+ interface OpenApiCallbackOptions {
685
+ dispatch(request: OpenApiCallbackRequest): void | Promise<void>;
686
+ }
687
+
638
688
  /**
639
689
  * Options for the OpenAPI plugin
640
690
  */
@@ -643,6 +693,7 @@ namespace Schmock {
643
693
  seed?: SeedConfig;
644
694
  validateRequests?: boolean;
645
695
  validateResponses?: boolean;
696
+ /** @deprecated Unsupported. Supplying this option throws OPENAPI_UNSUPPORTED_OPTION. */
646
697
  queryFeatures?: {
647
698
  pagination?: boolean;
648
699
  sorting?: boolean;
@@ -652,12 +703,24 @@ namespace Schmock {
652
703
  debug?: boolean;
653
704
  fakerSeed?: number;
654
705
  security?: boolean;
706
+ /**
707
+ * Enable callback delivery through an application-supplied dispatcher.
708
+ * Callbacks are disabled when this option is omitted; Schmock never
709
+ * performs callback network requests implicitly.
710
+ */
711
+ callbacks?: OpenApiCallbackOptions;
655
712
  /** Replace response schemas for specific routes. Key format: "METHOD /path" or "METHOD /path STATUS" */
656
713
  schemas?: Record<string, import("json-schema").JSONSchema7>;
657
714
  /** Called before generating a response body. Return a schema to replace the original, or void to keep it. */
658
715
  onSchema?: (
659
716
  schema: import("json-schema").JSONSchema7,
660
- context: { method: string; path: string; params: Record<string, string>; query: Record<string, string>; headers: Record<string, string> },
717
+ context: {
718
+ method: string;
719
+ path: string;
720
+ params: Record<string, string>;
721
+ query: Record<string, string>;
722
+ headers: Record<string, string>;
723
+ },
661
724
  ) => import("json-schema").JSONSchema7 | undefined;
662
725
  }
663
726
 
@@ -698,7 +761,6 @@ namespace Schmock {
698
761
  hostname: string;
699
762
  close(): void;
700
763
  }
701
-
702
764
  }
703
765
  }
704
766
  // >>> schmock ambient namespace <<<
@@ -726,6 +788,7 @@ namespace Schmock {
726
788
  * @returns A callable mock instance
727
789
  */
728
790
  export declare function schmock(config?: Schmock.GlobalConfig): Schmock.CallableMockInstance;
791
+ export { isBinaryBody } from "./binary.js";
729
792
  export { HTTP_METHODS, isHttpMethod, isRouteNotFound, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, toRouteKey, } from "./constants.js";
730
793
  export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors.js";
731
794
  export { badRequest, created, forbidden, noContent, notFound, paginate, serverError, unauthorized, } from "./helpers.js";
@@ -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,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"}
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;AAED,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,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
@@ -59,6 +59,7 @@ export function schmock(config) {
59
59
  instance.setCallableRef(callableInstance);
60
60
  return callableInstance;
61
61
  }
62
+ export { isBinaryBody } from "./binary.js";
62
63
  // Re-export constants and utilities
63
64
  export { HTTP_METHODS, isHttpMethod, isRouteNotFound, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, toRouteKey, } from "./constants.js";
64
65
  // Re-export errors
@@ -1 +1 @@
1
- {"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../src/interceptor.ts"],"names":[],"mappings":"AA2JA;;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,CA0JzB"}
1
+ {"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../src/interceptor.ts"],"names":[],"mappings":"AAwPA;;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,CAoJzB"}