Feat: code refactoring and jest tests
chore: More refactoring chore: More tests and refactors fix: Fixed age not getting calculated properly chore: Implemented mocking in jest chore: More mock tests chore: checkStuckScreen tests
This commit is contained in:
parent
65165b943b
commit
04fd82454d
12 changed files with 1047 additions and 66 deletions
34
lib/notifier/codes.js
Normal file
34
lib/notifier/codes.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
const T = require('../time')
|
||||
|
||||
const PING = 'PING'
|
||||
const STALE = 'STALE'
|
||||
const LOW_CRYPTO_BALANCE = 'LOW_CRYPTO_BALANCE'
|
||||
const HIGH_CRYPTO_BALANCE = 'HIGH_CRYPTO_BALANCE'
|
||||
const CASH_BOX_FULL = 'CASH_BOX_FULL'
|
||||
const LOW_CASH_OUT = 'LOW_CASH_OUT'
|
||||
|
||||
const CODES_DISPLAY = {
|
||||
PING: 'Machine Down',
|
||||
STALE: 'Machine Stuck',
|
||||
LOW_CRYPTO_BALANCE: 'Low Crypto Balance',
|
||||
HIGH_CRYPTO_BALANCE: 'High Crypto Balance',
|
||||
CASH_BOX_FULL: 'Cash box full',
|
||||
LOW_CASH_OUT: 'Low Cash-out'
|
||||
}
|
||||
|
||||
const NETWORK_DOWN_TIME = 1 * T.minute
|
||||
const STALE_STATE = 7 * T.minute
|
||||
const ALERT_SEND_INTERVAL = T.hour
|
||||
|
||||
module.exports = {
|
||||
PING,
|
||||
STALE,
|
||||
LOW_CRYPTO_BALANCE,
|
||||
HIGH_CRYPTO_BALANCE,
|
||||
CASH_BOX_FULL,
|
||||
LOW_CASH_OUT,
|
||||
CODES_DISPLAY,
|
||||
NETWORK_DOWN_TIME,
|
||||
STALE_STATE,
|
||||
ALERT_SEND_INTERVAL
|
||||
}
|
||||
100
lib/notifier/email.js
Normal file
100
lib/notifier/email.js
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
const _ = require('lodash/fp')
|
||||
const prettyMs = require('pretty-ms')
|
||||
const {
|
||||
PING,
|
||||
STALE,
|
||||
LOW_CRYPTO_BALANCE,
|
||||
HIGH_CRYPTO_BALANCE,
|
||||
CASH_BOX_FULL,
|
||||
LOW_CASH_OUT
|
||||
} = require('./codes')
|
||||
|
||||
function alertSubject(alertRec, config) {
|
||||
let alerts = []
|
||||
|
||||
if (config.balance) {
|
||||
alerts = _.concat(alerts, alertRec.general)
|
||||
}
|
||||
|
||||
_.keys(alertRec.devices).forEach(function (device) {
|
||||
if (config.balance) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
|
||||
}
|
||||
|
||||
if (config.errors) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
|
||||
}
|
||||
})
|
||||
|
||||
if (alerts.length === 0) return null
|
||||
|
||||
const alertTypes = _.map(codeDisplay, _.uniq(_.map('code', alerts))).sort()
|
||||
return '[Lamassu] Errors reported: ' + alertTypes.join(', ')
|
||||
}
|
||||
|
||||
function printEmailAlerts(alertRec, config) {
|
||||
let body = 'Errors were reported by your Lamassu Machines.\n'
|
||||
|
||||
if (config.balance && alertRec.general.length !== 0) {
|
||||
body = body + '\nGeneral errors:\n'
|
||||
body = body + emailAlerts(alertRec.general) + '\n'
|
||||
}
|
||||
|
||||
_.keys(alertRec.devices).forEach(function (device) {
|
||||
const deviceName = alertRec.deviceNames[device]
|
||||
body = body + '\nErrors for ' + deviceName + ':\n'
|
||||
|
||||
let alerts = []
|
||||
if (config.balance) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
|
||||
}
|
||||
|
||||
if (config.errors) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
|
||||
}
|
||||
|
||||
body = body + emailAlerts(alerts)
|
||||
})
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
function emailAlerts(alerts) {
|
||||
return alerts.map(emailAlert).join('\n') + '\n'
|
||||
}
|
||||
|
||||
function formatCurrency(num, code) {
|
||||
return numeral(num).format('0,0.00') + ' ' + code
|
||||
}
|
||||
|
||||
function emailAlert(alert) {
|
||||
switch (alert.code) {
|
||||
case PING:
|
||||
if (alert.age) {
|
||||
const pingAge = prettyMs(alert.age, { compact: true, verbose: true })
|
||||
return `Machine down for ${pingAge}`
|
||||
}
|
||||
return 'Machine down for a while.'
|
||||
case STALE: {
|
||||
const stuckAge = prettyMs(alert.age, { compact: true, verbose: true })
|
||||
return `Machine is stuck on ${alert.state} screen for ${stuckAge}`
|
||||
}
|
||||
case LOW_CRYPTO_BALANCE: {
|
||||
const balance = formatCurrency(alert.fiatBalance.balance, alert.fiatCode)
|
||||
return `Low balance in ${alert.cryptoCode} [${balance}]`
|
||||
}
|
||||
case HIGH_CRYPTO_BALANCE: {
|
||||
const highBalance = formatCurrency(
|
||||
alert.fiatBalance.balance,
|
||||
alert.fiatCode
|
||||
)
|
||||
return `High balance in ${alert.cryptoCode} [${highBalance}]`
|
||||
}
|
||||
case CASH_BOX_FULL:
|
||||
return `Cash box full on ${alert.machineName} [${alert.notes} banknotes]`
|
||||
case LOW_CASH_OUT:
|
||||
return `Cassette for ${alert.denomination} ${alert.fiatCode} low [${alert.notes} banknotes]`
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { alertSubject, printEmailAlerts }
|
||||
139
lib/notifier/index.js
Normal file
139
lib/notifier/index.js
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
const { STALE, STALE_STATE } = require('./codes')
|
||||
|
||||
const _ = require('lodash/fp')
|
||||
const queries = require('./queries')
|
||||
const logger = require('../logger')
|
||||
|
||||
const utils = require('./utils')
|
||||
const emailFuncs = require('./email')
|
||||
const smsFuncs = require('./sms')
|
||||
|
||||
function buildMessage(alerts, notifications) {
|
||||
const smsEnabled = utils.isActive(notifications.sms)
|
||||
const emailEnabled = utils.isActive(notifications.email)
|
||||
|
||||
let rec = {}
|
||||
if (smsEnabled) {
|
||||
rec = _.set(['sms', 'body'])(
|
||||
smsFuncs.printSmsAlerts(alerts, notifications.sms)
|
||||
)(rec)
|
||||
}
|
||||
if (emailEnabled) {
|
||||
rec = _.set(['email', 'subject'])(
|
||||
emailFuncs.alertSubject(alerts, notifications.email)
|
||||
)(rec)
|
||||
rec = _.set(['email', 'body'])(
|
||||
emailFuncs.printEmailAlerts(alerts, notifications.email)
|
||||
)(rec)
|
||||
}
|
||||
|
||||
return rec
|
||||
}
|
||||
|
||||
function checkNotification(plugins) {
|
||||
const notifications = plugins.getNotificationConfig()
|
||||
const smsEnabled = utils.isActive(notifications.sms)
|
||||
const emailEnabled = utils.isActive(notifications.email)
|
||||
|
||||
if (!smsEnabled && !emailEnabled) return Promise.resolve()
|
||||
|
||||
return getAlerts(plugins)
|
||||
.then(alerts => {
|
||||
const currentAlertFingerprint = utils.buildAlertFingerprint(
|
||||
alerts,
|
||||
notifications
|
||||
)
|
||||
if (!currentAlertFingerprint) {
|
||||
const inAlert = !!utils.getAlertFingerprint()
|
||||
// (fingerprint = null, lastAlertTime = null)
|
||||
utils.setAlertFingerprint(null, null)
|
||||
if (inAlert) {
|
||||
return utils.sendNoAlerts(plugins, smsEnabled, emailEnabled)
|
||||
}
|
||||
}
|
||||
if (utils.shouldNotAlert(currentAlertFingerprint)) {
|
||||
return
|
||||
}
|
||||
|
||||
const message = buildMessage(alerts, notifications)
|
||||
utils.setAlertFingerprint(currentAlertFingerprint, Date.now())
|
||||
return plugins.sendMessage(message)
|
||||
})
|
||||
.then(results => {
|
||||
if (results && results.length > 0)
|
||||
logger.debug('Successfully sent alerts')
|
||||
})
|
||||
.catch(logger.error)
|
||||
}
|
||||
|
||||
function getAlerts(plugins) {
|
||||
return Promise.all([
|
||||
plugins.checkBalances(),
|
||||
queries.machineEvents(),
|
||||
plugins.getMachineNames()
|
||||
]).then(([balances, events, devices]) =>
|
||||
buildAlerts(checkPings(devices), balances, events, devices)
|
||||
)
|
||||
}
|
||||
|
||||
function buildAlerts(pings, balances, events, devices) {
|
||||
const alerts = { devices: {}, deviceNames: {} }
|
||||
alerts.general = _.filter(r => !r.deviceId, balances)
|
||||
devices.forEach(function (device) {
|
||||
const deviceId = device.deviceId
|
||||
const deviceName = device.name
|
||||
const deviceEvents = events.filter(function (eventRow) {
|
||||
return eventRow.device_id === deviceId
|
||||
})
|
||||
const ping = pings[deviceId] || []
|
||||
const stuckScreen = checkStuckScreen(deviceEvents, deviceName)
|
||||
|
||||
if (!alerts.devices[deviceId]) alerts.devices[deviceId] = {}
|
||||
alerts.devices[deviceId].balanceAlerts = _.filter(
|
||||
['deviceId', deviceId],
|
||||
balances
|
||||
)
|
||||
alerts.devices[deviceId].deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping
|
||||
|
||||
alerts.deviceNames[deviceId] = deviceName
|
||||
})
|
||||
return alerts
|
||||
}
|
||||
|
||||
function checkPings(devices) {
|
||||
const deviceIds = _.map('deviceId', devices)
|
||||
const pings = _.map(utils.checkPing, devices)
|
||||
return _.zipObject(deviceIds)(pings)
|
||||
}
|
||||
|
||||
function checkStuckScreen(deviceEvents, machineName) {
|
||||
const sortedEvents = _.sortBy(
|
||||
utils.getDeviceTime,
|
||||
_.map(utils.parseEventNote, deviceEvents)
|
||||
)
|
||||
const lastEvent = _.last(sortedEvents)
|
||||
|
||||
if (!lastEvent) {
|
||||
return []
|
||||
}
|
||||
|
||||
const state = lastEvent.note.state
|
||||
const isIdle = lastEvent.note.isIdle
|
||||
|
||||
if (isIdle) {
|
||||
return []
|
||||
}
|
||||
|
||||
const age = Math.floor(lastEvent.age)
|
||||
if (age > STALE_STATE) {
|
||||
return [{ code: STALE, state, age, machineName }]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkNotification,
|
||||
checkPings,
|
||||
checkStuckScreen
|
||||
}
|
||||
3
lib/notifier/queries.js
Normal file
3
lib/notifier/queries.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
const dbm = require('../postgresql_interface')
|
||||
|
||||
module.exports = { machineEvents: dbm.machineEvents }
|
||||
53
lib/notifier/sms.js
Normal file
53
lib/notifier/sms.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
const _ = require('lodash/fp')
|
||||
const utils = require('./utils')
|
||||
|
||||
function printSmsAlerts(alertRec, config) {
|
||||
let alerts = []
|
||||
|
||||
if (config.balance) {
|
||||
alerts = _.concat(alerts, alertRec.general)
|
||||
}
|
||||
|
||||
_.keys(alertRec.devices).forEach(function (device) {
|
||||
if (config.balance) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
|
||||
}
|
||||
|
||||
if (config.errors) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
|
||||
}
|
||||
})
|
||||
|
||||
if (alerts.length === 0) return null
|
||||
|
||||
const alertsMap = _.groupBy('code', alerts)
|
||||
|
||||
const alertTypes = _.map(entry => {
|
||||
const code = entry[0]
|
||||
const machineNames = _.filter(
|
||||
_.negate(_.isEmpty),
|
||||
_.map('machineName', entry[1])
|
||||
)
|
||||
|
||||
return {
|
||||
codeDisplay: utils.codeDisplay(code),
|
||||
machineNames
|
||||
}
|
||||
}, _.toPairs(alertsMap))
|
||||
|
||||
const mapByCodeDisplay = _.map(it =>
|
||||
_.isEmpty(it.machineNames)
|
||||
? it.codeDisplay
|
||||
: `${it.codeDisplay} (${it.machineNames.join(', ')})`
|
||||
)
|
||||
|
||||
const displayAlertTypes = _.compose(
|
||||
_.uniq,
|
||||
mapByCodeDisplay,
|
||||
_.sortBy('codeDisplay')
|
||||
)(alertTypes)
|
||||
|
||||
return '[Lamassu] Errors reported: ' + displayAlertTypes.join(', ')
|
||||
}
|
||||
|
||||
module.exports = { printSmsAlerts }
|
||||
28
lib/notifier/test/email.test.js
Normal file
28
lib/notifier/test/email.test.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
const email = require('../email')
|
||||
|
||||
const alertRec = {
|
||||
devices: {
|
||||
f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: {
|
||||
balanceAlerts: [],
|
||||
deviceAlerts: [
|
||||
{ code: 'PING', age: 602784301.446, machineName: 'Abc123' }
|
||||
]
|
||||
}
|
||||
},
|
||||
deviceNames: {
|
||||
f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: 'Abc123'
|
||||
},
|
||||
general: []
|
||||
}
|
||||
|
||||
const printEmailMsg = `Errors were reported by your Lamassu Machines.
|
||||
|
||||
Errors for Abc123:
|
||||
Machine down for ~6 days
|
||||
`
|
||||
|
||||
test('Print Email Alers', () => {
|
||||
expect(email.printEmailAlerts(alertRec, { active: true, errors: true })).toBe(
|
||||
printEmailMsg
|
||||
)
|
||||
})
|
||||
234
lib/notifier/test/notifier.test.js
Normal file
234
lib/notifier/test/notifier.test.js
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
const notifier = require('..')
|
||||
|
||||
// mock plugins object with mock data to test functions
|
||||
const plugins = {
|
||||
sendMessage: rec => {
|
||||
return rec
|
||||
},
|
||||
getNotificationConfig: () => ({
|
||||
email_active: false,
|
||||
sms_active: true,
|
||||
email_errors: false,
|
||||
sms_errors: true,
|
||||
sms: { active: true, errors: true },
|
||||
email: { active: false, errors: false }
|
||||
}),
|
||||
getMachineNames: () => [
|
||||
{
|
||||
deviceId:
|
||||
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
cashbox: 0,
|
||||
cassette1: 444,
|
||||
cassette2: 222,
|
||||
version: '7.5.0-beta.0',
|
||||
model: 'unknown',
|
||||
pairedAt: '2020-11-13T16:20:31.624Z',
|
||||
lastPing: '2020-11-16T13:11:03.169Z',
|
||||
name: 'Abc123',
|
||||
paired: true,
|
||||
cashOut: true,
|
||||
statuses: [{ label: 'Unresponsive', type: 'error' }]
|
||||
}
|
||||
],
|
||||
checkBalances: () => []
|
||||
}
|
||||
|
||||
const devices = [
|
||||
{
|
||||
deviceId:
|
||||
'7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4',
|
||||
lastPing: '2020-11-16T13:11:03.169Z',
|
||||
name: 'Abc123'
|
||||
},
|
||||
{
|
||||
deviceId:
|
||||
'9871e58aa2643ff9445cbc299b50397430ada75157d6c29b4c93548fff0f48f7',
|
||||
lastPing: '2020-11-16T16:21:35.948Z',
|
||||
name: 'Machine 2'
|
||||
},
|
||||
{
|
||||
deviceId:
|
||||
'5ae0d02dedeb77b6521bd5eb7c9159bdc025873fa0bcb6f87aaddfbda0c50913',
|
||||
lastPing: '2020-11-19T15:07:57.089Z',
|
||||
name: 'Machine 3'
|
||||
},
|
||||
{
|
||||
deviceId:
|
||||
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
lastPing: '2020-11-23T19:34:41.031Z',
|
||||
name: 'New Machine 4 '
|
||||
}
|
||||
]
|
||||
|
||||
test('Exits checkNotifications with Promise.resolve() if SMS and Email are disabled', async () => {
|
||||
expect.assertions(1)
|
||||
await expect(
|
||||
notifier.checkNotification({
|
||||
getNotificationConfig: () => ({
|
||||
sms: { active: false, errors: false },
|
||||
email: { active: false, errors: false }
|
||||
})
|
||||
})
|
||||
).resolves.toBe(undefined)
|
||||
})
|
||||
|
||||
test('Exits checkNotifications with Promise.resolve() if SMS and Email are disabled even if errors or balance are defined to something', async () => {
|
||||
expect.assertions(1)
|
||||
await expect(
|
||||
notifier.checkNotification({
|
||||
getNotificationConfig: () => ({
|
||||
sms: { active: false, errors: true, balance: true },
|
||||
email: { active: false, errors: true, balance: true }
|
||||
})
|
||||
})
|
||||
).resolves.toBe(undefined)
|
||||
})
|
||||
|
||||
test("Check Pings should return code PING for devices that haven't been pinged recently", () => {
|
||||
expect(
|
||||
notifier.checkPings([
|
||||
{
|
||||
deviceId:
|
||||
'7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4',
|
||||
lastPing: '2020-11-16T13:11:03.169Z',
|
||||
name: 'Abc123'
|
||||
}
|
||||
])
|
||||
).toMatchObject({
|
||||
'7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4': [
|
||||
{ code: 'PING', machineName: 'Abc123' }
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
test('Checkpings returns empty array as the value for the id prop, if the lastPing is more recent than 60 seconds', () => {
|
||||
expect(
|
||||
notifier.checkPings([
|
||||
{
|
||||
deviceId:
|
||||
'7a531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4',
|
||||
lastPing: new Date(),
|
||||
name: 'Abc123'
|
||||
}
|
||||
])
|
||||
).toMatchObject({
|
||||
'7a531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4': []
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// https://stackoverflow.com/questions/58151010/difference-between-resetallmocks-resetmodules-resetmoduleregistry-restoreallm
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
const utils = require('../utils')
|
||||
|
||||
test('Check notification resolves to undefined if shouldNotAlert is called and is true', async () => {
|
||||
const mockShouldNotAlert = jest.spyOn(utils, 'shouldNotAlert')
|
||||
mockShouldNotAlert.mockReturnValue(true)
|
||||
|
||||
const result = await notifier.checkNotification(plugins)
|
||||
expect(mockShouldNotAlert).toHaveBeenCalledTimes(1)
|
||||
expect(result).toBe(undefined)
|
||||
})
|
||||
|
||||
test('Sendmessage is called if shouldNotAlert is called and is false', async () => {
|
||||
const mockShouldNotAlert = jest.spyOn(utils, 'shouldNotAlert')
|
||||
const mockSendMessage = jest.spyOn(plugins, 'sendMessage')
|
||||
|
||||
mockShouldNotAlert.mockReturnValue(false)
|
||||
const result = await notifier.checkNotification(plugins)
|
||||
|
||||
expect(mockShouldNotAlert).toHaveBeenCalledTimes(1)
|
||||
expect(mockSendMessage).toHaveBeenCalledTimes(1)
|
||||
expect(result).toBe(undefined)
|
||||
})
|
||||
|
||||
test('If no alert fingerprint and inAlert is true, exits on call to sendNoAlerts', async () => {
|
||||
// mock utils.buildAlertFingerprint to return null
|
||||
// mock utils.getAlertFingerprint to be true which will make inAlert true
|
||||
|
||||
const buildFp = jest.spyOn(utils, 'buildAlertFingerprint')
|
||||
const mockGetFp = jest.spyOn(utils, 'getAlertFingerprint')
|
||||
const mockSendNoAlerts = jest.spyOn(utils, 'sendNoAlerts')
|
||||
|
||||
buildFp.mockReturnValue(null)
|
||||
mockGetFp.mockReturnValue(true)
|
||||
await notifier.checkNotification(plugins)
|
||||
|
||||
expect(mockGetFp).toHaveBeenCalledTimes(1)
|
||||
expect(mockSendNoAlerts).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
|
||||
// vvv tests for checkstuckscreen...
|
||||
test('checkStuckScreen returns [] when no events are found', () => {
|
||||
expect(notifier.checkStuckScreen([], 'Abc123')).toEqual([])
|
||||
})
|
||||
|
||||
test('checkStuckScreen returns [] if most recent event is idle', () => {
|
||||
// device_time is what matters for the sorting of the events by recency
|
||||
expect(notifier.checkStuckScreen([{
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":false}',
|
||||
created: "2020-11-23T19:30:29.209Z",
|
||||
device_time: "1999-11-23T19:30:29.177Z",
|
||||
age: 157352628.123
|
||||
}, {
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":true}',
|
||||
created: "2020-11-23T19:30:29.209Z",
|
||||
device_time: "2020-11-23T19:30:29.177Z",
|
||||
age: 157352628.123
|
||||
}])).toEqual([])
|
||||
})
|
||||
|
||||
test('checkStuckScreen returns object array of length 1 with prop code: "STALE" if age > STALE_STATE', () => {
|
||||
// there is an age 0 and an isIdle true in the first object but it will be below the second one in the sorting order and thus ignored
|
||||
const result = notifier.checkStuckScreen([{
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":true}',
|
||||
created: "2020-11-23T19:30:29.209Z",
|
||||
device_time: "1999-11-23T19:30:29.177Z",
|
||||
age: 0
|
||||
}, {
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":false}',
|
||||
created: "2020-11-23T19:30:29.209Z",
|
||||
device_time: "2020-11-23T19:30:29.177Z",
|
||||
age: 157352628.123
|
||||
}])
|
||||
expect(result[0]).toMatchObject({code: "STALE"})
|
||||
})
|
||||
|
||||
test('checkStuckScreen returns empty array if age < STALE_STATE', () => {
|
||||
const STALE_STATE = require("../codes").STALE_STATE
|
||||
const result1 = notifier.checkStuckScreen([{
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":false}',
|
||||
created: "2020-11-23T19:30:29.209Z",
|
||||
device_time: "2020-11-23T19:30:29.177Z",
|
||||
age: 0
|
||||
}])
|
||||
const result2 = notifier.checkStuckScreen([{
|
||||
id: '48ae51c6-c5b4-485e-b81d-aa337fc025e2',
|
||||
device_id: 'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
event_type: 'stateChange',
|
||||
note: '{"state":"chooseCoin","isIdle":false}',
|
||||
created: "2020-11-23T19:30:29.209Z",
|
||||
device_time: "2020-11-23T19:30:29.177Z",
|
||||
age: STALE_STATE
|
||||
}])
|
||||
expect(result1).toEqual([])
|
||||
expect(result2).toEqual([])
|
||||
})
|
||||
22
lib/notifier/test/sms.test.js
Normal file
22
lib/notifier/test/sms.test.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const sms = require('../sms')
|
||||
|
||||
const alertRec = {
|
||||
devices: {
|
||||
f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: {
|
||||
balanceAlerts: [],
|
||||
deviceAlerts: [
|
||||
{ code: 'PING', age: 602784301.446, machineName: 'Abc123' }
|
||||
]
|
||||
}
|
||||
},
|
||||
deviceNames: {
|
||||
f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: 'Abc123'
|
||||
},
|
||||
general: []
|
||||
}
|
||||
|
||||
test('Print SMS alerts', () => {
|
||||
expect(sms.printSmsAlerts(alertRec, { active: true, errors: true })).toBe(
|
||||
'[Lamassu] Errors reported: Machine Down (Abc123)'
|
||||
)
|
||||
})
|
||||
100
lib/notifier/test/utils.test.js
Normal file
100
lib/notifier/test/utils.test.js
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
const utils = require('../utils')
|
||||
|
||||
const plugins = {
|
||||
sendMessage: rec => {
|
||||
return rec
|
||||
}
|
||||
}
|
||||
|
||||
const alertRec = {
|
||||
devices: {
|
||||
f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: {
|
||||
balanceAlerts: [],
|
||||
deviceAlerts: [
|
||||
{ code: 'PING', age: 1605532263169, machineName: 'Abc123' }
|
||||
]
|
||||
}
|
||||
},
|
||||
deviceNames: {
|
||||
f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05: 'Abc123'
|
||||
},
|
||||
general: []
|
||||
}
|
||||
|
||||
const notifications = {
|
||||
sms: { active: true, errors: true },
|
||||
email: { active: false, errors: false }
|
||||
}
|
||||
|
||||
test('Build alert fingerprint returns null if no sms or email alerts', () => {
|
||||
expect(
|
||||
utils.buildAlertFingerprint(
|
||||
{
|
||||
devices: {},
|
||||
deviceNames: {},
|
||||
general: []
|
||||
},
|
||||
notifications
|
||||
)
|
||||
).toBe(null)
|
||||
})
|
||||
|
||||
test('Build alert fingerprint returns null if sms and email are disabled', () => {
|
||||
expect(
|
||||
utils.buildAlertFingerprint(alertRec, {
|
||||
sms: { active: false, errors: true },
|
||||
email: { active: false, errors: false }
|
||||
})
|
||||
).toBe(null)
|
||||
})
|
||||
|
||||
test('Build alert fingerprint returns hash if email or [sms] are enabled and there are alerts in alertrec', () => {
|
||||
expect(
|
||||
typeof utils.buildAlertFingerprint(alertRec, {
|
||||
sms: { active: true, errors: true },
|
||||
email: { active: false, errors: false }
|
||||
})
|
||||
).toBe('string')
|
||||
})
|
||||
|
||||
test('Build alert fingerprint returns hash if [email] or sms are enabled and there are alerts in alertrec', () => {
|
||||
expect(
|
||||
typeof utils.buildAlertFingerprint(alertRec, {
|
||||
sms: { active: false, errors: false },
|
||||
email: { active: true, errors: true }
|
||||
})
|
||||
).toBe('string')
|
||||
})
|
||||
|
||||
test('Send no alerts returns empty object with sms and email disabled', () => {
|
||||
expect(utils.sendNoAlerts(plugins, false, false)).toEqual({})
|
||||
})
|
||||
|
||||
test('Send no alerts returns object with sms prop with sms only enabled', () => {
|
||||
expect(utils.sendNoAlerts(plugins, true, false)).toEqual({
|
||||
sms: {
|
||||
body: '[Lamassu] All clear'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('Send no alerts returns object with sms and email prop with both enabled', () => {
|
||||
expect(utils.sendNoAlerts(plugins, true, true)).toEqual({
|
||||
email: {
|
||||
body: 'No errors are reported for your machines.',
|
||||
subject: '[Lamassu] All clear'
|
||||
},
|
||||
sms: {
|
||||
body: '[Lamassu] All clear'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('Send no alerts returns object with email prop if only email is enabled', () => {
|
||||
expect(utils.sendNoAlerts(plugins, false, true)).toEqual({
|
||||
email: {
|
||||
body: 'No errors are reported for your machines.',
|
||||
subject: '[Lamassu] All clear'
|
||||
}
|
||||
})
|
||||
})
|
||||
111
lib/notifier/utils.js
Normal file
111
lib/notifier/utils.js
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
const _ = require('lodash/fp')
|
||||
const crypto = require('crypto')
|
||||
const {
|
||||
CODES_DISPLAY,
|
||||
NETWORK_DOWN_TIME,
|
||||
PING,
|
||||
ALERT_SEND_INTERVAL
|
||||
} = require('./codes')
|
||||
|
||||
function parseEventNote(event) {
|
||||
return _.set('note', JSON.parse(event.note), event)
|
||||
}
|
||||
|
||||
function checkPing(device) {
|
||||
const age = +Date.now() - +new Date(device.lastPing)
|
||||
if (age > NETWORK_DOWN_TIME)
|
||||
return [{ code: PING, age, machineName: device.name }]
|
||||
return []
|
||||
}
|
||||
|
||||
const getDeviceTime = _.flow(_.get('device_time'), Date.parse)
|
||||
|
||||
const isActive = it => it.active && (it.balance || it.errors)
|
||||
|
||||
const codeDisplay = code => CODES_DISPLAY[code]
|
||||
|
||||
const alertFingerprint = {
|
||||
fingerprint: null,
|
||||
lastAlertTime: null
|
||||
}
|
||||
|
||||
const getAlertFingerprint = () => alertFingerprint.fingerprint
|
||||
|
||||
const getLastAlertTime = () => alertFingerprint.lastAlertTime
|
||||
|
||||
const setAlertFingerprint = (fp, time = Date.now()) => {
|
||||
alertFingerprint.fingerprint = fp
|
||||
alertFingerprint.lastAlertTime = time
|
||||
}
|
||||
|
||||
const shouldNotAlert = currentAlertFingerprint => {
|
||||
return (
|
||||
currentAlertFingerprint === getAlertFingerprint() &&
|
||||
getLastAlertTime() - Date.now() < ALERT_SEND_INTERVAL
|
||||
)
|
||||
}
|
||||
|
||||
function getAlertTypes(alertRec, config) {
|
||||
let alerts = []
|
||||
if (!isActive(config)) return alerts
|
||||
|
||||
if (config.balance) {
|
||||
alerts = _.concat(alerts, alertRec.general)
|
||||
}
|
||||
|
||||
_.keys(alertRec.devices).forEach(function (device) {
|
||||
if (config.balance) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].balanceAlerts)
|
||||
}
|
||||
|
||||
if (config.errors) {
|
||||
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
|
||||
}
|
||||
})
|
||||
|
||||
return alerts
|
||||
}
|
||||
|
||||
function buildAlertFingerprint(alertRec, notifications) {
|
||||
const sms = getAlertTypes(alertRec, notifications.sms)
|
||||
const email = getAlertTypes(alertRec, notifications.email)
|
||||
if (sms.length === 0 && email.length === 0) return null
|
||||
|
||||
const smsTypes = _.map(codeDisplay, _.uniq(_.map('code', sms))).sort()
|
||||
const emailTypes = _.map(codeDisplay, _.uniq(_.map('code', email))).sort()
|
||||
|
||||
const subject = _.concat(smsTypes, emailTypes).join(', ')
|
||||
return crypto.createHash('sha256').update(subject).digest('hex')
|
||||
}
|
||||
|
||||
function sendNoAlerts(plugins, smsEnabled, emailEnabled) {
|
||||
const subject = '[Lamassu] All clear'
|
||||
|
||||
let rec = {}
|
||||
if (smsEnabled) {
|
||||
rec = _.set(['sms', 'body'])(subject)(rec)
|
||||
}
|
||||
|
||||
if (emailEnabled) {
|
||||
rec = _.set(['email', 'subject'])(subject)(rec)
|
||||
rec = _.set(['email', 'body'])('No errors are reported for your machines.')(
|
||||
rec
|
||||
)
|
||||
}
|
||||
|
||||
return plugins.sendMessage(rec)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
codeDisplay,
|
||||
parseEventNote,
|
||||
getDeviceTime,
|
||||
checkPing,
|
||||
isActive,
|
||||
getAlertFingerprint,
|
||||
getLastAlertTime,
|
||||
setAlertFingerprint,
|
||||
shouldNotAlert,
|
||||
buildAlertFingerprint,
|
||||
sendNoAlerts
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue