Use let or const to avoid scope issues and hoisting
var assert = require('node:assert')
1'use strict'23var assert = require('node:assert')4var AsyncLocalStorage = require('node:async_hooks').AsyncLocalStorage5const { Buffer } = require('node:buffer');67var express = require('..')8var request = require('supertest')910describe('express.urlencoded()', function () {11 before(function () {12 this.app = createApp()13 })1415 it('should parse x-www-form-urlencoded', function (done) {16 request(this.app)17 .post('/')18 .set('Content-Type', 'application/x-www-form-urlencoded')19 .send('user=tobi')20 .expect(200, '{"user":"tobi"}', done)21 })2223 it('should 400 when invalid content-length', function (done) {24 var app = express()2526 app.use(function (req, res, next) {27 req.headers['content-length'] = '20' // bad length28 next()29 })3031 app.use(express.urlencoded())3233 app.post('/', function (req, res) {34 res.json(req.body)35 })3637 request(app)38 .post('/')39 .set('Content-Type', 'application/x-www-form-urlencoded')40 .send('str=')41 .expect(400, /content length/, done)42 })4344 it('should handle Content-Length: 0', function (done) {45 request(this.app)46 .post('/')47 .set('Content-Type', 'application/x-www-form-urlencoded')48 .set('Content-Length', '0')49 .send('')50 .expect(200, '{}', done)51 })5253 it('should handle empty message-body', function (done) {54 request(createApp({ limit: '1kb' }))55 .post('/')56 .set('Content-Type', 'application/x-www-form-urlencoded')57 .set('Transfer-Encoding', 'chunked')58 .send('')59 .expect(200, '{}', done)60 })6162 it('should handle duplicated middleware', function (done) {63 var app = express()6465 app.use(express.urlencoded())66 app.use(express.urlencoded())6768 app.post('/', function (req, res) {69 res.json(req.body)70 })7172 request(app)73 .post('/')74 .set('Content-Type', 'application/x-www-form-urlencoded')75 .send('user=tobi')76 .expect(200, '{"user":"tobi"}', done)77 })7879 it('should not parse extended syntax', function (done) {80 request(this.app)81 .post('/')82 .set('Content-Type', 'application/x-www-form-urlencoded')83 .send('user[name][first]=Tobi')84 .expect(200, '{"user[name][first]":"Tobi"}', done)85 })8687 describe('with extended option', function () {88 describe('when false', function () {89 before(function () {90 this.app = createApp({ extended: false })91 })9293 it('should not parse extended syntax', function (done) {94 request(this.app)95 .post('/')96 .set('Content-Type', 'application/x-www-form-urlencoded')97 .send('user[name][first]=Tobi')98 .expect(200, '{"user[name][first]":"Tobi"}', done)99 })100101 it('should parse multiple key instances', function (done) {102 request(this.app)103 .post('/')104 .set('Content-Type', 'application/x-www-form-urlencoded')105 .send('user=Tobi&user=Loki')106 .expect(200, '{"user":["Tobi","Loki"]}', done)107 })108 })109110 describe('when true', function () {111 before(function () {112 this.app = createApp({ extended: true })113 })114115 it('should parse multiple key instances', function (done) {116 request(this.app)117 .post('/')118 .set('Content-Type', 'application/x-www-form-urlencoded')119 .send('user=Tobi&user=Loki')120 .expect(200, '{"user":["Tobi","Loki"]}', done)121 })122123 it('should parse extended syntax', function (done) {124 request(this.app)125 .post('/')126 .set('Content-Type', 'application/x-www-form-urlencoded')127 .send('user[name][first]=Tobi')128 .expect(200, '{"user":{"name":{"first":"Tobi"}}}', done)129 })130131 it('should parse parameters with dots', function (done) {132 request(this.app)133 .post('/')134 .set('Content-Type', 'application/x-www-form-urlencoded')135 .send('user.name=Tobi')136 .expect(200, '{"user.name":"Tobi"}', done)137 })138139 it('should parse fully-encoded extended syntax', function (done) {140 request(this.app)141 .post('/')142 .set('Content-Type', 'application/x-www-form-urlencoded')143 .send('user%5Bname%5D%5Bfirst%5D=Tobi')144 .expect(200, '{"user":{"name":{"first":"Tobi"}}}', done)145 })146147 it('should parse array index notation', function (done) {148 request(this.app)149 .post('/')150 .set('Content-Type', 'application/x-www-form-urlencoded')151 .send('foo[0]=bar&foo[1]=baz')152 .expect(200, '{"foo":["bar","baz"]}', done)153 })154155 it('should parse array index notation with large array', function (done) {156 var str = 'f[0]=0'157158 for (var i = 1; i < 500; i++) {159 str += '&f[' + i + ']=' + i.toString(16)160 }161162 request(this.app)163 .post('/')164 .set('Content-Type', 'application/x-www-form-urlencoded')165 .send(str)166 .expect(function (res) {167 var obj = JSON.parse(res.text)168 assert.strictEqual(Object.keys(obj).length, 1)169 assert.strictEqual(Array.isArray(obj.f), true)170 assert.strictEqual(obj.f.length, 500)171 })172 .expect(200, done)173 })174175 it('should parse array of objects syntax', function (done) {176 request(this.app)177 .post('/')178 .set('Content-Type', 'application/x-www-form-urlencoded')179 .send('foo[0][bar]=baz&foo[0][fizz]=buzz&foo[]=done!')180 .expect(200, '{"foo":[{"bar":"baz","fizz":"buzz"},"done!"]}', done)181 })182183 it('should parse deep object', function (done) {184 var str = 'foo'185186 for (var i = 0; i < 32; i++) {187 str += '[p]'188 }189190 str += '=bar'191192 request(this.app)193 .post('/')194 .set('Content-Type', 'application/x-www-form-urlencoded')195 .send(str)196 .expect(function (res) {197 var obj = JSON.parse(res.text)198 assert.strictEqual(Object.keys(obj).length, 1)199 assert.strictEqual(typeof obj.foo, 'object')200201 var depth = 0202 var ref = obj.foo203 while ((ref = ref.p)) { depth++ }204 assert.strictEqual(depth, 32)205 })206 .expect(200, done)207 })208 })209 })210211 describe('with inflate option', function () {212 describe('when false', function () {213 before(function () {214 this.app = createApp({ inflate: false })215 })216217 it('should not accept content-encoding', function (done) {218 var test = request(this.app).post('/')219 test.set('Content-Encoding', 'gzip')220 test.set('Content-Type', 'application/x-www-form-urlencoded')221 test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex'))222 test.expect(415, '[encoding.unsupported] content encoding unsupported', done)223 })224 })225226 describe('when true', function () {227 before(function () {228 this.app = createApp({ inflate: true })229 })230231 it('should accept content-encoding', function (done) {232 var test = request(this.app).post('/')233 test.set('Content-Encoding', 'gzip')234 test.set('Content-Type', 'application/x-www-form-urlencoded')235 test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex'))236 test.expect(200, '{"name":"论"}', done)237 })238 })239 })240241 describe('with limit option', function () {242 it('should 413 when over limit with Content-Length', function (done) {243 var buf = Buffer.alloc(1024, '.')244 request(createApp({ limit: '1kb' }))245 .post('/')246 .set('Content-Type', 'application/x-www-form-urlencoded')247 .set('Content-Length', '1028')248 .send('str=' + buf.toString())249 .expect(413, done)250 })251252 it('should 413 when over limit with chunked encoding', function (done) {253 var app = createApp({ limit: '1kb' })254 var buf = Buffer.alloc(1024, '.')255 var test = request(app).post('/')256 test.set('Content-Type', 'application/x-www-form-urlencoded')257 test.set('Transfer-Encoding', 'chunked')258 test.write('str=')259 test.write(buf.toString())260 test.expect(413, done)261 })262263 it('should 413 when inflated body over limit', function (done) {264 var app = createApp({ limit: '1kb' })265 var test = request(app).post('/')266 test.set('Content-Encoding', 'gzip')267 test.set('Content-Type', 'application/x-www-form-urlencoded')268 test.write(Buffer.from('1f8b080000000000000a2b2e29b2d51b05a360148c580000a0351f9204040000', 'hex'))269 test.expect(413, done)270 })271272 it('should accept number of bytes', function (done) {273 var buf = Buffer.alloc(1024, '.')274 request(createApp({ limit: 1024 }))275 .post('/')276 .set('Content-Type', 'application/x-www-form-urlencoded')277 .send('str=' + buf.toString())278 .expect(413, done)279 })280281 it('should not change when options altered', function (done) {282 var buf = Buffer.alloc(1024, '.')283 var options = { limit: '1kb' }284 var app = createApp(options)285286 options.limit = '100kb'287288 request(app)289 .post('/')290 .set('Content-Type', 'application/x-www-form-urlencoded')291 .send('str=' + buf.toString())292 .expect(413, done)293 })294295 it('should not hang response', function (done) {296 var app = createApp({ limit: '8kb' })297 var buf = Buffer.alloc(10240, '.')298 var test = request(app).post('/')299 test.set('Content-Type', 'application/x-www-form-urlencoded')300 test.write(buf)301 test.write(buf)302 test.write(buf)303 test.expect(413, done)304 })305306 it('should not error when inflating', function (done) {307 var app = createApp({ limit: '1kb' })308 var test = request(app).post('/')309 test.set('Content-Encoding', 'gzip')310 test.set('Content-Type', 'application/x-www-form-urlencoded')311 test.write(Buffer.from('1f8b080000000000000a2b2e29b2d51b05a360148c580000a0351f92040400', 'hex'))312 test.expect(413, done)313 })314 })315316 describe('with parameterLimit option', function () {317 describe('with extended: false', function () {318 it('should reject 0', function () {319 assert.throws(createApp.bind(null, { extended: false, parameterLimit: 0 }),320 /TypeError: option parameterLimit must be a positive number/)321 })322323 it('should reject string', function () {324 assert.throws(createApp.bind(null, { extended: false, parameterLimit: 'beep' }),325 /TypeError: option parameterLimit must be a positive number/)326 })327328 it('should 413 if over limit', function (done) {329 request(createApp({ extended: false, parameterLimit: 10 }))330 .post('/')331 .set('Content-Type', 'application/x-www-form-urlencoded')332 .send(createManyParams(11))333 .expect(413, '[parameters.too.many] too many parameters', done)334 })335336 it('should work when at the limit', function (done) {337 request(createApp({ extended: false, parameterLimit: 10 }))338 .post('/')339 .set('Content-Type', 'application/x-www-form-urlencoded')340 .send(createManyParams(10))341 .expect(expectKeyCount(10))342 .expect(200, done)343 })344345 it('should work if number is floating point', function (done) {346 request(createApp({ extended: false, parameterLimit: 10.1 }))347 .post('/')348 .set('Content-Type', 'application/x-www-form-urlencoded')349 .send(createManyParams(11))350 .expect(413, /too many parameters/, done)351 })352353 it('should work with large limit', function (done) {354 request(createApp({ extended: false, parameterLimit: 5000 }))355 .post('/')356 .set('Content-Type', 'application/x-www-form-urlencoded')357 .send(createManyParams(5000))358 .expect(expectKeyCount(5000))359 .expect(200, done)360 })361362 it('should work with Infinity limit', function (done) {363 request(createApp({ extended: false, parameterLimit: Infinity }))364 .post('/')365 .set('Content-Type', 'application/x-www-form-urlencoded')366 .send(createManyParams(10000))367 .expect(expectKeyCount(10000))368 .expect(200, done)369 })370 })371372 describe('with extended: true', function () {373 it('should reject 0', function () {374 assert.throws(createApp.bind(null, { extended: true, parameterLimit: 0 }),375 /TypeError: option parameterLimit must be a positive number/)376 })377378 it('should reject string', function () {379 assert.throws(createApp.bind(null, { extended: true, parameterLimit: 'beep' }),380 /TypeError: option parameterLimit must be a positive number/)381 })382383 it('should 413 if over limit', function (done) {384 request(createApp({ extended: true, parameterLimit: 10 }))385 .post('/')386 .set('Content-Type', 'application/x-www-form-urlencoded')387 .send(createManyParams(11))388 .expect(413, '[parameters.too.many] too many parameters', done)389 })390391 it('should work when at the limit', function (done) {392 request(createApp({ extended: true, parameterLimit: 10 }))393 .post('/')394 .set('Content-Type', 'application/x-www-form-urlencoded')395 .send(createManyParams(10))396 .expect(expectKeyCount(10))397 .expect(200, done)398 })399400 it('should work if number is floating point', function (done) {401 request(createApp({ extended: true, parameterLimit: 10.1 }))402 .post('/')403 .set('Content-Type', 'application/x-www-form-urlencoded')404 .send(createManyParams(11))405 .expect(413, /too many parameters/, done)406 })407408 it('should work with large limit', function (done) {409 request(createApp({ extended: true, parameterLimit: 5000 }))410 .post('/')411 .set('Content-Type', 'application/x-www-form-urlencoded')412 .send(createManyParams(5000))413 .expect(expectKeyCount(5000))414 .expect(200, done)415 })416417 it('should work with Infinity limit', function (done) {418 request(createApp({ extended: true, parameterLimit: Infinity }))419 .post('/')420 .set('Content-Type', 'application/x-www-form-urlencoded')421 .send(createManyParams(10000))422 .expect(expectKeyCount(10000))423 .expect(200, done)424 })425 })426 })427428 describe('with type option', function () {429 describe('when "application/vnd.x-www-form-urlencoded"', function () {430 before(function () {431 this.app = createApp({ type: 'application/vnd.x-www-form-urlencoded' })432 })433434 it('should parse for custom type', function (done) {435 request(this.app)436 .post('/')437 .set('Content-Type', 'application/vnd.x-www-form-urlencoded')438 .send('user=tobi')439 .expect(200, '{"user":"tobi"}', done)440 })441442 it('should ignore standard type', function (done) {443 request(this.app)444 .post('/')445 .set('Content-Type', 'application/x-www-form-urlencoded')446 .send('user=tobi')447 .expect(200, '', done)448 })449 })450451 describe('when ["urlencoded", "application/x-pairs"]', function () {452 before(function () {453 this.app = createApp({454 type: ['urlencoded', 'application/x-pairs']455 })456 })457458 it('should parse "application/x-www-form-urlencoded"', function (done) {459 request(this.app)460 .post('/')461 .set('Content-Type', 'application/x-www-form-urlencoded')462 .send('user=tobi')463 .expect(200, '{"user":"tobi"}', done)464 })465466 it('should parse "application/x-pairs"', function (done) {467 request(this.app)468 .post('/')469 .set('Content-Type', 'application/x-pairs')470 .send('user=tobi')471 .expect(200, '{"user":"tobi"}', done)472 })473474 it('should ignore application/x-foo', function (done) {475 request(this.app)476 .post('/')477 .set('Content-Type', 'application/x-foo')478 .send('user=tobi')479 .expect(200, '', done)480 })481 })482483 describe('when a function', function () {484 it('should parse when truthy value returned', function (done) {485 var app = createApp({ type: accept })486487 function accept (req) {488 return req.headers['content-type'] === 'application/vnd.something'489 }490491 request(app)492 .post('/')493 .set('Content-Type', 'application/vnd.something')494 .send('user=tobi')495 .expect(200, '{"user":"tobi"}', done)496 })497498 it('should work without content-type', function (done) {499 var app = createApp({ type: accept })500501 function accept (req) {502 return true503 }504505 var test = request(app).post('/')506 test.write('user=tobi')507 test.expect(200, '{"user":"tobi"}', done)508 })509510 it('should not invoke without a body', function (done) {511 var app = createApp({ type: accept })512513 function accept (req) {514 throw new Error('oops!')515 }516517 request(app)518 .get('/')519 .expect(404, done)520 })521 })522 })523524 describe('with verify option', function () {525 it('should assert value if function', function () {526 assert.throws(createApp.bind(null, { verify: 'lol' }),527 /TypeError: option verify must be function/)528 })529530 it('should error from verify', function (done) {531 var app = createApp({532 verify: function (req, res, buf) {533 if (buf[0] === 0x20) throw new Error('no leading space')534 }535 })536537 request(app)538 .post('/')539 .set('Content-Type', 'application/x-www-form-urlencoded')540 .send(' user=tobi')541 .expect(403, '[entity.verify.failed] no leading space', done)542 })543544 it('should allow custom codes', function (done) {545 var app = createApp({546 verify: function (req, res, buf) {547 if (buf[0] !== 0x20) return548 var err = new Error('no leading space')549 err.status = 400550 throw err551 }552 })553554 request(app)555 .post('/')556 .set('Content-Type', 'application/x-www-form-urlencoded')557 .send(' user=tobi')558 .expect(400, '[entity.verify.failed] no leading space', done)559 })560561 it('should allow custom type', function (done) {562 var app = createApp({563 verify: function (req, res, buf) {564 if (buf[0] !== 0x20) return565 var err = new Error('no leading space')566 err.type = 'foo.bar'567 throw err568 }569 })570571 request(app)572 .post('/')573 .set('Content-Type', 'application/x-www-form-urlencoded')574 .send(' user=tobi')575 .expect(403, '[foo.bar] no leading space', done)576 })577578 it('should allow pass-through', function (done) {579 var app = createApp({580 verify: function (req, res, buf) {581 if (buf[0] === 0x5b) throw new Error('no arrays')582 }583 })584585 request(app)586 .post('/')587 .set('Content-Type', 'application/x-www-form-urlencoded')588 .send('user=tobi')589 .expect(200, '{"user":"tobi"}', done)590 })591592 it('should 415 on unknown charset prior to verify', function (done) {593 var app = createApp({594 verify: function (req, res, buf) {595 throw new Error('unexpected verify call')596 }597 })598599 var test = request(app).post('/')600 test.set('Content-Type', 'application/x-www-form-urlencoded; charset=x-bogus')601 test.write(Buffer.from('00000000', 'hex'))602 test.expect(415, '[charset.unsupported] unsupported charset "X-BOGUS"', done)603 })604 })605606 describe('async local storage', function () {607 before(function () {608 var app = express()609 var store = { foo: 'bar' }610611 app.use(function (req, res, next) {612 req.asyncLocalStorage = new AsyncLocalStorage()613 req.asyncLocalStorage.run(store, next)614 })615616 app.use(express.urlencoded())617618 app.use(function (req, res, next) {619 var local = req.asyncLocalStorage.getStore()620621 if (local) {622 res.setHeader('x-store-foo', String(local.foo))623 }624625 next()626 })627628 app.use(function (err, req, res, next) {629 var local = req.asyncLocalStorage.getStore()630631 if (local) {632 res.setHeader('x-store-foo', String(local.foo))633 }634635 res.status(err.status || 500)636 res.send('[' + err.type + '] ' + err.message)637 })638639 app.post('/', function (req, res) {640 res.json(req.body)641 })642643 this.app = app644 })645646 it('should persist store', function (done) {647 request(this.app)648 .post('/')649 .set('Content-Type', 'application/x-www-form-urlencoded')650 .send('user=tobi')651 .expect(200)652 .expect('x-store-foo', 'bar')653 .expect('{"user":"tobi"}')654 .end(done)655 })656657 it('should persist store when unmatched content-type', function (done) {658 request(this.app)659 .post('/')660 .set('Content-Type', 'application/fizzbuzz')661 .send('buzz')662 .expect(200)663 .expect('x-store-foo', 'bar')664 .end(done)665 })666667 it('should persist store when inflated', function (done) {668 var test = request(this.app).post('/')669 test.set('Content-Encoding', 'gzip')670 test.set('Content-Type', 'application/x-www-form-urlencoded')671 test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex'))672 test.expect(200)673 test.expect('x-store-foo', 'bar')674 test.expect('{"name":"论"}')675 test.end(done)676 })677678 it('should persist store when inflate error', function (done) {679 var test = request(this.app).post('/')680 test.set('Content-Encoding', 'gzip')681 test.set('Content-Type', 'application/x-www-form-urlencoded')682 test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad6080000', 'hex'))683 test.expect(400)684 test.expect('x-store-foo', 'bar')685 test.end(done)686 })687688 it('should persist store when limit exceeded', function (done) {689 request(this.app)690 .post('/')691 .set('Content-Type', 'application/x-www-form-urlencoded')692 .send('user=' + Buffer.alloc(1024 * 100, '.').toString())693 .expect(413)694 .expect('x-store-foo', 'bar')695 .end(done)696 })697 })698699 describe('charset', function () {700 before(function () {701 this.app = createApp()702 })703704 it('should parse utf-8', function (done) {705 var test = request(this.app).post('/')706 test.set('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8')707 test.write(Buffer.from('6e616d653de8aeba', 'hex'))708 test.expect(200, '{"name":"论"}', done)709 })710711 it('should parse when content-length != char length', function (done) {712 var test = request(this.app).post('/')713 test.set('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8')714 test.set('Content-Length', '7')715 test.write(Buffer.from('746573743dc3a5', 'hex'))716 test.expect(200, '{"test":"å"}', done)717 })718719 it('should default to utf-8', function (done) {720 var test = request(this.app).post('/')721 test.set('Content-Type', 'application/x-www-form-urlencoded')722 test.write(Buffer.from('6e616d653de8aeba', 'hex'))723 test.expect(200, '{"name":"论"}', done)724 })725726 it('should fail on unknown charset', function (done) {727 var test = request(this.app).post('/')728 test.set('Content-Type', 'application/x-www-form-urlencoded; charset=koi8-r')729 test.write(Buffer.from('6e616d653dcec5d4', 'hex'))730 test.expect(415, '[charset.unsupported] unsupported charset "KOI8-R"', done)731 })732 })733734 describe('encoding', function () {735 before(function () {736 this.app = createApp({ limit: '10kb' })737 })738739 it('should parse without encoding', function (done) {740 var test = request(this.app).post('/')741 test.set('Content-Type', 'application/x-www-form-urlencoded')742 test.write(Buffer.from('6e616d653de8aeba', 'hex'))743 test.expect(200, '{"name":"论"}', done)744 })745746 it('should support identity encoding', function (done) {747 var test = request(this.app).post('/')748 test.set('Content-Encoding', 'identity')749 test.set('Content-Type', 'application/x-www-form-urlencoded')750 test.write(Buffer.from('6e616d653de8aeba', 'hex'))751 test.expect(200, '{"name":"论"}', done)752 })753754 it('should support gzip encoding', function (done) {755 var test = request(this.app).post('/')756 test.set('Content-Encoding', 'gzip')757 test.set('Content-Type', 'application/x-www-form-urlencoded')758 test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex'))759 test.expect(200, '{"name":"论"}', done)760 })761762 it('should support deflate encoding', function (done) {763 var test = request(this.app).post('/')764 test.set('Content-Encoding', 'deflate')765 test.set('Content-Type', 'application/x-www-form-urlencoded')766 test.write(Buffer.from('789ccb4bcc4db57db16e17001068042f', 'hex'))767 test.expect(200, '{"name":"论"}', done)768 })769770 it('should be case-insensitive', function (done) {771 var test = request(this.app).post('/')772 test.set('Content-Encoding', 'GZIP')773 test.set('Content-Type', 'application/x-www-form-urlencoded')774 test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex'))775 test.expect(200, '{"name":"论"}', done)776 })777778 it('should 415 on unknown encoding', function (done) {779 var test = request(this.app).post('/')780 test.set('Content-Encoding', 'nulls')781 test.set('Content-Type', 'application/x-www-form-urlencoded')782 test.write(Buffer.from('000000000000', 'hex'))783 test.expect(415, '[encoding.unsupported] unsupported content encoding "nulls"', done)784 })785 })786})787788function createManyParams (count) {789 var str = ''790791 if (count === 0) {792 return str793 }794795 str += '0=0'796797 for (var i = 1; i < count; i++) {798 var n = i.toString(36)799 str += '&' + n + '=' + n800 }801802 return str803}804805function createApp (options) {806 var app = express()807808 app.use(express.urlencoded(options))809810 app.use(function (err, req, res, next) {811 res.status(err.status || 500)812 res.send(String(req.headers['x-error-property']813 ? err[req.headers['x-error-property']]814 : ('[' + err.type + '] ' + err.message)))815 })816817 app.post('/', function (req, res) {818 res.json(req.body)819 })820821 return app822}823824function expectKeyCount (count) {825 return function (res) {826 assert.strictEqual(Object.keys(JSON.parse(res.text)).length, count)827 }828}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.