Skip to content

Commit 4ddee81

Browse files
authored
feat: support multipart request bodies (#47)
* feat: support multipart request bodies * chore: fix typescripts
1 parent dd98515 commit 4ddee81

File tree

6 files changed

+124
-10
lines changed

6 files changed

+124
-10
lines changed

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"find-my-way": "^9.2.0",
3939
"js-yaml": "^4.1.0",
4040
"lodash-es": "^4.17.21",
41+
"parse-multipart-data": "^1.5.0",
4142
"patch-package": "^8.0.0",
4243
"qs": "^6.14.0"
4344
},

src/__tests__/fixtures/request-body/baseline.json

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
11
[
2+
{
3+
"code": "request.body.incompatible",
4+
"message": "Request body is incompatible with the request body schema in the spec file: must be object",
5+
"mockDetails": {
6+
"interactionDescription": "should pass on multipart bodies",
7+
"interactionState": "[none]",
8+
"location": "[root].interactions[3].request.body",
9+
"value": "------xxx\r\nContent-Disposition: form-data; name=\"email\"\r\n\r\n[email protected]\r\n------xxx\r\nContent-Disposition: form-data; name=\"password\"\r\n\r\npassword\r\n------xxx--"
10+
},
11+
"specDetails": {
12+
"location": "[root].paths./login.post.requestBody.content.multipart/form-data.schema.type",
13+
"pathMethod": "post",
14+
"pathName": "/login",
15+
"value": "object"
16+
},
17+
"type": "error"
18+
},
219
{
320
"code": "request.content-type.missing",
421
"message": "Request content type header is not defined but spec specifies mime-types to consume",
522
"mockDetails": {
623
"interactionDescription": "should pass for 4xx responses even with missing content types",
724
"interactionState": "[none]",
8-
"location": "[root].interactions[4]",
25+
"location": "[root].interactions[5]",
926
"value": {
1027
"description": "should pass for 4xx responses even with missing content types",
1128
"request": {
@@ -37,7 +54,7 @@
3754
"mockDetails": {
3855
"interactionDescription": "should error with missing body",
3956
"interactionState": "[none]",
40-
"location": "[root].interactions[5].request.body"
57+
"location": "[root].interactions[6].request.body"
4158
},
4259
"specDetails": {
4360
"location": "[root].paths./login.post.requestBody.content.application/json.schema.type",
@@ -53,7 +70,7 @@
5370
"mockDetails": {
5471
"interactionDescription": "should error on empty body",
5572
"interactionState": "[none]",
56-
"location": "[root].interactions[6].request.body",
73+
"location": "[root].interactions[7].request.body",
5774
"value": ""
5875
},
5976
"specDetails": {
@@ -70,7 +87,7 @@
7087
"mockDetails": {
7188
"interactionDescription": "should error on schema mismatch for json body",
7289
"interactionState": "[none]",
73-
"location": "[root].interactions[7].request.body",
90+
"location": "[root].interactions[8].request.body",
7491
"value": {
7592
"email": "[email protected]"
7693
}
@@ -92,7 +109,7 @@
92109
"mockDetails": {
93110
"interactionDescription": "should error on schema mismatch for form body",
94111
"interactionState": "[none]",
95-
"location": "[root].interactions[8].request.body",
112+
"location": "[root].interactions[9].request.body",
96113
"value": "[email protected]"
97114
},
98115
"specDetails": {
@@ -105,5 +122,22 @@
105122
]
106123
},
107124
"type": "error"
125+
},
126+
{
127+
"code": "request.body.incompatible",
128+
"message": "Request body is incompatible with the request body schema in the spec file: must be object",
129+
"mockDetails": {
130+
"interactionDescription": "should error on schema mismatch for multipart body",
131+
"interactionState": "[none]",
132+
"location": "[root].interactions[10].request.body",
133+
"value": "------xxx\r\nContent-Disposition: form-data; name=\"email\"\r\n\r\n[email protected]\r\n------xxx--"
134+
},
135+
"specDetails": {
136+
"location": "[root].paths./login.post.requestBody.content.multipart/form-data.schema.type",
137+
"pathMethod": "post",
138+
"pathName": "/login",
139+
"value": "object"
140+
},
141+
"type": "error"
108142
}
109143
]

src/__tests__/fixtures/request-body/pact.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@
5353
"body": {}
5454
}
5555
},
56+
{
57+
"description": "should pass on multipart bodies",
58+
"request": {
59+
"method": "POST",
60+
"path": "/login",
61+
"headers": {
62+
"Content-Type": "multipart/form-data;boundary=----xxx"
63+
},
64+
"body": "------xxx\r\nContent-Disposition: form-data; name=\"email\"\r\n\r\n[email protected]\r\n------xxx\r\nContent-Disposition: form-data; name=\"password\"\r\n\r\npassword\r\n------xxx--"
65+
},
66+
"response": {
67+
"status": 201,
68+
"body": {}
69+
}
70+
},
5671
{
5772
"description": "should pass for 4xx responses",
5873
"request": {
@@ -137,6 +152,21 @@
137152
"status": 201,
138153
"body": {}
139154
}
155+
},
156+
{
157+
"description": "should error on schema mismatch for multipart body",
158+
"request": {
159+
"method": "POST",
160+
"path": "/login",
161+
"headers": {
162+
"Content-Type": "multipart/form-data;boundary=----xxx"
163+
},
164+
"body": "------xxx\r\nContent-Disposition: form-data; name=\"email\"\r\n\r\n[email protected]\r\n------xxx--"
165+
},
166+
"response": {
167+
"status": 201,
168+
"body": {}
169+
}
140170
}
141171
]
142172
}

src/__tests__/fixtures/request-body/results.json

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"mockDetails": {
66
"interactionDescription": "should pass for 4xx responses even with missing content types",
77
"interactionState": "[none]",
8-
"location": "[root].interactions[4].request.headers.content-type",
8+
"location": "[root].interactions[5].request.headers.content-type",
99
"value": null
1010
},
1111
"specDetails": {
@@ -61,7 +61,7 @@
6161
"mockDetails": {
6262
"interactionDescription": "should error with missing body",
6363
"interactionState": "[none]",
64-
"location": "[root].interactions[5].request.body"
64+
"location": "[root].interactions[6].request.body"
6565
},
6666
"specDetails": {
6767
"location": "[root].paths./login.post.requestBody.content.application/json.schema.components.schemas.request.type",
@@ -77,7 +77,7 @@
7777
"mockDetails": {
7878
"interactionDescription": "should error on empty body",
7979
"interactionState": "[none]",
80-
"location": "[root].interactions[6].request.body",
80+
"location": "[root].interactions[7].request.body",
8181
"value": ""
8282
},
8383
"specDetails": {
@@ -94,7 +94,7 @@
9494
"mockDetails": {
9595
"interactionDescription": "should error on schema mismatch for json body",
9696
"interactionState": "[none]",
97-
"location": "[root].interactions[7].request.body",
97+
"location": "[root].interactions[8].request.body",
9898
"value": {
9999
"email": "[email protected]"
100100
}
@@ -116,7 +116,7 @@
116116
"mockDetails": {
117117
"interactionDescription": "should error on schema mismatch for form body",
118118
"interactionState": "[none]",
119-
"location": "[root].interactions[8].request.body",
119+
"location": "[root].interactions[9].request.body",
120120
"value": {
121121
"email": "[email protected]"
122122
}
@@ -131,5 +131,27 @@
131131
]
132132
},
133133
"type": "error"
134+
},
135+
{
136+
"code": "request.body.incompatible",
137+
"message": "Request body is incompatible with the request body schema in the spec file: must have required property 'password'",
138+
"mockDetails": {
139+
"interactionDescription": "should error on schema mismatch for multipart body",
140+
"interactionState": "[none]",
141+
"location": "[root].interactions[10].request.body",
142+
"value": {
143+
"email": "[email protected]"
144+
}
145+
},
146+
"specDetails": {
147+
"location": "[root].paths./login.post.requestBody.content.multipart/form-data.schema.components.schemas.request.required",
148+
"pathMethod": "post",
149+
"pathName": "/login",
150+
"value": [
151+
"email",
152+
"password"
153+
]
154+
},
155+
"type": "error"
134156
}
135157
]

src/compare/requestBody.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { OpenAPIV2 } from "openapi-types";
44
import type Router from "find-my-way";
55
import { get } from "lodash-es";
66
import qs from "qs";
7+
import multipart from "parse-multipart-data";
78

89
import type { Interaction } from "../documents/pact";
910
import type { Result } from "../results/index";
@@ -27,6 +28,25 @@ const parseBody = (body: unknown, contentType: string) => {
2728
return qs.parse(body as string, { allowDots: true, comma: true });
2829
}
2930

31+
if (contentType.includes("multipart/form-data") && typeof body === "string") {
32+
const match = contentType.match(/boundary=(.*)/);
33+
const boundary = match?.[1];
34+
35+
if (boundary) {
36+
const parts = multipart.parse(Buffer.from(body), boundary) as {
37+
name: string;
38+
data: Buffer;
39+
}[];
40+
return parts.reduce(
41+
(acc, part) => {
42+
acc[part.name] = part.data.toString();
43+
return acc;
44+
},
45+
{} as Record<string, string>,
46+
);
47+
}
48+
}
49+
3050
return body;
3151
};
3252

0 commit comments

Comments
 (0)