build: Node.js@12.3
[express.git] / test / res.sendFile.js
blob5f494f1e0bc0f1fe2713cd567c24414c3d3fac87
2 var after = require('after');
3 var Buffer = require('safe-buffer').Buffer
4 var express = require('../')
5   , request = require('supertest')
6   , assert = require('assert');
7 var onFinished = require('on-finished');
8 var path = require('path');
9 var should = require('should');
10 var fixtures = path.join(__dirname, 'fixtures');
11 var utils = require('./support/utils');
13 describe('res', function(){
14   describe('.sendFile(path)', function () {
15     it('should error missing path', function (done) {
16       var app = createApp();
18       request(app)
19       .get('/')
20       .expect(500, /path.*required/, done);
21     });
23     it('should error for non-string path', function (done) {
24       var app = createApp(42)
26       request(app)
27       .get('/')
28       .expect(500, /TypeError: path must be a string to res.sendFile/, done)
29     })
31     it('should transfer a file', function (done) {
32       var app = createApp(path.resolve(fixtures, 'name.txt'));
34       request(app)
35       .get('/')
36       .expect(200, 'tobi', done);
37     });
39     it('should transfer a file with special characters in string', function (done) {
40       var app = createApp(path.resolve(fixtures, '% of dogs.txt'));
42       request(app)
43       .get('/')
44       .expect(200, '20%', done);
45     });
47     it('should include ETag', function (done) {
48       var app = createApp(path.resolve(fixtures, 'name.txt'));
50       request(app)
51       .get('/')
52       .expect('ETag', /^(?:W\/)?"[^"]+"$/)
53       .expect(200, 'tobi', done);
54     });
56     it('should 304 when ETag matches', function (done) {
57       var app = createApp(path.resolve(fixtures, 'name.txt'));
59       request(app)
60       .get('/')
61       .expect('ETag', /^(?:W\/)?"[^"]+"$/)
62       .expect(200, 'tobi', function (err, res) {
63         if (err) return done(err);
64         var etag = res.headers.etag;
65         request(app)
66         .get('/')
67         .set('If-None-Match', etag)
68         .expect(304, done);
69       });
70     });
72     it('should 404 for directory', function (done) {
73       var app = createApp(path.resolve(fixtures, 'blog'));
75       request(app)
76       .get('/')
77       .expect(404, done);
78     });
80     it('should 404 when not found', function (done) {
81       var app = createApp(path.resolve(fixtures, 'does-no-exist'));
83       app.use(function (req, res) {
84         res.statusCode = 200;
85         res.send('no!');
86       });
88       request(app)
89       .get('/')
90       .expect(404, done);
91     });
93     it('should not override manual content-types', function (done) {
94       var app = express();
96       app.use(function (req, res) {
97         res.contentType('application/x-bogus');
98         res.sendFile(path.resolve(fixtures, 'name.txt'));
99       });
101       request(app)
102       .get('/')
103       .expect('Content-Type', 'application/x-bogus')
104       .end(done);
105     })
107     it('should not error if the client aborts', function (done) {
108       var app = express();
109       var cb = after(2, done)
110       var error = null
112       app.use(function (req, res) {
113         setImmediate(function () {
114           res.sendFile(path.resolve(fixtures, 'name.txt'));
115           server.close(cb)
116           setTimeout(function () {
117             cb(error)
118           }, 10)
119         })
120         test.abort();
121       });
123       app.use(function (err, req, res, next) {
124         error = err
125         next(err)
126       });
128       var server = app.listen()
129       var test = request(server).get('/')
130       test.end()
131     })
133     describe('with "cacheControl" option', function () {
134       it('should enable cacheControl by default', function (done) {
135         var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'))
137         request(app)
138         .get('/')
139         .expect('Cache-Control', 'public, max-age=0')
140         .expect(200, done)
141       })
143       it('should accept cacheControl option', function (done) {
144         var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'), { cacheControl: false })
146         request(app)
147         .get('/')
148         .expect(utils.shouldNotHaveHeader('Cache-Control'))
149         .expect(200, done)
150       })
151     })
153     describe('with "dotfiles" option', function () {
154       it('should not serve dotfiles by default', function (done) {
155         var app = createApp(path.resolve(__dirname, 'fixtures/.name'));
157         request(app)
158         .get('/')
159         .expect(404, done);
160       });
162       it('should accept dotfiles option', function(done){
163         var app = createApp(path.resolve(__dirname, 'fixtures/.name'), { dotfiles: 'allow' });
165         request(app)
166         .get('/')
167         .expect(200)
168         .expect(shouldHaveBody(Buffer.from('tobi')))
169         .end(done)
170       });
171     });
173     describe('with "headers" option', function () {
174       it('should accept headers option', function (done) {
175         var headers = {
176           'x-success': 'sent',
177           'x-other': 'done'
178         };
179         var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'), { headers: headers });
181         request(app)
182         .get('/')
183         .expect('x-success', 'sent')
184         .expect('x-other', 'done')
185         .expect(200, done);
186       });
188       it('should ignore headers option on 404', function (done) {
189         var headers = { 'x-success': 'sent' };
190         var app = createApp(path.resolve(__dirname, 'fixtures/does-not-exist'), { headers: headers });
192         request(app)
193         .get('/')
194         .expect(utils.shouldNotHaveHeader('X-Success'))
195         .expect(404, done);
196       });
197     });
199     describe('with "immutable" option', function () {
200       it('should add immutable cache-control directive', function (done) {
201         var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'), {
202           immutable: true,
203           maxAge: '4h'
204         })
206         request(app)
207         .get('/')
208         .expect('Cache-Control', 'public, max-age=14400, immutable')
209         .expect(200, done)
210       })
211     })
213     describe('with "maxAge" option', function () {
214       it('should set cache-control max-age from number', function (done) {
215         var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'), {
216           maxAge: 14400000
217         })
219         request(app)
220         .get('/')
221         .expect('Cache-Control', 'public, max-age=14400')
222         .expect(200, done)
223       })
225       it('should set cache-control max-age from string', function (done) {
226         var app = createApp(path.resolve(__dirname, 'fixtures/name.txt'), {
227           maxAge: '4h'
228         })
230         request(app)
231         .get('/')
232         .expect('Cache-Control', 'public, max-age=14400')
233         .expect(200, done)
234       })
235     })
237     describe('with "root" option', function () {
238       it('should not transfer relative with without', function (done) {
239         var app = createApp('test/fixtures/name.txt');
241         request(app)
242         .get('/')
243         .expect(500, /must be absolute/, done);
244       })
246       it('should serve relative to "root"', function (done) {
247         var app = createApp('name.txt', {root: fixtures});
249         request(app)
250         .get('/')
251         .expect(200, 'tobi', done);
252       })
254       it('should disallow requesting out of "root"', function (done) {
255         var app = createApp('foo/../../user.html', {root: fixtures});
257         request(app)
258         .get('/')
259         .expect(403, done);
260       })
261     })
262   })
264   describe('.sendFile(path, fn)', function () {
265     it('should invoke the callback when complete', function (done) {
266       var cb = after(2, done);
267       var app = createApp(path.resolve(fixtures, 'name.txt'), cb);
269       request(app)
270       .get('/')
271       .expect(200, cb);
272     })
274     it('should invoke the callback when client aborts', function (done) {
275       var cb = after(1, done);
276       var app = express();
278       app.use(function (req, res) {
279         setImmediate(function () {
280           res.sendFile(path.resolve(fixtures, 'name.txt'), function (err) {
281             should(err).be.ok()
282             err.code.should.equal('ECONNABORTED');
283             server.close(cb)
284           });
285         });
286         test.abort();
287       });
289       var server = app.listen()
290       var test = request(server).get('/')
291       test.expect(200, cb);
292     })
294     it('should invoke the callback when client already aborted', function (done) {
295       var cb = after(1, done);
296       var app = express();
298       app.use(function (req, res) {
299         onFinished(res, function () {
300           res.sendFile(path.resolve(fixtures, 'name.txt'), function (err) {
301             should(err).be.ok()
302             err.code.should.equal('ECONNABORTED');
303             server.close(cb)
304           });
305         });
306         test.abort();
307       });
309       var server = app.listen()
310       var test = request(server).get('/')
311       test.expect(200, cb);
312     })
314     it('should invoke the callback without error when HEAD', function (done) {
315       var app = express();
316       var cb = after(2, done);
318       app.use(function (req, res) {
319         res.sendFile(path.resolve(fixtures, 'name.txt'), cb);
320       });
322       request(app)
323       .head('/')
324       .expect(200, cb);
325     });
327     it('should invoke the callback without error when 304', function (done) {
328       var app = express();
329       var cb = after(3, done);
331       app.use(function (req, res) {
332         res.sendFile(path.resolve(fixtures, 'name.txt'), cb);
333       });
335       request(app)
336       .get('/')
337       .expect('ETag', /^(?:W\/)?"[^"]+"$/)
338       .expect(200, 'tobi', function (err, res) {
339         if (err) return cb(err);
340         var etag = res.headers.etag;
341         request(app)
342         .get('/')
343         .set('If-None-Match', etag)
344         .expect(304, cb);
345       });
346     });
348     it('should invoke the callback on 404', function(done){
349       var app = express();
351       app.use(function (req, res) {
352         res.sendFile(path.resolve(fixtures, 'does-not-exist'), function (err) {
353           should(err).be.ok()
354           err.status.should.equal(404);
355           res.send('got it');
356         });
357       });
359       request(app)
360       .get('/')
361       .expect(200, 'got it', done);
362     })
363   })
365   describe('.sendFile(path, options)', function () {
366     it('should pass options to send module', function (done) {
367       request(createApp(path.resolve(fixtures, 'name.txt'), { start: 0, end: 1 }))
368       .get('/')
369       .expect(200, 'to', done)
370     })
371   })
373   describe('.sendfile(path, fn)', function(){
374     it('should invoke the callback when complete', function(done){
375       var app = express();
376       var cb = after(2, done);
378       app.use(function(req, res){
379         res.sendfile('test/fixtures/user.html', cb)
380       });
382       request(app)
383       .get('/')
384       .expect(200, cb);
385     })
387     it('should utilize the same options as express.static()', function(done){
388       var app = express();
390       app.use(function(req, res){
391         res.sendfile('test/fixtures/user.html', { maxAge: 60000 });
392       });
394       request(app)
395       .get('/')
396       .expect('Cache-Control', 'public, max-age=60')
397       .end(done);
398     })
400     it('should invoke the callback when client aborts', function (done) {
401       var cb = after(1, done);
402       var app = express();
404       app.use(function (req, res) {
405         setImmediate(function () {
406           res.sendfile('test/fixtures/name.txt', function (err) {
407             should(err).be.ok()
408             err.code.should.equal('ECONNABORTED');
409             server.close(cb)
410           });
411         });
412         test.abort();
413       });
415       var server = app.listen()
416       var test = request(server).get('/')
417       test.expect(200, cb);
418     })
420     it('should invoke the callback when client already aborted', function (done) {
421       var cb = after(1, done);
422       var app = express();
424       app.use(function (req, res) {
425         onFinished(res, function () {
426           res.sendfile('test/fixtures/name.txt', function (err) {
427             should(err).be.ok()
428             err.code.should.equal('ECONNABORTED');
429             server.close(cb)
430           });
431         });
432         test.abort();
433       });
435       var server = app.listen()
436       var test = request(server).get('/')
437       test.expect(200, cb);
438     })
440     it('should invoke the callback without error when HEAD', function (done) {
441       var app = express();
442       var cb = after(2, done);
444       app.use(function (req, res) {
445         res.sendfile('test/fixtures/name.txt', cb);
446       });
448       request(app)
449       .head('/')
450       .expect(200, cb);
451     });
453     it('should invoke the callback without error when 304', function (done) {
454       var app = express();
455       var cb = after(3, done);
457       app.use(function (req, res) {
458         res.sendfile('test/fixtures/name.txt', cb);
459       });
461       request(app)
462       .get('/')
463       .expect('ETag', /^(?:W\/)?"[^"]+"$/)
464       .expect(200, 'tobi', function (err, res) {
465         if (err) return cb(err);
466         var etag = res.headers.etag;
467         request(app)
468         .get('/')
469         .set('If-None-Match', etag)
470         .expect(304, cb);
471       });
472     });
474     it('should invoke the callback on 404', function(done){
475       var app = express();
476       var calls = 0;
478       app.use(function(req, res){
479         res.sendfile('test/fixtures/nope.html', function(err){
480           assert.equal(calls++, 0);
481           assert(!res.headersSent);
482           res.send(err.message);
483         });
484       });
486       request(app)
487       .get('/')
488       .expect(200, /^ENOENT.*?, stat/, done);
489     })
491     it('should not override manual content-types', function(done){
492       var app = express();
494       app.use(function(req, res){
495         res.contentType('txt');
496         res.sendfile('test/fixtures/user.html');
497       });
499       request(app)
500       .get('/')
501       .expect('Content-Type', 'text/plain; charset=utf-8')
502       .end(done);
503     })
505     it('should invoke the callback on 403', function(done){
506       var app = express()
508       app.use(function(req, res){
509         res.sendfile('test/fixtures/foo/../user.html', function(err){
510           assert(!res.headersSent);
511           res.send(err.message);
512         });
513       });
515       request(app)
516       .get('/')
517       .expect('Forbidden')
518       .expect(200, done);
519     })
521     it('should invoke the callback on socket error', function(done){
522       var app = express()
524       app.use(function(req, res){
525         res.sendfile('test/fixtures/user.html', function(err){
526           assert(!res.headersSent);
527           req.socket.listeners('error').should.have.length(1); // node's original handler
528           done();
529         });
531         req.socket.emit('error', new Error('broken!'));
532       });
534       request(app)
535       .get('/')
536       .end(function(){});
537     })
538   })
540   describe('.sendfile(path)', function(){
541     it('should not serve dotfiles', function(done){
542       var app = express();
544       app.use(function(req, res){
545         res.sendfile('test/fixtures/.name');
546       });
548       request(app)
549       .get('/')
550       .expect(404, done);
551     })
553     it('should accept dotfiles option', function(done){
554       var app = express();
556       app.use(function(req, res){
557         res.sendfile('test/fixtures/.name', { dotfiles: 'allow' });
558       });
560       request(app)
561       .get('/')
562       .expect(200)
563       .expect(shouldHaveBody(Buffer.from('tobi')))
564       .end(done)
565     })
567     it('should accept headers option', function(done){
568       var app = express();
569       var headers = {
570         'x-success': 'sent',
571         'x-other': 'done'
572       };
574       app.use(function(req, res){
575         res.sendfile('test/fixtures/user.html', { headers: headers });
576       });
578       request(app)
579       .get('/')
580       .expect('x-success', 'sent')
581       .expect('x-other', 'done')
582       .expect(200, done);
583     })
585     it('should ignore headers option on 404', function(done){
586       var app = express();
587       var headers = { 'x-success': 'sent' };
589       app.use(function(req, res){
590         res.sendfile('test/fixtures/user.nothing', { headers: headers });
591       });
593       request(app)
594       .get('/')
595         .expect(utils.shouldNotHaveHeader('X-Success'))
596         .expect(404, done);
597     })
599     it('should transfer a file', function (done) {
600       var app = express();
602       app.use(function (req, res) {
603         res.sendfile('test/fixtures/name.txt');
604       });
606       request(app)
607       .get('/')
608       .expect(200, 'tobi', done);
609     });
611     it('should transfer a directory index file', function (done) {
612       var app = express();
614       app.use(function (req, res) {
615         res.sendfile('test/fixtures/blog/');
616       });
618       request(app)
619       .get('/')
620       .expect(200, '<b>index</b>', done);
621     });
623     it('should 404 for directory without trailing slash', function (done) {
624       var app = express();
626       app.use(function (req, res) {
627         res.sendfile('test/fixtures/blog');
628       });
630       request(app)
631       .get('/')
632       .expect(404, done);
633     });
635     it('should transfer a file with urlencoded name', function (done) {
636       var app = express();
638       app.use(function (req, res) {
639         res.sendfile('test/fixtures/%25%20of%20dogs.txt');
640       });
642       request(app)
643       .get('/')
644       .expect(200, '20%', done);
645     });
647     it('should not error if the client aborts', function (done) {
648       var app = express();
649       var cb = after(2, done)
650       var error = null
652       app.use(function (req, res) {
653         setImmediate(function () {
654           res.sendfile(path.resolve(fixtures, 'name.txt'));
655           server.close(cb)
656           setTimeout(function () {
657             cb(error)
658           }, 10)
659         });
660         test.abort();
661       });
663       app.use(function (err, req, res, next) {
664         error = err
665         next(err)
666       });
668       var server = app.listen()
669       var test = request(server).get('/')
670       test.end()
671     })
673     describe('with an absolute path', function(){
674       it('should transfer the file', function(done){
675         var app = express();
677         app.use(function(req, res){
678           res.sendfile(path.join(__dirname, '/fixtures/user.html'))
679         });
681         request(app)
682         .get('/')
683         .expect('Content-Type', 'text/html; charset=UTF-8')
684         .expect(200, '<p>{{user.name}}</p>', done);
685       })
686     })
688     describe('with a relative path', function(){
689       it('should transfer the file', function(done){
690         var app = express();
692         app.use(function(req, res){
693           res.sendfile('test/fixtures/user.html');
694         });
696         request(app)
697         .get('/')
698         .expect('Content-Type', 'text/html; charset=UTF-8')
699         .expect(200, '<p>{{user.name}}</p>', done);
700       })
702       it('should serve relative to "root"', function(done){
703         var app = express();
705         app.use(function(req, res){
706           res.sendfile('user.html', { root: 'test/fixtures/' });
707         });
709         request(app)
710         .get('/')
711         .expect('Content-Type', 'text/html; charset=UTF-8')
712         .expect(200, '<p>{{user.name}}</p>', done);
713       })
715       it('should consider ../ malicious when "root" is not set', function(done){
716         var app = express();
718         app.use(function(req, res){
719           res.sendfile('test/fixtures/foo/../user.html');
720         });
722         request(app)
723         .get('/')
724         .expect(403, done);
725       })
727       it('should allow ../ when "root" is set', function(done){
728         var app = express();
730         app.use(function(req, res){
731           res.sendfile('foo/../user.html', { root: 'test/fixtures' });
732         });
734         request(app)
735         .get('/')
736         .expect(200, done);
737       })
739       it('should disallow requesting out of "root"', function(done){
740         var app = express();
742         app.use(function(req, res){
743           res.sendfile('foo/../../user.html', { root: 'test/fixtures' });
744         });
746         request(app)
747         .get('/')
748         .expect(403, done);
749       })
751       it('should next(404) when not found', function(done){
752         var app = express()
753           , calls = 0;
755         app.use(function(req, res){
756           res.sendfile('user.html');
757         });
759         app.use(function(req, res){
760           assert(0, 'this should not be called');
761         });
763         app.use(function(err, req, res, next){
764           ++calls;
765           next(err);
766         });
768         request(app)
769         .get('/')
770         .end(function(err, res){
771           res.statusCode.should.equal(404);
772           calls.should.equal(1);
773           done();
774         });
775       })
777       describe('with non-GET', function(){
778         it('should still serve', function(done){
779           var app = express()
781           app.use(function(req, res){
782             res.sendfile(path.join(__dirname, '/fixtures/name.txt'))
783           });
785           request(app)
786           .get('/')
787           .expect('tobi', done);
788         })
789       })
790     })
791   })
794 describe('.sendfile(path, options)', function () {
795   it('should pass options to send module', function (done) {
796     var app = express()
798     app.use(function (req, res) {
799       res.sendfile(path.resolve(fixtures, 'name.txt'), { start: 0, end: 1 })
800     })
802     request(app)
803       .get('/')
804       .expect(200, 'to', done)
805   })
808 function createApp(path, options, fn) {
809   var app = express();
811   app.use(function (req, res) {
812     res.sendFile(path, options, fn);
813   });
815   return app;
818 function shouldHaveBody (buf) {
819   return function (res) {
820     var body = !Buffer.isBuffer(res.body)
821       ? Buffer.from(res.text)
822       : res.body
823     assert.ok(body, 'response has body')
824     assert.strictEqual(body.toString('hex'), buf.toString('hex'))
825   }