Test monthly email script, add limit
* Move the script logic into functions so that running and initialization can be skipped in tests. * Add a test to send an email to the test subscriber * Add MONTHLY_CRON_LIMIT sets the maximum number of emails to send. * Refactor the database functions to allow a limit and a count of total subscribers.
This commit is contained in:
Родитель
d0b370eb5f
Коммит
c58bbdb041
|
@ -57,7 +57,8 @@ const optionalEnvVars = [
|
|||
'EDUCATION_VIDEO_URL_RELAY',
|
||||
'EDUCATION_VIDEO_URL_VPN',
|
||||
'ADMINS',
|
||||
'MONTHLY_CRON_ENABLED'
|
||||
'MONTHLY_CRON_ENABLED',
|
||||
'MONTHLY_CRON_LIMIT'
|
||||
]
|
||||
|
||||
const AppConstants = { }
|
||||
|
|
26
db/DB.js
26
db/DB.js
|
@ -439,15 +439,27 @@ const DB = {
|
|||
await knex('subscribers').update('monthly_email_optout', true).where('primary_verification_token', token)
|
||||
},
|
||||
|
||||
async getSubscribersWithUnresolvedBreaches () {
|
||||
const res = await knex('subscribers')
|
||||
.select('primary_email', 'primary_verification_token', 'breach_stats', 'signup_language')
|
||||
getSubscribersWithUnresolvedBreachesQuery () {
|
||||
return knex('subscribers')
|
||||
.whereRaw('monthly_email_optout IS NOT TRUE')
|
||||
.whereRaw("greatest(created_at, monthly_email_at) < (now() - interval '30 days')")
|
||||
// .whereJsonPath('breach_stats', '$.numBreaches.numUnresolved', '>', 0) // Requires psql 11
|
||||
.whereRaw("(breach_stats #>> '{numBreaches, numUnresolved}')::int > 0")
|
||||
},
|
||||
|
||||
return res
|
||||
async getSubscribersWithUnresolvedBreaches (limit = 0) {
|
||||
let query = this.getSubscribersWithUnresolvedBreachesQuery()
|
||||
.select('primary_email', 'primary_verification_token', 'breach_stats', 'signup_language')
|
||||
if (limit && limit > 0) {
|
||||
query = query.limit(limit).orderBy('created_at')
|
||||
}
|
||||
return await query
|
||||
},
|
||||
|
||||
async getSubscribersWithUnresolvedBreachesCount () {
|
||||
const query = this.getSubscribersWithUnresolvedBreachesQuery()
|
||||
const count = parseInt((await query.count({ count: '*' }))[0].count)
|
||||
return count
|
||||
},
|
||||
|
||||
async createConnection () {
|
||||
|
@ -457,8 +469,10 @@ const DB = {
|
|||
},
|
||||
|
||||
async destroyConnection () {
|
||||
await knex.destroy()
|
||||
knex = null
|
||||
if (knex !== null) {
|
||||
await knex.destroy()
|
||||
knex = null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,18 +2,28 @@ const DB = require('../db/DB')
|
|||
const EmailUtils = require('../email-utils')
|
||||
const { LocaleUtils } = require('../locale-utils')
|
||||
const AppConstants = require('../app-constants')
|
||||
const { argv } = require('node:process')
|
||||
const { env, argv } = require('node:process')
|
||||
|
||||
async function sendUnresolvedBreachEmails (subscribers = null) {
|
||||
/* Send a monthly email to each subscriber with unresolved breaches
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/send-email-to-unresolved-breach-subscribers.js
|
||||
*
|
||||
* For testing, pass a comma-separated arg with no spaces, for example:
|
||||
* node scripts/send-email-to-unresolved-breach-subscribers.js test1@test.com,test2@test.com,...
|
||||
*/
|
||||
|
||||
async function sendUnresolvedBreachEmails (subscribers = null, limit = 0) {
|
||||
let count = 0
|
||||
|
||||
LocaleUtils.init()
|
||||
await EmailUtils.init()
|
||||
await DB.createConnection()
|
||||
|
||||
if (!subscribers) subscribers = await DB.getSubscribersWithUnresolvedBreaches() // if test subscribers are not available, pull from db
|
||||
|
||||
console.log('- Recipient total:', subscribers.length)
|
||||
let total = 0
|
||||
if (subscribers) {
|
||||
total = subscribers.length
|
||||
} else {
|
||||
// if test subscribers are not available, pull from db
|
||||
total = await DB.getSubscribersWithUnresolvedBreachesCount()
|
||||
subscribers = await DB.getSubscribersWithUnresolvedBreaches(limit)
|
||||
}
|
||||
console.log(`- Attempting to email ${subscribers.length} of ${total} subscribers...`)
|
||||
|
||||
for (const subscriber of subscribers) {
|
||||
try {
|
||||
|
@ -38,18 +48,41 @@ async function sendUnresolvedBreachEmails (subscribers = null) {
|
|||
}
|
||||
}
|
||||
|
||||
await DB.destroyConnection()
|
||||
console.log(`- Successfully emailed ${count} of ${subscribers.length} subscribers`)
|
||||
console.log(`- Successfully emailed ${count} of ${subscribers.length} subscribers (${total} total)`)
|
||||
return {
|
||||
emailed: count,
|
||||
attempted: subscribers.length,
|
||||
total
|
||||
}
|
||||
}
|
||||
|
||||
if (argv[2]) {
|
||||
// For testing, pass a comma-separated arg with no spaces, for example:
|
||||
// node scripts/send-email-to-unresolved-breach-subscribers.js test1@test.com,test2@test.com,...
|
||||
const subscribers = []
|
||||
const emails = argv[2].split(',')
|
||||
async function init () {
|
||||
LocaleUtils.init()
|
||||
await EmailUtils.init()
|
||||
await DB.createConnection()
|
||||
}
|
||||
|
||||
console.log('Testing job for monthly unresolved breach emails...')
|
||||
console.log('- sending test emails to', emails)
|
||||
async function runScript (argv = []) {
|
||||
let subscribers = null
|
||||
const limit = AppConstants.MONTHLY_CRON_LIMIT
|
||||
if (argv[2]) {
|
||||
console.log('Testing job for monthly unresolved breach emails...')
|
||||
const res = await prepareTestSubscribers(argv[2])
|
||||
subscribers = res.subscribers
|
||||
console.log('- Sending test emails to', res.emails)
|
||||
} else if (AppConstants.MONTHLY_CRON_ENABLED === 'true') {
|
||||
console.log('Running job for monthly unresolved breach emails...')
|
||||
} else {
|
||||
console.log('Job for monthly unresolved breach emails was not run. MONTHLY_CRON_ENABLED expects the string "true".')
|
||||
return
|
||||
}
|
||||
return await sendUnresolvedBreachEmails(subscribers, limit)
|
||||
}
|
||||
|
||||
async function prepareTestSubscribers (emailCsv = '') {
|
||||
// Create subscriber records from a comma-separated list of emails
|
||||
const subscribers = []
|
||||
const emails = emailCsv.split(',')
|
||||
|
||||
emails.forEach(email => {
|
||||
subscribers.push({
|
||||
|
@ -72,10 +105,28 @@ if (argv[2]) {
|
|||
}
|
||||
})
|
||||
})
|
||||
sendUnresolvedBreachEmails(subscribers)
|
||||
} else if (AppConstants.MONTHLY_CRON_ENABLED === 'true') {
|
||||
console.log('Running job for monthly unresolved breach emails...')
|
||||
sendUnresolvedBreachEmails()
|
||||
} else {
|
||||
console.log('Job for monthly unresolved breach emails was not run. MONTHLY_CRON_ENABLED expects the string "true".')
|
||||
return {
|
||||
subscribers,
|
||||
emails
|
||||
}
|
||||
}
|
||||
|
||||
async function teardown () {
|
||||
await DB.destroyConnection()
|
||||
}
|
||||
|
||||
async function main () {
|
||||
if (env.NODE_ENV !== 'tests') {
|
||||
await init()
|
||||
try {
|
||||
await runScript(argv)
|
||||
} finally {
|
||||
await teardown()
|
||||
}
|
||||
}
|
||||
}
|
||||
main()
|
||||
|
||||
module.exports = {
|
||||
sendUnresolvedBreachEmails
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
const DB = require('../db/DB')
|
||||
const { TEST_SUBSCRIBERS, TEST_EMAIL_ADDRESSES } = require('../db/seeds/test_subscribers')
|
||||
const { sendUnresolvedBreachEmails } = require('../scripts/send-email-to-unresolved-breach-subscribers')
|
||||
const EmailUtils = require('../email-utils')
|
||||
|
||||
require('./resetDB')
|
||||
|
||||
jest.mock('../email-utils')
|
||||
|
||||
test('sendUnresolvedBreachEmails to test subscriber', async () => {
|
||||
const data = await sendUnresolvedBreachEmails()
|
||||
expect(data).toEqual({"attempted": 1, "emailed": 1, "total": 1})
|
||||
expect(EmailUtils.sendEmail).toHaveBeenCalled()
|
||||
const subscriber = await DB.getSubscriberById(12346)
|
||||
expect(subscriber.monthly_email_at).not.toBeNull()
|
||||
expect(Date.now() - subscriber.monthly_email_at).toBeLessThan(100)
|
||||
})
|
Загрузка…
Ссылка в новой задаче