MailSlurp logo

Test OTP, MFA, magic link and password reset flows

Documentation navigation
Search documentation

Test signup verification, email and SMS one-time passwords, authenticator MFA, magic links and password resets end to end, including expiry, resend and one-time-use checks.

Sign-up, login and account recovery all depend on a short code or link reaching the right person at the right time. A unit test with a mocked mail or SMS provider can prove that your application asked for a message to be sent. It cannot prove that the message arrived, that it contained a valid code, or that the user could finish the journey before the code expired.

Those gaps are where authentication bugs usually hide: a template variable that renders an empty code, a resend that leaves the old code valid, a link that works twice, or an SMS that arrives after the test has given up. Checking them by hand means a real mailbox, a real phone and a stopwatch for every release.

MailSlurp gives each test its own email inbox, phone number or virtual authenticator, and APIs that wait for the message and read the code. Your end-to-end test drives the application exactly as a user would, then uses MailSlurp to receive what the user would receive.

Choose a method for each journey

Most applications combine several of these journeys. Each one needs a different MailSlurp resource and a different final assertion.

Journey Where the code or link arrives MailSlurp resource What the test should prove
Sign-up email verification Email Inbox The code or link verifies the account once
Login MFA by email Email Inbox The code completes the challenge and expires on time
Login MFA by SMS SMS Phone number The SMS arrives within your time budget and the code is accepted
Authenticator app MFA (TOTP) Generated from a shared secret Virtual TOTP device Enrollment pairs correctly and later sign-ins accept the code
Magic link sign-in Link in an email Inbox The link signs the user in once and then stops working
Password reset Link or code in an email Inbox The new password works, the old one fails and the link cannot be reused

Set up the client

The examples use TypeScript with the MailSlurp JavaScript SDK and Playwright. The same API calls exist in every MailSlurp SDK.

npm install --save-dev @playwright/test mailslurp-client

Create an API key in the MailSlurp dashboard and expose it to your test runner as an environment variable:

import { MailSlurp } from 'mailslurp-client';

const apiKey = process.env.MAILSLURP_API_KEY;
if (!apiKey) throw new Error('Set MAILSLURP_API_KEY');
export const mailslurp = new MailSlurp({ apiKey });

Test email verification codes

Email codes are the most common second factor and the most common way to verify a new account.

Create an inbox for each test

Give every test a fresh inbox so that parallel runs never read each other’s codes. An expiring inbox cleans itself up if a test crashes before its own cleanup runs.

const inbox = await mailslurp.createInboxWithOptions({ expiresIn: 300_000 });
// Use inbox.emailAddress as the user's email in your application.

Wait for the code and extract it

After your test submits the sign-up or login form, wait for the newest unread email in that inbox. The getEmailCodes endpoint finds the verification code for you, so the test does not depend on the exact wording of the template.

const email = await mailslurp.waitForLatestEmail(inbox.id, 60_000, true);
const result = await mailslurp.emailController.getEmailCodes({ emailId: email.id });
if (!result.found || !result.code) {
  throw new Error(`No verification code found: ${result.warnings.join(', ')}`);
}
const code = result.code;

If your codes follow a strict format, a regular expression on email.body works too. Keep the code as a string so that leading zeros survive.

Complete the journey in the browser

The final assertion belongs to your application, not to MailSlurp. Enter the code where a user would and check the state the user should reach.

Full Playwright test for sign-up email verification
import { test, expect } from '@playwright/test';
import { MailSlurp } from 'mailslurp-client';

const mailslurp = new MailSlurp({ apiKey: process.env.MAILSLURP_API_KEY! });

test('new user verifies their email with a one-time code', async ({ page }) => {
  test.setTimeout(120_000);
  const inbox = await mailslurp.createInboxWithOptions({ expiresIn: 300_000 });
  try {
    await page.goto('https://app.example.com/sign-up');
    await page.getByLabel('Email').fill(inbox.emailAddress);
    await page.getByLabel('Password').fill('correct-horse-battery-staple');
    await page.getByRole('button', { name: 'Create account' }).click();

    const email = await mailslurp.waitForLatestEmail(inbox.id, 60_000, true);
    const { found, code } = await mailslurp.emailController.getEmailCodes({ emailId: email.id });
    expect(found).toBe(true);

    await page.getByLabel('Verification code').fill(code!);
    await page.getByRole('button', { name: 'Verify' }).click();
    await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
  } finally {
    await mailslurp.deleteInbox(inbox.id);
  }
});

Replace the URL, labels and final heading with your application’s own. Keep the test timeout longer than the email wait so that a slow message fails with a clear MailSlurp timeout rather than a generic runner timeout.

Test SMS verification codes

SMS codes need a real phone number that your application can send to. Provision a number in MailSlurp, then reserve it for the suite that uses it. Phone numbers are rented on Pro, Growth and Enterprise plans; see phone and SMS setup.

Record the time just before you trigger the SMS so that the wait ignores older messages on the same number:

