strong-tie/inbound-calls
0
1'use strict'2 3const t = require('node:test')4const fs = require('node:fs')5const test = t.test6const zlib = require('node:zlib')7const express = require('express')8 9const inject = require('../index')10 11function accumulate (stream, cb) {12 const chunks = []13 stream.on('error', cb)14 stream.on('data', (chunk) => {15 chunks.push(chunk)16 })17 stream.on('end', () => {18 cb(null, Buffer.concat(chunks))19 })20}21 22test('stream mode - non-chunked payload', (t, done) => {23 t.plan(9)24 const output = 'example.com:8080|/hello'25 26 const dispatch = function (req, res) {27 res.statusMessage = 'Super'28 res.setHeader('x-extra', 'hello')29 res.writeHead(200, { 'Content-Type': 'text/plain', 'Content-Length': output.length })30 res.end(req.headers.host + '|' + req.url)31 }32 33 inject(dispatch, {34 url: 'http://example.com:8080/hello',35 payloadAsStream: true36 }, (err, res) => {37 t.assert.ifError(err)38 t.assert.strictEqual(res.statusCode, 200)39 t.assert.strictEqual(res.statusMessage, 'Super')40 t.assert.ok(res.headers.date)41 t.assert.deepStrictEqual(res.headers, {42 date: res.headers.date,43 connection: 'keep-alive',44 'x-extra': 'hello',45 'content-type': 'text/plain',46 'content-length': output.length.toString()47 })48 t.assert.strictEqual(res.payload, undefined)49 t.assert.strictEqual(res.rawPayload, undefined)50 51 accumulate(res.stream(), (err, payload) => {52 t.assert.ifError(err)53 t.assert.strictEqual(payload.toString(), 'example.com:8080|/hello')54 done()55 })56 })57})58 59test('stream mode - passes headers', (t, done) => {60 t.plan(3)61 const dispatch = function (req, res) {62 res.writeHead(200, { 'Content-Type': 'text/plain' })63 res.end(req.headers.super)64 }65 66 inject(dispatch, {67 method: 'GET',68 url: 'http://example.com:8080/hello',69 headers: { Super: 'duper' },70 payloadAsStream: true71 }, (err, res) => {72 t.assert.ifError(err)73 accumulate(res.stream(), (err, payload) => {74 t.assert.ifError(err)75 t.assert.strictEqual(payload.toString(), 'duper')76 done()77 })78 })79})80 81test('stream mode - returns chunked payload', (t, done) => {82 t.plan(6)83 const dispatch = function (_req, res) {84 res.writeHead(200, 'OK')85 res.write('a')86 res.write('b')87 res.end()88 }89 90 inject(dispatch, { method: 'GET', url: '/', payloadAsStream: true }, (err, res) => {91 t.assert.ifError(err)92 t.assert.ok(res.headers.date)93 t.assert.ok(res.headers.connection)94 t.assert.strictEqual(res.headers['transfer-encoding'], 'chunked')95 accumulate(res.stream(), (err, payload) => {96 t.assert.ifError(err)97 t.assert.strictEqual(payload.toString(), 'ab')98 done()99 })100 })101})102 103test('stream mode - backpressure', (t, done) => {104 t.plan(7)105 let expected106 const dispatch = function (_req, res) {107 res.writeHead(200, 'OK')108 res.write('a')109 const buf = Buffer.alloc(1024 * 1024).fill('b')110 t.assert.strictEqual(res.write(buf), false)111 expected = 'a' + buf.toString()112 res.on('drain', () => {113 res.end()114 })115 }116 117 inject(dispatch, { method: 'GET', url: '/', payloadAsStream: true }, (err, res) => {118 t.assert.ifError(err)119 t.assert.ok(res.headers.date)120 t.assert.ok(res.headers.connection)121 t.assert.strictEqual(res.headers['transfer-encoding'], 'chunked')122 accumulate(res.stream(), (err, payload) => {123 t.assert.ifError(err)124 t.assert.strictEqual(payload.toString(), expected)125 done()126 })127 })128})129 130test('stream mode - sets trailers in response object', (t, done) => {131 t.plan(4)132 const dispatch = function (_req, res) {133 res.setHeader('Trailer', 'Test')134 res.addTrailers({ Test: 123 })135 res.end()136 }137 138 inject(dispatch, { method: 'GET', url: '/', payloadAsStream: true }, (err, res) => {139 t.assert.ifError(err)140 t.assert.strictEqual(res.headers.trailer, 'Test')141 t.assert.strictEqual(res.headers.test, undefined)142 t.assert.strictEqual(res.trailers.test, '123')143 done()144 })145})146 147test('stream mode - parses zipped payload', (t, done) => {148 t.plan(5)149 const dispatch = function (_req, res) {150 res.writeHead(200, 'OK')151 const stream = fs.createReadStream('./package.json')152 stream.pipe(zlib.createGzip()).pipe(res)153 }154 155 inject(dispatch, { method: 'GET', url: '/', payloadAsStream: true }, (err, res) => {156 t.assert.ifError(err)157 fs.readFile('./package.json', { encoding: 'utf-8' }, (err, file) => {158 t.assert.ifError(err)159 160 accumulate(res.stream(), (err, payload) => {161 t.assert.ifError(err)162 163 zlib.unzip(payload, (err, unzipped) => {164 t.assert.ifError(err)165 t.assert.strictEqual(unzipped.toString('utf-8'), file)166 done()167 })168 })169 })170 })171})172 173test('stream mode - returns multi buffer payload', (t, done) => {174 t.plan(3)175 const dispatch = function (_req, res) {176 res.writeHead(200)177 res.write('a')178 res.write(Buffer.from('b'))179 res.end()180 }181 182 inject(dispatch, { method: 'GET', url: '/', payloadAsStream: true }, (err, res) => {183 t.assert.ifError(err)184 185 const chunks = []186 const stream = res.stream()187 stream.on('data', (chunk) => {188 chunks.push(chunk)189 })190 191 stream.on('end', () => {192 t.assert.strictEqual(chunks.length, 2)193 t.assert.strictEqual(Buffer.concat(chunks).toString(), 'ab')194 done()195 })196 })197})198 199test('stream mode - returns null payload', (t, done) => {200 t.plan(4)201 const dispatch = function (_req, res) {202 res.writeHead(200, { 'Content-Length': 0 })203 res.end()204 }205 206 inject(dispatch, { method: 'GET', url: '/', payloadAsStream: true }, (err, res) => {207 t.assert.ifError(err)208 t.assert.strictEqual(res.payload, undefined)209 accumulate(res.stream(), (err, payload) => {210 t.assert.ifError(err)211 t.assert.strictEqual(payload.toString(), '')212 done()213 })214 })215})216 217test('stream mode - simulates error', (t, done) => {218 t.plan(3)219 const dispatch = function (req, res) {220 req.on('readable', () => {221 })222 223 req.on('error', () => {224 res.writeHead(200, { 'Content-Length': 0 })225 res.end('error')226 })227 }228 229 const body = 'something special just for you'230 inject(dispatch, { method: 'GET', url: '/', payload: body, simulate: { error: true }, payloadAsStream: true }, (err, res) => {231 t.assert.ifError(err)232 accumulate(res.stream(), (err, payload) => {233 t.assert.ifError(err)234 t.assert.strictEqual(payload.toString(), 'error')235 done()236 })237 })238})239 240test('stream mode - promises support', (t, done) => {241 t.plan(1)242 const dispatch = function (_req, res) {243 res.writeHead(200, { 'Content-Type': 'text/plain' })244 res.end('hello')245 }246 247 inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', payloadAsStream: true })248 .then((res) => {249 return new Promise((resolve, reject) => {250 accumulate(res.stream(), (err, payload) => {251 if (err) {252 return reject(err)253 }254 resolve(payload)255 })256 })257 })258 .then(payload => t.assert.strictEqual(payload.toString(), 'hello'))259 .catch(t.assert.fail)260 .finally(done)261})262 263test('stream mode - Response.json() should throw', (t, done) => {264 t.plan(2)265 266 const jsonData = {267 a: 1,268 b: '2'269 }270 271 const dispatch = function (_req, res) {272 res.writeHead(200, { 'Content-Type': 'application/json' })273 res.end(JSON.stringify(jsonData))274 }275 276 inject(dispatch, { method: 'GET', path: 'http://example.com:8080/hello', payloadAsStream: true }, (err, res) => {277 t.assert.ifError(err)278 const { json } = res279 t.assert.throws(json, Error)280 done()281 })282})283 284test('stream mode - error for response destroy', (t, done) => {285 t.plan(2)286 287 const dispatch = function (_req, res) {288 res.writeHead(200)289 setImmediate(() => {290 res.destroy()291 })292 }293 294 inject(dispatch, { method: 'GET', url: '/', payloadAsStream: true }, (err, res) => {295 t.assert.ifError(err)296 accumulate(res.stream(), (err) => {297 t.assert.ok(err)298 done()299 })300 })301})302 303test('stream mode - request destroy with error', (t, done) => {304 t.plan(3)305 306 const fakeError = new Error('some-err')307 308 const dispatch = function (req) {309 req.destroy(fakeError)310 }311 312 inject(dispatch, { method: 'GET', url: '/', payloadAsStream: true }, (err, res) => {313 t.assert.ok(err)314 t.assert.strictEqual(err, fakeError)315 t.assert.strictEqual(res, null)316 done()317 })318})319 320test('stream mode - Can abort a request using AbortController/AbortSignal', async (t) => {321 const dispatch = function (_req, res) {322 res.writeHead(200)323 }324 325 const controller = new AbortController()326 const res = await inject(dispatch, {327 method: 'GET',328 url: 'http://example.com:8080/hello',329 signal: controller.signal,330 payloadAsStream: true331 })332 controller.abort()333 334 await t.assert.rejects(async () => {335 for await (const c of res.stream()) {336 t.assert.fail(`should not loop, got ${c.toString()}`)337 }338 }, Error)339}, { skip: globalThis.AbortController == null })340 341test("stream mode - passes payload when using express' send", (t, done) => {342 t.plan(4)343 344 const app = express()345 346 app.get('/hello', (_req, res) => {347 res.send('some text')348 })349 350 inject(app, { method: 'GET', url: 'http://example.com:8080/hello', payloadAsStream: true }, (err, res) => {351 t.assert.ifError(err)352 t.assert.strictEqual(res.headers['content-length'], '9')353 accumulate(res.stream(), function (err, payload) {354 t.assert.ifError(err)355 t.assert.strictEqual(payload.toString(), 'some text')356 done()357 })358 })359})360 