strong-tie/inbound-calls
0
1'use strict'2 3const { test } = require('node:test')4const { Readable, finished, pipeline } = require('node:stream')5const qs = require('node:querystring')6const fs = require('node:fs')7const zlib = require('node:zlib')8const http = require('node:http')9const eos = require('end-of-stream')10const express = require('express')11const multer = require('multer')12 13const inject = require('../index')14const parseURL = require('../lib/parse-url')15 16const NpmFormData = require('form-data')17const formAutoContent = require('form-auto-content')18const httpMethods = [19 'delete',20 'get',21 'head',22 'options',23 'patch',24 'post',25 'put',26 'trace'27]28 29test('returns non-chunked payload', (t, done) => {30 t.plan(7)31 const output = 'example.com:8080|/hello'32 33 const dispatch = function (req, res) {34 res.statusMessage = 'Super'35 res.setHeader('x-extra', 'hello')36 res.writeHead(200, { 'Content-Type': 'text/plain', 'Content-Length': output.length })37 res.end(req.headers.host + '|' + req.url)38 }39 40 inject(dispatch, 'http://example.com:8080/hello', (err, res) => {41 t.assert.ifError(err)42 t.assert.strictEqual(res.statusCode, 200)43 t.assert.strictEqual(res.statusMessage, 'Super')44 t.assert.ok(res.headers.date)45 t.assert.deepStrictEqual(res.headers, {46 date: res.headers.date,47 connection: 'keep-alive',48 'x-extra': 'hello',49 'content-type': 'text/plain',50 'content-length': output.length.toString()51 })52 t.assert.strictEqual(res.payload, output)53 t.assert.strictEqual(res.rawPayload.toString(), 'example.com:8080|/hello')54 done()55 })56})57 58test('returns single buffer payload', (t, done) => {59 t.plan(6)60 const dispatch = function (req, res) {61 res.writeHead(200, { 'Content-Type': 'text/plain' })62 res.end(req.headers.host + '|' + req.url)63 }64 65 inject(dispatch, { url: 'http://example.com:8080/hello' }, (err, res) => {66 t.assert.ifError(err)67 t.assert.ok(res.headers.date)68 t.assert.ok(res.headers.connection)69 t.assert.strictEqual(res.headers['transfer-encoding'], 'chunked')70 t.assert.strictEqual(res.payload, 'example.com:8080|/hello')71 t.assert.strictEqual(res.rawPayload.toString(), 'example.com:8080|/hello')72 done()73 })74})75 76test('passes headers', (t, done) => {77 t.plan(2)78 const dispatch = function (req, res) {79 res.writeHead(200, { 'Content-Type': 'text/plain' })80 res.end(req.headers.super)81 }82 83 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', headers: { Super: 'duper' } }, (err, res) => {84 t.assert.ifError(err)85 t.assert.strictEqual(res.payload, 'duper')86 done()87 })88})89 90test('request has rawHeaders', (t, done) => {91 t.plan(3)92 const dispatch = function (req, res) {93 t.assert.ok(Array.isArray(req.rawHeaders))94 t.assert.deepStrictEqual(req.rawHeaders, ['super', 'duper', 'user-agent', 'lightMyRequest', 'host', 'example.com:8080'])95 res.writeHead(200)96 res.end()97 }98 99 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', headers: { Super: 'duper' } }, (err) => {100 t.assert.ifError(err)101 done()102 })103})104 105test('request inherits from custom class', (t, done) => {106 t.plan(2)107 const dispatch = function (req, res) {108 t.assert.ok(req instanceof http.IncomingMessage)109 res.writeHead(200)110 res.end()111 }112 113 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', Request: http.IncomingMessage }, (err) => {114 t.assert.ifError(err)115 done()116 })117})118 119test('request with custom class preserves stream data', (t, done) => {120 t.plan(2)121 const dispatch = function (req, res) {122 t.assert.ok(req._readableState)123 res.writeHead(200)124 res.end()125 }126 127 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', Request: http.IncomingMessage }, (err) => {128 t.assert.ifError(err)129 done()130 })131})132 133test('assert Request option has a valid prototype', (t) => {134 t.plan(2)135 const dispatch = function (_req, res) {136 t.assert.ifError('should not get here')137 res.writeHead(500)138 res.end()139 }140 141 const MyInvalidRequest = {}142 143 t.assert.throws(() => {144 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', Request: MyInvalidRequest }, () => {})145 }, Error)146 147 t.assert.throws(() => {148 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', Request: 'InvalidRequest' }, () => {})149 }, Error)150})151 152test('passes remote address', (t, done) => {153 t.plan(2)154 const dispatch = function (req, res) {155 res.writeHead(200, { 'Content-Type': 'text/plain' })156 res.end(req.socket.remoteAddress)157 }158 159 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', remoteAddress: '1.2.3.4' }, (err, res) => {160 t.assert.ifError(err)161 t.assert.strictEqual(res.payload, '1.2.3.4')162 done()163 })164})165 166test('passes a socket which emits events like a normal one does', (t, done) => {167 t.plan(2)168 const dispatch = function (req, res) {169 res.writeHead(200, { 'Content-Type': 'text/plain' })170 req.socket.on('timeout', () => {})171 res.end('added')172 }173 174 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello' }, (err, res) => {175 t.assert.ifError(err)176 t.assert.strictEqual(res.payload, 'added')177 done()178 })179})180 181test('includes deprecated connection on request', (t, done) => {182 t.plan(3)183 const warnings = process.listeners('warning')184 process.removeAllListeners('warning')185 function onWarning (err) {186 t.assert.strictEqual(err.code, 'FST_LIGHTMYREQUEST_DEP01')187 return false188 }189 process.on('warning', onWarning)190 t.after(() => {191 process.removeListener('warning', onWarning)192 for (const fn of warnings) {193 process.on('warning', fn)194 }195 })196 const dispatch = function (req, res) {197 res.writeHead(200, { 'Content-Type': 'text/plain' })198 res.end(req.connection.remoteAddress)199 }200 201 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', remoteAddress: '1.2.3.4' }, (err, res) => {202 t.assert.ifError(err)203 t.assert.strictEqual(res.payload, '1.2.3.4')204 done()205 })206})207 208const parseQuery = url => {209 const parsedURL = parseURL(url)210 return qs.parse(parsedURL.search.slice(1))211}212 213test('passes query', (t, done) => {214 t.plan(2)215 216 const query = {217 message: 'OK',218 xs: ['foo', 'bar']219 }220 221 const dispatch = function (req, res) {222 res.writeHead(200, { 'Content-Type': 'text/plain' })223 res.end(req.url)224 }225 226 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', query }, (err, res) => {227 t.assert.ifError(err)228 t.assert.deepEqual(parseQuery(res.payload), query)229 done()230 })231})232 233test('query will be merged into that in url', (t, done) => {234 t.plan(2)235 236 const query = {237 xs: ['foo', 'bar']238 }239 240 const dispatch = function (req, res) {241 res.writeHead(200, { 'Content-Type': 'text/plain' })242 res.end(req.url)243 }244 245 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello?message=OK', query }, (err, res) => {246 t.assert.ifError(err)247 t.assert.deepEqual(parseQuery(res.payload), Object.assign({ message: 'OK' }, query))248 done()249 })250})251 252test('passes query as a string', (t, done) => {253 t.plan(2)254 255 const query = 'message=OK&xs=foo&xs=bar'256 257 const dispatch = function (req, res) {258 res.writeHead(200, { 'Content-Type': 'text/plain' })259 res.end(req.url)260 }261 262 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', query }, (err, res) => {263 t.assert.ifError(err)264 t.assert.deepEqual(parseQuery(res.payload), {265 message: 'OK',266 xs: ['foo', 'bar']267 })268 done()269 })270})271 272test('query as a string will be merged into that in url', (t, done) => {273 t.plan(2)274 275 const query = 'xs=foo&xs=bar'276 277 const dispatch = function (req, res) {278 res.writeHead(200, { 'Content-Type': 'text/plain' })279 res.end(req.url)280 }281 282 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello?message=OK', query }, (err, res) => {283 t.assert.ifError(err)284 t.assert.deepEqual(parseQuery(res.payload), Object.assign({ message: 'OK' }, {285 xs: ['foo', 'bar']286 }))287 done()288 })289})290 291test('passes localhost as default remote address', (t, done) => {292 t.plan(2)293 const dispatch = function (req, res) {294 res.writeHead(200, { 'Content-Type': 'text/plain' })295 res.end(req.socket.remoteAddress)296 }297 298 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello' }, (err, res) => {299 t.assert.ifError(err)300 t.assert.strictEqual(res.payload, '127.0.0.1')301 done()302 })303})304 305test('passes host option as host header', (t, done) => {306 t.plan(2)307 const dispatch = function (req, res) {308 res.writeHead(200, { 'Content-Type': 'text/plain' })309 res.end(req.headers.host)310 }311 312 inject(dispatch, { method: 'GET', url: '/hello', headers: { host: 'test.example.com' } }, (err, res) => {313 t.assert.ifError(err)314 t.assert.strictEqual(res.payload, 'test.example.com')315 done()316 })317})318 319test('passes localhost as default host header', (t, done) => {320 t.plan(2)321 const dispatch = function (req, res) {322 res.writeHead(200, { 'Content-Type': 'text/plain' })323 res.end(req.headers.host)324 }325 326 inject(dispatch, { method: 'GET', url: '/hello' }, (err, res) => {327 t.assert.ifError(err)328 t.assert.strictEqual(res.payload, 'localhost:80')329 done()330 })331})332 333test('passes authority as host header', (t, done) => {334 t.plan(2)335 const dispatch = function (req, res) {336 res.writeHead(200, { 'Content-Type': 'text/plain' })337 res.end(req.headers.host)338 }339 340 inject(dispatch, { method: 'GET', url: '/hello', authority: 'something' }, (err, res) => {341 t.assert.ifError(err)342 t.assert.strictEqual(res.payload, 'something')343 done()344 })345})346 347test('passes uri host as host header', (t, done) => {348 t.plan(2)349 const dispatch = function (req, res) {350 res.writeHead(200, { 'Content-Type': 'text/plain' })351 res.end(req.headers.host)352 }353 354 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello' }, (err, res) => {355 t.assert.ifError(err)356 t.assert.strictEqual(res.payload, 'example.com:8080')357 done()358 })359})360 361test('includes default http port in host header', (t, done) => {362 t.plan(2)363 const dispatch = function (req, res) {364 res.writeHead(200, { 'Content-Type': 'text/plain' })365 res.end(req.headers.host)366 }367 368 inject(dispatch, 'http://example.com', (err, res) => {369 t.assert.ifError(err)370 t.assert.strictEqual(res.payload, 'example.com:80')371 done()372 })373})374 375test('includes default https port in host header', (t, done) => {376 t.plan(2)377 const dispatch = function (req, res) {378 res.writeHead(200, { 'Content-Type': 'text/plain' })379 res.end(req.headers.host)380 }381 382 inject(dispatch, 'https://example.com', (err, res) => {383 t.assert.ifError(err)384 t.assert.strictEqual(res.payload, 'example.com:443')385 done()386 })387})388 389test('optionally accepts an object as url', (t, done) => {390 t.plan(5)391 const output = 'example.com:8080|/hello?test=1234'392 393 const dispatch = function (req, res) {394 res.writeHead(200, { 'Content-Type': 'text/plain', 'Content-Length': output.length })395 res.end(req.headers.host + '|' + req.url)396 }397 398 const url = {399 protocol: 'http',400 hostname: 'example.com',401 port: '8080',402 pathname: 'hello',403 query: {404 test: '1234'405 }406 }407 408 inject(dispatch, { url }, (err, res) => {409 t.assert.ifError(err)410 t.assert.ok(res.headers.date)411 t.assert.ok(res.headers.connection)412 t.assert.ifError(res.headers['transfer-encoding'])413 t.assert.strictEqual(res.payload, output)414 done()415 })416})417 418test('leaves user-agent unmodified', (t, done) => {419 t.plan(2)420 const dispatch = function (req, res) {421 res.writeHead(200, { 'Content-Type': 'text/plain' })422 res.end(req.headers['user-agent'])423 }424 425 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', headers: { 'user-agent': 'duper' } }, (err, res) => {426 t.assert.ifError(err)427 t.assert.strictEqual(res.payload, 'duper')428 done()429 })430})431 432test('returns chunked payload', (t, done) => {433 t.plan(5)434 const dispatch = function (_req, res) {435 res.writeHead(200, 'OK')436 res.write('a')437 res.write('b')438 res.end()439 }440 441 inject(dispatch, { method: 'GET', url: '/' }, (err, res) => {442 t.assert.ifError(err)443 t.assert.ok(res.headers.date)444 t.assert.ok(res.headers.connection)445 t.assert.strictEqual(res.headers['transfer-encoding'], 'chunked')446 t.assert.strictEqual(res.payload, 'ab')447 done()448 })449})450 451test('sets trailers in response object', (t, done) => {452 t.plan(4)453 const dispatch = function (_req, res) {454 res.setHeader('Trailer', 'Test')455 res.addTrailers({ Test: 123 })456 res.end()457 }458 459 inject(dispatch, { method: 'GET', url: '/' }, (err, res) => {460 t.assert.ifError(err)461 t.assert.strictEqual(res.headers.trailer, 'Test')462 t.assert.strictEqual(res.headers.test, undefined)463 t.assert.strictEqual(res.trailers.test, '123')464 done()465 })466})467 468test('parses zipped payload', (t, done) => {469 t.plan(4)470 const dispatch = function (_req, res) {471 res.writeHead(200, 'OK')472 const stream = fs.createReadStream('./package.json')473 stream.pipe(zlib.createGzip()).pipe(res)474 }475 476 inject(dispatch, { method: 'GET', url: '/' }, (err, res) => {477 t.assert.ifError(err)478 fs.readFile('./package.json', { encoding: 'utf-8' }, (err, file) => {479 t.assert.ifError(err)480 481 zlib.unzip(res.rawPayload, (err, unzipped) => {482 t.assert.ifError(err)483 t.assert.strictEqual(unzipped.toString('utf-8'), file)484 done()485 })486 })487 })488})489 490test('returns multi buffer payload', (t, done) => {491 t.plan(2)492 const dispatch = function (_req, res) {493 res.writeHead(200)494 res.write('a')495 res.write(Buffer.from('b'))496 res.end()497 }498 499 inject(dispatch, { method: 'GET', url: '/' }, (err, res) => {500 t.assert.ifError(err)501 t.assert.strictEqual(res.payload, 'ab')502 done()503 })504})505 506test('returns null payload', (t, done) => {507 t.plan(2)508 const dispatch = function (_req, res) {509 res.writeHead(200, { 'Content-Length': 0 })510 res.end()511 }512 513 inject(dispatch, { method: 'GET', url: '/' }, (err, res) => {514 t.assert.ifError(err)515 t.assert.strictEqual(res.payload, '')516 done()517 })518})519 520test('allows ending twice', (t, done) => {521 t.plan(2)522 const dispatch = function (_req, res) {523 res.writeHead(200, { 'Content-Length': 0 })524 res.end()525 res.end()526 }527 528 inject(dispatch, { method: 'GET', url: '/' }, (err, res) => {529 t.assert.ifError(err)530 t.assert.strictEqual(res.payload, '')531 done()532 })533})534 535test('identifies injection object', (t, done) => {536 t.plan(6)537 const dispatchRequest = function (req, res) {538 t.assert.strictEqual(inject.isInjection(req), true)539 t.assert.strictEqual(inject.isInjection(res), true)540 541 res.writeHead(200, { 'Content-Length': 0 })542 res.end()543 }544 545 const dispatchCustomRequest = function (req, res) {546 t.assert.strictEqual(inject.isInjection(req), true)547 t.assert.strictEqual(inject.isInjection(res), true)548 549 res.writeHead(200, { 'Content-Length': 0 })550 res.end()551 }552 553 const options = { method: 'GET', url: '/' }554 const cb = (err) => { t.assert.ifError(err) }555 const cbDone = (err) => {556 t.assert.ifError(err)557 done()558 }559 560 inject(dispatchRequest, options, cb)561 inject(dispatchCustomRequest, { ...options, Request: http.IncomingMessage }, cbDone)562})563 564test('pipes response', (t, done) => {565 t.plan(3)566 let finished = false567 const dispatch = function (_req, res) {568 res.writeHead(200)569 const stream = getTestStream()570 571 res.on('finish', () => {572 finished = true573 })574 575 stream.pipe(res)576 }577 578 inject(dispatch, { method: 'GET', url: '/' }, (err, res) => {579 t.assert.ifError(err)580 t.assert.strictEqual(finished, true)581 t.assert.strictEqual(res.payload, 'hi')582 done()583 })584})585 586test('pipes response with old stream', (t, done) => {587 t.plan(3)588 let finished = false589 const dispatch = function (_req, res) {590 res.writeHead(200)591 const stream = getTestStream()592 stream.pause()593 const stream2 = new Readable().wrap(stream)594 stream.resume()595 596 res.on('finish', () => {597 finished = true598 })599 600 stream2.pipe(res)601 }602 603 inject(dispatch, { method: 'GET', url: '/' }, (err, res) => {604 t.assert.ifError(err)605 t.assert.strictEqual(finished, true)606 t.assert.strictEqual(res.payload, 'hi')607 done()608 })609})610 611test('echos object payload', (t, done) => {612 t.plan(3)613 const dispatch = function (req, res) {614 res.writeHead(200, { 'content-type': req.headers['content-type'] })615 req.pipe(res)616 }617 618 inject(dispatch, { method: 'POST', url: '/test', payload: { a: 1 } }, (err, res) => {619 t.assert.ifError(err)620 t.assert.strictEqual(res.headers['content-type'], 'application/json')621 t.assert.strictEqual(res.payload, '{"a":1}')622 done()623 })624})625 626test('supports body option in Request and property in Response', (t, done) => {627 t.plan(3)628 const dispatch = function (req, res) {629 res.writeHead(200, { 'content-type': req.headers['content-type'] })630 req.pipe(res)631 }632 633 inject(dispatch, { method: 'POST', url: '/test', body: { a: 1 } }, (err, res) => {634 t.assert.ifError(err)635 t.assert.strictEqual(res.headers['content-type'], 'application/json')636 t.assert.strictEqual(res.body, '{"a":1}')637 done()638 })639})640 641test('echos buffer payload', (t, done) => {642 t.plan(2)643 const dispatch = function (req, res) {644 res.writeHead(200)645 req.pipe(res)646 }647 648 inject(dispatch, { method: 'POST', url: '/test', payload: Buffer.from('test!') }, (err, res) => {649 t.assert.ifError(err)650 t.assert.strictEqual(res.payload, 'test!')651 done()652 })653})654 655test('echos object payload with non-english utf-8 string', (t, done) => {656 t.plan(3)657 const dispatch = function (req, res) {658 res.writeHead(200, { 'content-type': req.headers['content-type'] })659 req.pipe(res)660 }661 662 inject(dispatch, { method: 'POST', url: '/test', payload: { a: '½½א' } }, (err, res) => {663 t.assert.ifError(err)664 t.assert.strictEqual(res.headers['content-type'], 'application/json')665 t.assert.strictEqual(res.payload, '{"a":"½½א"}')666 done()667 })668})669 670test('echos object payload without payload', (t, done) => {671 t.plan(2)672 const dispatch = function (req, res) {673 res.writeHead(200)674 req.pipe(res)675 }676 677 inject(dispatch, { method: 'POST', url: '/test' }, (err, res) => {678 t.assert.ifError(err)679 t.assert.strictEqual(res.payload, '')680 done()681 })682})683 684test('retains content-type header', (t, done) => {685 t.plan(3)686 const dispatch = function (req, res) {687 res.writeHead(200, { 'content-type': req.headers['content-type'] })688 req.pipe(res)689 }690 691 inject(dispatch, { method: 'POST', url: '/test', payload: { a: 1 }, headers: { 'content-type': 'something' } }, (err, res) => {692 t.assert.ifError(err)693 t.assert.strictEqual(res.headers['content-type'], 'something')694 t.assert.strictEqual(res.payload, '{"a":1}')695 done()696 })697})698 699test('adds a content-length header if none set when payload specified', (t, done) => {700 t.plan(2)701 const dispatch = function (req, res) {702 res.writeHead(200, { 'Content-Type': 'text/plain' })703 res.end(req.headers['content-length'])704 }705 706 inject(dispatch, { method: 'POST', url: '/test', payload: { a: 1 } }, (err, res) => {707 t.assert.ifError(err)708 t.assert.strictEqual(res.payload, '{"a":1}'.length.toString())709 done()710 })711})712 713test('retains a content-length header when payload specified', (t, done) => {714 t.plan(2)715 const dispatch = function (req, res) {716 res.writeHead(200, { 'Content-Type': 'text/plain' })717 res.end(req.headers['content-length'])718 }719 720 inject(dispatch, { method: 'POST', url: '/test', payload: '', headers: { 'content-length': '10' } }, (err, res) => {721 t.assert.ifError(err)722 t.assert.strictEqual(res.payload, '10')723 done()724 })725})726 727test('can handle a stream payload', (t, done) => {728 t.plan(2)729 const dispatch = function (req, res) {730 readStream(req, (buff) => {731 res.writeHead(200, { 'Content-Type': 'text/plain' })732 res.end(buff)733 })734 }735 736 inject(dispatch, { method: 'POST', url: '/', payload: getTestStream() }, (err, res) => {737 t.assert.ifError(err)738 t.assert.strictEqual(res.payload, 'hi')739 done()740 })741})742 743test('can handle a stream payload that errors', (t, done) => {744 t.plan(2)745 const dispatch = function (req) {746 req.resume()747 }748 749 const payload = new Readable({750 read () {751 this.destroy(new Error('kaboom'))752 }753 })754 755 inject(dispatch, { method: 'POST', url: '/', payload }, (err) => {756 t.assert.ok(err)757 t.assert.equal(err.message, 'kaboom')758 done()759 })760})761 762test('can handle a stream payload of utf-8 strings', (t, done) => {763 t.plan(2)764 const dispatch = function (req, res) {765 readStream(req, (buff) => {766 res.writeHead(200, { 'Content-Type': 'text/plain' })767 res.end(buff)768 })769 }770 771 inject(dispatch, { method: 'POST', url: '/', payload: getTestStream('utf8') }, (err, res) => {772 t.assert.ifError(err)773 t.assert.strictEqual(res.payload, 'hi')774 done()775 })776})777 778test('can override stream payload content-length header', (t, done) => {779 t.plan(2)780 const dispatch = function (req, res) {781 res.writeHead(200, { 'Content-Type': 'text/plain' })782 res.end(req.headers['content-length'])783 }784 785 const headers = { 'content-length': '100' }786 787 inject(dispatch, { method: 'POST', url: '/', payload: getTestStream(), headers }, (err, res) => {788 t.assert.ifError(err)789 t.assert.strictEqual(res.payload, '100')790 done()791 })792})793 794test('writeHead returns single buffer payload', (t, done) => {795 t.plan(4)796 const reply = 'Hello World'797 const statusCode = 200798 const statusMessage = 'OK'799 const dispatch = function (_req, res) {800 res.writeHead(statusCode, statusMessage, { 'Content-Type': 'text/plain', 'Content-Length': reply.length })801 res.end(reply)802 }803 804 inject(dispatch, { method: 'GET', url: '/' }, (err, res) => {805 t.assert.ifError(err)806 t.assert.strictEqual(res.statusCode, statusCode)807 t.assert.strictEqual(res.statusMessage, statusMessage)808 t.assert.strictEqual(res.payload, reply)809 done()810 })811})812 813test('_read() plays payload', (t, done) => {814 t.plan(2)815 const dispatch = function (req, res) {816 let buffer = ''817 req.on('readable', () => {818 buffer = buffer + (req.read() || '')819 })820 821 req.on('close', () => {822 })823 824 req.on('end', () => {825 res.writeHead(200, { 'Content-Length': 0 })826 res.end(buffer)827 req.destroy()828 })829 }830 831 const body = 'something special just for you'832 inject(dispatch, { method: 'GET', url: '/', payload: body }, (err, res) => {833 t.assert.ifError(err)834 t.assert.strictEqual(res.payload, body)835 done()836 })837})838 839test('simulates split', (t, done) => {840 t.plan(2)841 const dispatch = function (req, res) {842 let buffer = ''843 req.on('readable', () => {844 buffer = buffer + (req.read() || '')845 })846 847 req.on('close', () => {848 })849 850 req.on('end', () => {851 res.writeHead(200, { 'Content-Length': 0 })852 res.end(buffer)853 req.destroy()854 })855 }856 857 const body = 'something special just for you'858 inject(dispatch, { method: 'GET', url: '/', payload: body, simulate: { split: true } }, (err, res) => {859 t.assert.ifError(err)860 t.assert.strictEqual(res.payload, body)861 done()862 })863})864 865test('simulates error', (t, done) => {866 t.plan(2)867 const dispatch = function (req, res) {868 req.on('readable', () => {869 })870 871 req.on('error', () => {872 res.writeHead(200, { 'Content-Length': 0 })873 res.end('error')874 })875 }876 877 const body = 'something special just for you'878 inject(dispatch, { method: 'GET', url: '/', payload: body, simulate: { error: true } }, (err, res) => {879 t.assert.ifError(err)880 t.assert.strictEqual(res.payload, 'error')881 done()882 })883})884 885test('simulates no end without payload', (t, done) => {886 t.plan(2)887 let end = false888 const dispatch = function (req) {889 req.resume()890 req.on('end', () => {891 end = true892 })893 }894 895 let replied = false896 inject(dispatch, { method: 'GET', url: '/', simulate: { end: false } }, () => {897 replied = true898 })899 900 setTimeout(() => {901 t.assert.strictEqual(end, false)902 t.assert.strictEqual(replied, false)903 done()904 }, 10)905})906 907test('simulates no end with payload', (t, done) => {908 t.plan(2)909 let end = false910 const dispatch = function (req) {911 req.resume()912 req.on('end', () => {913 end = true914 })915 }916 917 let replied = false918 inject(dispatch, { method: 'GET', url: '/', payload: '1234567', simulate: { end: false } }, () => {919 replied = true920 })921 922 setTimeout(() => {923 t.assert.strictEqual(end, false)924 t.assert.strictEqual(replied, false)925 done()926 }, 10)927})928 929test('simulates close', (t, done) => {930 t.plan(2)931 const dispatch = function (req, res) {932 let buffer = ''933 req.on('readable', () => {934 buffer = buffer + (req.read() || '')935 })936 937 req.on('close', () => {938 res.writeHead(200, { 'Content-Length': 0 })939 res.end('close')940 })941 942 req.on('end', () => {943 })944 }945 946 const body = 'something special just for you'947 inject(dispatch, { method: 'GET', url: '/', payload: body, simulate: { close: true } }, (err, res) => {948 t.assert.ifError(err)949 t.assert.strictEqual(res.payload, 'close')950 done()951 })952})953 954test('errors for invalid input options', (t) => {955 t.plan(1)956 957 t.assert.throws(958 () => inject({}, {}, () => {}),959 { name: 'AssertionError', message: 'dispatchFunc should be a function' }960 )961})962 963test('errors for missing url', (t) => {964 t.plan(1)965 966 t.assert.throws(967 () => inject(() => {}, {}, () => {}),968 { message: /must have required property 'url'/ }969 )970})971 972test('errors for an incorrect simulation object', (t) => {973 t.plan(1)974 975 t.assert.throws(976 () => inject(() => {}, { url: '/', simulate: 'sample string' }, () => {}),977 { message: /^must be object$/ }978 )979})980 981test('ignores incorrect simulation object', (t) => {982 t.plan(1)983 984 t.assert.doesNotThrow(() => inject(() => { }, { url: '/', simulate: 'sample string', validate: false }, () => { }))985})986 987test('errors for an incorrect simulation object values', (t) => {988 t.plan(1)989 990 t.assert.throws(991 () => inject(() => {}, { url: '/', simulate: { end: 'wrong input' } }, () => {}),992 { message: /^must be boolean$/ }993 )994})995 996test('promises support', (t, done) => {997 t.plan(1)998 const dispatch = function (_req, res) {999 res.writeHead(200, { 'Content-Type': 'text/plain' })1000 res.end('hello')1001 }1002 1003 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello' })1004 .then(res => {1005 t.assert.strictEqual(res.payload, 'hello')1006 done()1007 })1008 .catch(t.assert.fail)1009})1010 1011test('this should be the server instance', (t, done) => {1012 t.plan(2)1013 1014 const server = http.createServer()1015 1016 const dispatch = function (_req, res) {1017 t.assert.strictEqual(this, server)1018 res.end('hello')1019 }1020 1021 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', server })1022 .then(res => t.assert.strictEqual(res.statusCode, 200))1023 .catch(t.assert.fail)1024 .finally(done)1025})1026 1027test('should handle response errors', (t, done) => {1028 t.plan(1)1029 const dispatch = function (_req, res) {1030 res.connection.destroy(new Error('kaboom'))1031 }1032 1033 inject(dispatch, 'http://example.com:8080/hello', (err) => {1034 t.assert.ok(err)1035 done()1036 })1037})1038 1039test('should handle response errors (promises)', async (t) => {1040 t.plan(1)1041 const dispatch = function (_req, res) {1042 res.connection.destroy(new Error('kaboom'))1043 }1044 1045 await t.assert.rejects(1046 () => inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello' }),1047 { name: 'Error', message: 'kaboom' }1048 )1049})1050 1051test('should handle response timeout handler', (t, done) => {1052 t.plan(3)1053 const dispatch = function (_req, res) {1054 const handle = setTimeout(() => {1055 res.writeHead(200, { 'Content-Type': 'text/plain' })1056 res.end('incorrect')1057 }, 200)1058 res.setTimeout(100, () => {1059 clearTimeout(handle)1060 res.writeHead(200, { 'Content-Type': 'text/plain' })1061 res.end('correct')1062 })1063 res.on('timeout', () => {1064 t.assert.ok(true, 'Response timeout event not emitted')1065 })1066 }1067 inject(dispatch, { method: 'GET', url: '/test' }, (err, res) => {1068 t.assert.ifError(err)1069 t.assert.strictEqual(res.payload, 'correct')1070 done()1071 })1072})1073 1074test('should throw on unknown HTTP method', (t) => {1075 t.plan(1)1076 const dispatch = function () { }1077 1078 t.assert.throws(() => inject(dispatch, { method: 'UNKNOWN_METHOD', url: 'http://example.com:8080/hello' }, (err, _res) => {1079 t.assert.ok(err)1080 }), Error)1081})1082 1083test('should throw on unknown HTTP method (promises)', (t) => {1084 t.plan(1)1085 const dispatch = function () { }1086 1087 t.assert.throws(() => inject(dispatch, { method: 'UNKNOWN_METHOD', url: 'http://example.com:8080/hello' })1088 .then(() => {}), Error)1089})1090 1091test('HTTP method is case insensitive', (t, done) => {1092 t.plan(3)1093 1094 const dispatch = function (_req, res) {1095 res.end('Hi!')1096 }1097 1098 inject(dispatch, { method: 'get', url: 'http://example.com:8080/hello' }, (err, res) => {1099 t.assert.ifError(err)1100 t.assert.strictEqual(res.statusCode, 200)1101 t.assert.strictEqual(res.payload, 'Hi!')1102 done()1103 })1104})1105 1106test('form-data should be handled correctly', (t, done) => {1107 t.plan(4)1108 1109 const dispatch = function (req, res) {1110 t.assert.strictEqual(req.headers['transfer-encoding'], undefined)1111 let body = ''1112 req.on('data', d => {1113 body += d1114 })1115 req.on('end', () => {1116 res.end(body)1117 })1118 }1119 1120 const form = new NpmFormData()1121 form.append('my_field', 'my value')1122 1123 inject(dispatch, {1124 method: 'POST',1125 url: 'http://example.com:8080/hello',1126 headers: {1127 // Transfer-encoding is automatically deleted if Stream1 is used1128 'transfer-encoding': 'chunked'1129 },1130 payload: form1131 }, (err, res) => {1132 t.assert.ifError(err)1133 t.assert.strictEqual(res.statusCode, 200)1134 t.assert.ok(/--.+\r\nContent-Disposition: form-data; name="my_field"\r\n\r\nmy value\r\n--.+--\r\n/.test(res.payload))1135 done()1136 })1137})1138 1139test('path as alias to url', (t, done) => {1140 t.plan(2)1141 1142 const dispatch = function (req, res) {1143 res.writeHead(200, { 'Content-Type': 'text/plain' })1144 res.end(req.url)1145 }1146 1147 inject(dispatch, { method: 'GET', path: 'http://example.com:8080/hello' }, (err, res) => {1148 t.assert.ifError(err)1149 t.assert.strictEqual(res.payload, '/hello')1150 done()1151 })1152})1153 1154test('Should throw if both path and url are missing', (t) => {1155 t.plan(1)1156 1157 t.assert.throws(1158 () => inject(() => {}, { method: 'GET' }, () => {}),1159 { message: /must have required property 'url',must have required property 'path'/ }1160 )1161})1162 1163test('chainable api: backwards compatibility for promise (then)', (t, done) => {1164 t.plan(1)1165 1166 const dispatch = function (_req, res) {1167 res.writeHead(200, { 'Content-Type': 'text/plain' })1168 res.end('hello')1169 }1170 1171 inject(dispatch)1172 .get('/')1173 .then(res => t.assert.strictEqual(res.payload, 'hello'))1174 .catch(t.assert.fail)1175 .finally(done)1176})1177 1178test('chainable api: backwards compatibility for promise (catch)', (t, done) => {1179 t.plan(1)1180 1181 function dispatch () {1182 throw Error1183 }1184 1185 inject(dispatch)1186 .get('/')1187 .catch(err => t.assert.ok(err))1188 .finally(done)1189})1190 1191test('chainable api: multiple call of then should return the same promise', (t, done) => {1192 t.plan(2)1193 let id = 01194 1195 function dispatch (_req, res) {1196 res.writeHead(200, { 'Content-Type': 'text/plain', 'Request-Id': id })1197 ++id1198 t.assert.ok('request id incremented')1199 res.end('hello')1200 }