@schmock/core 1.2.2 → 1.4.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 +10 -0
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +152 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/package.json +2 -2
- package/src/builder.ts +184 -12
- package/src/index.ts +14 -0
- package/src/server.test.ts +116 -0
- package/src/steps/lifecycle-events.steps.ts +142 -0
- package/src/steps/response-delay.steps.ts +85 -0
- package/src/steps/standalone-server.steps.ts +233 -0
- package/src/types.ts +4 -2
- package/src/builder.d.ts.map +0 -1
- package/src/errors.d.ts.map +0 -1
- package/src/index.d.ts.map +0 -1
- package/src/parser.d.ts.map +0 -1
- package/src/types.d.ts.map +0 -1
package/dist/builder.d.ts
CHANGED
|
@@ -11,6 +11,9 @@ export declare class CallableMockInstance {
|
|
|
11
11
|
private logger;
|
|
12
12
|
private requestHistory;
|
|
13
13
|
private callableRef;
|
|
14
|
+
private server;
|
|
15
|
+
private serverInfo;
|
|
16
|
+
private listeners;
|
|
14
17
|
constructor(globalConfig?: Schmock.GlobalConfig);
|
|
15
18
|
defineRoute(route: Schmock.RouteKey, generator: Schmock.Generator, config: Schmock.RouteConfig): this;
|
|
16
19
|
setCallableRef(ref: Schmock.CallableMockInstance): void;
|
|
@@ -19,9 +22,16 @@ export declare class CallableMockInstance {
|
|
|
19
22
|
called(method?: Schmock.HttpMethod, path?: string): boolean;
|
|
20
23
|
callCount(method?: Schmock.HttpMethod, path?: string): number;
|
|
21
24
|
lastRequest(method?: Schmock.HttpMethod, path?: string): Schmock.RequestRecord | undefined;
|
|
25
|
+
getRoutes(): Schmock.RouteInfo[];
|
|
26
|
+
getState(): Record<string, unknown>;
|
|
27
|
+
on(event: string, listener: (data: unknown) => void): this;
|
|
28
|
+
off(event: string, listener: (data: unknown) => void): this;
|
|
29
|
+
private emit;
|
|
22
30
|
reset(): void;
|
|
23
31
|
resetHistory(): void;
|
|
24
32
|
resetState(): void;
|
|
33
|
+
listen(port?: number, hostname?: string): Promise<Schmock.ServerInfo>;
|
|
34
|
+
close(): void;
|
|
25
35
|
handle(method: Schmock.HttpMethod, path: string, options?: Schmock.RequestOptions): Promise<Schmock.Response>;
|
|
26
36
|
/**
|
|
27
37
|
* Apply configured response delay
|
package/dist/builder.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AA4EA;;;;GAIG;AACH,qBAAa,oBAAoB;IAWnB,OAAO,CAAC,YAAY;IAVhC,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,SAAS,CAAmD;gBAEhD,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,SAAS,IAAI,OAAO,CAAC,SAAS,EAAE;IAQhC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAMnC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAU1D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAK3D,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;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;IAgN5B;;;;OAIG;YACW,UAAU;IAgBxB;;;;;;;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 {
|
|
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,9 @@ export class CallableMockInstance {
|
|
|
57
58
|
logger;
|
|
58
59
|
requestHistory = [];
|
|
59
60
|
callableRef;
|
|
61
|
+
server;
|
|
62
|
+
serverInfo;
|
|
63
|
+
listeners = new Map();
|
|
60
64
|
constructor(globalConfig = {}) {
|
|
61
65
|
this.globalConfig = globalConfig;
|
|
62
66
|
this.logger = new DebugLogger(globalConfig.debug || false);
|
|
@@ -179,12 +183,47 @@ export class CallableMockInstance {
|
|
|
179
183
|
}
|
|
180
184
|
return this.requestHistory[this.requestHistory.length - 1];
|
|
181
185
|
}
|
|
186
|
+
// ===== Introspection =====
|
|
187
|
+
getRoutes() {
|
|
188
|
+
return this.routes.map((r) => ({
|
|
189
|
+
method: r.method,
|
|
190
|
+
path: r.path,
|
|
191
|
+
hasParams: r.params.length > 0,
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
getState() {
|
|
195
|
+
return this.globalConfig.state || {};
|
|
196
|
+
}
|
|
197
|
+
// ===== Lifecycle Events =====
|
|
198
|
+
on(event, listener) {
|
|
199
|
+
let set = this.listeners.get(event);
|
|
200
|
+
if (!set) {
|
|
201
|
+
set = new Set();
|
|
202
|
+
this.listeners.set(event, set);
|
|
203
|
+
}
|
|
204
|
+
set.add(listener);
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
off(event, listener) {
|
|
208
|
+
this.listeners.get(event)?.delete(listener);
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
emit(event, data) {
|
|
212
|
+
const set = this.listeners.get(event);
|
|
213
|
+
if (set) {
|
|
214
|
+
for (const listener of set) {
|
|
215
|
+
listener(data);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
182
219
|
// ===== Reset / Lifecycle =====
|
|
183
220
|
reset() {
|
|
221
|
+
this.close();
|
|
184
222
|
this.routes = [];
|
|
185
223
|
this.staticRoutes.clear();
|
|
186
224
|
this.plugins = [];
|
|
187
225
|
this.requestHistory = [];
|
|
226
|
+
this.listeners.clear();
|
|
188
227
|
if (this.globalConfig.state) {
|
|
189
228
|
for (const key of Object.keys(this.globalConfig.state)) {
|
|
190
229
|
delete this.globalConfig.state[key];
|
|
@@ -204,14 +243,96 @@ export class CallableMockInstance {
|
|
|
204
243
|
}
|
|
205
244
|
this.logger.log("lifecycle", "State cleared");
|
|
206
245
|
}
|
|
246
|
+
// ===== Standalone Server =====
|
|
247
|
+
listen(port = 0, hostname = "127.0.0.1") {
|
|
248
|
+
if (this.server) {
|
|
249
|
+
throw new SchmockError("Server is already running", "SERVER_ALREADY_RUNNING");
|
|
250
|
+
}
|
|
251
|
+
const httpServer = createServer((req, res) => {
|
|
252
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
253
|
+
const method = toHttpMethod(req.method ?? "GET");
|
|
254
|
+
const path = url.pathname;
|
|
255
|
+
const headers = {};
|
|
256
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
257
|
+
if (typeof value === "string") {
|
|
258
|
+
headers[key] = value;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const query = {};
|
|
262
|
+
url.searchParams.forEach((value, key) => {
|
|
263
|
+
query[key] = value;
|
|
264
|
+
});
|
|
265
|
+
const chunks = [];
|
|
266
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
267
|
+
req.on("end", () => {
|
|
268
|
+
const raw = Buffer.concat(chunks).toString();
|
|
269
|
+
let body;
|
|
270
|
+
const contentType = headers["content-type"] ?? "";
|
|
271
|
+
if (raw && contentType.includes("json")) {
|
|
272
|
+
try {
|
|
273
|
+
body = JSON.parse(raw);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
body = raw;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else if (raw) {
|
|
280
|
+
body = raw;
|
|
281
|
+
}
|
|
282
|
+
void this.handle(method, path, { headers, body, query }).then((schmockResponse) => {
|
|
283
|
+
const responseHeaders = {
|
|
284
|
+
...schmockResponse.headers,
|
|
285
|
+
};
|
|
286
|
+
if (!responseHeaders["content-type"] &&
|
|
287
|
+
schmockResponse.body !== undefined &&
|
|
288
|
+
typeof schmockResponse.body !== "string") {
|
|
289
|
+
responseHeaders["content-type"] = "application/json";
|
|
290
|
+
}
|
|
291
|
+
const responseBody = schmockResponse.body === undefined
|
|
292
|
+
? undefined
|
|
293
|
+
: typeof schmockResponse.body === "string"
|
|
294
|
+
? schmockResponse.body
|
|
295
|
+
: JSON.stringify(schmockResponse.body);
|
|
296
|
+
res.writeHead(schmockResponse.status, responseHeaders);
|
|
297
|
+
res.end(responseBody);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
this.server = httpServer;
|
|
302
|
+
return new Promise((resolve, reject) => {
|
|
303
|
+
httpServer.on("error", reject);
|
|
304
|
+
httpServer.listen(port, hostname, () => {
|
|
305
|
+
const addr = httpServer.address();
|
|
306
|
+
const actualPort = addr !== null && typeof addr === "object" ? addr.port : port;
|
|
307
|
+
this.serverInfo = { port: actualPort, hostname };
|
|
308
|
+
this.logger.log("server", `Listening on ${hostname}:${actualPort}`);
|
|
309
|
+
resolve(this.serverInfo);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
close() {
|
|
314
|
+
if (!this.server) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
this.server.close();
|
|
318
|
+
this.server = undefined;
|
|
319
|
+
this.serverInfo = undefined;
|
|
320
|
+
this.logger.log("server", "Server stopped");
|
|
321
|
+
}
|
|
207
322
|
async handle(method, path, options) {
|
|
208
323
|
const requestId = crypto.randomUUID();
|
|
324
|
+
const handleStart = performance.now();
|
|
209
325
|
this.logger.log("request", `[${requestId}] ${method} ${path}`, {
|
|
210
326
|
headers: options?.headers,
|
|
211
327
|
query: options?.query,
|
|
212
328
|
bodyType: options?.body ? typeof options.body : "none",
|
|
213
329
|
});
|
|
214
330
|
this.logger.time(`request-${requestId}`);
|
|
331
|
+
this.emit("request:start", {
|
|
332
|
+
method,
|
|
333
|
+
path,
|
|
334
|
+
headers: options?.headers || {},
|
|
335
|
+
});
|
|
215
336
|
try {
|
|
216
337
|
// Apply namespace if configured
|
|
217
338
|
let requestPath = path;
|
|
@@ -244,18 +365,31 @@ export class CallableMockInstance {
|
|
|
244
365
|
const matchedRoute = this.findRoute(method, requestPath);
|
|
245
366
|
if (!matchedRoute) {
|
|
246
367
|
this.logger.log("route", `[${requestId}] No route found for ${method} ${requestPath}`);
|
|
368
|
+
this.emit("request:notfound", { method, path: requestPath });
|
|
247
369
|
const error = new RouteNotFoundError(method, path);
|
|
248
370
|
const response = {
|
|
249
371
|
status: 404,
|
|
250
372
|
body: { error: error.message, code: error.code },
|
|
251
373
|
headers: {},
|
|
252
374
|
};
|
|
375
|
+
this.emit("request:end", {
|
|
376
|
+
method,
|
|
377
|
+
path: requestPath,
|
|
378
|
+
status: 404,
|
|
379
|
+
duration: performance.now() - handleStart,
|
|
380
|
+
});
|
|
253
381
|
this.logger.timeEnd(`request-${requestId}`);
|
|
254
382
|
return response;
|
|
255
383
|
}
|
|
256
384
|
this.logger.log("route", `[${requestId}] Matched route: ${method} ${matchedRoute.path}`);
|
|
257
385
|
// Extract parameters from the matched route
|
|
258
386
|
const params = this.extractParams(matchedRoute, requestPath);
|
|
387
|
+
this.emit("request:match", {
|
|
388
|
+
method,
|
|
389
|
+
path: requestPath,
|
|
390
|
+
routePath: matchedRoute.path,
|
|
391
|
+
params,
|
|
392
|
+
});
|
|
259
393
|
// Generate initial response from route handler
|
|
260
394
|
const context = {
|
|
261
395
|
method,
|
|
@@ -297,8 +431,8 @@ export class CallableMockInstance {
|
|
|
297
431
|
}
|
|
298
432
|
// Parse and prepare response
|
|
299
433
|
const response = this.parseResponse(result, matchedRoute.config);
|
|
300
|
-
// Apply
|
|
301
|
-
await this.applyDelay();
|
|
434
|
+
// Apply delay (route-level overrides global)
|
|
435
|
+
await this.applyDelay(matchedRoute.config.delay);
|
|
302
436
|
// Record request in history
|
|
303
437
|
this.requestHistory.push({
|
|
304
438
|
method,
|
|
@@ -310,6 +444,12 @@ export class CallableMockInstance {
|
|
|
310
444
|
timestamp: Date.now(),
|
|
311
445
|
response: { status: response.status, body: response.body },
|
|
312
446
|
});
|
|
447
|
+
this.emit("request:end", {
|
|
448
|
+
method,
|
|
449
|
+
path: requestPath,
|
|
450
|
+
status: response.status,
|
|
451
|
+
duration: performance.now() - handleStart,
|
|
452
|
+
});
|
|
313
453
|
// Log successful response
|
|
314
454
|
this.logger.log("response", `[${requestId}] Sending response ${response.status}`, {
|
|
315
455
|
status: response.status,
|
|
@@ -330,7 +470,7 @@ export class CallableMockInstance {
|
|
|
330
470
|
},
|
|
331
471
|
headers: {},
|
|
332
472
|
};
|
|
333
|
-
// Apply
|
|
473
|
+
// Apply delay even for error responses
|
|
334
474
|
await this.applyDelay();
|
|
335
475
|
this.logger.log("error", `[${requestId}] Returning error response 500`);
|
|
336
476
|
this.logger.timeEnd(`request-${requestId}`);
|
|
@@ -342,16 +482,16 @@ export class CallableMockInstance {
|
|
|
342
482
|
* Supports both fixed delays and random delays within a range
|
|
343
483
|
* @private
|
|
344
484
|
*/
|
|
345
|
-
async applyDelay() {
|
|
346
|
-
|
|
485
|
+
async applyDelay(routeDelay) {
|
|
486
|
+
const effectiveDelay = routeDelay ?? this.globalConfig.delay;
|
|
487
|
+
if (!effectiveDelay) {
|
|
347
488
|
return;
|
|
348
489
|
}
|
|
349
|
-
const
|
|
350
|
-
? Math.random() *
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
490
|
+
const ms = Array.isArray(effectiveDelay)
|
|
491
|
+
? Math.random() * (effectiveDelay[1] - effectiveDelay[0]) +
|
|
492
|
+
effectiveDelay[0]
|
|
493
|
+
: effectiveDelay;
|
|
494
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
355
495
|
}
|
|
356
496
|
/**
|
|
357
497
|
* Parse and normalize response result into Response object
|
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, RouteInfo, RouteKey, ServerInfo, StaticData, } from "./types.js";
|
|
28
28
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,OAAO,CACrB,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,GAC5B,OAAO,CAAC,oBAAoB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,OAAO,CACrB,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,GAC5B,OAAO,CAAC,oBAAoB,CA6C9B;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,SAAS,EACT,QAAQ,EACR,UAAU,EACV,UAAU,GACX,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,18 @@ 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
|
+
on: ((event, listener) => {
|
|
46
|
+
instance.on(event, listener);
|
|
47
|
+
return callableInstance;
|
|
48
|
+
}),
|
|
49
|
+
off: ((event, listener) => {
|
|
50
|
+
instance.off(event, listener);
|
|
51
|
+
return callableInstance;
|
|
52
|
+
}),
|
|
53
|
+
getRoutes: instance.getRoutes.bind(instance),
|
|
54
|
+
getState: instance.getState.bind(instance),
|
|
55
|
+
listen: instance.listen.bind(instance),
|
|
56
|
+
close: instance.close.bind(instance),
|
|
45
57
|
});
|
|
46
58
|
instance.setCallableRef(callableInstance);
|
|
47
59
|
return callableInstance;
|
package/dist/types.d.ts
CHANGED
|
@@ -15,4 +15,6 @@ 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;
|
|
19
|
+
export type RouteInfo = Schmock.RouteInfo;
|
|
18
20
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;AAC9C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;AAChE,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AACpC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC"}
|
|
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"}
|
package/dist/types.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/// <reference path="
|
|
1
|
+
/// <reference path="../schmock.d.ts" />
|
|
2
2
|
export {};
|
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.
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "bun build:lib && bun build:types",
|
|
22
|
-
"build:lib": "bun build --minify --outdir=dist src/index.ts",
|
|
22
|
+
"build:lib": "bun build --minify --target node --outdir=dist src/index.ts",
|
|
23
23
|
"build:types": "rm -f tsconfig.tsbuildinfo && tsc --build",
|
|
24
24
|
"pretest": "rm -f src/*.js src/*.d.ts || true",
|
|
25
25
|
"test": "vitest",
|
package/src/builder.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
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,9 @@ 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;
|
|
91
|
+
private listeners = new Map<string, Set<(data: unknown) => void>>();
|
|
87
92
|
|
|
88
93
|
constructor(private globalConfig: Schmock.GlobalConfig = {}) {
|
|
89
94
|
this.logger = new DebugLogger(globalConfig.debug || false);
|
|
@@ -251,13 +256,55 @@ export class CallableMockInstance {
|
|
|
251
256
|
return this.requestHistory[this.requestHistory.length - 1];
|
|
252
257
|
}
|
|
253
258
|
|
|
259
|
+
// ===== Introspection =====
|
|
260
|
+
|
|
261
|
+
getRoutes(): Schmock.RouteInfo[] {
|
|
262
|
+
return this.routes.map((r) => ({
|
|
263
|
+
method: r.method,
|
|
264
|
+
path: r.path,
|
|
265
|
+
hasParams: r.params.length > 0,
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
getState(): Record<string, unknown> {
|
|
270
|
+
return this.globalConfig.state || {};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ===== Lifecycle Events =====
|
|
274
|
+
|
|
275
|
+
on(event: string, listener: (data: unknown) => void): this {
|
|
276
|
+
let set = this.listeners.get(event);
|
|
277
|
+
if (!set) {
|
|
278
|
+
set = new Set();
|
|
279
|
+
this.listeners.set(event, set);
|
|
280
|
+
}
|
|
281
|
+
set.add(listener);
|
|
282
|
+
return this;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
off(event: string, listener: (data: unknown) => void): this {
|
|
286
|
+
this.listeners.get(event)?.delete(listener);
|
|
287
|
+
return this;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private emit(event: string, data: unknown): void {
|
|
291
|
+
const set = this.listeners.get(event);
|
|
292
|
+
if (set) {
|
|
293
|
+
for (const listener of set) {
|
|
294
|
+
listener(data);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
254
299
|
// ===== Reset / Lifecycle =====
|
|
255
300
|
|
|
256
301
|
reset(): void {
|
|
302
|
+
this.close();
|
|
257
303
|
this.routes = [];
|
|
258
304
|
this.staticRoutes.clear();
|
|
259
305
|
this.plugins = [];
|
|
260
306
|
this.requestHistory = [];
|
|
307
|
+
this.listeners.clear();
|
|
261
308
|
if (this.globalConfig.state) {
|
|
262
309
|
for (const key of Object.keys(this.globalConfig.state)) {
|
|
263
310
|
delete this.globalConfig.state[key];
|
|
@@ -280,12 +327,108 @@ export class CallableMockInstance {
|
|
|
280
327
|
this.logger.log("lifecycle", "State cleared");
|
|
281
328
|
}
|
|
282
329
|
|
|
330
|
+
// ===== Standalone Server =====
|
|
331
|
+
|
|
332
|
+
listen(port = 0, hostname = "127.0.0.1"): Promise<Schmock.ServerInfo> {
|
|
333
|
+
if (this.server) {
|
|
334
|
+
throw new SchmockError(
|
|
335
|
+
"Server is already running",
|
|
336
|
+
"SERVER_ALREADY_RUNNING",
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const httpServer = createServer((req, res) => {
|
|
341
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
342
|
+
const method = toHttpMethod(req.method ?? "GET");
|
|
343
|
+
const path = url.pathname;
|
|
344
|
+
|
|
345
|
+
const headers: Record<string, string> = {};
|
|
346
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
347
|
+
if (typeof value === "string") {
|
|
348
|
+
headers[key] = value;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const query: Record<string, string> = {};
|
|
353
|
+
url.searchParams.forEach((value, key) => {
|
|
354
|
+
query[key] = value;
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const chunks: Buffer[] = [];
|
|
358
|
+
req.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
359
|
+
req.on("end", () => {
|
|
360
|
+
const raw = Buffer.concat(chunks).toString();
|
|
361
|
+
let body: unknown;
|
|
362
|
+
const contentType = headers["content-type"] ?? "";
|
|
363
|
+
if (raw && contentType.includes("json")) {
|
|
364
|
+
try {
|
|
365
|
+
body = JSON.parse(raw);
|
|
366
|
+
} catch {
|
|
367
|
+
body = raw;
|
|
368
|
+
}
|
|
369
|
+
} else if (raw) {
|
|
370
|
+
body = raw;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
void this.handle(method, path, { headers, body, query }).then(
|
|
374
|
+
(schmockResponse) => {
|
|
375
|
+
const responseHeaders: Record<string, string> = {
|
|
376
|
+
...schmockResponse.headers,
|
|
377
|
+
};
|
|
378
|
+
if (
|
|
379
|
+
!responseHeaders["content-type"] &&
|
|
380
|
+
schmockResponse.body !== undefined &&
|
|
381
|
+
typeof schmockResponse.body !== "string"
|
|
382
|
+
) {
|
|
383
|
+
responseHeaders["content-type"] = "application/json";
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const responseBody =
|
|
387
|
+
schmockResponse.body === undefined
|
|
388
|
+
? undefined
|
|
389
|
+
: typeof schmockResponse.body === "string"
|
|
390
|
+
? schmockResponse.body
|
|
391
|
+
: JSON.stringify(schmockResponse.body);
|
|
392
|
+
|
|
393
|
+
res.writeHead(schmockResponse.status, responseHeaders);
|
|
394
|
+
res.end(responseBody);
|
|
395
|
+
},
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
this.server = httpServer;
|
|
401
|
+
|
|
402
|
+
return new Promise((resolve, reject) => {
|
|
403
|
+
httpServer.on("error", reject);
|
|
404
|
+
httpServer.listen(port, hostname, () => {
|
|
405
|
+
const addr = httpServer.address();
|
|
406
|
+
const actualPort =
|
|
407
|
+
addr !== null && typeof addr === "object" ? addr.port : port;
|
|
408
|
+
this.serverInfo = { port: actualPort, hostname };
|
|
409
|
+
this.logger.log("server", `Listening on ${hostname}:${actualPort}`);
|
|
410
|
+
resolve(this.serverInfo);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
close(): void {
|
|
416
|
+
if (!this.server) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
this.server.close();
|
|
420
|
+
this.server = undefined;
|
|
421
|
+
this.serverInfo = undefined;
|
|
422
|
+
this.logger.log("server", "Server stopped");
|
|
423
|
+
}
|
|
424
|
+
|
|
283
425
|
async handle(
|
|
284
426
|
method: Schmock.HttpMethod,
|
|
285
427
|
path: string,
|
|
286
428
|
options?: Schmock.RequestOptions,
|
|
287
429
|
): Promise<Schmock.Response> {
|
|
288
430
|
const requestId = crypto.randomUUID();
|
|
431
|
+
const handleStart = performance.now();
|
|
289
432
|
this.logger.log("request", `[${requestId}] ${method} ${path}`, {
|
|
290
433
|
headers: options?.headers,
|
|
291
434
|
query: options?.query,
|
|
@@ -293,6 +436,12 @@ export class CallableMockInstance {
|
|
|
293
436
|
});
|
|
294
437
|
this.logger.time(`request-${requestId}`);
|
|
295
438
|
|
|
439
|
+
this.emit("request:start", {
|
|
440
|
+
method,
|
|
441
|
+
path,
|
|
442
|
+
headers: options?.headers || {},
|
|
443
|
+
});
|
|
444
|
+
|
|
296
445
|
try {
|
|
297
446
|
// Apply namespace if configured
|
|
298
447
|
let requestPath = path;
|
|
@@ -340,12 +489,19 @@ export class CallableMockInstance {
|
|
|
340
489
|
"route",
|
|
341
490
|
`[${requestId}] No route found for ${method} ${requestPath}`,
|
|
342
491
|
);
|
|
492
|
+
this.emit("request:notfound", { method, path: requestPath });
|
|
343
493
|
const error = new RouteNotFoundError(method, path);
|
|
344
494
|
const response = {
|
|
345
495
|
status: 404,
|
|
346
496
|
body: { error: error.message, code: error.code },
|
|
347
497
|
headers: {},
|
|
348
498
|
};
|
|
499
|
+
this.emit("request:end", {
|
|
500
|
+
method,
|
|
501
|
+
path: requestPath,
|
|
502
|
+
status: 404,
|
|
503
|
+
duration: performance.now() - handleStart,
|
|
504
|
+
});
|
|
349
505
|
this.logger.timeEnd(`request-${requestId}`);
|
|
350
506
|
return response;
|
|
351
507
|
}
|
|
@@ -358,6 +514,13 @@ export class CallableMockInstance {
|
|
|
358
514
|
// Extract parameters from the matched route
|
|
359
515
|
const params = this.extractParams(matchedRoute, requestPath);
|
|
360
516
|
|
|
517
|
+
this.emit("request:match", {
|
|
518
|
+
method,
|
|
519
|
+
path: requestPath,
|
|
520
|
+
routePath: matchedRoute.path,
|
|
521
|
+
params,
|
|
522
|
+
});
|
|
523
|
+
|
|
361
524
|
// Generate initial response from route handler
|
|
362
525
|
const context: Schmock.RequestContext = {
|
|
363
526
|
method,
|
|
@@ -410,8 +573,8 @@ export class CallableMockInstance {
|
|
|
410
573
|
// Parse and prepare response
|
|
411
574
|
const response = this.parseResponse(result, matchedRoute.config);
|
|
412
575
|
|
|
413
|
-
// Apply
|
|
414
|
-
await this.applyDelay();
|
|
576
|
+
// Apply delay (route-level overrides global)
|
|
577
|
+
await this.applyDelay(matchedRoute.config.delay);
|
|
415
578
|
|
|
416
579
|
// Record request in history
|
|
417
580
|
this.requestHistory.push({
|
|
@@ -425,6 +588,13 @@ export class CallableMockInstance {
|
|
|
425
588
|
response: { status: response.status, body: response.body },
|
|
426
589
|
});
|
|
427
590
|
|
|
591
|
+
this.emit("request:end", {
|
|
592
|
+
method,
|
|
593
|
+
path: requestPath,
|
|
594
|
+
status: response.status,
|
|
595
|
+
duration: performance.now() - handleStart,
|
|
596
|
+
});
|
|
597
|
+
|
|
428
598
|
// Log successful response
|
|
429
599
|
this.logger.log(
|
|
430
600
|
"response",
|
|
@@ -455,7 +625,7 @@ export class CallableMockInstance {
|
|
|
455
625
|
headers: {},
|
|
456
626
|
};
|
|
457
627
|
|
|
458
|
-
// Apply
|
|
628
|
+
// Apply delay even for error responses
|
|
459
629
|
await this.applyDelay();
|
|
460
630
|
|
|
461
631
|
this.logger.log("error", `[${requestId}] Returning error response 500`);
|
|
@@ -469,18 +639,20 @@ export class CallableMockInstance {
|
|
|
469
639
|
* Supports both fixed delays and random delays within a range
|
|
470
640
|
* @private
|
|
471
641
|
*/
|
|
472
|
-
private async applyDelay(
|
|
473
|
-
|
|
642
|
+
private async applyDelay(
|
|
643
|
+
routeDelay?: number | [number, number],
|
|
644
|
+
): Promise<void> {
|
|
645
|
+
const effectiveDelay = routeDelay ?? this.globalConfig.delay;
|
|
646
|
+
if (!effectiveDelay) {
|
|
474
647
|
return;
|
|
475
648
|
}
|
|
476
649
|
|
|
477
|
-
const
|
|
478
|
-
? Math.random() *
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
: this.globalConfig.delay;
|
|
650
|
+
const ms = Array.isArray(effectiveDelay)
|
|
651
|
+
? Math.random() * (effectiveDelay[1] - effectiveDelay[0]) +
|
|
652
|
+
effectiveDelay[0]
|
|
653
|
+
: effectiveDelay;
|
|
482
654
|
|
|
483
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
655
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
484
656
|
}
|
|
485
657
|
|
|
486
658
|
/**
|
package/src/index.ts
CHANGED
|
@@ -52,6 +52,18 @@ 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
|
+
on: ((event: string, listener: (data: unknown) => void) => {
|
|
56
|
+
instance.on(event, listener);
|
|
57
|
+
return callableInstance;
|
|
58
|
+
}) as Schmock.CallableMockInstance["on"],
|
|
59
|
+
off: ((event: string, listener: (data: unknown) => void) => {
|
|
60
|
+
instance.off(event, listener);
|
|
61
|
+
return callableInstance;
|
|
62
|
+
}) as Schmock.CallableMockInstance["off"],
|
|
63
|
+
getRoutes: instance.getRoutes.bind(instance),
|
|
64
|
+
getState: instance.getState.bind(instance),
|
|
65
|
+
listen: instance.listen.bind(instance),
|
|
66
|
+
close: instance.close.bind(instance),
|
|
55
67
|
},
|
|
56
68
|
);
|
|
57
69
|
|
|
@@ -97,6 +109,8 @@ export type {
|
|
|
97
109
|
ResponseBody,
|
|
98
110
|
ResponseResult,
|
|
99
111
|
RouteConfig,
|
|
112
|
+
RouteInfo,
|
|
100
113
|
RouteKey,
|
|
114
|
+
ServerInfo,
|
|
101
115
|
StaticData,
|
|
102
116
|
} 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,142 @@
|
|
|
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/lifecycle-events.feature");
|
|
6
|
+
|
|
7
|
+
describeFeature(feature, ({ Scenario }) => {
|
|
8
|
+
let mock: Schmock.CallableMockInstance;
|
|
9
|
+
let events: Array<{ type: string; data: unknown }>;
|
|
10
|
+
let removedFired: boolean;
|
|
11
|
+
|
|
12
|
+
function collectEvent(type: string) {
|
|
13
|
+
return (data: unknown) => {
|
|
14
|
+
events.push({ type, data });
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Scenario("Events fire at correct times", ({ Given, And, When, Then }) => {
|
|
19
|
+
Given('a mock with a route "GET /items"', () => {
|
|
20
|
+
mock = schmock({ state: {} });
|
|
21
|
+
mock("GET /items", [{ id: 1 }], {});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
And("I register listeners for all events", () => {
|
|
25
|
+
events = [];
|
|
26
|
+
mock.on("request:start", collectEvent("request:start"));
|
|
27
|
+
mock.on("request:match", collectEvent("request:match"));
|
|
28
|
+
mock.on("request:notfound", collectEvent("request:notfound"));
|
|
29
|
+
mock.on("request:end", collectEvent("request:end"));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
When('I request "GET /items"', async () => {
|
|
33
|
+
await mock.handle("GET", "/items");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
Then('the "request:start" event fired', () => {
|
|
37
|
+
expect(events.some((e) => e.type === "request:start")).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
And('the "request:match" event fired with routePath "/items"', () => {
|
|
41
|
+
const match = events.find((e) => e.type === "request:match");
|
|
42
|
+
expect(match).toBeDefined();
|
|
43
|
+
const data = match?.data as Record<string, unknown>;
|
|
44
|
+
expect(data.routePath).toBe("/items");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
And('the "request:end" event fired with status 200', () => {
|
|
48
|
+
const end = events.find((e) => e.type === "request:end");
|
|
49
|
+
expect(end).toBeDefined();
|
|
50
|
+
const data = end?.data as Record<string, unknown>;
|
|
51
|
+
expect(data.status).toBe(200);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
Scenario("Not found event fires for unmatched routes", ({ Given, And, When, Then }) => {
|
|
56
|
+
Given('a mock with a route "GET /items"', () => {
|
|
57
|
+
mock = schmock({ state: {} });
|
|
58
|
+
mock("GET /items", [{ id: 1 }], {});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
And("I register listeners for all events", () => {
|
|
62
|
+
events = [];
|
|
63
|
+
mock.on("request:start", collectEvent("request:start"));
|
|
64
|
+
mock.on("request:match", collectEvent("request:match"));
|
|
65
|
+
mock.on("request:notfound", collectEvent("request:notfound"));
|
|
66
|
+
mock.on("request:end", collectEvent("request:end"));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
When('I request "GET /missing"', async () => {
|
|
70
|
+
await mock.handle("GET", "/missing");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
Then('the "request:start" event fired', () => {
|
|
74
|
+
expect(events.some((e) => e.type === "request:start")).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
And('the "request:notfound" event fired', () => {
|
|
78
|
+
expect(events.some((e) => e.type === "request:notfound")).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
And('the "request:end" event fired with status 404', () => {
|
|
82
|
+
const end = events.find((e) => e.type === "request:end");
|
|
83
|
+
expect(end).toBeDefined();
|
|
84
|
+
const data = end?.data as Record<string, unknown>;
|
|
85
|
+
expect(data.status).toBe(404);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
Scenario("Off removes listener", ({ Given, And, When, Then }) => {
|
|
90
|
+
Given('a mock with a route "GET /items"', () => {
|
|
91
|
+
mock = schmock({ state: {} });
|
|
92
|
+
mock("GET /items", [{ id: 1 }], {});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
And("I register and remove a listener", () => {
|
|
96
|
+
removedFired = false;
|
|
97
|
+
const listener = () => {
|
|
98
|
+
removedFired = true;
|
|
99
|
+
};
|
|
100
|
+
mock.on("request:start", listener);
|
|
101
|
+
mock.off("request:start", listener);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
When('I request "GET /items"', async () => {
|
|
105
|
+
await mock.handle("GET", "/items");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
Then("the removed listener did not fire", () => {
|
|
109
|
+
expect(removedFired).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
Scenario("Reset clears all listeners", ({ Given, And, When, Then }) => {
|
|
114
|
+
Given('a mock with a route "GET /items"', () => {
|
|
115
|
+
mock = schmock({ state: {} });
|
|
116
|
+
mock("GET /items", [{ id: 1 }], {});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
And("I register listeners for all events", () => {
|
|
120
|
+
events = [];
|
|
121
|
+
mock.on("request:start", collectEvent("request:start"));
|
|
122
|
+
mock.on("request:end", collectEvent("request:end"));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
When("I reset the mock", () => {
|
|
126
|
+
mock.reset();
|
|
127
|
+
events = [];
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
And('I add a route "GET /items" again', () => {
|
|
131
|
+
mock("GET /items", [{ id: 1 }], {});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
And('I request "GET /items" after reset', async () => {
|
|
135
|
+
await mock.handle("GET", "/items");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
Then("no events were collected after reset", () => {
|
|
139
|
+
expect(events).toHaveLength(0);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
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/response-delay.feature");
|
|
6
|
+
|
|
7
|
+
describeFeature(feature, ({ Scenario }) => {
|
|
8
|
+
let mock: Schmock.CallableMockInstance;
|
|
9
|
+
let elapsed: number;
|
|
10
|
+
let elapsed2: number;
|
|
11
|
+
|
|
12
|
+
Scenario("Route delay overrides global delay", ({ Given, And, When, Then }) => {
|
|
13
|
+
Given("a mock with global delay of 200ms", () => {
|
|
14
|
+
mock = schmock({ delay: 200, state: {} });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
And('a route "GET /fast" with delay of 10ms', () => {
|
|
18
|
+
mock("GET /fast", { ok: true }, { delay: 10 });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
And('a route "GET /default" with no delay override', () => {
|
|
22
|
+
mock("GET /default", { ok: true }, {});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
When('I request "GET /fast"', async () => {
|
|
26
|
+
const start = performance.now();
|
|
27
|
+
await mock.handle("GET", "/fast");
|
|
28
|
+
elapsed = performance.now() - start;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
Then("the response took less than 100ms", () => {
|
|
32
|
+
expect(elapsed).toBeLessThan(100);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
When('I request "GET /default" with timing', async () => {
|
|
36
|
+
const start = performance.now();
|
|
37
|
+
await mock.handle("GET", "/default");
|
|
38
|
+
elapsed2 = performance.now() - start;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
Then("that response took at least 150ms", () => {
|
|
42
|
+
expect(elapsed2).toBeGreaterThanOrEqual(150);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
Scenario("Route delay supports random range", ({ Given, And, When, Then }) => {
|
|
47
|
+
Given("a mock with no global delay", () => {
|
|
48
|
+
mock = schmock({ state: {} });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
And('a route "GET /random" with delay range 10 to 30', () => {
|
|
52
|
+
mock("GET /random", { ok: true }, { delay: [10, 30] });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
When('I request "GET /random"', async () => {
|
|
56
|
+
const start = performance.now();
|
|
57
|
+
await mock.handle("GET", "/random");
|
|
58
|
+
elapsed = performance.now() - start;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
Then("the response took at least 10ms", () => {
|
|
62
|
+
expect(elapsed).toBeGreaterThanOrEqual(8); // small tolerance
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
Scenario("No route delay inherits global delay", ({ Given, And, When, Then }) => {
|
|
67
|
+
Given("a mock with global delay of 50ms", () => {
|
|
68
|
+
mock = schmock({ delay: 50, state: {} });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
And('a route "GET /items" with no delay override', () => {
|
|
72
|
+
mock("GET /items", { ok: true }, {});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
When('I request "GET /items" with timing', async () => {
|
|
76
|
+
const start = performance.now();
|
|
77
|
+
await mock.handle("GET", "/items");
|
|
78
|
+
elapsed = performance.now() - start;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
Then("that response took at least 40ms", () => {
|
|
82
|
+
expect(elapsed).toBeGreaterThanOrEqual(40);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/// <reference path="
|
|
1
|
+
/// <reference path="../schmock.d.ts" />
|
|
2
2
|
|
|
3
|
-
// Re-export types for
|
|
3
|
+
// Re-export ambient types for consumers
|
|
4
4
|
export type HttpMethod = Schmock.HttpMethod;
|
|
5
5
|
export type RouteKey = Schmock.RouteKey;
|
|
6
6
|
export type ResponseBody = Schmock.ResponseBody;
|
|
@@ -18,3 +18,5 @@ 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;
|
|
22
|
+
export type RouteInfo = Schmock.RouteInfo;
|
package/src/builder.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["builder.ts"],"names":[],"mappings":"AAAA,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AAW7B,OAAO,KAAK,EACV,OAAO,EACP,aAAa,EAEb,YAAY,EAGZ,MAAM,EACP,MAAM,SAAS,CAAC;AAajB;;;;GAIG;AACH,qBAAa,cAAc,CAAC,MAAM,GAAG,OAAO,CAAE,YAAW,OAAO,CAAC,MAAM,CAAC;IACtE,OAAO,CAAC,OAAO,CAEb;IAEF,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IAK/C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAK/C,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAShC,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKpC,KAAK,IAAI,YAAY,CAAC,MAAM,CAAC;IAQ7B;;;OAGG;IACH,OAAO,CAAC,aAAa;CA0BtB"}
|
package/src/errors.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,YAAa,SAAQ,KAAK;aAGnB,IAAI,EAAE,MAAM;aACZ,OAAO,CAAC,EAAE,OAAO;gBAFjC,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,YAAA;CAMpC;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAQzC;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,YAAY;gBACnC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,YAAY;gBAC3C,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAQxC;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,YAAY;gBAC/B,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;CAQ7C;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;gBACxC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAQnE;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,OAAO;CAQ1D;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;gBACtC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAQ7D"}
|
package/src/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,IAAI,OAAO,CAEjC;AAGD,YAAY,EACV,OAAO,EACP,aAAa,EACb,UAAU,EACV,YAAY,EACZ,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,QAAQ,EACR,MAAM,GACP,MAAM,SAAS,CAAC;AAGjB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,uBAAuB,EACvB,WAAW,EACX,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,UAAU,CAAC"}
|
package/src/parser.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAuC3D"}
|
package/src/types.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["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,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,eAAe,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;AACtE,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,gBAAgB,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;AACxE,MAAM,MAAM,eAAe,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;AACtE,MAAM,MAAM,MAAM,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACpD,MAAM,MAAM,OAAO,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AACtD,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC"}
|