import axios from '../../../index.js';
import http from 'http';
import https from 'https';
import net from 'net';
import url from 'url';
import zlib from 'zlib';
import stream from 'stream';
import util from 'util';
import assert from 'assert';
import fs from 'fs';
import path from 'path';
import {lookup} from 'dns';
let server, proxy;
import AxiosError from '../../../lib/core/AxiosError.js';
import FormDataLegacy from 'form-data';
import formidable from 'formidable';
import express from 'express';
import multer from 'multer';
import bodyParser from 'body-parser';
const isBlobSupported = typeof Blob !== 'undefined';
import {Throttle} from 'stream-throttle';
import devNull from 'dev-null';
import {AbortController} from 'abortcontroller-polyfill/dist/cjs-ponyfill.js';
import {__setProxy} from "../../../lib/adapters/http.js";
import {FormData as FormDataPolyfill, Blob as BlobPolyfill, File as FilePolyfill} from 'formdata-node';

const FormDataSpecCompliant = typeof FormData !== 'undefined' ? FormData : FormDataPolyfill;
const BlobSpecCompliant = typeof Blob !== 'undefined' ? Blob : BlobPolyfill;
const FileSpecCompliant = typeof File !== 'undefined' ? File : FilePolyfill;

const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

import getStream from 'get-stream';

function setTimeoutAsync(ms) {
  return new Promise(resolve=> setTimeout(resolve, ms));
}

const pipelineAsync = util.promisify(stream.pipeline);
const finishedAsync = util.promisify(stream.finished);
const gzip = util.promisify(zlib.gzip);
const deflate = util.promisify(zlib.deflate);
const deflateRaw = util.promisify(zlib.deflateRaw);
const brotliCompress = util.promisify(zlib.brotliCompress);

function toleranceRange(positive, negative) {
  const p = (1 + 1 / positive);
  const n = (1 / negative);

  return (actualValue, value) => {
    return actualValue - value > 0 ? actualValue < value * p : actualValue > value * n;
  }
}

const nodeVersion = process.versions.node.split('.').map(v => parseInt(v, 10));
const nodeMajorVersion = nodeVersion[0];

var noop = ()=> {};

const LOCAL_SERVER_URL = 'http://localhost:4444';

const SERVER_HANDLER_STREAM_ECHO = (req, res) => req.pipe(res);

function startHTTPServer(handlerOrOptions, options) {

  const {handler, useBuffering = false, rate = undefined, port = 4444, keepAlive = 1000} =
    Object.assign(typeof handlerOrOptions === 'function' ? {
      handler: handlerOrOptions
    } : handlerOrOptions || {}, options);

  return new Promise((resolve, reject) => {
    const server = http.createServer(handler || async function (req, res) {
      try {
        req.headers['content-length'] && res.setHeader('content-length', req.headers['content-length']);

        var dataStream = req;

        if (useBuffering) {
          dataStream = stream.Readable.from(await getStream(req));
        }

        var streams = [dataStream];

        if (rate) {
          streams.push(new Throttle({rate}))
        }

        streams.push(res);

        stream.pipeline(streams, (err) => {
          err && console.log('Server warning: ' + err.message)
        });
      } catch (err){
        console.warn('HTTP server error:', err);
      }

    }).listen(port, function (err) {
      err ? reject(err) : resolve(this);
    });

    server.keepAliveTimeout = keepAlive;
  });
}

const stopHTTPServer = async (server, timeout = 10000) => {
  if (server) {
    if (typeof server.closeAllConnections === 'function') {
      server.closeAllConnections();
    }

    await Promise.race([new Promise(resolve => server.close(resolve)), setTimeoutAsync(timeout)]);
  }
}

const handleFormData = (req) => {
  return new Promise((resolve, reject) => {
    const form = new formidable.IncomingForm();

    form.parse(req, (err, fields, files) => {
      if (err) {
        return reject(err);
      }

      resolve({fields, files});
    });
  });
}

function generateReadableStream(length = 1024 * 1024, chunkSize = 10 * 1024, sleep = 50) {
  return stream.Readable.from(async function* (){
    let dataLength = 0;

    while(dataLength < length) {
      const leftBytes = length - dataLength;

      const chunk = Buffer.alloc(leftBytes > chunkSize? chunkSize : leftBytes);

      dataLength += chunk.length;

      yield chunk;

      if (sleep) {
        await setTimeoutAsync(sleep);
      }
    }
  }());
}

