Skip to content

Commit

Permalink
Merge pull request #189 from Financial-Times/fastly-key-expiration-ch…
Browse files Browse the repository at this point in the history
…eck-merge

FTDCS-196 - Fastly key expiration check merge
  • Loading branch information
JSRedondo committed Jun 20, 2022
2 parents 7d65dbe + b85aa53 commit bcb4dcb
Show file tree
Hide file tree
Showing 3 changed files with 296 additions and 1 deletion.
109 changes: 109 additions & 0 deletions src/checks/fastlyKeyExpiration.check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const fetch = require('node-fetch');
const moment = require('moment');
const logger = require('@financial-times/n-logger').default;
const Check = require('./check');
const status = require('./status');

const fastlyApiEndpoint = 'https://api.fastly.com/tokens/self';
/**
* @description Polls the current state of a Fastly key expiration date
* alert if the key expires in the next week or two
* alert if the key has a null expiry date
*/
class FastlyKeyExpirationCheck extends Check {
constructor(options) {
super(options);
this.fastlyKey = options.fastlyKey;
this.states = Object.freeze({
PENDING: {
status: status.PENDING,
checkOutput: 'Fastly key check has not yet run',
severity: this.severity
},
FAILED_VALIDATION: {
status: status.FAILED,
checkOutput: 'Fastly key expiration date is due within 2 weeks',
severity: this.severity
},
FAILED_URGENT_VALIDATION: {
status: status.FAILED,
checkOutput: 'Fastly key is expired',
severity: 1
},
FAILED_DATE: {
status: status.FAILED,
checkOutput: 'Invalid Fastly key expiring date',
severity: this.severity
},
ERRORED: {
status: status.ERRORED,
checkOutput: 'Fastly key check failed to fetch data',
severity: this.severity
},
PASSED: {
status: status.PASSED,
checkOutput: 'Fastly key expiration date is ok',
severity: this.severity
}
});
this.setState(this.states.PENDING);
}

setState(state) {
// To be able to assign a varialble number of parameters
Object.assign(this, state);
}

async getFastlyKeyMetadata() {
try {
const result = await fetch(fastlyApiEndpoint, {
headers: { 'Fastly-Key': this.fastlyKey }
});
const json = await result.json();
return json;
} catch (error) {
logger.error('Failed to get Fastly key metadata', error.message);
this.setState(this.states.ERRORED);
throw error;
}
}

parseStringDate(stringDate) {
const date = moment(stringDate, 'YYYY-MM-DDTHH:mm:ssZ');
if (!date.isValid()) {
logger.warn(`Invalid Fastly Key expiration date ${stringDate}`);
this.setState(this.states.FAILED_DATE);
throw new Error('Invalid date');
}
return date;
}

async getExpirationDate() {
const metadata = await this.getFastlyKeyMetadata();
const expirationDate = this.parseStringDate(metadata['expires_at']);
return expirationDate;
}

checkExpirationDate(expirationDate) {
const now = moment();
const limitDate = moment().add(2, 'weeks');
switch(true){
case expirationDate.isAfter(limitDate): return this.states.PASSED;
case expirationDate.isBefore(now): return this.states.FAILED_URGENT_VALIDATION;
default: return this.states.FAILED_VALIDATION
}
}

async tick() {
try {
const expirationDate = await this.getExpirationDate();
const state = this.checkExpirationDate(expirationDate);
this.setState(state);
} catch {
// This catch is meant to end the execution of the tick function.
// An expecific state has been set for each error type.
}
}
}

module.exports = FastlyKeyExpirationCheck;
3 changes: 2 additions & 1 deletion src/checks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ module.exports = {
graphiteThreshold: require('./graphiteThreshold.check'),
graphiteWorking: require('./graphiteWorking.check'),
cloudWatchAlarm: require('./cloudWatchAlarm.check'),
cloudWatchThreshold: require('./cloudWatchThreshold.check')
cloudWatchThreshold: require('./cloudWatchThreshold.check'),
fastlyKeys: require('./fastlyKeyExpiration.check')
};
185 changes: 185 additions & 0 deletions test/fastlyKeyExpiration.check.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
const sinon = require('sinon');
const { expect } = require('chai');
const proxyquire = require('proxyquire').noCallThru().noPreserveCache();
const logger = require('@financial-times/n-logger').default;
const FastlyCheck = require('../src/checks/fastlyKeyExpiration.check');
const status = require('../src/checks/status');

const millisecondsFortnight = 14 * 24 * 60 * 60 * 1000;
const defaultOptions = {
name: 'fastlyKeyExpiration',
severity: 2,
businessImpact: 'b',
panicGuide: 'p',
technicalSummary: 't',
fastlyKey: 'k'
};

