'use strict' const chai = require('chai') const sinon = require('sinon') const runServerless = require('../../../../../utils/run-serverless') chai.use(require('chai-as-promised')) chai.use(require('sinon-chai')) const expect = require('chai').expect describe('test/unit/lib/plugins/aws/remove/index.test.js', () => { const deleteObjectsStub = sinon.stub().resolves() const deleteStackStub = sinon.stub().resolves() const describeStackEventsStub = sinon.stub().resolves({ StackEvents: [ { EventId: '1e2f3g4h', StackName: 'new-service-dev', LogicalResourceId: 'new-service-dev', ResourceType: 'AWS::CloudFormation::Stack', Timestamp: new Date(), ResourceStatus: 'DELETE_COMPLETE', }, ], }) const describeRepositoriesStub = sinon.stub() const deleteRepositoryStub = sinon.stub().resolves() const awsRequestStubMap = { ECR: { deleteRepository: deleteRepositoryStub, describeRepositories: describeRepositoriesStub, }, S3: { deleteObjects: deleteObjectsStub, listObjectsV2: { Contents: [{ Key: 'first' }, { Key: 'second' }] }, headBucket: {}, }, CloudFormation: { describeStackEvents: describeStackEventsStub, deleteStack: deleteStackStub, describeStackResource: { StackResourceDetail: { PhysicalResourceId: 'resource-id' }, }, }, STS: { getCallerIdentity: { ResponseMetadata: { RequestId: 'ffffffff-ffff-ffff-ffff-ffffffffffff' }, UserId: 'XXXXXXXXXXXXXXXXXXXXX', Account: '999999999999', Arn: 'arn:aws:iam::999999999999:user/test', }, }, } beforeEach(() => { deleteObjectsStub.resetHistory() deleteStackStub.resetHistory() describeStackEventsStub.resetHistory() describeRepositoriesStub.reset() deleteRepositoryStub.resetHistory() }) it('executes expected operations during removal when repository does not exist', async () => { describeRepositoriesStub.throws({ providerError: { code: 'RepositoryNotFoundException' }, }) const { awsNaming } = await runServerless({ fixture: 'function', command: 'remove', awsRequestStubMap, }) expect(deleteObjectsStub).to.be.calledWithExactly({ Bucket: 'resource-id', Delete: { Objects: [{ Key: 'first' }, { Key: 'second' }], }, }) expect(deleteStackStub).to.be.calledWithExactly({ StackName: awsNaming.getStackName(), }) expect(describeStackEventsStub).to.be.calledWithExactly({ StackName: awsNaming.getStackName(), }) expect(deleteStackStub.calledAfter(deleteObjectsStub)).to.be.true expect(describeStackEventsStub.calledAfter(deleteStackStub)).to.be.true expect(deleteRepositoryStub).not.to.be.called }) it('executes expected operations during removal when repository cannot be accessed due to denied access', async () => { describeRepositoriesStub.throws({ providerError: { code: 'AccessDeniedException' }, }) const { awsNaming } = await runServerless({ fixture: 'function', command: 'remove', awsRequestStubMap, }) expect(deleteObjectsStub).to.be.calledWithExactly({ Bucket: 'resource-id', Delete: { Objects: [{ Key: 'first' }, { Key: 'second' }], }, }) expect(deleteStackStub).to.be.calledWithExactly({ StackName: awsNaming.getStackName(), }) expect(describeStackEventsStub).to.be.calledWithExactly({ StackName: awsNaming.getStackName(), }) expect(deleteStackStub.calledAfter(deleteObjectsStub)).to.be.true expect(describeStackEventsStub.calledAfter(deleteStackStub)).to.be.true expect(deleteRepositoryStub).not.to.be.called }) it('executes expected operations related to files removal when S3 bucket has files', async () => { await runServerless({ fixture: 'function', command: 'remove', awsRequestStubMap: { ...awsRequestStubMap, S3: { deleteObjects: deleteObjectsStub, listObjectsV2: { Contents: [] }, headBucket: {}, }, }, }) expect(deleteObjectsStub).not.to.be.called }) it('executes expected operations related to files removal when S3 bucket is empty', async () => { await runServerless({ fixture: 'function', command: 'remove', awsRequestStubMap, }) expect(deleteObjectsStub).to.be.calledWithExactly({ Bucket: 'resource-id', Delete: { Objects: [{ Key: 'first' }, { Key: 'second' }], }, }) }) it('skips attempts to remove S3 objects if S3 bucket not found', async () => { const { awsNaming } = await runServerless({ fixture: 'function', command: 'remove', awsRequestStubMap: { ...awsRequestStubMap, S3: { deleteObjects: deleteObjectsStub, listObjectsV2: { Contents: [{ Key: 'first' }, { Key: 'second' }] }, headBucket: () => { const err = new Error('err') err.code = 'AWS_S3_HEAD_BUCKET_NOT_FOUND' throw err }, }, }, }) expect(deleteObjectsStub).not.to.be.called expect(deleteStackStub).to.be.calledWithExactly({ StackName: awsNaming.getStackName(), }) expect(describeStackEventsStub).to.be.calledWithExactly({ StackName: awsNaming.getStackName(), }) expect(describeStackEventsStub.calledAfter(deleteStackStub)).to.be.true }) it('skips attempts to remove S3 objects if S3 bucket resource missing from CloudFormation template', async () => { const headBucketStub = sinon.stub() const { awsNaming } = await runServerless({ fixture: 'function', command: 'remove', awsRequestStubMap: { ...awsRequestStubMap, S3: { ...awsRequestStubMap.S3, headBucket: headBucketStub, }, CloudFormation: { ...awsRequestStubMap.CloudFormation, describeStackResource: () => { const err = new Error('does not exist for stack') err.providerError = { code: 'ValidationError', } throw err }, }, }, }) expect(headBucketStub).not.to.be.called expect(deleteObjectsStub).not.to.be.called expect(deleteStackStub).to.be.calledWithExactly({ StackName: awsNaming.getStackName(), }) expect(describeStackEventsStub).to.be.calledWithExactly({ StackName: awsNaming.getStackName(), }) expect(describeStackEventsStub.calledAfter(deleteStackStub)).to.be.true }) it('removes ECR repository if it exists', async () => { describeRepositoriesStub.resolves() const { awsNaming } = await runServerless({ fixture: 'function', command: 'remove', awsRequestStubMap, }) expect(deleteRepositoryStub).to.be.calledWithExactly({ repositoryName: awsNaming.getEcrRepositoryName(), registryId: '999999999999', force: true, }) }) it('should execute expected operations with versioning enabled if no object versions are present', async () => { const listObjectVersionsStub = sinon.stub().resolves() const { serverless } = await runServerless({ command: 'remove', fixture: 'function', configExt: { provider: { deploymentPrefix: 'serverless', deploymentBucket: { name: 'bucket', versioning: true, }, }, }, awsRequestStubMap: { ...awsRequestStubMap, S3: { listObjectVersions: listObjectVersionsStub, headBucket: {}, }, }, }) expect(listObjectVersionsStub).to.be.calledWithExactly({ Bucket: 'bucket', Prefix: `serverless/${serverless.service.service}/dev`, }) }) it('should execute expected operations with versioning enabled if object versions are present', async () => { const listObjectVersionsStub = sinon.stub().resolves({ Versions: [ { Key: 'object1', VersionId: null }, { Key: 'object2', VersionId: 'v1' }, ], DeleteMarkers: [{ Key: 'object3', VersionId: 'v2' }], }) const innerDeleteObjectsStub = sinon.stub().resolves({ Deleted: [ { Key: 'object1', VersionId: null }, { Key: 'object2', VersionId: 'v1' }, { Key: 'object3', VersionId: 'v2' }, ], }) const { serverless } = await runServerless({ command: 'remove', fixture: 'function', configExt: { provider: { deploymentPrefix: 'serverless', deploymentBucket: { name: 'bucket', versioning: true, }, }, }, awsRequestStubMap: { ...awsRequestStubMap, S3: { listObjectVersions: listObjectVersionsStub, deleteObjects: innerDeleteObjectsStub, headBucket: {}, }, }, }) expect(listObjectVersionsStub).to.be.calledWithExactly({ Bucket: 'bucket', Prefix: `serverless/${serverless.service.service}/dev`, }) expect(innerDeleteObjectsStub).to.be.calledWithExactly({ Bucket: 'bucket', Delete: { Objects: [ { Key: 'object1', VersionId: null }, { Key: 'object2', VersionId: 'v1' }, { Key: 'object3', VersionId: 'v2' }, ], }, }) }) it('should throw an error when deleteObjects operation was not successfull', async () => { const innerDeleteObjectsStub = sinon.stub().resolves({ Deleted: [], Errors: [ { Code: 'InternalError', }, ], }) await expect( runServerless({ command: 'remove', fixture: 'function', awsRequestStubMap: { ...awsRequestStubMap, S3: { ...awsRequestStubMap.S3, deleteObjects: innerDeleteObjectsStub, headBucket: {}, }, }, }), ).to.be.eventually.rejected.and.have.property( 'code', 'CANNOT_DELETE_S3_OBJECTS_GENERIC', ) }) it('should throw an error when deleteObjects operation was not successfull due to "AccessDenied"', async () => { const innerDeleteObjectsStub = sinon.stub().resolves({ Deleted: [], Errors: [ { Code: 'AccessDenied', }, ], }) await expect( runServerless({ command: 'remove', fixture: 'function', awsRequestStubMap: { ...awsRequestStubMap, S3: { ...awsRequestStubMap.S3, deleteObjects: innerDeleteObjectsStub, headBucket: {}, }, }, }), ).to.be.eventually.rejected.and.have.property( 'code', 'CANNOT_DELETE_S3_OBJECTS_ACCESS_DENIED', ) }) it('should throw an error when cannot list objects from the bucket', async () => { await expect( runServerless({ command: 'remove', fixture: 'function', awsRequestStubMap: { ...awsRequestStubMap, S3: { ...awsRequestStubMap.S3, listObjectsV2: () => { const err = new Error('ff') err.code = 'AWS_S3_LIST_OBJECTS_V2_ACCESS_DENIED' throw err }, headBucket: {}, }, }, }), ).to.be.eventually.rejected.and.have.property( 'code', 'AWS_S3_LIST_OBJECTS_V2_ACCESS_DENIED', ) }) })