summaryrefslogtreecommitdiffstats
path: root/test/node-fetch/main.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--test/node-fetch/main.js1661
1 files changed, 1661 insertions, 0 deletions
diff --git a/test/node-fetch/main.js b/test/node-fetch/main.js
new file mode 100644
index 0000000..358a969
--- /dev/null
+++ b/test/node-fetch/main.js
@@ -0,0 +1,1661 @@
+/* eslint no-unused-expressions: "off" */
+/* globals AbortController */
+
+// Test tools
+const zlib = require('zlib')
+const stream = require('stream')
+const vm = require('vm')
+const chai = require('chai')
+const crypto = require('crypto')
+const chaiPromised = require('chai-as-promised')
+const chaiIterator = require('chai-iterator')
+const chaiString = require('chai-string')
+const delay = require('delay')
+const { Blob } = require('buffer')
+
+const {
+ fetch,
+ Headers,
+ Request,
+ FormData,
+ Response,
+ setGlobalDispatcher,
+ Agent
+} = require('../../index.js')
+const HeadersOrig = require('../../lib/fetch/headers.js').Headers
+const RequestOrig = require('../../lib/fetch/request.js').Request
+const ResponseOrig = require('../../lib/fetch/response.js').Response
+const TestServer = require('./utils/server.js')
+const chaiTimeout = require('./utils/chai-timeout.js')
+const { ReadableStream } = require('stream/web')
+
+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
+
+describe('node-fetch', () => {
+ const local = new TestServer()
+ let base
+
+ before(async () => {
+ await local.start()
+ setGlobalDispatcher(new Agent({
+ connect: {
+ rejectUnauthorized: false
+ }
+ }))
+ 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(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)
+ })
+
+ it('should reject with error if url is relative path', () => {
+ const url = '/some/path'
+ return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError)
+ })
+
+ it('should reject with error if protocol is unsupported', () => {
+ const url = 'ftp://example.com/'
+ return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError)
+ })
+
+ 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(TypeError)
+ })
+
+ 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(ReadableStream)
+ 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('<html></html>')
+ })
+ })
+ })
+
+ 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 send request with custom headers array', () => {
+ 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 send request with multi-valued headers', () => {
+ const url = `${base}inspect`
+ const options = {
+ headers: { 'x-custom-header': ['abc', '123'] }
+ }
+ return fetch(url, options).then(res => {
+ return res.json()
+ }).then(res => {
+ expect(res.headers['x-custom-header']).to.equal('abc,123')
+ })
+ })
+
+ 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 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(TypeError)
+ })
+
+ it('should obey maximum redirect, reject case', () => {
+ const url = `${base}redirect/chain/20`
+ return expect(fetch(url)).to.eventually.be.rejected
+ .and.be.an.instanceOf(TypeError)
+ })
+
+ it('should obey redirect chain, resolve case', () => {
+ const url = `${base}redirect/chain/19`
+ return fetch(url).then(res => {
+ expect(res.url).to.equal(`${base}inspect`)
+ expect(res.status).to.equal(200)
+ })
+ })
+
+ 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(TypeError)
+ })
+
+ 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)
+ })
+ })
+
+ 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 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.rejectedWith(TypeError)
+ })
+
+ 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)
+ })
+ })
+
+ 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 = []
+ for await (const chunk of body) {
+ chunks.push(chunk)
+ }
+
+ return chunks
+ }
+
+ return expect(read(res.body))
+ .to.eventually.be.rejectedWith(Error)
+ })
+ })
+
+ 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)
+ })
+ })
+
+ it('should handle DNS-error response', () => {
+ const url = 'http://domain.invalid'
+ return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError)
+ })
+
+ 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', async () => {
+ const url = `${base}gzip-truncated`
+ const res = await fetch(url)
+ expect(res.headers.get('content-type')).to.equal('text/plain')
+ const result = await res.text()
+ 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')
+ })
+ })
+ })
+
+ xit('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 skip decompression if unsupported codings', () => {
+ const url = `${base}multiunsupported`
+ 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('multiunsupported')
+ })
+ })
+ })
+
+ it('should decompress multiple coding', () => {
+ const url = `${base}multisupported`
+ 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 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
+ })
+ })
+
+ 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
+ })
+ })
+
+ 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')
+ })
+ })
+
+ describe('AbortController', () => {
+ let controller
+
+ beforeEach(() => {
+ controller = new AbortController()
+ })
+
+ 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' })
+ }
+ }
+ )
+ ]
+
+ controller.abort()
+
+ return Promise.all(fetches.map(fetched => expect(fetched)
+ .to.eventually.be.rejected
+ .and.be.an.instanceOf(Error)
+ .and.have.property('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' })
+ }
+ }
+ )
+ ]
+
+ controller.abort()
+
+ return Promise.all(fetches.map(fetched => expect(fetched)
+ .to.eventually.be.rejected
+ .and.be.an.instanceOf(Error)
+ .and.have.property('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.have.property('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 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),
+ expect(fetch(`${base}inspect`, { signal: '' }))
+ .to.be.eventually.rejected
+ .and.be.an.instanceof(TypeError),
+ expect(fetch(`${base}inspect`, { signal: Object.create(null) }))
+ .to.be.eventually.rejected
+ .and.be.an.instanceof(TypeError)
+ ])
+ })
+
+ it('should gracefully handle a null signal', () => {
+ return fetch(`${base}hello`, { signal: null }).then(res => {
+ return expect(res.ok).to.be.true
+ })
+ })
+
+ 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 (BigUint64Array) body', () => {
+ const encoder = new TextEncoder()
+ const url = `${base}inspect`
+ const options = {
+ method: 'POST',
+ body: new BigUint64Array(encoder.encode('0123456789abcdef').buffer)
+ }
+ return fetch(url, options).then(res => res.json()).then(res => {
+ expect(res.method).to.equal('POST')
+ expect(res.body).to.equal('0123456789abcdef')
+ expect(res.headers['transfer-encoding']).to.be.undefined
+ expect(res.headers['content-type']).to.be.undefined
+ expect(res.headers['content-length']).to.equal('16')
+ })
+ })
+
+ 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'),
+ duplex: 'half'
+ }
+ 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 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('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.body).to.equal('a=1')
+ })
+ })
+
+ 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')
+ })
+ })
+
+ 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 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)
+ })
+ )
+ })
+
+ xit('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.mockState(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 (http://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
+ })
+
+ xit('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.mockState(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
+ })
+
+ xit('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.mockState(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
+ })
+
+ xit('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.mockState(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
+ })
+
+ xit('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.mockState(res => {
+ res.end(crypto.randomBytes((2 * 512 * 1024) - 1))
+ })
+ return expect(
+ fetch(url, { highWaterMark: 512 * 1024 }).then(res => res.clone().buffer())
+ ).not.to.timeout
+ })
+
+ xit('should allow get all responses of a header', () => {
+ // TODO: fix test.
+ 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 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('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 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 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 http 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
+ })
+ })
+
+ 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`)
+ })
+
+ it('should allow manual redirect handling', function () {
+ this.timeout(5000)
+ const url = `${base}redirect/302`
+ const options = {
+ redirect: 'manual'
+ }
+ return fetch(url, options).then(res => {
+ expect(res.status).to.equal(302)
+ expect(res.url).to.equal(url)
+ expect(res.type).to.equal('basic')
+ expect(res.headers.get('Location')).to.equal('/inspect')
+ expect(res.ok).to.be.false
+ })
+ })
+})