From b983f623b4c026e2694fcdfe7bb51ea1d7436803 Mon Sep 17 00:00:00 2001 From: Alexandre Germain Date: Fri, 19 Apr 2024 15:01:05 +0200 Subject: [PATCH] feat: add optional error codes to `networkError` Closes #365 --- src/index.js | 130 +++++++++++++++++++------------ test/network_error.spec.js | 154 +++++++++++++++++++++++++++++++------ types/index.d.ts | 6 +- 3 files changed, 214 insertions(+), 76 deletions(-) diff --git a/src/index.js b/src/index.js index da1da79..a89dbff 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ "use strict"; +var axios = require("axios"); var handleRequest = require("./handle_request"); var utils = require("./utils"); @@ -32,6 +33,71 @@ function getVerbObject() { }, {}); } +function throwNetErrorFactory(code) { + if (typeof code === 'string') { + return function (config) { + var url = { hostname: 'UNKNOWN', host: 'UNKNOWN' }; + try { + url = new URL(config.url, config.baseURL); + } catch (error) {} + var error = undefined; + switch (code) { + case 'ENOTFOUND': { + error = Object.assign( + utils.createAxiosError('getaddrinfo ENOTFOUND ' + url.hostname, config, undefined, 'ENOTFOUND'), + { + syscall: 'getaddrinfo', + hostname: url.hostname, + errno: -3008, + } + ); + } break; + + case 'ECONNREFUSED': { + error = Object.assign( + utils.createAxiosError('connect ECONNREFUSED ' + url.host, config, undefined, 'ECONNREFUSED'), + { + syscall: 'connect', + port: url.port ? parseInt(url.port, 10) : undefined, + address: url.hostname, + errno: -111 + } + ); + } break; + + case 'ECONNRESET': { + error = utils.createAxiosError("socket hang up", config, undefined, code); + } break; + + case 'ECONNABORTED': + case 'ETIMEDOUT': { + error = Object.assign( + utils.createAxiosError( + config.timeoutErrorMessage || + "timeout of " + config.timeout + "ms exceeded", + config, + undefined, + config.transitional && config.transitional.clarifyTimeoutError + ? "ETIMEDOUT" + : "ECONNABORTED" + ), + { name: axios.AxiosError.name } + ); + } break; + + default: { + error = utils.createAxiosError("Error " + code, config, undefined, code); + } break; + } + return Promise.reject(error); + }; + } else { + return function (config) { + return Promise.reject(utils.createAxiosError("Network Error", config)); + }; + } +} + function reset() { resetHandlers.call(this); resetHistory.call(this); @@ -131,72 +197,38 @@ VERBS.concat("any").forEach(function (method) { }, abortRequest: function () { + var throwNetError = throwNetErrorFactory('ECONNABORTED'); return reply(function (config) { - var error = utils.createAxiosError( - "Request aborted", - config, - undefined, - "ECONNABORTED" - ); - return Promise.reject(error); + return throwNetError(Object.assign({ timeoutErrorMessage: 'Request aborted' }, config)); }); }, abortRequestOnce: function () { + var throwNetError = throwNetErrorFactory('ECONNABORTED'); return replyOnce(function (config) { - var error = utils.createAxiosError( - "Request aborted", - config, - undefined, - "ECONNABORTED" - ); - return Promise.reject(error); + return throwNetError(Object.assign({ timeoutErrorMessage: 'Request aborted' }, config)); }); }, - networkError: function () { - return reply(function (config) { - var error = utils.createAxiosError("Network Error", config); - return Promise.reject(error); - }); + networkError: function (code) { + var throwNetError = throwNetErrorFactory(code); + return reply(throwNetError); }, - networkErrorOnce: function () { - return replyOnce(function (config) { - var error = utils.createAxiosError("Network Error", config); - return Promise.reject(error); - }); + networkErrorOnce: function (code) { + var throwNetError = throwNetErrorFactory(code); + return replyOnce(throwNetError); }, timeout: function () { - return reply(function (config) { - var error = utils.createAxiosError( - config.timeoutErrorMessage || - "timeout of " + config.timeout + "ms exceeded", - config, - undefined, - config.transitional && config.transitional.clarifyTimeoutError - ? "ETIMEDOUT" - : "ECONNABORTED" - ); - return Promise.reject(error); - }); + var throwNetError = throwNetErrorFactory('ETIMEDOUT'); + return reply(throwNetError); }, timeoutOnce: function () { - return replyOnce(function (config) { - var error = utils.createAxiosError( - config.timeoutErrorMessage || - "timeout of " + config.timeout + "ms exceeded", - config, - undefined, - config.transitional && config.transitional.clarifyTimeoutError - ? "ETIMEDOUT" - : "ECONNABORTED" - ); - return Promise.reject(error); - }); - }, + var throwNetError = throwNetErrorFactory('ETIMEDOUT'); + return replyOnce(throwNetError); + } }; }; }); diff --git a/test/network_error.spec.js b/test/network_error.spec.js index 493fbc0..d40e58f 100644 --- a/test/network_error.spec.js +++ b/test/network_error.spec.js @@ -1,5 +1,6 @@ var axios = require("axios"); var expect = require("chai").expect; +var http = require('http'); var MockAdapter = require("../src"); @@ -12,35 +13,138 @@ describe("networkError spec", function () { mock = new MockAdapter(instance); }); - it("mocks networkErrors", function () { - mock.onGet("/foo").networkError(); - - return instance.get("/foo").then( - function () { - expect.fail("should not be called"); - }, - function (error) { - expect(error.config).to.exist; - expect(error.response).to.not.exist; - expect(error.message).to.equal("Network Error"); - expect(error.isAxiosError).to.be.true; - } - ); + describe("Without code", function() { + it("mocks networkErrors", function () { + mock.onGet("/foo").networkError(); + + return instance.get("/foo").then( + function () { + expect.fail("should not be called"); + }, + function (error) { + expect(error.config).to.exist; + expect(error.response).to.not.exist; + expect(error.message).to.equal("Network Error"); + expect(error.isAxiosError).to.be.true; + } + ); + }); + + it("can mock a network error only once", function () { + mock.onGet("/foo").networkErrorOnce().onGet("/foo").reply(200); + + return instance + .get("/foo") + .then( + function () {}, + function () { + return instance.get("/foo"); + } + ) + .then(function (response) { + expect(response.status).to.equal(200); + }); + }); }); - it("can mock a network error only once", function () { - mock.onGet("/foo").networkErrorOnce().onGet("/foo").reply(200); + describe("With code", function () { + function filterErrorKeys(key) { + return key !== 'config' && key !== 'request' && key !== 'stack'; + } - return instance - .get("/foo") - .then( - function () {}, - function () { - return instance.get("/foo"); + function compareErrors() { + var url = arguments[0]; + var params = Array.from(arguments).slice(1); + return Promise.all([ + axios.get.apply(axios, [instance.defaults.baseURL + url].concat(params)).then(function() { + expect.fail('Should have rejected'); + }, function (error) { + return error; + }), + instance.get.apply(instance, [url].concat(params)).then(function() { + expect.fail('Should have rejected'); + }, function (error) { + return error; + }) + ]).then(function (errors) { + var base = errors[0]; + var mocked = errors[1]; + + var baseKeys = Object.keys(base).filter(filterErrorKeys); + for (var i = 0; i < baseKeys.length; i++) { + var key = baseKeys[i]; + expect(mocked[key], 'Property ' + key).to.equal(base[key]); } - ) - .then(function (response) { - expect(response.status).to.equal(200); }); + } + + it("should look like base axios ENOTFOUND responses", function() { + instance.defaults.baseURL = 'https://not-exi.st:1234'; + mock.onGet("/some-url").networkError('ENOTFOUND'); + + return compareErrors('/some-url'); + }); + + it("should look like base axios ECONNREFUSED responses", function() { + instance.defaults.baseURL = 'http://127.0.0.1:4321'; + mock.onGet("/some-url").networkError('ECONNREFUSED'); + + return compareErrors('/some-url'); + }); + + it("should look like base axios ECONNRESET responses", function() { + return new Promise(function(resolve) { + var server = http.createServer(function(request) { + request.destroy(); + }).listen(function() { + resolve(server); + }); + }).then(function(server) { + instance.defaults.baseURL = 'http://localhost:' + server.address().port; + mock.onGet("/some-url").networkError('ECONNRESET'); + + return compareErrors('/some-url').finally(function() { + server.close(); + }); + }); + }); + + it("should look like base axios ECONNABORTED responses", function() { + return new Promise(function(resolve) { + var server = http.createServer(function() {}).listen(function() { + resolve(server); + }); + }).then(function(server) { + instance.defaults.baseURL = 'http://localhost:' + server.address().port; + mock.onGet("/some-url").networkError('ECONNABORTED'); + + return compareErrors('/some-url', { timeout: 1 }).finally(function() { + server.close(); + }); + }); + }); + + it("should look like base axios ETIMEDOUT responses", function() { + return new Promise(function(resolve) { + var server = http.createServer(function() {}).listen(function() { + resolve(server); + }); + }).then(function(server) { + instance.defaults.baseURL = 'http://localhost:' + server.address().port; + mock.onGet("/some-url").networkError('ETIMEDOUT'); + + return compareErrors('/some-url', { timeout: 1 }).finally(function() { + server.close(); + }); + }); + }); + + // Did not found a way to simulate this + it.skip("should look like base axios EHOSTUNREACH responses", function() { + instance.defaults.baseURL = 'TODO'; + mock.onGet("/some-url").networkError('EHOSTUNREACH'); + + return compareErrors('/some-url'); + }); }); }); diff --git a/types/index.d.ts b/types/index.d.ts index a5afa6b..e03798b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -10,6 +10,8 @@ type ResponseSpecFunc = ( headers?: any ) => MockAdapter; +type NetErr = 'ENOTFOUND' | 'ECONNREFUSED' | 'ECONNRESET' | 'ECONNABORTED' | 'ETIMEDOUT' + declare namespace MockAdapter { export interface RequestHandler { reply: ResponseSpecFunc; @@ -17,8 +19,8 @@ declare namespace MockAdapter { passThrough(): MockAdapter; abortRequest(): MockAdapter; abortRequestOnce(): MockAdapter; - networkError(): MockAdapter; - networkErrorOnce(): MockAdapter; + networkError(code?: NetErr): MockAdapter; + networkErrorOnce(code?: NetErr): MockAdapter; timeout(): MockAdapter; timeoutOnce(): MockAdapter; }