Skip to content

Commit dc78d00

Browse files
authored
feat: Pass more detail to response creator functions (#67)
1 parent 9add18f commit dc78d00

File tree

6 files changed

+414
-8
lines changed

6 files changed

+414
-8
lines changed

docs/src/content/docs/mock-servers/extended-response-patterns.mdx

+40
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,43 @@ server.get("/users/123", async request => {
161161
```
162162

163163
This route will respond to any GET request to `/users/123` with a status code of 200 and a JSON response body containing an `id` and `name` property, but it will wait for 1 second before returning the response. You can use this to simulate network latency or other asynchronous behavior when the `delay` key is not sufficient.
164+
165+
### Access request context in response creators
166+
167+
Response creator functions receive a second argument that provides access to additional request context information. This object contains:
168+
169+
- `cookies` - A `Map` containing any cookies sent with the request
170+
- `params` - An object containing any URL parameters matched in the route pattern
171+
- `query` - A `URLSearchParams` object containing any query string parameters
172+
173+
Here's an example showing how to use this context:
174+
175+
```js
176+
import { MockServer } from "mentoss";
177+
178+
const server = new MockServer("https://api.example.com");
179+
180+
// Match URLs like /users/123
181+
server.get("/users/:id", (request, { cookies, params, query }) => {
182+
183+
// Access cookies
184+
const sessionId = cookies.get("sessionId");
185+
186+
// Access URL parameters
187+
const userId = params.id;
188+
189+
// Access query string parameters
190+
const format = query.get("format") ?? "json";
191+
192+
return {
193+
status: 200,
194+
body: {
195+
userId,
196+
sessionId,
197+
format
198+
}
199+
};
200+
});
201+
```
202+
203+
The context argument makes it easy to implement realistic API behaviors that depend on these request details.

src/mock-server.js

+32-1
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,26 @@ function assertValidResponsePattern(responsePattern) {
132132
}
133133
}
134134

135+
/**
136+
* Parses cookies from a Cookie header value
137+
* @param {string|null} cookieHeader The Cookie header value
138+
* @returns {Map<string,string>} A map of cookie names to values
139+
*/
140+
function parseCookies(cookieHeader) {
141+
const cookies = new Map();
142+
143+
if (!cookieHeader) {
144+
return cookies;
145+
}
146+
147+
cookieHeader.split(";").forEach(cookie => {
148+
const [name, value] = cookie.trim().split("=");
149+
cookies.set(decodeURIComponent(name), decodeURIComponent(value));
150+
});
151+
152+
return cookies;
153+
}
154+
135155
/**
136156
* Represents a route that the server can respond to.
137157
*/
@@ -209,8 +229,19 @@ export class Route {
209229
* @returns {Promise<Response>} The response to return.
210230
*/
211231
async createResponse(request, PreferredResponse) {
232+
const requestMatch = this.#matcher.traceMatches({
233+
method: request.method,
234+
url: request.url,
235+
headers: Object.fromEntries([...request.headers.entries()]),
236+
});
237+
238+
const cookies = parseCookies(request.headers.get('cookie'));
239+
const response = await this.#createResponse(request, {
240+
cookies,
241+
params: requestMatch.params,
242+
query: requestMatch.query,
243+
});
212244

213-
const response = await this.#createResponse(request);
214245
const { body, delay, ...init } = typeof response === "number" ? { status: response } : response;
215246

216247
if (!init.status) {

src/request-matcher.js

+41-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* @author Nicholas C. Zakas
44
*/
55

6-
/* globals FormData, URLPattern */
6+
/* globals FormData, URLPattern, URLSearchParams */
77

88
//-----------------------------------------------------------------------------
99
// Imports
@@ -117,32 +117,38 @@ export class RequestMatcher {
117117
* Checks if the request matches the matcher. Traces all of the details to help
118118
* with debugging.
119119
* @param {RequestPattern} request The request to check.
120-
* @returns {{matches:boolean, messages:string[]}} True if the request matches, false if not.
120+
* @returns {{matches:boolean, messages:string[], params:Record<string, string|undefined>, query:URLSearchParams}} True if the request matches, false if not.
121121
*/
122122
traceMatches(request) {
123+
123124
/*
124125
* Check the URL first. This is helpful for tracing when requests don't match
125126
* because people more typically get the method wrong rather than the URL.
126127
*/
127-
// then check the URL
128128
const urlMatch = this.#pattern.exec(request.url);
129129
if (!urlMatch) {
130130
return {
131131
matches: false,
132132
messages: ["❌ URL does not match."],
133+
params: {},
134+
query: new URLSearchParams(),
133135
};
134136
}
135137

136138
const messages = ["✅ URL matches."];
139+
const params = urlMatch.pathname.groups;
140+
const query = new URL(request.url).searchParams;
137141

138-
// first check the method
142+
// Method check
139143
if (request.method.toLowerCase() !== this.#method.toLowerCase()) {
140144
return {
141145
matches: false,
142146
messages: [
143147
...messages,
144148
`❌ Method does not match. Expected ${this.#method.toUpperCase()} but received ${request.method.toUpperCase()}.`,
145-
],
149+
],
150+
params,
151+
query,
146152
};
147153
}
148154

