@schmock/core 2.0.1 → 2.0.3
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.map +1 -1
- package/dist/builder.js +10 -6
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +5 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +15 -12
- package/package.json +5 -5
- package/src/builder.ts +15 -6
- package/src/constants.ts +5 -0
- package/src/parser.test.ts +17 -0
- package/src/parser.ts +15 -13
- package/src/steps/developer-experience.steps.ts +18 -0
- package/src/steps/request-history.steps.ts +18 -0
package/dist/builder.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAwDA;;;;GAIG;AACH,qBAAa,oBAAoB;IAanB,OAAO,CAAC,YAAY;IAZhC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,WAAW,CAA2C;IAC9D,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,UAAU,CAAiC;IACnD,OAAO,CAAC,eAAe,CAAwC;IAE/D,OAAO,CAAC,SAAS,CAAoC;gBAEjC,YAAY,GAAE,OAAO,CAAC,YAAiB;IAa3D,WAAW,CACT,KAAK,EAAE,OAAO,CAAC,QAAQ,EACvB,SAAS,EAAE,OAAO,CAAC,SAAS,EAC5B,MAAM,EAAE,OAAO,CAAC,WAAW,GAC1B,IAAI;
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAwDA;;;;GAIG;AACH,qBAAa,oBAAoB;IAanB,OAAO,CAAC,YAAY;IAZhC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,WAAW,CAA2C;IAC9D,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,UAAU,CAAiC;IACnD,OAAO,CAAC,eAAe,CAAwC;IAE/D,OAAO,CAAC,SAAS,CAAoC;gBAEjC,YAAY,GAAE,OAAO,CAAC,YAAiB;IAa3D,WAAW,CACT,KAAK,EAAE,OAAO,CAAC,QAAQ,EACvB,SAAS,EAAE,OAAO,CAAC,SAAS,EAC5B,MAAM,EAAE,OAAO,CAAC,WAAW,GAC1B,IAAI;IA+EP,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,oBAAoB,GAAG,IAAI;IAIvD,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI;IAoBlC,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE;IAS5E,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO;IAS3D,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM;IAS7D,WAAW,CACT,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAC3B,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,GAAG,SAAS;IAYpC,SAAS,IAAI,OAAO,CAAC,SAAS,EAAE;IAQhC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAMnC,EAAE,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,EAC/B,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GACnD,IAAI;IAUP,GAAG,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,EAChC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GACnD,IAAI;IAKP,OAAO,CAAC,IAAI;IAWZ,KAAK,IAAI,IAAI;IAiBb,YAAY,IAAI,IAAI;IAKpB,UAAU,IAAI,IAAI;IAWlB,MAAM,CAAC,IAAI,SAAI,EAAE,QAAQ,SAAc,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC;IAqDrE,KAAK,IAAI,IAAI;IAab,SAAS,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,gBAAgB,GAAG,OAAO,CAAC,eAAe;IAgBhE,MAAM,CACV,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,GAC/B,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;IA+O5B;;;;OAIG;YACW,UAAU;CAezB"}
|
package/dist/builder.js
CHANGED
|
@@ -103,10 +103,11 @@ export class CallableMockInstance {
|
|
|
103
103
|
}
|
|
104
104
|
// Parse the route key to create pattern and extract parameters
|
|
105
105
|
const parsed = parseRouteKey(route);
|
|
106
|
-
// Check for duplicate routes
|
|
106
|
+
// Check for duplicate routes — first registration wins, subsequent ones are dropped
|
|
107
107
|
const existing = this.routes.find((r) => r.method === parsed.method && r.path === parsed.path);
|
|
108
108
|
if (existing) {
|
|
109
109
|
this.logger.log("warning", `Duplicate route: ${route} — first registration wins`);
|
|
110
|
+
return this;
|
|
110
111
|
}
|
|
111
112
|
// Compile the route
|
|
112
113
|
const compiledRoute = {
|
|
@@ -119,12 +120,9 @@ export class CallableMockInstance {
|
|
|
119
120
|
};
|
|
120
121
|
this.routes.push(compiledRoute);
|
|
121
122
|
// Store static routes (no params) in Map for O(1) lookup
|
|
122
|
-
// Only store the first registration — "first registration wins" semantics
|
|
123
123
|
if (parsed.params.length === 0) {
|
|
124
124
|
const key = `${parsed.method} ${normalizePath(parsed.path)}`;
|
|
125
|
-
|
|
126
|
-
this.staticRoutes.set(key, compiledRoute);
|
|
127
|
-
}
|
|
125
|
+
this.staticRoutes.set(key, compiledRoute);
|
|
128
126
|
}
|
|
129
127
|
this.logger.log("route", `Route defined: ${route}`, {
|
|
130
128
|
contentType: config.contentType,
|
|
@@ -419,7 +417,7 @@ export class CallableMockInstance {
|
|
|
419
417
|
const response = parseResponse(result, matchedRoute.config);
|
|
420
418
|
// Apply delay (route-level overrides global)
|
|
421
419
|
await this.applyDelay(matchedRoute.config.delay);
|
|
422
|
-
// Record request in history
|
|
420
|
+
// Record request in history (FIFO-bounded when maxHistorySize is set)
|
|
423
421
|
this.requestHistory.push({
|
|
424
422
|
method,
|
|
425
423
|
path: requestPath,
|
|
@@ -430,6 +428,12 @@ export class CallableMockInstance {
|
|
|
430
428
|
timestamp: Date.now(),
|
|
431
429
|
response: { status: response.status, body: response.body },
|
|
432
430
|
});
|
|
431
|
+
const maxHistorySize = this.globalConfig.maxHistorySize;
|
|
432
|
+
if (typeof maxHistorySize === "number" &&
|
|
433
|
+
maxHistorySize >= 0 &&
|
|
434
|
+
this.requestHistory.length > maxHistorySize) {
|
|
435
|
+
this.requestHistory.splice(0, this.requestHistory.length - maxHistorySize);
|
|
436
|
+
}
|
|
433
437
|
this.emit("request:end", {
|
|
434
438
|
method,
|
|
435
439
|
path: requestPath,
|
package/dist/constants.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export declare function isRouteNotFound(response: {
|
|
|
16
16
|
/**
|
|
17
17
|
* Check if a value is a status tuple: [status, body] or [status, body, headers]
|
|
18
18
|
* Guards against misinterpreting numeric arrays like [1, 2, 3] as tuples.
|
|
19
|
+
*
|
|
20
|
+
* Known ambiguity: a length-2 numeric array whose first element happens to
|
|
21
|
+
* be in the HTTP-status range (e.g. [200, 300] as legitimate data) is
|
|
22
|
+
* indistinguishable from a status tuple by shape alone. Prefer the explicit
|
|
23
|
+
* status() helper or return an object response when the data could collide.
|
|
19
24
|
*/
|
|
20
25
|
export declare function isStatusTuple(value: unknown): value is [number, unknown] | [number, unknown, Record<string, string>];
|
|
21
26
|
//# sourceMappingURL=constants.d.ts.map
|
package/dist/constants.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAG,iBAA0B,CAAC;AAE/D,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAQpC,CAAC;AAEX,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,UAAU,CAEjE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAMvD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAG7E;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,OAAO,CASV;AAED
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAG,iBAA0B,CAAC;AAE/D,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAQpC,CAAC;AAEX,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,UAAU,CAEjE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAMvD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAG7E;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,OAAO,CASV;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAQxE"}
|
package/dist/constants.js
CHANGED
|
@@ -40,6 +40,11 @@ export function isRouteNotFound(response) {
|
|
|
40
40
|
/**
|
|
41
41
|
* Check if a value is a status tuple: [status, body] or [status, body, headers]
|
|
42
42
|
* Guards against misinterpreting numeric arrays like [1, 2, 3] as tuples.
|
|
43
|
+
*
|
|
44
|
+
* Known ambiguity: a length-2 numeric array whose first element happens to
|
|
45
|
+
* be in the HTTP-status range (e.g. [200, 300] as legitimate data) is
|
|
46
|
+
* indistinguishable from a status tuple by shape alone. Prefer the explicit
|
|
47
|
+
* status() helper or return an object response when the data could collide.
|
|
43
48
|
*/
|
|
44
49
|
export function isStatusTuple(value) {
|
|
45
50
|
return (Array.isArray(value) &&
|
package/dist/parser.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,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,
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,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,CAyC3D"}
|
package/dist/parser.js
CHANGED
|
@@ -17,20 +17,23 @@ export function parseRouteKey(routeKey) {
|
|
|
17
17
|
throw new RouteParseError(routeKey, 'Expected format: "METHOD /path" (e.g., "GET /users")');
|
|
18
18
|
}
|
|
19
19
|
const [, method, path] = match;
|
|
20
|
-
//
|
|
20
|
+
// Parameter names are restricted to [A-Za-z0-9_-] so that surrounding
|
|
21
|
+
// literals (".json", brackets, parens, etc.) terminate the name and can
|
|
22
|
+
// be escaped without bleeding into the parameter regex. Build the
|
|
23
|
+
// pattern by splitting the path on the param marker, escaping each
|
|
24
|
+
// literal segment, then substituting the capture group for each :name.
|
|
21
25
|
const params = [];
|
|
22
|
-
const paramPattern = /:([^/]+)/g;
|
|
23
|
-
let paramMatch;
|
|
24
|
-
paramMatch = paramPattern.exec(path);
|
|
25
|
-
while (paramMatch !== null) {
|
|
26
|
-
params.push(paramMatch[1]);
|
|
27
|
-
paramMatch = paramPattern.exec(path);
|
|
28
|
-
}
|
|
29
|
-
// Build regex pattern for matching
|
|
30
|
-
// Replace :param with capture groups
|
|
31
26
|
const regexPath = path
|
|
32
|
-
.
|
|
33
|
-
.
|
|
27
|
+
.split(/(:[a-zA-Z0-9_-]+)/g)
|
|
28
|
+
.map((segment) => {
|
|
29
|
+
const paramMatch = segment.match(/^:([a-zA-Z0-9_-]+)$/);
|
|
30
|
+
if (paramMatch) {
|
|
31
|
+
params.push(paramMatch[1]);
|
|
32
|
+
return "([^/]+)";
|
|
33
|
+
}
|
|
34
|
+
return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
35
|
+
})
|
|
36
|
+
.join("");
|
|
34
37
|
const pattern = new RegExp(`^${regexPath}$`);
|
|
35
38
|
return {
|
|
36
39
|
method: toHttpMethod(method),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schmock/core",
|
|
3
3
|
"description": "Core functionality for Schmock",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
},
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@amiceli/vitest-cucumber": "^6.
|
|
35
|
+
"@amiceli/vitest-cucumber": "^6.5.0",
|
|
36
36
|
"@types/json-schema": "^7.0.15",
|
|
37
|
-
"@types/node": "^25.
|
|
38
|
-
"@vitest/ui": "^4.1.
|
|
39
|
-
"vitest": "^4.1.
|
|
37
|
+
"@types/node": "^25.8.0",
|
|
38
|
+
"@vitest/ui": "^4.1.6",
|
|
39
|
+
"vitest": "^4.1.6"
|
|
40
40
|
}
|
|
41
41
|
}
|
package/src/builder.ts
CHANGED
|
@@ -129,7 +129,7 @@ export class CallableMockInstance {
|
|
|
129
129
|
// Parse the route key to create pattern and extract parameters
|
|
130
130
|
const parsed = parseRouteKey(route);
|
|
131
131
|
|
|
132
|
-
// Check for duplicate routes
|
|
132
|
+
// Check for duplicate routes — first registration wins, subsequent ones are dropped
|
|
133
133
|
const existing = this.routes.find(
|
|
134
134
|
(r) => r.method === parsed.method && r.path === parsed.path,
|
|
135
135
|
);
|
|
@@ -138,6 +138,7 @@ export class CallableMockInstance {
|
|
|
138
138
|
"warning",
|
|
139
139
|
`Duplicate route: ${route} — first registration wins`,
|
|
140
140
|
);
|
|
141
|
+
return this;
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
// Compile the route
|
|
@@ -153,12 +154,9 @@ export class CallableMockInstance {
|
|
|
153
154
|
this.routes.push(compiledRoute);
|
|
154
155
|
|
|
155
156
|
// Store static routes (no params) in Map for O(1) lookup
|
|
156
|
-
// Only store the first registration — "first registration wins" semantics
|
|
157
157
|
if (parsed.params.length === 0) {
|
|
158
158
|
const key = `${parsed.method} ${normalizePath(parsed.path)}`;
|
|
159
|
-
|
|
160
|
-
this.staticRoutes.set(key, compiledRoute);
|
|
161
|
-
}
|
|
159
|
+
this.staticRoutes.set(key, compiledRoute);
|
|
162
160
|
}
|
|
163
161
|
|
|
164
162
|
this.logger.log("route", `Route defined: ${route}`, {
|
|
@@ -564,7 +562,7 @@ export class CallableMockInstance {
|
|
|
564
562
|
// Apply delay (route-level overrides global)
|
|
565
563
|
await this.applyDelay(matchedRoute.config.delay);
|
|
566
564
|
|
|
567
|
-
// Record request in history
|
|
565
|
+
// Record request in history (FIFO-bounded when maxHistorySize is set)
|
|
568
566
|
this.requestHistory.push({
|
|
569
567
|
method,
|
|
570
568
|
path: requestPath,
|
|
@@ -575,6 +573,17 @@ export class CallableMockInstance {
|
|
|
575
573
|
timestamp: Date.now(),
|
|
576
574
|
response: { status: response.status, body: response.body },
|
|
577
575
|
});
|
|
576
|
+
const maxHistorySize = this.globalConfig.maxHistorySize;
|
|
577
|
+
if (
|
|
578
|
+
typeof maxHistorySize === "number" &&
|
|
579
|
+
maxHistorySize >= 0 &&
|
|
580
|
+
this.requestHistory.length > maxHistorySize
|
|
581
|
+
) {
|
|
582
|
+
this.requestHistory.splice(
|
|
583
|
+
0,
|
|
584
|
+
this.requestHistory.length - maxHistorySize,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
578
587
|
|
|
579
588
|
this.emit("request:end", {
|
|
580
589
|
method,
|
package/src/constants.ts
CHANGED
|
@@ -54,6 +54,11 @@ export function isRouteNotFound(response: {
|
|
|
54
54
|
/**
|
|
55
55
|
* Check if a value is a status tuple: [status, body] or [status, body, headers]
|
|
56
56
|
* Guards against misinterpreting numeric arrays like [1, 2, 3] as tuples.
|
|
57
|
+
*
|
|
58
|
+
* Known ambiguity: a length-2 numeric array whose first element happens to
|
|
59
|
+
* be in the HTTP-status range (e.g. [200, 300] as legitimate data) is
|
|
60
|
+
* indistinguishable from a status tuple by shape alone. Prefer the explicit
|
|
61
|
+
* status() helper or return an object response when the data could collide.
|
|
57
62
|
*/
|
|
58
63
|
export function isStatusTuple(
|
|
59
64
|
value: unknown,
|
package/src/parser.test.ts
CHANGED
|
@@ -127,5 +127,22 @@ describe("parseRouteKey", () => {
|
|
|
127
127
|
expect(match).toBeTruthy();
|
|
128
128
|
expect(match?.[1]).toBe("report-2023.pdf");
|
|
129
129
|
});
|
|
130
|
+
|
|
131
|
+
it("stops the param name at a literal '.' suffix and escapes the dot", () => {
|
|
132
|
+
const route = parseRouteKey("GET /files/:name.json");
|
|
133
|
+
|
|
134
|
+
expect(route.params).toEqual(["name"]);
|
|
135
|
+
expect("/files/report.json".match(route.pattern)?.[1]).toBe("report");
|
|
136
|
+
expect("/files/report.json2".match(route.pattern)).toBeNull();
|
|
137
|
+
expect("/files/reportXjson".match(route.pattern)).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("treats parens around params as literal characters", () => {
|
|
141
|
+
const route = parseRouteKey("GET /items/(:id)");
|
|
142
|
+
|
|
143
|
+
expect(route.params).toEqual(["id"]);
|
|
144
|
+
expect("/items/(42)".match(route.pattern)?.[1]).toBe("42");
|
|
145
|
+
expect("/items/42".match(route.pattern)).toBeNull();
|
|
146
|
+
});
|
|
130
147
|
});
|
|
131
148
|
});
|
package/src/parser.ts
CHANGED
|
@@ -34,22 +34,24 @@ export function parseRouteKey(routeKey: string): ParsedRoute {
|
|
|
34
34
|
|
|
35
35
|
const [, method, path] = match;
|
|
36
36
|
|
|
37
|
-
//
|
|
37
|
+
// Parameter names are restricted to [A-Za-z0-9_-] so that surrounding
|
|
38
|
+
// literals (".json", brackets, parens, etc.) terminate the name and can
|
|
39
|
+
// be escaped without bleeding into the parameter regex. Build the
|
|
40
|
+
// pattern by splitting the path on the param marker, escaping each
|
|
41
|
+
// literal segment, then substituting the capture group for each :name.
|
|
38
42
|
const params: string[] = [];
|
|
39
|
-
const paramPattern = /:([^/]+)/g;
|
|
40
|
-
let paramMatch: RegExpExecArray | null;
|
|
41
43
|
|
|
42
|
-
paramMatch = paramPattern.exec(path);
|
|
43
|
-
while (paramMatch !== null) {
|
|
44
|
-
params.push(paramMatch[1]);
|
|
45
|
-
paramMatch = paramPattern.exec(path);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Build regex pattern for matching
|
|
49
|
-
// Replace :param with capture groups
|
|
50
44
|
const regexPath = path
|
|
51
|
-
.
|
|
52
|
-
.
|
|
45
|
+
.split(/(:[a-zA-Z0-9_-]+)/g)
|
|
46
|
+
.map((segment) => {
|
|
47
|
+
const paramMatch = segment.match(/^:([a-zA-Z0-9_-]+)$/);
|
|
48
|
+
if (paramMatch) {
|
|
49
|
+
params.push(paramMatch[1]);
|
|
50
|
+
return "([^/]+)";
|
|
51
|
+
}
|
|
52
|
+
return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
53
|
+
})
|
|
54
|
+
.join("");
|
|
53
55
|
|
|
54
56
|
const pattern = new RegExp(`^${regexPath}$`);
|
|
55
57
|
|
|
@@ -183,4 +183,22 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
183
183
|
});
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
Scenario("Duplicate routes are not duplicated in getRoutes()", ({ Given, When, Then }) => {
|
|
187
|
+
let routesList: ReturnType<typeof mock.getRoutes>;
|
|
188
|
+
|
|
189
|
+
Given("I create a mock with two routes on {string} with different data", (_, route: string) => {
|
|
190
|
+
mock = schmock();
|
|
191
|
+
mock(route as Schmock.RouteKey, [{ id: 1 }]);
|
|
192
|
+
mock(route as Schmock.RouteKey, [{ id: 2 }]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
When("I call getRoutes", () => {
|
|
196
|
+
routesList = mock.getRoutes();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
Then("only one route entry is returned", () => {
|
|
200
|
+
expect(routesList).toHaveLength(1);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
186
204
|
});
|
|
@@ -255,4 +255,22 @@ describeFeature(feature, ({ Scenario }) => {
|
|
|
255
255
|
expect(mock.callCount()).toBe(0);
|
|
256
256
|
});
|
|
257
257
|
});
|
|
258
|
+
|
|
259
|
+
Scenario("maxHistorySize bounds the history with FIFO eviction", ({ Given, When, Then }) => {
|
|
260
|
+
Given("I create a mock with maxHistorySize {int} and a users route", (_, maxHistorySize: number) => {
|
|
261
|
+
mock = schmock({ maxHistorySize });
|
|
262
|
+
mock("GET /users", []);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
When("I issue {int} requests to {string}", async (_, count: number, request: string) => {
|
|
266
|
+
const [method, path] = request.split(" ");
|
|
267
|
+
for (let i = 0; i < count; i++) {
|
|
268
|
+
await mock.handle(method as any, path);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
Then("the call count should be {int}", (_, expected: number) => {
|
|
273
|
+
expect(mock.callCount()).toBe(expected);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
258
276
|
});
|