Merge tag '4.19.0' into 5.x
[express.git] / test / res.download.js
blobf7d795d57c002016e9aaea04fe86cb1d42b8a8ca
1 'use strict'
3 var after = require('after');
4 var assert = require('assert')
5 var asyncHooks = tryRequire('async_hooks')
6 var Buffer = require('safe-buffer').Buffer
7 var express = require('..');
8 var path = require('path')
9 var request = require('supertest');
10 var utils = require('./support/utils')
12 var FIXTURES_PATH = path.join(__dirname, 'fixtures')
14 var describeAsyncHooks = typeof asyncHooks.AsyncLocalStorage === 'function'
15   ? describe
16   : describe.skip
18 describe('res', function(){
19   describe('.download(path)', function(){
20     it('should transfer as an attachment', function(done){
21       var app = express();
23       app.use(function(req, res){
24         res.download('test/fixtures/user.html');
25       });
27       request(app)
28       .get('/')
29       .expect('Content-Type', 'text/html; charset=utf-8')
30       .expect('Content-Disposition', 'attachment; filename="user.html"')
31       .expect(200, '<p>{{user.name}}</p>', done)
32     })
34     it('should accept range requests', function (done) {
35       var app = express()
37       app.get('/', function (req, res) {
38         res.download('test/fixtures/user.html')
39       })
41       request(app)
42         .get('/')
43         .expect('Accept-Ranges', 'bytes')
44         .expect(200, '<p>{{user.name}}</p>', done)
45     })
47     it('should respond with requested byte range', function (done) {
48       var app = express()
50       app.get('/', function (req, res) {
51         res.download('test/fixtures/user.html')
52       })
54       request(app)
55         .get('/')
56         .set('Range', 'bytes=0-2')
57         .expect('Content-Range', 'bytes 0-2/20')
58         .expect(206, '<p>', done)
59     })
60   })
62   describe('.download(path, filename)', function(){
63     it('should provide an alternate filename', function(done){
64       var app = express();
66       app.use(function(req, res){
67         res.download('test/fixtures/user.html', 'document');
68       });
70       request(app)
71       .get('/')
72       .expect('Content-Type', 'text/html; charset=utf-8')
73       .expect('Content-Disposition', 'attachment; filename="document"')
74       .expect(200, done)
75     })
76   })
78   describe('.download(path, fn)', function(){
79     it('should invoke the callback', function(done){
80       var app = express();
81       var cb = after(2, done);
83       app.use(function(req, res){
84         res.download('test/fixtures/user.html', cb);
85       });
87       request(app)
88       .get('/')
89       .expect('Content-Type', 'text/html; charset=utf-8')
90       .expect('Content-Disposition', 'attachment; filename="user.html"')
91       .expect(200, cb);
92     })
94     describeAsyncHooks('async local storage', function () {
95       it('should presist store', function (done) {
96         var app = express()
97         var cb = after(2, done)
98         var store = { foo: 'bar' }
100         app.use(function (req, res, next) {
101           req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage()
102           req.asyncLocalStorage.run(store, next)
103         })
105         app.use(function (req, res) {
106           res.download('test/fixtures/name.txt', function (err) {
107             if (err) return cb(err)
109             var local = req.asyncLocalStorage.getStore()
111             assert.strictEqual(local.foo, 'bar')
112             cb()
113           })
114         })
116         request(app)
117           .get('/')
118           .expect('Content-Type', 'text/plain; charset=utf-8')
119           .expect('Content-Disposition', 'attachment; filename="name.txt"')
120           .expect(200, 'tobi', cb)
121       })
123       it('should presist store on error', function (done) {
124         var app = express()
125         var store = { foo: 'bar' }
127         app.use(function (req, res, next) {
128           req.asyncLocalStorage = new asyncHooks.AsyncLocalStorage()
129           req.asyncLocalStorage.run(store, next)
130         })
132         app.use(function (req, res) {
133           res.download('test/fixtures/does-not-exist', function (err) {
134             var local = req.asyncLocalStorage.getStore()
136             if (local) {
137               res.setHeader('x-store-foo', String(local.foo))
138             }
140             res.send(err ? 'got ' + err.status + ' error' : 'no error')
141           })
142         })
144         request(app)
145           .get('/')
146           .expect(200)
147           .expect('x-store-foo', 'bar')
148           .expect('got 404 error')
149           .end(done)
150       })
151     })
152   })
154   describe('.download(path, options)', function () {
155     it('should allow options to res.sendFile()', function (done) {
156       var app = express()
158       app.use(function (req, res) {
159         res.download('test/fixtures/.name', {
160           dotfiles: 'allow',
161           maxAge: '4h'
162         })
163       })
165       request(app)
166         .get('/')
167         .expect(200)
168         .expect('Content-Disposition', 'attachment; filename=".name"')
169         .expect('Cache-Control', 'public, max-age=14400')
170         .expect(utils.shouldHaveBody(Buffer.from('tobi')))
171         .end(done)
172     })
174     describe('with "headers" option', function () {
175       it('should set headers on response', function (done) {
176         var app = express()
178         app.use(function (req, res) {
179           res.download('test/fixtures/user.html', {
180             headers: {
181               'X-Foo': 'Bar',
182               'X-Bar': 'Foo'
183             }
184           })
185         })
187         request(app)
188           .get('/')
189           .expect(200)
190           .expect('X-Foo', 'Bar')
191           .expect('X-Bar', 'Foo')
192           .end(done)
193       })
195       it('should use last header when duplicated', function (done) {
196         var app = express()
198         app.use(function (req, res) {
199           res.download('test/fixtures/user.html', {
200             headers: {
201               'X-Foo': 'Bar',
202               'x-foo': 'bar'
203             }
204           })
205         })
207         request(app)
208           .get('/')
209           .expect(200)
210           .expect('X-Foo', 'bar')
211           .end(done)
212       })
214       it('should override Content-Type', function (done) {
215         var app = express()
217         app.use(function (req, res) {
218           res.download('test/fixtures/user.html', {
219             headers: {
220               'Content-Type': 'text/x-custom'
221             }
222           })
223         })
225         request(app)
226           .get('/')
227           .expect(200)
228           .expect('Content-Type', 'text/x-custom')
229           .end(done)
230       })
232       it('should not set headers on 404', function (done) {
233         var app = express()
235         app.use(function (req, res) {
236           res.download('test/fixtures/does-not-exist', {
237             headers: {
238               'X-Foo': 'Bar'
239             }
240           })
241         })
243         request(app)
244           .get('/')
245           .expect(404)
246           .expect(utils.shouldNotHaveHeader('X-Foo'))
247           .end(done)
248       })
250       describe('when headers contains Content-Disposition', function () {
251         it('should be ignored', function (done) {
252           var app = express()
254           app.use(function (req, res) {
255             res.download('test/fixtures/user.html', {
256               headers: {
257                 'Content-Disposition': 'inline'
258               }
259             })
260           })
262           request(app)
263             .get('/')
264             .expect(200)
265             .expect('Content-Disposition', 'attachment; filename="user.html"')
266             .end(done)
267         })
269         it('should be ignored case-insensitively', function (done) {
270           var app = express()
272           app.use(function (req, res) {
273             res.download('test/fixtures/user.html', {
274               headers: {
275                 'content-disposition': 'inline'
276               }
277             })
278           })
280           request(app)
281             .get('/')
282             .expect(200)
283             .expect('Content-Disposition', 'attachment; filename="user.html"')
284             .end(done)
285         })
286       })
287     })
289     describe('with "root" option', function () {
290       it('should allow relative path', function (done) {
291         var app = express()
293         app.use(function (req, res) {
294           res.download('name.txt', {
295             root: FIXTURES_PATH
296           })
297         })
299         request(app)
300           .get('/')
301           .expect(200)
302           .expect('Content-Disposition', 'attachment; filename="name.txt"')
303           .expect(utils.shouldHaveBody(Buffer.from('tobi')))
304           .end(done)
305       })
307       it('should allow up within root', function (done) {
308         var app = express()
310         app.use(function (req, res) {
311           res.download('fake/../name.txt', {
312             root: FIXTURES_PATH
313           })
314         })
316         request(app)
317           .get('/')
318           .expect(200)
319           .expect('Content-Disposition', 'attachment; filename="name.txt"')
320           .expect(utils.shouldHaveBody(Buffer.from('tobi')))
321           .end(done)
322       })
324       it('should reject up outside root', function (done) {
325         var app = express()
327         app.use(function (req, res) {
328           var p = '..' + path.sep +
329             path.relative(path.dirname(FIXTURES_PATH), path.join(FIXTURES_PATH, 'name.txt'))
331           res.download(p, {
332             root: FIXTURES_PATH
333           })
334         })
336         request(app)
337           .get('/')
338           .expect(403)
339           .expect(utils.shouldNotHaveHeader('Content-Disposition'))
340           .end(done)
341       })
343       it('should reject reading outside root', function (done) {
344         var app = express()
346         app.use(function (req, res) {
347           res.download('../name.txt', {
348             root: FIXTURES_PATH
349           })
350         })
352         request(app)
353           .get('/')
354           .expect(403)
355           .expect(utils.shouldNotHaveHeader('Content-Disposition'))
356           .end(done)
357       })
358     })
359   })
361   describe('.download(path, filename, fn)', function(){
362     it('should invoke the callback', function(done){
363       var app = express();
364       var cb = after(2, done);
366       app.use(function(req, res){
367         res.download('test/fixtures/user.html', 'document', cb)
368       });
370       request(app)
371       .get('/')
372       .expect('Content-Type', 'text/html; charset=utf-8')
373       .expect('Content-Disposition', 'attachment; filename="document"')
374       .expect(200, cb);
375     })
376   })
378   describe('.download(path, filename, options, fn)', function () {
379     it('should invoke the callback', function (done) {
380       var app = express()
381       var cb = after(2, done)
382       var options = {}
384       app.use(function (req, res) {
385         res.download('test/fixtures/user.html', 'document', options, cb)
386       })
388       request(app)
389       .get('/')
390       .expect(200)
391       .expect('Content-Type', 'text/html; charset=utf-8')
392       .expect('Content-Disposition', 'attachment; filename="document"')
393       .end(cb)
394     })
396     it('should allow options to res.sendFile()', function (done) {
397       var app = express()
399       app.use(function (req, res) {
400         res.download('test/fixtures/.name', 'document', {
401           dotfiles: 'allow',
402           maxAge: '4h'
403         })
404       })
406       request(app)
407         .get('/')
408         .expect(200)
409         .expect('Content-Disposition', 'attachment; filename="document"')
410         .expect('Cache-Control', 'public, max-age=14400')
411         .expect(utils.shouldHaveBody(Buffer.from('tobi')))
412         .end(done)
413     })
415     describe('when options.headers contains Content-Disposition', function () {
416       it('should be ignored', function (done) {
417         var app = express()
419         app.use(function (req, res) {
420           res.download('test/fixtures/user.html', 'document', {
421             headers: {
422               'Content-Type': 'text/x-custom',
423               'Content-Disposition': 'inline'
424             }
425           })
426         })
428         request(app)
429         .get('/')
430         .expect(200)
431         .expect('Content-Type', 'text/x-custom')
432         .expect('Content-Disposition', 'attachment; filename="document"')
433         .end(done)
434       })
436       it('should be ignored case-insensitively', function (done) {
437         var app = express()
439         app.use(function (req, res) {
440           res.download('test/fixtures/user.html', 'document', {
441             headers: {
442               'content-type': 'text/x-custom',
443               'content-disposition': 'inline'
444             }
445           })
446         })
448         request(app)
449         .get('/')
450         .expect(200)
451         .expect('Content-Type', 'text/x-custom')
452         .expect('Content-Disposition', 'attachment; filename="document"')
453         .end(done)
454       })
455     })
456   })
458   describe('on failure', function(){
459     it('should invoke the callback', function(done){
460       var app = express();
462       app.use(function (req, res, next) {
463         res.download('test/fixtures/foobar.html', function(err){
464           if (!err) return next(new Error('expected error'));
465           res.send('got ' + err.status + ' ' + err.code);
466         });
467       });
469       request(app)
470       .get('/')
471       .expect(200, 'got 404 ENOENT', done);
472     })
474     it('should remove Content-Disposition', function(done){
475       var app = express()
477       app.use(function (req, res, next) {
478         res.download('test/fixtures/foobar.html', function(err){
479           if (!err) return next(new Error('expected error'));
480           res.end('failed');
481         });
482       });
484       request(app)
485         .get('/')
486         .expect(utils.shouldNotHaveHeader('Content-Disposition'))
487         .expect(200, 'failed', done)
488     })
489   })
492 function tryRequire (name) {
493   try {
494     return require(name)
495   } catch (e) {
496     return {}
497   }