deps: body-parser@1.20.0
[express.git] / test / express.static.js
blob245fd5929ccd7ac974b090383ca837695eb0eb22
1 'use strict'
3 var assert = require('assert')
4 var Buffer = require('safe-buffer').Buffer
5 var express = require('..')
6 var path = require('path')
7 var request = require('supertest')
8 var utils = require('./support/utils')
10 var fixtures = path.join(__dirname, '/fixtures')
11 var relative = path.relative(process.cwd(), fixtures)
13 var skipRelative = ~relative.indexOf('..') || path.resolve(relative) === relative
15 describe('express.static()', function () {
16   describe('basic operations', function () {
17     before(function () {
18       this.app = createApp()
19     })
21     it('should require root path', function () {
22       assert.throws(express.static.bind(), /root path required/)
23     })
25     it('should require root path to be string', function () {
26       assert.throws(express.static.bind(null, 42), /root path.*string/)
27     })
29     it('should serve static files', function (done) {
30       request(this.app)
31         .get('/todo.txt')
32         .expect(200, '- groceries', done)
33     })
35     it('should support nesting', function (done) {
36       request(this.app)
37         .get('/users/tobi.txt')
38         .expect(200, 'ferret', done)
39     })
41     it('should set Content-Type', function (done) {
42       request(this.app)
43         .get('/todo.txt')
44         .expect('Content-Type', 'text/plain; charset=UTF-8')
45         .expect(200, done)
46     })
48     it('should set Last-Modified', function (done) {
49       request(this.app)
50         .get('/todo.txt')
51         .expect('Last-Modified', /\d{2} \w{3} \d{4}/)
52         .expect(200, done)
53     })
55     it('should default max-age=0', function (done) {
56       request(this.app)
57         .get('/todo.txt')
58         .expect('Cache-Control', 'public, max-age=0')
59         .expect(200, done)
60     })
62     it('should support urlencoded pathnames', function (done) {
63       request(this.app)
64         .get('/%25%20of%20dogs.txt')
65         .expect(200, '20%', done)
66     })
68     it('should not choke on auth-looking URL', function (done) {
69       request(this.app)
70         .get('//todo@txt')
71         .expect(404, 'Not Found', done)
72     })
74     it('should support index.html', function (done) {
75       request(this.app)
76         .get('/users/')
77         .expect(200)
78         .expect('Content-Type', /html/)
79         .expect('<p>tobi, loki, jane</p>', done)
80     })
82     it('should support ../', function (done) {
83       request(this.app)
84         .get('/users/../todo.txt')
85         .expect(200, '- groceries', done)
86     })
88     it('should support HEAD', function (done) {
89       request(this.app)
90         .head('/todo.txt')
91         .expect(200)
92         .expect(utils.shouldNotHaveBody())
93         .end(done)
94     })
96     it('should skip POST requests', function (done) {
97       request(this.app)
98         .post('/todo.txt')
99         .expect(404, 'Not Found', done)
100     })
102     it('should support conditional requests', function (done) {
103       var app = this.app
105       request(app)
106         .get('/todo.txt')
107         .end(function (err, res) {
108           if (err) throw err
109           request(app)
110             .get('/todo.txt')
111             .set('If-None-Match', res.headers.etag)
112             .expect(304, done)
113         })
114     })
116     it('should support precondition checks', function (done) {
117       request(this.app)
118         .get('/todo.txt')
119         .set('If-Match', '"foo"')
120         .expect(412, done)
121     })
123     it('should serve zero-length files', function (done) {
124       request(this.app)
125         .get('/empty.txt')
126         .expect(200, '', done)
127     })
129     it('should ignore hidden files', function (done) {
130       request(this.app)
131         .get('/.name')
132         .expect(404, 'Not Found', done)
133     })
134   });
136   (skipRelative ? describe.skip : describe)('current dir', function () {
137     before(function () {
138       this.app = createApp('.')
139     })
141     it('should be served with "."', function (done) {
142       var dest = relative.split(path.sep).join('/')
143       request(this.app)
144         .get('/' + dest + '/todo.txt')
145         .expect(200, '- groceries', done)
146     })
147   })
149   describe('acceptRanges', function () {
150     describe('when false', function () {
151       it('should not include Accept-Ranges', function (done) {
152         request(createApp(fixtures, { 'acceptRanges': false }))
153           .get('/nums.txt')
154           .expect(utils.shouldNotHaveHeader('Accept-Ranges'))
155           .expect(200, '123456789', done)
156       })
158       it('should ignore Rage request header', function (done) {
159         request(createApp(fixtures, { 'acceptRanges': false }))
160           .get('/nums.txt')
161           .set('Range', 'bytes=0-3')
162           .expect(utils.shouldNotHaveHeader('Accept-Ranges'))
163           .expect(utils.shouldNotHaveHeader('Content-Range'))
164           .expect(200, '123456789', done)
165       })
166     })
168     describe('when true', function () {
169       it('should include Accept-Ranges', function (done) {
170         request(createApp(fixtures, { 'acceptRanges': true }))
171           .get('/nums.txt')
172           .expect('Accept-Ranges', 'bytes')
173           .expect(200, '123456789', done)
174       })
176       it('should obey Rage request header', function (done) {
177         request(createApp(fixtures, { 'acceptRanges': true }))
178           .get('/nums.txt')
179           .set('Range', 'bytes=0-3')
180           .expect('Accept-Ranges', 'bytes')
181           .expect('Content-Range', 'bytes 0-3/9')
182           .expect(206, '1234', done)
183       })
184     })
185   })
187   describe('cacheControl', function () {
188     describe('when false', function () {
189       it('should not include Cache-Control', function (done) {
190         request(createApp(fixtures, { 'cacheControl': false }))
191           .get('/nums.txt')
192           .expect(utils.shouldNotHaveHeader('Cache-Control'))
193           .expect(200, '123456789', done)
194       })
196       it('should ignore maxAge', function (done) {
197         request(createApp(fixtures, { 'cacheControl': false, 'maxAge': 12000 }))
198           .get('/nums.txt')
199           .expect(utils.shouldNotHaveHeader('Cache-Control'))
200           .expect(200, '123456789', done)
201       })
202     })
204     describe('when true', function () {
205       it('should include Cache-Control', function (done) {
206         request(createApp(fixtures, { 'cacheControl': true }))
207           .get('/nums.txt')
208           .expect('Cache-Control', 'public, max-age=0')
209           .expect(200, '123456789', done)
210       })
211     })
212   })
214   describe('extensions', function () {
215     it('should be not be enabled by default', function (done) {
216       request(createApp(fixtures))
217         .get('/todo')
218         .expect(404, done)
219     })
221     it('should be configurable', function (done) {
222       request(createApp(fixtures, { 'extensions': 'txt' }))
223         .get('/todo')
224         .expect(200, '- groceries', done)
225     })
227     it('should support disabling extensions', function (done) {
228       request(createApp(fixtures, { 'extensions': false }))
229         .get('/todo')
230         .expect(404, done)
231     })
233     it('should support fallbacks', function (done) {
234       request(createApp(fixtures, { 'extensions': ['htm', 'html', 'txt'] }))
235         .get('/todo')
236         .expect(200, '<li>groceries</li>', done)
237     })
239     it('should 404 if nothing found', function (done) {
240       request(createApp(fixtures, { 'extensions': ['htm', 'html', 'txt'] }))
241         .get('/bob')
242         .expect(404, done)
243     })
244   })
246   describe('fallthrough', function () {
247     it('should default to true', function (done) {
248       request(createApp())
249         .get('/does-not-exist')
250         .expect(404, 'Not Found', done)
251     })
253     describe('when true', function () {
254       before(function () {
255         this.app = createApp(fixtures, { 'fallthrough': true })
256       })
258       it('should fall-through when OPTIONS request', function (done) {
259         request(this.app)
260           .options('/todo.txt')
261           .expect(404, 'Not Found', done)
262       })
264       it('should fall-through when URL malformed', function (done) {
265         request(this.app)
266           .get('/%')
267           .expect(404, 'Not Found', done)
268       })
270       it('should fall-through when traversing past root', function (done) {
271         request(this.app)
272           .get('/users/../../todo.txt')
273           .expect(404, 'Not Found', done)
274       })
276       it('should fall-through when URL too long', function (done) {
277         var app = express()
278         var root = fixtures + Array(10000).join('/foobar')
280         app.use(express.static(root, { 'fallthrough': true }))
281         app.use(function (req, res, next) {
282           res.sendStatus(404)
283         })
285         request(app)
286           .get('/')
287           .expect(404, 'Not Found', done)
288       })
290       describe('with redirect: true', function () {
291         before(function () {
292           this.app = createApp(fixtures, { 'fallthrough': true, 'redirect': true })
293         })
295         it('should fall-through when directory', function (done) {
296           request(this.app)
297             .get('/pets/')
298             .expect(404, 'Not Found', done)
299         })
301         it('should redirect when directory without slash', function (done) {
302           request(this.app)
303             .get('/pets')
304             .expect(301, /Redirecting/, done)
305         })
306       })
308       describe('with redirect: false', function () {
309         before(function () {
310           this.app = createApp(fixtures, { 'fallthrough': true, 'redirect': false })
311         })
313         it('should fall-through when directory', function (done) {
314           request(this.app)
315             .get('/pets/')
316             .expect(404, 'Not Found', done)
317         })
319         it('should fall-through when directory without slash', function (done) {
320           request(this.app)
321             .get('/pets')
322             .expect(404, 'Not Found', done)
323         })
324       })
325     })
327     describe('when false', function () {
328       before(function () {
329         this.app = createApp(fixtures, { 'fallthrough': false })
330       })
332       it('should 405 when OPTIONS request', function (done) {
333         request(this.app)
334           .options('/todo.txt')
335           .expect('Allow', 'GET, HEAD')
336           .expect(405, done)
337       })
339       it('should 400 when URL malformed', function (done) {
340         request(this.app)
341           .get('/%')
342           .expect(400, /BadRequestError/, done)
343       })
345       it('should 403 when traversing past root', function (done) {
346         request(this.app)
347           .get('/users/../../todo.txt')
348           .expect(403, /ForbiddenError/, done)
349       })
351       it('should 404 when URL too long', function (done) {
352         var app = express()
353         var root = fixtures + Array(10000).join('/foobar')
355         app.use(express.static(root, { 'fallthrough': false }))
356         app.use(function (req, res, next) {
357           res.sendStatus(404)
358         })
360         request(app)
361           .get('/')
362           .expect(404, /ENAMETOOLONG/, done)
363       })
365       describe('with redirect: true', function () {
366         before(function () {
367           this.app = createApp(fixtures, { 'fallthrough': false, 'redirect': true })
368         })
370         it('should 404 when directory', function (done) {
371           request(this.app)
372             .get('/pets/')
373             .expect(404, /NotFoundError|ENOENT/, done)
374         })
376         it('should redirect when directory without slash', function (done) {
377           request(this.app)
378             .get('/pets')
379             .expect(301, /Redirecting/, done)
380         })
381       })
383       describe('with redirect: false', function () {
384         before(function () {
385           this.app = createApp(fixtures, { 'fallthrough': false, 'redirect': false })
386         })
388         it('should 404 when directory', function (done) {
389           request(this.app)
390             .get('/pets/')
391             .expect(404, /NotFoundError|ENOENT/, done)
392         })
394         it('should 404 when directory without slash', function (done) {
395           request(this.app)
396             .get('/pets')
397             .expect(404, /NotFoundError|ENOENT/, done)
398         })
399       })
400     })
401   })
403   describe('hidden files', function () {
404     before(function () {
405       this.app = createApp(fixtures, { 'dotfiles': 'allow' })
406     })
408     it('should be served when dotfiles: "allow" is given', function (done) {
409       request(this.app)
410         .get('/.name')
411         .expect(200)
412         .expect(utils.shouldHaveBody(Buffer.from('tobi')))
413         .end(done)
414     })
415   })
417   describe('immutable', function () {
418     it('should default to false', function (done) {
419       request(createApp(fixtures))
420         .get('/nums.txt')
421         .expect('Cache-Control', 'public, max-age=0', done)
422     })
424     it('should set immutable directive in Cache-Control', function (done) {
425       request(createApp(fixtures, { 'immutable': true, 'maxAge': '1h' }))
426         .get('/nums.txt')
427         .expect('Cache-Control', 'public, max-age=3600, immutable', done)
428     })
429   })
431   describe('lastModified', function () {
432     describe('when false', function () {
433       it('should not include Last-Modified', function (done) {
434         request(createApp(fixtures, { 'lastModified': false }))
435           .get('/nums.txt')
436           .expect(utils.shouldNotHaveHeader('Last-Modified'))
437           .expect(200, '123456789', done)
438       })
439     })
441     describe('when true', function () {
442       it('should include Last-Modified', function (done) {
443         request(createApp(fixtures, { 'lastModified': true }))
444           .get('/nums.txt')
445           .expect('Last-Modified', /^\w{3}, \d+ \w+ \d+ \d+:\d+:\d+ \w+$/)
446           .expect(200, '123456789', done)
447       })
448     })
449   })
451   describe('maxAge', function () {
452     it('should accept string', function (done) {
453       request(createApp(fixtures, { 'maxAge': '30d' }))
454         .get('/todo.txt')
455         .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 30))
456         .expect(200, done)
457     })
459     it('should be reasonable when infinite', function (done) {
460       request(createApp(fixtures, { 'maxAge': Infinity }))
461         .get('/todo.txt')
462         .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 365))
463         .expect(200, done)
464     })
465   })
467   describe('redirect', function () {
468     before(function () {
469       this.app = express()
470       this.app.use(function (req, res, next) {
471         req.originalUrl = req.url =
472           req.originalUrl.replace(/\/snow(\/|$)/, '/snow \u2603$1')
473         next()
474       })
475       this.app.use(express.static(fixtures))
476     })
478     it('should redirect directories', function (done) {
479       request(this.app)
480         .get('/users')
481         .expect('Location', '/users/')
482         .expect(301, done)
483     })
485     it('should include HTML link', function (done) {
486       request(this.app)
487         .get('/users')
488         .expect('Location', '/users/')
489         .expect(301, /<a href="\/users\/">/, done)
490     })
492     it('should redirect directories with query string', function (done) {
493       request(this.app)
494         .get('/users?name=john')
495         .expect('Location', '/users/?name=john')
496         .expect(301, done)
497     })
499     it('should not redirect to protocol-relative locations', function (done) {
500       request(this.app)
501         .get('//users')
502         .expect('Location', '/users/')
503         .expect(301, done)
504     })
506     it('should ensure redirect URL is properly encoded', function (done) {
507       request(this.app)
508         .get('/snow')
509         .expect('Location', '/snow%20%E2%98%83/')
510         .expect('Content-Type', /html/)
511         .expect(301, />Redirecting to <a href="\/snow%20%E2%98%83\/">\/snow%20%E2%98%83\/<\/a></, done)
512     })
514     it('should respond with default Content-Security-Policy', function (done) {
515       request(this.app)
516         .get('/users')
517         .expect('Content-Security-Policy', "default-src 'none'")
518         .expect(301, done)
519     })
521     it('should not redirect incorrectly', function (done) {
522       request(this.app)
523         .get('/')
524         .expect(404, done)
525     })
527     describe('when false', function () {
528       before(function () {
529         this.app = createApp(fixtures, { 'redirect': false })
530       })
532       it('should disable redirect', function (done) {
533         request(this.app)
534           .get('/users')
535           .expect(404, done)
536       })
537     })
538   })
540   describe('setHeaders', function () {
541     before(function () {
542       this.app = express()
543       this.app.use(express.static(fixtures, { 'setHeaders': function (res) {
544         res.setHeader('x-custom', 'set')
545       } }))
546     })
548     it('should reject non-functions', function () {
549       assert.throws(express.static.bind(null, fixtures, { 'setHeaders': 3 }), /setHeaders.*function/)
550     })
552     it('should get called when sending file', function (done) {
553       request(this.app)
554         .get('/nums.txt')
555         .expect('x-custom', 'set')
556         .expect(200, done)
557     })
559     it('should not get called on 404', function (done) {
560       request(this.app)
561         .get('/bogus')
562         .expect(utils.shouldNotHaveHeader('x-custom'))
563         .expect(404, done)
564     })
566     it('should not get called on redirect', function (done) {
567       request(this.app)
568         .get('/users')
569         .expect(utils.shouldNotHaveHeader('x-custom'))
570         .expect(301, done)
571     })
572   })
574   describe('when traversing past root', function () {
575     before(function () {
576       this.app = createApp(fixtures, { 'fallthrough': false })
577     })
579     it('should catch urlencoded ../', function (done) {
580       request(this.app)
581         .get('/users/%2e%2e/%2e%2e/todo.txt')
582         .expect(403, done)
583     })
585     it('should not allow root path disclosure', function (done) {
586       request(this.app)
587         .get('/users/../../fixtures/todo.txt')
588         .expect(403, done)
589     })
590   })
592   describe('when request has "Range" header', function () {
593     before(function () {
594       this.app = createApp()
595     })
597     it('should support byte ranges', function (done) {
598       request(this.app)
599         .get('/nums.txt')
600         .set('Range', 'bytes=0-4')
601         .expect('12345', done)
602     })
604     it('should be inclusive', function (done) {
605       request(this.app)
606         .get('/nums.txt')
607         .set('Range', 'bytes=0-0')
608         .expect('1', done)
609     })
611     it('should set Content-Range', function (done) {
612       request(this.app)
613         .get('/nums.txt')
614         .set('Range', 'bytes=2-5')
615         .expect('Content-Range', 'bytes 2-5/9', done)
616     })
618     it('should support -n', function (done) {
619       request(this.app)
620         .get('/nums.txt')
621         .set('Range', 'bytes=-3')
622         .expect('789', done)
623     })
625     it('should support n-', function (done) {
626       request(this.app)
627         .get('/nums.txt')
628         .set('Range', 'bytes=3-')
629         .expect('456789', done)
630     })
632     it('should respond with 206 "Partial Content"', function (done) {
633       request(this.app)
634         .get('/nums.txt')
635         .set('Range', 'bytes=0-4')
636         .expect(206, done)
637     })
639     it('should set Content-Length to the # of octets transferred', function (done) {
640       request(this.app)
641         .get('/nums.txt')
642         .set('Range', 'bytes=2-3')
643         .expect('Content-Length', '2')
644         .expect(206, '34', done)
645     })
647     describe('when last-byte-pos of the range is greater than current length', function () {
648       it('is taken to be equal to one less than the current length', function (done) {
649         request(this.app)
650           .get('/nums.txt')
651           .set('Range', 'bytes=2-50')
652           .expect('Content-Range', 'bytes 2-8/9', done)
653       })
655       it('should adapt the Content-Length accordingly', function (done) {
656         request(this.app)
657           .get('/nums.txt')
658           .set('Range', 'bytes=2-50')
659           .expect('Content-Length', '7')
660           .expect(206, done)
661       })
662     })
664     describe('when the first- byte-pos of the range is greater than the current length', function () {
665       it('should respond with 416', function (done) {
666         request(this.app)
667           .get('/nums.txt')
668           .set('Range', 'bytes=9-50')
669           .expect(416, done)
670       })
672       it('should include a Content-Range header of complete length', function (done) {
673         request(this.app)
674           .get('/nums.txt')
675           .set('Range', 'bytes=9-50')
676           .expect('Content-Range', 'bytes */9')
677           .expect(416, done)
678       })
679     })
681     describe('when syntactically invalid', function () {
682       it('should respond with 200 and the entire contents', function (done) {
683         request(this.app)
684           .get('/nums.txt')
685           .set('Range', 'asdf')
686           .expect('123456789', done)
687       })
688     })
689   })
691   describe('when index at mount point', function () {
692     before(function () {
693       this.app = express()
694       this.app.use('/users', express.static(fixtures + '/users'))
695     })
697     it('should redirect correctly', function (done) {
698       request(this.app)
699         .get('/users')
700         .expect('Location', '/users/')
701         .expect(301, done)
702     })
703   })
705   describe('when mounted', function () {
706     before(function () {
707       this.app = express()
708       this.app.use('/static', express.static(fixtures))
709     })
711     it('should redirect relative to the originalUrl', function (done) {
712       request(this.app)
713         .get('/static/users')
714         .expect('Location', '/static/users/')
715         .expect(301, done)
716     })
718     it('should not choke on auth-looking URL', function (done) {
719       request(this.app)
720         .get('//todo@txt')
721         .expect(404, done)
722     })
723   })
725   //
726   // NOTE: This is not a real part of the API, but
727   //       over time this has become something users
728   //       are doing, so this will prevent unseen
729   //       regressions around this use-case.
730   //
731   describe('when mounted "root" as a file', function () {
732     before(function () {
733       this.app = express()
734       this.app.use('/todo.txt', express.static(fixtures + '/todo.txt'))
735     })
737     it('should load the file when on trailing slash', function (done) {
738       request(this.app)
739         .get('/todo.txt')
740         .expect(200, '- groceries', done)
741     })
743     it('should 404 when trailing slash', function (done) {
744       request(this.app)
745         .get('/todo.txt/')
746         .expect(404, done)
747     })
748   })
750   describe('when responding non-2xx or 304', function () {
751     it('should not alter the status', function (done) {
752       var app = express()
754       app.use(function (req, res, next) {
755         res.status(501)
756         next()
757       })
758       app.use(express.static(fixtures))
760       request(app)
761         .get('/todo.txt')
762         .expect(501, '- groceries', done)
763     })
764   })
766   describe('when index file serving disabled', function () {
767     before(function () {
768       this.app = express()
769       this.app.use('/static', express.static(fixtures, { 'index': false }))
770       this.app.use(function (req, res, next) {
771         res.sendStatus(404)
772       })
773     })
775     it('should next() on directory', function (done) {
776       request(this.app)
777         .get('/static/users/')
778         .expect(404, 'Not Found', done)
779     })
781     it('should redirect to trailing slash', function (done) {
782       request(this.app)
783         .get('/static/users')
784         .expect('Location', '/static/users/')
785         .expect(301, done)
786     })
788     it('should next() on mount point', function (done) {
789       request(this.app)
790         .get('/static/')
791         .expect(404, 'Not Found', done)
792     })
794     it('should redirect to trailing slash mount point', function (done) {
795       request(this.app)
796         .get('/static')
797         .expect('Location', '/static/')
798         .expect(301, done)
799     })
800   })
803 function createApp (dir, options, fn) {
804   var app = express()
805   var root = dir || fixtures
807   app.use(express.static(root, options))
809   app.use(function (req, res, next) {
810     res.sendStatus(404)
811   })
813   return app