@ 261107.5
current
'use strict'

const { tspl } = require('@matteo.collina/tspl')
const { test, describe } = require('node:test')
const { stream, Agent, Client, interceptors: { redirect } } = require('..')
const {
  startRedirectingServer,
  startRedirectingWithBodyServer,
  startRedirectingChainServers,
  startRedirectingWithoutLocationServer,
  startRedirectingWithAuthorization,
  startRedirectingWithCookie
} = require('./utils/redirecting-servers')
const { createReadable, createWritable } = require('./utils/stream')

test('should always have a history with the final URL even if no redirections were followed', async t => {
  t = tspl(t, { plan: 4 })

  const body = []
  const server = await startRedirectingServer()

  await stream(
    `http://${server}/200?key=value`,
    { opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) },
    ({ statusCode, headers, opaque, context: { history } }) => {
      t.strictEqual(statusCode, 200)
      t.strictEqual(headers.location, undefined)
      t.deepStrictEqual(history.map(x => x.toString()), [
        `http://${server}/200?key=value`
      ])

      return createWritable(opaque)
    }
  )

  t.strictEqual(body.join(''), `GET /5 key=value :: host@${server} connection@keep-alive`)
})

test('should not follow redirection by default if max redirect = 0', async t => {
  t = tspl(t, { plan: 3 })

  const body = []
  const server = await startRedirectingServer()

  await stream(`http://${server}`, { opaque: body, dispatcher: new Agent({}).compose(redirect({ maxRedirections: 0 })) }, ({ statusCode, headers, opaque }) => {
    t.strictEqual(statusCode, 302)
    t.strictEqual(headers.location, `http://${server}/302/1`)

    return createWritable(opaque)
  })

  t.strictEqual(body.length, 0)
})

test('should follow redirection after a HTTP 300', async t => {
  t = tspl(t, { plan: 4 })

  const body = []
  const server = await startRedirectingServer()

  await stream(
    `http://${server}/300?key=value`,
    { opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) },
    ({ statusCode, headers, opaque, context: { history } }) => {
      t.strictEqual(statusCode, 200)
      t.strictEqual(headers.location, undefined)
      t.deepStrictEqual(history.map(x => x.toString()), [
        `http://${server}/300?key=value`,
        `http://${server}/300/1?key=value`,
        `http://${server}/300/2?key=value`,
        `http://${server}/300/3?key=value`,
        `http://${server}/300/4?key=value`,
        `http://${server}/300/5?key=value`
      ])

      return createWritable(opaque)
    }
  )

  t.strictEqual(body.join(''), `GET /5 key=value :: host@${server} connection@keep-alive`)
})

test('should follow redirection after a HTTP 301 changing method to GET', async t => {
  t = tspl(t, { plan: 3 })

  const body = []
  const server = await startRedirectingServer()

  await stream(
    `http://${server}/301`,
    { method: 'POST', body: 'REQUEST', opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) },
    ({ statusCode, headers, opaque }) => {
      t.strictEqual(statusCode, 200)
      t.strictEqual(headers.location, undefined)

      return createWritable(opaque)
    }
  )

  t.strictEqual(body.join(''), `GET /5 :: host@${server} connection@keep-alive`)
})

test('should follow redirection after a HTTP 302', async t => {
  t = tspl(t, { plan: 3 })

  const body = []
  const server = await startRedirectingServer()

  await stream(
    `http://${server}/302`,
    { method: 'PUT', body: Buffer.from('REQUEST'), opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) },
    ({ statusCode, headers, opaque }) => {
      t.strictEqual(statusCode, 200)
      t.strictEqual(headers.location, undefined)

      return createWritable(opaque)
    }
  )

  t.strictEqual(body.join(''), `PUT /5 :: host@${server} connection@keep-alive content-length@7 :: REQUEST`)
})

