mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
463 lines
13 KiB
JavaScript
463 lines
13 KiB
JavaScript
'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)
|
|
})
|
|
})
|
|
})
|
|
})
|