3 var after
= require('after');
4 var assert
= require('assert')
5 var asyncHooks
= tryRequire('async_hooks')
6 var Buffer
= require('node:buffer').Buffer
7 var express
= require('../')
8 , request
= require('supertest')
9 var onFinished
= require('on-finished');
10 var path
= require('path');
11 var fixtures
= path
.join(__dirname
, 'fixtures');
12 var utils
= require('./support/utils');
14 var describeAsyncHooks
= typeof asyncHooks
.AsyncLocalStorage
=== 'function'
18 describe('res', function(){
19 describe('.sendFile(path)', function () {
20 it('should error missing path', function (done
) {
21 var app
= createApp();
25 .expect(500, /path.*required/, done
);
28 it('should error for non-string path', function (done
) {
29 var app
= createApp(42)
33 .expect(500, /TypeError: path must be a string to res.sendFile/, done
)
36 it('should error for non-absolute path', function (done
) {
37 var app
= createApp('name.txt')
41 .expect(500, /TypeError: path must be absolute/, done
)
44 it('should transfer a file', function (done
) {
45 var app
= createApp(path
.resolve(fixtures
, 'name.txt'));
49 .expect(200, 'tobi', done
);
52 it('should transfer a file with special characters in string', function (done
) {
53 var app
= createApp(path
.resolve(fixtures
, '% of dogs.txt'));
57 .expect(200, '20%', done
);
60 it('should include ETag', function (done
) {
61 var app
= createApp(path
.resolve(fixtures
, 'name.txt'));
65 .expect('ETag', /^(?:W\/)?"[^"]+"$/)
66 .expect(200, 'tobi', done
);
69 it('should 304 when ETag matches', function (done
) {
70 var app
= createApp(path
.resolve(fixtures
, 'name.txt'));
74 .expect('ETag', /^(?:W\/)?"[^"]+"$/)
75 .expect(200, 'tobi', function (err
, res
) {
76 if (err
) return done(err
);
77 var etag
= res
.headers
.etag
;
80 .set('If-None-Match', etag
)
85 it('should 404 for directory', function (done
) {
86 var app
= createApp(path
.resolve(fixtures
, 'blog'));
93 it('should 404 when not found', function (done
) {
94 var app
= createApp(path
.resolve(fixtures
, 'does-no-exist'));
96 app
.use(function (req
, res
) {
106 it('should send cache-control by default', function (done
) {
107 var app
= createApp(path
.resolve(__dirname
, 'fixtures/name.txt'))
111 .expect('Cache-Control', 'public, max-age=0')
115 it('should not serve dotfiles by default', function (done
) {
116 var app
= createApp(path
.resolve(__dirname
, 'fixtures/.name'))
123 it('should not override manual content-types', function (done
) {
126 app
.use(function (req
, res
) {
127 res
.contentType('application/x-bogus');
128 res
.sendFile(path
.resolve(fixtures
, 'name.txt'));
133 .expect('Content-Type', 'application/x-bogus')
137 it('should not error if the client aborts', function (done
) {
139 var cb
= after(2, done
)
142 app
.use(function (req
, res
) {
143 setImmediate(function () {
144 res
.sendFile(path
.resolve(fixtures
, 'name.txt'));
145 setTimeout(function () {
152 app
.use(function (err
, req
, res
, next
) {
157 var server
= app
.listen()
158 var test
= request(server
).get('/')
159 test
.end(function (err
) {
166 describe('.sendFile(path, fn)', function () {
167 it('should invoke the callback when complete', function (done
) {
168 var cb
= after(2, done
);
169 var app
= createApp(path
.resolve(fixtures
, 'name.txt'), cb
);
176 it('should invoke the callback when client aborts', function (done
) {
177 var cb
= after(2, done
)
180 app
.use(function (req
, res
) {
181 setImmediate(function () {
182 res
.sendFile(path
.resolve(fixtures
, 'name.txt'), function (err
) {
184 assert
.strictEqual(err
.code
, 'ECONNABORTED')
191 var server
= app
.listen()
192 var test
= request(server
).get('/')
193 test
.end(function (err
) {
199 it('should invoke the callback when client already aborted', function (done
) {
200 var cb
= after(2, done
)
203 app
.use(function (req
, res
) {
204 onFinished(res
, function () {
205 res
.sendFile(path
.resolve(fixtures
, 'name.txt'), function (err
) {
207 assert
.strictEqual(err
.code
, 'ECONNABORTED')
214 var server
= app
.listen()
215 var test
= request(server
).get('/')
216 test
.end(function (err
) {
222 it('should invoke the callback without error when HEAD', function (done
) {
224 var cb
= after(2, done
);
226 app
.use(function (req
, res
) {
227 res
.sendFile(path
.resolve(fixtures
, 'name.txt'), cb
);
235 it('should invoke the callback without error when 304', function (done
) {
237 var cb
= after(3, done
);
239 app
.use(function (req
, res
) {
240 res
.sendFile(path
.resolve(fixtures
, 'name.txt'), cb
);
245 .expect('ETag', /^(?:W\/)?"[^"]+"$/)
246 .expect(200, 'tobi', function (err
, res
) {
247 if (err
) return cb(err
);
248 var etag
= res
.headers
.etag
;
251 .set('If-None-Match', etag
)
256 it('should invoke the callback on 404', function(done
){
259 app
.use(function (req
, res
) {
260 res
.sendFile(path
.resolve(fixtures
, 'does-not-exist'), function (err
) {
261 res
.send(err
? 'got ' + err
.status
+ ' error' : 'no error')
267 .expect(200, 'got 404 error', done
)
270 describeAsyncHooks('async local storage', function () {
271 it('should presist store', function (done
) {
273 var cb
= after(2, done
)
274 var store
= { foo
: 'bar' }
276 app
.use(function (req
, res
, next
) {
277 req
.asyncLocalStorage
= new asyncHooks
.AsyncLocalStorage()
278 req
.asyncLocalStorage
.run(store
, next
)
281 app
.use(function (req
, res
) {
282 res
.sendFile(path
.resolve(fixtures
, 'name.txt'), function (err
) {
283 if (err
) return cb(err
)
285 var local
= req
.asyncLocalStorage
.getStore()
287 assert
.strictEqual(local
.foo
, 'bar')
294 .expect('Content-Type', 'text/plain; charset=utf-8')
295 .expect(200, 'tobi', cb
)
298 it('should presist store on error', function (done
) {
300 var store
= { foo
: 'bar' }
302 app
.use(function (req
, res
, next
) {
303 req
.asyncLocalStorage
= new asyncHooks
.AsyncLocalStorage()
304 req
.asyncLocalStorage
.run(store
, next
)
307 app
.use(function (req
, res
) {
308 res
.sendFile(path
.resolve(fixtures
, 'does-not-exist'), function (err
) {
309 var local
= req
.asyncLocalStorage
.getStore()
312 res
.setHeader('x-store-foo', String(local
.foo
))
315 res
.send(err
? 'got ' + err
.status
+ ' error' : 'no error')
322 .expect('x-store-foo', 'bar')
323 .expect('got 404 error')
329 describe('.sendFile(path, options)', function () {
330 it('should pass options to send module', function (done
) {
331 request(createApp(path
.resolve(fixtures
, 'name.txt'), { start
: 0, end
: 1 }))
333 .expect(200, 'to', done
)
336 describe('with "acceptRanges" option', function () {
337 describe('when true', function () {
338 it('should advertise byte range accepted', function (done
) {
341 app
.use(function (req
, res
) {
342 res
.sendFile(path
.resolve(fixtures
, 'nums.txt'), {
350 .expect('Accept-Ranges', 'bytes')
355 it('should respond to range request', function (done
) {
358 app
.use(function (req
, res
) {
359 res
.sendFile(path
.resolve(fixtures
, 'nums.txt'), {
366 .set('Range', 'bytes=0-4')
367 .expect(206, '12345', done
)
371 describe('when false', function () {
372 it('should not advertise accept-ranges', function (done
) {
375 app
.use(function (req
, res
) {
376 res
.sendFile(path
.resolve(fixtures
, 'nums.txt'), {
384 .expect(utils
.shouldNotHaveHeader('Accept-Ranges'))
388 it('should not honor range requests', function (done
) {
391 app
.use(function (req
, res
) {
392 res
.sendFile(path
.resolve(fixtures
, 'nums.txt'), {
399 .set('Range', 'bytes=0-4')
400 .expect(200, '123456789', done
)
405 describe('with "cacheControl" option', function () {
406 describe('when true', function () {
407 it('should send cache-control header', function (done
) {
410 app
.use(function (req
, res
) {
411 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
419 .expect('Cache-Control', 'public, max-age=0')
424 describe('when false', function () {
425 it('should not send cache-control header', function (done
) {
428 app
.use(function (req
, res
) {
429 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
437 .expect(utils
.shouldNotHaveHeader('Cache-Control'))
443 describe('with "dotfiles" option', function () {
444 describe('when "allow"', function () {
445 it('should allow dotfiles', function (done
) {
448 app
.use(function (req
, res
) {
449 res
.sendFile(path
.resolve(fixtures
, '.name'), {
457 .expect(utils
.shouldHaveBody(Buffer
.from('tobi')))
462 describe('when "deny"', function () {
463 it('should deny dotfiles', function (done
) {
466 app
.use(function (req
, res
) {
467 res
.sendFile(path
.resolve(fixtures
, '.name'), {
480 describe('when "ignore"', function () {
481 it('should ignore dotfiles', function (done
) {
484 app
.use(function (req
, res
) {
485 res
.sendFile(path
.resolve(fixtures
, '.name'), {
499 describe('with "headers" option', function () {
500 it('should set headers on response', function (done
) {
503 app
.use(function (req
, res
) {
504 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
515 .expect('X-Foo', 'Bar')
516 .expect('X-Bar', 'Foo')
520 it('should use last header when duplicated', function (done
) {
523 app
.use(function (req
, res
) {
524 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
535 .expect('X-Foo', 'bar')
539 it('should override Content-Type', function (done
) {
542 app
.use(function (req
, res
) {
543 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
545 'Content-Type': 'text/x-custom'
553 .expect('Content-Type', 'text/x-custom')
557 it('should not set headers on 404', function (done
) {
560 app
.use(function (req
, res
) {
561 res
.sendFile(path
.resolve(fixtures
, 'does-not-exist'), {
571 .expect(utils
.shouldNotHaveHeader('X-Foo'))
576 describe('with "immutable" option', function () {
577 describe('when true', function () {
578 it('should send cache-control header with immutable', function (done
) {
581 app
.use(function (req
, res
) {
582 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
590 .expect('Cache-Control', 'public, max-age=0, immutable')
595 describe('when false', function () {
596 it('should not send cache-control header with immutable', function (done
) {
599 app
.use(function (req
, res
) {
600 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
608 .expect('Cache-Control', 'public, max-age=0')
614 describe('with "lastModified" option', function () {
615 describe('when true', function () {
616 it('should send last-modified header', function (done
) {
619 app
.use(function (req
, res
) {
620 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
628 .expect(utils
.shouldHaveHeader('Last-Modified'))
632 it('should conditionally respond with if-modified-since', function (done
) {
635 app
.use(function (req
, res
) {
636 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
643 .set('If-Modified-Since', (new Date(Date
.now() + 99999).toUTCString()))
648 describe('when false', function () {
649 it('should not have last-modified header', function (done
) {
652 app
.use(function (req
, res
) {
653 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
661 .expect(utils
.shouldNotHaveHeader('Last-Modified'))
665 it('should not honor if-modified-since', function (done
) {
668 app
.use(function (req
, res
) {
669 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
676 .set('If-Modified-Since', (new Date(Date
.now() + 99999).toUTCString()))
678 .expect(utils
.shouldNotHaveHeader('Last-Modified'))
684 describe('with "maxAge" option', function () {
685 it('should set cache-control max-age to milliseconds', function (done
) {
688 app
.use(function (req
, res
) {
689 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
697 .expect('Cache-Control', 'public, max-age=20')
701 it('should cap cache-control max-age to 1 year', function (done
) {
704 app
.use(function (req
, res
) {
705 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
713 .expect('Cache-Control', 'public, max-age=31536000')
717 it('should min cache-control max-age to 0', function (done
) {
720 app
.use(function (req
, res
) {
721 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
729 .expect('Cache-Control', 'public, max-age=0')
733 it('should floor cache-control max-age', function (done
) {
736 app
.use(function (req
, res
) {
737 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
745 .expect('Cache-Control', 'public, max-age=21')
749 describe('when cacheControl: false', function () {
750 it('should not send cache-control', function (done
) {
753 app
.use(function (req
, res
) {
754 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
763 .expect(utils
.shouldNotHaveHeader('Cache-Control'))
768 describe('when string', function () {
769 it('should accept plain number as milliseconds', function (done
) {
772 app
.use(function (req
, res
) {
773 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
781 .expect('Cache-Control', 'public, max-age=20')
785 it('should accept suffix "s" for seconds', function (done
) {
788 app
.use(function (req
, res
) {
789 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
797 .expect('Cache-Control', 'public, max-age=20')
801 it('should accept suffix "m" for minutes', function (done
) {
804 app
.use(function (req
, res
) {
805 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
813 .expect('Cache-Control', 'public, max-age=1200')
817 it('should accept suffix "d" for days', function (done
) {
820 app
.use(function (req
, res
) {
821 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
829 .expect('Cache-Control', 'public, max-age=1728000')
835 describe('with "root" option', function () {
836 it('should allow relative path', function (done
) {
839 app
.use(function (req
, res
) {
840 res
.sendFile('name.txt', {
847 .expect(200, 'tobi', done
)
850 it('should allow up within root', function (done
) {
853 app
.use(function (req
, res
) {
854 res
.sendFile('fake/../name.txt', {
861 .expect(200, 'tobi', done
)
864 it('should reject up outside root', function (done
) {
867 app
.use(function (req
, res
) {
868 res
.sendFile('..' + path
.sep
+ path
.relative(path
.dirname(fixtures
), path
.join(fixtures
, 'name.txt')), {
878 it('should reject reading outside root', function (done
) {
881 app
.use(function (req
, res
) {
882 res
.sendFile('../name.txt', {
895 function createApp(path
, options
, fn
) {
898 app
.use(function (req
, res
) {
899 res
.sendFile(path
, options
, fn
);
905 function tryRequire (name
) {