test('should follow redirection after a HTTP 303 changing method to GET', async t => {
  t = tspl(t, { plan: 3 })

  const body = []
  const server = await startRedirectingServer()

  await stream(`http://${server}/303`, { opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) }, ({ statusCode, headers, opaque }) => {
    t.strictEqual(statusCode, 200)
    t.strictEqual(headers.location, undefined)

    return createWritable(opaque)
  })

  t.strictEqual(body.join(''), `GET /5 :: host@${server} connection@keep-alive`)
})

test('should remove Host and request body related headers when following HTTP 303 (array)', async t => {
  t = tspl(t, { plan: 3 })

  const body = []
  const server = await startRedirectingServer()

  await stream(
    `http://${server}/303`,
    {
      method: 'PATCH',
      headers: [
        'Content-Encoding',
        'gzip',
        'X-Foo1',
        '1',
        'X-Foo2',
        '2',
        'Content-Type',
        'application/json',
        'X-Foo3',
        '3',
        'Host',
        'localhost',
        'X-Bar',
        '4'
      ],
      opaque: body,
      dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 }))
    },
    ({ statusCode, headers, opaque }) => {
      t.strictEqual(statusCode, 200)
      t.strictEqual(headers.location, undefined)

      return createWritable(opaque)
    }
  )

  t.strictEqual(body.join(''), `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`)
})

test('should remove Host and request body related headers when following HTTP 303 (object)', async t => {
  t = tspl(t, { plan: 3 })

  const body = []
  const server = await startRedirectingServer()

  await stream(
    `http://${server}/303`,
    {
      method: 'PATCH',
      headers: {
        'Content-Encoding': 'gzip',
        'X-Foo1': '1',
        'X-Foo2': '2',
        'Content-Type': 'application/json',
        'X-Foo3': '3',
        Host: 'localhost',
        'X-Bar': '4'
      },
      opaque: body,
      dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 }))
    },
    ({ statusCode, headers, opaque }) => {
      t.strictEqual(statusCode, 200)
      t.strictEqual(headers.location, undefined)

      return createWritable(opaque)
    }
  )

  t.strictEqual(body.join(''), `GET /5 :: host@${server} connection@keep-alive x-foo1@1 x-foo2@2 x-foo3@3 x-bar@4`)
})

test('should follow redirection after a HTTP 307', async t => {
  t = tspl(t, { plan: 3 })

  const body = []
  const server = await startRedirectingServer()

  await stream(
    `http://${server}/307`,
    { method: 'DELETE', opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) },
    ({ statusCode, headers, opaque }) => {
      t.strictEqual(statusCode, 200)
      t.strictEqual(headers.location, undefined)

      return createWritable(opaque)
    }
  )

  t.strictEqual(body.join(''), `DELETE /5 :: host@${server} connection@keep-alive`)
})

test('should follow redirection after a HTTP 308', async t => {
  t = tspl(t, { plan: 3 })

  const body = []
  const server = await startRedirectingServer()

  await stream(
    `http://${server}/308`,
    { method: 'OPTIONS', opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) },
    ({ statusCode, headers, opaque }) => {
      t.strictEqual(statusCode, 200)
      t.strictEqual(headers.location, undefined)

      return createWritable(opaque)
    }
  )

  t.strictEqual(body.join(''), `OPTIONS /5 :: host@${server} connection@keep-alive`)
})

test('should ignore HTTP 3xx response bodies', async t => {
  t = tspl(t, { plan: 4 })

  const body = []
  const server = await startRedirectingWithBodyServer()

  await stream(
    `http://${server}/`,
    { opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 10 })) },
    ({ statusCode, headers, opaque, context: { history } }) => {
      t.strictEqual(statusCode, 200)
      t.strictEqual(headers.location, undefined)
      t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/`, `http://${server}/end`])

      return createWritable(opaque)
    }
  )

  t.strictEqual(body.join(''), 'FINAL')
})

