Skip to content

Commit 1b6e988

Browse files
authored
Added new attach helper for sending formdata (#203)
- If you want to send formdata via the body() method you can - But this requires using the formdata API correctly yourself - Note: the form-data module and Native FormData api aren't compatible - This new helper allows you to attach files using the same API as supertest - This will result in easier-to-read tests with less dependencies
1 parent 2a69a94 commit 1b6e988

File tree

7 files changed

+157
-6
lines changed

7 files changed

+157
-6
lines changed

packages/express-test/lib/Request.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const {default: reqresnext} = require('reqresnext');
22
const {CookieAccessInfo} = require('cookiejar');
33
const {parse} = require('url');
4-
const {isJSON} = require('./utils');
4+
const {isJSON, attachFile} = require('./utils');
55
const {Readable} = require('stream');
66

77
class RequestOptions {
@@ -24,6 +24,11 @@ class Request {
2424
this.cookieJar = cookieJar;
2525
}
2626

27+
attach(name, filePath) {
28+
const formData = attachFile(name, filePath);
29+
return this.body(formData);
30+
}
31+
2732
/**
2833
* @param {object|string|FormData|Buffer} body Set the request body object or FormData (for multipart/form-data)
2934
* @returns

packages/express-test/lib/utils.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
module.exports.isJSON = function isJSON(mime) {
1+
const fs = require('fs');
2+
const path = require('path');
3+
const mime = require('mime-types');
4+
const FormData = require('form-data');
5+
6+
module.exports.isJSON = function isJSON(mimeType) {
27
// should match /json or +json
38
// but not /json-seq
4-
return /[/+]json($|[^-\w])/i.test(mime);
9+
return /[/+]json($|[^-\w])/i.test(mimeType);
510
};
611

712
module.exports.normalizeURL = function normalizeURL(toNormalize) {
@@ -15,3 +20,17 @@ module.exports.normalizeURL = function normalizeURL(toNormalize) {
1520

1621
return normalized;
1722
};
23+
24+
module.exports.attachFile = function attachFile(name, filePath) {
25+
const formData = new FormData();
26+
const fileContent = fs.readFileSync(filePath);
27+
const filename = path.basename(filePath);
28+
const contentType = mime.lookup(filePath) || 'application/octet-stream';
29+
30+
formData.append(name, fileContent, {
31+
filename,
32+
contentType
33+
});
34+
35+
return formData;
36+
};

packages/express-test/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@
2222
"c8": "10.1.3",
2323
"express": "4.21.1",
2424
"express-session": "1.18.1",
25-
"form-data": "^4.0.0",
2625
"mocha": "10.7.3",
2726
"multer": "^1.4.5-lts.1",
2827
"sinon": "18.0.0"
2928
},
3029
"dependencies": {
3130
"@tryghost/jest-snapshot": "^0.5.17",
3231
"cookiejar": "^2.1.3",
32+
"form-data": "^4.0.2",
33+
"mime-types": "^3.0.1",
3334
"reqresnext": "^1.7.0"
3435
}
3536
}

packages/express-test/test/example-app.test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,31 @@ describe('Example App', function () {
360360
}
361361
});
362362

363+
it('can upload a file using the attach method', async function () {
364+
const fileContents = await fs.readFile(__dirname + '/fixtures/ghost-favicon.png');
365+
366+
const {body} = await agent
367+
.post('/api/upload/')
368+
.attach('image', __dirname + '/fixtures/ghost-favicon.png')
369+
.expectStatus(200);
370+
371+
assert.equal(body.originalname, 'ghost-favicon.png');
372+
assert.equal(body.mimetype, 'image/png');
373+
assert.equal(body.size, fileContents.length);
374+
assert.equal(body.fieldname, 'image');
375+
376+
// Do a real comparison in the uploaded file to check if it was uploaded and saved correctly
377+
const uploadedFileContents = await fs.readFile(body.path);
378+
assert.deepEqual(uploadedFileContents, fileContents);
379+
380+
// Delete the file
381+
try {
382+
await fs.unlink(body.path);
383+
} catch (e) {
384+
// ignore if this fails
385+
}
386+
});
387+
363388
it('can stream body', async function () {
364389
const stat = await fs.stat(__dirname + '/fixtures/long-json-body.json');
365390
const stream = createReadStream(__dirname + '/fixtures/long-json-body.json');
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test content for file upload

packages/express-test/test/utils.test.js

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const {assert} = require('./utils');
2+
const path = require('path');
3+
const FormData = require('form-data');
24

3-
const {isJSON, normalizeURL} = require('../lib/utils');
5+
const {isJSON, normalizeURL, attachFile} = require('../lib/utils');
46

57
describe('Utils', function () {
68
it('isJSON', function () {
@@ -30,4 +32,90 @@ describe('Utils', function () {
3032
assert.equal(url, 'http://example.com/?yolo=9000');
3133
});
3234
});
35+
36+
describe('attachFile', function () {
37+
it('creates FormData with file content', function () {
38+
const filePath = path.join(__dirname, 'fixtures/test-file.txt');
39+
const formData = attachFile('testfile', filePath);
40+
41+
assert.equal(formData instanceof FormData, true);
42+
assert.equal(typeof formData.getHeaders, 'function');
43+
assert.equal(typeof formData.getBuffer, 'function');
44+
});
45+
46+
it('sets correct filename from path', function () {
47+
const filePath = path.join(__dirname, 'fixtures/test-file.txt');
48+
const formData = attachFile('testfile', filePath);
49+
50+
// FormData doesn't expose a direct way to check the filename,
51+
// but we can verify it was created without errors
52+
const headers = formData.getHeaders();
53+
assert.match(headers['content-type'], /^multipart\/form-data; boundary=/);
54+
});
55+
56+
it('sets correct content type for text file', function () {
57+
const filePath = path.join(__dirname, 'fixtures/test-file.txt');
58+
const formData = attachFile('testfile', filePath);
59+
60+
// Get the form data buffer to check it contains the right content type
61+
const buffer = formData.getBuffer();
62+
const content = buffer.toString();
63+
64+
// Check that the form data contains the expected content type
65+
assert.match(content, /Content-Type: text\/plain/);
66+
});
67+
68+
it('sets correct content type for PNG image', function () {
69+
const filePath = path.join(__dirname, 'fixtures/ghost-favicon.png');
70+
const formData = attachFile('image', filePath);
71+
72+
// Get the form data buffer to check it contains the right content type
73+
const buffer = formData.getBuffer();
74+
const content = buffer.toString();
75+
76+
// Check that the form data contains the expected content type
77+
assert.match(content, /Content-Type: image\/png/);
78+
});
79+
80+
it('uses default content type for unknown file extension', function () {
81+
// Create a file with unknown extension
82+
const fs = require('fs');
83+
const unknownFile = path.join(__dirname, 'fixtures/test.unknown');
84+
fs.writeFileSync(unknownFile, 'test content');
85+
86+
try {
87+
const formData = attachFile('file', unknownFile);
88+
const buffer = formData.getBuffer();
89+
const content = buffer.toString();
90+
91+
// Check that the form data contains the default content type
92+
assert.match(content, /Content-Type: application\/octet-stream/);
93+
} finally {
94+
// Clean up
95+
fs.unlinkSync(unknownFile);
96+
}
97+
});
98+
99+
it('includes the correct field name', function () {
100+
const filePath = path.join(__dirname, 'fixtures/test-file.txt');
101+
const formData = attachFile('myfield', filePath);
102+
103+
const buffer = formData.getBuffer();
104+
const content = buffer.toString();
105+
106+
// Check that the form data contains the field name
107+
assert.match(content, /Content-Disposition: form-data; name="myfield"/);
108+
});
109+
110+
it('includes the file content', function () {
111+
const filePath = path.join(__dirname, 'fixtures/test-file.txt');
112+
const formData = attachFile('testfile', filePath);
113+
114+
const buffer = formData.getBuffer();
115+
const content = buffer.toString();
116+
117+
// Check that the form data contains the file content
118+
assert.match(content, /test content for file upload/);
119+
});
120+
});
33121
});

yarn.lock

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5243,7 +5243,7 @@ form-data-encoder@^2.1.2:
52435243
resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5"
52445244
integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==
52455245

5246-
form-data@^4.0.0:
5246+
form-data@^4.0.0, form-data@^4.0.2:
52475247
version "4.0.2"
52485248
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c"
52495249
integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==
@@ -7069,13 +7069,25 @@ [email protected]:
70697069
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
70707070
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
70717071

7072+
mime-db@^1.54.0:
7073+
version "1.54.0"
7074+
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5"
7075+
integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
7076+
70727077
mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34:
70737078
version "2.1.35"
70747079
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
70757080
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
70767081
dependencies:
70777082
mime-db "1.52.0"
70787083

7084+
mime-types@^3.0.1:
7085+
version "3.0.1"
7086+
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce"
7087+
integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==
7088+
dependencies:
7089+
mime-db "^1.54.0"
7090+
70797091
70807092
version "1.6.0"
70817093
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"

0 commit comments

Comments
 (0)