Skip to content

Commit

Permalink
feat: 🎨 finalize delivery feature
Browse files Browse the repository at this point in the history
  • Loading branch information
ignazio-bovo committed Sep 29, 2023
1 parent 2c1c541 commit 68b9136
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 151 deletions.
39 changes: 12 additions & 27 deletions src/mail-scheduler/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { ConfigVariable, config } from '../utils/config'
import {
EmailDeliveryAttempt,
EmailFailure,
EmailSuccess,
NotificationEmailDelivery,
} from '../model'
import { EmailDeliveryAttempt, NotificationEmailDelivery } from '../model'
import { EntityManager } from 'typeorm'
import { globalEm } from '../utils/globalEm'
import { uniqueId } from '../utils/crypto'
Expand All @@ -28,40 +23,30 @@ export async function mailsToDeliver(em: EntityManager): Promise<NotificationEma
return result
}

// Function to send new data
export async function sendNew() {
export async function deliverEmails() {
const em = await globalEm
const newEmailDeliveries = await mailsToDeliver(em)
for (let notificationDelivery of newEmailDeliveries) {

Check failure on line 29 in src/mail-scheduler/index.ts

View workflow job for this annotation

GitHub Actions / Local build, linting and formatting (ubuntu-latest, 16.x)

'notificationDelivery' is never reassigned. Use 'const' instead
const notification = notificationDelivery.notification
const toAccount = notification.account
const toAccount = notificationDelivery.notification.account
const appName = await config.get(ConfigVariable.AppName, em)
const content = '' // await createMailContent(em, toAccount, appName, notification)
let attempts = notificationDelivery.attempts

Check failure on line 33 in src/mail-scheduler/index.ts

View workflow job for this annotation

GitHub Actions / Local build, linting and formatting (ubuntu-latest, 16.x)

'attempts' is never reassigned. Use 'const' instead
const status = await executeMailDelivery(appName, em, toAccount, content)
const newAttempt = new EmailDeliveryAttempt({
id: uniqueId(),
timestamp: new Date(),
status,
})
attempts.push(newAttempt)
notificationDelivery.attempts = attempts
if (status.isTypeOf === 'EmailSuccess') {
attempts.push(
new EmailDeliveryAttempt({
id: uniqueId(),
timestamp: new Date(),
status,
})
)
notificationDelivery.discard = true
notificationDelivery.attempts = attempts
} else {
attempts.push(
new EmailDeliveryAttempt({
id: uniqueId(),
timestamp: new Date(),
status,
})
)
notificationDelivery.attempts = attempts
if (attempts.length >= (await getMaxAttempts(em))) {
notificationDelivery.discard = true
}
}
await em.save(notificationDelivery)
await em.save(newAttempt)
}
await em.save(newEmailDeliveries)
}
225 changes: 109 additions & 116 deletions src/mail-scheduler/tests/scheduler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,33 @@ import { expect } from 'chai'
import { NotificationEmailDelivery, Notification } from '../../model'
import { clearDb, populateDbWithSeedData } from './testUtils'
import { globalEm } from '../../utils/globalEm'
import { getMaxAttempts, sendNew } from '..'
import { getMaxAttempts, deliverEmails } from '..'
import { EntityManager } from 'typeorm'
import { RUNTIME_NOTIFICATION_ID_TAG } from '../../utils/notification/helpers'

const getDeliveryFromNotificationId = async (em: EntityManager, notificationId: string) => {
const res = await em.getRepository(NotificationEmailDelivery).findOneOrFail({
where: { notification: { id: notificationId } },
relations: { attempts: true },
})
return res
}

const correctRecipientAccountEmail = async (em: EntityManager, notificationId: string) => {
const accountId = (await em.getRepository(Notification).findOneByOrFail({ id: notificationId }))
.accountId
let account = await em.getRepository('Account').findOneByOrFail({ id: accountId })

Check failure on line 20 in src/mail-scheduler/tests/scheduler.test.ts

View workflow job for this annotation

GitHub Actions / Local build, linting and formatting (ubuntu-latest, 16.x)

'account' is never reassigned. Use 'const' instead
account.email = `correct-${accountId}@example.com`
await em.save(account)
}

describe('Scheduler', () => {
const okNotificationId = RUNTIME_NOTIFICATION_ID_TAG + '-1'
const errNotificationId = RUNTIME_NOTIFICATION_ID_TAG + '-2'
const okAtSecondNotificationId = RUNTIME_NOTIFICATION_ID_TAG + '-3'
const okNotificationId = RUNTIME_NOTIFICATION_ID_TAG + '-0'
const errNotificationId = RUNTIME_NOTIFICATION_ID_TAG + '-1'
const okAtSecondNotificationId = RUNTIME_NOTIFICATION_ID_TAG + '-2'
let successfulDelivery: NotificationEmailDelivery
let failingDelivery: NotificationEmailDelivery
let successfulAtSecondDelivery: NotificationEmailDelivery
let em: EntityManager
let maxAttempts: number

Expand All @@ -23,142 +42,116 @@ describe('Scheduler', () => {
await clearDb()
})

describe('case success at first attempt', () => {
let emailDelivery: NotificationEmailDelivery
describe('first run of deliverEmails', () => {
before(async () => {
await sendNew()

emailDelivery = await em.getRepository(NotificationEmailDelivery).findOneOrFail({
where: { notification: { id: errNotificationId } },
relations: { attempts: true },
})
await correctRecipientAccountEmail(em, okNotificationId)
await deliverEmails()
successfulDelivery = await getDeliveryFromNotificationId(em, okNotificationId)
failingDelivery = await getDeliveryFromNotificationId(em, errNotificationId)
successfulAtSecondDelivery = await getDeliveryFromNotificationId(em, okAtSecondNotificationId)
})
describe('successful delivery case: 👍', () => {
it('should change email delivery to discard when success', async () => {
expect(emailDelivery.discard).to.be.true
expect(successfulDelivery.discard).to.be.true
})
it('should create a successful attempt', async () => {
expect(emailDelivery.attempts).to.have.lengthOf(1)
expect(emailDelivery.attempts[0].notificationDeliveryId).to.equal(emailDelivery.id)
expect(emailDelivery.attempts[0].status.isTypeOf).to.equal('SuccessfulDelivery')
expect(successfulDelivery.attempts).to.have.lengthOf(1)
expect(successfulDelivery.attempts[0].notificationDeliveryId).to.equal(
successfulDelivery.id
)
expect(successfulDelivery.attempts[0].status.isTypeOf).to.equal('EmailSuccess')
})
})
describe('case failure at first attempt', async () => {
let emailDelivery: NotificationEmailDelivery
before(async () => {
await sendNew()
emailDelivery = await em.getRepository(NotificationEmailDelivery).findOneOrFail({
where: { notification: { id: errNotificationId } },
relations: { attempts: true },
})
})

describe('failing delivery case: 👎', async () => {

Check failure on line 65 in src/mail-scheduler/tests/scheduler.test.ts

View workflow job for this annotation

GitHub Actions / Local build, linting and formatting (ubuntu-latest, 16.x)

Promise returned in function argument where a void return was expected
it('should keep email delivery discard to false', () => {
expect(emailDelivery.discard).to.be.false
expect(failingDelivery.discard).to.be.false
})
it('should create a failed attempt', () => {
expect(emailDelivery.attempts).to.have.lengthOf(1)
expect(emailDelivery.attempts[0].notificationDeliveryId).to.equal(emailDelivery.id)
expect(emailDelivery.attempts[0].status.isTypeOf).to.equal('FailedDelivery')
expect(failingDelivery.attempts).to.have.lengthOf(1)
expect(failingDelivery.attempts[0].notificationDeliveryId).to.equal(failingDelivery.id)
expect(failingDelivery.attempts[0].status.isTypeOf).to.equal('EmailFailure')
})
})
describe('case second attempt is again a failure', async () => {
let emailDelivery: NotificationEmailDelivery
before(async () => {
await sendNew()
await sendNew()

emailDelivery = await em.getRepository(NotificationEmailDelivery).findOneOrFail({
where: { notification: { id: errNotificationId } },
relations: { attempts: true },
})
})
describe('successful at second attempt delivery case: 👎', async () => {

Check failure on line 75 in src/mail-scheduler/tests/scheduler.test.ts

View workflow job for this annotation

GitHub Actions / Local build, linting and formatting (ubuntu-latest, 16.x)

Promise returned in function argument where a void return was expected
it('should keep email delivery discard to false', () => {
expect(emailDelivery.discard).to.be.false
expect(successfulAtSecondDelivery.discard).to.be.false
})
it('should create another failed attempt', () => {
expect(emailDelivery.attempts).to.have.lengthOf(2)
expect(emailDelivery.attempts[0].notificationDeliveryId).to.equal(emailDelivery.id)
expect(emailDelivery.attempts[0].status.isTypeOf).to.equal('FailedDelivery')
expect(emailDelivery.attempts[1].status.isTypeOf).to.equal('FailedDelivery')
it('should create a failed attempt', () => {
expect(successfulAtSecondDelivery.attempts).to.have.lengthOf(1)
expect(successfulAtSecondDelivery.attempts[0].notificationDeliveryId).to.equal(
successfulAtSecondDelivery.id
)
expect(successfulAtSecondDelivery.attempts[0].status.isTypeOf).to.equal('EmailFailure')
})
})
describe('case max attempts reached', () => {
let emailDelivery: NotificationEmailDelivery
describe('second run of deliverEmails', () => {
before(async () => {
await sendNew() // first attempt
const maxAttempts = await getMaxAttempts(em)

for (let i = 0; i < maxAttempts - 1; i++) {
await sendNew()
}

emailDelivery = await em.getRepository(NotificationEmailDelivery).findOneOrFail({
where: { notification: { id: errNotificationId } },
relations: { attempts: true },
await correctRecipientAccountEmail(em, okAtSecondNotificationId)
await deliverEmails()
failingDelivery = await getDeliveryFromNotificationId(em, errNotificationId)
successfulAtSecondDelivery = await getDeliveryFromNotificationId(
em,
okAtSecondNotificationId
)
})
describe('failing delivery case: 👎', async () => {

Check failure on line 97 in src/mail-scheduler/tests/scheduler.test.ts

View workflow job for this annotation

GitHub Actions / Local build, linting and formatting (ubuntu-latest, 16.x)

Promise returned in function argument where a void return was expected
it('should keep email delivery discard to false', () => {
expect(failingDelivery.discard).to.be.false
})
})

it('should set the email delivery discard to true after MAX_ATTEMPTS', async () => {
expect(emailDelivery.discard).to.be.true
})
it('should create at most MAX_ATTEMPTS failed attempts', async () => {
const attemptsStatus = emailDelivery.attempts.map((attempt) => attempt.status.isTypeOf)
expect(emailDelivery.attempts).to.have.lengthOf(maxAttempts)
expect(attemptsStatus.every((status) => status === 'EmailFailure')).to.be.true
})
it('calling the sending function once more should not change anything', async () => {
await sendNew()

emailDelivery = await em.getRepository(NotificationEmailDelivery).findOneOrFail({
where: { notification: { id: errNotificationId } },
relations: { attempts: true },
it('should create another failed attempt', () => {
expect(failingDelivery.attempts).to.have.lengthOf(2)
expect(failingDelivery.attempts[0].notificationDeliveryId).to.equal(failingDelivery.id)
expect(failingDelivery.attempts[0].status.isTypeOf).to.equal('EmailFailure')
expect(failingDelivery.attempts[1].status.isTypeOf).to.equal('EmailFailure')
})

const attemptsStatus = emailDelivery.attempts.map((attempt) => attempt.status.isTypeOf)
expect(emailDelivery.attempts).to.have.lengthOf(maxAttempts)
expect(attemptsStatus.every((status) => status === 'EmailFailure')).to.be.true
})
})
describe('case success at second attempt', () => {
let emailDelivery: NotificationEmailDelivery
before(async () => {
await sendNew() // first attempt
})
describe('should be failuer at first attempt', async () => {
emailDelivery = await em.getRepository(NotificationEmailDelivery).findOneOrFail({
where: { notification: { id: errNotificationId } },
relations: { attempts: true },
})

it('should keep email delivery discard to false', () => {
expect(emailDelivery.discard).to.be.false
describe('successful at second delivery case: 👍', () => {
it('should change email delivery to discard when success', async () => {
expect(successfulAtSecondDelivery.discard).to.be.true
})
it('should create a failed attempt', () => {
expect(emailDelivery.attempts[-1].notificationDeliveryId).to.equal(emailDelivery.id)
expect(emailDelivery.attempts[-1].status.isTypeOf).to.equal('FailedDelivery')
it('should create a new successful attempt', async () => {
expect(successfulAtSecondDelivery.attempts).to.have.lengthOf(2)
expect(successfulAtSecondDelivery.attempts[1].notificationDeliveryId).to.equal(
successfulAtSecondDelivery.id
)
expect(successfulAtSecondDelivery.attempts[1].status.isTypeOf).to.equal('EmailSuccess')
})
})
describe('should be success at second attempt', async () => {
const accountId = (
await em.getRepository(Notification).findOneByOrFail({ id: okAtSecondNotificationId })
).account.id
const account = await em.getRepository('Account').findOneByOrFail({ id: accountId })
account.email = 'correct@example.com'
await em.save(account)

await sendNew() // second attempt

emailDelivery = await em.getRepository(NotificationEmailDelivery).findOneOrFail({
where: { notification: { id: errNotificationId } },
relations: { attempts: true },
describe('MAX_ATTEMPTS - 2 runs of deliverEmails', () => {
before(async () => {
const maxAttempts = await getMaxAttempts(em)
expect(maxAttempts).to.be.greaterThan(2)
for (let i = 0; i < maxAttempts - 2; i++) {
await deliverEmails()
}
failingDelivery = await getDeliveryFromNotificationId(em, errNotificationId)
})

it('should change email delivery to discard when success', async () => {
expect(emailDelivery.discard).to.be.true
describe('failing delivery case: 👎', async () => {

Check failure on line 129 in src/mail-scheduler/tests/scheduler.test.ts

View workflow job for this annotation

GitHub Actions / Local build, linting and formatting (ubuntu-latest, 16.x)

Promise returned in function argument where a void return was expected
it('should set the email delivery discard to true after MAX_ATTEMPTS', async () => {
expect(failingDelivery.discard).to.be.true
})
it('should create MAX_ATTEMPTS failed attempts', async () => {
const attemptsStatus = failingDelivery.attempts.map(
(attempt) => attempt.status.isTypeOf
)
expect(failingDelivery.attempts).to.have.lengthOf(maxAttempts)
expect(attemptsStatus.every((status) => status === 'EmailFailure')).to.be.true
})
})

it('should create a successful attempt', async () => {
expect(emailDelivery.attempts[-1].notificationDeliveryId).to.equal(emailDelivery.id)
expect(emailDelivery.attempts[-1].status.isTypeOf).to.equal('SuccessfulDelivery')
describe('one more run of deliverEmails', () => {
before(async () => {
await deliverEmails()
failingDelivery = await getDeliveryFromNotificationId(em, errNotificationId)
})
describe('failing delivery case: 👎', async () => {

Check failure on line 146 in src/mail-scheduler/tests/scheduler.test.ts

View workflow job for this annotation

GitHub Actions / Local build, linting and formatting (ubuntu-latest, 16.x)

Promise returned in function argument where a void return was expected
it('should not create more MAX_ATTEMPTS failed attempts', async () => {
const attemptsStatus = failingDelivery.attempts.map(
(attempt) => attempt.status.isTypeOf
)
expect(failingDelivery.attempts).to.have.lengthOf(maxAttempts)
expect(attemptsStatus.every((status) => status === 'EmailFailure')).to.be.true
})
})
})
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,10 @@ describe('Database seed data tests', () => {
expect(account?.membership.handle).to.equal('handle-1')
})
it('check that notification delivery entity is correct', async () => {
const result = await em
.getRepository(NotificationEmailDelivery)
.findOne({
where: { notification: { id: RUNTIME_NOTIFICATION_ID_TAG + '-1' } },
relations: { notification: { account: true } },
})
const result = await em.getRepository(NotificationEmailDelivery).findOne({
where: { notification: { id: RUNTIME_NOTIFICATION_ID_TAG + '-1' } },
relations: { notification: { account: true } },
})

expect(result).to.not.be.null
expect(result?.notification.account).to.not.be.null
Expand Down
6 changes: 4 additions & 2 deletions src/mail-scheduler/tests/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import {
NotificationEmailDelivery,
GatewayConfig,
AuctionWon,
EmailDeliveryAttempt,
} from '../../model'
import { defaultNotificationPreferences } from '../../utils/notification'
import { globalEm } from '../../utils/globalEm'
import { idStringFromNumber } from '../../utils/misc'
import { RUNTIME_NOTIFICATION_ID_TAG } from '../../utils/notification/helpers'
import { uniqueId } from '../../utils/crypto'

export const NUM_ENTITIES = 5
export const NUM_ENTITIES = 3

export async function populateDbWithSeedData() {
const em = await globalEm
Expand All @@ -42,7 +43,7 @@ export async function populateDbWithSeedData() {
const account = await em.getRepository(Account).save({
id: idStringFromNumber(i),
userId: user.id,
email: `testEmail_${i}@example.com`,
email: `incorrect${i}@example.com`,
isEmailConfirmed: false,
isBlocked: false,
registeredAt: member.createdAt,
Expand Down Expand Up @@ -72,6 +73,7 @@ export async function populateDbWithSeedData() {

export async function clearDb(): Promise<void> {
const em = await globalEm
await em.getRepository(EmailDeliveryAttempt).delete({})
await em.getRepository(NotificationEmailDelivery).delete({})
await em.getRepository(Notification).delete({})
await em.getRepository(Account).delete({})
Expand Down
Loading

0 comments on commit 68b9136

Please sign in to comment.