openhim-core
Version:
The OpenHIM core application that provides logging and routing of http requests
510 lines (454 loc) • 19.6 kB
text/coffeescript
should = require "should"
sinon = require "sinon"
http = require "http"
moment = require "moment"
alerts = require "../../lib/alerts"
testUtils = require "../testUtils"
config = require "../../lib/config/config"
config.alerts = config.get('alerts')
Channel = require("../../lib/model/channels").Channel
User = require("../../lib/model/users").User
ContactGroup = require("../../lib/model/contactGroups").ContactGroup
Event = require("../../lib/model/events").Event
Alert = require("../../lib/model/alerts").Alert
testUser1 = new User
firstname: 'User'
surname: 'One'
email: 'one@openhim.org'
passwordAlgorithm: 'sha512'
passwordHash: '3cc90918-7044-4e55-b61d-92ae73cb261e'
passwordSalt: '22a61686-66f6-483c-a524-185aac251fb0'
testUser2 = new User
firstname: 'User'
surname: 'Two'
email: 'two@openhim.org'
msisdn: '27721234567'
passwordAlgorithm: 'sha512'
passwordHash: '3cc90918-7044-4e55-b61d-92ae73cb261e'
passwordSalt: '22a61686-66f6-483c-a524-185aac251fb0'
testGroup1 = new ContactGroup
_id: "aaa908908bbb98cc1d0809ee"
group: 'group1'
users: [
{
user: 'one@openhim.org'
method: 'email'
}
{
user: 'two@openhim.org'
method: 'email'
maxAlerts: '1 per day'
}
]
testGroup2 = new ContactGroup
_id: "bbb908908ccc98cc1d0888aa"
group: 'group2'
users: [ { user: 'one@openhim.org', method: 'email' } ]
testFailureRate = 50
testChannel = new Channel
name: 'test'
urlPattern: '/test'
allow: '*'
alerts: [
{
condition: 'status'
status: "404"
groups: ['aaa908908bbb98cc1d0809ee']
}
{
condition: 'status'
status: '5xx'
groups: ['bbb908908ccc98cc1d0888aa']
users: [ { user: 'two@openhim.org', method: 'sms' } ]
failureRate: testFailureRate
}
]
disabledChannel = new Channel
name: 'disabled'
urlPattern: '/disabled'
allow: '*'
alerts: [
{
condition: 'status'
status: "404"
groups: ['aaa908908bbb98cc1d0809ee']
}
]
status: 'disabled'
autoRetryChannel = new Channel
name: 'autoretry'
urlPattern: '/autoretry'
allow: '*'
autoRetryEnabled: true
autoRetryPeriodMinutes: 1
autoRetryMaxAttempts: 3
alerts: [
{
condition: 'auto-retry-max-attempted'
groups: ['aaa908908bbb98cc1d0809ee']
}
]
testTransactions = [
new Event
transactionID: 'aaa908908bbb98cc1daaaaa0'
route: 'primary'
event: 'end'
status: 404
new Event
transactionID: 'aaa908908bbb98cc1daaaaa1'
route: 'route'
event: 'end'
status: 404
new Event
transactionID: 'aaa908908bbb98cc1daaaaa2'
route: 'primary'
event: 'end'
status: 400
new Event
transactionID: 'aaa908908bbb98cc1daaaaa3'
route: 'primary'
event: 'end'
status: 500
new Event
transactionID: 'aaa908908bbb98cc1daaaaa4'
route: 'primary'
event: 'end'
status: 500
new Event
transactionID: 'aaa908908bbb98cc1daaaaa5'
route: 'primary'
event: 'end'
status: 500
new Event
transactionID: 'aaa908908bbb98cc1daaaaa6'
route: 'primary'
event: 'end'
status: 404
new Event
transactionID: 'aaa908908bbb98cc1daaaaa7'
route: 'primary'
event: 'end'
status: 404
new Event
transactionID: 'aaa908908bbb98cc1daaaaa8'
route: 'primary'
event: 'end'
status: 500
autoRetryAttempt: 2
new Event
transactionID: 'aaa908908bbb98cc1daaaaa9'
route: 'primary'
event: 'end'
status: 500
autoRetryAttempt: 3
new Event
transactionID: 'aaa908908bbb98cc1daaaaa9'
route: 'channel'
event: 'end'
status: 500
autoRetryAttempt: 3
new Event
transactionID: 'aaa908908bbb98cc1daaaaa9'
route: 'primary'
event: 'end'
status: 200
autoRetryAttempt: 3
]
dateFrom = new Date()
dateFrom.setHours 0, 0, 0, 0
describe "Transaction Alerts", ->
before (done) ->
Event.ensureIndexes ->
Alert.ensureIndexes ->
testUser1.save -> testUser2.save -> testGroup1.save -> testGroup2.save ->
testChannel.save -> disabledChannel.save -> autoRetryChannel.save ->
for testTransaction in testTransactions
testTransaction.channelID = testChannel._id
testTransactions[6].channelID = "000000000000000000000000"
testTransactions[7].channelID = disabledChannel._id
testTransactions[8].channelID = autoRetryChannel._id
testTransactions[9].channelID = autoRetryChannel._id
testTransactions[10].channelID = autoRetryChannel._id
testTransactions[11].channelID = autoRetryChannel._id
done()
after (done) ->
User.remove {}, -> ContactGroup.remove {}, -> Channel.remove {}, -> done()
afterEach (done) ->
Alert.remove {}, ->
Event.remove {}, ->
for testTransaction in testTransactions
testTransaction.isNew = true
delete testTransaction._id
done()
describe "config", ->
it "default config should contain alerting config fields", (done) ->
config.alerts.should.exist
config.alerts.enableAlerts.should.exist
config.alerts.pollPeriodMinutes.should.exist
config.alerts.himInstance.should.exist
config.alerts.consoleURL.should.exist
done()
describe ".findTransactionsMatchingStatus", ->
it "should return transactions that match an exact status", (done) ->
testTransactions[0].save (err) ->
return done err if err
alerts.findTransactionsMatchingStatus testChannel, { condition: 'status', status: "404" }, dateFrom, (err, results) ->
results.length.should.be.exactly 1
results[0]._id.equals(testTransactions[0]._id).should.be.true()
done()
it "should return transactions that have a matching status in a route response", (done) ->
testTransactions[1].save (err) ->
return done err if err
alerts.findTransactionsMatchingStatus testChannel, { condition: 'status', status: "404" }, dateFrom, (err, results) ->
results.length.should.be.exactly 1
results[0]._id.equals(testTransactions[1]._id).should.be.true()
done()
it "should only return transactions for the requested channel", (done) ->
testTransactions[0].save (err) ->
return done err if err
testTransactions[6].save (err) ->
return done err if err
alerts.findTransactionsMatchingStatus testChannel, { condition: 'status', status: "404" }, dateFrom, (err, results) ->
results.length.should.be.exactly 1
results[0]._id.equals(testTransactions[0]._id).should.be.true()
done()
it "should not return transactions that occur before dateFrom", (done) ->
testTransactions[0].save (err) ->
return done err if err
newFrom = moment().add(1, 'days').toDate()
alerts.findTransactionsMatchingStatus testChannel, { condition: 'status', status: "404" }, newFrom, (err, results) ->
results.length.should.be.exactly 0
done()
it "should return all matching transactions for a fuzzy status search for the specified channel", (done) ->
testTransactions[0].save (err) ->
return done err if err
testTransactions[1].save (err) ->
return done err if err
testTransactions[2].save (err) ->
return done err if err
testTransactions[3].save (err) ->
return done err if err
testTransactions[6].save (err) ->
return done err if err
alerts.findTransactionsMatchingStatus testChannel, { condition: 'status', status: "4xx" }, dateFrom, (err, results) ->
results.length.should.be.exactly 3
resultIDs = results.map (result) -> result._id
resultIDs.should.containEql testTransactions[0]._id
resultIDs.should.containEql testTransactions[1]._id
resultIDs.should.containEql testTransactions[2]._id
resultIDs.should.not.containEql testTransactions[6]._id
done()
it "should not return any transactions when their count is below the failure rate", (done) ->
testTransactions[0].save (err) ->
return done err if err
testTransactions[1].save (err) ->
return done err if err
testTransactions[3].save (err) ->
return done err if err
alerts.findTransactionsMatchingStatus testChannel, { condition: 'status', status: "500", failureRate: testFailureRate }, dateFrom, (err, results) ->
results.length.should.be.exactly 0
done()
it "should return transactions when their count is equal to the failure rate", (done) ->
testTransactions[0].save (err) ->
return done err if err
testTransactions[1].save (err) ->
return done err if err
testTransactions[3].save (err) ->
return done err if err
testTransactions[4].save (err) ->
return done err if err
alerts.findTransactionsMatchingStatus testChannel, { condition: 'status', status: "500", failureRate: testFailureRate }, dateFrom, (err, results) ->
results.length.should.be.exactly 2
resultIDs = results.map (result) -> result._id
resultIDs.should.containEql testTransactions[3]._id
resultIDs.should.containEql testTransactions[4]._id
done()
it "should return transactions when their count is above the failure rate", (done) ->
testTransactions[0].save (err) ->
return done err if err
testTransactions[1].save (err) ->
return done err if err
testTransactions[3].save (err) ->
return done err if err
testTransactions[4].save (err) ->
return done err if err
testTransactions[5].save (err) ->
return done err if err
alerts.findTransactionsMatchingStatus testChannel, { condition: 'status', status: "500", failureRate: testFailureRate }, dateFrom, (err, results) ->
results.length.should.be.exactly 3
resultIDs = results.map (result) -> result._id
resultIDs.should.containEql testTransactions[3]._id
resultIDs.should.containEql testTransactions[4]._id
resultIDs.should.containEql testTransactions[5]._id
done()
it "should not return any transactions when the count is equal/above the failure rate, but an alert has already been sent", (done) ->
alert = new Alert
user: 'one@openhim.org'
method: 'email'
channelID: testChannel._id
condition: 'status'
status: '500'
alertStatus: 'Completed'
alert.save (err) ->
testTransactions[0].save (err) ->
return done err if err
testTransactions[1].save (err) ->
return done err if err
testTransactions[3].save (err) ->
return done err if err
testTransactions[4].save (err) ->
return done err if err
alerts.findTransactionsMatchingStatus testChannel, { condition: 'status', status: "500", failureRate: testFailureRate }, dateFrom, (err, results) ->
results.length.should.be.exactly 0
done()
describe ".findTransactionsMaxRetried", ->
it "should not return transactions have not reached max retries", (done) ->
testTransactions[8].save (err) ->
return done err if err
alerts.findTransactionsMaxRetried autoRetryChannel, autoRetryChannel.alerts[0], dateFrom, (err, results) ->
results.length.should.be.exactly 0
done()
it "should return transactions have reached max retries", (done) ->
testTransactions[9].save (err) ->
return done err if err
alerts.findTransactionsMaxRetried autoRetryChannel, autoRetryChannel.alerts[0], dateFrom, (err, results) ->
results.length.should.be.exactly 1
results[0]._id.equals(testTransactions[9]._id).should.be.true()
done()
it "should not return successful transactions that have reached max retries", (done) ->
testTransactions[11].save (err) ->
return done err if err
alerts.findTransactionsMaxRetried autoRetryChannel, autoRetryChannel.alerts[0], dateFrom, (err, results) ->
results.length.should.be.exactly 0
done()
it "should not return duplicate transaction IDs where multiple events exist for the same transaction", (done) ->
testTransactions[9].save (err) ->
return done err if err
testTransactions[10].save (err) ->
return done err if err
alerts.findTransactionsMaxRetried autoRetryChannel, autoRetryChannel.alerts[0], dateFrom, (err, results) ->
results.length.should.be.exactly 1
results[0].transactionID.equals(testTransactions[9].transactionID).should.be.true()
done()
describe ".alertingTask", ->
buildJobStub = (date) ->
jobStub = {}
jobStub.attrs = {}
if date
jobStub.attrs.data = {}
jobStub.attrs.data.lastAlertDate = date
return jobStub
mockContactHandler = (spy, err=null) -> (method, contactAddress, title, messagePlain, messageHTML, callback) ->
spy method, contactAddress, title, messagePlain, messageHTML
callback err
it "should not contact users if there no matching transactions", (done) ->
contactSpy = sinon.spy()
alerts.alertingTask buildJobStub(null), mockContactHandler(contactSpy), ->
contactSpy.called.should.be.false
done()
it "should set the last run date as a job attribute", (done) ->
jobStub = buildJobStub null
contactSpy = sinon.spy()
alerts.alertingTask jobStub, mockContactHandler(contactSpy), ->
jobStub.attrs.data.should.exist
jobStub.attrs.data.lastAlertDate.should.exist
jobStub.attrs.data.lastAlertDate.should.be.instanceof(Date)
done()
it "should contact users when there are matching transactions", (done) ->
contactSpy = sinon.spy()
testTransactions[0].save (err) ->
return done err if err
alerts.alertingTask buildJobStub(dateFrom), mockContactHandler(contactSpy), ->
contactSpy.calledTwice.should.be.true()
contactSpy.withArgs('email', 'one@openhim.org', 'OpenHIM Alert', sinon.match.string, sinon.match.string).calledOnce.should.be.true()
contactSpy.withArgs('email', 'two@openhim.org', 'OpenHIM Alert', sinon.match.string, sinon.match.string).calledOnce.should.be.true()
done()
it "should store an alert log item in mongo for each alert generated", (done) ->
contactSpy = sinon.spy()
testTransactions[0].save (err) ->
return done err if err
alerts.alertingTask buildJobStub(dateFrom), mockContactHandler(contactSpy), ->
contactSpy.called.should.be.true()
Alert.find {}, (err, results) ->
return done err if err
results.length.should.be.exactly 2
resultUsers = results.map (result) -> result.user
resultUsers.should.containEql testUser1.email
resultUsers.should.containEql testUser2.email
done()
it "should contact users using their specified method", (done) ->
contactSpy = sinon.spy()
testTransactions[3].save (err) ->
return done err if err
testTransactions[4].save (err) ->
return done err if err
alerts.alertingTask buildJobStub(dateFrom), mockContactHandler(contactSpy), ->
contactSpy.calledTwice.should.be.true()
contactSpy.withArgs('email', testUser1.email, 'OpenHIM Alert', sinon.match.string, sinon.match.string).calledOnce.should.be.true()
contactSpy.withArgs('sms', testUser2.msisdn, 'OpenHIM Alert', sinon.match.string, null).calledOnce.should.be.true()
done()
it "should not send alerts to users with a maxAlerts restriction if they've already received an alert for the same day", (done) ->
contactSpy = sinon.spy()
testTransactions[0].save (err) ->
return done err if err
alerts.alertingTask buildJobStub(dateFrom), mockContactHandler(contactSpy), ->
contactSpy.calledTwice.should.be.true()
secondSpy = sinon.spy()
alerts.alertingTask buildJobStub(dateFrom), mockContactHandler(secondSpy), ->
secondSpy.calledOnce.should.be.true()
secondSpy.withArgs('email', testUser1.email, 'OpenHIM Alert', sinon.match.string, sinon.match.string).calledOnce.should.be.true()
done()
it "should send alerts to users if an alert for the same day was already attempted but it failed", (done) ->
contactSpy = sinon.spy()
testTransactions[0].save (err) ->
return done err if err
alerts.alertingTask buildJobStub(dateFrom), mockContactHandler(contactSpy, "Test Failure"), ->
contactSpy.calledTwice.should.be.true()
secondSpy = sinon.spy()
alerts.alertingTask buildJobStub(dateFrom), mockContactHandler(secondSpy), ->
secondSpy.calledTwice.should.be.true()
secondSpy.withArgs('email', 'one@openhim.org', 'OpenHIM Alert', sinon.match.string, sinon.match.string).calledOnce.should.be.true()
secondSpy.withArgs('email', 'two@openhim.org', 'OpenHIM Alert', sinon.match.string, sinon.match.string).calledOnce.should.be.true()
done()
it "should not generate alerts for disabled channels", (done) ->
contactSpy = sinon.spy()
testTransactions[0].save (err) ->
return done err if err
testTransactions[7].save (err) ->
return done err if err
alerts.alertingTask buildJobStub(dateFrom), mockContactHandler(contactSpy), ->
contactSpy.called.should.be.true()
Alert.find {}, (err, results) ->
return done err if err
results.length.should.be.exactly 2
resultUsers = results.map (result) -> result.user
resultUsers.should.containEql testUser1.email
resultUsers.should.containEql testUser2.email
resultChannels = results.map (result) -> result.channelID
resultChannels.should.containEql testChannel._id.toHexString()
resultChannels.should.not.containEql disabledChannel._id.toHexString()
done()
it "should contact users when there are matching max auto retried transactions", (done) ->
contactSpy = sinon.spy()
testTransactions[9].save (err) ->
return done err if err
alerts.alertingTask buildJobStub(dateFrom), mockContactHandler(contactSpy), ->
contactSpy.calledTwice.should.be.true()
contactSpy.withArgs('email', 'one@openhim.org', 'OpenHIM Alert', sinon.match.string, sinon.match.string).calledOnce.should.be.true()
contactSpy.withArgs('email', 'two@openhim.org', 'OpenHIM Alert', sinon.match.string, sinon.match.string).calledOnce.should.be.true()
done()