Use let or const to avoid scope issues and hoisting
var assert = require('node:assert')
1'use strict'23var assert = require('node:assert')4var express = require('..')5var path = require('node:path')6const { Buffer } = require('node:buffer');78var request = require('supertest')9var utils = require('./support/utils')1011var fixtures = path.join(__dirname, '/fixtures')12var relative = path.relative(process.cwd(), fixtures)1314var skipRelative = ~relative.indexOf('..') || path.resolve(relative) === relative1516describe('express.static()', function () {17 describe('basic operations', function () {18 before(function () {19 this.app = createApp()20 })2122 it('should require root path', function () {23 assert.throws(express.static.bind(), /root path required/)24 })2526 it('should require root path to be string', function () {27 assert.throws(express.static.bind(null, 42), /root path.*string/)28 })2930 it('should serve static files', function (done) {31 request(this.app)32 .get('/todo.txt')33 .expect(200, '- groceries', done)34 })3536 it('should support nesting', function (done) {37 request(this.app)38 .get('/users/tobi.txt')39 .expect(200, 'ferret', done)40 })4142 it('should set Content-Type', function (done) {43 request(this.app)44 .get('/todo.txt')45 .expect('Content-Type', 'text/plain; charset=utf-8')46 .expect(200, done)47 })4849 it('should set Last-Modified', function (done) {50 request(this.app)51 .get('/todo.txt')52 .expect('Last-Modified', /\d{2} \w{3} \d{4}/)53 .expect(200, done)54 })5556 it('should default max-age=0', function (done) {57 request(this.app)58 .get('/todo.txt')59 .expect('Cache-Control', 'public, max-age=0')60 .expect(200, done)61 })6263 it('should support urlencoded pathnames', function (done) {64 request(this.app)65 .get('/%25%20of%20dogs.txt')66 .expect(200, '20%', done)67 })6869 it('should not choke on auth-looking URL', function (done) {70 request(this.app)71 .get('//todo@txt')72 .expect(404, 'Not Found', done)73 })7475 it('should support index.html', function (done) {76 request(this.app)77 .get('/users/')78 .expect(200)79 .expect('Content-Type', /html/)80 .expect('<p>tobi, loki, jane</p>', done)81 })8283 it('should support ../', function (done) {84 request(this.app)85 .get('/users/../todo.txt')86 .expect(200, '- groceries', done)87 })8889 it('should support HEAD', function (done) {90 request(this.app)91 .head('/todo.txt')92 .expect(200)93 .expect(utils.shouldNotHaveBody())94 .end(done)95 })9697 it('should skip POST requests', function (done) {98 request(this.app)99 .post('/todo.txt')100 .expect(404, 'Not Found', done)101 })102103 it('should support conditional requests', function (done) {104 var app = this.app105106 request(app)107 .get('/todo.txt')108 .end(function (err, res) {109 if (err) throw err110 request(app)111 .get('/todo.txt')112 .set('If-None-Match', res.headers.etag)113 .expect(304, done)114 })115 })116117 it('should support precondition checks', function (done) {118 request(this.app)119 .get('/todo.txt')120 .set('If-Match', '"foo"')121 .expect(412, done)122 })123124 it('should serve zero-length files', function (done) {125 request(this.app)126 .get('/empty.txt')127 .expect(200, '', done)128 })129130 it('should ignore hidden files', function (done) {131 request(this.app)132 .get('/.name')133 .expect(404, 'Not Found', done)134 })135 });136137 (skipRelative ? describe.skip : describe)('current dir', function () {138 before(function () {139 this.app = createApp('.')140 })141142 it('should be served with "."', function (done) {143 var dest = relative.split(path.sep).join('/')144 request(this.app)145 .get('/' + dest + '/todo.txt')146 .expect(200, '- groceries', done)147 })148 })149150 describe('acceptRanges', function () {151 describe('when false', function () {152 it('should not include Accept-Ranges', function (done) {153 request(createApp(fixtures, { 'acceptRanges': false }))154 .get('/nums.txt')155 .expect(utils.shouldNotHaveHeader('Accept-Ranges'))156 .expect(200, '123456789', done)157 })158159 it('should ignore Rage request header', function (done) {160 request(createApp(fixtures, { 'acceptRanges': false }))161 .get('/nums.txt')162 .set('Range', 'bytes=0-3')163 .expect(utils.shouldNotHaveHeader('Accept-Ranges'))164 .expect(utils.shouldNotHaveHeader('Content-Range'))165 .expect(200, '123456789', done)166 })167 })168169 describe('when true', function () {170 it('should include Accept-Ranges', function (done) {171 request(createApp(fixtures, { 'acceptRanges': true }))172 .get('/nums.txt')173 .expect('Accept-Ranges', 'bytes')174 .expect(200, '123456789', done)175 })176177 it('should obey Rage request header', function (done) {178 request(createApp(fixtures, { 'acceptRanges': true }))179 .get('/nums.txt')180 .set('Range', 'bytes=0-3')181 .expect('Accept-Ranges', 'bytes')182 .expect('Content-Range', 'bytes 0-3/9')183 .expect(206, '1234', done)184 })185 })186 })187188 describe('cacheControl', function () {189 describe('when false', function () {190 it('should not include Cache-Control', function (done) {191 request(createApp(fixtures, { 'cacheControl': false }))192 .get('/nums.txt')193 .expect(utils.shouldNotHaveHeader('Cache-Control'))194 .expect(200, '123456789', done)195 })196197 it('should ignore maxAge', function (done) {198 request(createApp(fixtures, { 'cacheControl': false, 'maxAge': 12000 }))199 .get('/nums.txt')200 .expect(utils.shouldNotHaveHeader('Cache-Control'))201 .expect(200, '123456789', done)202 })203 })204205 describe('when true', function () {206 it('should include Cache-Control', function (done) {207 request(createApp(fixtures, { 'cacheControl': true }))208 .get('/nums.txt')209 .expect('Cache-Control', 'public, max-age=0')210 .expect(200, '123456789', done)211 })212 })213 })214215 describe('extensions', function () {216 it('should be not be enabled by default', function (done) {217 request(createApp(fixtures))218 .get('/todo')219 .expect(404, done)220 })221222 it('should be configurable', function (done) {223 request(createApp(fixtures, { 'extensions': 'txt' }))224 .get('/todo')225 .expect(200, '- groceries', done)226 })227228 it('should support disabling extensions', function (done) {229 request(createApp(fixtures, { 'extensions': false }))230 .get('/todo')231 .expect(404, done)232 })233234 it('should support fallbacks', function (done) {235 request(createApp(fixtures, { 'extensions': ['htm', 'html', 'txt'] }))236 .get('/todo')237 .expect(200, '<li>groceries</li>', done)238 })239240 it('should 404 if nothing found', function (done) {241 request(createApp(fixtures, { 'extensions': ['htm', 'html', 'txt'] }))242 .get('/bob')243 .expect(404, done)244 })245 })246247 describe('fallthrough', function () {248 it('should default to true', function (done) {249 request(createApp())250 .get('/does-not-exist')251 .expect(404, 'Not Found', done)252 })253254 describe('when true', function () {255 before(function () {256 this.app = createApp(fixtures, { 'fallthrough': true })257 })258259 it('should fall-through when OPTIONS request', function (done) {260 request(this.app)261 .options('/todo.txt')262 .expect(404, 'Not Found', done)263 })264265 it('should fall-through when URL malformed', function (done) {266 request(this.app)267 .get('/%')268 .expect(404, 'Not Found', done)269 })270271 it('should fall-through when traversing past root', function (done) {272 request(this.app)273 .get('/users/../../todo.txt')274 .expect(404, 'Not Found', done)275 })276277 it('should fall-through when URL too long', function (done) {278 var app = express()279 var root = fixtures + Array(10000).join('/foobar')280281 app.use(express.static(root, { 'fallthrough': true }))282 app.use(function (req, res, next) {283 res.sendStatus(404)284 })285286 request(app)287 .get('/')288 .expect(404, 'Not Found', done)289 })290291 describe('with redirect: true', function () {292 before(function () {293 this.app = createApp(fixtures, { 'fallthrough': true, 'redirect': true })294 })295296 it('should fall-through when directory', function (done) {297 request(this.app)298 .get('/pets/')299 .expect(404, 'Not Found', done)300 })301302 it('should redirect when directory without slash', function (done) {303 request(this.app)304 .get('/pets')305 .expect(301, /Redirecting/, done)306 })307 })308309 describe('with redirect: false', function () {310 before(function () {311 this.app = createApp(fixtures, { 'fallthrough': true, 'redirect': false })312 })313314 it('should fall-through when directory', function (done) {315 request(this.app)316 .get('/pets/')317 .expect(404, 'Not Found', done)318 })319320 it('should fall-through when directory without slash', function (done) {321 request(this.app)322 .get('/pets')323 .expect(404, 'Not Found', done)324 })325 })326 })327328 describe('when false', function () {329 before(function () {330 this.app = createApp(fixtures, { 'fallthrough': false })331 })332333 it('should 405 when OPTIONS request', function (done) {334 request(this.app)335 .options('/todo.txt')336 .expect('Allow', 'GET, HEAD')337 .expect(405, done)338 })339340 it('should 400 when URL malformed', function (done) {341 request(this.app)342 .get('/%')343 .expect(400, /BadRequestError/, done)344 })345346 it('should 403 when traversing past root', function (done) {347 request(this.app)348 .get('/users/../../todo.txt')349 .expect(403, /ForbiddenError/, done)350 })351352 it('should 404 when URL too long', function (done) {353 var app = express()354 var root = fixtures + Array(10000).join('/foobar')355356 app.use(express.static(root, { 'fallthrough': false }))357 app.use(function (req, res, next) {358 res.sendStatus(404)359 })360361 request(app)362 .get('/')363 .expect(404, /ENAMETOOLONG/, done)364 })365366 describe('with redirect: true', function () {367 before(function () {368 this.app = createApp(fixtures, { 'fallthrough': false, 'redirect': true })369 })370371 it('should 404 when directory', function (done) {372 request(this.app)373 .get('/pets/')374 .expect(404, /NotFoundError|ENOENT/, done)375 })376377 it('should redirect when directory without slash', function (done) {378 request(this.app)379 .get('/pets')380 .expect(301, /Redirecting/, done)381 })382 })383384 describe('with redirect: false', function () {385 before(function () {386 this.app = createApp(fixtures, { 'fallthrough': false, 'redirect': false })387 })388389 it('should 404 when directory', function (done) {390 request(this.app)391 .get('/pets/')392 .expect(404, /NotFoundError|ENOENT/, done)393 })394395 it('should 404 when directory without slash', function (done) {396 request(this.app)397 .get('/pets')398 .expect(404, /NotFoundError|ENOENT/, done)399 })400 })401 })402 })403404 describe('hidden files', function () {405 before(function () {406 this.app = createApp(fixtures, { 'dotfiles': 'allow' })407 })408409 it('should be served when dotfiles: "allow" is given', function (done) {410 request(this.app)411 .get('/.name')412 .expect(200)413 .expect(utils.shouldHaveBody(Buffer.from('tobi')))414 .end(done)415 })416 })417418 describe('immutable', function () {419 it('should default to false', function (done) {420 request(createApp(fixtures))421 .get('/nums.txt')422 .expect('Cache-Control', 'public, max-age=0', done)423 })424425 it('should set immutable directive in Cache-Control', function (done) {426 request(createApp(fixtures, { 'immutable': true, 'maxAge': '1h' }))427 .get('/nums.txt')428 .expect('Cache-Control', 'public, max-age=3600, immutable', done)429 })430 })431432 describe('lastModified', function () {433 describe('when false', function () {434 it('should not include Last-Modified', function (done) {435 request(createApp(fixtures, { 'lastModified': false }))436 .get('/nums.txt')437 .expect(utils.shouldNotHaveHeader('Last-Modified'))438 .expect(200, '123456789', done)439 })440 })441442 describe('when true', function () {443 it('should include Last-Modified', function (done) {444 request(createApp(fixtures, { 'lastModified': true }))445 .get('/nums.txt')446 .expect('Last-Modified', /^\w{3}, \d+ \w+ \d+ \d+:\d+:\d+ \w+$/)447 .expect(200, '123456789', done)448 })449 })450 })451452 describe('maxAge', function () {453 it('should accept string', function (done) {454 request(createApp(fixtures, { 'maxAge': '30d' }))455 .get('/todo.txt')456 .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 30))457 .expect(200, done)458 })459460 it('should be reasonable when infinite', function (done) {461 request(createApp(fixtures, { 'maxAge': Infinity }))462 .get('/todo.txt')463 .expect('cache-control', 'public, max-age=' + (60 * 60 * 24 * 365))464 .expect(200, done)465 })466 })467468 describe('redirect', function () {469 before(function () {470 this.app = express()471 this.app.use(function (req, res, next) {472 req.originalUrl = req.url =473 req.originalUrl.replace(/\/snow(\/|$)/, '/snow \u2603$1')474 next()475 })476 this.app.use(express.static(fixtures))477 })478479 it('should redirect directories', function (done) {480 request(this.app)481 .get('/users')482 .expect('Location', '/users/')483 .expect(301, done)484 })485486 it('should include HTML link', function (done) {487 request(this.app)488 .get('/users')489 .expect('Location', '/users/')490 .expect(301, /\/users\//, done)491 })492493 it('should redirect directories with query string', function (done) {494 request(this.app)495 .get('/users?name=john')496 .expect('Location', '/users/?name=john')497 .expect(301, done)498 })499500 it('should not redirect to protocol-relative locations', function (done) {501 request(this.app)502 .get('//users')503 .expect('Location', '/users/')504 .expect(301, done)505 })506507 it('should ensure redirect URL is properly encoded', function (done) {508 request(this.app)509 .get('/snow')510 .expect('Location', '/snow%20%E2%98%83/')511 .expect('Content-Type', /html/)512 .expect(301, />Redirecting to \/snow%20%E2%98%83\/</, done)513 })514515 it('should respond with default Content-Security-Policy', function (done) {516 request(this.app)517 .get('/users')518 .expect('Content-Security-Policy', "default-src 'none'")519 .expect(301, done)520 })521522 it('should not redirect incorrectly', function (done) {523 request(this.app)524 .get('/')525 .expect(404, done)526 })527528 describe('when false', function () {529 before(function () {530 this.app = createApp(fixtures, { 'redirect': false })531 })532533 it('should disable redirect', function (done) {534 request(this.app)535 .get('/users')536 .expect(404, done)537 })538 })539 })540541 describe('setHeaders', function () {542 before(function () {543 this.app = express()544 this.app.use(express.static(fixtures, { 'setHeaders': function (res) {545 res.setHeader('x-custom', 'set')546 } }))547 })548549 it('should reject non-functions', function () {550 assert.throws(express.static.bind(null, fixtures, { 'setHeaders': 3 }), /setHeaders.*function/)551 })552553 it('should get called when sending file', function (done) {554 request(this.app)555 .get('/nums.txt')556 .expect('x-custom', 'set')557 .expect(200, done)558 })559560 it('should not get called on 404', function (done) {561 request(this.app)562 .get('/bogus')563 .expect(utils.shouldNotHaveHeader('x-custom'))564 .expect(404, done)565 })566567 it('should not get called on redirect', function (done) {568 request(this.app)569 .get('/users')570 .expect(utils.shouldNotHaveHeader('x-custom'))571 .expect(301, done)572 })573 })574575 describe('when traversing past root', function () {576 before(function () {577 this.app = createApp(fixtures, { 'fallthrough': false })578 })579580 it('should catch urlencoded ../', function (done) {581 request(this.app)582 .get('/users/%2e%2e/%2e%2e/todo.txt')583 .expect(403, done)584 })585586 it('should not allow root path disclosure', function (done) {587 request(this.app)588 .get('/users/../../fixtures/todo.txt')589 .expect(403, done)590 })591 })592593 describe('when request has "Range" header', function () {594 before(function () {595 this.app = createApp()596 })597598 it('should support byte ranges', function (done) {599 request(this.app)600 .get('/nums.txt')601 .set('Range', 'bytes=0-4')602 .expect('12345', done)603 })604605 it('should be inclusive', function (done) {606 request(this.app)607 .get('/nums.txt')608 .set('Range', 'bytes=0-0')609 .expect('1', done)610 })611612 it('should set Content-Range', function (done) {613 request(this.app)614 .get('/nums.txt')615 .set('Range', 'bytes=2-5')616 .expect('Content-Range', 'bytes 2-5/9', done)617 })618619 it('should support -n', function (done) {620 request(this.app)621 .get('/nums.txt')622 .set('Range', 'bytes=-3')623 .expect('789', done)624 })625626 it('should support n-', function (done) {627 request(this.app)628 .get('/nums.txt')629 .set('Range', 'bytes=3-')630 .expect('456789', done)631 })632633 it('should respond with 206 "Partial Content"', function (done) {634 request(this.app)635 .get('/nums.txt')636 .set('Range', 'bytes=0-4')637 .expect(206, done)638 })639640 it('should set Content-Length to the # of octets transferred', function (done) {641 request(this.app)642 .get('/nums.txt')643 .set('Range', 'bytes=2-3')644 .expect('Content-Length', '2')645 .expect(206, '34', done)646 })647648 describe('when last-byte-pos of the range is greater than current length', function () {649 it('is taken to be equal to one less than the current length', function (done) {650 request(this.app)651 .get('/nums.txt')652 .set('Range', 'bytes=2-50')653 .expect('Content-Range', 'bytes 2-8/9', done)654 })655656 it('should adapt the Content-Length accordingly', function (done) {657 request(this.app)658 .get('/nums.txt')659 .set('Range', 'bytes=2-50')660 .expect('Content-Length', '7')661 .expect(206, done)662 })663 })664665 describe('when the first- byte-pos of the range is greater than the current length', function () {666 it('should respond with 416', function (done) {667 request(this.app)668 .get('/nums.txt')669 .set('Range', 'bytes=9-50')670 .expect(416, done)671 })672673 it('should include a Content-Range header of complete length', function (done) {674 request(this.app)675 .get('/nums.txt')676 .set('Range', 'bytes=9-50')677 .expect('Content-Range', 'bytes */9')678 .expect(416, done)679 })680 })681682 describe('when syntactically invalid', function () {683 it('should respond with 200 and the entire contents', function (done) {684 request(this.app)685 .get('/nums.txt')686 .set('Range', 'asdf')687 .expect('123456789', done)688 })689 })690 })691692 describe('when index at mount point', function () {693 before(function () {694 this.app = express()695 this.app.use('/users', express.static(fixtures + '/users'))696 })697698 it('should redirect correctly', function (done) {699 request(this.app)700 .get('/users')701 .expect('Location', '/users/')702 .expect(301, done)703 })704 })705706 describe('when mounted', function () {707 before(function () {708 this.app = express()709 this.app.use('/static', express.static(fixtures))710 })711712 it('should redirect relative to the originalUrl', function (done) {713 request(this.app)714 .get('/static/users')715 .expect('Location', '/static/users/')716 .expect(301, done)717 })718719 it('should not choke on auth-looking URL', function (done) {720 request(this.app)721 .get('//todo@txt')722 .expect(404, done)723 })724 })725726 //727 // NOTE: This is not a real part of the API, but728 // over time this has become something users729 // are doing, so this will prevent unseen730 // regressions around this use-case.731 //732 describe('when mounted "root" as a file', function () {733 before(function () {734 this.app = express()735 this.app.use('/todo.txt', express.static(fixtures + '/todo.txt'))736 })737738 it('should load the file when on trailing slash', function (done) {739 request(this.app)740 .get('/todo.txt')741 .expect(200, '- groceries', done)742 })743744 it('should 404 when trailing slash', function (done) {745 request(this.app)746 .get('/todo.txt/')747 .expect(404, done)748 })749 })750751 describe('when responding non-2xx or 304', function () {752 it('should not alter the status', function (done) {753 var app = express()754755 app.use(function (req, res, next) {756 res.status(501)757 next()758 })759 app.use(express.static(fixtures))760761 request(app)762 .get('/todo.txt')763 .expect(501, '- groceries', done)764 })765 })766767 describe('when index file serving disabled', function () {768 before(function () {769 this.app = express()770 this.app.use('/static', express.static(fixtures, { 'index': false }))771 this.app.use(function (req, res, next) {772 res.sendStatus(404)773 })774 })775776 it('should next() on directory', function (done) {777 request(this.app)778 .get('/static/users/')779 .expect(404, 'Not Found', done)780 })781782 it('should redirect to trailing slash', function (done) {783 request(this.app)784 .get('/static/users')785 .expect('Location', '/static/users/')786 .expect(301, done)787 })788789 it('should next() on mount point', function (done) {790 request(this.app)791 .get('/static/')792 .expect(404, 'Not Found', done)793 })794795 it('should redirect to trailing slash mount point', function (done) {796 request(this.app)797 .get('/static')798 .expect('Location', '/static/')799 .expect(301, done)800 })801 })802})803804function createApp (dir, options, fn) {805 var app = express()806 var root = dir || fixtures807808 app.use(express.static(root, options))809810 app.use(function (req, res, next) {811 res.sendStatus(404)812 })813814 return app815}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.