Skip to content

Commit ab7dff4

Browse files
committed
feat: match formdata bodies
1 parent db6aba5 commit ab7dff4

File tree

3 files changed

+102
-46
lines changed

3 files changed

+102
-46
lines changed

packages/fetch-mock/src/Matchers.ts

+27-1
Original file line numberDiff line numberDiff line change
@@ -186,13 +186,29 @@ const getExpressParamsMatcher: MatcherGenerator = ({
186186
};
187187
};
188188

189+
const formDataToObject = (formData) => {
190+
const fields = [...formData];
191+
const result = {};
192+
fields.forEach(([key, value]) => {
193+
result[key] = result[key] || [];
194+
result[key].push(value);
195+
});
196+
return result;
197+
};
198+
189199
const getBodyMatcher: MatcherGenerator = (route) => {
190-
const { body: expectedBody } = route;
200+
let { body: expectedBody } = route;
201+
let expectedBodyType = 'json';
191202

192203
if (!expectedBody) {
193204
return;
194205
}
195206

207+
if (expectedBody instanceof FormData) {
208+
expectedBodyType = 'formData';
209+
expectedBody = formDataToObject(expectedBody);
210+
}
211+
196212
return ({ options: { body, method = 'get' } }) => {
197213
if (['get', 'head', 'delete'].includes(method.toLowerCase())) {
198214
// GET requests don’t send a body so even if it exists in the options
@@ -206,9 +222,19 @@ const getBodyMatcher: MatcherGenerator = (route) => {
206222
try {
207223
if (typeof body === 'string') {
208224
sentBody = JSON.parse(body);
225+
if (expectedBodyType !== 'json') {
226+
return false;
227+
}
209228
}
210229
} catch {} //eslint-disable-line no-empty
211230

231+
if (body instanceof FormData) {
232+
if (expectedBodyType !== 'formData') {
233+
return false;
234+
}
235+
sentBody = formDataToObject(body);
236+
}
237+
212238
return (
213239
sentBody &&
214240
(route.matchPartialBody

packages/fetch-mock/src/RequestUtils.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,12 @@ export async function createCallLogFromRequest(
9696
};
9797

9898
try {
99-
derivedOptions.body = await request.clone().text();
99+
try {
100+
derivedOptions.body = await request.clone().formData();
101+
} catch {
102+
derivedOptions.body = await request.clone().text();
103+
}
100104
} catch {} // eslint-disable-line no-empty
101-
102105
if (request.headers) {
103106
derivedOptions.headers = normalizeHeaders(request.headers);
104107
}

packages/fetch-mock/src/__tests__/Matchers/body.test.js

+70-43
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ describe('body matching', () => {
297297
Headers,
298298
Response,
299299
});
300+
300301
const router = new Router({ Request, Headers }, { routes: [route] });
301302
const normalizedRequest = await createCallLogFromRequest(
302303
new Request('http://a.com/', {
@@ -328,6 +329,51 @@ describe('body matching', () => {
328329
).toBe(false);
329330
});
330331

332+
describe('multivalue fields', () => {
333+
it('match multivalue fields', async () => {
334+
const routeBody = new FormData();
335+
routeBody.append('foo', 'bar');
336+
routeBody.append('foo', 'baz');
337+
const route = new Route({ body: routeBody, response: 200 });
338+
339+
const requestBody = new FormData();
340+
requestBody.append('foo', 'bar');
341+
requestBody.append('foo', 'baz');
342+
expect(
343+
route.matcher({
344+
url: 'http://a.com/',
345+
options: {
346+
method: 'POST',
347+
body: requestBody,
348+
headers: { 'Content-Type': 'multipart/form-data' },
349+
},
350+
}),
351+
).toBe(true);
352+
});
353+
354+
it('not match mismatched multivalue fields', async () => {
355+
const routeBody = new FormData();
356+
routeBody.append('foo', 'bar');
357+
routeBody.append('foo', 'baz');
358+
const route = new Route({ body: routeBody, response: 200 });
359+
360+
const requestBody = new FormData();
361+
requestBody.append('foo', 'bar');
362+
requestBody.append('foo', 'baz');
363+
requestBody.append('foo', 'barry');
364+
expect(
365+
route.matcher({
366+
url: 'http://a.com/',
367+
options: {
368+
method: 'POST',
369+
body: requestBody,
370+
headers: { 'Content-Type': 'multipart/form-data' },
371+
},
372+
}),
373+
).toBe(false);
374+
});
375+
});
376+
331377
it('should not match if body sent isn’t FormData', () => {
332378
const route = new Route({ body: constructFormData(), response: 200 });
333379
expect(
@@ -367,46 +413,37 @@ describe('body matching', () => {
367413
).toBe(true);
368414
});
369415

370-
describe.skip('partial body matching', () => {
416+
describe('partial body matching', () => {
371417
it('match when missing properties', () => {
372418
const route = new Route({
373-
body: { ham: 'sandwich' },
419+
body: constructFormData(),
374420
matchPartialBody: true,
375421
response: 200,
376422
});
377-
expect(
378-
route.matcher({
379-
url: 'http://a.com',
380-
options: {
381-
method: 'POST',
382-
body: JSON.stringify({ ham: 'sandwich', egg: 'mayonaise' }),
383-
},
384-
}),
385-
).toBe(true);
386-
});
387423

388-
it('match when missing nested properties', () => {
389-
const route = new Route({
390-
body: { meal: { ham: 'sandwich' } },
391-
matchPartialBody: true,
392-
response: 200,
393-
});
424+
const requestBody = constructFormData();
425+
requestBody.append('fuzz', 'ball');
394426
expect(
395427
route.matcher({
396428
url: 'http://a.com',
397429
options: {
398430
method: 'POST',
399-
body: JSON.stringify({
400-
meal: { ham: 'sandwich', egg: 'mayonaise' },
401-
}),
431+
body: requestBody,
402432
},
403433
}),
404434
).toBe(true);
405435
});
406436

407-
it('not match when properties at wrong depth', () => {
437+
it('match when starting subset of multivalue field', () => {
438+
const routeBody = new FormData();
439+
routeBody.append('foo', 'bar');
440+
routeBody.append('foo', 'baz');
441+
const requestBody = new FormData();
442+
requestBody.append('foo', 'bar');
443+
requestBody.append('foo', 'baz');
444+
requestBody.append('foo', 'barry');
408445
const route = new Route({
409-
body: { ham: 'sandwich' },
446+
body: routeBody,
410447
matchPartialBody: true,
411448
response: 200,
412449
});
@@ -415,32 +452,22 @@ describe('body matching', () => {
415452
url: 'http://a.com',
416453
options: {
417454
method: 'POST',
418-
body: JSON.stringify({ meal: { ham: 'sandwich' } }),
419-
},
420-
}),
421-
).toBe(false);
422-
});
423-
424-
it('match when starting subset of array', () => {
425-
const route = new Route({
426-
body: { ham: [1, 2] },
427-
matchPartialBody: true,
428-
response: 200,
429-
});
430-
expect(
431-
route.matcher({
432-
url: 'http://a.com',
433-
options: {
434-
method: 'POST',
435-
body: JSON.stringify({ ham: [1, 2, 3] }),
455+
body: requestBody,
436456
},
437457
}),
438458
).toBe(true);
439459
});
440460

441461
it('match when subset of array has gaps', () => {
462+
const routeBody = new FormData();
463+
routeBody.append('foo', 'bar');
464+
routeBody.append('foo', 'barry');
465+
const requestBody = new FormData();
466+
requestBody.append('foo', 'bar');
467+
requestBody.append('foo', 'baz');
468+
requestBody.append('foo', 'barry');
442469
const route = new Route({
443-
body: { ham: [1, 3] },
470+
body: routeBody,
444471
matchPartialBody: true,
445472
response: 200,
446473
});
@@ -449,7 +476,7 @@ describe('body matching', () => {
449476
url: 'http://a.com',
450477
options: {
451478
method: 'POST',
452-
body: JSON.stringify({ ham: [1, 2, 3] }),
479+
body: requestBody,
453480
},
454481
}),
455482
).toBe(true);

0 commit comments

Comments
 (0)