@emartech/escher-request
Version:
Requests with Escher authentication
345 lines (344 loc) • 17 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const axios_1 = __importDefault(require("axios"));
const http_1 = __importDefault(require("http"));
const https_1 = __importDefault(require("https"));
const sinon_1 = __importDefault(require("sinon"));
const chai_1 = require("chai");
const wrapper_1 = require("./wrapper");
const requestError_1 = require("./requestError");
const nock_1 = __importDefault(require("nock"));
describe('RequestWrapper', function () {
afterEach(() => {
nock_1.default.cleanAll();
});
describe('functionality tests', () => {
let apiResponse;
let expectedApiResponse;
let extendedRequestOptions;
let expectedRequestOptions;
let cancelToken;
beforeEach(function () {
apiResponse = {
headers: { 'content-type': 'application/json' },
data: JSON.stringify({ data: 1 }),
status: 200,
statusText: 'OK'
};
expectedApiResponse = {
headers: { 'content-type': 'application/json' },
body: { data: 1 },
statusCode: 200,
statusMessage: 'OK'
};
extendedRequestOptions = {
secure: true,
port: 443,
host: 'very.host.io',
rejectUnauthorized: true,
headers: [
['content-type', 'very-format'],
['x-custom', 'alma']
],
prefix: '',
timeout: 15000,
allowEmptyResponse: false,
maxContentLength: 10485760,
keepAlive: false,
credentialScope: '',
method: 'GET',
url: 'http://very.host.io:443/purchases/1/content',
path: '/purchases/1/content'
};
const CancelToken = axios_1.default.CancelToken;
const source = CancelToken.source();
cancelToken = source.token;
expectedRequestOptions = {
method: 'get',
url: 'http://very.host.io:443/purchases/1/content',
data: undefined,
headers: {
'content-type': 'very-format',
'x-custom': 'alma'
},
timeout: 15000,
maxContentLength: 10485760,
cancelToken: cancelToken
};
});
describe('request handling', function () {
let wrapper;
let requestGetStub;
let source;
beforeEach(function () {
const instanceStub = axios_1.default.create();
sinon_1.default.stub(axios_1.default, 'create').returns(instanceStub);
requestGetStub = sinon_1.default.stub(instanceStub, 'request');
requestGetStub.resolves(apiResponse);
source = {
token: cancelToken,
cancel: sinon_1.default.stub()
};
sinon_1.default.stub(axios_1.default.CancelToken, 'source').returns(source);
wrapper = new wrapper_1.RequestWrapper(extendedRequestOptions, 'http:');
});
it('should send GET request and return its response', async () => {
const response = await wrapper.send();
(0, chai_1.expect)(response).to.be.eql(expectedApiResponse);
const requestArgument = requestGetStub.args[0][0];
(0, chai_1.expect)(requestArgument).to.containSubset(expectedRequestOptions);
(0, chai_1.expect)(requestArgument).not.to.have.own.property('httpAgent');
(0, chai_1.expect)(requestArgument).not.to.have.own.property('httpsAgent');
});
it('should pass http agents to axios', async () => {
const agents = {
httpAgent: new http_1.default.Agent({ keepAlive: true }),
httpsAgent: new https_1.default.Agent({ keepAlive: true })
};
wrapper = new wrapper_1.RequestWrapper(Object.assign(agents, extendedRequestOptions), 'http:');
await wrapper.send();
const requestArgument = requestGetStub.args[0][0];
(0, chai_1.expect)(requestArgument.httpAgent).to.eql(agents.httpAgent);
(0, chai_1.expect)(requestArgument.httpsAgent).to.eql(agents.httpsAgent);
});
context('error responses', () => {
it('should return JSON in EscherRequestError if response data is json parsable', async () => {
requestGetStub.restore();
(0, nock_1.default)('http://very.host.io:443')
.get('/purchases/1/content')
.reply(400, { replyText: 'Unknown route' }, { 'Content-Type': 'application/json; charset=utf-8' });
try {
await wrapper.send();
throw new Error('Error should have been thrown');
}
catch (err) {
const error = err;
(0, chai_1.expect)(error).to.be.an.instanceof(requestError_1.EscherRequestError);
(0, chai_1.expect)(error.message).to.eql('Error in http response (status: 400)');
(0, chai_1.expect)(error.code).to.eql(400);
(0, chai_1.expect)(error.data).to.eql({ replyText: 'Unknown route' });
}
});
it('should return text and not fail parsing response data if wrong content-type headers are set', async () => {
requestGetStub.restore();
(0, nock_1.default)('http://very.host.io:443')
.get('/purchases/1/content')
.reply(500, 'Unexpected Error', { 'Content-Type': 'application/json; charset=utf-8' });
try {
await wrapper.send();
throw new Error('Error should have been thrown');
}
catch (err) {
const error = err;
(0, chai_1.expect)(error).to.be.an.instanceof(requestError_1.EscherRequestError);
(0, chai_1.expect)(error.message).to.eql('Error in http response (status: 500)');
(0, chai_1.expect)(error.code).to.eql(500);
(0, chai_1.expect)(error.data).to.eql('Unexpected Error');
}
});
});
describe('when empty response is allowed', function () {
beforeEach(function () {
extendedRequestOptions.allowEmptyResponse = true;
wrapper = new wrapper_1.RequestWrapper(extendedRequestOptions, 'http:');
});
it('should allow body to be empty', async () => {
apiResponse.headers['content-type'] = 'text/html';
apiResponse.data = '';
apiResponse.status = 204;
const response = await wrapper.send();
(0, chai_1.expect)(response.statusCode).to.eql(204);
});
it('should throw error if json is not parsable (empty)', async () => {
apiResponse.data = '';
try {
await wrapper.send();
}
catch (err) {
const error = err;
(0, chai_1.expect)(error).to.be.an.instanceof(requestError_1.EscherRequestError);
(0, chai_1.expect)(error.message).to.match(/Unexpected end/);
(0, chai_1.expect)(error.code).to.eql(500);
return;
}
throw new Error('Error should have been thrown');
});
});
describe('when empty response is not allowed', function () {
it('should throw error if response body is empty', async () => {
apiResponse.data = '';
try {
await wrapper.send();
}
catch (err) {
const error = err;
(0, chai_1.expect)(error).to.be.an.instanceof(requestError_1.EscherRequestError);
(0, chai_1.expect)(error.message).to.eql('Empty http response');
(0, chai_1.expect)(error.code).to.eql(500);
(0, chai_1.expect)(error.data).to.eql(expectedApiResponse.statusMessage);
return;
}
throw new Error('Error should have been thrown');
});
it('should throw error with status message if response body is empty and status message exists', async () => {
apiResponse.data = '';
apiResponse.statusText = 'dummy status message';
try {
await wrapper.send();
}
catch (err) {
const error = err;
(0, chai_1.expect)(error.data).to.eql(apiResponse.statusText);
return;
}
throw new Error('Error should have been thrown');
});
it('should throw a http response error even if the response body is empty', async () => {
requestGetStub.restore();
(0, nock_1.default)('http://very.host.io:443')
.get('/purchases/1/content')
.reply(404, { replyText: '404 Not Found' });
try {
await wrapper.send();
throw new Error('should throw');
}
catch (err) {
const error = err;
(0, chai_1.expect)(error).to.be.an.instanceOf(requestError_1.EscherRequestError);
(0, chai_1.expect)(error.code).to.eql(404);
(0, chai_1.expect)(error.message).to.eql('Error in http response (status: 404)');
(0, chai_1.expect)(error.data).to.eql({ replyText: '404 Not Found' });
}
});
});
describe('when there was an axios error', function () {
let isCancel;
let axiosError;
beforeEach(function () {
axiosError = {
message: 'axios error message',
stack: []
};
isCancel = sinon_1.default.stub(axios_1.default, 'isCancel');
requestGetStub.rejects(axiosError);
});
context('when the request has not been canceled', function () {
beforeEach(function () {
isCancel.returns(false);
});
it('cancels the request', async () => {
try {
await wrapper.send();
throw new Error('should throw SuiteRequestError');
}
catch {
(0, chai_1.expect)(source.cancel).to.have.been.calledWith();
}
});
});
context('when the request has already been canceled', function () {
beforeEach(function () {
isCancel.returns(true);
});
it('does not cancel the request', async () => {
try {
await wrapper.send();
throw new Error('should throw SuiteRequestError');
}
catch {
(0, chai_1.expect)(source.cancel).not.to.have.been.calledWith();
}
});
});
it('should pass original error code to SuiteRequestError', async () => {
try {
axiosError.code = 'ECONNABORTED';
await wrapper.send();
throw new Error('should throw SuiteRequestError');
}
catch (err) {
const error = err;
(0, chai_1.expect)(error.originalCode).to.eql('ECONNABORTED');
(0, chai_1.expect)(error.code).to.eql(503);
(0, chai_1.expect)(error.data).to.eql({ replyText: 'axios error message' });
}
});
});
it('should throw error if json is not parsable (malformed)', async () => {
apiResponse.data = 'this is an invalid json';
try {
await wrapper.send();
}
catch (err) {
const error = err;
(0, chai_1.expect)(error).to.be.an.instanceof(requestError_1.EscherRequestError);
(0, chai_1.expect)(error.message).to.match(/Unexpected token/);
(0, chai_1.expect)(error.code).to.eql(500);
return;
}
throw new Error('Error should have been thrown');
});
it('should parse JSON if content-type header contains charset too', async () => {
const testJson = { text: 'Test JSON text' };
apiResponse.headers['content-type'] = 'application/json; charset=utf-8';
apiResponse.data = JSON.stringify(testJson);
const response = await wrapper.send();
(0, chai_1.expect)(response.body).to.eql(testJson);
});
it('should send GET request with given timeout in options', async () => {
extendedRequestOptions.timeout = 60000;
await (new wrapper_1.RequestWrapper(extendedRequestOptions, 'http:')).send();
const requestArgument = requestGetStub.args[0][0];
(0, chai_1.expect)(requestArgument.timeout).to.eql(extendedRequestOptions.timeout);
});
});
});
describe('retry test', () => {
const requestOptions = {
secure: true,
port: 443,
host: 'very.host.io',
method: 'get',
url: 'http://very.host.io:443/purchases/1/content',
path: '/purchases/1/content'
};
it('should not retry if error code is below 500', async () => {
(0, nock_1.default)('http://very.host.io:443')
.get('/purchases/1/content').times(1)
.reply(404, { replyText: '404 Not Found' })
.get('/purchases/1/content')
.reply(200, { data: 1 }, { 'content-type': 'application/json' });
const retryConfig = { retries: 1 };
const wrapper = new wrapper_1.RequestWrapper({ ...requestOptions, retryConfig }, 'http:', undefined);
try {
await wrapper.send();
}
catch (err) {
const error = err;
(0, chai_1.expect)(error).to.be.an.instanceOf(requestError_1.EscherRequestError);
(0, chai_1.expect)(error.code).to.eql(404);
(0, chai_1.expect)(error.message).to.eql('Error in http response (status: 404)');
(0, chai_1.expect)(error.data).to.eql({ replyText: '404 Not Found' });
}
});
it('should send the request with the correct retry', async () => {
(0, nock_1.default)('http://very.host.io:443')
.get('/purchases/1/content').times(1)
.reply(500)
.get('/purchases/1/content')
.reply(200, { data: 1 }, { 'content-type': 'application/json' });
const expectedApiResponse = {
headers: { 'content-type': 'application/json' },
body: { data: 1 },
statusCode: 200
};
const retryConfig = { retries: 1 };
const wrapper = new wrapper_1.RequestWrapper({ ...requestOptions, retryConfig }, 'http:', undefined);
const response = await wrapper.send();
(0, chai_1.expect)(response).to.containSubset(expectedApiResponse);
});
});
});