'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', () => { it('should enable aws logging when debug log is enabled', () => { const configStub = sinon.stub(); overrideEnv(() => { process.env.SLS_DEBUG = true; proxyquire('../../../../lib/aws/request', { 'aws-sdk': { config: configStub }, }); expect(typeof configStub.logger).to.equal('function'); }); }); 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 () => { process.env.SLS_DEBUG = true; const awsRequest = require('../../../../lib/aws/request'); return expect( awsRequest( { name: 'S3', }, 'putObject', { Bucket: 'test-bucket', Key: 'test-key', } ) ).to.be.rejectedWith( 'AWS provider credentials not found. Learn how to set up AWS provider credentials in our docs here: <\u001b[32mhttp://slss.io/aws-creds-setup\u001b[39m>.' ); }); 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. Learn how to set up AWS provider credentials in our docs here: <\u001b[32mhttp://slss.io/aws-creds-setup\u001b[39m>.' ); }); }); it('should invoke expected AWS SDK methods', async () => { class FakeS3 { putObject() { return { promise: async () => { return { called: true }; }, }; } } const awsRequest = proxyquire('../../../../lib/aws/request', { 'aws-sdk': { 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', { 'aws-sdk': { 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', { 'aws-sdk': { 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: () => Promise.resolve({ region: this.config.region, }), }; } } const awsRequest = proxyquire('../../../../lib/aws/request', { 'aws-sdk': { 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', { 'aws-sdk': { S3: FakeS3 }, 'timers-ext/promise/sleep': () => Promise.resolve(), }); 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', { 'aws-sdk': { S3: FakeS3 }, 'timers-ext/promise/sleep': () => Promise.resolve(), }); const res = await awsRequest({ name: 'S3' }, 'error'); expect(res).to.exist; expect(sendFake.promise).to.have.been.calledTwice; }); it('should not retry if error 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', { 'aws-sdk': { S3: FakeS3 }, }); expect(awsRequest({ name: 'S3' }, 'error')).to.be.rejected; return expect(sendFake.promise).to.have.been.calledOnce; }); it('should expose non-retryable errors', () => { const error = { statusCode: 500, message: 'Some error message', }; class FakeS3 { error() { return { promise: async () => { throw error; }, }; } } const awsRequest = proxyquire('../../../../lib/aws/request', { 'aws-sdk': { S3: FakeS3 }, }); return expect(awsRequest({ name: 'S3' }, 'error')).to.be.rejected; }); }); 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: () => Promise.reject(awsErrorResponse), }; } } const awsRequest = proxyquire('../../../../lib/aws/request', { 'aws-sdk': { 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: () => Promise.reject(awsErrorResponse), }; } } const awsRequest = proxyquire('../../../../lib/aws/request', { 'aws-sdk': { 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: () => Promise.resolve(this), }; } } const awsRequest = proxyquire('../../../../lib/aws/request', { 'aws-sdk': { 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', { 'aws-sdk': { 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', { 'aws-sdk': { 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); }); }); }); });