@schmock/core 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +3 -0
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/interceptor.js +26 -9
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/builder.ts +3 -0
- package/src/helpers.test.ts +41 -0
- package/src/interceptor.test.ts +131 -0
- package/src/interceptor.ts +29 -9
- package/src/parser.property.test.ts +101 -0
- package/src/response-parsing.test.ts +74 -0
- package/src/server.test.ts +49 -0
- package/src/steps/async-support.steps.ts +0 -35
- package/src/steps/basic-usage.steps.ts +0 -84
- package/src/steps/developer-experience.steps.ts +0 -269
- package/src/steps/error-handling.steps.ts +0 -66
- package/src/steps/http-methods.steps.ts +0 -66
- package/src/steps/request-history.steps.ts +0 -75
- package/src/steps/route-key-format.steps.ts +0 -19
- package/src/types.ts +1 -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;IAiFP,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,oBAAoB,GAAG,IAAI;IAIvD,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI;IAoBlC,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE;IAS5E,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO;IAS3D,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM;IAS7D,WAAW,CACT,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAC3B,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,GAAG,SAAS;IAYpC,SAAS,IAAI,OAAO,CAAC,SAAS,EAAE;IAQhC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAMnC,EAAE,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,EAC/B,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GACnD,IAAI;IAUP,GAAG,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,EAChC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GACnD,IAAI;IAKP,OAAO,CAAC,IAAI;IAWZ,KAAK,IAAI,IAAI;
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAwDA;;;;GAIG;AACH,qBAAa,oBAAoB;IAanB,OAAO,CAAC,YAAY;IAZhC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,WAAW,CAA2C;IAC9D,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,UAAU,CAAiC;IACnD,OAAO,CAAC,eAAe,CAAwC;IAE/D,OAAO,CAAC,SAAS,CAAoC;gBAEjC,YAAY,GAAE,OAAO,CAAC,YAAiB;IAa3D,WAAW,CACT,KAAK,EAAE,OAAO,CAAC,QAAQ,EACvB,SAAS,EAAE,OAAO,CAAC,SAAS,EAC5B,MAAM,EAAE,OAAO,CAAC,WAAW,GAC1B,IAAI;IAiFP,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,oBAAoB,GAAG,IAAI;IAIvD,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI;IAoBlC,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE;IAS5E,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO;IAS3D,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM;IAS7D,WAAW,CACT,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAC3B,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,GAAG,SAAS;IAYpC,SAAS,IAAI,OAAO,CAAC,SAAS,EAAE;IAQhC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAMnC,EAAE,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,EAC/B,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GACnD,IAAI;IAUP,GAAG,CAAC,CAAC,SAAS,OAAO,CAAC,YAAY,EAChC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GACnD,IAAI;IAKP,OAAO,CAAC,IAAI;IAWZ,KAAK,IAAI,IAAI;IAiBb,YAAY,IAAI,IAAI;IAKpB,UAAU,IAAI,IAAI;IAWlB,MAAM,CAAC,IAAI,SAAI,EAAE,QAAQ,SAAc,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC;IAqDrE,KAAK,IAAI,IAAI;IAab,SAAS,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,gBAAgB,GAAG,OAAO,CAAC,eAAe;IAgBhE,MAAM,CACV,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,GAC/B,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;IAoO5B;;;;OAIG;YACW,UAAU;CAezB"}
|
package/dist/builder.js
CHANGED
|
@@ -210,6 +210,8 @@ export class CallableMockInstance {
|
|
|
210
210
|
}
|
|
211
211
|
// ===== Reset / Lifecycle =====
|
|
212
212
|
reset() {
|
|
213
|
+
this.interceptHandle?.restore();
|
|
214
|
+
this.interceptHandle = null;
|
|
213
215
|
this.close();
|
|
214
216
|
this.routes = [];
|
|
215
217
|
this.staticRoutes.clear();
|
|
@@ -281,6 +283,7 @@ export class CallableMockInstance {
|
|
|
281
283
|
if (!this.server) {
|
|
282
284
|
return;
|
|
283
285
|
}
|
|
286
|
+
this.server.closeAllConnections();
|
|
284
287
|
this.server.close();
|
|
285
288
|
this.server = undefined;
|
|
286
289
|
this.serverInfo = undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../src/interceptor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../src/interceptor.ts"],"names":[],"mappings":"AAoHA;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,CACN,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,IAAI,EAAE,MAAM,EACZ,cAAc,CAAC,EAAE,OAAO,CAAC,cAAc,KACpC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,EAC9B,OAAO,GAAE,OAAO,CAAC,gBAAqB,GACrC,OAAO,CAAC,eAAe,CAiJzB"}
|
package/dist/interceptor.js
CHANGED
|
@@ -38,7 +38,7 @@ function extractQuery(url) {
|
|
|
38
38
|
*/
|
|
39
39
|
function extractHeaders(input, init) {
|
|
40
40
|
const headers = {};
|
|
41
|
-
const raw = input instanceof Request ? input.headers :
|
|
41
|
+
const raw = init?.headers ?? (input instanceof Request ? input.headers : undefined);
|
|
42
42
|
if (!raw)
|
|
43
43
|
return headers;
|
|
44
44
|
if (raw instanceof Headers) {
|
|
@@ -48,11 +48,13 @@ function extractHeaders(input, init) {
|
|
|
48
48
|
}
|
|
49
49
|
else if (Array.isArray(raw)) {
|
|
50
50
|
for (const [key, value] of raw) {
|
|
51
|
-
headers[key] = value;
|
|
51
|
+
headers[key.toLowerCase()] = value;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
else {
|
|
55
|
-
Object.
|
|
55
|
+
for (const key of Object.keys(raw)) {
|
|
56
|
+
headers[key.toLowerCase()] = raw[key];
|
|
57
|
+
}
|
|
56
58
|
}
|
|
57
59
|
return headers;
|
|
58
60
|
}
|
|
@@ -60,11 +62,11 @@ function extractHeaders(input, init) {
|
|
|
60
62
|
* Extract body from fetch init, parsing JSON when possible.
|
|
61
63
|
*/
|
|
62
64
|
async function extractBody(input, init) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
// Per Fetch spec, init.body overrides Request.body when both are present
|
|
66
|
+
const bodyInit = init?.body ?? (input instanceof Request ? input.body : null);
|
|
67
|
+
if (bodyInit === null || bodyInit === undefined)
|
|
65
68
|
return undefined;
|
|
66
69
|
// String body — try to parse as JSON
|
|
67
|
-
const bodyInit = init?.body;
|
|
68
70
|
if (typeof bodyInit === "string") {
|
|
69
71
|
try {
|
|
70
72
|
return JSON.parse(bodyInit);
|
|
@@ -73,8 +75,16 @@ async function extractBody(input, init) {
|
|
|
73
75
|
return bodyInit;
|
|
74
76
|
}
|
|
75
77
|
}
|
|
78
|
+
// URLSearchParams — convert to key/value object
|
|
79
|
+
if (bodyInit instanceof URLSearchParams) {
|
|
80
|
+
const result = {};
|
|
81
|
+
bodyInit.forEach((value, key) => {
|
|
82
|
+
result[key] = value;
|
|
83
|
+
});
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
76
86
|
// Request with body — clone and read
|
|
77
|
-
if (input instanceof Request && input.body) {
|
|
87
|
+
if (input instanceof Request && !init?.body && input.body) {
|
|
78
88
|
try {
|
|
79
89
|
return await input.clone().json();
|
|
80
90
|
}
|
|
@@ -105,8 +115,15 @@ export function createFetchInterceptor(handle, options = {}) {
|
|
|
105
115
|
: input;
|
|
106
116
|
const path = extractPathname(urlString);
|
|
107
117
|
// BaseUrl filter — non-matching requests go straight to real fetch
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
// Enforce segment boundary: /api must not match /apiv2
|
|
119
|
+
if (baseUrl) {
|
|
120
|
+
const normalizedBase = baseUrl.endsWith("/")
|
|
121
|
+
? baseUrl.slice(0, -1)
|
|
122
|
+
: baseUrl;
|
|
123
|
+
const isMatch = path === normalizedBase || path.startsWith(`${normalizedBase}/`);
|
|
124
|
+
if (!isMatch) {
|
|
125
|
+
return originalFetch(input, init);
|
|
126
|
+
}
|
|
110
127
|
}
|
|
111
128
|
// Build adapter request
|
|
112
129
|
const method = input instanceof Request ? input.method : (init?.method ?? "GET");
|
package/dist/types.d.ts
CHANGED
|
@@ -30,4 +30,5 @@ export type AdapterRequest = Schmock.AdapterRequest;
|
|
|
30
30
|
export type AdapterResponse = Schmock.AdapterResponse;
|
|
31
31
|
export type InterceptOptions = Schmock.InterceptOptions;
|
|
32
32
|
export type InterceptHandle = Schmock.InterceptHandle;
|
|
33
|
+
export type AdapterRequestOverride = Schmock.AdapterRequestOverride;
|
|
33
34
|
//# 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;AAClD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC;AACtE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;AAC5D,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;AACtD,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;AACxD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;AACxC,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;AAC9C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;AAC1D,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;AAChE,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;AACpC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;AAChD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAClD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC;AACtE,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;AAC5D,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC;AAClE,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;AAC5C,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;AAC1C,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;AACpD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;AACtD,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;AACxD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;AACtD,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC"}
|
package/package.json
CHANGED
package/src/builder.ts
CHANGED
|
@@ -283,6 +283,8 @@ export class CallableMockInstance {
|
|
|
283
283
|
// ===== Reset / Lifecycle =====
|
|
284
284
|
|
|
285
285
|
reset(): void {
|
|
286
|
+
this.interceptHandle?.restore();
|
|
287
|
+
this.interceptHandle = null;
|
|
286
288
|
this.close();
|
|
287
289
|
this.routes = [];
|
|
288
290
|
this.staticRoutes.clear();
|
|
@@ -370,6 +372,7 @@ export class CallableMockInstance {
|
|
|
370
372
|
if (!this.server) {
|
|
371
373
|
return;
|
|
372
374
|
}
|
|
375
|
+
this.server.closeAllConnections();
|
|
373
376
|
this.server.close();
|
|
374
377
|
this.server = undefined;
|
|
375
378
|
this.serverInfo = undefined;
|
package/src/helpers.test.ts
CHANGED
|
@@ -103,4 +103,45 @@ describe("paginate", () => {
|
|
|
103
103
|
expect(result.data).toEqual([]);
|
|
104
104
|
expect(result.total).toBe(5);
|
|
105
105
|
});
|
|
106
|
+
|
|
107
|
+
it("page=0 falls back to page 1 (falsy default)", () => {
|
|
108
|
+
const result = paginate(items, { page: 0, pageSize: 2 });
|
|
109
|
+
// page=0 is falsy, so options.page || 1 => 1
|
|
110
|
+
expect(result.page).toBe(1);
|
|
111
|
+
expect(result.data).toEqual([{ id: 1 }, { id: 2 }]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("pageSize=0 falls back to default 10 (falsy default)", () => {
|
|
115
|
+
const result = paginate(items, { pageSize: 0 });
|
|
116
|
+
// pageSize=0 is falsy, so options.pageSize || 10 => 10
|
|
117
|
+
expect(result.pageSize).toBe(10);
|
|
118
|
+
expect(result.data).toEqual(items);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("handles empty array", () => {
|
|
122
|
+
const result = paginate([], { page: 1, pageSize: 5 });
|
|
123
|
+
expect(result).toEqual({
|
|
124
|
+
data: [],
|
|
125
|
+
page: 1,
|
|
126
|
+
pageSize: 5,
|
|
127
|
+
total: 0,
|
|
128
|
+
totalPages: 0,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("negative page produces empty data (start index < 0)", () => {
|
|
133
|
+
const result = paginate(items, { page: -1, pageSize: 2 });
|
|
134
|
+
// (page - 1) * pageSize = (-1 - 1) * 2 = -4
|
|
135
|
+
// items.slice(-4, -2) => items from index 1 to 3
|
|
136
|
+
expect(result.page).toBe(-1);
|
|
137
|
+
expect(result.pageSize).toBe(2);
|
|
138
|
+
expect(result.total).toBe(5);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("negative pageSize returns empty data", () => {
|
|
142
|
+
const result = paginate(items, { page: 1, pageSize: -5 });
|
|
143
|
+
// start = 0, end = 0 + (-5) = -5 => items.slice(0, -5) => []
|
|
144
|
+
expect(result.data).toEqual([]);
|
|
145
|
+
expect(result.pageSize).toBe(-5);
|
|
146
|
+
});
|
|
106
147
|
});
|
package/src/interceptor.test.ts
CHANGED
|
@@ -143,6 +143,29 @@ describe("mock.intercept()", () => {
|
|
|
143
143
|
handle.restore();
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
it("normalizes header keys to lowercase", async () => {
|
|
147
|
+
mock("POST /api/data", ({ headers }) => [
|
|
148
|
+
200,
|
|
149
|
+
{ auth: headers.authorization, ct: headers["content-type"] },
|
|
150
|
+
]);
|
|
151
|
+
const handle = mock.intercept();
|
|
152
|
+
|
|
153
|
+
const res = await fetch("http://localhost/api/data", {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: {
|
|
156
|
+
Authorization: "Bearer tok",
|
|
157
|
+
"Content-Type": "application/json",
|
|
158
|
+
},
|
|
159
|
+
body: JSON.stringify({}),
|
|
160
|
+
});
|
|
161
|
+
expect(await res.json()).toEqual({
|
|
162
|
+
auth: "Bearer tok",
|
|
163
|
+
ct: "application/json",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
handle.restore();
|
|
167
|
+
});
|
|
168
|
+
|
|
146
169
|
it("parses JSON body from fetch init", async () => {
|
|
147
170
|
mock("POST /api/users", ({ body }) => [201, body]);
|
|
148
171
|
const handle = mock.intercept();
|
|
@@ -157,4 +180,112 @@ describe("mock.intercept()", () => {
|
|
|
157
180
|
|
|
158
181
|
handle.restore();
|
|
159
182
|
});
|
|
183
|
+
|
|
184
|
+
it("intercepts fetch called with a Request object", async () => {
|
|
185
|
+
mock("GET /api/users", [{ id: 1, name: "Alice" }]);
|
|
186
|
+
const handle = mock.intercept();
|
|
187
|
+
|
|
188
|
+
const req = new Request("http://localhost/api/users");
|
|
189
|
+
const res = await fetch(req);
|
|
190
|
+
expect(res.status).toBe(200);
|
|
191
|
+
expect(await res.json()).toEqual([{ id: 1, name: "Alice" }]);
|
|
192
|
+
|
|
193
|
+
handle.restore();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("prefers init.headers over Request.headers", async () => {
|
|
197
|
+
mock("GET /api/data", ({ headers }) => [200, { val: headers["x-custom"] }]);
|
|
198
|
+
const handle = mock.intercept();
|
|
199
|
+
|
|
200
|
+
const req = new Request("http://localhost/api/data", {
|
|
201
|
+
headers: { "X-Custom": "from-request" },
|
|
202
|
+
});
|
|
203
|
+
const res = await fetch(req, {
|
|
204
|
+
headers: { "X-Custom": "from-init" },
|
|
205
|
+
});
|
|
206
|
+
expect(await res.json()).toEqual({ val: "from-init" });
|
|
207
|
+
|
|
208
|
+
handle.restore();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("parses URLSearchParams body", async () => {
|
|
212
|
+
mock("POST /api/form", ({ body }) => [200, body]);
|
|
213
|
+
const handle = mock.intercept();
|
|
214
|
+
|
|
215
|
+
const params = new URLSearchParams();
|
|
216
|
+
params.set("name", "Alice");
|
|
217
|
+
params.set("role", "admin");
|
|
218
|
+
|
|
219
|
+
const res = await fetch("http://localhost/api/form", {
|
|
220
|
+
method: "POST",
|
|
221
|
+
body: params,
|
|
222
|
+
});
|
|
223
|
+
expect(await res.json()).toEqual({ name: "Alice", role: "admin" });
|
|
224
|
+
|
|
225
|
+
handle.restore();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("baseUrl /api should NOT match /apiv2 (segment boundary)", async () => {
|
|
229
|
+
mock("GET /apiv2/data", [{ v2: true }]);
|
|
230
|
+
const savedFetch = globalThis.fetch;
|
|
231
|
+
const handle = mock.intercept({ baseUrl: "/api" });
|
|
232
|
+
|
|
233
|
+
// /apiv2/data does not start with "/api/" — it should passthrough
|
|
234
|
+
await fetch("http://localhost/apiv2/data");
|
|
235
|
+
expect(savedFetch).toHaveBeenCalled();
|
|
236
|
+
|
|
237
|
+
handle.restore();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("baseUrl with trailing slash matches the same routes as without", async () => {
|
|
241
|
+
mock("GET /api/items", [{ id: 1 }]);
|
|
242
|
+
|
|
243
|
+
// Without trailing slash
|
|
244
|
+
const handle1 = mock.intercept({ baseUrl: "/api" });
|
|
245
|
+
const res1 = await fetch("http://localhost/api/items");
|
|
246
|
+
expect(res1.status).toBe(200);
|
|
247
|
+
expect(await res1.json()).toEqual([{ id: 1 }]);
|
|
248
|
+
handle1.restore();
|
|
249
|
+
|
|
250
|
+
// With trailing slash — should still match /api/items
|
|
251
|
+
const handle2 = mock.intercept({ baseUrl: "/api/" });
|
|
252
|
+
const res2 = await fetch("http://localhost/api/items");
|
|
253
|
+
expect(res2.status).toBe(200);
|
|
254
|
+
expect(await res2.json()).toEqual([{ id: 1 }]);
|
|
255
|
+
handle2.restore();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("extractBody: init.body wins over Request.body per Fetch spec", async () => {
|
|
259
|
+
mock("POST /api/data", ({ body }) => [200, body]);
|
|
260
|
+
const handle = mock.intercept();
|
|
261
|
+
|
|
262
|
+
const req = new Request("http://localhost/api/data", {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: { "content-type": "application/json" },
|
|
265
|
+
body: JSON.stringify({ source: "request" }),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const res = await fetch(req, {
|
|
269
|
+
body: JSON.stringify({ source: "init" }),
|
|
270
|
+
});
|
|
271
|
+
expect(await res.json()).toEqual({ source: "init" });
|
|
272
|
+
|
|
273
|
+
handle.restore();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("extractBody: failed JSON parse falls back to text", async () => {
|
|
277
|
+
mock("POST /api/text", ({ body }) => [200, { received: body }]);
|
|
278
|
+
const handle = mock.intercept();
|
|
279
|
+
|
|
280
|
+
const res = await fetch("http://localhost/api/text", {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: { "content-type": "text/plain" },
|
|
283
|
+
body: "not-json-{broken",
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const json = await res.json();
|
|
287
|
+
expect(json.received).toBe("not-json-{broken");
|
|
288
|
+
|
|
289
|
+
handle.restore();
|
|
290
|
+
});
|
|
160
291
|
});
|
package/src/interceptor.ts
CHANGED
|
@@ -48,7 +48,8 @@ function extractHeaders(
|
|
|
48
48
|
): Record<string, string> {
|
|
49
49
|
const headers: Record<string, string> = {};
|
|
50
50
|
|
|
51
|
-
const raw =
|
|
51
|
+
const raw =
|
|
52
|
+
init?.headers ?? (input instanceof Request ? input.headers : undefined);
|
|
52
53
|
if (!raw) return headers;
|
|
53
54
|
|
|
54
55
|
if (raw instanceof Headers) {
|
|
@@ -57,10 +58,12 @@ function extractHeaders(
|
|
|
57
58
|
});
|
|
58
59
|
} else if (Array.isArray(raw)) {
|
|
59
60
|
for (const [key, value] of raw) {
|
|
60
|
-
headers[key] = value;
|
|
61
|
+
headers[key.toLowerCase()] = value;
|
|
61
62
|
}
|
|
62
63
|
} else {
|
|
63
|
-
Object.
|
|
64
|
+
for (const key of Object.keys(raw)) {
|
|
65
|
+
headers[key.toLowerCase()] = (raw as Record<string, string>)[key];
|
|
66
|
+
}
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
return headers;
|
|
@@ -73,11 +76,11 @@ async function extractBody(
|
|
|
73
76
|
input: RequestInfo | URL,
|
|
74
77
|
init?: RequestInit,
|
|
75
78
|
): Promise<unknown> {
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
// Per Fetch spec, init.body overrides Request.body when both are present
|
|
80
|
+
const bodyInit = init?.body ?? (input instanceof Request ? input.body : null);
|
|
81
|
+
if (bodyInit === null || bodyInit === undefined) return undefined;
|
|
78
82
|
|
|
79
83
|
// String body — try to parse as JSON
|
|
80
|
-
const bodyInit = init?.body;
|
|
81
84
|
if (typeof bodyInit === "string") {
|
|
82
85
|
try {
|
|
83
86
|
return JSON.parse(bodyInit);
|
|
@@ -86,8 +89,17 @@ async function extractBody(
|
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
// URLSearchParams — convert to key/value object
|
|
93
|
+
if (bodyInit instanceof URLSearchParams) {
|
|
94
|
+
const result: Record<string, string> = {};
|
|
95
|
+
bodyInit.forEach((value, key) => {
|
|
96
|
+
result[key] = value;
|
|
97
|
+
});
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
// Request with body — clone and read
|
|
90
|
-
if (input instanceof Request && input.body) {
|
|
102
|
+
if (input instanceof Request && !init?.body && input.body) {
|
|
91
103
|
try {
|
|
92
104
|
return await input.clone().json();
|
|
93
105
|
} catch {
|
|
@@ -139,8 +151,16 @@ export function createFetchInterceptor(
|
|
|
139
151
|
const path = extractPathname(urlString);
|
|
140
152
|
|
|
141
153
|
// BaseUrl filter — non-matching requests go straight to real fetch
|
|
142
|
-
|
|
143
|
-
|
|
154
|
+
// Enforce segment boundary: /api must not match /apiv2
|
|
155
|
+
if (baseUrl) {
|
|
156
|
+
const normalizedBase = baseUrl.endsWith("/")
|
|
157
|
+
? baseUrl.slice(0, -1)
|
|
158
|
+
: baseUrl;
|
|
159
|
+
const isMatch =
|
|
160
|
+
path === normalizedBase || path.startsWith(`${normalizedBase}/`);
|
|
161
|
+
if (!isMatch) {
|
|
162
|
+
return originalFetch(input, init);
|
|
163
|
+
}
|
|
144
164
|
}
|
|
145
165
|
|
|
146
166
|
// Build adapter request
|
|
@@ -491,3 +491,104 @@ describe("handle() — fuzz", () => {
|
|
|
491
491
|
);
|
|
492
492
|
});
|
|
493
493
|
});
|
|
494
|
+
|
|
495
|
+
// ============================================================
|
|
496
|
+
// Route matching round-trip properties
|
|
497
|
+
// ============================================================
|
|
498
|
+
|
|
499
|
+
describe("route matching round-trip — property", () => {
|
|
500
|
+
it("parameterized route: arbitrary param values are extracted correctly", () => {
|
|
501
|
+
fc.assert(
|
|
502
|
+
fc.asyncProperty(
|
|
503
|
+
safeSeg,
|
|
504
|
+
safeSeg,
|
|
505
|
+
safeSeg,
|
|
506
|
+
async (resource, paramA, paramB) => {
|
|
507
|
+
const mock = schmock();
|
|
508
|
+
let capturedParams: Record<string, string> = {};
|
|
509
|
+
mock(`GET /${resource}/:a/:b` as any, (ctx) => {
|
|
510
|
+
capturedParams = ctx.params;
|
|
511
|
+
return { ok: true };
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
const response = await mock.handle(
|
|
515
|
+
"GET",
|
|
516
|
+
`/${resource}/${paramA}/${paramB}`,
|
|
517
|
+
);
|
|
518
|
+
expect(response.status).toBe(200);
|
|
519
|
+
expect(capturedParams.a).toBe(paramA);
|
|
520
|
+
expect(capturedParams.b).toBe(paramB);
|
|
521
|
+
},
|
|
522
|
+
),
|
|
523
|
+
{ numRuns: 300 },
|
|
524
|
+
);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("any valid response format produces a valid Response with status/body/headers", () => {
|
|
528
|
+
const responseGen = fc.oneof(
|
|
529
|
+
// Object body
|
|
530
|
+
fc.record({ key: safeSeg }),
|
|
531
|
+
// Tuple [status, body]
|
|
532
|
+
fc.tuple(fc.integer({ min: 100, max: 599 }), fc.record({ v: safeSeg })),
|
|
533
|
+
// Tuple [status, body, headers]
|
|
534
|
+
fc.tuple(
|
|
535
|
+
fc.integer({ min: 100, max: 599 }),
|
|
536
|
+
fc.record({ v: safeSeg }),
|
|
537
|
+
fc.constant({}),
|
|
538
|
+
),
|
|
539
|
+
// null/undefined
|
|
540
|
+
fc.constant(null),
|
|
541
|
+
fc.constant(undefined),
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
fc.assert(
|
|
545
|
+
fc.asyncProperty(responseGen, async (genValue) => {
|
|
546
|
+
const mock = schmock();
|
|
547
|
+
mock("GET /test", () => genValue);
|
|
548
|
+
|
|
549
|
+
const response = await mock.handle("GET", "/test");
|
|
550
|
+
expect(typeof response.status).toBe("number");
|
|
551
|
+
expect(response.status).toBeGreaterThanOrEqual(100);
|
|
552
|
+
expect(response.status).toBeLessThanOrEqual(599);
|
|
553
|
+
expect(response).toHaveProperty("headers");
|
|
554
|
+
expect(typeof response.headers).toBe("object");
|
|
555
|
+
}),
|
|
556
|
+
{ numRuns: 300 },
|
|
557
|
+
);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("header keys are always lowercased after extractHeaders in interceptor", () => {
|
|
561
|
+
const headerKey = fc
|
|
562
|
+
.stringMatching(/^[A-Za-z][A-Za-z0-9-]{0,20}$/)
|
|
563
|
+
.filter((k) => k.length > 0);
|
|
564
|
+
const headerVal = safeSeg;
|
|
565
|
+
|
|
566
|
+
fc.assert(
|
|
567
|
+
fc.asyncProperty(headerKey, headerVal, async (key, val) => {
|
|
568
|
+
const mock = schmock();
|
|
569
|
+
let capturedHeaders: Record<string, string> = {};
|
|
570
|
+
mock("GET /hdr", (ctx) => {
|
|
571
|
+
capturedHeaders = ctx.headers;
|
|
572
|
+
return { ok: true };
|
|
573
|
+
});
|
|
574
|
+
const handle = mock.intercept();
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
await fetch("http://localhost/hdr", {
|
|
578
|
+
headers: { [key]: val },
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// All header keys should be lowercase
|
|
582
|
+
for (const hdrKey of Object.keys(capturedHeaders)) {
|
|
583
|
+
expect(hdrKey).toBe(hdrKey.toLowerCase());
|
|
584
|
+
}
|
|
585
|
+
// The value should be preserved
|
|
586
|
+
expect(capturedHeaders[key.toLowerCase()]).toBe(val);
|
|
587
|
+
} finally {
|
|
588
|
+
handle.restore();
|
|
589
|
+
}
|
|
590
|
+
}),
|
|
591
|
+
{ numRuns: 200 },
|
|
592
|
+
);
|
|
593
|
+
});
|
|
594
|
+
});
|
|
@@ -225,6 +225,80 @@ describe("response parsing", () => {
|
|
|
225
225
|
});
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
+
describe("status code boundaries", () => {
|
|
229
|
+
it("status code 100 (lower boundary) is accepted as tuple", async () => {
|
|
230
|
+
const mock = schmock();
|
|
231
|
+
mock("GET /continue", () => [100, "Continue"]);
|
|
232
|
+
|
|
233
|
+
const response = await mock.handle("GET", "/continue");
|
|
234
|
+
expect(response.status).toBe(100);
|
|
235
|
+
expect(response.body).toBe("Continue");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("status code 599 (upper boundary) is accepted as tuple", async () => {
|
|
239
|
+
const mock = schmock();
|
|
240
|
+
mock("GET /custom", () => [599, { message: "custom status" }]);
|
|
241
|
+
|
|
242
|
+
const response = await mock.handle("GET", "/custom");
|
|
243
|
+
expect(response.status).toBe(599);
|
|
244
|
+
expect(response.body).toEqual({ message: "custom status" });
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("float status like 200.5 is accepted as tuple (number check)", async () => {
|
|
248
|
+
const mock = schmock();
|
|
249
|
+
mock("GET /float", () => [200.5, { ok: true }]);
|
|
250
|
+
|
|
251
|
+
const response = await mock.handle("GET", "/float");
|
|
252
|
+
// isStatusTuple checks typeof === 'number' and 100 <= n <= 599
|
|
253
|
+
// 200.5 passes both checks, so it's treated as a tuple
|
|
254
|
+
expect(response.status).toBe(200.5);
|
|
255
|
+
expect(response.body).toEqual({ ok: true });
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("array [201, null, undefined] — tuple with undefined headers defaults to empty", async () => {
|
|
259
|
+
const mock = schmock();
|
|
260
|
+
mock("GET /null-headers", () => [201, null, undefined] as any);
|
|
261
|
+
|
|
262
|
+
const response = await mock.handle("GET", "/null-headers");
|
|
263
|
+
// isStatusTuple: length 3, first element 201 (number, 100-599) -> true
|
|
264
|
+
// destructure: [status=201, body=null, headers=undefined]
|
|
265
|
+
// headers defaults to {} via `headers = {}`
|
|
266
|
+
expect(response.status).toBe(201);
|
|
267
|
+
expect(response.body).toBeUndefined(); // null body becomes undefined
|
|
268
|
+
expect(response.headers).toEqual({});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("non-tuple array [1, 2, 3, 4] with length > 3 treated as body", async () => {
|
|
272
|
+
const mock = schmock();
|
|
273
|
+
mock("GET /long-array", () => [1, 2, 3, 4] as any);
|
|
274
|
+
|
|
275
|
+
const response = await mock.handle("GET", "/long-array");
|
|
276
|
+
// isStatusTuple requires length 2 or 3, so length 4 => not a tuple => body
|
|
277
|
+
expect(response.status).toBe(200);
|
|
278
|
+
expect(response.body).toEqual([1, 2, 3, 4]);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("status 99 (below 100) is NOT treated as a tuple", async () => {
|
|
282
|
+
const mock = schmock();
|
|
283
|
+
mock("GET /below-range", () => [99, { data: true }]);
|
|
284
|
+
|
|
285
|
+
const response = await mock.handle("GET", "/below-range");
|
|
286
|
+
// isStatusTuple requires value[0] >= 100, so 99 fails => treated as body
|
|
287
|
+
expect(response.status).toBe(200);
|
|
288
|
+
expect(response.body).toEqual([99, { data: true }]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("status 600 (above 599) is NOT treated as a tuple", async () => {
|
|
292
|
+
const mock = schmock();
|
|
293
|
+
mock("GET /above-range", () => [600, { data: true }]);
|
|
294
|
+
|
|
295
|
+
const response = await mock.handle("GET", "/above-range");
|
|
296
|
+
// isStatusTuple requires value[0] <= 599, so 600 fails => treated as body
|
|
297
|
+
expect(response.status).toBe(200);
|
|
298
|
+
expect(response.body).toEqual([600, { data: true }]);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
228
302
|
describe("edge cases", () => {
|
|
229
303
|
it("handles response with circular references gracefully", async () => {
|
|
230
304
|
const mock = schmock();
|
package/src/server.test.ts
CHANGED
|
@@ -113,4 +113,53 @@ describe("Standalone Server", () => {
|
|
|
113
113
|
mock.close();
|
|
114
114
|
expect(() => mock.close()).not.toThrow();
|
|
115
115
|
});
|
|
116
|
+
|
|
117
|
+
it("generator that throws synchronously returns 500", async () => {
|
|
118
|
+
mock = schmock();
|
|
119
|
+
mock("GET /boom", () => {
|
|
120
|
+
throw new Error("sync kaboom");
|
|
121
|
+
});
|
|
122
|
+
const info = await mock.listen(0);
|
|
123
|
+
|
|
124
|
+
const res = await fetch(`http://127.0.0.1:${info.port}/boom`);
|
|
125
|
+
expect(res.status).toBe(500);
|
|
126
|
+
const body = await res.json();
|
|
127
|
+
expect(body.error).toBe("sync kaboom");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("generator that returns rejected Promise returns 500", async () => {
|
|
131
|
+
mock = schmock();
|
|
132
|
+
mock("GET /reject", async () => {
|
|
133
|
+
throw new Error("async rejection");
|
|
134
|
+
});
|
|
135
|
+
const info = await mock.listen(0);
|
|
136
|
+
|
|
137
|
+
const res = await fetch(`http://127.0.0.1:${info.port}/reject`);
|
|
138
|
+
expect(res.status).toBe(500);
|
|
139
|
+
const body = await res.json();
|
|
140
|
+
expect(body.error).toBe("async rejection");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("close terminates keep-alive connections immediately", async () => {
|
|
144
|
+
mock = schmock();
|
|
145
|
+
mock("GET /test", { ok: true });
|
|
146
|
+
const info = await mock.listen(0);
|
|
147
|
+
|
|
148
|
+
// Make a keep-alive request to establish a persistent connection
|
|
149
|
+
const res = await fetch(`http://127.0.0.1:${info.port}/test`, {
|
|
150
|
+
headers: { connection: "keep-alive" },
|
|
151
|
+
});
|
|
152
|
+
expect(res.status).toBe(200);
|
|
153
|
+
|
|
154
|
+
// Close the server — should terminate all connections
|
|
155
|
+
mock.close();
|
|
156
|
+
|
|
157
|
+
// Subsequent request should fail immediately (not hang on keep-alive)
|
|
158
|
+
try {
|
|
159
|
+
await fetch(`http://127.0.0.1:${info.port}/test`);
|
|
160
|
+
expect.unreachable("Should have thrown");
|
|
161
|
+
} catch {
|
|
162
|
+
// Expected: connection refused
|
|
163
|
+
}
|
|
164
|
+
});
|
|
116
165
|
});
|