From c0f3899f41c34870d274e8d72c4ce46fc2f3db3c Mon Sep 17 00:00:00 2001 From: Serhii Popov Date: Tue, 16 Jun 2020 06:10:35 +0300 Subject: [PATCH] Add EC2 tests --- package/src/autoscaling-scheduler.js | 14 ++- package/src/ec2-scheduler.js | 7 +- .../test/unit/autoscaling-scheduler.test.js | 87 +++++++++++++++++++ package/test/unit/ec2-scheduler.test.js | 59 +++++++++++++ package/test/unit/rds-scheduler.test.js | 28 +++--- package/test/unit/spot-scheduler.test.js | 17 +++- 6 files changed, 181 insertions(+), 31 deletions(-) create mode 100644 package/test/unit/autoscaling-scheduler.test.js create mode 100644 package/test/unit/ec2-scheduler.test.js diff --git a/package/src/autoscaling-scheduler.js b/package/src/autoscaling-scheduler.js index d36d37a..342ab45 100644 --- a/package/src/autoscaling-scheduler.js +++ b/package/src/autoscaling-scheduler.js @@ -18,10 +18,9 @@ class AutoScalingScheduler { * * @param {String} action Perform an action name * @param {Array} resourceTags "{tag:value}" pairs to use for filter resources - * @param callback * @returns {Promise} */ - async run(action, resourceTags, callback) { + async run(action, resourceTags) { if (!resourceTags) { throw new Error('"resourceTags" must be specified, otherwise you will shoutdown all instances'); } @@ -30,12 +29,10 @@ class AutoScalingScheduler { let asgData = await this.autoScaling.describeAutoScalingGroups().promise(); for (const asg of asgData.AutoScalingGroups) { if (asg.Tags.length && Utils.matchTags(resourceTags, asg.Tags)) { - let data = await this[action](asg); - //callback(null, data); + await this[action](asg); } } } catch (e) { - //callback(e, null); console.error(e.stack); } } @@ -51,9 +48,8 @@ class AutoScalingScheduler { AutoScalingGroupName: asg.AutoScalingGroupName, }; let data = await this.autoScaling.suspendProcesses(params).promise(); - console.log(`Suspend AutoScaling group ${asg.AutoScalingGroupName}`, JSON.stringify(data)); - //let instanceIds = asg.Instances.forEach((instance) => { instance.InstanceId }); + console.log(`Suspend AutoScaling group ${asg.AutoScalingGroupName}`, JSON.stringify(data)); for (const instance of asg.Instances) { let params = { @@ -68,6 +64,7 @@ class AutoScalingScheduler { data = await this.ec2.stopInstances(params).promise(); } catch (e) { // Otherwise try to terminate it (in most cases for EC2 Spot instances) + //console.log("I'm going to terminate instance"); data = await this.ec2.terminateInstances(params).promise(); } console.log(`Stop EC2 instance ${instance.InstanceId}`, JSON.stringify(data)); @@ -99,9 +96,8 @@ class AutoScalingScheduler { // @todo Start only instances which can be started: "Values": ["pending", "stopping", "stopped"], let data = await this.ec2.startInstances(params).promise(); - console.log(`Stop EC2 instance ${instance.InstanceId}`, JSON.stringify(data)); + console.log(`Start EC2 instance ${instance.InstanceId}`, JSON.stringify(data)); } - return data; } } diff --git a/package/src/ec2-scheduler.js b/package/src/ec2-scheduler.js index 898c997..359d427 100644 --- a/package/src/ec2-scheduler.js +++ b/package/src/ec2-scheduler.js @@ -18,10 +18,9 @@ class Ec2Scheduler { * * @param action String * @param resourceTags Array {key:value} pairs to use for filter resources - * @param callback * @returns {Promise} */ - async run(action, resourceTags, callback) { + async run(action, resourceTags) { if (!resourceTags) { throw new Error('Resource tags must be specified otherwise you will shoutdown all instances'); } @@ -49,16 +48,16 @@ class Ec2Scheduler { }; let autoScaling = await this.autoScaling.describeAutoScalingInstances(asParams).promise(); + console.log('autoScaling', autoScaling); + if (!autoScaling.AutoScalingInstances.length) { let data = await this[action](instance.InstanceId); - //callback(null, data); console.log(`${Utils.ucFirst(action)} EC2 instance ${instance.InstanceId}`, JSON.stringify(data)); } } } } catch (e) { - //callback(e, null); console.error(e.stack); } } diff --git a/package/test/unit/autoscaling-scheduler.test.js b/package/test/unit/autoscaling-scheduler.test.js new file mode 100644 index 0000000..b21a228 --- /dev/null +++ b/package/test/unit/autoscaling-scheduler.test.js @@ -0,0 +1,87 @@ +// Great tutorial how to use Sinon @link https://semaphoreci.com/community/tutorials/best-practices-for-spies-stubs-and-mocks-in-sinon-js +// Use sinon assertions and matchers where it is possible https://sinonjs.org/releases/v9.0.2/matchers/ +require('app-module-path').addPath(process.cwd() + '/src'); + +const AWSMock = require('aws-sdk-mock'); +const AWS = require('aws-sdk'); +const chai = require('chai'); +const sinon = require('sinon'); +const AutoScalingScheduler = require('autoscaling-scheduler'); + +describe('AWS AutoScaling Group Lambda Scheduler', () => { + let consoleLogStub = null; + beforeEach(() => { + AWSMock.setSDKInstance(AWS); + // Ignore console.log() output + consoleLogStub = sinon.stub(console, 'log'); + }); + + afterEach(() => { + AWSMock.restore(); + consoleLogStub.restore(); + }); + + [ + { action: 'stop', method: 'stopInstances', processMethod: 'suspendProcesses', responseKey: 'StoppingInstances' }, + { action: 'start', method: 'startInstances', processMethod: 'resumeProcesses', responseKey: 'StartingInstances' }, + ].forEach(function (run) { + it(`run: "${run.method}" should be called once`, async () => { + let tags = [{ "Key": "ToStop", "Value": "true" }, { "Key": "Environment", "Value": "test" }]; + + // Important creating the spy/sub in such way, because there are several calls to AWS under the hood + let actionInstancesSpy = sinon.spy((params, callback) => { + callback(null, { [run.responseKey]: [{ InstanceId: "TEST-EC2-ID-123" }] }); + }) + + // Mock successful execution + AWSMock.mock('EC2', run.method, actionInstancesSpy); + + AWSMock.mock('AutoScaling', 'describeAutoScalingGroups', async (callback) => { + callback(null, { AutoScalingGroups: [ + { + Tags: tags, + AutoScalingGroupName: "TEST-AUTO-SCALING-GROUP-NAME", + Instances: [{ InstanceId: "TEST-EC2-ID-123" }] + } + ]}); + }); + AWSMock.mock('AutoScaling', run.processMethod, async (params, callback) => { + callback(null, { }); + }); + + let spotScheduler = new AutoScalingScheduler('eu-central-1'); + await spotScheduler.run(run.action, tags); + + // Assert on your Sinon spy as normal + sinon.assert.calledOnce(actionInstancesSpy); + sinon.assert.calledWith(actionInstancesSpy, { InstanceIds: ['TEST-EC2-ID-123'] }); + }); + }); + + it(`stop: "terminateInstances should be called if stopInstances throw Error`, async () => { + // Important creating the spy/sub in such way, because there are several calls to AWS under the hood + let stopInstancesSpy = sinon.spy((params, callback) => { + throw Error(); + }); + AWSMock.mock('EC2', 'stopInstances', stopInstancesSpy); + + let terminateInstancesSpy = sinon.spy((params, callback) => { + callback(null, { 'TerminateInstances': [{ InstanceId: "TEST-EC2-ID-123" }] }); + }) + AWSMock.mock('EC2', 'terminateInstances', terminateInstancesSpy); + + AWSMock.mock('AutoScaling', 'suspendProcesses', async (params, callback) => { + callback(null, { }); + }); + + let spotScheduler = new AutoScalingScheduler('eu-central-1'); + await spotScheduler.stop({ + AutoScalingGroupName: "TEST-AUTO-SCALING-GROUP-NAME", + Instances: [{ InstanceId: "TEST-EC2-ID-123" }] + }); + + // Assert on your Sinon spy as normal + sinon.assert.threw(stopInstancesSpy); + sinon.assert.calledWith(terminateInstancesSpy, { InstanceIds: ['TEST-EC2-ID-123'] }); + }); +}); diff --git a/package/test/unit/ec2-scheduler.test.js b/package/test/unit/ec2-scheduler.test.js new file mode 100644 index 0000000..f7090ac --- /dev/null +++ b/package/test/unit/ec2-scheduler.test.js @@ -0,0 +1,59 @@ +// Great tutorial how to use Sinon @link https://semaphoreci.com/community/tutorials/best-practices-for-spies-stubs-and-mocks-in-sinon-js +// Use sinon assertions and matchers where it is possible https://sinonjs.org/releases/v9.0.2/matchers/ +require('app-module-path').addPath(process.cwd() + '/src'); + +const AWSMock = require('aws-sdk-mock'); +const AWS = require('aws-sdk'); +const chai = require('chai'); +const sinon = require('sinon'); +const Ec2Scheduler = require('ec2-scheduler'); + +describe('AWS EC2 Lambda Scheduler', () => { + let consoleLogStub = null; + beforeEach(() => { + AWSMock.setSDKInstance(AWS); + // Ignore console.log() output + consoleLogStub = sinon.stub(console, 'log'); + }); + + afterEach(() => { + AWSMock.restore(); + consoleLogStub.restore(); + }); + + [ + { action: 'stop', method: 'stopInstances', responseKey: 'StoppingInstances' }, + { action: 'start', method: 'startInstances', responseKey: 'StartingInstances' }, + ].forEach(function (run) { + it(`run: "${run.method}" should be called once`, async () => { + let tags = [{ "Key": "ToStop", "Value": "true" }, { "Key": "Environment", "Value": "test" }]; + + // Important creating the spy/sub in such way, because there are several calls to AWS under the hood + let actionInstancesSpy = sinon.spy((params, callback) => { + callback(null, { [run.responseKey]: [{ InstanceId: "TEST-EC2-ID-123" }] }); + }) + + // Mock successful execution + AWSMock.mock('EC2', run.method, actionInstancesSpy); + + AWSMock.mock('EC2', 'describeInstances', async (params, callback) => { + callback(null, { Reservations: [ + { + Instances: [{ InstanceId: "TEST-EC2-ID-123" }] + } + ]}); + }); + + AWSMock.mock('AutoScaling', 'describeAutoScalingInstances', async (params, callback) => { + callback(null, { AutoScalingInstances: []}); + }); + + let spotScheduler = new Ec2Scheduler('eu-central-1'); + await spotScheduler.run(run.action, tags); + + // Assert on your Sinon spy as normal + sinon.assert.calledOnce(actionInstancesSpy); + sinon.assert.calledWith(actionInstancesSpy, { InstanceIds: ['TEST-EC2-ID-123'] }); + }); + }); +}); diff --git a/package/test/unit/rds-scheduler.test.js b/package/test/unit/rds-scheduler.test.js index 84d3206..322e1ba 100644 --- a/package/test/unit/rds-scheduler.test.js +++ b/package/test/unit/rds-scheduler.test.js @@ -13,9 +13,20 @@ chai.use(chaiAsPromised); let expect = chai.expect; let assert = chai.assert; +let consoleLogStub = null; + describe('AWS RDS Lambda Scheduler', () => { beforeEach(function() { AWSMock.setSDKInstance(AWS); + consoleLogStub = sinon.stub(console, 'log'); + }); + + afterEach(function() { + // Important! Restore AWS SDK + AWSMock.restore(); + + // Ignore console.log() output + consoleLogStub.restore(); }); it('run: resourceTags cannot be empty', async() => { @@ -26,8 +37,7 @@ describe('AWS RDS Lambda Scheduler', () => { }); it('run: "stop" action should be called once', async() => { - // Ignore console.log() output - let consoleLogSpy = sinon.stub(console, 'log'); + //let consoleLogSpy = sinon.stub(console, 'log'); let tags = [{ "Key": "ToStop", "Value": "true" }, { "Key": "Environment", "Value": "stage" }]; @@ -45,10 +55,6 @@ describe('AWS RDS Lambda Scheduler', () => { sinon.assert.calledOnce(stopStub); sinon.assert.calledWith(stopStub, 'DB-INSTANCE-TEST-ID'); - - // Important! Restore AWS SDK - AWSMock.restore('RDS'); - consoleLogSpy.restore(); }); it('run: "stop" action should not be called according to mismatching of tags', async() => { @@ -69,14 +75,11 @@ describe('AWS RDS Lambda Scheduler', () => { sinon.assert.notCalled(stopStub); //sinon.assert.calledWith(stopStub, 'DB-INSTANCE-TEST-ID'); - - // Important! Restore AWS SDK - AWSMock.restore('RDS'); }); it('stop: should stop instance by certain ID', async() => { // Ignore console.log() output - let consoleLogSpy = sinon.stub(console, 'log'); + //let consoleLogSpy = sinon.stub(console, 'log'); let stopDBInstanceSpy = sinon.spy((params, callback) => { callback(null, { 'StoppingInstances': [{ DBInstanceIdentifier: "TEST-RDS-ID-123" }] }); @@ -99,11 +102,6 @@ describe('AWS RDS Lambda Scheduler', () => { assert.isTrue(stopDBInstanceSpy.calledWith(expectedParams), 'should pass correct parameters'); // Expect passed JSON parameters have required 'DBInstanceIdentifier' property expect(stopDBInstanceSpy.getCall(0).args[0]).to.have.property('DBInstanceIdentifier'); - - // Important! Restore AWS SDK - AWSMock.restore('RDS'); - - consoleLogSpy.restore(); }); }); diff --git a/package/test/unit/spot-scheduler.test.js b/package/test/unit/spot-scheduler.test.js index 3e2dc26..74ef444 100644 --- a/package/test/unit/spot-scheduler.test.js +++ b/package/test/unit/spot-scheduler.test.js @@ -8,9 +8,19 @@ const chai = require('chai'); const sinon = require('sinon'); const SpotScheduler = require('spot-scheduler'); -describe('AWS EC2 Spot Instances Lambda Scheduler', () => { +let consoleLogStub = null; + +describe('AWS EC2 Spot Instances Lambda Scheduler', async () => { beforeEach(function() { AWSMock.setSDKInstance(AWS); + consoleLogStub = sinon.stub(console, 'log'); + }); + + afterEach(function() { + AWSMock.restore(); + + // Ignore console.log() output + consoleLogStub.restore(); }); [ @@ -22,7 +32,7 @@ describe('AWS EC2 Spot Instances Lambda Scheduler', () => { let tags = [{ "Key": "ToStop", "Value": "true" }, { "Key": "Environment", "Value": "stage" }]; // Ignore console.log() output - let consoleLogSpy = sinon.stub(console, 'log'); + //let consoleLogSpy = sandbox.stub(console, 'log'); // Important creating the spy/sub in such way there are several calls to AWS under the hood let actionInstancesSpy = sinon.spy((params, callback) => { callback(null, { [run.responseKey]: [{ InstanceId: "TEST-SPOT-ID-123" }] }); @@ -49,7 +59,8 @@ describe('AWS EC2 Spot Instances Lambda Scheduler', () => { AWSMock.restore('EC2'); AWSMock.restore('AutoScaling'); - consoleLogSpy.restore(); + //actionInstancesSpy.restore(); + //consoleLogSpy.restore(); }); }); });