@@ -161,6 +167,8 @@ export class RequestMatcher {
161167
...messages,
162168
"❌ Query string does not match. Expected query string but received none.",
163169
],
170+
params,
171+
query,
164172
};
165173
}
166174

@@ -172,6 +180,8 @@ export class RequestMatcher {
172180
...messages,
173181
`❌ Query string does not match. Expected ${key}=${value} but received ${key}=${actualQuery[key]}.`,
174182
],
183+
params,
184+
query,
175185
};
176186
}
177187
}
@@ -190,6 +200,8 @@ export class RequestMatcher {
190200
...messages,
191201
"❌ URL parameters do not match. Expected parameters but received none.",
192202
],
203+
params,
204+
query,
193205
};
194206
}
195207

@@ -201,6 +213,8 @@ export class RequestMatcher {
201213
...messages,
202214
`❌ URL parameters do not match. Expected ${key}=${value} but received ${key}=${actualParams[key]}.`,
203215
],
216+
params,
217+
query,
204218
};
205219
}
206220
}
@@ -228,6 +242,8 @@ export class RequestMatcher {
228242
...messages,
229243
`❌ Headers do not match. Expected ${key}=${value} but received ${key}=${actualValue ? actualValue[1] : "none"}.`,
230244
],
245+
params,
246+
query,
231247
};
232248
}
233249
}
@@ -245,6 +261,8 @@ export class RequestMatcher {
245261
...messages,
246262
"❌ Body does not match. Expected body but received none.",
247263
],
264+
params,
265+
query,
248266
};
249267
}
250268

@@ -256,6 +274,8 @@ export class RequestMatcher {
256274
...messages,
257275
`❌ Body does not match. Expected ${this.#body} but received ${request.body}`,
258276
],
277+
params,
278+
query,
259279
};
260280
}
261281

@@ -268,6 +288,8 @@ export class RequestMatcher {
268288
...messages,
269289
"❌ Body does not match. Expected FormData but received none.",
270290
],
291+
params,
292+
query,
271293
};
272294
}
273295

@@ -279,6 +301,8 @@ export class RequestMatcher {
279301
...messages,
280302
`❌ Body does not match. Expected ${key}=${value} but received ${key}=${request.body.get(key)}.`,
281303
],
304+
params,
305+
query,
282306
};
283307
}
284308
}
@@ -292,6 +316,8 @@ export class RequestMatcher {
292316
...messages,
293317
`❌ Body does not match. Expected ArrayBuffer but received ${request.body.constructor.name}.`,
294318
],
319+
params,
320+
query,
295321
};
296322
}
297323

@@ -303,6 +329,8 @@ export class RequestMatcher {
303329
...messages,
304330
`❌ Body does not match. Expected array buffer byte length ${this.#body.byteLength} but received ${request.body.byteLength}`,
305331
],
332+
params,
333+
query,
306334
};
307335
}
308336

