current commit
260416.3 · fix(fetch): use || for CRLF check in multipart formdata-parser (#5049)
'use strict'

const { test } = require('node:test')
const { FormData, Response, Request } = require('../../')
const { isFormDataLike } = require('../../lib/core/util')

test('arg validation', (t) => {
  const form = new FormData()

  // constructor
  t.assert.throws(() => {
    // eslint-disable-next-line
    new FormData('asd')
  }, TypeError)

  // append
  t.assert.throws(() => {
    FormData.prototype.append.call(null)
  }, TypeError)
  t.assert.throws(() => {
    form.append()
  }, TypeError)
  t.assert.throws(() => {
    form.append('k', 'not usv', '')
  }, TypeError)

  // delete
  t.assert.throws(() => {
    FormData.prototype.delete.call(null)
  }, TypeError)
  t.assert.throws(() => {
    form.delete()
  }, TypeError)

  // get
  t.assert.throws(() => {
    FormData.prototype.get.call(null)
  }, TypeError)
  t.assert.throws(() => {
    form.get()
  }, TypeError)

  // getAll
  t.assert.throws(() => {
    FormData.prototype.getAll.call(null)
  }, TypeError)
  t.assert.throws(() => {
    form.getAll()
  }, TypeError)

  // has
  t.assert.throws(() => {
    FormData.prototype.has.call(null)
  }, TypeError)
  t.assert.throws(() => {
    form.has()
  }, TypeError)

  // set
  t.assert.throws(() => {
    FormData.prototype.set.call(null)
  }, TypeError)
  t.assert.throws(() => {
    form.set('k')
  }, TypeError)
  t.assert.throws(() => {
    form.set('k', 'not usv', '')
  }, TypeError)

  // iterator
  t.assert.throws(() => {
    Reflect.apply(FormData.prototype[Symbol.iterator], null)
  }, TypeError)

  // toStringTag
  t.assert.doesNotThrow(() => {
    FormData.prototype[Symbol.toStringTag].charAt(0)
  })
})

test('set blob', (t) => {
  const form = new FormData()

  form.set('key', new Blob([]), undefined)
  t.assert.strictEqual(form.get('key').name, 'blob')

  form.set('key1', new Blob([]), null)
  t.assert.strictEqual(form.get('key1').name, 'null')
})

test('append file', (t) => {
  const form = new FormData()
  form.set('asd', new File([], 'asd1', { type: 'text/plain' }), 'asd2')
  form.append('asd2', new File([], 'asd1'), 'asd2')

  t.assert.strictEqual(form.has('asd'), true)
  t.assert.strictEqual(form.has('asd2'), true)
  t.assert.strictEqual(form.get('asd').name, 'asd2')
  t.assert.strictEqual(form.get('asd2').name, 'asd2')
  t.assert.strictEqual(form.get('asd').type, 'text/plain')
  form.delete('asd')
  t.assert.strictEqual(form.get('asd'), null)
  t.assert.strictEqual(form.has('asd2'), true)
  t.assert.strictEqual(form.has('asd'), false)
})

test('append blob', async (t) => {
  const form = new FormData()
  form.set('asd', new Blob(['asd1'], { type: 'text/plain' }))

  t.assert.strictEqual(form.has('asd'), true)
  t.assert.strictEqual(form.get('asd').type, 'text/plain')
  t.assert.strictEqual(await form.get('asd').text(), 'asd1')
  form.delete('asd')
  t.assert.strictEqual(form.get('asd'), null)

  form.append('key', new Blob([]), undefined)
  t.assert.strictEqual(form.get('key').name, 'blob')

  form.append('key1', new Blob([]), null)
  t.assert.strictEqual(form.get('key1').name, 'null')
})

test('append string', (t) => {
  const form = new FormData()
  form.set('k1', 'v1')
  form.set('k2', 'v2')
  t.assert.deepStrictEqual([...form], [['k1', 'v1'], ['k2', 'v2']])
  t.assert.strictEqual(form.has('k1'), true)
  t.assert.strictEqual(form.get('k1'), 'v1')
  form.append('k1', 'v1+')
  t.assert.deepStrictEqual(form.getAll('k1'), ['v1', 'v1+'])
  form.set('k2', 'v1++')
  t.assert.strictEqual(form.get('k2'), 'v1++')
  form.delete('asd')
  t.assert.strictEqual(form.get('asd'), null)
})

test('formData.entries', async (t) => {
  const form = new FormData()

  await t.test('with 0 entries', (t) => {
    t.plan(1)

    const entries = [...form.entries()]
    t.assert.deepStrictEqual(entries, [])
  })

  await t.test('with 1+ entries', (t) => {
    t.plan(2)

    form.set('k1', 'v1')
    form.set('k2', 'v2')

    const entries = [...form.entries()]
    const entries2 = [...form.entries()]
    t.assert.deepStrictEqual(entries, [['k1', 'v1'], ['k2', 'v2']])
    t.assert.deepStrictEqual(entries, entries2)
  })
})

test('formData.keys', async (t) => {
  const form = new FormData()

  await t.test('with 0 keys', (t) => {
    t.plan(1)

    const keys = [...form.entries()]
    t.assert.deepStrictEqual(keys, [])
  })

  await t.test('with 1+ keys', (t) => {
    t.plan(2)

    form.set('k1', 'v1')
    form.set('k2', 'v2')

    const keys = [...form.keys()]
    const keys2 = [...form.keys()]
    t.assert.deepStrictEqual(keys, ['k1', 'k2'])
    t.assert.deepStrictEqual(keys, keys2)
  })
})

test('formData.values', async (t) => {
  const form = new FormData()

  await t.test('with 0 values', (t) => {
    t.plan(1)

    const values = [...form.values()]
    t.assert.deepStrictEqual(values, [])
  })

  await t.test('with 1+ values', (t) => {
    t.plan(2)

    form.set('k1', 'v1')
    form.set('k2', 'v2')

    const values = [...form.values()]
    const values2 = [...form.values()]
    t.assert.deepStrictEqual(values, ['v1', 'v2'])
    t.assert.deepStrictEqual(values, values2)
  })
})

test('formData forEach', async (t) => {
  await t.test('invalid arguments', (t) => {
    t.assert.throws(() => {
      FormData.prototype.forEach.call({})
    }, TypeError('Illegal invocation'))

    t.assert.throws(() => {
      const fd = new FormData()

      fd.forEach({})
    }, TypeError)
  })

  await t.test('with a callback', (t) => {
    const fd = new FormData()

    fd.set('a', 'b')
    fd.set('c', 'd')

    let i = 0
    fd.forEach((value, key, self) => {
      if (i++ === 0) {
        t.assert.strictEqual(value, 'b')
        t.assert.strictEqual(key, 'a')
      } else {
        t.assert.strictEqual(value, 'd')
        t.assert.strictEqual(key, 'c')
      }

      t.assert.strictEqual(fd, self)
    })
  })

  await t.test('with a thisArg', (t) => {
    const fd = new FormData()
    fd.set('b', 'a')

    fd.forEach(function (value, key, self) {
      t.assert.strictEqual(this, globalThis)
      t.assert.strictEqual(fd, self)
      t.assert.strictEqual(key, 'b')
      t.assert.strictEqual(value, 'a')
    })

    const thisArg = Symbol('thisArg')
    fd.forEach(function () {
      t.assert.strictEqual(this, thisArg)
    }, thisArg)
  })
})

test('formData toStringTag', (t) => {
  const form = new FormData()
  t.assert.strictEqual(form[Symbol.toStringTag], 'FormData')
  t.assert.strictEqual(FormData.prototype[Symbol.toStringTag], 'FormData')
})

test('formData.constructor.name', (t) => {
  const form = new FormData()
  t.assert.strictEqual(form.constructor.name, 'FormData')
})

test('formData should be an instance of FormData', async (t) => {
  await t.test('Invalid class FormData', (t) => {
    class FormData {
      constructor () {
        this.data = []
      }

      append (key, value) {
        this.data.push([key, value])
      }

      get (key) {
        return this.data.find(([k]) => k === key)
      }
    }

    const form = new FormData()
    t.assert.strictEqual(isFormDataLike(form), false)
  })

  await t.test('Invalid function FormData', (t) => {
    function FormData () {
      const data = []
      return {
        append (key, value) {
          data.push([key, value])
        },
        get (key) {
          return data.find(([k]) => k === key)
        }
      }
    }

    const form = new FormData()
    t.assert.strictEqual(isFormDataLike(form), false)
  })

  await t.test('Valid FormData', (t) => {
    const form = new FormData()
    t.assert.strictEqual(isFormDataLike(form), true)
  })
})

test('FormData should be compatible with third-party libraries', (t) => {
  t.plan(1)

  class FormData {
    constructor () {
      this.data = []
    }

    get [Symbol.toStringTag] () {
      return 'FormData'
    }

    append () {}
    delete () {}
    get () {}
    getAll () {}
    has () {}
    set () {}
    entries () {}
    keys () {}
    values () {}
    forEach () {}
  }

  const form = new FormData()
  t.assert.strictEqual(isFormDataLike(form), true)
})

test('arguments', (t) => {
  t.assert.strictEqual(FormData.length, 0)
  t.assert.strictEqual(FormData.prototype.append.length, 2)
  t.assert.strictEqual(FormData.prototype.delete.length, 1)
  t.assert.strictEqual(FormData.prototype.get.length, 1)
  t.assert.strictEqual(FormData.prototype.getAll.length, 1)
  t.assert.strictEqual(FormData.prototype.has.length, 1)
  t.assert.strictEqual(FormData.prototype.set.length, 2)
})

// https://github.com/nodejs/undici/pull/1814
test('FormData returned from bodyMixin.formData is not a clone', async (t) => {
  const fd = new FormData()
  fd.set('foo', 'bar')

  const res = new Response(fd)
  fd.set('foo', 'foo')

  const fd2 = await res.formData()

  t.assert.strictEqual(fd2.get('foo'), 'bar')
  t.assert.strictEqual(fd.get('foo'), 'foo')

  fd2.set('foo', 'baz')

  t.assert.strictEqual(fd2.get('foo'), 'baz')
  t.assert.strictEqual(fd.get('foo'), 'foo')
})

test('.formData() with multipart/form-data body that ends with --\r\n', async (t) => {
  const request = new Request('http://localhost', {
    method: 'POST',
    headers: {
      'Content-Type': 'multipart/form-data; boundary=----formdata-undici-0.6204674738279623'
    },
    body:
      '------formdata-undici-0.6204674738279623\r\n' +
      'Content-Disposition: form-data; name="fiŝo"\r\n' +
      '\r\n' +
      'value1\r\n' +
      '------formdata-undici-0.6204674738279623--\r\n'
  })

  await request.formData()
})

test('.formData() rejects malformed multipart header line ending with bare CR', async (t) => {
  const boundary = '----formdata-undici-bare-cr-0000000000'
  const body = Buffer.concat([
    Buffer.from('--' + boundary + '\r\n'),
    Buffer.from('Content-Disposition: form-data; name="x"'),
    Buffer.from([0x0d]), // bare CR (no LF)
    Buffer.from('Content-Type: text/plain\r\n'),
    Buffer.from('\r\n'),
    Buffer.from('hello\r\n'),
    Buffer.from('--' + boundary + '--\r\n')
  ])

  const request = new Request('http://localhost', {
    method: 'POST',
    headers: {
      'Content-Type': 'multipart/form-data; boundary=' + boundary
    },
    body
  })

  await t.assert.rejects(request.formData(), TypeError)
})