From 611a562c3170b12fd196d64a3947194d6ed95c6e Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Thu, 5 Aug 2021 08:28:18 +0200 Subject: [PATCH] test: port node-fetch --- index.js | 18 +- lib/api/api-fetch.js | 134 +- lib/api/headers.js | 2 +- lib/api/response.js | 142 ++ package.json | 16 +- test/node-fetch/external-encoding.js | 43 + test/node-fetch/form-data.js | 103 ++ test/node-fetch/headers.js | 280 +++ test/node-fetch/main.js | 2307 +++++++++++++++++++++++++ test/node-fetch/request.js | 277 +++ test/node-fetch/response.js | 218 +++ test/node-fetch/utils/chai-timeout.js | 15 + test/node-fetch/utils/dummy.txt | 1 + test/node-fetch/utils/read-stream.js | 9 + test/node-fetch/utils/server.js | 430 +++++ 15 files changed, 3861 insertions(+), 134 deletions(-) create mode 100644 lib/api/response.js create mode 100644 test/node-fetch/external-encoding.js create mode 100644 test/node-fetch/form-data.js create mode 100644 test/node-fetch/headers.js create mode 100644 test/node-fetch/main.js create mode 100644 test/node-fetch/request.js create mode 100644 test/node-fetch/response.js create mode 100644 test/node-fetch/utils/chai-timeout.js create mode 100644 test/node-fetch/utils/dummy.txt create mode 100644 test/node-fetch/utils/read-stream.js create mode 100644 test/node-fetch/utils/server.js diff --git a/index.js b/index.js index b4b90b30033..dffaed5f8ea 100644 --- a/index.js +++ b/index.js @@ -84,7 +84,23 @@ function makeDispatcher (fn) { module.exports.setGlobalDispatcher = setGlobalDispatcher module.exports.getGlobalDispatcher = getGlobalDispatcher -module.exports.fetch = makeDispatcher(api.fetch) +const _fetch = makeDispatcher(api.fetch) +module.exports.fetch = async function fetch (...args) { + try { + return await _fetch(...args) + } catch (err) { + // TODO (fix): This is a little weird. Spec compliant? + if (err.code === 'ERR_INVALID_URL') { + const er = new TypeError('Invalid URL') + er.cause = err + throw er + } + throw err + } +} +module.exports.Headers = require('./lib/api/headers') +module.exports.Response = require('./lib/api/response') + module.exports.request = makeDispatcher(api.request) module.exports.stream = makeDispatcher(api.stream) module.exports.pipeline = makeDispatcher(api.pipeline) diff --git a/lib/api/api-fetch.js b/lib/api/api-fetch.js index 43a844109d3..4e0f193ef87 100644 --- a/lib/api/api-fetch.js +++ b/lib/api/api-fetch.js @@ -2,10 +2,11 @@ 'use strict' -const Headers = require('./headers') const { kHeadersList } = require('../core/symbols') +const Headers = require('./headers') +const Response = require('./response') const { Readable } = require('stream') -const { METHODS, STATUS_CODES } = require('http') +const { METHODS } = require('http') const { InvalidArgumentError, NotSupportedError, @@ -13,139 +14,10 @@ const { } = require('../core/errors') const util = require('../core/util') const { addSignal, removeSignal } = require('./abort-signal') -const { Blob } = require('buffer') - -const kType = Symbol('type') -const kStatus = Symbol('status') -const kStatusText = Symbol('status text') -const kUrlList = Symbol('url list') -const kHeaders = Symbol('headers') -const kBody = Symbol('body') let ReadableStream let TransformStream -class Response { - constructor ({ - type, - url, - body, - statusCode, - headers, - context - }) { - this[kType] = type || 'default' - this[kStatus] = statusCode || 0 - this[kStatusText] = STATUS_CODES[statusCode] || '' - this[kUrlList] = Array.isArray(url) ? url : (url ? [url] : []) - this[kHeaders] = headers || new Headers() - this[kBody] = body || null - - if (context && context.history) { - this[kUrlList].push(...context.history) - } - } - - get type () { - return this[kType] - } - - get url () { - const length = this[kUrlList].length - return length === 0 ? '' : this[kUrlList][length - 1].toString() - } - - get redirected () { - return this[kUrlList].length > 1 - } - - get status () { - return this[kStatus] - } - - get ok () { - return this[kStatus] >= 200 && this[kStatus] <= 299 - } - - get statusText () { - return this[kStatusText] - } - - get headers () { - return this[kHeaders] - } - - async blob () { - const chunks = [] - if (this.body) { - if (this.bodyUsed || this.body.locked) { - throw new TypeError('unusable') - } - - for await (const chunk of this.body) { - chunks.push(chunk) - } - } - - return new Blob(chunks, { type: this.headers.get('Content-Type') || '' }) - } - - async arrayBuffer () { - const blob = await this.blob() - return await blob.arrayBuffer() - } - - async text () { - const blob = await this.blob() - return await blob.text() - } - - async json () { - return JSON.parse(await this.text()) - } - - async formData () { - // TODO: Implement. - throw new NotSupportedError('formData') - } - - get body () { - return this[kBody] - } - - get bodyUsed () { - return util.isDisturbed(this.body) - } - - clone () { - let body = null - - if (this[kBody]) { - if (util.isDisturbed(this[kBody])) { - throw new TypeError('disturbed') - } - - if (this[kBody].locked) { - throw new TypeError('locked') - } - - // https://fetch.spec.whatwg.org/#concept-body-clone - const [out1, out2] = this[kBody].tee() - - this[kBody] = out1 - body = out2 - } - - return new Response({ - type: this[kType], - statusCode: this[kStatus], - url: this[kUrlList], - headers: this[kHeaders], - body - }) - } -} - class FetchHandler { constructor (opts, callback) { if (!opts || typeof opts !== 'object') { diff --git a/lib/api/headers.js b/lib/api/headers.js index ea22426271b..5a5d7da05c3 100644 --- a/lib/api/headers.js +++ b/lib/api/headers.js @@ -49,7 +49,7 @@ function isHeaders (object) { function fill (headers, object) { if (isHeaders(object)) { // Object is instance of Headers - headers[kHeadersList] = Array.splice(object[kHeadersList]) + headers[kHeadersList] = [...object[kHeadersList]] } else if (Array.isArray(object)) { // Support both 1D and 2D arrays of header entries if (Array.isArray(object[0])) { diff --git a/lib/api/response.js b/lib/api/response.js new file mode 100644 index 00000000000..2de45efc22e --- /dev/null +++ b/lib/api/response.js @@ -0,0 +1,142 @@ +'use strict' + +const Headers = require('./headers') +const { Blob } = require('buffer') +const { STATUS_CODES } = require('http') +const { + NotSupportedError +} = require('../core/errors') +const util = require('../core/util') + +const kType = Symbol('type') +const kStatus = Symbol('status') +const kStatusText = Symbol('status text') +const kUrlList = Symbol('url list') +const kHeaders = Symbol('headers') +const kBody = Symbol('body') +const kBodyUsed = Symbol('body used') + +class Response { + constructor ({ + type, + url, + body, + statusCode, + headers, + context + }) { + this[kType] = type || 'default' + this[kStatus] = statusCode || 0 + this[kStatusText] = STATUS_CODES[statusCode] || '' + this[kUrlList] = Array.isArray(url) ? url : (url ? [url] : []) + this[kHeaders] = headers || new Headers() + this[kBody] = body || null + this[kBodyUsed] = false + + if (context && context.history) { + this[kUrlList].push(...context.history) + } + } + + get type () { + return this[kType] + } + + get url () { + const length = this[kUrlList].length + return length === 0 ? '' : this[kUrlList][length - 1].toString() + } + + get redirected () { + return this[kUrlList].length > 1 + } + + get status () { + return this[kStatus] + } + + get ok () { + return this[kStatus] >= 200 && this[kStatus] <= 299 + } + + get statusText () { + return this[kStatusText] + } + + get headers () { + return this[kHeaders] + } + + async blob () { + const chunks = [] + if (this.body) { + if (this.bodyUsed || this.body.locked) { + throw new TypeError('unusable') + } + + this[kBodyUsed] = true + for await (const chunk of this.body) { + chunks.push(chunk) + } + } + + return new Blob(chunks, { type: this.headers.get('Content-Type') || '' }) + } + + async arrayBuffer () { + const blob = await this.blob() + return await blob.arrayBuffer() + } + + async text () { + const blob = await this.blob() + return await blob.text() + } + + async json () { + return JSON.parse(await this.text()) + } + + async formData () { + // TODO: Implement. + throw new NotSupportedError('formData') + } + + get body () { + return this[kBody] + } + + get bodyUsed () { + return util.isDisturbed(this.body) || this[kBodyUsed] + } + + clone () { + let body = null + + if (this[kBody]) { + if (util.isDisturbed(this[kBody])) { + throw new TypeError('disturbed') + } + + if (this[kBody].locked) { + throw new TypeError('locked') + } + + // https://fetch.spec.whatwg.org/#concept-body-clone + const [out1, out2] = this[kBody].tee() + + this[kBody] = out1 + body = out2 + } + + return new Response({ + type: this[kType], + statusCode: this[kStatus], + url: this[kUrlList], + headers: this[kHeaders], + body + }) + } +} + +module.exports = Response diff --git a/package.json b/package.json index b89098adc78..a16118c868a 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "build:wasm": "node build/wasm.js --docker", "lint": "standard | snazzy", "lint:fix": "standard --fix | snazzy", - "test": "tap test/*.js --no-coverage && jest test/jest/test", + "test": "tap test/*.js --no-coverage && mocha test/node-fetch && jest test/jest/test", + "test:node-fetch": "mocha test/node-fetch", "test:tdd": "tap test/*.js -w --no-coverage-report", "test:typescript": "tsd", "coverage": "standard | snazzy && tap test/*.js", @@ -46,17 +47,27 @@ "prepare": "husky install", "fuzz": "jsfuzz test/fuzzing/fuzz.js corpus" }, + "eslintConfig": {}, "devDependencies": { "@sinonjs/fake-timers": "^7.0.5", "@types/node": "^15.0.2", "abort-controller": "^3.0.0", + "abortcontroller-polyfill": "^1.7.3", + "busboy": "^0.3.1", + "chai": "^4.3.4", + "chai-as-promised": "^7.1.1", + "chai-iterator": "^3.0.2", + "chai-string": "^1.5.0", "concurrently": "^6.1.0", "cronometro": "^0.8.0", + "delay": "^5.0.0", "docsify-cli": "^4.4.2", "https-pem": "^2.0.0", "husky": "^6.0.0", "jest": "^27.0.5", "jsfuzz": "^1.0.15", + "mocha": "^9.0.3", + "p-timeout": "^3.2.0", "pre-commit": "^1.2.2", "proxy": "^1.0.2", "proxyquire": "^2.1.3", @@ -72,6 +83,9 @@ "node": ">=12.18" }, "standard": { + "env": [ + "mocha" + ], "ignore": [ "lib/llhttp/constants.js", "lib/llhttp/utils.js" diff --git a/test/node-fetch/external-encoding.js b/test/node-fetch/external-encoding.js new file mode 100644 index 00000000000..5deeeba814e --- /dev/null +++ b/test/node-fetch/external-encoding.js @@ -0,0 +1,43 @@ +// import chai from 'chai' +// import { fetch } from '../../index.js' + +// const { expect } = chai + +// describe('external encoding', () => { +// describe('data uri', () => { +// it('should accept base64-encoded gif data uri', () => { +// return fetch('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=').then(r => { +// expect(r.status).to.equal(200) +// expect(r.headers.get('Content-Type')).to.equal('image/gif') + +// return r.buffer().then(b => { +// expect(b).to.be.an.instanceOf(Buffer) +// }) +// }) +// }) + +// it('should accept data uri with specified charset', async () => { +// const r = await fetch('data:text/plain;charset=UTF-8;page=21,the%20data:1234,5678') +// expect(r.status).to.equal(200) +// expect(r.headers.get('Content-Type')).to.equal('text/plain;charset=UTF-8;page=21') + +// const b = await r.text() +// expect(b).to.equal('the data:1234,5678') +// }) + +// it('should accept data uri of plain text', () => { +// return fetch('data:,Hello%20World!').then(r => { +// expect(r.status).to.equal(200) +// expect(r.headers.get('Content-Type')).to.equal('text/plain;charset=US-ASCII') +// return r.text().then(t => expect(t).to.equal('Hello World!')) +// }) +// }) + +// it('should reject invalid data uri', () => { +// return fetch('data:@@@@').catch(error => { +// expect(error).to.exist +// expect(error.message).to.include('malformed data: URI') +// }) +// }) +// }) +// }) diff --git a/test/node-fetch/form-data.js b/test/node-fetch/form-data.js new file mode 100644 index 00000000000..dc6b3a98db5 --- /dev/null +++ b/test/node-fetch/form-data.js @@ -0,0 +1,103 @@ +// import { FormData } from 'formdata-node' +// import Blob from 'fetch-blob' + +// import chai from 'chai' + +// import { getFormDataLength, getBoundary, formDataIterator } from '../src/utils/form-data.js' +// import read from './utils/read-stream.js' + +// const { expect } = chai + +// const carriage = '\r\n' + +// const getFooter = boundary => `--${boundary}--${carriage.repeat(2)}` + +// describe('FormData', () => { +// it('should return a length for empty form-data', () => { +// const form = new FormData() +// const boundary = getBoundary() + +// expect(getFormDataLength(form, boundary)).to.be.equal(Buffer.byteLength(getFooter(boundary))) +// }) + +// it('should add a Blob field\'s size to the FormData length', () => { +// const form = new FormData() +// const boundary = getBoundary() + +// const string = 'Hello, world!' +// const expected = Buffer.byteLength( +// `--${boundary}${carriage}` + +// `Content-Disposition: form-data; name="field"${carriage.repeat(2)}` + +// string + +// `${carriage}${getFooter(boundary)}` +// ) + +// form.set('field', string) + +// expect(getFormDataLength(form, boundary)).to.be.equal(expected) +// }) + +// it('should return a length for a Blob field', () => { +// const form = new FormData() +// const boundary = getBoundary() + +// const blob = new Blob(['Hello, world!'], { type: 'text/plain' }) + +// form.set('blob', blob) + +// const expected = blob.size + Buffer.byteLength( +// `--${boundary}${carriage}` + +// 'Content-Disposition: form-data; name="blob"; ' + +// `filename="blob"${carriage}Content-Type: text/plain` + +// `${carriage.repeat(3)}${getFooter(boundary)}` +// ) + +// expect(getFormDataLength(form, boundary)).to.be.equal(expected) +// }) + +// it('should create a body from empty form-data', async () => { +// const form = new FormData() +// const boundary = getBoundary() + +// expect(String(await read(formDataIterator(form, boundary)))).to.be.equal(getFooter(boundary)) +// }) + +// it('should set default content-type', async () => { +// const form = new FormData() +// const boundary = getBoundary() + +// form.set('blob', new Blob([])) + +// expect(String(await read(formDataIterator(form, boundary)))).to.contain('Content-Type: application/octet-stream') +// }) + +// it('should create a body with a FormData field', async () => { +// const form = new FormData() +// const boundary = getBoundary() +// const string = 'Hello, World!' + +// form.set('field', string) + +// const expected = `--${boundary}${carriage}` + +// `Content-Disposition: form-data; name="field"${carriage.repeat(2)}` + +// string + +// `${carriage}${getFooter(boundary)}` + +// expect(String(await read(formDataIterator(form, boundary)))).to.be.equal(expected) +// }) + +// it('should create a body with a FormData Blob field', async () => { +// const form = new FormData() +// const boundary = getBoundary() + +// const expected = `--${boundary}${carriage}` + +// 'Content-Disposition: form-data; name="blob"; ' + +// `filename="blob"${carriage}Content-Type: text/plain${carriage.repeat(2)}` + +// 'Hello, World!' + +// `${carriage}${getFooter(boundary)}` + +// form.set('blob', new Blob(['Hello, World!'], { type: 'text/plain' })) + +// expect(String(await read(formDataIterator(form, boundary)))).to.be.equal(expected) +// }) +// }) diff --git a/test/node-fetch/headers.js b/test/node-fetch/headers.js new file mode 100644 index 00000000000..f147dc07fc5 --- /dev/null +++ b/test/node-fetch/headers.js @@ -0,0 +1,280 @@ +const { format } = require('util') +const chai = require('chai') +const chaiIterator = require('chai-iterator') +const Headers = require('../../lib/api/headers.js') + +chai.use(chaiIterator) + +const { expect } = chai + +describe('Headers', () => { + // it('should have attributes conforming to Web IDL', () => { + // const headers = new Headers() + // expect(Object.getOwnPropertyNames(headers)).to.be.empty + // const enumerableProperties = [] + + // for (const property in headers) { + // enumerableProperties.push(property) + // } + + // for (const toCheck of [ + // 'append', + // 'delete', + // 'entries', + // 'forEach', + // 'get', + // 'has', + // 'keys', + // 'set', + // 'values' + // ]) { + // expect(enumerableProperties).to.contain(toCheck) + // } + // }) + + it('should allow iterating through all headers with forEach', () => { + const headers = new Headers([ + ['b', '2'], + ['c', '4'], + ['b', '3'], + ['a', '1'] + ]) + expect(headers).to.have.property('forEach') + + const result = [] + for (const [key, value] of headers.entries()) { + result.push([key, value]) + } + + expect(result).to.deep.equal([ + ['a', '1'], + ['b', '2, 3'], + ['c', '4'] + ]) + }) + + it('should be iterable with forEach', () => { + const headers = new Headers() + headers.append('Accept', 'application/json') + headers.append('Accept', 'text/plain') + headers.append('Content-Type', 'text/html') + + const results = [] + headers.forEach((value, key, object) => { + results.push({ value, key, object }) + }) + + expect(results.length).to.equal(2) + expect({ key: 'accept', value: 'application/json, text/plain', object: headers }).to.deep.equal(results[0]) + expect({ key: 'content-type', value: 'text/html', object: headers }).to.deep.equal(results[1]) + }) + + // it('should set "this" to undefined by default on forEach', () => { + // const headers = new Headers({ Accept: 'application/json' }) + // headers.forEach(function () { + // expect(this).to.be.undefined + // }) + // }) + + it('should accept thisArg as a second argument for forEach', () => { + const headers = new Headers({ Accept: 'application/json' }) + const thisArg = {} + headers.forEach(function () { + expect(this).to.equal(thisArg) + }, thisArg) + }) + + it('should allow iterating through all headers with for-of loop', () => { + const headers = new Headers([ + ['b', '2'], + ['c', '4'], + ['a', '1'] + ]) + headers.append('b', '3') + expect(headers).to.be.iterable + + const result = [] + for (const pair of headers) { + result.push(pair) + } + + expect(result).to.deep.equal([ + ['a', '1'], + ['b', '2, 3'], + ['c', '4'] + ]) + }) + + it('should allow iterating through all headers with entries()', () => { + const headers = new Headers([ + ['b', '2'], + ['c', '4'], + ['a', '1'] + ]) + headers.append('b', '3') + + expect(headers.entries()).to.be.iterable + .and.to.deep.iterate.over([ + ['a', '1'], + ['b', '2, 3'], + ['c', '4'] + ]) + }) + + it('should allow iterating through all headers with keys()', () => { + const headers = new Headers([ + ['b', '2'], + ['c', '4'], + ['a', '1'] + ]) + headers.append('b', '3') + + expect(headers.keys()).to.be.iterable + .and.to.iterate.over(['a', 'b', 'c']) + }) + + it('should allow iterating through all headers with values()', () => { + const headers = new Headers([ + ['b', '2'], + ['c', '4'], + ['a', '1'] + ]) + headers.append('b', '3') + + expect(headers.values()).to.be.iterable + .and.to.iterate.over(['1', '2, 3', '4']) + }) + + it('should reject illegal header', () => { + const headers = new Headers() + expect(() => new Headers({ 'He y': 'ok' })).to.throw(TypeError) + expect(() => new Headers({ 'Hé-y': 'ok' })).to.throw(TypeError) + expect(() => new Headers({ 'He-y': 'ăk' })).to.throw(TypeError) + expect(() => headers.append('Hé-y', 'ok')).to.throw(TypeError) + expect(() => headers.delete('Hé-y')).to.throw(TypeError) + expect(() => headers.get('Hé-y')).to.throw(TypeError) + expect(() => headers.has('Hé-y')).to.throw(TypeError) + expect(() => headers.set('Hé-y', 'ok')).to.throw(TypeError) + // Should reject empty header + expect(() => headers.append('', 'ok')).to.throw(TypeError) + }) + + // it('should ignore unsupported attributes while reading headers', () => { + // const FakeHeader = function () {} + // // Prototypes are currently ignored + // // This might change in the future: #181 + // FakeHeader.prototype.z = 'fake' + + // const res = new FakeHeader() + // res.a = 'string' + // res.b = ['1', '2'] + // res.c = '' + // res.d = [] + // res.e = 1 + // res.f = [1, 2] + // res.g = { a: 1 } + // res.h = undefined + // res.i = null + // res.j = Number.NaN + // res.k = true + // res.l = false + // res.m = Buffer.from('test') + + // const h1 = new Headers(res) + // h1.set('n', [1, 2]) + // h1.append('n', ['3', 4]) + + // const h1Raw = h1.raw() + + // expect(h1Raw.a).to.include('string') + // expect(h1Raw.b).to.include('1,2') + // expect(h1Raw.c).to.include('') + // expect(h1Raw.d).to.include('') + // expect(h1Raw.e).to.include('1') + // expect(h1Raw.f).to.include('1,2') + // expect(h1Raw.g).to.include('[object Object]') + // expect(h1Raw.h).to.include('undefined') + // expect(h1Raw.i).to.include('null') + // expect(h1Raw.j).to.include('NaN') + // expect(h1Raw.k).to.include('true') + // expect(h1Raw.l).to.include('false') + // expect(h1Raw.m).to.include('test') + // expect(h1Raw.n).to.include('1,2') + // expect(h1Raw.n).to.include('3,4') + + // expect(h1Raw.z).to.be.undefined + // }) + + // it('should wrap headers', () => { + // const h1 = new Headers({ + // a: '1' + // }) + // const h1Raw = h1.raw() + + // const h2 = new Headers(h1) + // h2.set('b', '1') + // const h2Raw = h2.raw() + + // const h3 = new Headers(h2) + // h3.append('a', '2') + // const h3Raw = h3.raw() + + // expect(h1Raw.a).to.include('1') + // expect(h1Raw.a).to.not.include('2') + + // expect(h2Raw.a).to.include('1') + // expect(h2Raw.a).to.not.include('2') + // expect(h2Raw.b).to.include('1') + + // expect(h3Raw.a).to.include('1') + // expect(h3Raw.a).to.include('2') + // expect(h3Raw.b).to.include('1') + // }) + + // it('should accept headers as an iterable of tuples', () => { + // let headers + + // headers = new Headers([ + // ['a', '1'], + // ['b', '2'], + // ['a', '3'] + // ]) + // expect(headers.get('a')).to.equal('1, 3') + // expect(headers.get('b')).to.equal('2') + + // headers = new Headers([ + // new Set(['a', '1']), + // ['b', '2'], + // new Map([['a', null], ['3', null]]).keys() + // ]) + // expect(headers.get('a')).to.equal('1, 3') + // expect(headers.get('b')).to.equal('2') + + // headers = new Headers(new Map([ + // ['a', '1'], + // ['b', '2'] + // ])) + // expect(headers.get('a')).to.equal('1') + // expect(headers.get('b')).to.equal('2') + // }) + + // it('should throw a TypeError if non-tuple exists in a headers initializer', () => { + // expect(() => new Headers([['b', '2', 'huh?']])).to.throw(TypeError) + // expect(() => new Headers(['b2'])).to.throw(TypeError) + // expect(() => new Headers('b2')).to.throw(TypeError) + // expect(() => new Headers({ [Symbol.iterator]: 42 })).to.throw(TypeError) + // }) + + // it('should use a custom inspect function', () => { + // const headers = new Headers([ + // ['Host', 'thehost'], + // ['Host', 'notthehost'], + // ['a', '1'], + // ['b', '2'], + // ['a', '3'] + // ]) + + // // eslint-disable-next-line quotes + // expect(format(headers)).to.equal("{ a: [ '1', '3' ], b: '2', host: 'thehost' }") + // }) +}) diff --git a/test/node-fetch/main.js b/test/node-fetch/main.js new file mode 100644 index 00000000000..dc2977cc898 --- /dev/null +++ b/test/node-fetch/main.js @@ -0,0 +1,2307 @@ +// Test tools +const zlib = require('zlib') +const crypto = require('crypto') +const http = require('http') +const fs = require('fs') +const stream = require('stream') +const path = require('path') +const { lookup } = require('dns') +const vm = require('vm') +const chai = require('chai') +const chaiPromised = require('chai-as-promised') +const chaiIterator = require('chai-iterator') +const chaiString = require('chai-string') +const FormData = require('form-data') +// const { FormData as FormDataNode } = require('formdata-node' +const delay = require('delay') +const AbortControllerMysticatea = require('abort-controller') +const abortControllerPolyfill = require('abortcontroller-polyfill/dist/abortcontroller.js') + +// Test subjects +// const Blob = require('fetch-blob' +// const { fileFromSync } = require('fetch-blob/= require(js' + +const { + fetch, + // FetchError, + Headers, + // Request, + Response +} = require('../../index.js') +// const { FetchError as FetchErrorOrig } = require('../src/errors/fetch-error.js' +const HeadersOrig = require('../../lib/api/headers.js') +// const RequestOrig = require('../src/request.js' +const ResponseOrig = require('../../lib/api/response.js') +// const Body, { getTotalBytes, extractContentType } = require('../src/body.js' +const TestServer = require('./utils/server.js') +const chaiTimeout = require('./utils/chai-timeout.js') + +const AbortControllerPolyfill = abortControllerPolyfill.AbortController + +function isNodeLowerThan (version) { + return !~process.version.localeCompare(version, undefined, { numeric: true }) +} + +const { + Uint8Array: VMUint8Array +} = vm.runInNewContext('this') + +chai.use(chaiPromised) +chai.use(chaiIterator) +chai.use(chaiString) +chai.use(chaiTimeout) +const { expect } = chai + +function streamToPromise (stream, dataHandler) { + return new Promise((resolve, reject) => { + stream.on('data', (...args) => { + Promise.resolve() + .then(() => dataHandler(...args)) + .catch(reject) + }) + stream.on('end', resolve) + stream.on('error', reject) + }) +} + +describe('node-fetch', () => { + const local = new TestServer() + let base + + before(async () => { + await local.start() + base = `http://${local.hostname}:${local.port}/` + }) + + after(async () => { + return local.stop() + }) + + it('should return a promise', () => { + const url = `${base}hello` + const p = fetch(url) + expect(p).to.be.an.instanceof(Promise) + expect(p).to.have.property('then') + }) + + it('should expose Headers, Response and Request constructors', () => { + // expect(FetchError).to.equal(FetchErrorOrig) + expect(Headers).to.equal(HeadersOrig) + expect(Response).to.equal(ResponseOrig) + // expect(Request).to.equal(RequestOrig) + }) + + // it('should support proper toString output for Headers, Response and Request objects', () => { + // expect(new Headers().toString()).to.equal('[object Headers]') + // expect(new Response().toString()).to.equal('[object Response]') + // expect(new Request(base).toString()).to.equal('[object Request]') + // }) + + it('should reject with error if url is protocol relative', () => { + const url = '//example.com/' + return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, /Invalid URL/) + }) + + it('should reject with error if url is relative path', () => { + const url = '/some/path' + return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, /Invalid URL/) + }) + + // it('should reject with error if protocol is unsupported', () => { + // const url = 'ftp://example.com/' + // return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, /URL scheme "ftp" is not supported/) + // }) + + // it('should reject with error on network failure', function () { + // this.timeout(5000) + // const url = 'http://localhost:50000/' + // return expect(fetch(url)).to.eventually.be.rejected + // .and.be.an.instanceOf(FetchError) + // .and.include({ type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED' }) + // }) + + // it('error should contain system error if one occurred', () => { + // const err = new FetchError('a message', 'system', new Error('an error')) + // return expect(err).to.have.property('erroredSysCall') + // }) + + // it('error should not contain system error if none occurred', () => { + // const err = new FetchError('a message', 'a type') + // return expect(err).to.not.have.property('erroredSysCall') + // }) + + // it('system error is extracted from failed requests', function () { + // this.timeout(5000) + // const url = 'http://localhost:50000/' + // return expect(fetch(url)).to.eventually.be.rejected + // .and.be.an.instanceOf(FetchError) + // .and.have.property('erroredSysCall') + // }) + + it('should resolve into response', () => { + const url = `${base}hello` + return fetch(url).then(res => { + expect(res).to.be.an.instanceof(Response) + expect(res.headers).to.be.an.instanceof(Headers) + // expect(res.body).to.be.an.instanceof(stream.Transform) + expect(res.bodyUsed).to.be.false + + expect(res.url).to.equal(url) + expect(res.ok).to.be.true + expect(res.status).to.equal(200) + expect(res.statusText).to.equal('OK') + }) + }) + + // it('Response.redirect should resolve into response', () => { + // const res = Response.redirect('http://localhost') + // expect(res).to.be.an.instanceof(Response) + // expect(res.headers).to.be.an.instanceof(Headers) + // expect(res.headers.get('location')).to.equal('http://localhost/') + // expect(res.status).to.equal(302) + // }) + + // it('Response.redirect /w invalid url should fail', () => { + // expect(() => { + // Response.redirect('localhost') + // }).to.throw() + // }) + + // it('Response.redirect /w invalid status should fail', () => { + // expect(() => { + // Response.redirect('http://localhost', 200) + // }).to.throw() + // }) + + it('should accept plain text response', () => { + const url = `${base}plain` + return fetch(url).then(res => { + expect(res.headers.get('content-type')).to.equal('text/plain') + return res.text().then(result => { + expect(res.bodyUsed).to.be.true + expect(result).to.be.a('string') + expect(result).to.equal('text') + }) + }) + }) + + it('should accept html response (like plain text)', () => { + const url = `${base}html` + return fetch(url).then(res => { + expect(res.headers.get('content-type')).to.equal('text/html') + return res.text().then(result => { + expect(res.bodyUsed).to.be.true + expect(result).to.be.a('string') + expect(result).to.equal('') + }) + }) + }) + + it('should accept json response', () => { + const url = `${base}json` + return fetch(url).then(res => { + expect(res.headers.get('content-type')).to.equal('application/json') + return res.json().then(result => { + expect(res.bodyUsed).to.be.true + expect(result).to.be.an('object') + expect(result).to.deep.equal({ name: 'value' }) + }) + }) + }) + + it('should send request with custom headers', () => { + const url = `${base}inspect` + const options = { + headers: { 'x-custom-header': 'abc' } + } + return fetch(url, options).then(res => { + return res.json() + }).then(res => { + expect(res.headers['x-custom-header']).to.equal('abc') + }) + }) + + it('should accept headers instance', () => { + const url = `${base}inspect` + const options = { + headers: new Headers({ 'x-custom-header': 'abc' }) + } + return fetch(url, options).then(res => { + return res.json() + }).then(res => { + expect(res.headers['x-custom-header']).to.equal('abc') + }) + }) + + it('should accept custom host header', () => { + const url = `${base}inspect` + const options = { + headers: { + host: 'example.com' + } + } + return fetch(url, options).then(res => { + return res.json() + }).then(res => { + expect(res.headers.host).to.equal('example.com') + }) + }) + + it('should accept custom HoSt header', () => { + const url = `${base}inspect` + const options = { + headers: { + HoSt: 'example.com' + } + } + return fetch(url, options).then(res => { + return res.json() + }).then(res => { + expect(res.headers.host).to.equal('example.com') + }) + }) + + // it('should follow redirect code 301', () => { + // const url = `${base}redirect/301` + // return fetch(url).then(res => { + // expect(res.url).to.equal(`${base}inspect`) + // expect(res.status).to.equal(200) + // expect(res.ok).to.be.true + // }) + // }) + + // it('should follow redirect code 302', () => { + // const url = `${base}redirect/302` + // return fetch(url).then(res => { + // expect(res.url).to.equal(`${base}inspect`) + // expect(res.status).to.equal(200) + // }) + // }) + + // it('should follow redirect code 303', () => { + // const url = `${base}redirect/303` + // return fetch(url).then(res => { + // expect(res.url).to.equal(`${base}inspect`) + // expect(res.status).to.equal(200) + // }) + // }) + + // it('should follow redirect code 307', () => { + // const url = `${base}redirect/307` + // return fetch(url).then(res => { + // expect(res.url).to.equal(`${base}inspect`) + // expect(res.status).to.equal(200) + // }) + // }) + + // it('should follow redirect code 308', () => { + // const url = `${base}redirect/308` + // return fetch(url).then(res => { + // expect(res.url).to.equal(`${base}inspect`) + // expect(res.status).to.equal(200) + // }) + // }) + + // it('should follow redirect chain', () => { + // const url = `${base}redirect/chain` + // return fetch(url).then(res => { + // expect(res.url).to.equal(`${base}inspect`) + // expect(res.status).to.equal(200) + // }) + // }) + + // it('should follow POST request redirect code 301 with GET', () => { + // const url = `${base}redirect/301` + // const options = { + // method: 'POST', + // body: 'a=1' + // } + // return fetch(url, options).then(res => { + // expect(res.url).to.equal(`${base}inspect`) + // expect(res.status).to.equal(200) + // return res.json().then(result => { + // expect(result.method).to.equal('GET') + // expect(result.body).to.equal('') + // }) + // }) + // }) + + // it('should follow PATCH request redirect code 301 with PATCH', () => { + // const url = `${base}redirect/301` + // const options = { + // method: 'PATCH', + // body: 'a=1' + // } + // return fetch(url, options).then(res => { + // expect(res.url).to.equal(`${base}inspect`) + // expect(res.status).to.equal(200) + // return res.json().then(res => { + // expect(res.method).to.equal('PATCH') + // expect(res.body).to.equal('a=1') + // }) + // }) + // }) + + // it('should follow POST request redirect code 302 with GET', () => { + // const url = `${base}redirect/302` + // const options = { + // method: 'POST', + // body: 'a=1' + // } + // return fetch(url, options).then(res => { + // expect(res.url).to.equal(`${base}inspect`) + // expect(res.status).to.equal(200) + // return res.json().then(result => { + // expect(result.method).to.equal('GET') + // expect(result.body).to.equal('') + // }) + // }) + // }) + + // it('should follow PATCH request redirect code 302 with PATCH', () => { + // const url = `${base}redirect/302` + // const options = { + // method: 'PATCH', + // body: 'a=1' + // } + // return fetch(url, options).then(res => { + // expect(res.url).to.equal(`${base}inspect`) + // expect(res.status).to.equal(200) + // return res.json().then(res => { + // expect(res.method).to.equal('PATCH') + // expect(res.body).to.equal('a=1') + // }) + // }) + // }) + + // it('should follow redirect code 303 with GET', () => { + // const url = `${base}redirect/303` + // const options = { + // method: 'PUT', + // body: 'a=1' + // } + // return fetch(url, options).then(res => { + // expect(res.url).to.equal(`${base}inspect`) + // expect(res.status).to.equal(200) + // return res.json().then(result => { + // expect(result.method).to.equal('GET') + // expect(result.body).to.equal('') + // }) + // }) + // }) + + // it('should follow PATCH request redirect code 307 with PATCH', () => { + // const url = `${base}redirect/307` + // const options = { + // method: 'PATCH', + // body: 'a=1' + // } + // return fetch(url, options).then(res => { + // expect(res.url).to.equal(`${base}inspect`) + // expect(res.status).to.equal(200) + // return res.json().then(result => { + // expect(result.method).to.equal('PATCH') + // expect(result.body).to.equal('a=1') + // }) + // }) + // }) + + // it('should not follow non-GET redirect if body is a readable stream', () => { + // const url = `${base}redirect/307` + // const options = { + // method: 'PATCH', + // body: stream.Readable.from('tada') + // } + // return expect(fetch(url, options)).to.eventually.be.rejected + // .and.be.an.instanceOf(FetchError) + // .and.have.property('type', 'unsupported-redirect') + // }) + + // it('should obey maximum redirect, reject case', () => { + // const url = `${base}redirect/chain` + // const options = { + // follow: 1 + // } + // return expect(fetch(url, options)).to.eventually.be.rejected + // .and.be.an.instanceOf(FetchError) + // .and.have.property('type', 'max-redirect') + // }) + + // it('should obey redirect chain, resolve case', () => { + // const url = `${base}redirect/chain` + // const options = { + // follow: 2 + // } + // return fetch(url, options).then(res => { + // expect(res.url).to.equal(`${base}inspect`) + // expect(res.status).to.equal(200) + // }) + // }) + + // it('should allow not following redirect', () => { + // const url = `${base}redirect/301` + // const options = { + // follow: 0 + // } + // return expect(fetch(url, options)).to.eventually.be.rejected + // .and.be.an.instanceOf(FetchError) + // .and.have.property('type', 'max-redirect') + // }) + + // it('should support redirect mode, manual flag', () => { + // const url = `${base}redirect/301` + // const options = { + // redirect: 'manual' + // } + // return fetch(url, options).then(res => { + // expect(res.url).to.equal(url) + // expect(res.status).to.equal(301) + // expect(res.headers.get('location')).to.equal(`${base}inspect`) + // }) + // }) + + // it('should support redirect mode, manual flag, broken Location header', () => { + // const url = `${base}redirect/bad-location` + // const options = { + // redirect: 'manual' + // } + // return fetch(url, options).then(res => { + // expect(res.url).to.equal(url) + // expect(res.status).to.equal(301) + // expect(res.headers.get('location')).to.equal(`${base}redirect/%C3%A2%C2%98%C2%83`) + // }) + // }) + + // it('should support redirect mode, error flag', () => { + // const url = `${base}redirect/301` + // const options = { + // redirect: 'error' + // } + // return expect(fetch(url, options)).to.eventually.be.rejected + // .and.be.an.instanceOf(FetchError) + // .and.have.property('type', 'no-redirect') + // }) + + // it('should support redirect mode, manual flag when there is no redirect', () => { + // const url = `${base}hello` + // const options = { + // redirect: 'manual' + // } + // return fetch(url, options).then(res => { + // expect(res.url).to.equal(url) + // expect(res.status).to.equal(200) + // expect(res.headers.get('location')).to.be.null + // }) + // }) + + // it('should follow redirect code 301 and keep existing headers', () => { + // const url = `${base}redirect/301` + // const options = { + // headers: new Headers({ 'x-custom-header': 'abc' }) + // } + // return fetch(url, options).then(res => { + // expect(res.url).to.equal(`${base}inspect`) + // return res.json() + // }).then(res => { + // expect(res.headers['x-custom-header']).to.equal('abc') + // }) + // }) + + // it('should treat broken redirect as ordinary response (follow)', () => { + // const url = `${base}redirect/no-location` + // return fetch(url).then(res => { + // expect(res.url).to.equal(url) + // expect(res.status).to.equal(301) + // expect(res.headers.get('location')).to.be.null + // }) + // }) + + // it('should treat broken redirect as ordinary response (manual)', () => { + // const url = `${base}redirect/no-location` + // const options = { + // redirect: 'manual' + // } + // return fetch(url, options).then(res => { + // expect(res.url).to.equal(url) + // expect(res.status).to.equal(301) + // expect(res.headers.get('location')).to.be.null + // }) + // }) + + // it('should throw a TypeError on an invalid redirect option', () => { + // const url = `${base}redirect/301` + // const options = { + // redirect: 'foobar' + // } + // return fetch(url, options).then(() => { + // expect.fail() + // }, error => { + // expect(error).to.be.an.instanceOf(TypeError) + // expect(error.message).to.equal('Redirect option \'foobar\' is not a valid value of RequestRedirect') + // }) + // }) + + // it('should set redirected property on response when redirect', () => { + // const url = `${base}redirect/301` + // return fetch(url).then(res => { + // expect(res.redirected).to.be.true + // }) + // }) + + // it('should not set redirected property on response without redirect', () => { + // const url = `${base}hello` + // return fetch(url).then(res => { + // expect(res.redirected).to.be.false + // }) + // }) + + // it('should ignore invalid headers', () => { + // const headers = fromRawHeaders([ + // 'Invalid-Header ', + // 'abc\r\n', + // 'Invalid-Header-Value', + // '\u0007k\r\n', + // 'Cookie', + // '\u0007k\r\n', + // 'Cookie', + // '\u0007kk\r\n' + // ]) + // expect(headers).to.be.instanceOf(Headers) + // expect(headers.raw()).to.deep.equal({}) + // }) + + it('should handle client-error response', () => { + const url = `${base}error/400` + return fetch(url).then(res => { + expect(res.headers.get('content-type')).to.equal('text/plain') + expect(res.status).to.equal(400) + expect(res.statusText).to.equal('Bad Request') + expect(res.ok).to.be.false + return res.text().then(result => { + expect(res.bodyUsed).to.be.true + expect(result).to.be.a('string') + expect(result).to.equal('client error') + }) + }) + }) + + it('should handle server-error response', () => { + const url = `${base}error/500` + return fetch(url).then(res => { + expect(res.headers.get('content-type')).to.equal('text/plain') + expect(res.status).to.equal(500) + expect(res.statusText).to.equal('Internal Server Error') + expect(res.ok).to.be.false + return res.text().then(result => { + expect(res.bodyUsed).to.be.true + expect(result).to.be.a('string') + expect(result).to.equal('server error') + }) + }) + }) + + // it('should handle network-error response', () => { + // const url = `${base}error/reset` + // return expect(fetch(url)).to.eventually.be.rejected + // .and.be.an.instanceOf(FetchError) + // .and.have.property('code', 'ECONNRESET') + // }) + + // it('should handle network-error partial response', () => { + // const url = `${base}error/premature` + // return fetch(url).then(res => { + // expect(res.status).to.equal(200) + // expect(res.ok).to.be.true + // return expect(res.text()).to.eventually.be.rejectedWith(Error) + // .and.have.property('message').matches(/Premature close|The operation was aborted|aborted/) + // }) + // }) + + // it('should handle network-error in chunked response', () => { + // const url = `${base}error/premature/chunked` + // return fetch(url).then(res => { + // expect(res.status).to.equal(200) + // expect(res.ok).to.be.true + + // return expect(new Promise((resolve, reject) => { + // res.body.on('error', reject) + // res.body.on('close', resolve) + // })).to.eventually.be.rejectedWith(Error, 'Premature close') + // .and.have.property('code', 'ERR_STREAM_PREMATURE_CLOSE') + // }) + // }) + + // it('should handle network-error in chunked response async iterator', () => { + // const url = `${base}error/premature/chunked` + // return fetch(url).then(res => { + // expect(res.status).to.equal(200) + // expect(res.ok).to.be.true + + // const read = async body => { + // const chunks = [] + + // if (isNodeLowerThan('v14.15.2')) { + // // In older Node.js versions, some errors don't come out in the async iterator; we have + // // to pick them up from the event-emitter and then throw them after the async iterator + // let error + // body.on('error', err => { + // error = err + // }) + + // for await (const chunk of body) { + // chunks.push(chunk) + // } + + // if (error) { + // throw error + // } + + // return new Promise(resolve => { + // body.on('close', () => resolve(chunks)) + // }) + // } + + // for await (const chunk of body) { + // chunks.push(chunk) + // } + + // return chunks + // } + + // return expect(read(res.body)) + // .to.eventually.be.rejectedWith(Error, 'Premature close') + // .and.have.property('code', 'ERR_STREAM_PREMATURE_CLOSE') + // }) + // }) + + // it('should handle network-error in chunked response in consumeBody', () => { + // const url = `${base}error/premature/chunked` + // return fetch(url).then(res => { + // expect(res.status).to.equal(200) + // expect(res.ok).to.be.true + + // return expect(res.text()) + // .to.eventually.be.rejectedWith(Error, 'Premature close') + // }) + // }) + + // it('should handle DNS-error response', () => { + // const url = 'http://domain.invalid' + // return expect(fetch(url)).to.eventually.be.rejected + // .and.be.an.instanceOf(FetchError) + // .and.have.property('code').that.matches(/ENOTFOUND|EAI_AGAIN/) + // }) + + // it('should reject invalid json response', () => { + // const url = `${base}error/json` + // return fetch(url).then(res => { + // expect(res.headers.get('content-type')).to.equal('application/json') + // return expect(res.json()).to.eventually.be.rejectedWith(Error) + // }) + // }) + + // it('should handle response with no status text', () => { + // const url = `${base}no-status-text` + // return fetch(url).then(res => { + // expect(res.statusText).to.equal('') + // }) + // }) + + it('should handle no content response', () => { + const url = `${base}no-content` + return fetch(url).then(res => { + expect(res.status).to.equal(204) + expect(res.statusText).to.equal('No Content') + expect(res.ok).to.be.true + return res.text().then(result => { + expect(result).to.be.a('string') + expect(result).to.be.empty + }) + }) + }) + + it('should reject when trying to parse no content response as json', () => { + const url = `${base}no-content` + return fetch(url).then(res => { + expect(res.status).to.equal(204) + expect(res.statusText).to.equal('No Content') + expect(res.ok).to.be.true + return expect(res.json()).to.eventually.be.rejectedWith(Error) + }) + }) + + it('should handle no content response with gzip encoding', () => { + const url = `${base}no-content/gzip` + return fetch(url).then(res => { + expect(res.status).to.equal(204) + expect(res.statusText).to.equal('No Content') + expect(res.headers.get('content-encoding')).to.equal('gzip') + expect(res.ok).to.be.true + return res.text().then(result => { + expect(result).to.be.a('string') + expect(result).to.be.empty + }) + }) + }) + + it('should handle not modified response', () => { + const url = `${base}not-modified` + return fetch(url).then(res => { + expect(res.status).to.equal(304) + expect(res.statusText).to.equal('Not Modified') + expect(res.ok).to.be.false + return res.text().then(result => { + expect(result).to.be.a('string') + expect(result).to.be.empty + }) + }) + }) + + it('should handle not modified response with gzip encoding', () => { + const url = `${base}not-modified/gzip` + return fetch(url).then(res => { + expect(res.status).to.equal(304) + expect(res.statusText).to.equal('Not Modified') + expect(res.headers.get('content-encoding')).to.equal('gzip') + expect(res.ok).to.be.false + return res.text().then(result => { + expect(result).to.be.a('string') + expect(result).to.be.empty + }) + }) + }) + + // it('should decompress gzip response', () => { + // const url = `${base}gzip` + // return fetch(url).then(res => { + // expect(res.headers.get('content-type')).to.equal('text/plain') + // return res.text().then(result => { + // expect(result).to.be.a('string') + // expect(result).to.equal('hello world') + // }) + // }) + // }) + + // it('should decompress slightly invalid gzip response', () => { + // const url = `${base}gzip-truncated` + // return fetch(url).then(res => { + // expect(res.headers.get('content-type')).to.equal('text/plain') + // return res.text().then(result => { + // expect(result).to.be.a('string') + // expect(result).to.equal('hello world') + // }) + // }) + // }) + + // it('should make capitalised Content-Encoding lowercase', () => { + // const url = `${base}gzip-capital` + // return fetch(url).then(res => { + // expect(res.headers.get('content-encoding')).to.equal('gzip') + // return res.text().then(result => { + // expect(result).to.be.a('string') + // expect(result).to.equal('hello world') + // }) + // }) + // }) + + // it('should decompress deflate response', () => { + // const url = `${base}deflate` + // return fetch(url).then(res => { + // expect(res.headers.get('content-type')).to.equal('text/plain') + // return res.text().then(result => { + // expect(result).to.be.a('string') + // expect(result).to.equal('hello world') + // }) + // }) + // }) + + // it('should decompress deflate raw response from old apache server', () => { + // const url = `${base}deflate-raw` + // return fetch(url).then(res => { + // expect(res.headers.get('content-type')).to.equal('text/plain') + // return res.text().then(result => { + // expect(result).to.be.a('string') + // expect(result).to.equal('hello world') + // }) + // }) + // }) + + // it('should decompress brotli response', function () { + // if (typeof zlib.createBrotliDecompress !== 'function') { + // this.skip() + // } + + // const url = `${base}brotli` + // return fetch(url).then(res => { + // expect(res.headers.get('content-type')).to.equal('text/plain') + // return res.text().then(result => { + // expect(result).to.be.a('string') + // expect(result).to.equal('hello world') + // }) + // }) + // }) + + // it('should handle no content response with brotli encoding', function () { + // if (typeof zlib.createBrotliDecompress !== 'function') { + // this.skip() + // } + + // const url = `${base}no-content/brotli` + // return fetch(url).then(res => { + // expect(res.status).to.equal(204) + // expect(res.statusText).to.equal('No Content') + // expect(res.headers.get('content-encoding')).to.equal('br') + // expect(res.ok).to.be.true + // return res.text().then(result => { + // expect(result).to.be.a('string') + // expect(result).to.be.empty + // }) + // }) + // }) + + // it('should skip decompression if unsupported', () => { + // const url = `${base}sdch` + // return fetch(url).then(res => { + // expect(res.headers.get('content-type')).to.equal('text/plain') + // return res.text().then(result => { + // expect(result).to.be.a('string') + // expect(result).to.equal('fake sdch string') + // }) + // }) + // }) + + // it('should reject if response compression is invalid', () => { + // const url = `${base}invalid-content-encoding` + // return fetch(url).then(res => { + // expect(res.headers.get('content-type')).to.equal('text/plain') + // return expect(res.text()).to.eventually.be.rejected + // .and.be.an.instanceOf(FetchError) + // .and.have.property('code', 'Z_DATA_ERROR') + // }) + // }) + + // it('should handle errors on the body stream even if it is not used', done => { + // const url = `${base}invalid-content-encoding` + // fetch(url) + // .then(res => { + // expect(res.status).to.equal(200) + // }) + // .catch(() => {}) + // .then(() => { + // // Wait a few ms to see if a uncaught error occurs + // setTimeout(() => { + // done() + // }, 20) + // }) + // }) + + // it('should collect handled errors on the body stream to reject if the body is used later', () => { + // const url = `${base}invalid-content-encoding` + // return fetch(url).then(delay(20)).then(res => { + // expect(res.headers.get('content-type')).to.equal('text/plain') + // return expect(res.text()).to.eventually.be.rejected + // .and.be.an.instanceOf(FetchError) + // .and.have.property('code', 'Z_DATA_ERROR') + // }) + // }) + + // it('should allow disabling auto decompression', () => { + // const url = `${base}gzip` + // const options = { + // compress: false + // } + // return fetch(url, options).then(res => { + // expect(res.headers.get('content-type')).to.equal('text/plain') + // return res.text().then(result => { + // expect(result).to.be.a('string') + // expect(result).to.not.equal('hello world') + // }) + // }) + // }) + + // it('should not overwrite existing accept-encoding header when auto decompression is true', () => { + // const url = `${base}inspect` + // const options = { + // compress: true, + // headers: { + // 'Accept-Encoding': 'gzip' + // } + // } + // return fetch(url, options).then(res => res.json()).then(res => { + // expect(res.headers['accept-encoding']).to.equal('gzip') + // }) + // }) + + const testAbortController = (name, buildAbortController, moreTests = null) => { + describe(`AbortController (${name})`, () => { + let controller + + beforeEach(() => { + controller = buildAbortController() + }) + + it('should support request cancellation with signal', () => { + const fetches = [ + fetch( + `${base}timeout`, + { + method: 'POST', + signal: controller.signal, + headers: { + 'Content-Type': 'application/json', + body: JSON.stringify({ hello: 'world' }) + } + } + ) + ] + setTimeout(() => { + controller.abort() + }, 100) + + return Promise.all(fetches.map(fetched => expect(fetched) + .to.eventually.be.rejected + .and.be.an.instanceOf(Error) + .and.include({ + type: 'aborted', + name: 'AbortError' + }) + )) + }) + + it('should support multiple request cancellation with signal', () => { + const fetches = [ + fetch(`${base}timeout`, { signal: controller.signal }), + fetch( + `${base}timeout`, + { + method: 'POST', + signal: controller.signal, + headers: { + 'Content-Type': 'application/json', + body: JSON.stringify({ hello: 'world' }) + } + } + ) + ] + setTimeout(() => { + controller.abort() + }, 100) + + return Promise.all(fetches.map(fetched => expect(fetched) + .to.eventually.be.rejected + .and.be.an.instanceOf(Error) + .and.include({ + type: 'aborted', + name: 'AbortError' + }) + )) + }) + + it('should reject immediately if signal has already been aborted', () => { + const url = `${base}timeout` + const options = { + signal: controller.signal + } + controller.abort() + const fetched = fetch(url, options) + return expect(fetched).to.eventually.be.rejected + .and.be.an.instanceOf(Error) + .and.include({ + type: 'aborted', + name: 'AbortError' + }) + }) + + it('should allow redirects to be aborted', () => { + const request = new Request(`${base}redirect/slow`, { + signal: controller.signal + }) + setTimeout(() => { + controller.abort() + }, 20) + return expect(fetch(request)).to.be.eventually.rejected + .and.be.an.instanceOf(Error) + .and.have.property('name', 'AbortError') + }) + + it('should allow redirected response body to be aborted', () => { + const request = new Request(`${base}redirect/slow-stream`, { + signal: controller.signal + }) + return expect(fetch(request).then(res => { + expect(res.headers.get('content-type')).to.equal('text/plain') + const result = res.text() + controller.abort() + return result + })).to.be.eventually.rejected + .and.be.an.instanceOf(Error) + .and.have.property('name', 'AbortError') + }) + + it('should reject response body with AbortError when aborted before stream has been read completely', () => { + return expect(fetch( + `${base}slow`, + { signal: controller.signal } + )) + .to.eventually.be.fulfilled + .then(res => { + const promise = res.text() + controller.abort() + return expect(promise) + .to.eventually.be.rejected + .and.be.an.instanceof(Error) + .and.have.property('name', 'AbortError') + }) + }) + + it('should reject response body methods immediately with AbortError when aborted before stream is disturbed', () => { + return expect(fetch( + `${base}slow`, + { signal: controller.signal } + )) + .to.eventually.be.fulfilled + .then(res => { + controller.abort() + return expect(res.text()) + .to.eventually.be.rejected + .and.be.an.instanceof(Error) + .and.have.property('name', 'AbortError') + }) + }) + + it('should emit error event to response body with an AbortError when aborted before underlying stream is closed', done => { + expect(fetch( + `${base}slow`, + { signal: controller.signal } + )) + .to.eventually.be.fulfilled + .then(res => { + res.body.once('error', err => { + expect(err) + .to.be.an.instanceof(Error) + .and.have.property('name', 'AbortError') + done() + }) + controller.abort() + }) + }) + + it('should cancel request body of type Stream with AbortError when aborted', () => { + const body = new stream.Readable({ objectMode: true }) + body._read = () => {} + const promise = fetch( + `${base}slow`, + { signal: controller.signal, body, method: 'POST' } + ) + + const result = Promise.all([ + new Promise((resolve, reject) => { + body.on('error', error => { + try { + expect(error).to.be.an.instanceof(Error).and.have.property('name', 'AbortError') + resolve() + } catch (error_) { + reject(error_) + } + }) + }), + expect(promise).to.eventually.be.rejected + .and.be.an.instanceof(Error) + .and.have.property('name', 'AbortError') + ]) + + controller.abort() + + return result + }) + + if (moreTests) { + moreTests() + } + }) + } + + // testAbortController('polyfill', + // () => new AbortControllerPolyfill(), + // () => { + // it('should remove internal AbortSignal event listener after request is aborted', () => { + // const controller = new AbortControllerPolyfill() + // const { signal } = controller + + // setTimeout(() => { + // controller.abort() + // }, 20) + + // return expect(fetch(`${base}timeout`, { signal })) + // .to.eventually.be.rejected + // .and.be.an.instanceof(Error) + // .and.have.property('name', 'AbortError') + // .then(() => { + // return expect(signal.listeners.abort.length).to.equal(0) + // }) + // }) + + // it('should remove internal AbortSignal event listener after request and response complete without aborting', () => { + // const controller = new AbortControllerPolyfill() + // const { signal } = controller + // const fetchHtml = fetch(`${base}html`, { signal }) + // .then(res => res.text()) + // const fetchResponseError = fetch(`${base}error/reset`, { signal }) + // const fetchRedirect = fetch(`${base}redirect/301`, { signal }).then(res => res.json()) + // return Promise.all([ + // expect(fetchHtml).to.eventually.be.fulfilled.and.equal(''), + // expect(fetchResponseError).to.be.eventually.rejected, + // expect(fetchRedirect).to.eventually.be.fulfilled + // ]).then(() => { + // expect(signal.listeners.abort.length).to.equal(0) + // }) + // }) + // } + // ) + + // testAbortController('mysticatea', () => new AbortControllerMysticatea()) + + // if (process.version > 'v15') { + // testAbortController('native', () => new AbortController()) + // } + + // it('should throw a TypeError if a signal is not of type AbortSignal or EventTarget', () => { + // return Promise.all([ + // expect(fetch(`${base}inspect`, { signal: {} })) + // .to.be.eventually.rejected + // .and.be.an.instanceof(TypeError) + // .and.have.property('message').includes('AbortSignal'), + // expect(fetch(`${base}inspect`, { signal: '' })) + // .to.be.eventually.rejected + // .and.be.an.instanceof(TypeError) + // .and.have.property('message').includes('AbortSignal'), + // expect(fetch(`${base}inspect`, { signal: Object.create(null) })) + // .to.be.eventually.rejected + // .and.be.an.instanceof(TypeError) + // .and.have.property('message').includes('AbortSignal') + // ]) + // }) + + it('should gracefully handle a nullish signal', () => { + return Promise.all([ + fetch(`${base}hello`, { signal: null }).then(res => { + return expect(res.ok).to.be.true + }), + fetch(`${base}hello`, { signal: undefined }).then(res => { + return expect(res.ok).to.be.true + }) + ]) + }) + + // it('should set default User-Agent', () => { + // const url = `${base}inspect` + // return fetch(url).then(res => res.json()).then(res => { + // expect(res.headers['user-agent']).to.startWith('node-fetch') + // }) + // }) + + it('should allow setting User-Agent', () => { + const url = `${base}inspect` + const options = { + headers: { + 'user-agent': 'faked' + } + } + return fetch(url, options).then(res => res.json()).then(res => { + expect(res.headers['user-agent']).to.equal('faked') + }) + }) + + it('should set default Accept header', () => { + const url = `${base}inspect` + fetch(url).then(res => res.json()).then(res => { + expect(res.headers.accept).to.equal('*/*') + }) + }) + + it('should allow setting Accept header', () => { + const url = `${base}inspect` + const options = { + headers: { + accept: 'application/json' + } + } + return fetch(url, options).then(res => res.json()).then(res => { + expect(res.headers.accept).to.equal('application/json') + }) + }) + + it('should allow POST request', () => { + const url = `${base}inspect` + const options = { + method: 'POST' + } + return fetch(url, options).then(res => { + return res.json() + }).then(res => { + expect(res.method).to.equal('POST') + expect(res.headers['transfer-encoding']).to.be.undefined + expect(res.headers['content-type']).to.be.undefined + expect(res.headers['content-length']).to.equal('0') + }) + }) + + it('should allow POST request with string body', () => { + const url = `${base}inspect` + const options = { + method: 'POST', + body: 'a=1' + } + return fetch(url, options).then(res => { + return res.json() + }).then(res => { + expect(res.method).to.equal('POST') + expect(res.body).to.equal('a=1') + expect(res.headers['transfer-encoding']).to.be.undefined + expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8') + expect(res.headers['content-length']).to.equal('3') + }) + }) + + it('should allow POST request with buffer body', () => { + const url = `${base}inspect` + const options = { + method: 'POST', + body: Buffer.from('a=1', 'utf-8') + } + return fetch(url, options).then(res => { + return res.json() + }).then(res => { + expect(res.method).to.equal('POST') + expect(res.body).to.equal('a=1') + expect(res.headers['transfer-encoding']).to.be.undefined + expect(res.headers['content-type']).to.be.undefined + expect(res.headers['content-length']).to.equal('3') + }) + }) + + it('should allow POST request with ArrayBuffer body', () => { + const encoder = new TextEncoder() + const url = `${base}inspect` + const options = { + method: 'POST', + body: encoder.encode('Hello, world!\n').buffer + } + return fetch(url, options).then(res => res.json()).then(res => { + expect(res.method).to.equal('POST') + expect(res.body).to.equal('Hello, world!\n') + expect(res.headers['transfer-encoding']).to.be.undefined + expect(res.headers['content-type']).to.be.undefined + expect(res.headers['content-length']).to.equal('14') + }) + }) + + it('should allow POST request with ArrayBuffer body from a VM context', () => { + const url = `${base}inspect` + const options = { + method: 'POST', + body: new VMUint8Array(Buffer.from('Hello, world!\n')).buffer + } + return fetch(url, options).then(res => res.json()).then(res => { + expect(res.method).to.equal('POST') + expect(res.body).to.equal('Hello, world!\n') + expect(res.headers['transfer-encoding']).to.be.undefined + expect(res.headers['content-type']).to.be.undefined + expect(res.headers['content-length']).to.equal('14') + }) + }) + + it('should allow POST request with ArrayBufferView (Uint8Array) body', () => { + const encoder = new TextEncoder() + const url = `${base}inspect` + const options = { + method: 'POST', + body: encoder.encode('Hello, world!\n') + } + return fetch(url, options).then(res => res.json()).then(res => { + expect(res.method).to.equal('POST') + expect(res.body).to.equal('Hello, world!\n') + expect(res.headers['transfer-encoding']).to.be.undefined + expect(res.headers['content-type']).to.be.undefined + expect(res.headers['content-length']).to.equal('14') + }) + }) + + // it('should allow POST request with ArrayBufferView (DataView) body', () => { + // const encoder = new TextEncoder() + // const url = `${base}inspect` + // const options = { + // method: 'POST', + // body: new DataView(encoder.encode('Hello, world!\n').buffer) + // } + // return fetch(url, options).then(res => res.json()).then(res => { + // expect(res.method).to.equal('POST') + // expect(res.body).to.equal('Hello, world!\n') + // expect(res.headers['transfer-encoding']).to.be.undefined + // expect(res.headers['content-type']).to.be.undefined + // expect(res.headers['content-length']).to.equal('14') + // }) + // }) + + it('should allow POST request with ArrayBufferView (Uint8Array) body from a VM context', () => { + const url = `${base}inspect` + const options = { + method: 'POST', + body: new VMUint8Array(Buffer.from('Hello, world!\n')) + } + return fetch(url, options).then(res => res.json()).then(res => { + expect(res.method).to.equal('POST') + expect(res.body).to.equal('Hello, world!\n') + expect(res.headers['transfer-encoding']).to.be.undefined + expect(res.headers['content-type']).to.be.undefined + expect(res.headers['content-length']).to.equal('14') + }) + }) + + it('should allow POST request with ArrayBufferView (Uint8Array, offset, length) body', () => { + const encoder = new TextEncoder() + const url = `${base}inspect` + const options = { + method: 'POST', + body: encoder.encode('Hello, world!\n').subarray(7, 13) + } + return fetch(url, options).then(res => res.json()).then(res => { + expect(res.method).to.equal('POST') + expect(res.body).to.equal('world!') + expect(res.headers['transfer-encoding']).to.be.undefined + expect(res.headers['content-type']).to.be.undefined + expect(res.headers['content-length']).to.equal('6') + }) + }) + + // it('should allow POST request with blob body without type', () => { + // const url = `${base}inspect` + // const options = { + // method: 'POST', + // body: new Blob(['a=1']) + // } + // return fetch(url, options).then(res => { + // return res.json() + // }).then(res => { + // expect(res.method).to.equal('POST') + // expect(res.body).to.equal('a=1') + // expect(res.headers['transfer-encoding']).to.be.undefined + // expect(res.headers['content-type']).to.be.undefined + // expect(res.headers['content-length']).to.equal('3') + // }) + // }) + + // it('should allow POST request with blob body with type', () => { + // const url = `${base}inspect` + // const options = { + // method: 'POST', + // body: new Blob(['a=1'], { + // type: 'text/plain;charset=UTF-8' + // }) + // } + // return fetch(url, options).then(res => { + // return res.json() + // }).then(res => { + // expect(res.method).to.equal('POST') + // expect(res.body).to.equal('a=1') + // expect(res.headers['transfer-encoding']).to.be.undefined + // expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8') + // expect(res.headers['content-length']).to.equal('3') + // }) + // }) + + // it('should allow POST request with readable stream as body', () => { + // const url = `${base}inspect` + // const options = { + // method: 'POST', + // body: stream.Readable.from('a=1') + // } + // return fetch(url, options).then(res => { + // return res.json() + // }).then(res => { + // expect(res.method).to.equal('POST') + // expect(res.body).to.equal('a=1') + // expect(res.headers['transfer-encoding']).to.equal('chunked') + // expect(res.headers['content-type']).to.be.undefined + // expect(res.headers['content-length']).to.be.undefined + // }) + // }) + + // it('should allow POST request with form-data as body', () => { + // const form = new FormData() + // form.append('a', '1') + + // const url = `${base}multipart` + // const options = { + // method: 'POST', + // body: form + // } + // return fetch(url, options).then(res => { + // return res.json() + // }).then(res => { + // expect(res.method).to.equal('POST') + // expect(res.headers['content-type']).to.startWith('multipart/form-data;boundary=') + // expect(res.headers['content-length']).to.be.a('string') + // expect(res.body).to.equal('a=1') + // }) + // }) + + // it('should allow POST request with form-data using stream as body', () => { + // const form = new FormData() + // form.append('my_field', fs.createReadStream('test/utils/dummy.txt')) + + // const url = `${base}multipart` + // const options = { + // method: 'POST', + // body: form + // } + + // return fetch(url, options).then(res => { + // return res.json() + // }).then(res => { + // expect(res.method).to.equal('POST') + // expect(res.headers['content-type']).to.startWith('multipart/form-data;boundary=') + // expect(res.headers['content-length']).to.be.undefined + // expect(res.body).to.contain('my_field=') + // }) + // }) + + // it('should allow POST request with form-data as body and custom headers', () => { + // const form = new FormData() + // form.append('a', '1') + + // const headers = form.getHeaders() + // headers.b = '2' + + // const url = `${base}multipart` + // const options = { + // method: 'POST', + // body: form, + // headers + // } + // return fetch(url, options).then(res => { + // return res.json() + // }).then(res => { + // expect(res.method).to.equal('POST') + // expect(res.headers['content-type']).to.startWith('multipart/form-data; boundary=') + // expect(res.headers['content-length']).to.be.a('string') + // expect(res.headers.b).to.equal('2') + // expect(res.body).to.equal('a=1') + // }) + // }) + + // it('should support spec-compliant form-data as POST body', () => { + // const form = new FormDataNode() + + // const filename = path.join('test', 'utils', 'dummy.txt') + + // form.set('field', 'some text') + // form.set('file', fileFromSync(filename)) + + // const url = `${base}multipart` + // const options = { + // method: 'POST', + // body: form + // } + + // return fetch(url, options).then(res => res.json()).then(res => { + // expect(res.method).to.equal('POST') + // expect(res.headers['content-type']).to.startWith('multipart/form-data') + // expect(res.body).to.contain('field=') + // expect(res.body).to.contain('file=') + // }) + // }) + + // it('should allow POST request with object body', () => { + // const url = `${base}inspect` + // // Note that fetch simply calls tostring on an object + // const options = { + // method: 'POST', + // body: { a: 1 } + // } + // return fetch(url, options).then(res => { + // return res.json() + // }).then(res => { + // expect(res.method).to.equal('POST') + // expect(res.body).to.equal('[object Object]') + // expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8') + // expect(res.headers['content-length']).to.equal('15') + // }) + // }) + + // it('constructing a Response with URLSearchParams as body should have a Content-Type', () => { + // const parameters = new URLSearchParams() + // const res = new Response(parameters) + // res.headers.get('Content-Type') + // expect(res.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8') + // }) + + // it('constructing a Request with URLSearchParams as body should have a Content-Type', () => { + // const parameters = new URLSearchParams() + // const request = new Request(base, { method: 'POST', body: parameters }) + // expect(request.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8') + // }) + + // it('Reading a body with URLSearchParams should echo back the result', () => { + // const parameters = new URLSearchParams() + // parameters.append('a', '1') + // return new Response(parameters).text().then(text => { + // expect(text).to.equal('a=1') + // }) + // }) + + // // Body should been cloned... + // it('constructing a Request/Response with URLSearchParams and mutating it should not affected body', () => { + // const parameters = new URLSearchParams() + // const request = new Request(`${base}inspect`, { method: 'POST', body: parameters }) + // parameters.append('a', '1') + // return request.text().then(text => { + // expect(text).to.equal('') + // }) + // }) + + it('should allow POST request with URLSearchParams as body', () => { + const parameters = new URLSearchParams() + parameters.append('a', '1') + + const url = `${base}inspect` + const options = { + method: 'POST', + body: parameters + } + return fetch(url, options).then(res => { + return res.json() + }).then(res => { + expect(res.method).to.equal('POST') + expect(res.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8') + expect(res.headers['content-length']).to.equal('3') + expect(res.body).to.equal('a=1') + }) + }) + + it('should still recognize URLSearchParams when extended', () => { + class CustomSearchParameters extends URLSearchParams {} + const parameters = new CustomSearchParameters() + parameters.append('a', '1') + + const url = `${base}inspect` + const options = { + method: 'POST', + body: parameters + } + return fetch(url, options).then(res => { + return res.json() + }).then(res => { + expect(res.method).to.equal('POST') + expect(res.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8') + expect(res.headers['content-length']).to.equal('3') + expect(res.body).to.equal('a=1') + }) + }) + + // /* For 100% code coverage, checks for duck-typing-only detection + // * where both constructor.name and brand tests fail */ + // it('should still recognize URLSearchParams when extended from polyfill', () => { + // class CustomPolyfilledSearchParameters extends URLSearchParams {} + // const parameters = new CustomPolyfilledSearchParameters() + // parameters.append('a', '1') + + // const url = `${base}inspect` + // const options = { + // method: 'POST', + // body: parameters + // } + // return fetch(url, options).then(res => { + // return res.json() + // }).then(res => { + // expect(res.method).to.equal('POST') + // expect(res.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8') + // expect(res.headers['content-length']).to.equal('3') + // expect(res.body).to.equal('a=1') + // }) + // }) + + // it('should overwrite Content-Length if possible', () => { + // const url = `${base}inspect` + // // Note that fetch simply calls tostring on an object + // const options = { + // method: 'POST', + // headers: { + // 'Content-Length': '1000' + // }, + // body: 'a=1' + // } + // return fetch(url, options).then(res => { + // return res.json() + // }).then(res => { + // expect(res.method).to.equal('POST') + // expect(res.body).to.equal('a=1') + // expect(res.headers['transfer-encoding']).to.be.undefined + // expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8') + // expect(res.headers['content-length']).to.equal('3') + // }) + // }) + + it('should allow PUT request', () => { + const url = `${base}inspect` + const options = { + method: 'PUT', + body: 'a=1' + } + return fetch(url, options).then(res => { + return res.json() + }).then(res => { + expect(res.method).to.equal('PUT') + expect(res.body).to.equal('a=1') + }) + }) + + it('should allow DELETE request', () => { + const url = `${base}inspect` + const options = { + method: 'DELETE' + } + return fetch(url, options).then(res => { + return res.json() + }).then(res => { + expect(res.method).to.equal('DELETE') + }) + }) + + it('should allow DELETE request with string body', () => { + const url = `${base}inspect` + const options = { + method: 'DELETE', + body: 'a=1' + } + return fetch(url, options).then(res => { + return res.json() + }).then(res => { + expect(res.method).to.equal('DELETE') + expect(res.body).to.equal('a=1') + expect(res.headers['transfer-encoding']).to.be.undefined + expect(res.headers['content-length']).to.equal('3') + }) + }) + + it('should allow PATCH request', () => { + const url = `${base}inspect` + const options = { + method: 'PATCH', + body: 'a=1' + } + return fetch(url, options).then(res => { + return res.json() + }).then(res => { + expect(res.method).to.equal('PATCH') + expect(res.body).to.equal('a=1') + }) + }) + + // it('should allow HEAD request', () => { + // const url = `${base}hello` + // const options = { + // method: 'HEAD' + // } + // return fetch(url, options).then(res => { + // expect(res.status).to.equal(200) + // expect(res.statusText).to.equal('OK') + // expect(res.headers.get('content-type')).to.equal('text/plain') + // expect(res.body).to.be.an.instanceof(stream.Transform) + // return res.text() + // }).then(text => { + // expect(text).to.equal('') + // }) + // }) + + it('should allow HEAD request with content-encoding header', () => { + const url = `${base}error/404` + const options = { + method: 'HEAD' + } + return fetch(url, options).then(res => { + expect(res.status).to.equal(404) + expect(res.headers.get('content-encoding')).to.equal('gzip') + return res.text() + }).then(text => { + expect(text).to.equal('') + }) + }) + + // it('should allow OPTIONS request', () => { + // const url = `${base}options` + // const options = { + // method: 'OPTIONS' + // } + // return fetch(url, options).then(res => { + // expect(res.status).to.equal(200) + // expect(res.statusText).to.equal('OK') + // expect(res.headers.get('allow')).to.equal('GET, HEAD, OPTIONS') + // expect(res.body).to.be.an.instanceof(stream.Transform) + // }) + // }) + + it('should reject decoding body twice', () => { + const url = `${base}plain` + return fetch(url).then(res => { + expect(res.headers.get('content-type')).to.equal('text/plain') + return res.text().then(() => { + expect(res.bodyUsed).to.be.true + return expect(res.text()).to.eventually.be.rejectedWith(Error) + }) + }) + }) + + // it('should support maximum response size, multiple chunk', () => { + // const url = `${base}size/chunk` + // const options = { + // size: 5 + // } + // return fetch(url, options).then(res => { + // expect(res.status).to.equal(200) + // expect(res.headers.get('content-type')).to.equal('text/plain') + // return expect(res.text()).to.eventually.be.rejected + // .and.be.an.instanceOf(FetchError) + // .and.have.property('type', 'max-size') + // }) + // }) + + // it('should support maximum response size, single chunk', () => { + // const url = `${base}size/long` + // const options = { + // size: 5 + // } + // return fetch(url, options).then(res => { + // expect(res.status).to.equal(200) + // expect(res.headers.get('content-type')).to.equal('text/plain') + // return expect(res.text()).to.eventually.be.rejected + // .and.be.an.instanceOf(FetchError) + // .and.have.property('type', 'max-size') + // }) + // }) + + // it('should allow piping response body as stream', () => { + // const url = `${base}hello` + // return fetch(url).then(res => { + // expect(res.body).to.be.an.instanceof(stream.Transform) + // return streamToPromise(res.body, chunk => { + // if (chunk === null) { + // return + // } + + // expect(chunk.toString()).to.equal('world') + // }) + // }) + // }) + + // it('should allow cloning a response, and use both as stream', () => { + // const url = `${base}hello` + // return fetch(url).then(res => { + // const r1 = res.clone() + // expect(res.body).to.be.an.instanceof(stream.Transform) + // expect(r1.body).to.be.an.instanceof(stream.Transform) + // const dataHandler = chunk => { + // if (chunk === null) { + // return + // } + + // expect(chunk.toString()).to.equal('world') + // } + + // return Promise.all([ + // streamToPromise(res.body, dataHandler), + // streamToPromise(r1.body, dataHandler) + // ]) + // }) + // }) + + it('should allow cloning a json response and log it as text response', () => { + const url = `${base}json` + return fetch(url).then(res => { + const r1 = res.clone() + return Promise.all([res.json(), r1.text()]).then(results => { + expect(results[0]).to.deep.equal({ name: 'value' }) + expect(results[1]).to.equal('{"name":"value"}') + }) + }) + }) + + it('should allow cloning a json response, and then log it as text response', () => { + const url = `${base}json` + return fetch(url).then(res => { + const r1 = res.clone() + return res.json().then(result => { + expect(result).to.deep.equal({ name: 'value' }) + return r1.text().then(result => { + expect(result).to.equal('{"name":"value"}') + }) + }) + }) + }) + + it('should allow cloning a json response, first log as text response, then return json object', () => { + const url = `${base}json` + return fetch(url).then(res => { + const r1 = res.clone() + return r1.text().then(result => { + expect(result).to.equal('{"name":"value"}') + return res.json().then(result => { + expect(result).to.deep.equal({ name: 'value' }) + }) + }) + }) + }) + + // it('should not allow cloning a response after its been used', () => { + // const url = `${base}hello` + // return fetch(url).then(res => + // res.text().then(() => { + // expect(() => { + // res.clone() + // }).to.throw(Error) + // }) + // ) + // }) + + // it('the default highWaterMark should equal 16384', () => { + // const url = `${base}hello` + // return fetch(url).then(res => { + // expect(res.highWaterMark).to.equal(16384) + // }) + // }) + + // it('should timeout on cloning response without consuming one of the streams when the second packet size is equal default highWaterMark', function () { + // this.timeout(300) + // const url = local.mockResponse(res => { + // // Observed behavior of TCP packets splitting: + // // - response body size <= 65438 → single packet sent + // // - response body size > 65438 → multiple packets sent + // // Max TCP packet size is 64kB (https://stackoverflow.com/a/2614188/5763764), + // // but first packet probably transfers more than the response body. + // const firstPacketMaxSize = 65438 + // const secondPacketSize = 16 * 1024 // = defaultHighWaterMark + // res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize)) + // }) + // return expect( + // fetch(url).then(res => res.clone().buffer()) + // ).to.timeout + // }) + + // it('should timeout on cloning response without consuming one of the streams when the second packet size is equal custom highWaterMark', function () { + // this.timeout(300) + // const url = local.mockResponse(res => { + // const firstPacketMaxSize = 65438 + // const secondPacketSize = 10 + // res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize)) + // }) + // return expect( + // fetch(url, { highWaterMark: 10 }).then(res => res.clone().buffer()) + // ).to.timeout + // }) + + // it('should not timeout on cloning response without consuming one of the streams when the second packet size is less than default highWaterMark', function () { + // // TODO: fix test. + // if (!isNodeLowerThan('v16.0.0')) { + // this.skip() + // } + + // this.timeout(300) + // const url = local.mockResponse(res => { + // const firstPacketMaxSize = 65438 + // const secondPacketSize = 16 * 1024 // = defaultHighWaterMark + // res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1)) + // }) + // return expect( + // fetch(url).then(res => res.clone().buffer()) + // ).not.to.timeout + // }) + + // it('should not timeout on cloning response without consuming one of the streams when the second packet size is less than custom highWaterMark', function () { + // // TODO: fix test. + // if (!isNodeLowerThan('v16.0.0')) { + // this.skip() + // } + + // this.timeout(300) + // const url = local.mockResponse(res => { + // const firstPacketMaxSize = 65438 + // const secondPacketSize = 10 + // res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1)) + // }) + // return expect( + // fetch(url, { highWaterMark: 10 }).then(res => res.clone().buffer()) + // ).not.to.timeout + // }) + + // it('should not timeout on cloning response without consuming one of the streams when the response size is double the custom large highWaterMark - 1', function () { + // // TODO: fix test. + // if (!isNodeLowerThan('v16.0.0')) { + // this.skip() + // } + + // this.timeout(300) + // const url = local.mockResponse(res => { + // res.end(crypto.randomBytes((2 * 512 * 1024) - 1)) + // }) + // return expect( + // fetch(url, { highWaterMark: 512 * 1024 }).then(res => res.clone().buffer()) + // ).not.to.timeout + // }) + + it('should allow get all responses of a header', () => { + const url = `${base}cookie` + return fetch(url).then(res => { + const expected = 'a=1, b=1' + expect(res.headers.get('set-cookie')).to.equal(expected) + expect(res.headers.get('Set-Cookie')).to.equal(expected) + }) + }) + + // it('should return all headers using raw()', () => { + // const url = `${base}cookie` + // return fetch(url).then(res => { + // const expected = [ + // 'a=1', + // 'b=1' + // ] + + // expect(res.headers.raw()['set-cookie']).to.deep.equal(expected) + // }) + // }) + + it('should allow deleting header', () => { + const url = `${base}cookie` + return fetch(url).then(res => { + res.headers.delete('set-cookie') + expect(res.headers.get('set-cookie')).to.be.null + }) + }) + + // it('should send request with connection keep-alive if agent is provided', () => { + // const url = `${base}inspect` + // const options = { + // agent: new http.Agent({ + // keepAlive: true + // }) + // } + // return fetch(url, options).then(res => { + // return res.json() + // }).then(res => { + // expect(res.headers.connection).to.equal('keep-alive') + // }) + // }) + + // it('should support fetch with Request instance', () => { + // const url = `${base}hello` + // const request = new Request(url) + // return fetch(request).then(res => { + // expect(res.url).to.equal(url) + // expect(res.ok).to.be.true + // expect(res.status).to.equal(200) + // }) + // }) + + // it('should support fetch with Node.js URL object', () => { + // const url = `${base}hello` + // const urlObject = new URL(url) + // const request = new Request(urlObject) + // return fetch(request).then(res => { + // expect(res.url).to.equal(url) + // expect(res.ok).to.be.true + // expect(res.status).to.equal(200) + // }) + // }) + + // it('should support fetch with WHATWG URL object', () => { + // const url = `${base}hello` + // const urlObject = new URL(url) + // const request = new Request(urlObject) + // return fetch(request).then(res => { + // expect(res.url).to.equal(url) + // expect(res.ok).to.be.true + // expect(res.status).to.equal(200) + // }) + // }) + + // it('should keep `?` sign in URL when no params are given', () => { + // const url = `${base}question?` + // const urlObject = new URL(url) + // const request = new Request(urlObject) + // return fetch(request).then(res => { + // expect(res.url).to.equal(url) + // expect(res.ok).to.be.true + // expect(res.status).to.equal(200) + // }) + // }) + + // it('if params are given, do not modify anything', () => { + // const url = `${base}question?a=1` + // const urlObject = new URL(url) + // const request = new Request(urlObject) + // return fetch(request).then(res => { + // expect(res.url).to.equal(url) + // expect(res.ok).to.be.true + // expect(res.status).to.equal(200) + // }) + // }) + + // it('should preserve the hash (#) symbol', () => { + // const url = `${base}question?#` + // const urlObject = new URL(url) + // const request = new Request(urlObject) + // return fetch(request).then(res => { + // expect(res.url).to.equal(url) + // expect(res.ok).to.be.true + // expect(res.status).to.equal(200) + // }) + // }) + + // it('should support reading blob as text', () => { + // return new Response('hello') + // .blob() + // .then(blob => blob.text()) + // .then(body => { + // expect(body).to.equal('hello') + // }) + // }) + + // it('should support reading blob as arrayBuffer', () => { + // return new Response('hello') + // .blob() + // .then(blob => blob.arrayBuffer()) + // .then(ab => { + // const string = String.fromCharCode.apply(null, new Uint8Array(ab)) + // expect(string).to.equal('hello') + // }) + // }) + + // it('should support reading blob as stream', () => { + // return new Response('hello') + // .blob() + // .then(blob => streamToPromise(stream.Readable.from(blob.stream()), data => { + // const string = Buffer.from(data).toString() + // expect(string).to.equal('hello') + // })) + // }) + + // it('should support blob round-trip', () => { + // const url = `${base}hello` + + // let length + // let type + + // return fetch(url).then(res => res.blob()).then(async blob => { + // const url = `${base}inspect` + // length = blob.size + // type = blob.type + // return fetch(url, { + // method: 'POST', + // body: blob + // }) + // }).then(res => res.json()).then(({ body, headers }) => { + // expect(body).to.equal('world') + // expect(headers['content-type']).to.equal(type) + // expect(headers['content-length']).to.equal(String(length)) + // }) + // }) + + // it('should support overwrite Request instance', () => { + // const url = `${base}inspect` + // const request = new Request(url, { + // method: 'POST', + // headers: { + // a: '1' + // } + // }) + // return fetch(request, { + // method: 'GET', + // headers: { + // a: '2' + // } + // }).then(res => { + // return res.json() + // }).then(body => { + // expect(body.method).to.equal('GET') + // expect(body.headers.a).to.equal('2') + // }) + // }) + + // it('should support arrayBuffer(), blob(), text(), json() and buffer() method in Body constructor', () => { + // const body = new Body('a=1') + // expect(body).to.have.property('arrayBuffer') + // expect(body).to.have.property('blob') + // expect(body).to.have.property('text') + // expect(body).to.have.property('json') + // expect(body).to.have.property('buffer') + // }) + + // /* eslint-disable-next-line func-names */ + // it('should create custom FetchError', function funcName () { + // const systemError = new Error('system') + // systemError.code = 'ESOMEERROR' + + // const err = new FetchError('test message', 'test-error', systemError) + // expect(err).to.be.an.instanceof(Error) + // expect(err).to.be.an.instanceof(FetchError) + // expect(err.name).to.equal('FetchError') + // expect(err.message).to.equal('test message') + // expect(err.type).to.equal('test-error') + // expect(err.code).to.equal('ESOMEERROR') + // expect(err.errno).to.equal('ESOMEERROR') + // // Reading the stack is quite slow (~30-50ms) + // expect(err.stack).to.include('funcName').and.to.startWith(`${err.name}: ${err.message}`) + // }) + + // it('should support https request', function () { + // this.timeout(5000) + // const url = 'https://github.com/' + // const options = { + // method: 'HEAD' + // } + // return fetch(url, options).then(res => { + // expect(res.status).to.equal(200) + // expect(res.ok).to.be.true + // }) + // }) + + // // Issue #414 + // it('should reject if attempt to accumulate body stream throws', () => { + // const res = new Response(stream.Readable.from((async function * () { + // yield Buffer.from('tada') + // await new Promise(resolve => { + // setTimeout(resolve, 200) + // }) + // yield { tada: 'yes' } + // })())) + + // return expect(res.text()).to.eventually.be.rejected + // .and.be.an.instanceOf(FetchError) + // .and.include({ type: 'system' }) + // .and.have.property('message').that.include('Could not create Buffer') + // }) + + // it('supports supplying a lookup function to the agent', () => { + // const url = `${base}redirect/301` + // let called = 0 + // function lookupSpy (hostname, options, callback) { + // called++ + + // // eslint-disable-next-line node/prefer-promises/dns + // return lookup(hostname, options, callback) + // } + + // const agent = http.Agent({ lookup: lookupSpy }) + // return fetch(url, { agent }).then(() => { + // expect(called).to.equal(2) + // }) + // }) + + // it('supports supplying a famliy option to the agent', () => { + // const url = `${base}redirect/301` + // const families = [] + // const family = Symbol('family') + // function lookupSpy (hostname, options, callback) { + // families.push(options.family) + + // // eslint-disable-next-line node/prefer-promises/dns + // return lookup(hostname, {}, callback) + // } + + // const agent = http.Agent({ lookup: lookupSpy, family }) + // return fetch(url, { agent }).then(() => { + // expect(families).to.have.length(2) + // expect(families[0]).to.equal(family) + // expect(families[1]).to.equal(family) + // }) + // }) + + // it('should allow a function supplying the agent', () => { + // const url = `${base}inspect` + + // const agent = new http.Agent({ + // keepAlive: true + // }) + + // let parsedURL + + // return fetch(url, { + // agent (_parsedURL) { + // parsedURL = _parsedURL + // return agent + // } + // }).then(res => { + // return res.json() + // }).then(res => { + // // The agent provider should have been called + // expect(parsedURL.protocol).to.equal('http:') + // // The agent we returned should have been used + // expect(res.headers.connection).to.equal('keep-alive') + // }) + // }) + + // it('should calculate content length and extract content type for each body type', () => { + // const url = `${base}hello` + // const bodyContent = 'a=1' + + // const streamBody = stream.Readable.from(bodyContent) + // const streamRequest = new Request(url, { + // method: 'POST', + // body: streamBody, + // size: 1024 + // }) + + // const blobBody = new Blob([bodyContent], { type: 'text/plain' }) + // const blobRequest = new Request(url, { + // method: 'POST', + // body: blobBody, + // size: 1024 + // }) + + // const formBody = new FormData() + // formBody.append('a', '1') + // const formRequest = new Request(url, { + // method: 'POST', + // body: formBody, + // size: 1024 + // }) + + // const bufferBody = Buffer.from(bodyContent) + // const bufferRequest = new Request(url, { + // method: 'POST', + // body: bufferBody, + // size: 1024 + // }) + + // const stringRequest = new Request(url, { + // method: 'POST', + // body: bodyContent, + // size: 1024 + // }) + + // const nullRequest = new Request(url, { + // method: 'GET', + // body: null, + // size: 1024 + // }) + + // expect(getTotalBytes(streamRequest)).to.be.null + // expect(getTotalBytes(blobRequest)).to.equal(blobBody.size) + // expect(getTotalBytes(formRequest)).to.not.be.null + // expect(getTotalBytes(bufferRequest)).to.equal(bufferBody.length) + // expect(getTotalBytes(stringRequest)).to.equal(bodyContent.length) + // expect(getTotalBytes(nullRequest)).to.equal(0) + + // expect(extractContentType(streamBody)).to.be.null + // expect(extractContentType(blobBody)).to.equal('text/plain') + // expect(extractContentType(formBody)).to.startWith('multipart/form-data') + // expect(extractContentType(bufferBody)).to.be.null + // expect(extractContentType(bodyContent)).to.equal('text/plain;charset=UTF-8') + // expect(extractContentType(null)).to.be.null + // }) + + // it('should encode URLs as UTF-8', async () => { + // const url = `${base}möbius` + // const res = await fetch(url) + // expect(res.url).to.equal(`${base}m%C3%B6bius`) + // }) +}) diff --git a/test/node-fetch/request.js b/test/node-fetch/request.js new file mode 100644 index 00000000000..867ee732489 --- /dev/null +++ b/test/node-fetch/request.js @@ -0,0 +1,277 @@ +// import stream from 'stream' +// import http from 'http' + +// import AbortController from 'abort-controller' +// import chai from 'chai' +// import FormData from 'form-data' +// import Blob from 'fetch-blob' + +// import { Request } from '../src/index.js' +// import TestServer from './utils/server.js' + +// const { expect } = chai + +// describe('Request', () => { +// const local = new TestServer() +// let base + +// before(async () => { +// await local.start() +// base = `http://${local.hostname}:${local.port}/` +// }) + +// after(async () => { +// return local.stop() +// }) + +// it('should have attributes conforming to Web IDL', () => { +// const request = new Request('https://github.com/') +// const enumerableProperties = [] +// for (const property in request) { +// enumerableProperties.push(property) +// } + +// for (const toCheck of [ +// 'body', +// 'bodyUsed', +// 'arrayBuffer', +// 'blob', +// 'json', +// 'text', +// 'method', +// 'url', +// 'headers', +// 'redirect', +// 'clone', +// 'signal' +// ]) { +// expect(enumerableProperties).to.contain(toCheck) +// } + +// for (const toCheck of [ +// 'body', 'bodyUsed', 'method', 'url', 'headers', 'redirect', 'signal' +// ]) { +// expect(() => { +// request[toCheck] = 'abc' +// }).to.throw() +// } +// }) + +// it('should support wrapping Request instance', () => { +// const url = `${base}hello` + +// const form = new FormData() +// form.append('a', '1') +// const { signal } = new AbortController() + +// const r1 = new Request(url, { +// method: 'POST', +// follow: 1, +// body: form, +// signal +// }) +// const r2 = new Request(r1, { +// follow: 2 +// }) + +// expect(r2.url).to.equal(url) +// expect(r2.method).to.equal('POST') +// expect(r2.signal).to.equal(signal) +// // Note that we didn't clone the body +// expect(r2.body).to.equal(form) +// expect(r1.follow).to.equal(1) +// expect(r2.follow).to.equal(2) +// expect(r1.counter).to.equal(0) +// expect(r2.counter).to.equal(0) +// }) + +// it('should override signal on derived Request instances', () => { +// const parentAbortController = new AbortController() +// const derivedAbortController = new AbortController() +// const parentRequest = new Request(`${base}hello`, { +// signal: parentAbortController.signal +// }) +// const derivedRequest = new Request(parentRequest, { +// signal: derivedAbortController.signal +// }) +// expect(parentRequest.signal).to.equal(parentAbortController.signal) +// expect(derivedRequest.signal).to.equal(derivedAbortController.signal) +// }) + +// it('should allow removing signal on derived Request instances', () => { +// const parentAbortController = new AbortController() +// const parentRequest = new Request(`${base}hello`, { +// signal: parentAbortController.signal +// }) +// const derivedRequest = new Request(parentRequest, { +// signal: null +// }) +// expect(parentRequest.signal).to.equal(parentAbortController.signal) +// expect(derivedRequest.signal).to.equal(null) +// }) + +// it('should throw error with GET/HEAD requests with body', () => { +// expect(() => new Request(base, { body: '' })) +// .to.throw(TypeError) +// expect(() => new Request(base, { body: 'a' })) +// .to.throw(TypeError) +// expect(() => new Request(base, { body: '', method: 'HEAD' })) +// .to.throw(TypeError) +// expect(() => new Request(base, { body: 'a', method: 'HEAD' })) +// .to.throw(TypeError) +// expect(() => new Request(base, { body: 'a', method: 'get' })) +// .to.throw(TypeError) +// expect(() => new Request(base, { body: 'a', method: 'head' })) +// .to.throw(TypeError) +// }) + +// it('should default to null as body', () => { +// const request = new Request(base) +// expect(request.body).to.equal(null) +// return request.text().then(result => expect(result).to.equal('')) +// }) + +// it('should support parsing headers', () => { +// const url = base +// const request = new Request(url, { +// headers: { +// a: '1' +// } +// }) +// expect(request.url).to.equal(url) +// expect(request.headers.get('a')).to.equal('1') +// }) + +// it('should support arrayBuffer() method', () => { +// const url = base +// const request = new Request(url, { +// method: 'POST', +// body: 'a=1' +// }) +// expect(request.url).to.equal(url) +// return request.arrayBuffer().then(result => { +// expect(result).to.be.an.instanceOf(ArrayBuffer) +// const string = String.fromCharCode.apply(null, new Uint8Array(result)) +// expect(string).to.equal('a=1') +// }) +// }) + +// it('should support text() method', () => { +// const url = base +// const request = new Request(url, { +// method: 'POST', +// body: 'a=1' +// }) +// expect(request.url).to.equal(url) +// return request.text().then(result => { +// expect(result).to.equal('a=1') +// }) +// }) + +// it('should support json() method', () => { +// const url = base +// const request = new Request(url, { +// method: 'POST', +// body: '{"a":1}' +// }) +// expect(request.url).to.equal(url) +// return request.json().then(result => { +// expect(result.a).to.equal(1) +// }) +// }) + +// it('should support buffer() method', () => { +// const url = base +// const request = new Request(url, { +// method: 'POST', +// body: 'a=1' +// }) +// expect(request.url).to.equal(url) +// return request.buffer().then(result => { +// expect(result.toString()).to.equal('a=1') +// }) +// }) + +// it('should support blob() method', () => { +// const url = base +// const request = new Request(url, { +// method: 'POST', +// body: Buffer.from('a=1') +// }) +// expect(request.url).to.equal(url) +// return request.blob().then(result => { +// expect(result).to.be.an.instanceOf(Blob) +// expect(result.size).to.equal(3) +// expect(result.type).to.equal('') +// }) +// }) + +// it('should support clone() method', () => { +// const url = base +// const body = stream.Readable.from('a=1') +// const agent = new http.Agent() +// const { signal } = new AbortController() +// const request = new Request(url, { +// body, +// method: 'POST', +// redirect: 'manual', +// headers: { +// b: '2' +// }, +// follow: 3, +// compress: false, +// agent, +// signal +// }) +// const cl = request.clone() +// expect(cl.url).to.equal(url) +// expect(cl.method).to.equal('POST') +// expect(cl.redirect).to.equal('manual') +// expect(cl.headers.get('b')).to.equal('2') +// expect(cl.follow).to.equal(3) +// expect(cl.compress).to.equal(false) +// expect(cl.method).to.equal('POST') +// expect(cl.counter).to.equal(0) +// expect(cl.agent).to.equal(agent) +// expect(cl.signal).to.equal(signal) +// // Clone body shouldn't be the same body +// expect(cl.body).to.not.equal(body) +// return Promise.all([cl.text(), request.text()]).then(results => { +// expect(results[0]).to.equal('a=1') +// expect(results[1]).to.equal('a=1') +// }) +// }) + +// it('should support ArrayBuffer as body', () => { +// const encoder = new TextEncoder() +// const request = new Request(base, { +// method: 'POST', +// body: encoder.encode('a=1').buffer +// }) +// return request.text().then(result => { +// expect(result).to.equal('a=1') +// }) +// }) + +// it('should support Uint8Array as body', () => { +// const encoder = new TextEncoder() +// const request = new Request(base, { +// method: 'POST', +// body: encoder.encode('a=1') +// }) +// return request.text().then(result => { +// expect(result).to.equal('a=1') +// }) +// }) + +// it('should support DataView as body', () => { +// const encoder = new TextEncoder() +// const request = new Request(base, { +// method: 'POST', +// body: new DataView(encoder.encode('a=1').buffer) +// }) +// return request.text().then(result => { +// expect(result).to.equal('a=1') +// }) +// }) +// }) diff --git a/test/node-fetch/response.js b/test/node-fetch/response.js new file mode 100644 index 00000000000..e1afad41e3c --- /dev/null +++ b/test/node-fetch/response.js @@ -0,0 +1,218 @@ + +// import * as stream from 'stream' +// import chai from 'chai' +// import Blob from 'fetch-blob' +// import { Response } from '../src/index.js' +// import TestServer from './utils/server.js' + +// const { expect } = chai + +// describe('Response', () => { +// const local = new TestServer() +// let base + +// before(async () => { +// await local.start() +// base = `http://${local.hostname}:${local.port}/` +// }) + +// after(async () => { +// return local.stop() +// }) + +// it('should have attributes conforming to Web IDL', () => { +// const res = new Response() +// const enumerableProperties = [] +// for (const property in res) { +// enumerableProperties.push(property) +// } + +// for (const toCheck of [ +// 'body', +// 'bodyUsed', +// 'arrayBuffer', +// 'blob', +// 'json', +// 'text', +// 'type', +// 'url', +// 'status', +// 'ok', +// 'redirected', +// 'statusText', +// 'headers', +// 'clone' +// ]) { +// expect(enumerableProperties).to.contain(toCheck) +// } + +// for (const toCheck of [ +// 'body', +// 'bodyUsed', +// 'type', +// 'url', +// 'status', +// 'ok', +// 'redirected', +// 'statusText', +// 'headers' +// ]) { +// expect(() => { +// res[toCheck] = 'abc' +// }).to.throw() +// } +// }) + +// it('should support empty options', () => { +// const res = new Response(stream.Readable.from('a=1')) +// return res.text().then(result => { +// expect(result).to.equal('a=1') +// }) +// }) + +// it('should support parsing headers', () => { +// const res = new Response(null, { +// headers: { +// a: '1' +// } +// }) +// expect(res.headers.get('a')).to.equal('1') +// }) + +// it('should support text() method', () => { +// const res = new Response('a=1') +// return res.text().then(result => { +// expect(result).to.equal('a=1') +// }) +// }) + +// it('should support json() method', () => { +// const res = new Response('{"a":1}') +// return res.json().then(result => { +// expect(result.a).to.equal(1) +// }) +// }) + +// it('should support buffer() method', () => { +// const res = new Response('a=1') +// return res.buffer().then(result => { +// expect(result.toString()).to.equal('a=1') +// }) +// }) + +// it('should support blob() method', () => { +// const res = new Response('a=1', { +// method: 'POST', +// headers: { +// 'Content-Type': 'text/plain' +// } +// }) +// return res.blob().then(result => { +// expect(result).to.be.an.instanceOf(Blob) +// expect(result.size).to.equal(3) +// expect(result.type).to.equal('text/plain') +// }) +// }) + +// it('should support clone() method', () => { +// const body = stream.Readable.from('a=1') +// const res = new Response(body, { +// headers: { +// a: '1' +// }, +// url: base, +// status: 346, +// statusText: 'production' +// }) +// const cl = res.clone() +// expect(cl.headers.get('a')).to.equal('1') +// expect(cl.type).to.equal('default') +// expect(cl.url).to.equal(base) +// expect(cl.status).to.equal(346) +// expect(cl.statusText).to.equal('production') +// expect(cl.ok).to.be.false +// // Clone body shouldn't be the same body +// expect(cl.body).to.not.equal(body) +// return cl.text().then(result => { +// expect(result).to.equal('a=1') +// }) +// }) + +// it('should support stream as body', () => { +// const body = stream.Readable.from('a=1') +// const res = new Response(body) +// return res.text().then(result => { +// expect(result).to.equal('a=1') +// }) +// }) + +// it('should support string as body', () => { +// const res = new Response('a=1') +// return res.text().then(result => { +// expect(result).to.equal('a=1') +// }) +// }) + +// it('should support buffer as body', () => { +// const res = new Response(Buffer.from('a=1')) +// return res.text().then(result => { +// expect(result).to.equal('a=1') +// }) +// }) + +// it('should support ArrayBuffer as body', () => { +// const encoder = new TextEncoder() +// const res = new Response(encoder.encode('a=1')) +// return res.text().then(result => { +// expect(result).to.equal('a=1') +// }) +// }) + +// it('should support blob as body', async () => { +// const res = new Response(new Blob(['a=1'])) +// return res.text().then(result => { +// expect(result).to.equal('a=1') +// }) +// }) + +// it('should support Uint8Array as body', () => { +// const encoder = new TextEncoder() +// const res = new Response(encoder.encode('a=1')) +// return res.text().then(result => { +// expect(result).to.equal('a=1') +// }) +// }) + +// it('should support DataView as body', () => { +// const encoder = new TextEncoder() +// const res = new Response(new DataView(encoder.encode('a=1').buffer)) +// return res.text().then(result => { +// expect(result).to.equal('a=1') +// }) +// }) + +// it('should default to null as body', () => { +// const res = new Response() +// expect(res.body).to.equal(null) + +// return res.text().then(result => expect(result).to.equal('')) +// }) + +// it('should default to 200 as status code', () => { +// const res = new Response(null) +// expect(res.status).to.equal(200) +// }) + +// it('should default to empty string as url', () => { +// const res = new Response() +// expect(res.url).to.equal('') +// }) + +// it('should support error() static method', () => { +// const res = Response.error() +// expect(res).to.be.an.instanceof(Response) +// expect(res.type).to.equal('error') +// expect(res.status).to.equal(0) +// expect(res.statusText).to.equal('') +// }) +// }) diff --git a/test/node-fetch/utils/chai-timeout.js b/test/node-fetch/utils/chai-timeout.js new file mode 100644 index 00000000000..6838a4cc322 --- /dev/null +++ b/test/node-fetch/utils/chai-timeout.js @@ -0,0 +1,15 @@ +const pTimeout = require('p-timeout') + +module.exports = ({ Assertion }, utils) => { + utils.addProperty(Assertion.prototype, 'timeout', async function () { + let timeouted = false + await pTimeout(this._obj, 150, () => { + timeouted = true + }) + return this.assert( + timeouted, + 'expected promise to timeout but it was resolved', + 'expected promise not to timeout but it timed out' + ) + }) +} diff --git a/test/node-fetch/utils/dummy.txt b/test/node-fetch/utils/dummy.txt new file mode 100644 index 00000000000..5ca51916b39 --- /dev/null +++ b/test/node-fetch/utils/dummy.txt @@ -0,0 +1 @@ +i am a dummy \ No newline at end of file diff --git a/test/node-fetch/utils/read-stream.js b/test/node-fetch/utils/read-stream.js new file mode 100644 index 00000000000..7d791532934 --- /dev/null +++ b/test/node-fetch/utils/read-stream.js @@ -0,0 +1,9 @@ +module.exports = async function readStream (stream) { + const chunks = [] + + for await (const chunk of stream) { + chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk)) + } + + return Buffer.concat(chunks) +} diff --git a/test/node-fetch/utils/server.js b/test/node-fetch/utils/server.js new file mode 100644 index 00000000000..9b625ae92f5 --- /dev/null +++ b/test/node-fetch/utils/server.js @@ -0,0 +1,430 @@ +const http = require('http') +const zlib = require('zlib') +const { once } = require('events') +const Busboy = require('busboy') + +module.exports = class TestServer { + constructor () { + this.server = http.createServer(this.router) + // Node 8 default keepalive timeout is 5000ms + // make it shorter here as we want to close server quickly at the end of tests + this.server.keepAliveTimeout = 1000 + this.server.on('error', err => { + console.log(err.stack) + }) + this.server.on('connection', socket => { + socket.setTimeout(1500) + }) + } + + async start () { + this.server.listen(0, 'localhost') + return once(this.server, 'listening') + } + + async stop () { + this.server.close() + return once(this.server, 'close') + } + + get port () { + return this.server.address().port + } + + get hostname () { + return 'localhost' + } + + mockResponse (responseHandler) { + this.server.nextResponseHandler = responseHandler + return `http://${this.hostname}:${this.port}/mocked` + } + + router (request, res) { + const p = request.url + + if (p === '/mocked') { + if (this.nextResponseHandler) { + this.nextResponseHandler(res) + this.nextResponseHandler = undefined + } else { + throw new Error('No mocked response. Use ’TestServer.mockResponse()’.') + } + } + + if (p === '/hello') { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + res.end('world') + } + + if (p.includes('question')) { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + res.end('ok') + } + + if (p === '/plain') { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + res.end('text') + } + + if (p === '/no-status-text') { + res.writeHead(200, '', {}).end() + } + + if (p === '/options') { + res.statusCode = 200 + res.setHeader('Allow', 'GET, HEAD, OPTIONS') + res.end('hello world') + } + + if (p === '/html') { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/html') + res.end('') + } + + if (p === '/json') { + res.statusCode = 200 + res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify({ + name: 'value' + })) + } + + if (p === '/gzip') { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + res.setHeader('Content-Encoding', 'gzip') + zlib.gzip('hello world', (err, buffer) => { + if (err) { + throw err + } + + res.end(buffer) + }) + } + + if (p === '/gzip-truncated') { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + res.setHeader('Content-Encoding', 'gzip') + zlib.gzip('hello world', (err, buffer) => { + if (err) { + throw err + } + + // Truncate the CRC checksum and size check at the end of the stream + res.end(buffer.slice(0, -8)) + }) + } + + if (p === '/gzip-capital') { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + res.setHeader('Content-Encoding', 'GZip') + zlib.gzip('hello world', (err, buffer) => { + if (err) { + throw err + } + + res.end(buffer) + }) + } + + if (p === '/deflate') { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + res.setHeader('Content-Encoding', 'deflate') + zlib.deflate('hello world', (err, buffer) => { + if (err) { + throw err + } + + res.end(buffer) + }) + } + + if (p === '/brotli') { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + if (typeof zlib.createBrotliDecompress === 'function') { + res.setHeader('Content-Encoding', 'br') + zlib.brotliCompress('hello world', (err, buffer) => { + if (err) { + throw err + } + + res.end(buffer) + }) + } + } + + if (p === '/deflate-raw') { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + res.setHeader('Content-Encoding', 'deflate') + zlib.deflateRaw('hello world', (err, buffer) => { + if (err) { + throw err + } + + res.end(buffer) + }) + } + + if (p === '/sdch') { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + res.setHeader('Content-Encoding', 'sdch') + res.end('fake sdch string') + } + + if (p === '/invalid-content-encoding') { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + res.setHeader('Content-Encoding', 'gzip') + res.end('fake gzip string') + } + + if (p === '/timeout') { + setTimeout(() => { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + res.end('text') + }, 1000) + } + + if (p === '/slow') { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + res.write('test') + setTimeout(() => { + res.end('test') + }, 1000) + } + + if (p === '/cookie') { + res.statusCode = 200 + res.setHeader('Set-Cookie', ['a=1', 'b=1']) + res.end('cookie') + } + + if (p === '/size/chunk') { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + setTimeout(() => { + res.write('test') + }, 10) + setTimeout(() => { + res.end('test') + }, 20) + } + + if (p === '/size/long') { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + res.end('testtest') + } + + if (p === '/redirect/301') { + res.statusCode = 301 + res.setHeader('Location', '/inspect') + res.end() + } + + if (p === '/redirect/302') { + res.statusCode = 302 + res.setHeader('Location', '/inspect') + res.end() + } + + if (p === '/redirect/303') { + res.statusCode = 303 + res.setHeader('Location', '/inspect') + res.end() + } + + if (p === '/redirect/307') { + res.statusCode = 307 + res.setHeader('Location', '/inspect') + res.end() + } + + if (p === '/redirect/308') { + res.statusCode = 308 + res.setHeader('Location', '/inspect') + res.end() + } + + if (p === '/redirect/chain') { + res.statusCode = 301 + res.setHeader('Location', '/redirect/301') + res.end() + } + + if (p === '/redirect/no-location') { + res.statusCode = 301 + res.end() + } + + if (p === '/redirect/slow') { + res.statusCode = 301 + res.setHeader('Location', '/redirect/301') + setTimeout(() => { + res.end() + }, 1000) + } + + if (p === '/redirect/slow-chain') { + res.statusCode = 301 + res.setHeader('Location', '/redirect/slow') + setTimeout(() => { + res.end() + }, 10) + } + + if (p === '/redirect/slow-stream') { + res.statusCode = 301 + res.setHeader('Location', '/slow') + res.end() + } + + if (p === '/redirect/bad-location') { + res.socket.write('HTTP/1.1 301\r\nLocation: ☃\r\nContent-Length: 0\r\n') + res.socket.end('\r\n') + } + + if (p === '/error/400') { + res.statusCode = 400 + res.setHeader('Content-Type', 'text/plain') + res.end('client error') + } + + if (p === '/error/404') { + res.statusCode = 404 + res.setHeader('Content-Encoding', 'gzip') + res.end() + } + + if (p === '/error/500') { + res.statusCode = 500 + res.setHeader('Content-Type', 'text/plain') + res.end('server error') + } + + if (p === '/error/reset') { + res.destroy() + } + + if (p === '/error/premature') { + res.writeHead(200, { 'content-length': 50 }) + res.write('foo') + setTimeout(() => { + res.destroy() + }, 100) + } + + if (p === '/error/premature/chunked') { + res.writeHead(200, { + 'Content-Type': 'application/json', + 'Transfer-Encoding': 'chunked' + }) + + res.write(`${JSON.stringify({ data: 'hi' })}\n`) + + setTimeout(() => { + res.write(`${JSON.stringify({ data: 'bye' })}\n`) + }, 200) + + setTimeout(() => { + res.destroy() + }, 400) + } + + if (p === '/error/json') { + res.statusCode = 200 + res.setHeader('Content-Type', 'application/json') + res.end('invalid json') + } + + if (p === '/no-content') { + res.statusCode = 204 + res.end() + } + + if (p === '/no-content/gzip') { + res.statusCode = 204 + res.setHeader('Content-Encoding', 'gzip') + res.end() + } + + if (p === '/no-content/brotli') { + res.statusCode = 204 + res.setHeader('Content-Encoding', 'br') + res.end() + } + + if (p === '/not-modified') { + res.statusCode = 304 + res.end() + } + + if (p === '/not-modified/gzip') { + res.statusCode = 304 + res.setHeader('Content-Encoding', 'gzip') + res.end() + } + + if (p === '/inspect') { + res.statusCode = 200 + res.setHeader('Content-Type', 'application/json') + let body = '' + request.on('data', c => { + body += c + }) + request.on('end', () => { + res.end(JSON.stringify({ + method: request.method, + url: request.url, + headers: request.headers, + body + })) + }) + } + + if (p === '/multipart') { + res.statusCode = 200 + res.setHeader('Content-Type', 'application/json') + const busboy = new Busboy({ headers: request.headers }) + let body = '' + busboy.on('file', async (fieldName, file, fileName) => { + body += `${fieldName}=${fileName}` + // consume file data + // eslint-disable-next-line no-empty, no-unused-vars + for await (const c of file) {} + }) + + busboy.on('field', (fieldName, value) => { + body += `${fieldName}=${value}` + }) + busboy.on('finish', () => { + res.end(JSON.stringify({ + method: request.method, + url: request.url, + headers: request.headers, + body + })) + }) + request.pipe(busboy) + } + + if (p === '/m%C3%B6bius') { + res.statusCode = 200 + res.setHeader('Content-Type', 'text/plain') + res.end('ok') + } + } +}