'use strict'; /* eslint-disable no-unused-expressions */ const AwsProvider = require('../../../../../../lib/plugins/aws/provider'); const AwsDeploy = require('../../../../../../lib/plugins/aws/deploy/index'); const chai = require('chai'); const Serverless = require('../../../../../../lib/Serverless'); const sinon = require('sinon'); const path = require('path'); const runServerless = require('../../../../../utils/run-serverless'); // Configure chai chai.use(require('chai-as-promised')); chai.use(require('sinon-chai')); const expect = require('chai').expect; describe('AwsDeploy', () => { let awsDeploy; let serverless; let options; beforeEach(() => { serverless = new Serverless(); options = { stage: 'dev', region: 'us-east-1', }; serverless.setProvider('aws', new AwsProvider(serverless, options)); serverless.serviceDir = 'foo'; awsDeploy = new AwsDeploy(serverless, options); }); describe('#constructor()', () => { it('should set the serverless instance', () => { expect(awsDeploy.serverless).to.equal(serverless); }); it('should set options', () => { expect(awsDeploy.options).to.equal(options); }); it('should set the service path if provided', () => { expect(awsDeploy.servicePath).to.equal('foo'); }); it('should default to an empty service path if not provided', () => { serverless.serviceDir = false; awsDeploy = new AwsDeploy(serverless, options); expect(awsDeploy.servicePath).to.equal(''); }); it('should use the options package path if provided', () => { options.package = 'package-options'; awsDeploy = new AwsDeploy(serverless, options); expect(awsDeploy.packagePath).to.equal('package-options'); }); it('should use the services package path if provided', () => { serverless.service = { package: { path: 'package-service', }, }; awsDeploy = new AwsDeploy(serverless, options); expect(awsDeploy.packagePath).to.equal('package-service'); }); it('should default to the .serverless directory as the package path', () => { expect(awsDeploy.packagePath).to.equal(path.join('foo', '.serverless')); }); it('should set the provider variable to an instance of AwsProvider', () => expect(awsDeploy.provider).to.be.instanceof(AwsProvider)); it('should have commands', () => expect(awsDeploy.commands).to.be.not.empty); it('should have hooks', () => expect(awsDeploy.hooks).to.be.not.empty); }); describe('hooks', () => { let spawnStub; beforeEach(() => { spawnStub = sinon.stub(serverless.pluginManager, 'spawn'); }); afterEach(() => { serverless.pluginManager.spawn.restore(); }); describe('"before:deploy:deploy" hook', () => { let extendedValidateStub; let spawnPackageStub; let spawnAwsCommonValidateStub; let spawnAwsCommonMoveArtifactsToTemp; beforeEach(() => { extendedValidateStub = sinon.stub(awsDeploy, 'extendedValidate').resolves(); spawnPackageStub = spawnStub.withArgs('package').resolves(); spawnAwsCommonValidateStub = spawnStub.withArgs('aws:common:validate').resolves(); spawnAwsCommonMoveArtifactsToTemp = spawnStub .withArgs('aws:common:moveArtifactsToTemp') .resolves(); }); afterEach(() => { awsDeploy.extendedValidate.restore(); }); it('should use the default packaging mechanism if no packaging config is provided', () => { awsDeploy.options.package = false; awsDeploy.serverless.service.package.path = false; return awsDeploy.hooks['before:deploy:deploy']().then(() => { expect(spawnAwsCommonValidateStub.calledOnce).to.equal(true); expect(extendedValidateStub.calledAfter(spawnAwsCommonValidateStub)).to.equal(true); expect(spawnAwsCommonMoveArtifactsToTemp.calledOnce).to.equal(false); }); }); it('should move the artifacts to the tmp dir if options based config is provided', () => { awsDeploy.options.package = true; awsDeploy.serverless.service.package.path = false; return awsDeploy.hooks['before:deploy:deploy']().then(() => { expect(spawnAwsCommonValidateStub.calledOnce).to.equal(true); expect( spawnAwsCommonMoveArtifactsToTemp.calledAfter(spawnAwsCommonValidateStub) ).to.equal(true); expect(extendedValidateStub.calledAfter(spawnAwsCommonMoveArtifactsToTemp)).to.equal( true ); expect(spawnPackageStub.calledOnce).to.equal(false); }); }); it('should move the artifacts to the tmp dir if service based config is provided', () => { awsDeploy.options.package = false; awsDeploy.serverless.service.package.path = true; return awsDeploy.hooks['before:deploy:deploy']().then(() => { expect(spawnAwsCommonValidateStub.calledOnce).to.equal(true); expect( spawnAwsCommonMoveArtifactsToTemp.calledAfter(spawnAwsCommonValidateStub) ).to.equal(true); expect(extendedValidateStub.calledAfter(spawnAwsCommonMoveArtifactsToTemp)).to.equal( true ); expect(spawnPackageStub.calledOnce).to.equal(false); }); }); }); it('should run "deploy:finalize" hook', () => { const spawnAwsDeployFinalizeStub = spawnStub.withArgs('aws:deploy:finalize').resolves(); return awsDeploy.hooks['deploy:finalize']().then(() => { expect(spawnAwsDeployFinalizeStub.calledOnce).to.equal(true); }); }); describe('"aws:deploy:finalize:cleanup" hook', () => { let cleanupS3BucketStub; let spawnAwsCommonCleanupTempDirStub; beforeEach(() => { cleanupS3BucketStub = sinon.stub(awsDeploy, 'cleanupS3Bucket').resolves(); spawnAwsCommonCleanupTempDirStub = spawnStub .withArgs('aws:common:cleanupTempDir') .resolves(); }); afterEach(() => { awsDeploy.cleanupS3Bucket.restore(); }); it('should do the default cleanup if no packaging config is used', () => { awsDeploy.options.package = false; awsDeploy.serverless.service.package.path = false; return awsDeploy.hooks['aws:deploy:finalize:cleanup']().then(() => { expect(cleanupS3BucketStub.calledOnce).to.equal(true); expect(spawnAwsCommonCleanupTempDirStub.calledOnce).to.equal(false); }); }); it('should cleanup the tmp dir if options based packaging config is used', () => { awsDeploy.options.package = true; awsDeploy.serverless.service.package.path = false; return awsDeploy.hooks['aws:deploy:finalize:cleanup']().then(() => { expect(cleanupS3BucketStub.calledOnce).to.equal(true); expect(spawnAwsCommonCleanupTempDirStub.calledAfter(cleanupS3BucketStub)).to.equal(true); }); }); it('should cleanup the tmp dir if service based packaging config is used', () => { awsDeploy.options.package = false; awsDeploy.serverless.service.package.path = true; return awsDeploy.hooks['aws:deploy:finalize:cleanup']().then(() => { expect(cleanupS3BucketStub.calledOnce).to.equal(true); expect(spawnAwsCommonCleanupTempDirStub.calledAfter(cleanupS3BucketStub)).to.equal(true); }); }); it('should not cleanup if a deployment was not necessary', () => { awsDeploy.serverless.service.provider.shouldNotDeploy = true; return awsDeploy.hooks['aws:deploy:finalize:cleanup']().then(() => { expect(cleanupS3BucketStub.called).to.equal(false); expect(spawnAwsCommonCleanupTempDirStub.called).to.equal(false); }); }); }); }); }); describe('test/unit/lib/plugins/aws/deploy/index.test.js', () => { const baseAwsRequestStubMap = { STS: { getCallerIdentity: { ResponseMetadata: { RequestId: 'ffffffff-ffff-ffff-ffff-ffffffffffff' }, UserId: 'XXXXXXXXXXXXXXXXXXXXX', Account: '999999999999', Arn: 'arn:aws:iam::999999999999:user/test', }, }, }; it('with nonexistent stack - first deploy', async () => { const describeStacksStub = sinon .stub() .onFirstCall() .throws('error', 'stack does not exist') .onSecondCall() .resolves({ Stacks: [{}] }); const createStackStub = sinon.stub().resolves({}); const s3UploadStub = sinon.stub().resolves(); const updateStackStub = sinon.stub().resolves({}); const deleteObjectsStub = sinon.stub().resolves({}); const awsRequestStubMap = { ...baseAwsRequestStubMap, ECR: { describeRepositories: sinon.stub().throws({ providerError: { code: 'RepositoryNotFoundException' }, }), }, S3: { deleteObjects: deleteObjectsStub, listObjectsV2: { Contents: [] }, upload: s3UploadStub, }, CloudFormation: { describeStacks: describeStacksStub, createStack: createStackStub, describeStackEvents: { StackEvents: [ { EventId: '1e2f3g4h', StackName: 'new-service-dev', LogicalResourceId: 'new-service-dev', ResourceType: 'AWS::CloudFormation::Stack', Timestamp: new Date(), ResourceStatus: 'CREATE_COMPLETE', }, ], }, describeStackResource: { StackResourceDetail: { PhysicalResourceId: 's3-bucket-resource' }, }, validateTemplate: {}, updateStack: updateStackStub, listStackResources: {}, }, }; await runServerless({ fixture: 'function', command: 'deploy', awsRequestStubMap, }); expect(createStackStub).to.be.calledOnce; const wasCloudFormationTemplateUploadInitiated = s3UploadStub.args.some((call) => call[0].Key.endsWith('compiled-cloudformation-template.json') ); expect(wasCloudFormationTemplateUploadInitiated).to.be.true; expect(updateStackStub).to.be.calledOnce; expect(deleteObjectsStub).not.to.be.called; }); it('with existing stack - subsequent deploy', async () => { const s3BucketPrefix = 'serverless/test-aws-deploy-with-existing-stack/dev'; const createStackStub = sinon.stub().resolves({}); const s3UploadStub = sinon.stub().resolves(); const updateStackStub = sinon.stub().resolves({}); const listObjectsV2Stub = sinon .stub() .onFirstCall() .resolves({ Contents: [] }) .onSecondCall() .resolves({ Contents: [ { Key: `${s3BucketPrefix}/1589988704351-2020-05-20T15:31:44.359Z/compiled-cloudformation-template.json`, }, { Key: `${s3BucketPrefix}/1589988704351-2020-05-20T15:31:44.359Z/artifact.zip`, }, { Key: `${s3BucketPrefix}/1589988704352-2020-05-20T15:31:44.359Z/compiled-cloudformation-template.json`, }, { Key: `${s3BucketPrefix}/1589988704352-2020-05-20T15:31:44.359Z/artifact.zip`, }, ], }); const deleteObjectsStub = sinon.stub().resolves(); const awsRequestStubMap = { ...baseAwsRequestStubMap, ECR: { describeRepositories: sinon.stub().throws({ providerError: { code: 'RepositoryNotFoundException' }, }), }, S3: { deleteObjects: deleteObjectsStub, listObjectsV2: listObjectsV2Stub, upload: s3UploadStub, }, CloudFormation: { describeStacks: { Stacks: [{}] }, createStack: createStackStub, describeStackEvents: { StackEvents: [ { EventId: '1e2f3g4h', StackName: 'new-service-dev', LogicalResourceId: 'new-service-dev', ResourceType: 'AWS::CloudFormation::Stack', Timestamp: new Date(), ResourceStatus: 'UPDATE_COMPLETE', }, ], }, describeStackResource: { StackResourceDetail: { PhysicalResourceId: 's3-bucket-resource' }, }, validateTemplate: {}, updateStack: updateStackStub, listStackResources: {}, }, }; await runServerless({ fixture: 'function', command: 'deploy', awsRequestStubMap, configExt: { // Default, non-deterministic service-name invalidates this test as S3 Bucket cleanup relies on it service: 'test-aws-deploy-with-existing-stack', provider: { deploymentBucket: { maxPreviousDeploymentArtifacts: 1, }, }, }, }); expect(createStackStub).to.not.be.called; const wasCloudFormationTemplateUploadInitiated = s3UploadStub.args.some((call) => call[0].Key.endsWith('compiled-cloudformation-template.json') ); expect(wasCloudFormationTemplateUploadInitiated).to.be.true; expect(updateStackStub).to.be.calledOnce; expect(deleteObjectsStub).to.be.calledWithExactly({ Bucket: 's3-bucket-resource', Delete: { Objects: [ { Key: `${s3BucketPrefix}/1589988704351-2020-05-20T15:31:44.359Z/compiled-cloudformation-template.json`, }, { Key: `${s3BucketPrefix}/1589988704351-2020-05-20T15:31:44.359Z/artifact.zip` }, ], }, }); }); it('with existing stack - should skip deploy if nothing changed', async () => { const createStackStub = sinon.stub().resolves({}); const s3UploadStub = sinon.stub().resolves(); const updateStackStub = sinon.stub().resolves({}); const listObjectsV2Stub = sinon.stub().resolves({ Contents: [ { Key: 'serverless/test-package-artifact/dev/1589988704359-2020-05-20T15:31:44.359Z/compiled-cloudformation-template.json', LastModified: new Date(), ETag: '"5102a4cf710cae6497dba9e61b85d0a4"', Size: 356, StorageClass: 'STANDARD', }, { Key: 'serverless/test-package-artifact/dev/1589988704359-2020-05-20T15:31:44.359Z/my-own.zip', LastModified: new Date(), ETag: '"5102a4cf710cae6497dba9e61b85d0a4"', Size: 356, StorageClass: 'STANDARD', }, ], }); const s3HeadObjectStub = sinon.stub(); s3HeadObjectStub .withArgs({ Bucket: 's3-bucket-resource', Key: 'serverless/test-package-artifact/dev/1589988704359-2020-05-20T15:31:44.359Z/compiled-cloudformation-template.json', }) .returns({ Metadata: { filesha256: 'qxp+iwSTMhcRUfHzka4AE4XAWawS8GnEyBh1WpGb7Vw=' }, }); s3HeadObjectStub .withArgs({ Bucket: 's3-bucket-resource', Key: 'serverless/test-package-artifact/dev/1589988704359-2020-05-20T15:31:44.359Z/my-own.zip', }) .returns({ Metadata: { filesha256: 'T0qEYHOE4Xv2E8Ar03xGogAlElcdf/dQh/lh9ao7Glo=' }, }); const awsRequestStubMap = { ...baseAwsRequestStubMap, S3: { headObject: s3HeadObjectStub, listObjectsV2: listObjectsV2Stub, upload: s3UploadStub, }, CloudFormation: { describeStacks: { Stacks: [{}] }, createStack: createStackStub, describeStackEvents: { StackEvents: [ { EventId: '1e2f3g4h', StackName: 'new-service-dev', LogicalResourceId: 'new-service-dev', ResourceType: 'AWS::CloudFormation::Stack', Timestamp: new Date(), ResourceStatus: 'UPDATE_COMPLETE', }, ], }, describeStackResource: { StackResourceDetail: { PhysicalResourceId: 's3-bucket-resource' }, }, validateTemplate: {}, updateStack: updateStackStub, listStackResources: {}, }, }; const { serverless } = await runServerless({ fixture: 'packageArtifactInServerlessDir', command: 'deploy', awsRequestStubMap, configExt: { // Default, non-deterministic service-name invalidates this test service: 'test-aws-deploy-should-be-skipped', }, }); expect(serverless.service.provider.shouldNotDeploy).to.be.true; expect(createStackStub).to.not.be.called; expect(updateStackStub).to.not.be.called; expect(s3UploadStub).to.not.be.called; }); });