test('should follow a redirect chain up to the allowed number of times', async t => {
  t = tspl(t, { plan: 4 })

  const body = []
  const server = await startRedirectingServer()

  await stream(
    `http://${server}/300`,
    { opaque: body, dispatcher: new Client(`http://${server}/`).compose(redirect({ maxRedirections: 2 })) },
    ({ statusCode, headers, opaque, context: { history } }) => {
      t.strictEqual(statusCode, 300)
      t.strictEqual(headers.location, `http://${server}/300/3`)
      t.deepStrictEqual(history.map(x => x.toString()), [`http://${server}/300`, `http://${server}/300/1`, `http://${server}/300/2`])

      return createWritable(opaque)
    }
  )

  t.strictEqual(body.length, 0)
})

test('should follow redirections when going cross origin', async t => {
  t = tspl(t, { plan: 4 })

  const [server1, server2, server3] = await startRedirectingChainServers()
  const body = []

  await stream(
    `http://${server1}`,
    { method: 'POST', opaque: body, dispatcher: new Agent({}).compose(redirect({ maxRedirections: 10 })) },
    ({ statusCode, headers, opaque, context: { history } }) => {
      t.strictEqual(statusCode, 200)
      t.strictEqual(headers.location, undefined)
      t.deepStrictEqual(history.map(x => x.toString()), [
        `http://${server1}/`,
        `http://${server2}/`,
        `http://${server3}/`,
        `http://${server2}/end`,
        `http://${server3}/end`,
        `http://${server1}/end`
      ])

      return createWritable(opaque)
    }
  )

  t.strictEqual(body.join(''), 'GET')
})

describe('when a Location response header is NOT present', async () => {
  const redirectCodes = [300, 301, 302, 303, 307, 308]
  const server = await startRedirectingWithoutLocationServer()

  for (const code of redirectCodes) {
    test(`should return the original response after a HTTP ${code}`, async t => {
      t = tspl(t, { plan: 3 })

      const body = []

      await stream(
        `http://${server}/${code}`,
        { opaque: body },
        ({ statusCode, headers, opaque }) => {
          t.strictEqual(statusCode, code)
          t.strictEqual(headers.location, undefined)

          return createWritable(opaque)
        }
      )

      t.strictEqual(body.length, 0)
      await t.completed
    })
  }
})

test('should not follow redirects when using Readable request bodies', async t => {
  t = tspl(t, { plan: 3 })

  const body = []
  const server = await startRedirectingServer()

  await stream(
    `http://${server}`,
    {
      method: 'POST',
      body: createReadable('REQUEST'),
      opaque: body
    },
    ({ statusCode, headers, opaque }) => {
      t.strictEqual(statusCode, 302)
      t.strictEqual(headers.location, `http://${server}/302/1`)

      return createWritable(opaque)
    }
  )

  t.strictEqual(body.length, 0)
})

test('should handle errors', async t => {
  t = tspl(t, { plan: 2 })

  const body = []

  try {
    await stream('http://localhost:0', { opaque: body }, ({ statusCode, headers, opaque }) => {
      return createWritable(opaque)
    })

    throw new Error('Did not throw')
  } catch (error) {
    t.match(error.code, /EADDRNOTAVAIL|ECONNREFUSED/)
    t.strictEqual(body.length, 0)
  }
})

test('removes authorization header on third party origin', async t => {
  t = tspl(t, { plan: 1 })

  const body = []

  const [server1] = await startRedirectingWithAuthorization('secret')
  await stream(`http://${server1}`, {
    opaque: body,
    headers: {
      authorization: 'secret'
    }
  }, ({ statusCode, headers, opaque }) => createWritable(opaque))

  t.strictEqual(body.length, 0)
})

test('removes cookie header on third party origin', async t => {
  t = tspl(t, { plan: 1 })

  const body = []

  const [server1] = await startRedirectingWithCookie('a=b')
  await stream(`http://${server1}`, {
    opaque: body,
    headers: {
      cookie: 'a=b'
    }
  }, ({ statusCode, headers, opaque }) => createWritable(opaque))

  t.strictEqual(body.length, 0)
})