353 lines
11 KiB
JavaScript

'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');
});
});