'use strict' const sinon = require('sinon') const chai = require('chai') const proxyquire = require('proxyquire') const overrideEnv = require('process-utils/override-env') const expect = chai.expect chai.use(require('chai-as-promised')) chai.use(require('sinon-chai')) describe('#request', () => { describe('Credentials support', () => { // awsRequest supports credentials from two sources: // - an AWS credentials object passed as part of params in the call // - environment variable read by the AWS SDK // Ensure we control the process env variable so that no credentials // are available by default let rEnv beforeEach(() => { const { restoreEnv } = overrideEnv() rEnv = restoreEnv }) afterEach(() => { rEnv() }) it('should produce a meaningful error when no supported credentials are provided', async () => { const awsRequest = require('../../../../lib/aws/request') return expect( awsRequest( { name: 'S3', }, 'putObject', { Bucket: 'test-bucket', Key: 'test-key', }, ), ).to.be.eventually.rejected.and.have.property( 'code', 'AWS_CREDENTIALS_NOT_FOUND', ) }) it('should support passing params without credentials', async () => { const awsRequest = require('../../../../lib/aws/request') return expect( awsRequest( { name: 'S3', params: { isS3TransferAccelerationEnabled: true }, }, 'putObject', { Bucket: 'test-bucket', Key: 'test-key', }, ), ).to.be.rejectedWith('AWS provider credentials not found.') }) }) it('should invoke expected AWS SDK methods', async () => { class FakeS3 { putObject() { return { promise: async () => { return { called: true } }, } } } const awsRequest = proxyquire('../../../../lib/aws/request', { './sdk-v2': { S3: FakeS3 }, }) const res = await awsRequest({ name: 'S3' }, 'putObject') expect(res.called).to.equal(true) }) it('should support string for service argument', async () => { class FakeS3 { putObject() { return { promise: async () => { return { called: true } }, } } } const awsRequest = proxyquire('../../../../lib/aws/request', { './sdk-v2': { S3: FakeS3 }, }) const res = await awsRequest('S3', 'putObject', {}) return expect(res.called).to.equal(true) }) it('should handle subclasses', async () => { class DocumentClient { put() { return { promise: () => { return { called: true } }, } } } const awsRequest = proxyquire('../../../../lib/aws/request', { './sdk-v2': { DynamoDB: { DocumentClient } }, }) const res = await awsRequest({ name: 'DynamoDB.DocumentClient' }, 'put', {}) return expect(res.called).to.equal(true) }) it('should request to the specified region if region in options set', async () => { class FakeCloudFormation { constructor(config) { this.config = config } describeStacks() { return { promise: async () => ({ region: this.config.region, }), } } } const awsRequest = proxyquire('../../../../lib/aws/request', { './sdk-v2': { CloudFormation: FakeCloudFormation }, }) const res = await awsRequest( { name: 'CloudFormation', params: { credentials: {}, region: 'ap-northeast-1' }, }, 'describeStacks', { StackName: 'foo' }, ) return expect(res).to.eql({ region: 'ap-northeast-1' }) }) describe('Retries', () => { it('should retry on retryable errors (429)', async () => { const error = { statusCode: 429, retryable: true, message: 'Testing retry', } const sendFake = { promise: sinon.stub(), } sendFake.promise.onCall(0).returns(Promise.reject(error)) sendFake.promise.onCall(1).returns(Promise.resolve({ data: {} })) class FakeS3 { error() { return sendFake } } const awsRequest = proxyquire('../../../../lib/aws/request', { './sdk-v2': { S3: FakeS3 }, 'timers-ext/promise/sleep': async () => {}, }) const res = await awsRequest({ name: 'S3' }, 'error') expect(sendFake.promise).to.have.been.calledTwice expect(res).to.exist }) it('should retry if error code is 429 and retryable is set to false', async () => { const error = { statusCode: 429, retryable: false, message: 'Testing retry', } const sendFake = { promise: sinon.stub(), } sendFake.promise.onCall(0).returns(Promise.reject(error)) sendFake.promise.onCall(1).returns(Promise.resolve({})) class FakeS3 { error() { return sendFake } } const awsRequest = proxyquire('../../../../lib/aws/request', { './sdk-v2': { S3: FakeS3 }, 'timers-ext/promise/sleep': async () => {}, }) const res = await awsRequest({ name: 'S3' }, 'error') expect(res).to.exist expect(sendFake.promise).to.have.been.calledTwice }) it('should not retry if status code is 403 and retryable is set to true', async () => { const error = { providerError: { statusCode: 403, retryable: true, code: 'retry', message: 'Testing retry', }, } const sendFake = { promise: sinon.stub(), } sendFake.promise.onFirstCall().rejects(error) sendFake.promise.onSecondCall().resolves({}) class FakeS3 { error() { return sendFake } } const awsRequest = proxyquire('../../../../lib/aws/request', { './sdk-v2': { S3: FakeS3 }, }) expect(awsRequest({ name: 'S3' }, 'error')).to.be.rejected return expect(sendFake.promise).to.have.been.calledOnce }) it('should not retry if error code is ExpiredTokenException and retryable is set to true', async () => { const error = { providerError: { statusCode: 400, retryable: true, code: 'ExpiredTokenException', message: 'Testing retry', }, } const sendFake = { promise: sinon.stub(), } sendFake.promise.onFirstCall().rejects(error) sendFake.promise.onSecondCall().resolves({}) class FakeS3 { error() { return sendFake } } const awsRequest = proxyquire('../../../../lib/aws/request', { './sdk-v2': { S3: FakeS3 }, }) expect(awsRequest({ name: 'S3' }, 'error')).to.be.rejected return expect(sendFake.promise).to.have.been.calledOnce }) it('should expose non-retryable errors', async () => { const error = { statusCode: 500, message: 'Some error message', code: 'SomeError', } class FakeS3 { test() { return { promise: async () => { throw error }, } } } const awsRequest = proxyquire('../../../../lib/aws/request', { './sdk-v2': { S3: FakeS3 }, }) await expect( awsRequest({ name: 'S3' }, 'test'), ).to.eventually.be.rejected.and.have.property( 'code', 'AWS_S3_TEST_SOME_ERROR', ) }) it('should handle numeric error codes', async () => { const error = { statusCode: 500, message: 'Some error message', code: 500, } class FakeS3 { test() { return { promise: async () => { throw error }, } } } const awsRequest = proxyquire('../../../../lib/aws/request', { './sdk-v2': { S3: FakeS3 }, }) await expect( awsRequest({ name: 'S3' }, 'test'), ).to.eventually.be.rejected.and.have.property( 'code', 'AWS_S3_TEST_HTTP_500_ERROR', ) }) }) it('should expose original error message in thrown error message', () => { const awsErrorResponse = { message: 'Something went wrong...', code: 'Forbidden', region: null, time: '2019-01-24T00:29:01.780Z', requestId: 'DAF12C1111A62C6', extendedRequestId: '1OnSExiLCOsKrsdjjyds31w=', statusCode: 403, retryable: false, retryDelay: 13.433158364430508, } class FakeS3 { error() { return { promise: async () => Promise.reject(awsErrorResponse), } } } const awsRequest = proxyquire('../../../../lib/aws/request', { './sdk-v2': { S3: FakeS3 }, }) return expect(awsRequest({ name: 'S3' }, 'error')).to.be.rejectedWith( awsErrorResponse.message, ) }) it('should default to error code if error message is non-existent', () => { const awsErrorResponse = { message: null, code: 'Forbidden', region: null, time: '2019-01-24T00:29:01.780Z', requestId: 'DAF12C1111A62C6', extendedRequestId: '1OnSExiLCOsKrsdjjyds31w=', statusCode: 403, retryable: false, retryDelay: 13.433158364430508, } class FakeS3 { error() { return { promise: async () => Promise.reject(awsErrorResponse), } } } const awsRequest = proxyquire('../../../../lib/aws/request', { './sdk-v2': { S3: FakeS3 }, }) return expect(awsRequest({ name: 'S3' }, 'error')).to.be.rejectedWith( awsErrorResponse.code, ) }) it('should enable S3 acceleration if "--aws-s3-accelerate" CLI option is provided', async () => { // mocking S3 for testing class FakeS3 { constructor(params) { this.useAccelerateEndpoint = params.useAccelerateEndpoint } putObject() { return { promise: async () => this, } } } const awsRequest = proxyquire('../../../../lib/aws/request', { './sdk-v2': { S3: FakeS3 }, }) const service = await awsRequest( { name: 'S3', params: { isS3TransferAccelerationEnabled: true } }, 'putObject', {}, ) return expect(service.useAccelerateEndpoint).to.be.true }) describe('Caching through memoize', () => { it('should reuse the result if arguments are the same', async () => { // mocking CF for testing const expectedResult = { called: true } const promiseStub = sinon .stub() .returns(Promise.resolve({ called: true })) class FakeCF { describeStacks() { return { promise: promiseStub, } } } const awsRequest = proxyquire('../../../../lib/aws/request', { './sdk-v2': { CloudFormation: FakeCF }, }) const numTests = 100 const executeRequest = () => awsRequest.memoized( { name: 'CloudFormation', params: { credentials: {}, useCache: true }, }, 'describeStacks', {}, ) const requests = [] for (let n = 0; n < numTests; n++) { requests.push(executeRequest()) } return Promise.all(requests).then((results) => { expect(Object.keys(results).length).to.equal(numTests) results.forEach((result) => { expect(result).to.deep.equal(expectedResult) }) expect(promiseStub).to.have.been.calledOnce }) }) it('should not reuse the result if the region change', async () => { const expectedResult = { called: true } const promiseStub = sinon .stub() .returns(Promise.resolve({ called: true })) class FakeCF { constructor(credentials) { this.credentials = credentials } describeStacks() { return { promise: promiseStub, } } } const awsRequest = proxyquire('../../../../lib/aws/request', { './sdk-v2': { CloudFormation: FakeCF }, }) const executeRequestWithRegion = (region) => awsRequest( { name: 'CloudFormation', params: { region, credentials: {}, useCache: true }, }, 'describeStacks', { StackName: 'same-stack' }, ) const requests = [] requests.push(executeRequestWithRegion('us-east-1')) requests.push(executeRequestWithRegion('ap-northeast-1')) return Promise.all(requests).then((results) => { expect(Object.keys(results).length).to.equal(2) results.forEach((result) => { expect(result).to.deep.equal(expectedResult) }) return expect(promiseStub.callCount).to.equal(2) }) }) }) })