@schmock/core 1.2.1 → 1.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.
package/dist/builder.d.ts CHANGED
@@ -11,6 +11,8 @@ export declare class CallableMockInstance {
11
11
  private logger;
12
12
  private requestHistory;
13
13
  private callableRef;
14
+ private server;
15
+ private serverInfo;
14
16
  constructor(globalConfig?: Schmock.GlobalConfig);
15
17
  defineRoute(route: Schmock.RouteKey, generator: Schmock.Generator, config: Schmock.RouteConfig): this;
16
18
  setCallableRef(ref: Schmock.CallableMockInstance): void;
@@ -22,6 +24,8 @@ export declare class CallableMockInstance {
22
24
  reset(): void;
23
25
  resetHistory(): void;
24
26
  resetState(): void;
27
+ listen(port?: number, hostname?: string): Promise<Schmock.ServerInfo>;
28
+ close(): void;
25
29
  handle(method: Schmock.HttpMethod, path: string, options?: Schmock.RequestOptions): Promise<Schmock.Response>;
26
30
  /**
27
31
  * Apply configured response delay
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AA0EA;;;;GAIG;AACH,qBAAa,oBAAoB;IAQnB,OAAO,CAAC,YAAY;IAPhC,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;gBAE1C,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;IAqFP,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,KAAK,IAAI,IAAI;IAab,YAAY,IAAI,IAAI;IAKpB,UAAU,IAAI,IAAI;IASZ,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;IAoL5B;;;;OAIG;YACW,UAAU;IAcxB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IAwDrB;;;;;;;;;;OAUG;YACW,iBAAiB;IAqG/B;;;;;;;;OAQG;IACH,OAAO,CAAC,SAAS;IA0BjB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;CActB"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AA4EA;;;;GAIG;AACH,qBAAa,oBAAoB;IAUnB,OAAO,CAAC,YAAY;IAThC,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;gBAE/B,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;IAqFP,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,KAAK,IAAI,IAAI;IAcb,YAAY,IAAI,IAAI;IAKpB,UAAU,IAAI,IAAI;IAWlB,MAAM,CAAC,IAAI,SAAI,EAAE,QAAQ,SAAc,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC;IAmFrE,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;IAoL5B;;;;OAIG;YACW,UAAU;IAcxB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IAwDrB;;;;;;;;;;OAUG;YACW,iBAAiB;IAqG/B;;;;;;;;OAQG;IACH,OAAO,CAAC,SAAS;IA0BjB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;CActB"}
package/dist/builder.js CHANGED
@@ -1,4 +1,5 @@
1
- import { isStatusTuple } from "./constants.js";
1
+ import { createServer } from "node:http";
2
+ import { isStatusTuple, toHttpMethod } from "./constants.js";
2
3
  import { PluginError, RouteDefinitionError, RouteNotFoundError, SchmockError, } from "./errors.js";
3
4
  import { parseRouteKey } from "./parser.js";
4
5
  function errorMessage(error) {
@@ -57,6 +58,8 @@ export class CallableMockInstance {
57
58
  logger;
58
59
  requestHistory = [];
59
60
  callableRef;
61
+ server;
62
+ serverInfo;
60
63
  constructor(globalConfig = {}) {
61
64
  this.globalConfig = globalConfig;
62
65
  this.logger = new DebugLogger(globalConfig.debug || false);
@@ -181,6 +184,7 @@ export class CallableMockInstance {
181
184
  }
182
185
  // ===== Reset / Lifecycle =====
183
186
  reset() {
187
+ this.close();
184
188
  this.routes = [];
185
189
  this.staticRoutes.clear();
186
190
  this.plugins = [];
@@ -204,6 +208,82 @@ export class CallableMockInstance {
204
208
  }
205
209
  this.logger.log("lifecycle", "State cleared");
206
210
  }
211
+ // ===== Standalone Server =====
212
+ listen(port = 0, hostname = "127.0.0.1") {
213
+ if (this.server) {
214
+ throw new SchmockError("Server is already running", "SERVER_ALREADY_RUNNING");
215
+ }
216
+ const httpServer = createServer((req, res) => {
217
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
218
+ const method = toHttpMethod(req.method ?? "GET");
219
+ const path = url.pathname;
220
+ const headers = {};
221
+ for (const [key, value] of Object.entries(req.headers)) {
222
+ if (typeof value === "string") {
223
+ headers[key] = value;
224
+ }
225
+ }
226
+ const query = {};
227
+ url.searchParams.forEach((value, key) => {
228
+ query[key] = value;
229
+ });
230
+ const chunks = [];
231
+ req.on("data", (chunk) => chunks.push(chunk));
232
+ req.on("end", () => {
233
+ const raw = Buffer.concat(chunks).toString();
234
+ let body;
235
+ const contentType = headers["content-type"] ?? "";
236
+ if (raw && contentType.includes("json")) {
237
+ try {
238
+ body = JSON.parse(raw);
239
+ }
240
+ catch {
241
+ body = raw;
242
+ }
243
+ }
244
+ else if (raw) {
245
+ body = raw;
246
+ }
247
+ void this.handle(method, path, { headers, body, query }).then((schmockResponse) => {
248
+ const responseHeaders = {
249
+ ...schmockResponse.headers,
250
+ };
251
+ if (!responseHeaders["content-type"] &&
252
+ schmockResponse.body !== undefined &&
253
+ typeof schmockResponse.body !== "string") {
254
+ responseHeaders["content-type"] = "application/json";
255
+ }
256
+ const responseBody = schmockResponse.body === undefined
257
+ ? undefined
258
+ : typeof schmockResponse.body === "string"
259
+ ? schmockResponse.body
260
+ : JSON.stringify(schmockResponse.body);
261
+ res.writeHead(schmockResponse.status, responseHeaders);
262
+ res.end(responseBody);
263
+ });
264
+ });
265
+ });
266
+ this.server = httpServer;
267
+ return new Promise((resolve, reject) => {
268
+ httpServer.on("error", reject);
269
+ httpServer.listen(port, hostname, () => {
270
+ const addr = httpServer.address();
271
+ const actualPort = addr !== null && typeof addr === "object" ? addr.port : port;
272
+ this.serverInfo = { port: actualPort, hostname };
273
+ this.logger.log("server", `Listening on ${hostname}:${actualPort}`);
274
+ resolve(this.serverInfo);
275
+ });
276
+ });
277
+ }
278
+ close() {
279
+ if (!this.server) {
280
+ return;
281
+ }
282
+ this.server.close();
283
+ this.server = undefined;
284
+ this.serverInfo = undefined;
285
+ this.logger.log("server", "Server stopped");
286
+ }
207
287
  async handle(method, path, options) {
208
288
  const requestId = crypto.randomUUID();
209
289
  this.logger.log("request", `[${requestId}] ${method} ${path}`, {
package/dist/index.d.ts CHANGED
@@ -24,5 +24,5 @@
24
24
  export declare function schmock(config?: Schmock.GlobalConfig): Schmock.CallableMockInstance;
25
25
  export { HTTP_METHODS, isHttpMethod, isStatusTuple, ROUTE_NOT_FOUND_CODE, toHttpMethod, } from "./constants.js";
26
26
  export { PluginError, ResourceLimitError, ResponseGenerationError, RouteDefinitionError, RouteNotFoundError, RouteParseError, SchemaGenerationError, SchemaValidationError, SchmockError, } from "./errors.js";
27
- export type { CallableMockInstance, Generator, GeneratorFunction, GlobalConfig, HttpMethod, Plugin, PluginContext, PluginResult, RequestContext, RequestOptions, RequestRecord, Response, ResponseBody, ResponseResult, RouteConfig, RouteKey, StaticData, } from "./types.js";
27
+ export type { CallableMockInstance, Generator, GeneratorFunction, GlobalConfig, HttpMethod, Plugin, PluginContext, PluginResult, RequestContext, RequestOptions, RequestRecord, Response, ResponseBody, ResponseResult, RouteConfig, RouteKey, ServerInfo, StaticData, } from "./types.js";
28
28
  //# 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,CAiC9B;AAGD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,YAAY,GACb,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,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,QAAQ,EACR,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,CAmC9B;AAGD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,YAAY,GACb,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,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,QAAQ,EACR,UAAU,EACV,UAAU,GACX,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -42,6 +42,8 @@ export function schmock(config) {
42
42
  reset: instance.reset.bind(instance),
43
43
  resetHistory: instance.resetHistory.bind(instance),
44
44
  resetState: instance.resetState.bind(instance),
45
+ listen: instance.listen.bind(instance),
46
+ close: instance.close.bind(instance),
45
47
  });
46
48
  instance.setCallableRef(callableInstance);
47
49
  return callableInstance;
package/dist/types.d.ts CHANGED
@@ -15,4 +15,5 @@ export type PluginContext = Schmock.PluginContext;
15
15
  export type PluginResult = Schmock.PluginResult;
16
16
  export type StaticData = Schmock.StaticData;
17
17
  export type RequestRecord = Schmock.RequestRecord;
18
+ export type ServerInfo = Schmock.ServerInfo;
18
19
  //# 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"}
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"}
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.2.1",
4
+ "version": "1.3.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -18,7 +18,8 @@
18
18
  "./package.json": "./package.json"
19
19
  },
20
20
  "scripts": {
21
- "build": "rm -rf dist tsconfig.tsbuildinfo && tsc --build",
21
+ "build": "bun build:lib && bun build:types",
22
+ "build:lib": "bun build --minify --target node --outdir=dist src/index.ts",
22
23
  "build:types": "rm -f tsconfig.tsbuildinfo && tsc --build",
23
24
  "pretest": "rm -f src/*.js src/*.d.ts || true",
24
25
  "test": "vitest",
package/src/builder.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { isStatusTuple } from "./constants.js";
1
+ import type { Server } from "node:http";
2
+ import { createServer } from "node:http";
3
+ import { isStatusTuple, toHttpMethod } from "./constants.js";
2
4
  import {
3
5
  PluginError,
4
6
  RouteDefinitionError,
@@ -84,6 +86,8 @@ export class CallableMockInstance {
84
86
  private logger: DebugLogger;
85
87
  private requestHistory: Schmock.RequestRecord[] = [];
86
88
  private callableRef: Schmock.CallableMockInstance | undefined;
89
+ private server: Server | undefined;
90
+ private serverInfo: Schmock.ServerInfo | undefined;
87
91
 
88
92
  constructor(private globalConfig: Schmock.GlobalConfig = {}) {
89
93
  this.logger = new DebugLogger(globalConfig.debug || false);
@@ -254,6 +258,7 @@ export class CallableMockInstance {
254
258
  // ===== Reset / Lifecycle =====
255
259
 
256
260
  reset(): void {
261
+ this.close();
257
262
  this.routes = [];
258
263
  this.staticRoutes.clear();
259
264
  this.plugins = [];
@@ -280,6 +285,101 @@ export class CallableMockInstance {
280
285
  this.logger.log("lifecycle", "State cleared");
281
286
  }
282
287
 
288
+ // ===== Standalone Server =====
289
+
290
+ listen(port = 0, hostname = "127.0.0.1"): Promise<Schmock.ServerInfo> {
291
+ if (this.server) {
292
+ throw new SchmockError(
293
+ "Server is already running",
294
+ "SERVER_ALREADY_RUNNING",
295
+ );
296
+ }
297
+
298
+ const httpServer = createServer((req, res) => {
299
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
300
+ const method = toHttpMethod(req.method ?? "GET");
301
+ const path = url.pathname;
302
+
303
+ const headers: Record<string, string> = {};
304
+ for (const [key, value] of Object.entries(req.headers)) {
305
+ if (typeof value === "string") {
306
+ headers[key] = value;
307
+ }
308
+ }
309
+
310
+ const query: Record<string, string> = {};
311
+ url.searchParams.forEach((value, key) => {
312
+ query[key] = value;
313
+ });
314
+
315
+ const chunks: Buffer[] = [];
316
+ req.on("data", (chunk: Buffer) => chunks.push(chunk));
317
+ req.on("end", () => {
318
+ const raw = Buffer.concat(chunks).toString();
319
+ let body: unknown;
320
+ const contentType = headers["content-type"] ?? "";
321
+ if (raw && contentType.includes("json")) {
322
+ try {
323
+ body = JSON.parse(raw);
324
+ } catch {
325
+ body = raw;
326
+ }
327
+ } else if (raw) {
328
+ body = raw;
329
+ }
330
+
331
+ void this.handle(method, path, { headers, body, query }).then(
332
+ (schmockResponse) => {
333
+ const responseHeaders: Record<string, string> = {
334
+ ...schmockResponse.headers,
335
+ };
336
+ if (
337
+ !responseHeaders["content-type"] &&
338
+ schmockResponse.body !== undefined &&
339
+ typeof schmockResponse.body !== "string"
340
+ ) {
341
+ responseHeaders["content-type"] = "application/json";
342
+ }
343
+
344
+ const responseBody =
345
+ schmockResponse.body === undefined
346
+ ? undefined
347
+ : typeof schmockResponse.body === "string"
348
+ ? schmockResponse.body
349
+ : JSON.stringify(schmockResponse.body);
350
+
351
+ res.writeHead(schmockResponse.status, responseHeaders);
352
+ res.end(responseBody);
353
+ },
354
+ );
355
+ });
356
+ });
357
+
358
+ this.server = httpServer;
359
+
360
+ return new Promise((resolve, reject) => {
361
+ httpServer.on("error", reject);
362
+ httpServer.listen(port, hostname, () => {
363
+ const addr = httpServer.address();
364
+ const actualPort =
365
+ addr !== null && typeof addr === "object" ? addr.port : port;
366
+ this.serverInfo = { port: actualPort, hostname };
367
+ this.logger.log("server", `Listening on ${hostname}:${actualPort}`);
368
+ resolve(this.serverInfo);
369
+ });
370
+ });
371
+ }
372
+
373
+ close(): void {
374
+ if (!this.server) {
375
+ return;
376
+ }
377
+ this.server.close();
378
+ this.server = undefined;
379
+ this.serverInfo = undefined;
380
+ this.logger.log("server", "Server stopped");
381
+ }
382
+
283
383
  async handle(
284
384
  method: Schmock.HttpMethod,
285
385
  path: string,
package/src/index.ts CHANGED
@@ -52,6 +52,8 @@ export function schmock(
52
52
  reset: instance.reset.bind(instance),
53
53
  resetHistory: instance.resetHistory.bind(instance),
54
54
  resetState: instance.resetState.bind(instance),
55
+ listen: instance.listen.bind(instance),
56
+ close: instance.close.bind(instance),
55
57
  },
56
58
  );
57
59
 
@@ -98,5 +100,6 @@ export type {
98
100
  ResponseResult,
99
101
  RouteConfig,
100
102
  RouteKey,
103
+ ServerInfo,
101
104
  StaticData,
102
105
  } from "./types.js";
@@ -0,0 +1,116 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { schmock } from "./index";
3
+
4
+ describe("Standalone Server", () => {
5
+ let mock: Schmock.CallableMockInstance;
6
+
7
+ afterEach(() => {
8
+ mock?.close();
9
+ });
10
+
11
+ it("returns actual port when port=0", async () => {
12
+ mock = schmock();
13
+ mock("GET /test", { ok: true });
14
+ const info = await mock.listen(0);
15
+ expect(info.port).toBeGreaterThan(0);
16
+ expect(info.hostname).toBe("127.0.0.1");
17
+ });
18
+
19
+ it("propagates request headers", async () => {
20
+ mock = schmock();
21
+ mock("GET /headers", ({ headers }) => ({
22
+ auth: headers.authorization,
23
+ }));
24
+ const info = await mock.listen(0);
25
+
26
+ const res = await fetch(`http://127.0.0.1:${info.port}/headers`, {
27
+ headers: { authorization: "Bearer secret" },
28
+ });
29
+ const body = await res.json();
30
+ expect(body.auth).toBe("Bearer secret");
31
+ });
32
+
33
+ it("handles concurrent requests", async () => {
34
+ mock = schmock();
35
+ mock("GET /slow", () => ({ value: "done" }));
36
+ const info = await mock.listen(0);
37
+
38
+ const results = await Promise.all(
39
+ Array.from({ length: 10 }, () =>
40
+ fetch(`http://127.0.0.1:${info.port}/slow`).then((r) => r.json()),
41
+ ),
42
+ );
43
+
44
+ for (const body of results) {
45
+ expect(body.value).toBe("done");
46
+ }
47
+ });
48
+
49
+ it("parses text body when content-type is not JSON", async () => {
50
+ mock = schmock();
51
+ mock("POST /text", ({ body: reqBody }) => ({ received: reqBody }));
52
+ const info = await mock.listen(0);
53
+
54
+ const res = await fetch(`http://127.0.0.1:${info.port}/text`, {
55
+ method: "POST",
56
+ headers: { "content-type": "text/plain" },
57
+ body: "hello world",
58
+ });
59
+ const resBody = await res.json();
60
+ expect(resBody.received).toBe("hello world");
61
+ });
62
+
63
+ it("returns 204 with no body for undefined response", async () => {
64
+ mock = schmock();
65
+ mock("DELETE /item", () => undefined);
66
+ const info = await mock.listen(0);
67
+
68
+ const res = await fetch(`http://127.0.0.1:${info.port}/item`, {
69
+ method: "DELETE",
70
+ });
71
+ expect(res.status).toBe(204);
72
+ });
73
+
74
+ it("reset stops the server", async () => {
75
+ mock = schmock();
76
+ mock("GET /test", { ok: true });
77
+ const info = await mock.listen(0);
78
+
79
+ const res = await fetch(`http://127.0.0.1:${info.port}/test`);
80
+ expect(res.status).toBe(200);
81
+
82
+ mock.reset();
83
+
84
+ try {
85
+ await fetch(`http://127.0.0.1:${info.port}/test`);
86
+ expect.unreachable("Should have thrown");
87
+ } catch {
88
+ // Expected: connection refused
89
+ }
90
+ });
91
+
92
+ it("handles route params in server mode", async () => {
93
+ mock = schmock();
94
+ mock("GET /users/:id", ({ params }) => ({ id: params.id }));
95
+ const info = await mock.listen(0);
96
+
97
+ const res = await fetch(`http://127.0.0.1:${info.port}/users/42`);
98
+ const body = await res.json();
99
+ expect(body.id).toBe("42");
100
+ });
101
+
102
+ it("double listen throws", async () => {
103
+ mock = schmock();
104
+ mock("GET /test", { ok: true });
105
+ await mock.listen(0);
106
+ expect(() => mock.listen(0)).toThrow("Server is already running");
107
+ });
108
+
109
+ it("close is idempotent", async () => {
110
+ mock = schmock();
111
+ mock("GET /test", { ok: true });
112
+ await mock.listen(0);
113
+ mock.close();
114
+ expect(() => mock.close()).not.toThrow();
115
+ });
116
+ });
@@ -0,0 +1,233 @@
1
+ import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
2
+ import { expect } from "vitest";
3
+ import { schmock } from "../index";
4
+
5
+ const feature = await loadFeature("../../features/standalone-server.feature");
6
+
7
+ describeFeature(feature, ({ Scenario }) => {
8
+ let mock: Schmock.CallableMockInstance;
9
+ let serverInfo: Schmock.ServerInfo;
10
+ let httpResponse: Response;
11
+
12
+ function baseUrl(): string {
13
+ return `http://${serverInfo.hostname}:${serverInfo.port}`;
14
+ }
15
+
16
+ Scenario("Start and stop a simple server", ({ Given, When, Then, And }) => {
17
+ Given("I create a mock with a GET /hello route", () => {
18
+ mock = schmock();
19
+ mock("GET /hello", { message: "hello" });
20
+ });
21
+
22
+ When("I start the server on a random port", async () => {
23
+ serverInfo = await mock.listen(0);
24
+ });
25
+
26
+ Then("the server should be running", () => {
27
+ expect(serverInfo.port).toBeGreaterThan(0);
28
+ });
29
+
30
+ When("I fetch {string} from the server", async (_, route: string) => {
31
+ const [, path] = route.split(" ");
32
+ httpResponse = await fetch(`${baseUrl()}${path}`);
33
+ });
34
+
35
+ Then("the HTTP response status should be {int}", (_, status: number) => {
36
+ expect(httpResponse.status).toBe(status);
37
+ });
38
+
39
+ And(
40
+ "the HTTP response body should have {string} equal to {string}",
41
+ async (_, key: string, value: string) => {
42
+ const body = await httpResponse.json();
43
+ expect(body[key]).toBe(value);
44
+ },
45
+ );
46
+
47
+ When("I stop the server", () => {
48
+ mock.close();
49
+ });
50
+
51
+ Then("the server should not be running", async () => {
52
+ try {
53
+ await fetch(`${baseUrl()}/hello`);
54
+ expect.unreachable("Should not be able to connect");
55
+ } catch {
56
+ // Connection refused — server is down
57
+ }
58
+ });
59
+ });
60
+
61
+ Scenario("Handle POST with JSON body", ({ Given, When, Then, And }) => {
62
+ Given("I create a mock echoing POST at {string}", (_, path: string) => {
63
+ mock = schmock();
64
+ mock(`POST ${path}`, ({ body }) => body);
65
+ });
66
+
67
+ When("I start the server on a random port", async () => {
68
+ serverInfo = await mock.listen(0);
69
+ });
70
+
71
+ And(
72
+ "I fetch {string} with JSON body:",
73
+ async (_, route: string, docString: string) => {
74
+ const [method, path] = route.split(" ");
75
+ httpResponse = await fetch(`${baseUrl()}${path}`, {
76
+ method,
77
+ headers: { "content-type": "application/json" },
78
+ body: docString,
79
+ });
80
+ },
81
+ );
82
+
83
+ Then(
84
+ "the response status from POST should be {int}",
85
+ (_, status: number) => {
86
+ expect(httpResponse.status).toBe(status);
87
+ },
88
+ );
89
+
90
+ And(
91
+ "the response body from POST should have {string} equal to {string}",
92
+ async (_, key: string, value: string) => {
93
+ const body = await httpResponse.json();
94
+ expect(body[key]).toBe(value);
95
+ },
96
+ );
97
+
98
+ When("I stop the server", () => {
99
+ mock.close();
100
+ });
101
+ });
102
+
103
+ Scenario(
104
+ "Return 404 for unregistered routes",
105
+ ({ Given, When, And, Then }) => {
106
+ Given("I create a mock with a GET /hello route", () => {
107
+ mock = schmock();
108
+ mock("GET /hello", { message: "hello" });
109
+ });
110
+
111
+ When("I start the server on a random port", async () => {
112
+ serverInfo = await mock.listen(0);
113
+ });
114
+
115
+ And("I fetch {string} from the server", async (_, route: string) => {
116
+ const [, path] = route.split(" ");
117
+ httpResponse = await fetch(`${baseUrl()}${path}`);
118
+ });
119
+
120
+ Then("the HTTP response status should be {int}", (_, status: number) => {
121
+ expect(httpResponse.status).toBe(status);
122
+ });
123
+
124
+ When("I stop the server", () => {
125
+ mock.close();
126
+ });
127
+ },
128
+ );
129
+
130
+ Scenario("Query parameters are forwarded", ({ Given, When, And, Then }) => {
131
+ let parsedBody: Record<string, string>;
132
+
133
+ Given(
134
+ "I create a mock reflecting query params at {string}",
135
+ (_, route: string) => {
136
+ const [method, path] = route.split(" ");
137
+ mock = schmock();
138
+ mock(`${method} ${path}` as Schmock.RouteKey, ({ query }) => query);
139
+ },
140
+ );
141
+
142
+ When("I start the server on a random port", async () => {
143
+ serverInfo = await mock.listen(0);
144
+ });
145
+
146
+ And("I fetch {string} from the server", async (_, route: string) => {
147
+ const [, pathWithQuery] = route.split(" ");
148
+ httpResponse = await fetch(`${baseUrl()}${pathWithQuery}`);
149
+ parsedBody = await httpResponse.json();
150
+ });
151
+
152
+ Then("the HTTP response status should be {int}", (_, status: number) => {
153
+ expect(httpResponse.status).toBe(status);
154
+ });
155
+
156
+ And(
157
+ "the HTTP response body should have {string} equal to {string}",
158
+ (_, key: string, value: string) => {
159
+ expect(parsedBody[key]).toBe(value);
160
+ },
161
+ );
162
+
163
+ And(
164
+ "the query param {string} should equal {string}",
165
+ (_, key: string, value: string) => {
166
+ expect(parsedBody[key]).toBe(value);
167
+ },
168
+ );
169
+
170
+ When("I stop the server", () => {
171
+ mock.close();
172
+ });
173
+ });
174
+
175
+ Scenario("Double listen throws an error", ({ Given, When, Then }) => {
176
+ Given("I create a mock with a GET /hello route", () => {
177
+ mock = schmock();
178
+ mock("GET /hello", { message: "hello" });
179
+ });
180
+
181
+ When("I start the server on a random port", async () => {
182
+ serverInfo = await mock.listen(0);
183
+ });
184
+
185
+ Then("starting the server again should throw", () => {
186
+ expect(() => mock.listen(0)).toThrow("Server is already running");
187
+ mock.close();
188
+ });
189
+ });
190
+
191
+ Scenario("Close is idempotent", ({ Given, When, And, Then }) => {
192
+ Given("I create a mock with a GET /hello route", () => {
193
+ mock = schmock();
194
+ mock("GET /hello", { message: "hello" });
195
+ });
196
+
197
+ When("I start the server on a random port", async () => {
198
+ serverInfo = await mock.listen(0);
199
+ });
200
+
201
+ And("I stop the server", () => {
202
+ mock.close();
203
+ });
204
+
205
+ Then("stopping the server again should not throw", () => {
206
+ expect(() => mock.close()).not.toThrow();
207
+ });
208
+ });
209
+
210
+ Scenario("Reset stops the server", ({ Given, When, And, Then }) => {
211
+ Given("I create a mock with a GET /hello route", () => {
212
+ mock = schmock();
213
+ mock("GET /hello", { message: "hello" });
214
+ });
215
+
216
+ When("I start the server on a random port", async () => {
217
+ serverInfo = await mock.listen(0);
218
+ });
219
+
220
+ And("I reset the mock", () => {
221
+ mock.reset();
222
+ });
223
+
224
+ Then("the server should not be running", async () => {
225
+ try {
226
+ await fetch(`${baseUrl()}/hello`);
227
+ expect.unreachable("Should not be able to connect");
228
+ } catch {
229
+ // Connection refused — server is down
230
+ }
231
+ });
232
+ });
233
+ });
package/src/types.ts CHANGED
@@ -18,3 +18,4 @@ export type PluginContext = Schmock.PluginContext;
18
18
  export type PluginResult = Schmock.PluginResult;
19
19
  export type StaticData = Schmock.StaticData;
20
20
  export type RequestRecord = Schmock.RequestRecord;
21
+ export type ServerInfo = Schmock.ServerInfo;