serverless/test/unit/lib/plugins/aws/lib/monitor-stack.test.js
2024-05-29 11:51:04 -04:00

1056 lines
35 KiB
JavaScript

'use strict'
const chai = require('chai')
const sinon = require('sinon')
const Serverless = require('../../../../../../lib/serverless')
const AwsProvider = require('../../../../../../lib/plugins/aws/provider')
const CLI = require('../../../../../../lib/classes/cli')
const monitorStack = require('../../../../../../lib/plugins/aws/lib/monitor-stack')
chai.use(require('chai-as-promised'))
const { expect } = chai
describe('monitorStack', () => {
const serverless = new Serverless({ commands: [], options: {} })
const awsPlugin = {}
beforeEach(() => {
const options = {
stage: 'dev',
region: 'us-east-1',
}
awsPlugin.serverless = serverless
awsPlugin.provider = new AwsProvider(serverless, options)
awsPlugin.serverless.cli = new CLI(serverless)
awsPlugin.options = options
Object.assign(awsPlugin, monitorStack)
})
describe('#monitorStack()', () => {
it('should skip monitoring if the stack was already created', async () => {
const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request')
return awsPlugin
.monitorStack('update', 'alreadyCreated', { frequency: 10 })
.then(() => {
expect(describeStackEventsStub.callCount).to.be.equal(0)
awsPlugin.provider.request.restore()
})
})
it('should keep monitoring until CREATE_COMPLETE stack status', async () => {
const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request')
const cfDataMock = {
StackId: 'new-service-dev',
}
const updateStartEvent = {
StackEvents: [
{
EventId: '1a2b3c4d',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'CREATE_IN_PROGRESS',
},
],
}
const updateFinishedEvent = {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'CREATE_COMPLETE',
},
],
}
describeStackEventsStub.onCall(0).resolves(updateStartEvent)
describeStackEventsStub.onCall(1).resolves(updateFinishedEvent)
return awsPlugin
.monitorStack('create', cfDataMock, { frequency: 10 })
.then((stackStatus) => {
expect(describeStackEventsStub.callCount).to.be.equal(2)
expect(
describeStackEventsStub.calledWithExactly(
'CloudFormation',
'describeStackEvents',
{
StackName: cfDataMock.StackId,
},
),
).to.be.equal(true)
expect(stackStatus).to.be.equal('CREATE_COMPLETE')
awsPlugin.provider.request.restore()
})
})
it('should keep monitoring until UPDATE_COMPLETE stack status', async () => {
const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request')
const cfDataMock = {
StackId: 'new-service-dev',
}
const updateStartEvent = {
StackEvents: [
{
EventId: '1a2b3c4d',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_IN_PROGRESS',
},
],
}
const updateFinishedEvent = {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_COMPLETE',
},
],
}
describeStackEventsStub.onCall(0).resolves(updateStartEvent)
describeStackEventsStub.onCall(1).resolves(updateFinishedEvent)
return awsPlugin
.monitorStack('update', cfDataMock, { frequency: 10 })
.then((stackStatus) => {
expect(describeStackEventsStub.callCount).to.be.equal(2)
expect(
describeStackEventsStub.calledWithExactly(
'CloudFormation',
'describeStackEvents',
{
StackName: cfDataMock.StackId,
},
),
).to.be.equal(true)
expect(stackStatus).to.be.equal('UPDATE_COMPLETE')
awsPlugin.provider.request.restore()
})
})
it('should keep monitoring until DELETE_COMPLETE stack status', async () => {
const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request')
const cfDataMock = {
StackId: 'new-service-dev',
}
const updateStartEvent = {
StackEvents: [
{
EventId: '1a2b3c4d',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'DELETE_IN_PROGRESS',
},
],
}
const updateFinishedEvent = {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'DELETE_COMPLETE',
},
],
}
describeStackEventsStub.onCall(0).resolves(updateStartEvent)
describeStackEventsStub.onCall(1).resolves(updateFinishedEvent)
return awsPlugin
.monitorStack('delete', cfDataMock, { frequency: 10 })
.then((stackStatus) => {
expect(describeStackEventsStub.callCount).to.be.equal(2)
expect(
describeStackEventsStub.calledWithExactly(
'CloudFormation',
'describeStackEvents',
{
StackName: cfDataMock.StackId,
},
),
).to.be.equal(true)
expect(stackStatus).to.be.equal('DELETE_COMPLETE')
awsPlugin.provider.request.restore()
})
})
it('should not stop monitoring on CREATE_COMPLETE nested stack status', async () => {
const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request')
const cfDataMock = {
StackId: 'new-service-dev',
}
const updateStartEvent = {
StackEvents: [
{
EventId: '1a2b3c4d',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'CREATE_IN_PROGRESS',
},
],
}
const nestedStackEvent = {
StackEvents: [
{
EventId: '1e2f3g4z',
StackName: 'new-service-dev',
LogicalResourceId: 'nested-stack-name',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'CREATE_COMPLETE',
},
],
}
const updateFinishedEvent = {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'CREATE_COMPLETE',
},
],
}
describeStackEventsStub.onCall(0).resolves(updateStartEvent)
describeStackEventsStub.onCall(1).resolves(nestedStackEvent)
describeStackEventsStub.onCall(2).resolves(updateFinishedEvent)
return awsPlugin
.monitorStack('create', cfDataMock, { frequency: 10 })
.then((stackStatus) => {
expect(describeStackEventsStub.callCount).to.be.equal(3)
expect(
describeStackEventsStub.calledWithExactly(
'CloudFormation',
'describeStackEvents',
{
StackName: cfDataMock.StackId,
},
),
).to.be.equal(true)
expect(stackStatus).to.be.equal('CREATE_COMPLETE')
awsPlugin.provider.request.restore()
})
})
it('should not stop monitoring on UPDATE_COMPLETE nested stack status', async () => {
const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request')
const cfDataMock = {
StackId: 'new-service-dev',
}
const updateStartEvent = {
StackEvents: [
{
EventId: '1a2b3c4d',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_IN_PROGRESS',
},
],
}
const nestedStackEvent = {
StackEvents: [
{
EventId: '1e2f3g4z',
StackName: 'new-service-dev',
LogicalResourceId: 'nested-stack-name',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_COMPLETE',
},
],
}
const updateFinishedEvent = {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_COMPLETE',
},
],
}
describeStackEventsStub.onCall(0).resolves(updateStartEvent)
describeStackEventsStub.onCall(1).resolves(nestedStackEvent)
describeStackEventsStub.onCall(2).resolves(updateFinishedEvent)
return awsPlugin
.monitorStack('update', cfDataMock, { frequency: 10 })
.then((stackStatus) => {
expect(describeStackEventsStub.callCount).to.be.equal(3)
expect(
describeStackEventsStub.calledWithExactly(
'CloudFormation',
'describeStackEvents',
{
StackName: cfDataMock.StackId,
},
),
).to.be.equal(true)
expect(stackStatus).to.be.equal('UPDATE_COMPLETE')
awsPlugin.provider.request.restore()
})
})
it('should not stop monitoring on DELETE_COMPLETE nested stack status', async () => {
const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request')
const cfDataMock = {
StackId: 'new-service-dev',
}
const updateStartEvent = {
StackEvents: [
{
EventId: '1a2b3c4d',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'DELETE_IN_PROGRESS',
},
],
}
const nestedStackEvent = {
StackEvents: [
{
EventId: '1e2f3g4z',
StackName: 'new-service-dev',
LogicalResourceId: 'nested-stack-name',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'DELETE_COMPLETE',
},
],
}
const updateFinishedEvent = {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'DELETE_COMPLETE',
},
],
}
describeStackEventsStub.onCall(0).resolves(updateStartEvent)
describeStackEventsStub.onCall(1).resolves(nestedStackEvent)
describeStackEventsStub.onCall(2).resolves(updateFinishedEvent)
return awsPlugin
.monitorStack('delete', cfDataMock, { frequency: 10 })
.then((stackStatus) => {
expect(describeStackEventsStub.callCount).to.be.equal(3)
expect(
describeStackEventsStub.calledWithExactly(
'CloudFormation',
'describeStackEvents',
{
StackName: cfDataMock.StackId,
},
),
).to.be.equal(true)
expect(stackStatus).to.be.equal('DELETE_COMPLETE')
awsPlugin.provider.request.restore()
})
})
it('should keep monitoring until DELETE_COMPLETE or stack not found catch', async () => {
const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request')
const cfDataMock = {
StackId: 'new-service-dev',
}
const updateStartEvent = {
StackEvents: [
{
EventId: '1a2b3c4d',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'DELETE_IN_PROGRESS',
},
],
}
const stackNotFoundError = {
message: 'Stack new-service-dev does not exist',
}
describeStackEventsStub.onCall(0).resolves(updateStartEvent)
describeStackEventsStub.onCall(1).rejects(stackNotFoundError)
return awsPlugin
.monitorStack('delete', cfDataMock, { frequency: 10 })
.then((stackStatus) => {
expect(describeStackEventsStub.callCount).to.be.equal(2)
expect(
describeStackEventsStub.calledWithExactly(
'CloudFormation',
'describeStackEvents',
{
StackName: cfDataMock.StackId,
},
),
).to.be.equal(true)
expect(stackStatus).to.be.equal('DELETE_COMPLETE')
awsPlugin.provider.request.restore()
})
})
it('should output all stack events information with the --verbose option', () => {
awsPlugin.options.verbose = true
const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request')
const cfDataMock = {
StackId: 'new-service-dev',
}
const updateStartEvent = {
StackEvents: [
{
EventId: '1a2b3c4d',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_IN_PROGRESS',
},
],
}
const updateFailedEvent = {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'mochaS3',
ResourceType: 'AWS::S3::Bucket',
Timestamp: new Date(),
ResourceStatus: 'CREATE_FAILED',
ResourceStatusReason: 'Bucket already exists',
},
],
}
const updateRollbackEvent = {
StackEvents: [
{
EventId: '1i2j3k4l',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_ROLLBACK_IN_PROGRESS',
},
],
}
const updateRollbackComplete = {
StackEvents: [
{
EventId: '1m2n3o4p',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'ROLLBACK_COMPLETE',
},
],
}
describeStackEventsStub.onCall(0).resolves(updateStartEvent)
describeStackEventsStub.onCall(1).resolves(updateFailedEvent)
describeStackEventsStub.onCall(2).resolves(updateRollbackEvent)
describeStackEventsStub.onCall(3).resolves(updateRollbackComplete)
return awsPlugin
.monitorStack('update', cfDataMock, { frequency: 10 })
.catch((e) => {
let errorMessage = 'An error occurred: '
errorMessage += 'mochaS3 - Bucket already exists.'
if (e.name !== 'ServerlessError') throw e
expect(e.name).to.be.equal('ServerlessError')
expect(e.message).to.be.equal(errorMessage)
expect(describeStackEventsStub.callCount).to.be.equal(4)
expect(
describeStackEventsStub.calledWithExactly(
'CloudFormation',
'describeStackEvents',
{
StackName: cfDataMock.StackId,
},
),
).to.be.equal(true)
awsPlugin.provider.request.restore()
})
})
it('should keep monitoring when 1st ResourceType is not "AWS::CloudFormation::Stack"', async () => {
const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request')
const cfDataMock = {
StackId: 'new-service-dev',
}
const firstNoStackResourceTypeEvent = {
StackEvents: [
{
EventId: '1a2b3c4d',
StackName: 'new-service-dev',
LogicalResourceId: 'somebucket',
ResourceType: 'AWS::S3::Bucket',
Timestamp: new Date(),
},
],
}
const updateStartEvent = {
StackEvents: [
{
EventId: '1a2b3c4d',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_IN_PROGRESS',
},
],
}
const updateComplete = {
StackEvents: [
{
EventId: '1m2n3o4p',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_COMPLETE',
},
],
}
describeStackEventsStub.onCall(0).resolves(firstNoStackResourceTypeEvent)
describeStackEventsStub.onCall(1).resolves(updateStartEvent)
describeStackEventsStub.onCall(2).resolves(updateComplete)
return awsPlugin
.monitorStack('update', cfDataMock, { frequency: 10 })
.then(() => {
expect(describeStackEventsStub.callCount).to.be.equal(3)
expect(
describeStackEventsStub.calledWithExactly(
'CloudFormation',
'describeStackEvents',
{
StackName: cfDataMock.StackId,
},
),
).to.be.equal(true)
awsPlugin.provider.request.restore()
})
})
it('should catch describeStackEvents error if stack was not in deleting state', () => {
const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request')
const cfDataMock = {
StackId: 'new-service-dev',
}
const failedDescribeStackEvents = {
message: 'Something went wrong.',
}
describeStackEventsStub.onCall(0).rejects(failedDescribeStackEvents)
return awsPlugin
.monitorStack('update', cfDataMock, { frequency: 10 })
.catch((e) => {
expect(e.message).to.be.equal('Something went wrong.')
expect(describeStackEventsStub.callCount).to.be.equal(1)
expect(
describeStackEventsStub.calledWithExactly(
'CloudFormation',
'describeStackEvents',
{
StackName: cfDataMock.StackId,
},
),
).to.be.equal(true)
awsPlugin.provider.request.restore()
})
})
it('should throw an error and exit immediately if stack status is *_FAILED', () => {
const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request')
const cfDataMock = {
StackId: 'new-service-dev',
}
const updateStartEvent = {
StackEvents: [
{
EventId: '1a2b3c4d',
LogicalResourceId: 'mocha',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_IN_PROGRESS',
},
],
}
const updateFailedEvent = {
StackEvents: [
{
EventId: '1e2f3g4h',
LogicalResourceId: 'mochaS3',
ResourceType: 'S3::Bucket',
Timestamp: new Date(),
ResourceStatus: 'CREATE_FAILED',
ResourceStatusReason: 'Bucket already exists',
},
],
}
const updateRollbackEvent = {
StackEvents: [
{
EventId: '1i2j3k4l',
LogicalResourceId: 'mocha',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_ROLLBACK_IN_PROGRESS',
},
],
}
const updateRollbackFailedEvent = {
StackEvents: [
{
EventId: '1m2n3o4p',
LogicalResourceId: 'mocha',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_ROLLBACK_FAILED',
},
],
}
describeStackEventsStub.onCall(0).resolves(updateStartEvent)
describeStackEventsStub.onCall(1).resolves(updateFailedEvent)
describeStackEventsStub.onCall(2).resolves(updateRollbackEvent)
describeStackEventsStub.onCall(3).resolves(updateRollbackFailedEvent)
return awsPlugin
.monitorStack('update', cfDataMock, { frequency: 10 })
.catch((e) => {
let errorMessage = 'An error occurred: '
errorMessage += 'mochaS3 - Bucket already exists.'
expect(e.name).to.be.equal('ServerlessError')
expect(e.message).to.be.equal(errorMessage)
// callCount is 2 because Serverless will immediately exits and shows the error
expect(describeStackEventsStub.callCount).to.be.equal(2)
expect(
describeStackEventsStub.calledWithExactly(
'CloudFormation',
'describeStackEvents',
{
StackName: cfDataMock.StackId,
},
),
).to.be.equal(true)
awsPlugin.provider.request.restore()
})
})
it('should throw an error and exit immediately if stack status is DELETE_FAILED', () => {
const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request')
const cfDataMock = {
StackId: 'new-service-dev',
}
const deleteStartEvent = {
StackEvents: [
{
EventId: '1a2b3c4d',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'DELETE_IN_PROGRESS',
},
],
}
const deleteItemEvent = {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'mochaLambda',
ResourceType: 'AWS::Lambda::Function',
Timestamp: new Date(),
ResourceStatus: 'DELETE_IN_PROGRESS',
},
],
}
const deleteItemFailedEvent = {
StackEvents: [
{
EventId: '1i2j3k4l',
StackName: 'new-service-dev',
LogicalResourceId: 'mochaLambda',
ResourceType: 'AWS::Lambda::Function',
Timestamp: new Date(),
ResourceStatus: 'DELETE_FAILED',
ResourceStatusReason:
'You are not authorized to perform this operation',
},
],
}
const deleteFailedEvent = {
StackEvents: [
{
EventId: '1m2n3o4p',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'DELETE_FAILED',
},
],
}
describeStackEventsStub.onCall(0).resolves(deleteStartEvent)
describeStackEventsStub.onCall(1).resolves(deleteItemEvent)
describeStackEventsStub.onCall(2).resolves(deleteItemFailedEvent)
describeStackEventsStub.onCall(3).resolves(deleteFailedEvent)
return awsPlugin
.monitorStack('delete', cfDataMock, { frequency: 10 })
.catch((e) => {
let errorMessage = 'An error occurred: '
errorMessage +=
'mochaLambda - You are not authorized to perform this operation.'
expect(e.name).to.be.equal('ServerlessError')
expect(e.message).to.be.equal(errorMessage)
// callCount is 2 because Serverless will immediately exits and shows the error
expect(describeStackEventsStub.callCount).to.be.equal(3)
expect(
describeStackEventsStub.calledWithExactly(
'CloudFormation',
'describeStackEvents',
{
StackName: cfDataMock.StackId,
},
),
).to.be.equal(true)
awsPlugin.provider.request.restore()
})
})
it(
'should throw an error if stack status is DELETE_FAILED and should output all ' +
'stack events information with the --verbose option',
() => {
awsPlugin.options.verbose = true
const describeStackEventsStub = sinon.stub(
awsPlugin.provider,
'request',
)
const cfDataMock = {
StackId: 'new-service-dev',
}
const deleteStartEvent = {
StackEvents: [
{
EventId: '1a2b3c4d',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'DELETE_IN_PROGRESS',
},
],
}
const deleteItemEvent = {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'mochaLambda',
ResourceType: 'AWS::Lambda::Function',
Timestamp: new Date(),
ResourceStatus: 'DELETE_IN_PROGRESS',
},
],
}
const deleteItemFailedEvent = {
StackEvents: [
{
EventId: '1i2j3k4l',
StackName: 'new-service-dev',
LogicalResourceId: 'mochaLambda',
ResourceType: 'AWS::Lambda::Function',
Timestamp: new Date(),
ResourceStatus: 'DELETE_FAILED',
ResourceStatusReason:
'You are not authorized to perform this operation',
},
],
}
const deleteFailedEvent = {
StackEvents: [
{
EventId: '1m2n3o4p',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'DELETE_FAILED',
},
],
}
describeStackEventsStub.onCall(0).resolves(deleteStartEvent)
describeStackEventsStub.onCall(1).resolves(deleteItemEvent)
describeStackEventsStub.onCall(2).resolves(deleteItemFailedEvent)
describeStackEventsStub.onCall(3).resolves(deleteFailedEvent)
return awsPlugin
.monitorStack('delete', cfDataMock, { frequency: 10 })
.catch((e) => {
let errorMessage = 'An error occurred: '
errorMessage +=
'mochaLambda - You are not authorized to perform this operation.'
expect(e.name).to.be.equal('ServerlessError')
expect(e.message).to.be.equal(errorMessage)
// callCount is 2 because Serverless will immediately exits and shows the error
expect(describeStackEventsStub.callCount).to.be.equal(4)
expect(
describeStackEventsStub.calledWithExactly(
'CloudFormation',
'describeStackEvents',
{
StackName: cfDataMock.StackId,
},
),
).to.be.equal(true)
awsPlugin.provider.request.restore()
})
},
)
it(
'should throw an error if stack status is DELETE_COMPLETE and should output all ' +
'stack events information with the --verbose option',
async () => {
awsPlugin.options.verbose = true
const describeStackEventsStub = sinon.stub(
awsPlugin.provider,
'request',
)
const cfDataMock = {
StackId: 'new-service-dev',
}
const createStartEvent = {
StackEvents: [
{
EventId: '1a2b3c4d',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'CREATE_IN_PROGRESS',
},
],
}
const createItemFailedEvent = {
StackEvents: [
{
EventId: '1m2n3o4p',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'DELETE_COMPLETE',
},
{
EventId: '1i2j3k4l',
StackName: 'new-service-dev',
LogicalResourceId: 'myBucket',
ResourceType: 'AWS::S3::Bucket',
Timestamp: new Date(),
ResourceStatus: 'DELETE_IN_PROGRESS',
},
{
EventId: '1a2b3c4e',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'DELETE_IN_PROGRESS',
},
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'myBucket',
ResourceType: 'AWS::S3::Bucket',
Timestamp: new Date(),
ResourceStatus: 'CREATE_FAILED',
ResourceStatusReason: 'Invalid Property for X',
},
],
}
describeStackEventsStub.onCall(0).resolves(createStartEvent)
describeStackEventsStub.onCall(1).resolves(createItemFailedEvent)
await expect(
awsPlugin.monitorStack('create', cfDataMock, { frequency: 10 }),
).to.eventually.be.rejectedWith('myBucket - Invalid Property for X.')
awsPlugin.provider.request.restore()
},
)
it('should resolve properly first stack event (when CREATE fails and is followed with DELETE)', async () => {
awsPlugin.options.verbose = true
const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request')
const cfDataMock = {
StackId: 'new-service-dev',
}
const createStartEvent = {
StackEvents: [
{
EventId: '1m2n3o4p',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'DELETE_COMPLETE',
},
{
EventId: '1i2j3k4l',
StackName: 'new-service-dev',
LogicalResourceId: 'myBucket',
ResourceType: 'AWS::S3::Bucket',
Timestamp: new Date(),
ResourceStatus: 'DELETE_IN_PROGRESS',
},
{
EventId: '1a2b3c4e',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'DELETE_IN_PROGRESS',
},
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'myBucket',
ResourceType: 'AWS::S3::Bucket',
Timestamp: new Date(),
ResourceStatus: 'CREATE_FAILED',
ResourceStatusReason: 'Invalid Property for X',
},
{
EventId: '1a2b3c4d',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'CREATE_IN_PROGRESS',
},
],
}
describeStackEventsStub.onCall(0).resolves(createStartEvent)
await expect(
awsPlugin.monitorStack('create', cfDataMock, { frequency: 10 }),
).to.eventually.be.rejectedWith('myBucket - Invalid Property for X.')
awsPlugin.provider.request.restore()
})
it('should record an error and fail if status is UPDATE_ROLLBACK_IN_PROGRESS', () => {
const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request')
const cfDataMock = {
StackId: 'new-service-dev',
}
const updateStartEvent = {
StackEvents: [
{
EventId: '1a2b3c4d',
LogicalResourceId: 'mocha',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_IN_PROGRESS',
},
],
}
const updateRollbackEvent = {
StackEvents: [
{
EventId: '1i2j3k4l',
LogicalResourceId: 'mocha',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_ROLLBACK_IN_PROGRESS',
},
],
}
const updateRollbackCompleteEvent = {
StackEvents: [
{
EventId: '1m2n3o4p',
LogicalResourceId: 'mocha',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'UPDATE_ROLLBACK_COMPLETE',
},
],
}
describeStackEventsStub.onCall(0).resolves(updateStartEvent)
describeStackEventsStub.onCall(1).resolves(updateRollbackEvent)
describeStackEventsStub.onCall(2).resolves(updateRollbackCompleteEvent)
return awsPlugin
.monitorStack('update', cfDataMock, { frequency: 10 })
.catch((e) => {
let errorMessage = 'An error occurred: '
errorMessage += 'mocha - UPDATE_ROLLBACK_IN_PROGRESS.'
expect(e.name).to.be.equal('ServerlessError')
expect(e.message).to.be.equal(errorMessage)
// callCount is 2 because Serverless will immediately exits and shows the error
expect(describeStackEventsStub.callCount).to.be.equal(2)
expect(
describeStackEventsStub.calledWithExactly(
'CloudFormation',
'describeStackEvents',
{
StackName: cfDataMock.StackId,
},
),
).to.be.equal(true)
awsPlugin.provider.request.restore()
})
})
})
})