3 var assert
= require('assert')
4 var express
= require('..')
5 var path
= require('path')
6 var request
= require('supertest')
7 var utils
= require('./support/utils')
9 var fixtures
= path
.join(__dirname
, '/fixtures')
10 var relative
= path
.relative(process
.cwd(), fixtures
)
12 var skipRelative
= ~relative
.indexOf('..') || path
.resolve(relative
) === relative
14 describe('express.static()', function () {
15 describe('basic operations', function () {
17 this.app
= createApp()
20 it('should require root path', function () {
21 assert
.throws(express
.static.bind(), /root path required/)
24 it('should require root path to be string', function () {
25 assert
.throws(express
.static.bind(null, 42), /root path.*string/)
28 it('should serve static files', function (done
) {
31 .expect(200, '- groceries', done
)
34 it('should support nesting', function (done
) {
36 .get('/users/tobi.txt')
37 .expect(200, 'ferret', done
)
40 it('should set Content-Type', function (done
) {
43 .expect('Content-Type', 'text/plain; charset=utf-8')
47 it('should set Last-Modified', function (done
) {
50 .expect('Last-Modified', /\d{2} \w{3} \d{4}/)
54 it('should default max-age=0', function (done
) {
57 .expect('Cache-Control', 'public, max-age=0')
61 it('should support urlencoded pathnames', function (done
) {
63 .get('/%25%20of%20dogs.txt')
64 .expect(200, '20%', done
)
67 it('should not choke on auth-looking URL', function (done
) {
70 .expect(404, 'Not Found', done
)
73 it('should support index.html', function (done
) {
77 .expect('Content-Type', /html/)
78 .expect('<p>tobi, loki, jane</p>', done
)
81 it('should support ../', function (done
) {
83 .get('/users/../todo.txt')
84 .expect(200, '- groceries', done
)
87 it('should support HEAD', function (done
) {
91 .expect(utils
.shouldNotHaveBody())
95 it('should skip POST requests', function (done
) {
98 .expect(404, 'Not Found', done
)
101 it('should support conditional requests', function (done
) {
106 .end(function (err
, res
) {
110 .set('If-None-Match', res
.headers
.etag
)
115 it('should support precondition checks', function (done
) {
118 .set('If-Match', '"foo"')
122 it('should serve zero-length files', function (done
) {
125 .expect(200, '', done
)
128 it('should ignore hidden files', function (done
) {
131 .expect(404, 'Not Found', done
)
135 (skipRelative
? describe
.skip
: describe
)('current dir', function () {
137 this.app
= createApp('.')
140 it('should be served with "."', function (done
) {
141 var dest
= relative
.split(path
.sep
).join('/')
143 .get('/' + dest
+ '/todo.txt')
144 .expect(200, '- groceries', done
)
148 describe('acceptRanges', function () {
149 describe('when false', function () {
150 it('should not include Accept-Ranges', function (done
) {
151 request(createApp(fixtures
, { 'acceptRanges': false }))
153 .expect(utils
.shouldNotHaveHeader('Accept-Ranges'))
154 .expect(200, '123456789', done
)
157 it('should ignore Rage request header', function (done
) {
158 request(createApp(fixtures
, { 'acceptRanges': false }))
160 .set('Range', 'bytes=0-3')
161 .expect(utils
.shouldNotHaveHeader('Accept-Ranges'))
162 .expect(utils
.shouldNotHaveHeader('Content-Range'))
163 .expect(200, '123456789', done
)
167 describe('when true', function () {
168 it('should include Accept-Ranges', function (done
) {
169 request(createApp(fixtures
, { 'acceptRanges': true }))
171 .expect('Accept-Ranges', 'bytes')
172 .expect(200, '123456789', done
)
175 it('should obey Rage request header', function (done
) {
176 request(createApp(fixtures
, { 'acceptRanges': true }))
178 .set('Range', 'bytes=0-3')
179 .expect('Accept-Ranges', 'bytes')
180 .expect('Content-Range', 'bytes 0-3/9')
181 .expect(206, '1234', done
)
186 describe('cacheControl', function () {
187 describe('when false', function () {
188 it('should not include Cache-Control', function (done
) {
189 request(createApp(fixtures
, { 'cacheControl': false }))
191 .expect(utils
.shouldNotHaveHeader('Cache-Control'))
192 .expect(200, '123456789', done
)
195 it('should ignore maxAge', function (done
) {
196 request(createApp(fixtures
, { 'cacheControl': false, 'maxAge': 12000 }))
198 .expect(utils
.shouldNotHaveHeader('Cache-Control'))
199 .expect(200, '123456789', done
)
203 describe('when true', function () {
204 it('should include Cache-Control', function (done
) {
205 request(createApp(fixtures
, { 'cacheControl': true }))
207 .expect('Cache-Control', 'public, max-age=0')
208 .expect(200, '123456789', done
)
213 describe('extensions', function () {
214 it('should be not be enabled by default', function (done
) {
215 request(createApp(fixtures
))
220 it('should be configurable', function (done
) {
221 request(createApp(fixtures
, { 'extensions': 'txt' }))
223 .expect(200, '- groceries', done
)
226 it('should support disabling extensions', function (done
) {
227 request(createApp(fixtures
, { 'extensions': false }))
232 it('should support fallbacks', function (done
) {
233 request(createApp(fixtures
, { 'extensions': ['htm', 'html', 'txt'] }))
235 .expect(200, '<li>groceries</li>', done
)
238 it('should 404 if nothing found', function (done
) {
239 request(createApp(fixtures
, { 'extensions': ['htm', 'html', 'txt'] }))
245 describe('fallthrough', function () {
246 it('should default to true', function (done
) {
248 .get('/does-not-exist')
249 .expect(404, 'Not Found', done
)
252 describe('when true', function () {
254 this.app
= createApp(fixtures
, { 'fallthrough': true })
257 it('should fall-through when OPTIONS request', function (done
) {
259 .options('/todo.txt')
260 .expect(404, 'Not Found', done
)
263 it('should fall-through when URL malformed', function (done
) {
266 .expect(404, 'Not Found', done
)
269 it('should fall-through when traversing past root', function (done
) {
271 .get('/users/../../todo.txt')
272 .expect(404, 'Not Found', done
)
275 it('should fall-through when URL too long', function (done
) {
277 var root
= fixtures
+ Array(10000).join('/foobar')
279 app
.use(express
.static(root
, { 'fallthrough': true }))
280 app
.use(function (req
, res
, next
) {
286 .expect(404, 'Not Found', done
)
289 describe('with redirect: true', function () {
291 this.app
= createApp(fixtures
, { 'fallthrough': true, 'redirect': true })
294 it('should fall-through when directory', function (done
) {
297 .expect(404, 'Not Found', done
)
300 it('should redirect when directory without slash', function (done
) {
303 .expect(301, /Redirecting/, done
)
307 describe('with redirect: false', function () {
309 this.app
= createApp(fixtures
, { 'fallthrough': true, 'redirect': false })
312 it('should fall-through when directory', function (done
) {
315 .expect(404, 'Not Found', done
)
318 it('should fall-through when directory without slash', function (done
) {
321 .expect(404, 'Not Found', done
)
326 describe('when false', function () {
328 this.app
= createApp(fixtures
, { 'fallthrough': false })
331 it('should 405 when OPTIONS request', function (done
) {
333 .options('/todo.txt')
334 .expect('Allow', 'GET, HEAD')
338 it('should 400 when URL malformed', function (done
) {
341 .expect(400, /BadRequestError/, done
)
344 it('should 403 when traversing past root', function (done
) {
346 .get('/users/../../todo.txt')
347 .expect(403, /ForbiddenError/, done
)
350 it('should 404 when URL too long', function (done
) {
352 var root
= fixtures
+ Array(10000).join('/foobar')
354 app
.use(express
.static(root
, { 'fallthrough': false }))
355 app
.use(function (req
, res
, next
) {
361 .expect(404, /ENAMETOOLONG/, done
)
364 describe('with redirect: true', function () {
366 this.app
= createApp(fixtures
, { 'fallthrough': false, 'redirect': true })
369 it('should 404 when directory', function (done
) {
372 .expect(404, /NotFoundError|ENOENT/, done
)
375 it('should redirect when directory without slash', function (done
) {
378 .expect(301, /Redirecting/, done
)
382 describe('with redirect: false', function () {
384 this.app
= createApp(fixtures
, { 'fallthrough': false, 'redirect': false })
387 it('should 404 when directory', function (done
) {
390 .expect(404, /NotFoundError|ENOENT/, done
)
393 it('should 404 when directory without slash', function (done
) {
396 .expect(404, /NotFoundError|ENOENT/, done
)
402 describe('hidden files', function () {
404 this.app
= createApp(fixtures
, { 'dotfiles': 'allow' })
407 it('should be served when dotfiles: "allow" is given', function (done
) {
411 .expect(utils
.shouldHaveBody(Buffer
.from('tobi')))
416 describe('immutable', function () {
417 it('should default to false', function (done
) {
418 request(createApp(fixtures
))
420 .expect('Cache-Control', 'public, max-age=0', done
)
423 it('should set immutable directive in Cache-Control', function (done
) {
424 request(createApp(fixtures
, { 'immutable': true, 'maxAge': '1h' }))
426 .expect('Cache-Control', 'public, max-age=3600, immutable', done
)
430 describe('lastModified', function () {
431 describe('when false', function () {
432 it('should not include Last-Modified', function (done
) {
433 request(createApp(fixtures
, { 'lastModified': false }))
435 .expect(utils
.shouldNotHaveHeader('Last-Modified'))
436 .expect(200, '123456789', done
)
440 describe('when true', function () {
441 it('should include Last-Modified', function (done
) {
442 request(createApp(fixtures
, { 'lastModified': true }))
444 .expect('Last-Modified', /^\w{3}, \d+ \w+ \d+ \d+:\d+:\d+ \w+$/)
445 .expect(200, '123456789', done
)
450 describe('maxAge', function () {
451 it('should accept string', function (done
) {
452 request(createApp(fixtures
, { 'maxAge': '30d' }))
454 .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 30))
458 it('should be reasonable when infinite', function (done
) {
459 request(createApp(fixtures
, { 'maxAge': Infinity
}))
461 .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 365))
466 describe('redirect', function () {
469 this.app
.use(function (req
, res
, next
) {
470 req
.originalUrl
= req
.url
=
471 req
.originalUrl
.replace(/\/snow(\/|$)/, '/snow \u2603$1')
474 this.app
.use(express
.static(fixtures
))
477 it('should redirect directories', function (done
) {
480 .expect('Location', '/users/')
484 it('should include HTML link', function (done
) {
487 .expect('Location', '/users/')
488 .expect(301, /\/users\//, done
)
491 it('should redirect directories with query string', function (done
) {
493 .get('/users?name=john')
494 .expect('Location', '/users/?name=john')
498 it('should not redirect to protocol-relative locations', function (done
) {
501 .expect('Location', '/users/')
505 it('should ensure redirect URL is properly encoded', function (done
) {
508 .expect('Location', '/snow%20%E2%98%83/')
509 .expect('Content-Type', /html/)
510 .expect(301, />Redirecting to \/snow%20%E2%98%83\/</, done
)
513 it('should respond with default Content-Security-Policy', function (done
) {
516 .expect('Content-Security-Policy', "default-src 'none'")
520 it('should not redirect incorrectly', function (done
) {
526 describe('when false', function () {
528 this.app
= createApp(fixtures
, { 'redirect': false })
531 it('should disable redirect', function (done
) {
539 describe('setHeaders', function () {
542 this.app
.use(express
.static(fixtures
, { 'setHeaders': function (res
) {
543 res
.setHeader('x-custom', 'set')
547 it('should reject non-functions', function () {
548 assert
.throws(express
.static.bind(null, fixtures
, { 'setHeaders': 3 }), /setHeaders.*function/)
551 it('should get called when sending file', function (done
) {
554 .expect('x-custom', 'set')
558 it('should not get called on 404', function (done
) {
561 .expect(utils
.shouldNotHaveHeader('x-custom'))
565 it('should not get called on redirect', function (done
) {
568 .expect(utils
.shouldNotHaveHeader('x-custom'))
573 describe('when traversing past root', function () {
575 this.app
= createApp(fixtures
, { 'fallthrough': false })
578 it('should catch urlencoded ../', function (done
) {
580 .get('/users/%2e%2e/%2e%2e/todo.txt')
584 it('should not allow root path disclosure', function (done
) {
586 .get('/users/../../fixtures/todo.txt')
591 describe('when request has "Range" header', function () {
593 this.app
= createApp()
596 it('should support byte ranges', function (done
) {
599 .set('Range', 'bytes=0-4')
600 .expect('12345', done
)
603 it('should be inclusive', function (done
) {
606 .set('Range', 'bytes=0-0')
610 it('should set Content-Range', function (done
) {
613 .set('Range', 'bytes=2-5')
614 .expect('Content-Range', 'bytes 2-5/9', done
)
617 it('should support -n', function (done
) {
620 .set('Range', 'bytes=-3')
624 it('should support n-', function (done
) {
627 .set('Range', 'bytes=3-')
628 .expect('456789', done
)
631 it('should respond with 206 "Partial Content"', function (done
) {
634 .set('Range', 'bytes=0-4')
638 it('should set Content-Length to the # of octets transferred', function (done
) {
641 .set('Range', 'bytes=2-3')
642 .expect('Content-Length', '2')
643 .expect(206, '34', done
)
646 describe('when last-byte-pos of the range is greater than current length', function () {
647 it('is taken to be equal to one less than the current length', function (done
) {
650 .set('Range', 'bytes=2-50')
651 .expect('Content-Range', 'bytes 2-8/9', done
)
654 it('should adapt the Content-Length accordingly', function (done
) {
657 .set('Range', 'bytes=2-50')
658 .expect('Content-Length', '7')
663 describe('when the first- byte-pos of the range is greater than the current length', function () {
664 it('should respond with 416', function (done
) {
667 .set('Range', 'bytes=9-50')
671 it('should include a Content-Range header of complete length', function (done
) {
674 .set('Range', 'bytes=9-50')
675 .expect('Content-Range', 'bytes */9')
680 describe('when syntactically invalid', function () {
681 it('should respond with 200 and the entire contents', function (done
) {
684 .set('Range', 'asdf')
685 .expect('123456789', done
)
690 describe('when index at mount point', function () {
693 this.app
.use('/users', express
.static(fixtures
+ '/users'))
696 it('should redirect correctly', function (done
) {
699 .expect('Location', '/users/')
704 describe('when mounted', function () {
707 this.app
.use('/static', express
.static(fixtures
))
710 it('should redirect relative to the originalUrl', function (done
) {
712 .get('/static/users')
713 .expect('Location', '/static/users/')
717 it('should not choke on auth-looking URL', function (done
) {
725 // NOTE: This is not a real part of the API, but
726 // over time this has become something users
727 // are doing, so this will prevent unseen
728 // regressions around this use-case.
730 describe('when mounted "root" as a file', function () {
733 this.app
.use('/todo.txt', express
.static(fixtures
+ '/todo.txt'))
736 it('should load the file when on trailing slash', function (done
) {
739 .expect(200, '- groceries', done
)
742 it('should 404 when trailing slash', function (done
) {
749 describe('when responding non-2xx or 304', function () {
750 it('should not alter the status', function (done
) {
753 app
.use(function (req
, res
, next
) {
757 app
.use(express
.static(fixtures
))
761 .expect(501, '- groceries', done
)
765 describe('when index file serving disabled', function () {
768 this.app
.use('/static', express
.static(fixtures
, { 'index': false }))
769 this.app
.use(function (req
, res
, next
) {
774 it('should next() on directory', function (done
) {
776 .get('/static/users/')
777 .expect(404, 'Not Found', done
)
780 it('should redirect to trailing slash', function (done
) {
782 .get('/static/users')
783 .expect('Location', '/static/users/')
787 it('should next() on mount point', function (done
) {
790 .expect(404, 'Not Found', done
)
793 it('should redirect to trailing slash mount point', function (done
) {
796 .expect('Location', '/static/')
802 function createApp (dir
, options
, fn
) {
804 var root
= dir
|| fixtures
806 app
.use(express
.static(root
, options
))
808 app
.use(function (req
, res
, next
) {