@@ -318,6 +346,8 @@ export class RequestMatcher {
318346
...messages,
319347
`❌ Body does not match. Expected byte ${i} to be ${expectedBody[i]} but received ${actualBody[i]}.`,
320348
],
349+
params,
350+
query,
321351
};
322352
}
323353
}
@@ -332,6 +362,8 @@ export class RequestMatcher {
332362
...messages,
333363
"❌ Body does not match. Expected object but received none.",
334364
],
365+
params,
366+
query,
335367
};
336368
}
337369

@@ -343,6 +375,8 @@ export class RequestMatcher {
343375
...messages,
344376
`❌ Body does not match. Expected ${JSON.stringify(this.#body)} but received ${JSON.stringify(request.body)}.`,
345377
],
378+
params,
379+
query,
346380
};
347381
}
348382

@@ -353,6 +387,8 @@ export class RequestMatcher {
353387
return {
354388
matches: true,
355389
messages,
390+
params,
391+
query,
356392
};
357393
}
358394

src/types.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,33 @@ export interface ResponsePattern {
3838
delay?: number;
3939
}
4040

41+
/**
42+
* Additional information that's helpful for evaluating a request.
43+
*/
44+
export interface RequestInfo {
45+
46+
/**
47+
* The cookies sent with the request.
48+
*/
49+
cookies: Map<string, string>;
50+
51+
/**
52+
* The URL parameters found in the request.
53+
*/
54+
params: Record<string, string|undefined>;
55+
56+
/**
57+
* The query parameters found in the request.
58+
*/
59+
query: URLSearchParams;
60+
}
61+
4162
/**
4263
* Create a response based on the request.
4364
* @param request The request to create a response for.
4465
* @returns The response to send back.
4566
*/
46-
export type ResponseCreator = (request: Request) => ResponsePattern | number | Promise<ResponsePattern> | Promise<number>;
67+
export type ResponseCreator = (request: Request, requestInfo: RequestInfo) => ResponsePattern | number | Promise<ResponsePattern> | Promise<number>;
4768

4869
export interface Credentials {
4970
/**

tests/mock-server.test.js

+57-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* @author Nicholas C. Zakas
44
*/
55

6-
/* globals FormData, Request, TextEncoder */
6+
/* globals FormData, Request, TextEncoder, URLSearchParams */
77

88
//-----------------------------------------------------------------------------
99
// Imports
@@ -330,6 +330,62 @@ describe("MockServer", () => {
330330
await server.receive(request);
331331
});
332332

333+
it("should pass request info object as second parameter to response creator", async () => {
334+
server.get("/users/:id", (request, info) => {
335+
assert.ok(info.cookies instanceof Map);
336+
assert.ok(info.query instanceof URLSearchParams);
337+
assert.strictEqual(typeof info.params, "object");
338+
339+
assert.strictEqual(info.cookies.get("session"), "abc123");
340+
assert.strictEqual(info.params.id, "123");
341+
assert.strictEqual(info.query.get("sort"), "asc");
342+
343+
return { status: 200, body: "OK" };
344+
});
345+
346+
const request = createRequest({
347+
method: "GET",
348+
url: `${BASE_URL}/users/123?sort=asc`,
349+
headers: {
350+
Cookie: "session=abc123"
351+
}
352+
});
353+
354+
const response = await server.receive(request);
355+
assert.strictEqual(response.status, 200);
356+
});
357+
358+
it("should provide empty cookies map when no cookies present", async () => {
359+
server.get("/test", (request, info) => {
360+
assert.strictEqual(info.cookies.size, 0);
361+
return { status: 200 };
362+
});
363+
364+
const request = createRequest({
365+
method: "GET",
366+
url: `${BASE_URL}/test`
367+
});
368+
369+
await server.receive(request);
370+
});
371+
372+
it("should parse multiple cookies correctly", async () => {
373+
server.get("/test", (request, info) => {
374+
assert.strictEqual(info.cookies.get("session"), "abc123");
375+
assert.strictEqual(info.cookies.get("user"), "john");
376+
return { status: 200 };
377+
});
378+
379+
const request = createRequest({
380+
method: "GET",
381+
url: `${BASE_URL}/test`,
382+
headers: {
383+
Cookie: "session=abc123; user=john"
384+
}
385+
});
386+
387+
await server.receive(request);
388+
});
333389
});
334390
});
335391

0 commit comments

Comments
 (0)