Skip to content

Commit

Permalink
feat: add support for faking emails
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Remove "traps" in favor of "fakes"
  • Loading branch information
Harminder Virk authored and Harminder Virk committed Apr 11, 2022
1 parent 1163cb8 commit 28a8d8f
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 162 deletions.
37 changes: 28 additions & 9 deletions adonis-typings/mail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ declare module '@ioc:Adonis/Addons/Mail' {
watch?: string
}

export type MessageSearchNode = Omit<MessageNode, 'attachments' | 'icalEvent'>

/**
* Shape of the message instance passed to `send` method callback
*/
Expand Down Expand Up @@ -540,6 +542,12 @@ declare module '@ioc:Adonis/Addons/Mail' {
*/
export interface FakeDriverContract extends MailDriverContract {
send(message: MessageNode): Promise<FakeMailResponse>
find(
messageOrCallback: MessageSearchNode | ((mail: MessageSearchNode) => boolean)
): MessageNode | null
filter(
messageOrCallback: MessageSearchNode | ((mail: MessageSearchNode) => boolean)
): MessageNode[]
}

/*
Expand All @@ -554,11 +562,6 @@ declare module '@ioc:Adonis/Addons/Mail' {
*/
export type MessageComposeCallback = (message: MessageContract) => void | Promise<void>

/**
* Callback to wrap emails
*/
export type TrapCallback = (message: MessageNode) => any

/**
* Callback to monitor queues response
*/
Expand Down Expand Up @@ -639,6 +642,22 @@ declare module '@ioc:Adonis/Addons/Mail' {
close(): Promise<void>
}

/**
* Fake mail manager to trap emails and write
* assertions against them
*/
export interface FakeMailManagerContract {
isFaked(mailer: keyof MailersList): boolean
use(mailer: keyof MailersList): MailerContract<any>
exists(messageOrCallback: MessageSearchNode | ((mail: MessageSearchNode) => boolean)): boolean
find(
messageOrCallback: MessageSearchNode | ((mail: MessageSearchNode) => boolean)
): MessageNode | null
filter(
messageOrCallback: MessageSearchNode | ((mail: MessageSearchNode) => boolean)
): MessageNode[]
}

/**
* Shape of the mailer
*/
Expand All @@ -650,19 +669,19 @@ declare module '@ioc:Adonis/Addons/Mail' {
{ [P in keyof MailersList]: MailerContract<P> }
> {
/**
* Trap emails
* Fake one or more mailers
*/
trap(callback: TrapCallback): void
fake(mailers?: keyof MailersList | keyof MailersList[]): FakeMailManagerContract

/**
* Define a callback to monitor queued emails
*/
monitorQueue(callback: QueueMonitorCallback): void

/**
* Restore trap
* Restore fakes
*/
restore(): void
restore(mailers?: keyof MailersList | keyof MailersList[]): void

/**
* Pretty print mailer event data
Expand Down
38 changes: 33 additions & 5 deletions src/Drivers/Fake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,51 @@
import nodemailer from 'nodemailer'
import {
MessageNode,
TrapCallback,
FakeDriverContract,
FakeMailResponse,
MessageSearchNode,
} from '@ioc:Adonis/Addons/Mail'
import { subsetCompare } from '../utils'

/**
* Smtp driver to send email using smtp
*/
export class FakeDriver implements FakeDriverContract {
private transporter: any
public mails: MessageNode[] = []

constructor(private listener: TrapCallback) {
constructor() {
this.transporter = nodemailer.createTransport({
jsonTransport: true,
})
}

/**
* Find an email
*/
public find(
messageOrCallback: MessageSearchNode | ((mail: MessageSearchNode) => boolean)
): MessageNode | null {
if (typeof messageOrCallback === 'function') {
return this.mails.find(messageOrCallback) || null
}

return this.mails.find((mail) => subsetCompare(messageOrCallback, mail)) || null
}

/**
* Filter emails
*/
public filter(
messageOrCallback: MessageSearchNode | ((mail: MessageSearchNode) => boolean)
): MessageNode[] {
if (typeof messageOrCallback === 'function') {
return this.mails.filter(messageOrCallback)
}

return this.mails.filter((mail) => subsetCompare(messageOrCallback, mail))
}

/**
* Send message
*/
Expand All @@ -37,16 +65,16 @@ export class FakeDriver implements FakeDriverContract {
throw new Error('Driver transport has been closed and cannot be used for sending emails')
}

const listenerResponse = this.listener(message)
const response = await this.transporter.sendMail(message)
return { ...response, ...listenerResponse }
this.mails.push(message)
return this.transporter.sendMail(message)
}

/**
* Close transporter connection, helpful when using connections pool
*/
public async close() {
this.transporter.close()
this.mails = []
this.transporter = null
}
}
86 changes: 86 additions & 0 deletions src/Fake/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* @adonisjs/mail
*
* (c) AdonisJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/// <reference path="../../adonis-typings/mail.ts" />

import {
MailerContract,
MailersList,
MessageNode,
MessageSearchNode,
FakeMailManagerContract,
} from '@ioc:Adonis/Addons/Mail'

export class FakeMailManager implements FakeMailManagerContract {
public fakedMailers: Map<keyof MailersList, MailerContract<any>> = new Map()

/**
* Returns the faked mailer instance
*/
public use(mailer: keyof MailersList) {
return this.fakedMailers.get(mailer)!
}

/**
* Restore mailer fake
*/
public restore(mailer: keyof MailersList) {
const mailerInstance = this.fakedMailers.get(mailer)
if (mailerInstance) {
mailerInstance.close()
this.fakedMailers.delete(mailer)
}
}

/**
* Find if a mailer is faked
*/
public isFaked(mailer: keyof MailersList): boolean {
return this.fakedMailers.has(mailer)
}

/**
* Find if an email exists
*/
public exists(
messageOrCallback: MessageSearchNode | ((mail: MessageSearchNode) => boolean)
): boolean {
return !!this.find(messageOrCallback)
}

/**
* Find an email
*/
public find(
messageOrCallback: MessageSearchNode | ((mail: MessageSearchNode) => boolean)
): MessageSearchNode | null {
for (let [, mailer] of this.fakedMailers) {
const message = mailer.driver.find(messageOrCallback)
if (message) {
return message
}
}

return null
}

/**
* Filter emails
*/
public filter(
messageOrCallback: MessageSearchNode | ((mail: MessageSearchNode) => boolean)
): MessageNode[] {
let messages: MessageNode[] = []
for (let [, mailer] of this.fakedMailers) {
messages = messages.concat(mailer.driver.filter(messageOrCallback))
}

return messages
}
}
88 changes: 58 additions & 30 deletions src/Mail/MailManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { ApplicationContract } from '@ioc:Adonis/Core/Application'
import {
MailConfig,
MailersList,
TrapCallback,
MailerContract,
CompiledMailNode,
MailDriverContract,
Expand All @@ -28,6 +27,7 @@ import {
} from '@ioc:Adonis/Addons/Mail'

import { Mailer } from './Mailer'
import { FakeMailManager } from '../Fake'
import { BaseMailer } from '../BaseMailer'
import { prettyPrint } from '../Helpers/prettyPrint'

Expand All @@ -47,21 +47,6 @@ export class MailManager
>
implements MailManagerContract
{
/**
* Caching driver instances. One must call `close` to clean it up
*/
protected singleton = true

/**
* Reference to the fake driver
*/
private fakeMailer?: MailerContract<any>

/**
* Method to pretty print sent emails
*/
public prettyPrint = prettyPrint

/**
* Emails queue to scheduling emails to be delivered later
*/
Expand All @@ -82,6 +67,21 @@ export class MailManager
}
}

/**
* Reference to the fake mailer manager
*/
private fakeMailManager = new FakeMailManager()

/**
* Caching driver instances. One must call `close` to clean it up
*/
protected singleton = true

/**
* Method to pretty print sent emails
*/
public prettyPrint = prettyPrint

/**
* Reference to the base mailer since Ioc container doesn't allow
* multiple exports
Expand Down Expand Up @@ -228,12 +228,22 @@ export class MailManager
}

/**
* Fake email calls. The "sendLater" emails will be invoked right
* away as well
* Fake one or more mailers. Calling the method multiple times
* appends to the list of faked mailers
*/
public trap(callback: TrapCallback) {
public fake(mailers?: keyof MailersList | keyof MailersList[]) {
mailers = mailers || this.getDefaultMappingName()
const mailersToFake = Array.isArray(mailers) ? mailers : [mailers]

const { FakeDriver } = require('../Drivers/Fake')
this.fakeMailer = new Mailer('fake' as any, this, false, new FakeDriver(callback))
mailersToFake.forEach((mailer) => {
this.fakeMailManager.fakedMailers.set(
mailer,
new Mailer('fake' as any, this, false, new FakeDriver())
)
})

return this.fakeMailManager
}

/**
Expand All @@ -244,41 +254,59 @@ export class MailManager
}

/**
* Restore previously created trap.
* Restore fakes
*/
public restore() {
this.fakeMailer = undefined
public restore(mailers?: keyof MailersList | keyof MailersList[]) {
mailers = mailers || this.getDefaultMappingName()
const mailersToRestore = Array.isArray(mailers) ? mailers : [mailers]

mailersToRestore.forEach((mailer) => {
this.fakeMailManager.restore(mailer)
})
}

/**
* Sends email using the default `mailer`
*/
public async send(callback: MessageComposeCallback) {
if (this.fakeMailer) {
return this.fakeMailer.send(callback)
/**
* Use fake and return its response
*/
if (this.fakeMailManager.isFaked(this.getDefaultMappingName())) {
return this.fakeMailManager.use(this.getDefaultMappingName()).send(callback)
}

return this.use().send(callback)
}

/**
* Send email by pushing it to the in-memory queue
*/
public async sendLater(callback: MessageComposeCallback) {
if (this.fakeMailer) {
return this.fakeMailer.sendLater(callback)
/**
* Use fake and return its response
*/
if (this.fakeMailManager.isFaked(this.getDefaultMappingName())) {
return this.fakeMailManager.use(this.getDefaultMappingName()).send(callback)
}

return this.use().sendLater(callback)
}

/**
* Use a named or the default mailer
*/
public use(name?: keyof MailersList) {
if (this.fakeMailer) {
return this.fakeMailer
name = name || this.getDefaultMappingName()

/**
* Use fake
*/
if (this.fakeMailManager.isFaked(name)) {
return this.fakeMailManager.use(name)
}

return name ? super.use(name) : super.use()
return super.use(name)
}

/**
Expand Down
Loading

0 comments on commit 28a8d8f

Please sign in to comment.