3 var after
= require('after');
4 var assert
= require('assert')
5 var asyncHooks
= tryRequire('async_hooks')
6 var express
= require('../')
7 , request
= require('supertest')
8 var onFinished
= require('on-finished');
9 var path
= require('path');
10 var fixtures
= path
.join(__dirname
, 'fixtures');
11 var utils
= require('./support/utils');
13 var describeAsyncHooks
= typeof asyncHooks
.AsyncLocalStorage
=== 'function'
17 describe('res', function(){
18 describe('.sendFile(path)', function () {
19 it('should error missing path', function (done
) {
20 var app
= createApp();
24 .expect(500, /path.*required/, done
);
27 it('should error for non-string path', function (done
) {
28 var app
= createApp(42)
32 .expect(500, /TypeError: path must be a string to res.sendFile/, done
)
35 it('should error for non-absolute path', function (done
) {
36 var app
= createApp('name.txt')
40 .expect(500, /TypeError: path must be absolute/, done
)
43 it('should transfer a file', function (done
) {
44 var app
= createApp(path
.resolve(fixtures
, 'name.txt'));
48 .expect(200, 'tobi', done
);
51 it('should transfer a file with special characters in string', function (done
) {
52 var app
= createApp(path
.resolve(fixtures
, '% of dogs.txt'));
56 .expect(200, '20%', done
);
59 it('should include ETag', function (done
) {
60 var app
= createApp(path
.resolve(fixtures
, 'name.txt'));
64 .expect('ETag', /^(?:W\/)?"[^"]+"$/)
65 .expect(200, 'tobi', done
);
68 it('should 304 when ETag matches', function (done
) {
69 var app
= createApp(path
.resolve(fixtures
, 'name.txt'));
73 .expect('ETag', /^(?:W\/)?"[^"]+"$/)
74 .expect(200, 'tobi', function (err
, res
) {
75 if (err
) return done(err
);
76 var etag
= res
.headers
.etag
;
79 .set('If-None-Match', etag
)
84 it('should 404 for directory', function (done
) {
85 var app
= createApp(path
.resolve(fixtures
, 'blog'));
92 it('should 404 when not found', function (done
) {
93 var app
= createApp(path
.resolve(fixtures
, 'does-no-exist'));
95 app
.use(function (req
, res
) {
105 it('should send cache-control by default', function (done
) {
106 var app
= createApp(path
.resolve(__dirname
, 'fixtures/name.txt'))
110 .expect('Cache-Control', 'public, max-age=0')
114 it('should not serve dotfiles by default', function (done
) {
115 var app
= createApp(path
.resolve(__dirname
, 'fixtures/.name'))
122 it('should not override manual content-types', function (done
) {
125 app
.use(function (req
, res
) {
126 res
.contentType('application/x-bogus');
127 res
.sendFile(path
.resolve(fixtures
, 'name.txt'));
132 .expect('Content-Type', 'application/x-bogus')
136 it('should not error if the client aborts', function (done
) {
138 var cb
= after(2, done
)
141 app
.use(function (req
, res
) {
142 setImmediate(function () {
143 res
.sendFile(path
.resolve(fixtures
, 'name.txt'));
144 setTimeout(function () {
151 app
.use(function (err
, req
, res
, next
) {
156 var server
= app
.listen()
157 var test
= request(server
).get('/')
158 test
.end(function (err
) {
165 describe('.sendFile(path, fn)', function () {
166 it('should invoke the callback when complete', function (done
) {
167 var cb
= after(2, done
);
168 var app
= createApp(path
.resolve(fixtures
, 'name.txt'), cb
);
175 it('should invoke the callback when client aborts', function (done
) {
176 var cb
= after(2, done
)
179 app
.use(function (req
, res
) {
180 setImmediate(function () {
181 res
.sendFile(path
.resolve(fixtures
, 'name.txt'), function (err
) {
183 assert
.strictEqual(err
.code
, 'ECONNABORTED')
190 var server
= app
.listen()
191 var test
= request(server
).get('/')
192 test
.end(function (err
) {
198 it('should invoke the callback when client already aborted', function (done
) {
199 var cb
= after(2, done
)
202 app
.use(function (req
, res
) {
203 onFinished(res
, function () {
204 res
.sendFile(path
.resolve(fixtures
, 'name.txt'), function (err
) {
206 assert
.strictEqual(err
.code
, 'ECONNABORTED')
213 var server
= app
.listen()
214 var test
= request(server
).get('/')
215 test
.end(function (err
) {
221 it('should invoke the callback without error when HEAD', function (done
) {
223 var cb
= after(2, done
);
225 app
.use(function (req
, res
) {
226 res
.sendFile(path
.resolve(fixtures
, 'name.txt'), cb
);
234 it('should invoke the callback without error when 304', function (done
) {
236 var cb
= after(3, done
);
238 app
.use(function (req
, res
) {
239 res
.sendFile(path
.resolve(fixtures
, 'name.txt'), cb
);
244 .expect('ETag', /^(?:W\/)?"[^"]+"$/)
245 .expect(200, 'tobi', function (err
, res
) {
246 if (err
) return cb(err
);
247 var etag
= res
.headers
.etag
;
250 .set('If-None-Match', etag
)
255 it('should invoke the callback on 404', function(done
){
258 app
.use(function (req
, res
) {
259 res
.sendFile(path
.resolve(fixtures
, 'does-not-exist'), function (err
) {
260 res
.send(err
? 'got ' + err
.status
+ ' error' : 'no error')
266 .expect(200, 'got 404 error', done
)
269 describeAsyncHooks('async local storage', function () {
270 it('should presist store', function (done
) {
272 var cb
= after(2, done
)
273 var store
= { foo
: 'bar' }
275 app
.use(function (req
, res
, next
) {
276 req
.asyncLocalStorage
= new asyncHooks
.AsyncLocalStorage()
277 req
.asyncLocalStorage
.run(store
, next
)
280 app
.use(function (req
, res
) {
281 res
.sendFile(path
.resolve(fixtures
, 'name.txt'), function (err
) {
282 if (err
) return cb(err
)
284 var local
= req
.asyncLocalStorage
.getStore()
286 assert
.strictEqual(local
.foo
, 'bar')
293 .expect('Content-Type', 'text/plain; charset=utf-8')
294 .expect(200, 'tobi', cb
)
297 it('should presist store on error', function (done
) {
299 var store
= { foo
: 'bar' }
301 app
.use(function (req
, res
, next
) {
302 req
.asyncLocalStorage
= new asyncHooks
.AsyncLocalStorage()
303 req
.asyncLocalStorage
.run(store
, next
)
306 app
.use(function (req
, res
) {
307 res
.sendFile(path
.resolve(fixtures
, 'does-not-exist'), function (err
) {
308 var local
= req
.asyncLocalStorage
.getStore()
311 res
.setHeader('x-store-foo', String(local
.foo
))
314 res
.send(err
? 'got ' + err
.status
+ ' error' : 'no error')
321 .expect('x-store-foo', 'bar')
322 .expect('got 404 error')
328 describe('.sendFile(path, options)', function () {
329 it('should pass options to send module', function (done
) {
330 request(createApp(path
.resolve(fixtures
, 'name.txt'), { start
: 0, end
: 1 }))
332 .expect(200, 'to', done
)
335 describe('with "acceptRanges" option', function () {
336 describe('when true', function () {
337 it('should advertise byte range accepted', function (done
) {
340 app
.use(function (req
, res
) {
341 res
.sendFile(path
.resolve(fixtures
, 'nums.txt'), {
349 .expect('Accept-Ranges', 'bytes')
354 it('should respond to range request', function (done
) {
357 app
.use(function (req
, res
) {
358 res
.sendFile(path
.resolve(fixtures
, 'nums.txt'), {
365 .set('Range', 'bytes=0-4')
366 .expect(206, '12345', done
)
370 describe('when false', function () {
371 it('should not advertise accept-ranges', function (done
) {
374 app
.use(function (req
, res
) {
375 res
.sendFile(path
.resolve(fixtures
, 'nums.txt'), {
383 .expect(utils
.shouldNotHaveHeader('Accept-Ranges'))
387 it('should not honor range requests', function (done
) {
390 app
.use(function (req
, res
) {
391 res
.sendFile(path
.resolve(fixtures
, 'nums.txt'), {
398 .set('Range', 'bytes=0-4')
399 .expect(200, '123456789', done
)
404 describe('with "cacheControl" option', function () {
405 describe('when true', function () {
406 it('should send cache-control header', function (done
) {
409 app
.use(function (req
, res
) {
410 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
418 .expect('Cache-Control', 'public, max-age=0')
423 describe('when false', function () {
424 it('should not send cache-control header', function (done
) {
427 app
.use(function (req
, res
) {
428 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
436 .expect(utils
.shouldNotHaveHeader('Cache-Control'))
442 describe('with "dotfiles" option', function () {
443 describe('when "allow"', function () {
444 it('should allow dotfiles', function (done
) {
447 app
.use(function (req
, res
) {
448 res
.sendFile(path
.resolve(fixtures
, '.name'), {
456 .expect(utils
.shouldHaveBody(Buffer
.from('tobi')))
461 describe('when "deny"', function () {
462 it('should deny dotfiles', function (done
) {
465 app
.use(function (req
, res
) {
466 res
.sendFile(path
.resolve(fixtures
, '.name'), {
479 describe('when "ignore"', function () {
480 it('should ignore dotfiles', function (done
) {
483 app
.use(function (req
, res
) {
484 res
.sendFile(path
.resolve(fixtures
, '.name'), {
498 describe('with "headers" option', function () {
499 it('should set headers on response', function (done
) {
502 app
.use(function (req
, res
) {
503 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
514 .expect('X-Foo', 'Bar')
515 .expect('X-Bar', 'Foo')
519 it('should use last header when duplicated', function (done
) {
522 app
.use(function (req
, res
) {
523 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
534 .expect('X-Foo', 'bar')
538 it('should override Content-Type', function (done
) {
541 app
.use(function (req
, res
) {
542 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
544 'Content-Type': 'text/x-custom'
552 .expect('Content-Type', 'text/x-custom')
556 it('should not set headers on 404', function (done
) {
559 app
.use(function (req
, res
) {
560 res
.sendFile(path
.resolve(fixtures
, 'does-not-exist'), {
570 .expect(utils
.shouldNotHaveHeader('X-Foo'))
575 describe('with "immutable" option', function () {
576 describe('when true', function () {
577 it('should send cache-control header with immutable', function (done
) {
580 app
.use(function (req
, res
) {
581 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
589 .expect('Cache-Control', 'public, max-age=0, immutable')
594 describe('when false', function () {
595 it('should not send cache-control header with immutable', function (done
) {
598 app
.use(function (req
, res
) {
599 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
607 .expect('Cache-Control', 'public, max-age=0')
613 describe('with "lastModified" option', function () {
614 describe('when true', function () {
615 it('should send last-modified header', function (done
) {
618 app
.use(function (req
, res
) {
619 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
627 .expect(utils
.shouldHaveHeader('Last-Modified'))
631 it('should conditionally respond with if-modified-since', function (done
) {
634 app
.use(function (req
, res
) {
635 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
642 .set('If-Modified-Since', (new Date(Date
.now() + 99999).toUTCString()))
647 describe('when false', function () {
648 it('should not have last-modified header', function (done
) {
651 app
.use(function (req
, res
) {
652 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
660 .expect(utils
.shouldNotHaveHeader('Last-Modified'))
664 it('should not honor if-modified-since', function (done
) {
667 app
.use(function (req
, res
) {
668 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
675 .set('If-Modified-Since', (new Date(Date
.now() + 99999).toUTCString()))
677 .expect(utils
.shouldNotHaveHeader('Last-Modified'))
683 describe('with "maxAge" option', function () {
684 it('should set cache-control max-age to milliseconds', function (done
) {
687 app
.use(function (req
, res
) {
688 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
696 .expect('Cache-Control', 'public, max-age=20')
700 it('should cap cache-control max-age to 1 year', function (done
) {
703 app
.use(function (req
, res
) {
704 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
712 .expect('Cache-Control', 'public, max-age=31536000')
716 it('should min cache-control max-age to 0', function (done
) {
719 app
.use(function (req
, res
) {
720 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
728 .expect('Cache-Control', 'public, max-age=0')
732 it('should floor cache-control max-age', function (done
) {
735 app
.use(function (req
, res
) {
736 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
744 .expect('Cache-Control', 'public, max-age=21')
748 describe('when cacheControl: false', function () {
749 it('should not send cache-control', function (done
) {
752 app
.use(function (req
, res
) {
753 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
762 .expect(utils
.shouldNotHaveHeader('Cache-Control'))
767 describe('when string', function () {
768 it('should accept plain number as milliseconds', function (done
) {
771 app
.use(function (req
, res
) {
772 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
780 .expect('Cache-Control', 'public, max-age=20')
784 it('should accept suffix "s" for seconds', function (done
) {
787 app
.use(function (req
, res
) {
788 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
796 .expect('Cache-Control', 'public, max-age=20')
800 it('should accept suffix "m" for minutes', function (done
) {
803 app
.use(function (req
, res
) {
804 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
812 .expect('Cache-Control', 'public, max-age=1200')
816 it('should accept suffix "d" for days', function (done
) {
819 app
.use(function (req
, res
) {
820 res
.sendFile(path
.resolve(fixtures
, 'user.html'), {
828 .expect('Cache-Control', 'public, max-age=1728000')
834 describe('with "root" option', function () {
835 it('should allow relative path', function (done
) {
838 app
.use(function (req
, res
) {
839 res
.sendFile('name.txt', {
846 .expect(200, 'tobi', done
)
849 it('should allow up within root', function (done
) {
852 app
.use(function (req
, res
) {
853 res
.sendFile('fake/../name.txt', {
860 .expect(200, 'tobi', done
)
863 it('should reject up outside root', function (done
) {
866 app
.use(function (req
, res
) {
867 res
.sendFile('..' + path
.sep
+ path
.relative(path
.dirname(fixtures
), path
.join(fixtures
, 'name.txt')), {
877 it('should reject reading outside root', function (done
) {
880 app
.use(function (req
, res
) {
881 res
.sendFile('../name.txt', {
894 function createApp(path
, options
, fn
) {
897 app
.use(function (req
, res
) {
898 res
.sendFile(path
, options
, fn
);
904 function tryRequire (name
) {