describe('supports http with nodejs', function () {
  afterEach(async function () {
    await Promise.all([stopHTTPServer(server), stopHTTPServer(proxy)]);

    server = null;
    proxy = null;

    delete process.env.http_proxy;
    delete process.env.https_proxy;
    delete process.env.no_proxy;
  });

  it('should throw an error if the timeout property is not parsable as a number', function (done) {

    server = http.createServer(function (req, res) {
      setTimeout(function () {
        res.end();
      }, 1000);
    }).listen(4444, function () {
      var success = false, failure = false;
      var error;

      axios.get('http://localhost:4444/', {
        timeout: { strangeTimeout: 250 }
      }).then(function (res) {
        success = true;
      }).catch(function (err) {
        error = err;
        failure = true;
      });

      setTimeout(function () {
        assert.equal(success, false, 'request should not succeed');
        assert.equal(failure, true, 'request should fail');
        assert.equal(error.code, AxiosError.ERR_BAD_OPTION_VALUE);
        assert.equal(error.message, 'error trying to parse `config.timeout` to int');
        done();
      }, 300);
    });
  });

  it('should parse the timeout property', function (done) {

    server = http.createServer(function (req, res) {
      setTimeout(function () {
        res.end();
      }, 1000);
    }).listen(4444, function () {
      var success = false, failure = false;
      var error;

      axios.get('http://localhost:4444/', {
        timeout: '250'
      }).then(function (res) {
        success = true;
      }).catch(function (err) {
        error = err;
        failure = true;
      });

      setTimeout(function () {
        assert.equal(success, false, 'request should not succeed');
        assert.equal(failure, true, 'request should fail');
        assert.equal(error.code, 'ECONNABORTED');
        assert.equal(error.message, 'timeout of 250ms exceeded');
        done();
      }, 300);
    });
  });

  it('should respect the timeout property', function (done) {

    server = http.createServer(function (req, res) {
      setTimeout(function () {
        res.end();
      }, 1000);
    }).listen(4444, function () {
      var success = false, failure = false;
      var error;

      axios.get('http://localhost:4444/', {
        timeout: 250
      }).then(function (res) {
        success = true;
      }).catch(function (err) {
        error = err;
        failure = true;
      });

      setTimeout(function () {
        assert.equal(success, false, 'request should not succeed');
        assert.equal(failure, true, 'request should fail');
        assert.equal(error.code, 'ECONNABORTED');
        assert.equal(error.message, 'timeout of 250ms exceeded');
        done();
      }, 300);
    });
  });

  it('should respect the timeoutErrorMessage property', function (done) {

    server = http.createServer(function (req, res) {
      setTimeout(function () {
        res.end();
      }, 1000);
    }).listen(4444, function () {
      var success = false, failure = false;
      var error;

      axios.get('http://localhost:4444/', {
        timeout: 250,
        timeoutErrorMessage: 'oops, timeout',
      }).then(function (res) {
        success = true;
      }).catch(function (err) {
        error = err;
        failure = true;
      });

      setTimeout(function () {
        assert.strictEqual(success, false, 'request should not succeed');
        assert.strictEqual(failure, true, 'request should fail');
        assert.strictEqual(error.code, 'ECONNABORTED');
        assert.strictEqual(error.message, 'oops, timeout');
        done();
      }, 300);
    });
  });

  it('should allow passing JSON', function (done) {
    var data = {
      firstName: 'Fred',
      lastName: 'Flintstone',
      emailAddr: 'fred@example.com'
    };

    server = http.createServer(function (req, res) {
      res.setHeader('Content-Type', 'application/json');
      res.end(JSON.stringify(data));
    }).listen(4444, function () {
      axios.get('http://localhost:4444/').then(function (res) {
        assert.deepEqual(res.data, data);
        done();
      }).catch(done);
    });
  });

  it('should allow passing JSON with BOM', function (done) {
    var data = {
      firstName: 'Fred',
      lastName: 'Flintstone',
      emailAddr: 'fred@example.com'
    };

    server = http.createServer(function (req, res) {
      res.setHeader('Content-Type', 'application/json');
      var bomBuffer = Buffer.from([0xEF, 0xBB, 0xBF])
      var jsonBuffer = Buffer.from(JSON.stringify(data));
      res.end(Buffer.concat([bomBuffer, jsonBuffer]));
    }).listen(4444, function () {
      axios.get('http://localhost:4444/').then(function (res) {
        assert.deepEqual(res.data, data);
        done();
      }).catch(done);;
    });
  });

  it('should redirect', function (done) {
    var str = 'test response';

    server = http.createServer(function (req, res) {
      var parsed = url.parse(req.url);

      if (parsed.pathname === '/one') {
        res.setHeader('Location', '/two');
        res.statusCode = 302;
        res.end();
      } else {
        res.end(str);
      }
    }).listen(4444, function () {
      axios.get('http://localhost:4444/one').then(function (res) {
        assert.equal(res.data, str);
        assert.equal(res.request.path, '/two');
        done();
      }).catch(done);
    });
  });

  it('should not redirect', function (done) {
    server = http.createServer(function (req, res) {
      res.setHeader('Location', '/foo');
      res.statusCode = 302;
      res.end();
    }).listen(4444, function () {
      axios.get('http://localhost:4444/', {
        maxRedirects: 0,
        validateStatus: function () {
          return true;
        }
      }).then(function (res) {
        assert.equal(res.status, 302);
        assert.equal(res.headers['location'], '/foo');
        done();
      }).catch(done);
    });
  });

  it('should support max redirects', function (done) {
    var i = 1;
    server = http.createServer(function (req, res) {
      res.setHeader('Location', '/' + i);
      res.statusCode = 302;
      res.end();
      i++;
    }).listen(4444, function () {
      axios.get('http://localhost:4444/', {
        maxRedirects: 3
      }).catch(function (error) {
        assert.equal(error.code, AxiosError.ERR_FR_TOO_MANY_REDIRECTS);
        assert.equal(error.message, 'Maximum number of redirects exceeded');
        done();
      }).catch(done);
    });
  });

  it('should support beforeRedirect', function (done) {
    server = http.createServer(function (req, res) {
      res.setHeader('Location', '/foo');
      res.statusCode = 302;
      res.end();
    }).listen(4444, function () {
      axios.get('http://localhost:4444/', {
        maxRedirects: 3,
        beforeRedirect: function (options, responseDetails) {
          if (options.path === '/foo' && responseDetails.headers.location === '/foo') {
            throw new Error(
              'Provided path is not allowed'
            );
          }
        }
      }).catch(function (error) {
        assert.equal(error.message, 'Redirected request failed: Provided path is not allowed');
        done();
      }).catch(done);
    });
  });

  it('should support beforeRedirect and proxy with redirect', async () => {
    let requestCount = 0;
    let totalRedirectCount = 5;

    server = await startHTTPServer(function (req, res) {
      requestCount += 1;
      if (requestCount <= totalRedirectCount) {
        res.setHeader('Location', 'http://localhost:4444');
        res.writeHead(302);
      }
      res.end();
    }, {port: 4444});

    let proxyUseCount = 0;
    proxy = await startHTTPServer(function (req, res) {
      proxyUseCount += 1;
      const targetUrl = new URL(req.url, 'http://' + req.headers.host);
      const opts = {
        host: targetUrl.hostname,
        port: targetUrl.port,
        path: targetUrl.path,
        method: req.method
      };

      const request = http.get(opts, function (response) {
        res.writeHead(response.statusCode, response.headers);
        stream.pipeline(response, res, () => {});
      });

      request.on('error', (err) => {
        console.warn('request error', err);
        res.statusCode = 500;
        res.end();
      })

    }, {port: 4000});

    let configBeforeRedirectCount = 0;

    await axios.get('http://localhost:4444/', {
      proxy: {
        host: 'localhost',
        port: 4000
      },
      maxRedirects: totalRedirectCount,
      beforeRedirect: function (options) {
        configBeforeRedirectCount += 1;
      }
    }).then(function (res) {
      assert.equal(totalRedirectCount, configBeforeRedirectCount, 'should invoke config.beforeRedirect option on every redirect');
      assert.equal(totalRedirectCount + 1, proxyUseCount, 'should go through proxy on every redirect');
    });
  });

  it('should wrap HTTP errors and keep stack', async function () {
    if (nodeMajorVersion <= 12) {
      this.skip(); // node 12 support for async stack traces appears lacking
      return;
    }

    server = await startHTTPServer((req, res) => {
      res.statusCode = 400;
      res.end();
    });

    return assert.rejects(
      async function findMeInStackTrace() {
        await axios.head('http://localhost:4444/one')
      },
      function (err) {
        assert.equal(err.name, 'AxiosError')
        assert.equal(err.isAxiosError, true)
        const matches = [...err.stack.matchAll(/findMeInStackTrace/g)]
        assert.equal(matches.length, 1, err.stack)
        return true;
      }
    )
  });

  it('should wrap interceptor errors and keep stack', function (done) {
    if (nodeMajorVersion <= 12) {
      this.skip(); // node 12 support for async stack traces appears lacking
      return;
    }
    const axiosInstance = axios.create();
    axiosInstance.interceptors.request.use((res) => {
      throw new Error('from request interceptor')
    });
    server = http.createServer(function (req, res) {
      res.end();
    }).listen(4444, function () {
      void assert.rejects(
        async function findMeInStackTrace() {
          await axiosInstance.get('http://localhost:4444/one')
        },
        function (err) {
          assert.equal(err.name, 'Error')
          assert.equal(err.message, 'from request interceptor')
          const matches = [...err.stack.matchAll(/findMeInStackTrace/g)]
          assert.equal(matches.length, 1, err.stack)
          return true;
        }
      ).then(done).catch(done);
    });
  });

  it('should preserve the HTTP verb on redirect', function (done) {
    server = http.createServer(function (req, res) {
      if (req.method.toLowerCase() !== "head") {
        res.statusCode = 400;
        res.end();
        return;
      }

      var parsed = url.parse(req.url);
      if (parsed.pathname === '/one') {
        res.setHeader('Location', '/two');
        res.statusCode = 302;
        res.end();
      } else {
        res.end();
      }
    }).listen(4444, function () {
      axios.head('http://localhost:4444/one').then(function (res) {
        assert.equal(res.status, 200);
        done();
      }).catch(done);
    });
  });

  describe('compression', async () => {
    it('should support transparent gunzip', function (done) {
      var data = {
        firstName: 'Fred',
        lastName: 'Flintstone',
        emailAddr: 'fred@example.com'
      };

      zlib.gzip(JSON.stringify(data), function (err, zipped) {

        server = http.createServer(function (req, res) {
          res.setHeader('Content-Type', 'application/json');
          res.setHeader('Content-Encoding', 'gzip');
          res.end(zipped);
        }).listen(4444, function () {
          axios.get('http://localhost:4444/').then(function (res) {
            assert.deepEqual(res.data, data);
            done();
          }).catch(done);
        });
      });
    });

    it('should support gunzip error handling', async () => {
      server = await startHTTPServer((req, res) => {
        res.setHeader('Content-Type', 'application/json');
        res.setHeader('Content-Encoding', 'gzip');
        res.end('invalid response');
      });

      await assert.rejects(async () => {
        await axios.get(LOCAL_SERVER_URL);
      })
    });

    it('should support disabling automatic decompression of response data', function(done) {
      var data = 'Test data';

      zlib.gzip(data, function(err, zipped) {
        server = http.createServer(function(req, res) {
          res.setHeader('Content-Type', 'text/html;charset=utf-8');
          res.setHeader('Content-Encoding', 'gzip');
          res.end(zipped);
        }).listen(4444, function() {
          axios.get('http://localhost:4444/', {
            decompress: false,
            responseType: 'arraybuffer'

          }).then(function(res) {
            assert.equal(res.data.toString('base64'), zipped.toString('base64'));
            done();
          }).catch(done);
        });
      });
    });

    describe('algorithms', ()=> {
      const responseBody ='str';

      for (const [typeName, zipped] of Object.entries({
        gzip: gzip(responseBody),
        GZIP: gzip(responseBody),
        compress: gzip(responseBody),
        deflate: deflate(responseBody),
        'deflate-raw': deflateRaw(responseBody),
        br: brotliCompress(responseBody)
      })) {
        const type = typeName.split('-')[0];

        describe(`${typeName} decompression`, async () => {
          it(`should support decompression`, async () => {
            server = await startHTTPServer(async (req, res) => {
              res.setHeader('Content-Encoding', type);
              res.end(await zipped);
            });

            const {data} = await axios.get(LOCAL_SERVER_URL);

            assert.strictEqual(data, responseBody);
          });

          it(`should not fail if response content-length header is missing (${type})`, async () => {
            server = await startHTTPServer(async (req, res) => {
              res.setHeader('Content-Encoding', type);
              res.removeHeader('Content-Length');
              res.end(await zipped);
            });

            const {data} = await axios.get(LOCAL_SERVER_URL);

            assert.strictEqual(data, responseBody);
          });

          it('should not fail with chunked responses (without Content-Length header)', async () => {
            server = await startHTTPServer(async (req, res) => {
              res.setHeader('Content-Encoding', type);
              res.setHeader('Transfer-Encoding', 'chunked');
              res.removeHeader('Content-Length');
              res.write(await zipped);
              res.end();
            });

            const {data} = await axios.get(LOCAL_SERVER_URL);

            assert.strictEqual(data, responseBody);
          });

          it('should not fail with an empty response without content-length header (Z_BUF_ERROR)', async () => {
            server = await startHTTPServer((req, res) => {
              res.setHeader('Content-Encoding', type);
              res.removeHeader('Content-Length');
              res.end();
            });

            const {data} = await axios.get(LOCAL_SERVER_URL);

            assert.strictEqual(data, '');
          });

          it('should not fail with an empty response with content-length header (Z_BUF_ERROR)', async () => {
            server = await startHTTPServer((req, res) => {
              res.setHeader('Content-Encoding', type);
              res.end();
            });

            await axios.get(LOCAL_SERVER_URL);
          });
        });
      }

    });
  });

  it('should support UTF8', function (done) {
    var str = Array(100000).join('ж');

    server = http.createServer(function (req, res) {
      res.setHeader('Content-Type', 'text/html; charset=UTF-8');
      res.end(str);
    }).listen(4444, function () {
      axios.get('http://localhost:4444/').then(function (res) {
        assert.equal(res.data, str);
        done();
      }).catch(done);
    });
  });

  it('should support basic auth', function (done) {
    server = http.createServer(function (req, res) {
      res.end(req.headers.authorization);
    }).listen(4444, function () {
      var user = 'foo';
      var headers = { Authorization: 'Bearer 1234' };
      axios.get('http://' + user + '@localhost:4444/', { headers: headers }).then(function (res) {
        var base64 = Buffer.from(user + ':', 'utf8').toString('base64');
        assert.equal(res.data, 'Basic ' + base64);
        done();
      }).catch(done);
    });
  });

  it('should support basic auth with a header', function (done) {
    server = http.createServer(function (req, res) {
      res.end(req.headers.authorization);
    }).listen(4444, function () {
      var auth = { username: 'foo', password: 'bar' };
      var headers = { AuThOrIzAtIoN: 'Bearer 1234' }; // wonky casing to ensure caseless comparison
      axios.get('http://localhost:4444/', { auth: auth, headers: headers }).then(function (res) {
        var base64 = Buffer.from('foo:bar', 'utf8').toString('base64');
        assert.equal(res.data, 'Basic ' + base64);
        done();
      }).catch(done);
    });
  });

  it('should provides a default User-Agent header', function (done) {
    server = http.createServer(function (req, res) {
      res.end(req.headers['user-agent']);
    }).listen(4444, function () {
      axios.get('http://localhost:4444/').then(function (res) {
        assert.ok(/^axios\/[\d.]+[-]?[a-z]*[.]?[\d]+$/.test(res.data), `User-Agent header does not match: ${res.data}`);
        done();
      }).catch(done);
    });
  });

  it('should allow the User-Agent header to be overridden', function (done) {
    server = http.createServer(function (req, res) {
      res.end(req.headers['user-agent']);
    }).listen(4444, function () {
      var headers = { 'UsEr-AgEnT': 'foo bar' }; // wonky casing to ensure caseless comparison
      axios.get('http://localhost:4444/', { headers }).then(function (res) {
        assert.equal(res.data, 'foo bar');
        done();
      }).catch(done);
    });
  });

  it('should allow the Content-Length header to be overridden', function (done) {
    server = http.createServer(function (req, res) {
      assert.strictEqual(req.headers['content-length'], '42');
      res.end();
    }).listen(4444, function () {
      var headers = { 'CoNtEnT-lEnGtH': '42' }; // wonky casing to ensure caseless comparison
      axios.post('http://localhost:4444/', 'foo', { headers }).then(function () {
        done();
      }).catch(done);
    });
  });

  it('should support max content length', async function () {
    server = await startHTTPServer(function (req, res) {
      res.setHeader('Content-Type', 'text/html; charset=UTF-8');
      res.end(Array(5000).join('#'));
    }, {port: 4444});

    await assert.rejects(() => {
      return axios.get('http://localhost:4444/', {
        maxContentLength: 2000,
        maxRedirects: 0
      })
    },/maxContentLength size of 2000 exceeded/);
  });

  it('should support max content length for redirected', function (done) {
    var str = Array(100000).join('ж');

    server = http.createServer(function (req, res) {
      var parsed = url.parse(req.url);

      if (parsed.pathname === '/two') {
        res.setHeader('Content-Type', 'text/html; charset=UTF-8');
        res.end(str);
      } else {
        res.setHeader('Location', '/two');
        res.statusCode = 302;
        res.end();
      }
    }).listen(4444, function () {
      var success = false, failure = false, error;

      axios.get('http://localhost:4444/one', {
        maxContentLength: 2000,
      }).then(function (res) {
        success = true;
      }).catch(function (err) {
        error = err;
        failure = true;
      });

      setTimeout(function () {
        assert.equal(success, false, 'request should not succeed');
        assert.equal(failure, true, 'request should fail');
        assert.equal(error.message, 'maxContentLength size of 2000 exceeded');
        done();
      }, 100);
    });
  });

  it('should support max body length', function (done) {
    var data = Array(100000).join('ж');

    server = http.createServer(function (req, res) {
      res.setHeader('Content-Type', 'text/html; charset=UTF-8');
      res.end();
    }).listen(4444, function () {
      var success = false, failure = false, error;

      axios.post('http://localhost:4444/', {
        data: data
      }, {
        maxBodyLength: 2000
      }).then(function (res) {
        success = true;
      }).catch(function (err) {
        error = err;
        failure = true;
      });


      setTimeout(function () {
        assert.equal(success, false, 'request should not succeed');
        assert.equal(failure, true, 'request should fail');
        assert.equal(error.message, 'Request body larger than maxBodyLength limit');
        done();
      }, 100);
    });
  });

  it('should properly support default max body length (follow-redirects as well)', function (done) {
    // taken from https://github.com/follow-redirects/follow-redirects/blob/22e81fc37132941fb83939d1dc4c2282b5c69521/index.js#L461
    var followRedirectsMaxBodyDefaults = 10 * 1024 *1024;
    var data = Array(2 * followRedirectsMaxBodyDefaults).join('ж');

    server = http.createServer(function (req, res) {
      // consume the req stream
      req.on('data', noop);
      // and wait for the end before responding, otherwise an ECONNRESET error will be thrown
      req.on('end', ()=> {
        res.end('OK');
      });
    }).listen(4444, function (err) {
      if (err) {
        return done(err);
      }
      // send using the default -1 (unlimited axios maxBodyLength)
      axios.post('http://localhost:4444/', {
        data: data
      }).then(function (res) {
        assert.equal(res.data, 'OK', 'should handle response');
        done();
      }).catch(done);
    });
  });

  it('should display error while parsing params', function (done) {
    server = http.createServer(function () {

    }).listen(4444, function () {
      axios.get('http://localhost:4444/', {
        params: {
          errorParam: new Date(undefined),
        },
      }).catch(function (err) {
        assert.deepEqual(err.exists, true)
        done();
      }).catch(done);
    });
  });

  it('should support sockets', function (done) {
    // Different sockets for win32 vs darwin/linux
    var socketName = './test.sock';

    if (process.platform === 'win32') {
      socketName = '\\\\.\\pipe\\libuv-test';
    }

    server = net.createServer(function (socket) {
      socket.on('data', function () {
        socket.end('HTTP/1.1 200 OK\r\n\r\n');
      });
    }).listen(socketName, function () {
      axios({
        socketPath: socketName,
        url: 'http://localhost:4444/socket'
      })
        .then(function (resp) {
          assert.equal(resp.status, 200);
          assert.equal(resp.statusText, 'OK');
          done();
        }).catch(done);
    });
  });

  describe('streams', function(){
    it('should support streams', function (done) {
      server = http.createServer(function (req, res) {
        req.pipe(res);
      }).listen(4444, function () {
        axios.post('http://localhost:4444/',
          fs.createReadStream(__filename), {
            responseType: 'stream'
          }).then(function (res) {
          var stream = res.data;
          var string = '';
          stream.on('data', function (chunk) {
            string += chunk.toString('utf8');
          });
          stream.on('end', function () {
            assert.equal(string, fs.readFileSync(__filename, 'utf8'));
            done();
          });
        }).catch(done);
      });
    });

    it('should pass errors for a failed stream', async function () {
      server = await startHTTPServer();

      var notExitPath = path.join(__dirname, 'does_not_exist');

      try {
        await axios.post(LOCAL_SERVER_URL,
          fs.createReadStream(notExitPath)
        );
        assert.fail('expected ENOENT error');
      } catch (err) {
        assert.equal(err.message, `ENOENT: no such file or directory, open \'${notExitPath}\'`);
      }
    });

    it('should destroy the response stream with an error on request stream destroying', async function () {
      server = await startHTTPServer();

      let stream = generateReadableStream();

      setTimeout(function () {
        stream.destroy();
      }, 1000);

      const {data} = await axios.post(LOCAL_SERVER_URL, stream, {responseType: 'stream'});

      let streamError;

      data.on('error', function(err) {
        streamError = err;
      });

      try {
        await pipelineAsync(data, devNull());
        assert.fail('stream was not aborted');
      } catch(e) {
        console.log(`pipeline error: ${e}`);
      } finally {
        assert.strictEqual(streamError && streamError.code, 'ERR_CANCELED');
      }
    });
  });

  it('should support buffers', function (done) {
    var buf = Buffer.alloc(1024, 'x'); // Unsafe buffer < Buffer.poolSize (8192 bytes)
    server = http.createServer(function (req, res) {
      assert.equal(req.headers['content-length'], buf.length.toString());
      req.pipe(res);
    }).listen(4444, function () {
      axios.post('http://localhost:4444/',
        buf, {
          responseType: 'stream'
        }).then(function (res) {
          var stream = res.data;
          var string = '';
          stream.on('data', function (chunk) {
            string += chunk.toString('utf8');
          });
          stream.on('end', function () {
            assert.equal(string, buf.toString());
            done();
          });
        }).catch(done);
    });
  });

  it('should support HTTP proxies', function (done) {
    server = http.createServer(function (req, res) {
      res.setHeader('Content-Type', 'text/html; charset=UTF-8');
      res.end('12345');
    }).listen(4444, function () {
      proxy = http.createServer(function (request, response) {
        var parsed = url.parse(request.url);
        var opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: parsed.path
        };

        http.get(opts, function (res) {
          var body = '';
          res.on('data', function (data) {
            body += data;
          });
          res.on('end', function () {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.end(body + '6789');
          });
        });

      }).listen(4000, function () {
        axios.get('http://localhost:4444/', {
          proxy: {
            host: 'localhost',
            port: 4000
          }
        }).then(function (res) {
          assert.equal(res.data, '123456789', 'should pass through proxy');
          done();
        }).catch(done);
      });
    });
  });

  it('should support HTTPS proxies', function (done) {
    var options = {
      key: fs.readFileSync(path.join(__dirname, 'key.pem')),
      cert: fs.readFileSync(path.join(__dirname, 'cert.pem'))
    };

    server = https.createServer(options, function (req, res) {
      res.setHeader('Content-Type', 'text/html; charset=UTF-8');
      res.end('12345');
    }).listen(4444, function () {
      proxy = https.createServer(options, function (request, response) {
        var parsed = url.parse(request.url);
        var opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: parsed.path,
          protocol: parsed.protocol,
          rejectUnauthorized: false
        };

        https.get(opts, function (res) {
          var body = '';
          res.on('data', function (data) {
            body += data;
          });
          res.on('end', function () {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.end(body + '6789');
          });
        });
      }).listen(4000, function () {
        axios.get('https://localhost:4444/', {
          proxy: {
            host: 'localhost',
            port: 4000,
            protocol: 'https:'
          },
          httpsAgent: new https.Agent({
            rejectUnauthorized: false
          })
        }).then(function (res) {
          assert.equal(res.data, '123456789', 'should pass through proxy');
          done();
        }).catch(done);
      });
    });
  });

  it('should not pass through disabled proxy', function (done) {
    // set the env variable
    process.env.http_proxy = 'http://does-not-exists.example.com:4242/';

    server = http.createServer(function (req, res) {
      res.setHeader('Content-Type', 'text/html; charset=UTF-8');
      res.end('123456789');
    }).listen(4444, function () {
      axios.get('http://localhost:4444/', {
        proxy: false
      }).then(function (res) {
        assert.equal(res.data, '123456789', 'should not pass through proxy');
        done();
      }).catch(done);
    });
  });

  it('should support proxy set via env var', function (done) {
    server = http.createServer(function (req, res) {
      res.setHeader('Content-Type', 'text/html; charset=UTF-8');
      res.end('4567');
    }).listen(4444, function () {
      proxy = http.createServer(function (request, response) {
        var parsed = url.parse(request.url);
        var opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: parsed.path
        };

        http.get(opts, function (res) {
          var body = '';
          res.on('data', function (data) {
            body += data;
          });
          res.on('end', function () {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.end(body + '1234');
          });
        });

      }).listen(4000, function () {
        // set the env variable
        process.env.http_proxy = 'http://localhost:4000/';

        axios.get('http://localhost:4444/').then(function (res) {
          assert.equal(res.data, '45671234', 'should use proxy set by process.env.http_proxy');
          done();
        }).catch(done);
      });
    });
  });

  it('should support HTTPS proxy set via env var', function (done) {
    var options = {
      key: fs.readFileSync(path.join(__dirname, 'key.pem')),
      cert: fs.readFileSync(path.join(__dirname, 'cert.pem'))
    };

    server = https.createServer(options, function (req, res) {
      res.setHeader('Content-Type', 'text/html; charset=UTF-8');
      res.end('12345');
    }).listen(4444, function () {
      proxy = https.createServer(options, function (request, response) {
        var parsed = url.parse(request.url);
        var opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: parsed.path,
          protocol: parsed.protocol,
          rejectUnauthorized: false
        };

        https.get(opts, function (res) {
          var body = '';
          res.on('data', function (data) {
            body += data;
          });
          res.on('end', function () {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.end(body + '6789');
          });
        });
      }).listen(4000, function () {
        process.env.https_proxy = 'https://localhost:4000/';

        axios.get('https://localhost:4444/', {
          httpsAgent: new https.Agent({
            rejectUnauthorized: false
          })
        }).then(function (res) {
          assert.equal(res.data, '123456789', 'should pass through proxy');
          done();
        }).catch(done);
      });
    });
  });

  it('should re-evaluate proxy on redirect when proxy set via env var', function (done) {
    process.env.http_proxy = 'http://localhost:4000'
    process.env.no_proxy = 'localhost:4000'

    var proxyUseCount = 0;

    server = http.createServer(function (req, res) {
      res.setHeader('Location', 'http://localhost:4000/redirected');
      res.statusCode = 302;
      res.end();
    }).listen(4444, function () {
      proxy = http.createServer(function (request, response) {
        var parsed = url.parse(request.url);
        if (parsed.pathname === '/redirected') {
          response.statusCode = 200;
          response.end();
          return;
        }

        proxyUseCount += 1;

        var opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: parsed.path,
          protocol: parsed.protocol,
          rejectUnauthorized: false
        };

        http.get(opts, function (res) {
          var body = '';
          res.on('data', function (data) {
            body += data;
          });
          res.on('end', function () {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.setHeader('Location', res.headers.location);
            response.end(body);
          });
        });
      }).listen(4000, function () {
        axios.get('http://localhost:4444/').then(function(res) {
          assert.equal(res.status, 200);
          assert.equal(proxyUseCount, 1);
          done();
        }).catch(done);
      });
    });
  });

  it('should not use proxy for domains in no_proxy', function (done) {
    server = http.createServer(function (req, res) {
      res.setHeader('Content-Type', 'text/html; charset=UTF-8');
      res.end('4567');
    }).listen(4444, function () {
      proxy = http.createServer(function (request, response) {
        var parsed = url.parse(request.url);
        var opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: parsed.path
        };

        http.get(opts, function (res) {
          var body = '';
          res.on('data', function (data) {
            body += data;
          });
          res.on('end', function () {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.end(body + '1234');
          });
        });

      }).listen(4000, function () {
        // set the env variable
        process.env.http_proxy = 'http://localhost:4000/';
        process.env.no_proxy = 'foo.com, localhost,bar.net , , quix.co';

        axios.get('http://localhost:4444/').then(function (res) {
          assert.equal(res.data, '4567', 'should not use proxy for domains in no_proxy');
          done();
        }).catch(done);
      });
    });
  });

  it('should use proxy for domains not in no_proxy', function (done) {
    server = http.createServer(function (req, res) {
      res.setHeader('Content-Type', 'text/html; charset=UTF-8');
      res.end('4567');
    }).listen(4444, function () {
      proxy = http.createServer(function (request, response) {
        var parsed = url.parse(request.url);
        var opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: parsed.path
        };

        http.get(opts, function (res) {
          var body = '';
          res.on('data', function (data) {
            body += data;
          });
          res.on('end', function () {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.end(body + '1234');
          });
        });

      }).listen(4000, function () {
        // set the env variable
        process.env.http_proxy = 'http://localhost:4000/';
        process.env.no_proxy = 'foo.com, ,bar.net , quix.co';

        axios.get('http://localhost:4444/').then(function (res) {
          assert.equal(res.data, '45671234', 'should use proxy for domains not in no_proxy');
          done();
        }).catch(done);
      });
    });
  });

  it('should support HTTP proxy auth', function (done) {
    server = http.createServer(function (req, res) {
      res.end();
    }).listen(4444, function () {
      proxy = http.createServer(function (request, response) {
        var parsed = url.parse(request.url);
        var opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: parsed.path
        };
        var proxyAuth = request.headers['proxy-authorization'];

        http.get(opts, function (res) {
          var body = '';
          res.on('data', function (data) {
            body += data;
          });
          res.on('end', function () {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.end(proxyAuth);
          });
        });

      }).listen(4000, function () {
        axios.get('http://localhost:4444/', {
          proxy: {
            host: 'localhost',
            port: 4000,
            auth: {
              username: 'user',
              password: 'pass'
            }
          }
        }).then(function (res) {
          var base64 = Buffer.from('user:pass', 'utf8').toString('base64');
          assert.equal(res.data, 'Basic ' + base64, 'should authenticate to the proxy');
          done();
        }).catch(done);
      });
    });
  });

  it('should support proxy auth from env', function (done) {
    server = http.createServer(function (req, res) {
      res.end();
    }).listen(4444, function () {
      proxy = http.createServer(function (request, response) {
        var parsed = url.parse(request.url);
        var opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: parsed.path
        };
        var proxyAuth = request.headers['proxy-authorization'];

        http.get(opts, function (res) {
          var body = '';
          res.on('data', function (data) {
            body += data;
          });
          res.on('end', function () {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.end(proxyAuth);
          });
        });

      }).listen(4000, function () {
        process.env.http_proxy = 'http://user:pass@localhost:4000/';

        axios.get('http://localhost:4444/').then(function (res) {
          var base64 = Buffer.from('user:pass', 'utf8').toString('base64');
          assert.equal(res.data, 'Basic ' + base64, 'should authenticate to the proxy set by process.env.http_proxy');
          done();
        }).catch(done);
      });
    });
  });

  it('should support proxy auth with header', function (done) {
    server = http.createServer(function (req, res) {
      res.end();
    }).listen(4444, function () {
      proxy = http.createServer(function (request, response) {
        var parsed = url.parse(request.url);
        var opts = {
          host: parsed.hostname,
          port: parsed.port,
          path: parsed.path
        };
        var proxyAuth = request.headers['proxy-authorization'];

        http.get(opts, function (res) {
          var body = '';
          res.on('data', function (data) {
            body += data;
          });
          res.on('end', function () {
            response.setHeader('Content-Type', 'text/html; charset=UTF-8');
            response.end(proxyAuth);
          });
        });

      }).listen(4000, function () {
        axios.get('http://localhost:4444/', {
          proxy: {
            host: 'localhost',
            port: 4000,
            auth: {
              username: 'user',
              password: 'pass'
            }
          },
          headers: {
            'Proxy-Authorization': 'Basic abc123'
          }
        }).then(function (res) {
          var base64 = Buffer.from('user:pass', 'utf8').toString('base64');
          assert.equal(res.data, 'Basic ' + base64, 'should authenticate to the proxy');
          done();
        }).catch(done);
      });
    });
  });

  context('different options for direct proxy configuration (without env variables)', () => {
    const destination = 'www.example.com';

    const testCases = [{
      description: 'hostname and trailing colon in protocol',
      proxyConfig: { hostname: '127.0.0.1', protocol: 'http:', port: 80 },
      expectedOptions: { host: '127.0.0.1', protocol: 'http:', port: 80, path: destination }
    }, {
      description: 'hostname and no trailing colon in protocol',
      proxyConfig: { hostname: '127.0.0.1', protocol: 'http', port: 80 },
      expectedOptions: { host: '127.0.0.1', protocol: 'http:', port: 80, path: destination }
    }, {
      description: 'both hostname and host -> hostname takes precedence',
      proxyConfig: { hostname: '127.0.0.1', host: '0.0.0.0', protocol: 'http', port: 80 },
      expectedOptions: { host: '127.0.0.1', protocol: 'http:', port: 80, path: destination }
    }, {
      description: 'only host and https protocol',
      proxyConfig: { host: '0.0.0.0', protocol: 'https', port: 80 },
      expectedOptions: { host: '0.0.0.0', protocol: 'https:', port: 80, path: destination }
    }];

    for (const test of testCases) {
      it(test.description, () => {
        const options = { headers: {}, beforeRedirects: {} };
        __setProxy(options, test.proxyConfig, destination);
        for (const [key, expected] of Object.entries(test.expectedOptions)) {
          assert.equal(options[key], expected);
        }
      });
    }
  });

  it('should support cancel', function (done) {
    var source = axios.CancelToken.source();
    server = http.createServer(function (req, res) {
      // call cancel() when the request has been sent, but a response has not been received
      source.cancel('Operation has been canceled.');
    }).listen(4444, function () {
      void assert.rejects(
        async function findMeInStackTrace() {
          await axios.get('http://localhost:4444/', {
            cancelToken: source.token
          });
        },
        function (thrown) {
          assert.ok(thrown instanceof axios.Cancel, 'Promise must be rejected with a CanceledError object');
          assert.equal(thrown.message, 'Operation has been canceled.');
          if (nodeMajorVersion > 12) {
            assert.match(thrown.stack, /findMeInStackTrace/);
          }
          return true;
        },
      ).then(done).catch(done);
    });
  });

  it('should combine baseURL and url', function (done) {
    server = http.createServer(function (req, res) {
      res.end();
    }).listen(4444, function () {
      axios.get('/foo', {
        baseURL: 'http://localhost:4444/',
      }).then(function (res) {
        assert.equal(res.config.baseURL, 'http://localhost:4444/');
        assert.equal(res.config.url, '/foo');
        done();
      }).catch(done);
    });
  });

  it('should support HTTP protocol', function (done) {
    server = http.createServer(function (req, res) {
      setTimeout(function () {
        res.end();
      }, 1000);
    }).listen(4444, function () {
      axios.get('http://localhost:4444')
        .then(function (res) {
          assert.equal(res.request.agent.protocol, 'http:');
          done();
        })
    })
  });

  it('should support HTTPS protocol', function (done) {
    server = http.createServer(function (req, res) {
      setTimeout(function () {
        res.end();
      }, 1000);
    }).listen(4444, function () {
      axios.get('https://www.google.com')
        .then(function (res) {
          assert.equal(res.request.agent.protocol, 'https:');
          done();
        })
    })
  });

  it('should return malformed URL', function (done) {
    var success = false, failure = false;
    var error;

    server = http.createServer(function (req, res) {
      setTimeout(function () {
        res.end();
      }, 1000);
    }).listen(4444, function () {
      axios.get('tel:484-695-3408')
        .then(function (res) {
          success = true;
        }).catch(function (err) {
          error = err;
          failure = true;
        })

      setTimeout(function () {
        assert.equal(success, false, 'request should not succeed');
        assert.equal(failure, true, 'request should fail');
        assert.equal(error.message, 'Unsupported protocol tel:');
        done();
      }, 300);
    })
  });

  it('should return unsupported protocol', function (done) {
    var success = false, failure = false;
    var error;

    server = http.createServer(function (req, res) {
      setTimeout(function () {
        res.end();
      }, 1000);
    }).listen(4444, function () {
      axios.get('ftp:google.com')
        .then(function (res) {
          success = true;
        }).catch(function (err) {
          error = err;
          failure = true;
        })

      setTimeout(function () {
        assert.equal(success, false, 'request should not succeed');
        assert.equal(failure, true, 'request should fail');
        assert.equal(error.message, 'Unsupported protocol ftp:');
        done();
      }, 300);
    })
  });

  it('should supply a user-agent if one is not specified', function (done) {
    server = http.createServer(function (req, res) {
      assert.equal(req.headers["user-agent"], 'axios/' + axios.VERSION);
      res.end();
    }).listen(4444, function () {
      axios.get('http://localhost:4444/'
      ).then(function (res) {
        done();
      }).catch(done);
    });
  });

  it('should omit a user-agent if one is explicitly disclaimed', function (done) {
    server = http.createServer(function (req, res) {
      assert.equal("user-agent" in req.headers, false);
      assert.equal("User-Agent" in req.headers, false);
      res.end();
    }).listen(4444, function () {
      axios.get('http://localhost:4444/', {
        headers: {
          "User-Agent": null
        }
      }).then(function (res) {
        done();
      }).catch(done);
    });
  });

  it('should throw an error if http server that aborts a chunked request', function (done) {
    server = http.createServer(function (req, res) {
      res.writeHead(200, { 'Content-Type': 'text/plain' });
      res.write('chunk 1');
      setTimeout(function () {
        res.write('chunk 2');
      }, 100);
      setTimeout(function() {
        res.destroy();
      }, 200);
    }).listen(4444, function () {
      var success = false, failure = false;
      var error;

      axios.get('http://localhost:4444/aborted', {
        timeout: 500
      }).then(function (res) {
        success = true;
      }).catch(function (err) {
        error = err;
        failure = true;
      }).then(function () {
        assert.strictEqual(success, false, 'request should not succeed');
        assert.strictEqual(failure, true, 'request should fail');
        assert.strictEqual(error.code, 'ERR_BAD_RESPONSE');
        assert.strictEqual(error.message, 'maxContentLength size of -1 exceeded');
        done();
      }).catch(done);
    });
  });

  it('should able to cancel multiple requests with CancelToken', function(done){
    server = http.createServer(function (req, res) {
      res.end('ok');
    }).listen(4444, function () {
      var CancelToken = axios.CancelToken;
      var source = CancelToken.source();
      var canceledStack = [];

      var requests = [1, 2, 3, 4, 5].map(function(id){
        return axios
          .get('/foo/bar', { cancelToken: source.token })
          .catch(function (e) {
            if (!axios.isCancel(e)) {
              throw e;
            }

            canceledStack.push(id);
          });
      });

      source.cancel("Aborted by user");

      Promise.all(requests).then(function () {
        assert.deepStrictEqual(canceledStack.sort(), [1, 2, 3, 4, 5])
      }).then(done, done);
    });
  });

  describe('FormData', function () {
    describe('form-data instance (https://www.npmjs.com/package/form-data)', (done) => {
      it('should allow passing FormData', function (done) {
        var form = new FormDataLegacy();
        var file1 = Buffer.from('foo', 'utf8');
        const image = path.resolve(__dirname, './axios.png');
        const fileStream = fs.createReadStream(image);

        const stat = fs.statSync(image);

        form.append('foo', "bar");
        form.append('file1', file1, {
          filename: 'bar.jpg',
          filepath: 'temp/bar.jpg',
          contentType: 'image/jpeg'
        });

        form.append('fileStream', fileStream);

        server = http.createServer(function (req, res) {
          var receivedForm = new formidable.IncomingForm();

          assert.ok(req.rawHeaders.find(header => header.toLowerCase() === 'content-length'));

          receivedForm.parse(req, function (err, fields, files) {
            if (err) {
              return done(err);
            }

            res.end(JSON.stringify({
              fields: fields,
              files: files
            }));
          });
        }).listen(4444, function () {
          axios.post('http://localhost:4444/', form, {
            headers: {
              'Content-Type': 'multipart/form-data'
            }
          }).then(function (res) {
            assert.deepStrictEqual(res.data.fields, {foo: 'bar'});

            assert.strictEqual(res.data.files.file1.mimetype, 'image/jpeg');
            assert.strictEqual(res.data.files.file1.originalFilename, 'temp/bar.jpg');
            assert.strictEqual(res.data.files.file1.size, 3);

            assert.strictEqual(res.data.files.fileStream.mimetype, 'image/png');
            assert.strictEqual(res.data.files.fileStream.originalFilename, 'axios.png');
            assert.strictEqual(res.data.files.fileStream.size, stat.size);

            done();
          }).catch(done);
        });
      });
    });

    describe('SpecCompliant FormData', (done) => {
      it('should allow passing FormData', async function () {
        server = await startHTTPServer(async (req, res) => {
          const {fields, files} = await handleFormData(req);

          res.end(JSON.stringify({
            fields,
            files
          }));
        });

        const form = new FormDataSpecCompliant();

        const blobContent = 'blob-content';

        const blob = new BlobSpecCompliant([blobContent], {type: 'image/jpeg'})

        form.append('foo1', 'bar1');
        form.append('foo2', 'bar2');

        form.append('file1', blob);

        const {data} = await axios.post(LOCAL_SERVER_URL, form, {
          maxRedirects: 0
        });

        assert.deepStrictEqual(data.fields, {foo1: 'bar1' ,'foo2': 'bar2'});

        assert.deepStrictEqual(typeof data.files.file1, 'object');

        const {size, mimetype, originalFilename} = data.files.file1;

        assert.deepStrictEqual(
          {size, mimetype, originalFilename},
          {mimetype: 'image/jpeg', originalFilename: 'blob', size: Buffer.from(blobContent).byteLength}
        );
      });
    });

    describe('toFormData helper', function () {
      it('should properly serialize nested objects for parsing with multer.js (express.js)', function (done) {
        var app = express();

        var obj = {
          arr1: ['1', '2', '3'],
          arr2: ['1', ['2'], '3'],
          obj: {x: '1', y: {z: '1'}},
          users: [{name: 'Peter', surname: 'griffin'}, {name: 'Thomas', surname: 'Anderson'}]
        };

        app.post('/', multer().none(), function (req, res, next) {
          res.send(JSON.stringify(req.body));
        });

        server = app.listen(3001, function () {
          // multer can parse the following key/value pairs to an array (indexes: null, false, true):
          // arr: '1'
          // arr: '2'
          // -------------
          // arr[]: '1'
          // arr[]: '2'
          // -------------
          // arr[0]: '1'
          // arr[1]: '2'
          // -------------
          Promise.all([null, false, true].map(function (mode) {
            return axios.postForm('http://localhost:3001/', obj, {formSerializer: {indexes: mode}})
              .then(function (res) {
                assert.deepStrictEqual(res.data, obj, 'Index mode ' + mode);
              });
          })).then(function (){
            done();
          }, done)
        });
      });
    });
  });

  describe('Blob', function () {
    it('should support Blob', async () => {
      server = await startHTTPServer(async (req, res) => {
        res.end(await getStream(req));
      });

      const blobContent = 'blob-content';

      const blob = new BlobSpecCompliant([blobContent], {type: 'image/jpeg'})

      const {data} = await axios.post(LOCAL_SERVER_URL, blob, {
        maxRedirects: 0
      });

      assert.deepStrictEqual(data, blobContent);
    });
  });

  describe('URLEncoded Form', function () {
    it('should post object data as url-encoded form if content-type is application/x-www-form-urlencoded', function (done) {
      var app = express();

      var obj = {
        arr1: ['1', '2', '3'],
        arr2: ['1', ['2'], '3'],
        obj: {x: '1', y: {z: '1'}},
        users: [{name: 'Peter', surname: 'griffin'}, {name: 'Thomas', surname: 'Anderson'}]
      };

      app.use(bodyParser.urlencoded({ extended: true }));

      app.post('/', function (req, res, next) {
        res.send(JSON.stringify(req.body));
      });

      server = app.listen(3001, function () {
        return axios.post('http://localhost:3001/', obj, {
          headers: {
            'content-type': 'application/x-www-form-urlencoded'
          }
        })
          .then(function (res) {
            assert.deepStrictEqual(res.data, obj);
            done();
          }).catch(done);
      });
    });
  });

  it('should respect formSerializer config', function (done) {
    const obj = {
      arr1: ['1', '2', '3'],
      arr2: ['1', ['2'], '3'],
    };

    const form = new URLSearchParams();

    form.append('arr1[0]', '1');
    form.append('arr1[1]', '2');
    form.append('arr1[2]', '3');

    form.append('arr2[0]', '1');
    form.append('arr2[1][0]', '2');
    form.append('arr2[2]', '3');

    server = http.createServer(function (req, res) {
      req.pipe(res);
    }).listen(3001, () => {
      return axios.post('http://localhost:3001/', obj, {
        headers: {
          'content-type': 'application/x-www-form-urlencoded'
        },
        formSerializer: {
          indexes: true
        }
      })
        .then(function (res) {
          assert.strictEqual(res.data, form.toString());
          done();
        }).catch(done);
    });
  });

  describe('Data URL', function () {
    it('should support requesting data URL as a Buffer', function (done) {
      const buffer = Buffer.from('123');

      const dataURI = 'data:application/octet-stream;base64,' + buffer.toString('base64');

      axios.get(dataURI).then(({data})=> {
        assert.deepStrictEqual(data, buffer);
        done();
      }).catch(done);
    });

    it('should support requesting data URL as a Blob (if supported by the environment)', function (done) {

      if (!isBlobSupported) {
        this.skip();
        return;
      }

      const buffer = Buffer.from('123');

      const dataURI = 'data:application/octet-stream;base64,' + buffer.toString('base64');

      axios.get(dataURI, {responseType: 'blob'}).then(async ({data})=> {
        assert.strictEqual(data.type, 'application/octet-stream');
        assert.deepStrictEqual(await data.text(), '123');
        done();
      }).catch(done);
    });

    it('should support requesting data URL as a String (text)', function (done) {
      const buffer = Buffer.from('123', 'utf-8');

      const dataURI = 'data:application/octet-stream;base64,' + buffer.toString('base64');

      axios.get(dataURI, {responseType: "text"}).then(({data})=> {
        assert.deepStrictEqual(data, '123');
        done();
      }).catch(done);
    });

    it('should support requesting data URL as a Stream', function (done) {
      const buffer = Buffer.from('123', 'utf-8');

      const dataURI = 'data:application/octet-stream;base64,' + buffer.toString('base64');

      axios.get(dataURI, {responseType: "stream"}).then(({data})=> {
        var str = '';

        data.on('data', function(response){
          str += response.toString();
        });

        data.on('end', function(){
          assert.strictEqual(str, '123');
          done();
        });
      }).catch(done);
    });
  });

  describe('progress', function () {
    describe('upload', function () {
      it('should support upload progress capturing', async function () {
        this.timeout(15000);
        server = await startHTTPServer({
          rate: 100 * 1024
        });

        let content = '';
        const count = 10;
        const chunk = "test";
        const chunkLength = Buffer.byteLength(chunk);
        const contentLength = count * chunkLength;

        var readable = stream.Readable.from(async function* () {
          let i = count;

          while (i-- > 0) {
            await setTimeoutAsync(1100);
            content += chunk;
            yield chunk;
          }
        }());

        const samples = [];

        const {data} = await axios.post(LOCAL_SERVER_URL, readable, {
          onUploadProgress: ({loaded, total, progress, bytes, upload}) => {
            console.log('onUploadProgress', loaded, '/', total);
            samples.push({
              loaded,
              total,
              progress,
              bytes,
              upload
            });
          },
          headers: {
            'Content-Length': contentLength
          },
          responseType: 'text'
        });

        assert.strictEqual(data, content);

        assert.deepStrictEqual(samples, Array.from(function* () {
          for (let i = 1; i <= 10; i++) {
            yield ({
              loaded: chunkLength * i,
              total: contentLength,
              progress: (chunkLength * i) / contentLength,
              bytes: 4,
              upload: true
            });
          }
        }()));
      });
    });

    describe('download', function () {
      it('should support download progress capturing', async function () {
        this.timeout(15000);

        server = await startHTTPServer({
          rate: 100 * 1024
        });

        let content = '';
        const count = 10;
        const chunk = "test";
        const chunkLength = Buffer.byteLength(chunk);
        const contentLength = count * chunkLength;

        var readable = stream.Readable.from(async function* () {
          let i = count;

          while (i-- > 0) {
            await setTimeoutAsync(1100);
            content += chunk;
            yield chunk;
          }
        }());

        const samples = [];

        const {data} = await axios.post(LOCAL_SERVER_URL, readable, {
          onDownloadProgress: ({loaded, total, progress, bytes, download}) => {
            console.log('onDownloadProgress', loaded, '/', total);
            samples.push({
              loaded,
              total,
              progress,
              bytes,
              download
            });
          },
          headers: {
            'Content-Length': contentLength
          },
          responseType: 'text',
          maxRedirects: 0
        });

        assert.strictEqual(data, content);

        assert.deepStrictEqual(samples, Array.from(function* () {
          for (let i = 1; i <= 10; i++) {
            yield ({
              loaded: chunkLength * i,
              total: contentLength,
              progress: (chunkLength * i) / contentLength,
              bytes: 4,
              download: true
            });
          }
        }()));
      });
    });
  });

  describe('Rate limit', function () {
    it('should support upload rate limit', async function () {
      const secs = 10;
      const configRate = 100_000;
      const chunkLength = configRate * secs;

      server = await startHTTPServer();

      const buf = Buffer.alloc(chunkLength).fill('s');
      const samples = [];
      const skip = 2;
      const compareValues = toleranceRange(10, 50);

      const {data} = await axios.post(LOCAL_SERVER_URL, buf, {
        onUploadProgress: ({loaded, total, progress, bytes, rate}) => {
          samples.push({
            loaded,
            total,
            progress,
            bytes,
            rate
          });
        },
        maxRate: [configRate],
        responseType: 'text',
        maxRedirects: 0
      });

      samples.slice(skip).forEach(({rate, progress}, i, _samples)=> {
        assert.ok(compareValues(rate, configRate),
          `Rate sample at index ${i} is out of the expected range (${rate} / ${configRate}) [${
            _samples.map(({rate}) => rate).join(', ')
          }]`
        );

        const progressTicksRate = 2;
        const expectedProgress = ((i + skip) / secs) / progressTicksRate;

        assert.ok(
          Math.abs(expectedProgress - progress) < 0.25,
          `Progress sample at index ${i} is out of the expected range (${progress} / ${expectedProgress}) [${
            _samples.map(({progress}) => progress).join(', ')
          }]`
        );
      });

      assert.strictEqual(data, buf.toString(), 'content corrupted');
    });

    it('should support download rate limit', async function () {
      const secs = 10;
      const configRate = 100_000;
      const chunkLength = configRate * secs;

      server = await startHTTPServer();

      const buf = Buffer.alloc(chunkLength).fill('s');
      const samples = [];
      const skip = 2;
      const compareValues = toleranceRange(10, 50);

      const {data} = await axios.post(LOCAL_SERVER_URL, buf, {
        onDownloadProgress: ({loaded, total, progress, bytes, rate}) => {
          samples.push({
            loaded,
            total,
            progress,
            bytes,
            rate
          });
        },
        maxRate: [0, configRate],
        responseType: 'text',
        maxRedirects: 0
      });

      samples.slice(skip).forEach(({rate, progress}, i, _samples)=> {
        assert.ok(compareValues(rate, configRate),
          `Rate sample at index ${i} is out of the expected range (${rate} / ${configRate}) [${
            _samples.map(({rate}) => rate).join(', ')
          }]`
        );

        const progressTicksRate = 3;
        const expectedProgress = ((i + skip) / secs) / progressTicksRate;

        assert.ok(
          Math.abs(expectedProgress - progress) < 0.25,
          `Progress sample at index ${i} is out of the expected range (${progress} / ${expectedProgress}) [${
            _samples.map(({progress}) => progress).join(', ')
          }]`
        );
      });

      assert.strictEqual(data, buf.toString(), 'content corrupted');
    });
  });

  describe('request aborting', function() {
    it('should be able to abort the response stream', async function () {
      server = await startHTTPServer({
        rate: 100_000,
        useBuffering: true
      });

      const buf = Buffer.alloc(1024 * 1024);

      const controller = new AbortController();

      var {data} = await axios.post(LOCAL_SERVER_URL, buf, {
        responseType: 'stream',
        signal: controller.signal,
        maxRedirects: 0
      });

      setTimeout(() => {
        controller.abort();
      }, 500);

      let streamError;

      data.on('error', function(err) {
        streamError = err;
      });

      try {
        await pipelineAsync(data, devNull());
        assert.fail('stream was not aborted');
      } catch(e) {
        console.log(`pipeline error: ${e}`);
      } finally {
        assert.strictEqual(streamError && streamError.code, 'ERR_CANCELED');
      }
    });
  })

  it('should properly handle synchronous errors inside the adapter', function () {
    return assert.rejects(() => axios.get('http://192.168.0.285'), /Invalid URL/);
  });

  it('should support function as paramsSerializer value', async () => {
    server = await startHTTPServer((req, res) => res.end(req.url));

    const {data} = await axios.post(LOCAL_SERVER_URL, 'test', {
      params: {
        x: 1
      },
      paramsSerializer: () => 'foo',
      maxRedirects: 0
    });

    assert.strictEqual(data, '/?foo');
  });

  describe('DNS', function() {
    it('should support a custom DNS lookup function', async function () {
      server = await startHTTPServer(SERVER_HANDLER_STREAM_ECHO);

      const payload = 'test';

      let isCalled = false;

      const {data} = await axios.post(`http://fake-name.axios:4444`, payload,{
        lookup: (hostname, opt, cb) =>  {
          isCalled = true;
          cb(null, '127.0.0.1', 4);
        }
      });

      assert.ok(isCalled);

      assert.strictEqual(data, payload);
    });

    it('should support a custom DNS lookup function with address entry passing', async function () {
      server = await startHTTPServer(SERVER_HANDLER_STREAM_ECHO);

      const payload = 'test';

      let isCalled = false;

      const {data} = await axios.post(`http://fake-name.axios:4444`, payload,{
        lookup: (hostname, opt, cb) =>  {
          isCalled = true;
          cb(null, {address: '127.0.0.1', family: 4});
        }
      });

      assert.ok(isCalled);

      assert.strictEqual(data, payload);
    });

    it('should support a custom DNS lookup function (async)', async function () {
      server = await startHTTPServer(SERVER_HANDLER_STREAM_ECHO);

      const payload = 'test';

      let isCalled = false;

      const {data} = await axios.post(`http://fake-name.axios:4444`, payload,{
        lookup: async (hostname, opt) =>  {
          isCalled = true;
          return ['127.0.0.1', 4];
        }
      });

      assert.ok(isCalled);

      assert.strictEqual(data, payload);
    });

    it('should support a custom DNS lookup function with address entry (async)', async function () {
      server = await startHTTPServer(SERVER_HANDLER_STREAM_ECHO);

      const payload = 'test';

      let isCalled = false;

      const {data} = await axios.post(`http://fake-name.axios:4444`, payload,{
        lookup: async (hostname, opt) =>  {
          isCalled = true;
          return {address: '127.0.0.1', family: 4};
        }
      });

      assert.ok(isCalled);

      assert.strictEqual(data, payload);
    });

    it('should support a custom DNS lookup function that returns only IP address (async)', async function () {
      server = await startHTTPServer(SERVER_HANDLER_STREAM_ECHO);

      const payload = 'test';

      let isCalled = false;

      const {data} = await axios.post(`http://fake-name.axios:4444`, payload,{
        lookup: async (hostname, opt) =>  {
          isCalled = true;
          return '127.0.0.1';
        }
      });

      assert.ok(isCalled);

      assert.strictEqual(data, payload);
    });

    it('should handle errors', () => {
      return assert.rejects(async () => {
        await axios.get('https://no-such-domain-987654.com', {
          lookup
        });
      }, /ENOTFOUND/);
    });
  });
});
