cleanup: remove unnecessary require for global Buffer
[express.git] / test / express.static.js
blob65a3718a6019b05550d7f157df122dda8d8fa7ff
1 'use strict'
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 () {
16 before(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) {
29 request(this.app)
30 .get('/todo.txt')
31 .expect(200, '- groceries', done)
34 it('should support nesting', function (done) {
35 request(this.app)
36 .get('/users/tobi.txt')
37 .expect(200, 'ferret', done)
40 it('should set Content-Type', function (done) {
41 request(this.app)
42 .get('/todo.txt')
43 .expect('Content-Type', 'text/plain; charset=utf-8')
44 .expect(200, done)
47 it('should set Last-Modified', function (done) {
48 request(this.app)
49 .get('/todo.txt')
50 .expect('Last-Modified', /\d{2} \w{3} \d{4}/)
51 .expect(200, done)
54 it('should default max-age=0', function (done) {
55 request(this.app)
56 .get('/todo.txt')
57 .expect('Cache-Control', 'public, max-age=0')
58 .expect(200, done)
61 it('should support urlencoded pathnames', function (done) {
62 request(this.app)
63 .get('/%25%20of%20dogs.txt')
64 .expect(200, '20%', done)
67 it('should not choke on auth-looking URL', function (done) {
68 request(this.app)
69 .get('//todo@txt')
70 .expect(404, 'Not Found', done)
73 it('should support index.html', function (done) {
74 request(this.app)
75 .get('/users/')
76 .expect(200)
77 .expect('Content-Type', /html/)
78 .expect('<p>tobi, loki, jane</p>', done)
81 it('should support ../', function (done) {
82 request(this.app)
83 .get('/users/../todo.txt')
84 .expect(200, '- groceries', done)
87 it('should support HEAD', function (done) {
88 request(this.app)
89 .head('/todo.txt')
90 .expect(200)
91 .expect(utils.shouldNotHaveBody())
92 .end(done)
95 it('should skip POST requests', function (done) {
96 request(this.app)
97 .post('/todo.txt')
98 .expect(404, 'Not Found', done)
101 it('should support conditional requests', function (done) {
102 var app = this.app
104 request(app)
105 .get('/todo.txt')
106 .end(function (err, res) {
107 if (err) throw err
108 request(app)
109 .get('/todo.txt')
110 .set('If-None-Match', res.headers.etag)
111 .expect(304, done)
115 it('should support precondition checks', function (done) {
116 request(this.app)
117 .get('/todo.txt')
118 .set('If-Match', '"foo"')
119 .expect(412, done)
122 it('should serve zero-length files', function (done) {
123 request(this.app)
124 .get('/empty.txt')
125 .expect(200, '', done)
128 it('should ignore hidden files', function (done) {
129 request(this.app)
130 .get('/.name')
131 .expect(404, 'Not Found', done)
135 (skipRelative ? describe.skip : describe)('current dir', function () {
136 before(function () {
137 this.app = createApp('.')
140 it('should be served with "."', function (done) {
141 var dest = relative.split(path.sep).join('/')
142 request(this.app)
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 }))
152 .get('/nums.txt')
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 }))
159 .get('/nums.txt')
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 }))
170 .get('/nums.txt')
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 }))
177 .get('/nums.txt')
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 }))
190 .get('/nums.txt')
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 }))
197 .get('/nums.txt')
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 }))
206 .get('/nums.txt')
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))
216 .get('/todo')
217 .expect(404, done)
220 it('should be configurable', function (done) {
221 request(createApp(fixtures, { 'extensions': 'txt' }))
222 .get('/todo')
223 .expect(200, '- groceries', done)
226 it('should support disabling extensions', function (done) {
227 request(createApp(fixtures, { 'extensions': false }))
228 .get('/todo')
229 .expect(404, done)
232 it('should support fallbacks', function (done) {
233 request(createApp(fixtures, { 'extensions': ['htm', 'html', 'txt'] }))
234 .get('/todo')
235 .expect(200, '<li>groceries</li>', done)
238 it('should 404 if nothing found', function (done) {
239 request(createApp(fixtures, { 'extensions': ['htm', 'html', 'txt'] }))
240 .get('/bob')
241 .expect(404, done)
245 describe('fallthrough', function () {
246 it('should default to true', function (done) {
247 request(createApp())
248 .get('/does-not-exist')
249 .expect(404, 'Not Found', done)
252 describe('when true', function () {
253 before(function () {
254 this.app = createApp(fixtures, { 'fallthrough': true })
257 it('should fall-through when OPTIONS request', function (done) {
258 request(this.app)
259 .options('/todo.txt')
260 .expect(404, 'Not Found', done)
263 it('should fall-through when URL malformed', function (done) {
264 request(this.app)
265 .get('/%')
266 .expect(404, 'Not Found', done)
269 it('should fall-through when traversing past root', function (done) {
270 request(this.app)
271 .get('/users/../../todo.txt')
272 .expect(404, 'Not Found', done)
275 it('should fall-through when URL too long', function (done) {
276 var app = express()
277 var root = fixtures + Array(10000).join('/foobar')
279 app.use(express.static(root, { 'fallthrough': true }))
280 app.use(function (req, res, next) {
281 res.sendStatus(404)
284 request(app)
285 .get('/')
286 .expect(404, 'Not Found', done)
289 describe('with redirect: true', function () {
290 before(function () {
291 this.app = createApp(fixtures, { 'fallthrough': true, 'redirect': true })
294 it('should fall-through when directory', function (done) {
295 request(this.app)
296 .get('/pets/')
297 .expect(404, 'Not Found', done)
300 it('should redirect when directory without slash', function (done) {
301 request(this.app)
302 .get('/pets')
303 .expect(301, /Redirecting/, done)
307 describe('with redirect: false', function () {
308 before(function () {
309 this.app = createApp(fixtures, { 'fallthrough': true, 'redirect': false })
312 it('should fall-through when directory', function (done) {
313 request(this.app)
314 .get('/pets/')
315 .expect(404, 'Not Found', done)
318 it('should fall-through when directory without slash', function (done) {
319 request(this.app)
320 .get('/pets')
321 .expect(404, 'Not Found', done)
326 describe('when false', function () {
327 before(function () {
328 this.app = createApp(fixtures, { 'fallthrough': false })
331 it('should 405 when OPTIONS request', function (done) {
332 request(this.app)
333 .options('/todo.txt')
334 .expect('Allow', 'GET, HEAD')
335 .expect(405, done)
338 it('should 400 when URL malformed', function (done) {
339 request(this.app)
340 .get('/%')
341 .expect(400, /BadRequestError/, done)
344 it('should 403 when traversing past root', function (done) {
345 request(this.app)
346 .get('/users/../../todo.txt')
347 .expect(403, /ForbiddenError/, done)
350 it('should 404 when URL too long', function (done) {
351 var app = express()
352 var root = fixtures + Array(10000).join('/foobar')
354 app.use(express.static(root, { 'fallthrough': false }))
355 app.use(function (req, res, next) {
356 res.sendStatus(404)
359 request(app)
360 .get('/')
361 .expect(404, /ENAMETOOLONG/, done)
364 describe('with redirect: true', function () {
365 before(function () {
366 this.app = createApp(fixtures, { 'fallthrough': false, 'redirect': true })
369 it('should 404 when directory', function (done) {
370 request(this.app)
371 .get('/pets/')
372 .expect(404, /NotFoundError|ENOENT/, done)
375 it('should redirect when directory without slash', function (done) {
376 request(this.app)
377 .get('/pets')
378 .expect(301, /Redirecting/, done)
382 describe('with redirect: false', function () {
383 before(function () {
384 this.app = createApp(fixtures, { 'fallthrough': false, 'redirect': false })
387 it('should 404 when directory', function (done) {
388 request(this.app)
389 .get('/pets/')
390 .expect(404, /NotFoundError|ENOENT/, done)
393 it('should 404 when directory without slash', function (done) {
394 request(this.app)
395 .get('/pets')
396 .expect(404, /NotFoundError|ENOENT/, done)
402 describe('hidden files', function () {
403 before(function () {
404 this.app = createApp(fixtures, { 'dotfiles': 'allow' })
407 it('should be served when dotfiles: "allow" is given', function (done) {
408 request(this.app)
409 .get('/.name')
410 .expect(200)
411 .expect(utils.shouldHaveBody(Buffer.from('tobi')))
412 .end(done)
416 describe('immutable', function () {
417 it('should default to false', function (done) {
418 request(createApp(fixtures))
419 .get('/nums.txt')
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' }))
425 .get('/nums.txt')
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 }))
434 .get('/nums.txt')
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 }))
443 .get('/nums.txt')
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' }))
453 .get('/todo.txt')
454 .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 30))
455 .expect(200, done)
458 it('should be reasonable when infinite', function (done) {
459 request(createApp(fixtures, { 'maxAge': Infinity }))
460 .get('/todo.txt')
461 .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 365))
462 .expect(200, done)
466 describe('redirect', function () {
467 before(function () {
468 this.app = express()
469 this.app.use(function (req, res, next) {
470 req.originalUrl = req.url =
471 req.originalUrl.replace(/\/snow(\/|$)/, '/snow \u2603$1')
472 next()
474 this.app.use(express.static(fixtures))
477 it('should redirect directories', function (done) {
478 request(this.app)
479 .get('/users')
480 .expect('Location', '/users/')
481 .expect(301, done)
484 it('should include HTML link', function (done) {
485 request(this.app)
486 .get('/users')
487 .expect('Location', '/users/')
488 .expect(301, /\/users\//, done)
491 it('should redirect directories with query string', function (done) {
492 request(this.app)
493 .get('/users?name=john')
494 .expect('Location', '/users/?name=john')
495 .expect(301, done)
498 it('should not redirect to protocol-relative locations', function (done) {
499 request(this.app)
500 .get('//users')
501 .expect('Location', '/users/')
502 .expect(301, done)
505 it('should ensure redirect URL is properly encoded', function (done) {
506 request(this.app)
507 .get('/snow')
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) {
514 request(this.app)
515 .get('/users')
516 .expect('Content-Security-Policy', "default-src 'none'")
517 .expect(301, done)
520 it('should not redirect incorrectly', function (done) {
521 request(this.app)
522 .get('/')
523 .expect(404, done)
526 describe('when false', function () {
527 before(function () {
528 this.app = createApp(fixtures, { 'redirect': false })
531 it('should disable redirect', function (done) {
532 request(this.app)
533 .get('/users')
534 .expect(404, done)
539 describe('setHeaders', function () {
540 before(function () {
541 this.app = express()
542 this.app.use(express.static(fixtures, { 'setHeaders': function (res) {
543 res.setHeader('x-custom', 'set')
544 } }))
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) {
552 request(this.app)
553 .get('/nums.txt')
554 .expect('x-custom', 'set')
555 .expect(200, done)
558 it('should not get called on 404', function (done) {
559 request(this.app)
560 .get('/bogus')
561 .expect(utils.shouldNotHaveHeader('x-custom'))
562 .expect(404, done)
565 it('should not get called on redirect', function (done) {
566 request(this.app)
567 .get('/users')
568 .expect(utils.shouldNotHaveHeader('x-custom'))
569 .expect(301, done)
573 describe('when traversing past root', function () {
574 before(function () {
575 this.app = createApp(fixtures, { 'fallthrough': false })
578 it('should catch urlencoded ../', function (done) {
579 request(this.app)
580 .get('/users/%2e%2e/%2e%2e/todo.txt')
581 .expect(403, done)
584 it('should not allow root path disclosure', function (done) {
585 request(this.app)
586 .get('/users/../../fixtures/todo.txt')
587 .expect(403, done)
591 describe('when request has "Range" header', function () {
592 before(function () {
593 this.app = createApp()
596 it('should support byte ranges', function (done) {
597 request(this.app)
598 .get('/nums.txt')
599 .set('Range', 'bytes=0-4')
600 .expect('12345', done)
603 it('should be inclusive', function (done) {
604 request(this.app)
605 .get('/nums.txt')
606 .set('Range', 'bytes=0-0')
607 .expect('1', done)
610 it('should set Content-Range', function (done) {
611 request(this.app)
612 .get('/nums.txt')
613 .set('Range', 'bytes=2-5')
614 .expect('Content-Range', 'bytes 2-5/9', done)
617 it('should support -n', function (done) {
618 request(this.app)
619 .get('/nums.txt')
620 .set('Range', 'bytes=-3')
621 .expect('789', done)
624 it('should support n-', function (done) {
625 request(this.app)
626 .get('/nums.txt')
627 .set('Range', 'bytes=3-')
628 .expect('456789', done)
631 it('should respond with 206 "Partial Content"', function (done) {
632 request(this.app)
633 .get('/nums.txt')
634 .set('Range', 'bytes=0-4')
635 .expect(206, done)
638 it('should set Content-Length to the # of octets transferred', function (done) {
639 request(this.app)
640 .get('/nums.txt')
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) {
648 request(this.app)
649 .get('/nums.txt')
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) {
655 request(this.app)
656 .get('/nums.txt')
657 .set('Range', 'bytes=2-50')
658 .expect('Content-Length', '7')
659 .expect(206, done)
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) {
665 request(this.app)
666 .get('/nums.txt')
667 .set('Range', 'bytes=9-50')
668 .expect(416, done)
671 it('should include a Content-Range header of complete length', function (done) {
672 request(this.app)
673 .get('/nums.txt')
674 .set('Range', 'bytes=9-50')
675 .expect('Content-Range', 'bytes */9')
676 .expect(416, done)
680 describe('when syntactically invalid', function () {
681 it('should respond with 200 and the entire contents', function (done) {
682 request(this.app)
683 .get('/nums.txt')
684 .set('Range', 'asdf')
685 .expect('123456789', done)
690 describe('when index at mount point', function () {
691 before(function () {
692 this.app = express()
693 this.app.use('/users', express.static(fixtures + '/users'))
696 it('should redirect correctly', function (done) {
697 request(this.app)
698 .get('/users')
699 .expect('Location', '/users/')
700 .expect(301, done)
704 describe('when mounted', function () {
705 before(function () {
706 this.app = express()
707 this.app.use('/static', express.static(fixtures))
710 it('should redirect relative to the originalUrl', function (done) {
711 request(this.app)
712 .get('/static/users')
713 .expect('Location', '/static/users/')
714 .expect(301, done)
717 it('should not choke on auth-looking URL', function (done) {
718 request(this.app)
719 .get('//todo@txt')
720 .expect(404, 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 () {
731 before(function () {
732 this.app = express()
733 this.app.use('/todo.txt', express.static(fixtures + '/todo.txt'))
736 it('should load the file when on trailing slash', function (done) {
737 request(this.app)
738 .get('/todo.txt')
739 .expect(200, '- groceries', done)
742 it('should 404 when trailing slash', function (done) {
743 request(this.app)
744 .get('/todo.txt/')
745 .expect(404, done)
749 describe('when responding non-2xx or 304', function () {
750 it('should not alter the status', function (done) {
751 var app = express()
753 app.use(function (req, res, next) {
754 res.status(501)
755 next()
757 app.use(express.static(fixtures))
759 request(app)
760 .get('/todo.txt')
761 .expect(501, '- groceries', done)
765 describe('when index file serving disabled', function () {
766 before(function () {
767 this.app = express()
768 this.app.use('/static', express.static(fixtures, { 'index': false }))
769 this.app.use(function (req, res, next) {
770 res.sendStatus(404)
774 it('should next() on directory', function (done) {
775 request(this.app)
776 .get('/static/users/')
777 .expect(404, 'Not Found', done)
780 it('should redirect to trailing slash', function (done) {
781 request(this.app)
782 .get('/static/users')
783 .expect('Location', '/static/users/')
784 .expect(301, done)
787 it('should next() on mount point', function (done) {
788 request(this.app)
789 .get('/static/')
790 .expect(404, 'Not Found', done)
793 it('should redirect to trailing slash mount point', function (done) {
794 request(this.app)
795 .get('/static')
796 .expect('Location', '/static/')
797 .expect(301, done)
802 function createApp (dir, options, fn) {
803 var app = express()
804 var root = dir || fixtures
806 app.use(express.static(root, options))
808 app.use(function (req, res, next) {
809 res.sendStatus(404)
812 return app