const phoneNumberId = process.env.MAILSLURP_PHONE_NUMBER_ID;
if (!phoneNumberId) throw new Error('Set MAILSLURP_PHONE_NUMBER_ID');
const phone = await mailslurp.phoneController.getPhoneNumber({ phoneNumberId });

const smsSince = new Date();
// Enter phone.phoneNumber in your application and request the code here.
const sms = await mailslurp.waitController.waitForLatestSms({
  waitForSingleSmsOptions: {
    phoneNumberId: phone.id,
    since: smsSince,
    timeout: 60_000,
    unreadOnly: true,
  },
});
const match = /\b(\d{6})\b/.exec(sms.body);
if (!match) throw new Error(`No six-digit code in SMS: ${sms.body}`);
const smsCode = match[1];

Adjust the pattern to your sender’s message format. When several test workers need SMS at the same time, give each worker its own number with phone number pools instead of sharing one.

Test authenticator app MFA (TOTP)

Authenticator codes are not delivered anywhere. The application and the authenticator app each calculate the code from a shared secret and the current time. To test this, pair a MailSlurp virtual TOTP device with the secret your application issues during enrollment.

Most enrollment screens show a QR code that encodes an otpauth:// URL. Read that URL, or the base32 secret shown beside it, from the page or your test API:

const device = await mailslurp.mfaController.createTotpDeviceForOtpAuthUrl({
  createTotpDeviceOtpAuthUrlOptions: { otpAuthUrl },
});

const { code } = await mailslurp.mfaController.getTotpDeviceCode({
  id: device.id,
  minSecondsUntilExpire: 5,
});
// Enter code in the application's MFA challenge.

minSecondsUntilExpire asks MailSlurp for a code with at least that many seconds left in its time step, which avoids typing a code that expires mid-submit. If you only have the base32 secret, use createTotpDeviceForCustom with { secret }. Keep the device for later sign-in tests of the same user and delete it when the user is removed. MFA TOTP devices covers pairing values and enrollment in more detail.

Links need the same care as codes: the right link must arrive, work once and then stop working.

Wait for the email by subject so that a welcome or marketing message in the same inbox is not mistaken for the sign-in email, then read its links:

const email = await mailslurp.waitController.waitForMatchingFirstEmail({
  inboxId: inbox.id,
  timeout: 60_000,
  unreadOnly: true,
  matchOptions: { matches: [{ field: 'SUBJECT', should: 'CONTAIN', value: 'Sign in' }] },
});
const { links } = await mailslurp.emailController.getEmailLinks({ emailId: email.id });
const magicLink = links.find((link) => link.includes('/auth/magic'));
if (!magicLink) throw new Error('Sign-in link not found');
await page.goto(magicLink);

For a magic link, assert that the user is signed in, then open the same link again and expect it to be rejected. For a password reset, set a new password and check three things: the new password signs in, the old password no longer does, and the reset link refuses a second use. Use the Next.js Auth magic link guide for a complete framework example.

Cover the cases that break in production

A test that only follows the happy path misses most authentication defects. Add these cases once the basic journey passes.

Case How to test it with MailSlurp Expected result
Code expiry Wait for the code, hold it past your expiry window, then submit it The application rejects it with a clear message
Resend Request a second code, then use waitController.waitForMatchingEmails with count: 2 and submit the first code Only the newest code works
One-time use Submit a valid code, sign out, submit the same code again The second attempt fails
Wrong code limit Submit incorrect codes until your limit, then the correct one The account or challenge locks as designed
Delivery time Record the time before triggering and compare it with the message’s createdAt The message arrives within your budget
Address variants Use an inbox address with capitals or a plus suffix in sign-up The code still reaches the user and verifies the account
Authenticator time step Request TOTP codes with and without minSecondsUntilExpire near the boundary Your accepted time window matches your policy

Keep authentication tests reliable in CI

  • Create one inbox per test and set expiresIn so that abandoned inboxes disappear.
  • Use unreadOnly or a since time on waits so that a retry never reads a code from an earlier attempt.
  • Match by subject or sender when an inbox can receive more than one kind of message.
  • Make the runner’s timeout longer than every MailSlurp wait inside the test.
  • Delete inboxes in a finally block; keep phone numbers and TOTP devices that other suites reuse.
  • Store the API key and phone number IDs as CI secrets, never in the repository.

Troubleshoot message waits explains what to check when a wait times out.

Framework and provider examples

These guides build the same journeys in specific tools:

Tool or provider Guide
Playwright Test email and SMS with Playwright and SMS OTP and MFA with Playwright
Selenium Selenium email testing and Selenium with Auth0 TOTP MFA
Auth0 Playwright with Auth0 TOTP MFA
Page objects Page object model for email and SMS MFA
Email OTP Test one-time password emails
SMS 2FA Test 2FA SMS codes
Postman Test user authentication and MFA with Postman
ACCELQ ACCELQ email and OTP testing
Any framework Email and SMS integration testing guide