@ranger-testing/inbox-relay
v0.0.16
Published
`@ranger-testing/inbox-relay` is a wrapper around the Ranger API which allows tests to receive messages and make assertions on their content, including One-Time-Passowords (OTP), Magic Auth Links or Confirm Email Links, and transactional messages.
Readme
Ranger Inbox Relay Client
@ranger-testing/inbox-relay is a wrapper around the Ranger API which allows tests to receive
messages and make assertions on their content, including One-Time-Passowords (OTP), Magic Auth Links
or Confirm Email Links, and transactional messages.
The client library is the only way tests can interact with inboxes.
Table of Contents
Getting Started
For working locally:
npm install @ranger-testing/inbox-relayConcepts
Permanent Inboxes
Permanent Inboxes are managed through the Debug Tool, and should be created to correspond with a Permanent Test User within a third party system. These inboxes are best used for tests which do not involve the creation and disposal of new user accounts.
It is not possible to create or delete a Permanent Inbox during a test.
Temporary Inboxes
Temporary Inboxes are created during a test, or during the beforeAll for an entire suite of tests.
They are automatically cleaned-up after a configurable expiration period.
Temporary Inboxes are created with InboxRelayClient.createTemporaryInbox().
Locking an Inbox and Reading Messages
- An inbox must be locked before messages can be received.
- This guarantees that messages from parallel tests do not conflict.
- All locks have a (configurable) expiration, so that an inbox cannot remain locked indefinitely due to errors.
When you need to use an inbox, you must first request a lock with InboxRelayClient.lockInbox().
This method retries and waits for a lock until one is available. If the inbox is not already locked,
this is immediate. Otherwise, the method waits until the previous lock is released, up to a
configured timeout.
Once your lock is successful, you can perform actions that will cause emails to be sent to the
inbox. Once you are finished, you can await and retrieve all messages delivered to the inbox since
your lock began. Messages are retrieved with InboxRelayClient.unlockAndReadMessages().
The inbox lock is automatically released after you retrive messages. It is not possible to retrieve messages that were received before you locked the inbox, or after you unlock it. You cannot unlock the same inbox twice without locking it again.
If you need to retrieve messages more than once (because later test steps depend on message content), you must lock the inbox more than once.
Considerations
- Locking prevents an entire class of test-flake issues, especially when we are reusing a single Test User for multiple tests.
- In most cases, email should not take very long, and tests will not spend much time waiting.
- Multiple inboxes can be used to spend less time waiting for locks. There is no downside to creating multiple test users with dedicated inboxes if a use case calls for it.
- Because of this, Inboxes do not need to be unique per-test.
Use Case Examples
Magic Link Auth: Permanent Inbox
Customer has asked Ranger to test Magic Link login with an existing user account.
- Ranger creates a Permanent Inbox for
[email protected]using the Debug Tool. - Ranger or Customer creates the Test User in the Customer environment using that email address.
- Ranger tests can assert proper magic-link login behavior:
import { InboxRelayClient } from '@ranger-testing/inbox-relay'
const API_TOKEN = process.env.API_TOKEN
const PERMANENT_INBOX = '[email protected]'
const inboxes = new InboxRelayClient({ apiToken: API_TOKEN })
export async function run(page: Page) {
// >>> Request a lock for the (already-existing) permanent inbox:
await inboxes.lockInbox({ address: PERMANENT_INBOX })
// >>> Next, perform the steps that send the magic-link auth email:
const usernameInput = page.getByRole('textbox', { name: 'Username' })
await usernameInput.fill(PERMANENT_INBOX)
const loginButton = page.getByRole('button', { name: 'Log In' })
await loginButton.click()
// >>> Unlock the inbox and retrieve the message:
const message = (await inboxes.unlockAndReadMessages())[0]
// >>> Expect that the magic link works:
await page.goto(message.magicLink)
expect(page.locator('button[aria-label="Profile Menu"]')).toContainText('Ranger Test User')
}Transactional Emails: Permanent Inbox
Customer has asked Ranger to test transactional Task Notification Emails, which are sent when a task is assigned, completed, and reviewed.
- Ranger creates a Permanent Inbox for
[email protected]using the Debug Tool. - Ranger or Customer creates a test user in the Customer environment using that email adddress.
- Ranger tests can assert proper transactional email behavior:
import { InboxRelayClient } from '@ranger-testing/inbox-relay'
const API_TOKEN = process.env.API_TOKEN
const PERMANENT_INBOX = '[email protected]'
const inboxes = new InboxRelayClient({ apiToken: API_TOKEN })
export async function run(page: Page) {
// >>> Request a lock for the (already-existing) permanent inbox:
await inboxes.lockInbox({ address: PERMANENT_INBOX })
// >>> Next, perform the steps that send one or more emails:
const assigneeDropdown = page.getByRole('combobox', { name: 'Assignee' })
await assigneeDropdown.selectOption(PERMANENT_INBOX)
const assignButton = page.getByRole('button', { name: 'Assign' })
await assignButton.click()
// >>> Unlock the inbox and retrieve messages:
const message = (await inboxes.unlockAndReadMessages())[0]
// >>> Expect the message to contain the right content:
expect(message.content).toMatch(/You were assigned task "\w*?"/)
// >>> Optionally assert on the number of attachments
expect(message.attachmentCount).toBeGreaterThan(0)
}Sign Up Flow: Temporary Inbox
Customer has asked Ranger to test their new-user signup flow. The user should receive an a confirm-email link email as well as a welcome email.
This test case requires us to create a temporary inbox before completing the signup flow:
import { InboxRelayClient } from '@ranger-testing/inbox-relay'
const API_TOKEN = process.env.API_TOKEN
const inboxes = new InboxRelayClient({ apiToken: API_TOKEN })
export async function run(page: Page) {
// >>> Create and lock the temporary inbox:
const { address } = await inboxes.createTemporaryInbox()
await inboxes.lockInbox({ address })
// >>> Next, sign up the new user:
const newUserEmailInput = page.getByRole('textbox', { name: 'Email Address' })
await newUserEmailInput.fill(TEMPORARY_INBOX)
const submitButton = page.getByRole('button', { name: 'Sign Up' })
await submitButton.click()
// >>> Unlock the inbox and retrieve messages:
const messages = await inboxes.unlockAndReadMessages({
// We are expecting both a welcome email and a confirm-email link
expectedMessages: 2
})
// Find the message which contains a confirm link
const confirmEmail = messages.find((message) => !!message.magicLink)
// >>> Expect the confirm-email link to work
await page.goto(confirmEmail.magicLink)
expect(page.getByTestId('success-callout')).toContainText('confirmed successfully')
}InboxMessage Contents
export interface InboxMessage {
/** The sender of the message. */
sender: string | null
/** The subject of the message. */
subject: string | null
/** The body of the message. */
content: string | null
/** Some HTML emails include a `text/plain` alternative. If so, it is provided here. */
contentText: string | null
/** The instant the message was received. */
receivedTimestamp: Date
/** An alphanumeric one-time-password, if one was found. */
otp: string | null
/** Any links that were found in the body of the message. */
links: string[]
/** Our best guess as to which link is a magic link, if one is found. */
magicLink: string | null
/** Number of attachments in the message. */
attachmentCount: number
}PDF Attachment Processing: Temporary Inbox
Customer has asked Ranger to test that invoice PDFs are properly generated and emailed to users after purchase completion.
This test case demonstrates downloading and parsing PDF attachments:
import { InboxRelayClient } from '@ranger-testing/inbox-relay'
import pdf from 'pdf-parse'
const API_TOKEN = process.env.API_TOKEN
const inboxes = new InboxRelayClient({ apiToken: API_TOKEN })
export async function run(page: Page) {
// >>> Create and lock the temporary inbox:
const { address } = await inboxes.createTemporaryInbox()
await inboxes.lockInbox({ address })
// >>> Complete a purchase that generates an invoice email:
const emailInput = page.getByRole('textbox', { name: 'Email' })
await emailInput.fill(address)
const purchaseButton = page.getByRole('button', { name: 'Complete Purchase' })
await purchaseButton.click()
// >>> Unlock the inbox and retrieve messages with attachments:
const messages = await inboxes.unlockAndReadMessages({
includeAttachments: true
})
// >>> Download and parse the PDF attachment:
const pdfAttachment = messages
.flatMap((msg) => msg.attachments)
.find((att) => att?.name.endsWith('.pdf'));
expect(pdfAttachment).toBeTruthy();
const buffer = Buffer.from(pdfAttachment.contentBase64, 'base64');
const pdfData = await pdf(buffer);
// >>> Assert on PDF content:
expect(pdfData.text).toContain('Invoice #')
expect(pdfData.text).toContain('Total Amount: $')
expect(pdfData.text).toContain(address) // Customer email should be in invoice
}SMS OTP Verification: Phone Number
Customer has asked Ranger to test SMS OTP verification for two-factor authentication.
This test case demonstrates using findAndLockPhoneNumber to automatically find and lock an available phone number for receiving SMS messages:
import { InboxRelayClient } from '@ranger-testing/inbox-relay'
const API_TOKEN = process.env.API_TOKEN
const inboxes = new InboxRelayClient({ apiToken: API_TOKEN })
export async function run(page: Page) {
// >>> Find and lock an available phone number:
const { address: phoneNumber } = await inboxes.findAndLockPhoneNumber()
// >>> Enter the phone number in the 2FA setup form:
const phoneInput = page.getByRole('textbox', { name: 'Phone Number' })
await phoneInput.fill(phoneNumber)
const sendCodeButton = page.getByRole('button', { name: 'Send Code' })
await sendCodeButton.click()
// >>> Unlock the phone number and retrieve the SMS message:
const message = (await inboxes.unlockAndReadMessages())[0]
// >>> Extract the OTP and complete verification:
const otpInput = page.getByRole('textbox', { name: 'Verification Code' })
await otpInput.fill(message.otp!)
const verifyButton = page.getByRole('button', { name: 'Verify' })
await verifyButton.click()
// >>> Expect successful verification:
expect(page.getByTestId('success-message')).toContainText('Phone verified successfully')
}