End-to-end testing overview
Testing
Passwordless authentication with email OTP provides a seamless user experience, but testing these flows presents a unique challenge: you need to programmatically capture and extract verification codes from emails. Unlike traditional password-based authentication, passwordless flows require coordinating between your test automation and email delivery systems.
Passwordless authentication flows send a one-time password (OTP) via email. To automate testing, you need to:
Test email services like Mailosaur or Mailtrap provide API access to test inboxes, making it easy to retrieve OTP codes programmatically.
Mailosaur provides dedicated test email addresses and an API to retrieve messages:
import MailosaurClient from 'mailosaur';
const mailosaur = new MailosaurClient(process.env.MAILOSAUR_API_KEY!);const serverId = process.env.MAILOSAUR_SERVER_ID!;const timestamp = Date.now()const testEmail = `test-${timestamp}@${serverId}.mailosaur.net`
// Request OTPawait page.fill('input[name="p_email"]', testEmail);await page.click('button[type="submit"]');
// Wait for OTP emailconst email = await mailosaur.messages.get(serverId, { sentTo: testEmail,});
// Extract 6-digit OTP code from emailconst otpMatch = email.text?.body?.match(/\b(\d{6})\b/);const otpCode = otpMatch?.[1];
if (!otpCode) { throw new Error('Could not extract OTP code from email');}
// Enter OTPawait page.fill('input[name="p_confirmation_code"]', otpCode);await page.click('button[type="submit"]');
// Clean up - delete the emailawait mailosaur.messages.del(email.id!);Mailtrap also provides API access to test inboxes:
import MailtrapClient from 'mailtrap';
const mailtrap = new MailtrapClient({ token: process.env.MAILTRAP_API_TOKEN!,});
const inboxId = process.env.MAILTRAP_INBOX_ID!;const timestamp = Date.now();const testEmail = `test-${timestamp}@${process.env.MAILTRAP_DOMAIN}`;
// Request OTPawait page.fill('input[name="p_email"]', testEmail);await page.click('button[type="submit"]');
// Wait for OTP email (polling)let email = null;const startTime = Date.now();while (!email && Date.now() - startTime < 30000) { const messages = await mailtrap.testing.inboxes.getMessages(inboxId); email = messages.find((msg) => msg.to_email === testEmail); if (!email) { await new Promise((r) => setTimeout(r, 1000)); }}
if (!email) { throw new Error('Timeout waiting for OTP email');}
// Extract OTP codeconst otpMatch = email.html_body?.match(/\b(\d{6})\b/);const otpCode = otpMatch?.[1];
// Enter OTPawait page.fill('input[name="p_confirmation_code"]', otpCode);await page.click('button[type="submit"]');If you have your own email infrastructure, you can route test emails to cloud storage (S3, database, etc.) and query them from your tests.
Route test emails to S3, then query from your tests:
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'us-east-1' });
export async function waitForOTPEmail( email: string, timeoutMs = 30000): Promise<string> { const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) { try { const command = new GetObjectCommand({ Bucket: 'your-test-emails-bucket', Key: `${email}/latest.txt`, });
const response = await s3.send(command); const body = await response.Body?.transformToString();
const otpMatch = body?.match(/\b(\d{6})\b/); if (otpMatch) { return otpMatch[1]; } } catch (e) { // Email not arrived yet }
await new Promise((r) => setTimeout(r, 1000)); }
throw new Error('Timeout waiting for OTP email');}Store emails in a database and query them:
import { db } from './db';
export async function waitForOTPEmail( email: string, timeoutMs = 30000): Promise<string> { const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) { const emailRecord = await db.emails.findOne({ where: { to: email }, orderBy: { createdAt: 'desc' }, });
if (emailRecord) { const otpMatch = emailRecord.body?.match(/\b(\d{6})\b/); if (otpMatch) { return otpMatch[1]; } }
await new Promise((r) => setTimeout(r, 1000)); }
throw new Error('Timeout waiting for OTP email');}Configure a subdomain’s email to be programmatically accessible through IMAP or a custom API:
import Imap from 'imap';
const imap = new Imap({ user: process.env.TEST_EMAIL_USER!, password: process.env.TEST_EMAIL_PASSWORD!, host: 'imap.example.com', port: 993, tls: true,});
export async function getOTPFromEmail( email: string, timeoutMs = 30000): Promise<string> { return new Promise((resolve, reject) => { imap.once('ready', () => { imap.openBox('INBOX', false, () => { imap.search(['UNSEEN', ['TO', email]], (err, results) => { if (err) reject(err);
const fetch = imap.fetch(results, { bodies: '' }); fetch.on('message', (msg) => { msg.on('body', (stream) => { let body = ''; stream.on('data', (chunk) => { body += chunk.toString('utf8'); }); stream.once('end', () => { const otpMatch = body.match(/\b(\d{6})\b/); if (otpMatch) { resolve(otpMatch[1]); } }); }); }); }); }); });
imap.connect();
setTimeout(() => { reject(new Error('Timeout waiting for OTP email')); }, timeoutMs); });}The following limitations apply to passwordless flow testing:
Test passwordless flows with: