UNPKG

metaapi.cloud-sdk

Version:

SDK for MetaApi, a professional cloud forex API which includes MetaTrader REST API and MetaTrader websocket API. Supports both MetaTrader 5 (MT5) and MetaTrader 4 (MT4). CopyFactory copy trading API included. (https://metaapi.cloud)

317 lines (278 loc) 10.2 kB
'use strict'; import should from 'should'; import sinon from 'sinon'; import HttpClient, {HttpClientMock} from './httpClient'; import {ValidationError, ApiError, InternalError, TooManyRequestsError} from './errorHandler'; /** * @test {HttpClient#request} */ describe('HttpClient#request', () => { let httpClient; /** * @test {HttpClient#request} */ describe('Real request', () => { beforeEach(() => { httpClient = new HttpClient(); }); /** * @test {HttpClient#request} */ it('should load HTML page', async () => { const opts = {url: 'http://google.com'}; const response = await httpClient.request(opts); response.should.match(/doctype html/); }); /** * @test {HttpClient#request} */ it('should return NotFound error if server returns 404', async () => { let opts = {url: 'http://google.com/not-found'}; try { const response = await httpClient.request(opts); should.not.exist(response); } catch (err) { err.name.should.be.eql('NotFoundError'); } }); /** * @test {HttpClient#request} */ it('should return timeout error if request is timed out', async () => { httpClient = new HttpClient(0.001, {retries: 2}); let opts = {url: 'http://metaapi.cloud'}; try { const response = await httpClient.request(opts); should.not.exist(response); } catch (err) { err.name.should.be.eql('ApiError'); err.message.should.be.equalOneOf('ETIMEDOUT' + '. Request URL: ' + opts.url, 'ESOCKETTIMEDOUT'); } }).timeout(10000); }); /** * @test {HttpClient#request} */ describe('Retry request', () => { const opts = {url: 'http://metaapi.cloud'}; let sandbox, stub, clock; before(() => { sandbox = sinon.createSandbox(); }); beforeEach(() => { stub = sandbox.stub(); httpClient = new HttpClientMock(stub); clock = sandbox.useFakeTimers({ shouldAdvanceTime: true, now: new Date('2020-10-05T07:00:00.000Z') }); }); afterEach(() => { sandbox.restore(); clock.restore(); }); /** * @test {HttpClient#request} */ describe('when InternalError or ApiError error occured', () => { /** * @test {HttpClient#request} */ it('should retry request on fail with ApiError error', async () => { stub.onFirstCall().rejects(new ApiError(ApiError, 'test', 0)) .onSecondCall().rejects(new ApiError(ApiError, 'test', 0)) .onThirdCall().resolves({ data: 'response' }); clock.tickAsync(5000); const response = await httpClient.request(opts); response.should.match('response'); sinon.assert.calledThrice(stub); }).timeout(10000); /** * @test {HttpClient#request} */ it('should retry request on fail with InternalError error', async () => { stub.onFirstCall().rejects(new InternalError('test')) .onSecondCall().rejects(new InternalError('test')) .onThirdCall().resolves({ data: 'response'}); clock.tickAsync(5000); const response = await httpClient.request(opts); response.should.match('response'); sinon.assert.calledThrice(stub); }).timeout(10000); /** * @test {HttpClient#request} */ it('should return error if retry limit exceeded', async () => { stub.rejects(new ApiError(ApiError, 'test', 0)); httpClient = new HttpClientMock(stub, 60, {retries: 2}); try { clock.tickAsync(5000); const response = await httpClient.request(opts); should.not.exist(response); } catch (err) { err.name.should.eql('ApiError'); err.message.should.eql('test'); } sinon.assert.calledThrice(stub); }).timeout(10000); /** * @test {HttpClient#request} */ it('should not retry if error is neither InternalError nor ApiError', async () => { stub.onFirstCall().rejects(new ValidationError('test')) .onSecondCall().rejects(new ValidationError('test')) .onThirdCall().resolves('response'); try { clock.tickAsync(5000); const response = await httpClient.request(opts); should.not.exist(response); } catch (err) { err.name.should.eql('ValidationError'); err.message.should.eql('test'); } sinon.assert.calledOnce(stub); }).timeout(10000); }); /** * @test {HttpClient#request} */ describe('when TooManyRequestsError error occured', () => { const getTooManyRequestsError = (sec) => { const date = new Date(); date.setSeconds(date.getSeconds() + sec); const recommendedRetryTime = date.toUTCString(); return new TooManyRequestsError('test', {recommendedRetryTime}); }; /** * @test {HttpClient#request} */ it('should retry request after waiting on fail with TooManyRequestsError error', async () => { stub.onFirstCall().rejects(getTooManyRequestsError(2)) .onSecondCall().rejects(getTooManyRequestsError(3)) .onThirdCall().resolves({ data: 'response' }); clock.tickAsync(5000); const response = await httpClient.request(opts); response.should.eql('response'); sinon.assert.calledThrice(stub); }).timeout(10000); /** * @test {HttpClient#request} */ it('should return error if recommended retry time is too long', async () => { stub.onFirstCall().rejects(getTooManyRequestsError(2)) .onSecondCall().rejects(getTooManyRequestsError(300)) .onThirdCall().resolves('response'); try { clock.tickAsync(5000); const response = await httpClient.request(opts); should.not.exist(response); } catch (err) { err.name.should.eql('TooManyRequestsError'); err.message.should.eql('test'); } sinon.assert.calledTwice(stub); }).timeout(10000); /** * @test {HttpClient#request} */ it('should not count retrying TooManyRequestsError error', async () => { stub.onFirstCall().rejects(getTooManyRequestsError(1)) .onSecondCall().rejects(new ApiError(ApiError, 'test', 0)) .onThirdCall().resolves({ data: 'response' }); httpClient = new HttpClientMock(stub, 60, {retries: 1}); clock.tickAsync(5000); const response = await httpClient.request(opts); response.should.eql('response'); sinon.assert.calledThrice(stub); }).timeout(10000); }); /** * @test {HttpClient#request} */ describe('when status 202 response received', () => { /** * @test {HttpClient#request} */ it('should wait for the retry-after header time before retrying', async () => { stub.callsFake((options) => { options.callback(null, {headers: {'retry-after': 3}, status: 202}); }).onThirdCall().resolves({ data: 'response' }); clock.tickAsync(5000); const response = await httpClient.request(opts); response.should.eql('response'); sinon.assert.calledThrice(stub); }).timeout(10000); /** * @test {HttpClient#request} */ it('should wait for the retry-after header time before retrying if header is in http time', async () => { stub.callsFake((options) => { options.callback(null, {headers: {'retry-after': 'Mon, 05 Oct 2020 07:00:02 GMT'}, status: 202}); }).onThirdCall().resolves({ data: 'response' }); clock.tickAsync(3000); const response = await httpClient.request(opts); response.should.eql('response'); sinon.assert.calledThrice(stub); }).timeout(10000); /** * @test {HttpClient#request} */ it('should return TimeoutError error if retry-after header time is too long', async () => { stub.callsFake((options) => { options.callback(null, { headers: { 'retry-after': 30 }, status: 202 }); }); httpClient = new HttpClientMock(stub, 60, {maxDelayInSeconds: 3, longRunningRequestTimeoutInMinutes: 0.25}); try { await httpClient.request(opts); should.not.exist('Should not exist this assertion'); } catch (err) { err.name.should.eql('TimeoutError'); err.message.should.eql('Timed out waiting for the response'); } sinon.assert.calledOnce(stub); }).timeout(10000); /** * @test {HttpClient#request} */ it('should return TimeoutError error if timed out to retry', async () => { stub.callsFake((options) => { options.callback(null, { headers: {'retry-after': 1}, status: 202 }); }); httpClient = new HttpClientMock(stub, 60, {maxDelayInSeconds: 2, retries: 3, longRunningRequestTimeoutInMinutes: 0.1}); try { clock.tickAsync(5000); await httpClient.request(opts); should.not.exist('Should not exist this assertion'); } catch (err) { err.name.should.eql('TimeoutError'); err.message.should.eql('Timed out waiting for the response'); } sinon.assert.callCount(stub, 6); }).timeout(10000); /** * @test {HttpClient#request} */ it('should wait for the recommendedRetryTime metadata field value before retrying', async () => { stub.callsFake((options) => { options.callback(null, { headers: {}, status: 202, data: {metadata: {recommendedRetryTime: new Date(Date.now() + 3000).toISOString()}} }); }).onThirdCall().resolves({ data: 'response' }); clock.tickAsync(5000); const response = await httpClient.request(opts); response.should.eql('response'); sinon.assert.calledThrice(stub); }).timeout(10000); }); }); });