describe('Fastly Key Expiration Check', () => {
beforeEach(() => {
sinon.spy(logger);
});
afterEach(() => {
sinon.restore();
});
it('FastlyKeyExpirationCheck instance set & freeze states properly ', () => {
const fastlyKeyExpirationCheck = new FastlyCheck(defaultOptions);
const initialStates = {
PENDING: {
status: status.PENDING,
checkOutput: 'Fastly key check has not yet run',
severity: 2
},
FAILED_VALIDATION: {
status: status.FAILED,
checkOutput: 'Fastly key expiration date is due within 2 weeks',
severity: 2
},
FAILED_URGENT_VALIDATION: {
status: status.FAILED,
checkOutput: 'Fastly key is expired',
severity: 1
},
FAILED_DATE: {
status: status.FAILED,
checkOutput: 'Invalid Fastly key expiring date',
severity: 2
},
ERRORED: {
status: status.ERRORED,
checkOutput: 'Fastly key check failed to fetch data',
severity: 2
},
PASSED: {
status: status.PASSED,
checkOutput: 'Fastly key expiration date is ok',
severity: 2
}
};
expect(fastlyKeyExpirationCheck.states).to.be.deep.equal(initialStates);
fastlyKeyExpirationCheck.severity = 1;
expect(fastlyKeyExpirationCheck.states).to.be.deep.equal(initialStates);
});

it('returns ok=false if fastly key check has not yet run', async () => {
sinon.stub(FastlyCheck.prototype, 'tick');
const fastlyCheck = new FastlyCheck(defaultOptions);
const result = fastlyCheck.getStatus();
expect(result.ok).to.be.false;
// Default error message for ERRORED states in parent function Check
expect(result.checkOutput).to.be.equal(
fastlyCheck.states.PENDING.checkOutput
);
expect(result.severity).to.be.equal(fastlyCheck.states.PENDING.severity);
});
it('returns ok=false if key check failed to fetch data & logs errors', async () => {
// Arrange
const FastlyCheck = proxyquire('../src/checks/fastlyKeyExpiration.check', {
'node-fetch': sinon.stub().rejects(new Error('Timeout'))
});
const fastlyCheck = new FastlyCheck(defaultOptions);
// Force the update of the state
await fastlyCheck.tick();
// Act
const result = fastlyCheck.getStatus();
// Assert
expect(result.ok).to.be.false;
// Default error message for ERRORED states in parent function Check
expect(result.checkOutput).to.be.equal('Healthcheck failed to execute');
expect(result.severity).to.be.equal(fastlyCheck.states.ERRORED.severity);
expect(logger.error.args).to.be.deep.equal([
['Failed to get Fastly key metadata', 'Timeout']
]);
});
it('returns ok=false, severity=1 if expiration date is due now', async () => {
// Arrange
const now = new Date();
const limitDate = new Date(
now.getTime() // now
);
// Fix now as reference time
sinon.useFakeTimers(now);
const fastlyCheck = new FastlyCheck(defaultOptions);
const metadata = { expires_at: limitDate.toISOString() };
sinon.stub(fastlyCheck, 'getFastlyKeyMetadata').resolves(metadata);
// Force the update of the state
await fastlyCheck.tick();
// Act
const result = fastlyCheck.getStatus();
// Assert
expect(result.ok).to.be.false;
expect(result.checkOutput).to.be.equal(
fastlyCheck.states.FAILED_URGENT_VALIDATION.checkOutput
);
expect(result.severity).to.be.equal(
fastlyCheck.states.FAILED_URGENT_VALIDATION.severity
);
});
it('returns ok=false if expiration date is due within 2 weeks', async () => {
// Arrange
const now = new Date();
const limitDate = new Date(
now.getTime() + millisecondsFortnight // 2 weeks from now
);
// Fix now as reference time
sinon.useFakeTimers(now);
const fastlyCheck = new FastlyCheck(defaultOptions);
const metadata = { expires_at: limitDate.toISOString() };
sinon.stub(fastlyCheck, 'getFastlyKeyMetadata').resolves(metadata);
// Force the update of the state
await fastlyCheck.tick();
// Act
const result = fastlyCheck.getStatus();
// Assert
expect(result.ok).to.be.false;
expect(result.checkOutput).to.be.equal(
fastlyCheck.states.FAILED_VALIDATION.checkOutput
);
expect(result.severity).to.be.equal(
fastlyCheck.states.FAILED_VALIDATION.severity
);
});
it('returns ok=true if expiration date is due after 2 weeks', async () => {
// Arrange
const now = new Date();
const limitDate = new Date(
now.getTime() + millisecondsFortnight + 1000 // 2 weeks from now + 1 second
);
// Fix now as reference time
sinon.useFakeTimers(now);
const fastlyCheck = new FastlyCheck(defaultOptions);
const metadata = { expires_at: limitDate.toISOString() };
sinon.stub(fastlyCheck, 'getFastlyKeyMetadata').resolves(metadata);
// Force the update of the state
await fastlyCheck.tick();
// Act
const result = fastlyCheck.getStatus();
// Assert
expect(result.ok).to.be.true;
expect(result.checkOutput).to.be.equal(
fastlyCheck.states.PASSED.checkOutput
);
expect(result.severity).to.be.equal(fastlyCheck.states.PASSED.severity);
});
it('returns ok=false if expiration date is not valid & logs warning', async () => {
// Arrange
const fastlyCheck = new FastlyCheck(defaultOptions);
const metadata = { expires_at: 'aaaa' };
sinon.stub(fastlyCheck, 'getFastlyKeyMetadata').resolves(metadata);
// Force the update of the state
await fastlyCheck.tick();
// Act
const result = fastlyCheck.getStatus();
// Assert
expect(result.ok).to.be.false;
expect(result.checkOutput).to.be.equal(
fastlyCheck.states.FAILED_DATE.checkOutput
);
expect(result.severity).to.be.equal(
fastlyCheck.states.FAILED_DATE.severity
);
expect(logger.warn.args).to.be.deep.equal([
['Invalid Fastly Key expiration date aaaa']
]);
});
});

0 comments on commit bcb4dcb

Please sign in to comment.