-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #189 from Financial-Times/fastly-key-expiration-ch…
…eck-merge FTDCS-196 - Fastly key expiration check merge
- Loading branch information
Showing
3 changed files
with
296 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'] | ||
]); | ||
}); | ||
}); |