← Back to Blog

Magic Link Testing Without Polluting Real Inboxes

Stop using shared Gmail accounts for magic link tests. Learn how to test password-reset and magic-link authentication flows with ephemeral inboxes in your E2E tests.

testingmagic-linksauthenticatione2e
Magic Link Testing Without Polluting Real Inboxes

Testing magic link and password-reset flows in end-to-end tests often leads to messy workarounds: shared Gmail accounts that fill up with test emails, flaky IMAP parsing, or worse—mocking the entire email flow and losing confidence that the feature works.

A better approach uses ephemeral, per-test disposable inboxes that receive real email, extract the link automatically, and expire after the test completes.

The Problem with Real Inboxes in Tests

When your app sends magic links or password-reset URLs via email, your E2E tests need to:

  1. Receive real email to validate the full flow
  2. Extract the link from HTML or plain text
  3. Continue the browser flow by visiting the extracted URL
  4. Avoid state pollution between test runs

Traditional approaches create problems:

  • Shared team Gmail/Outlook — Tests race for the same inbox; old emails confuse new tests
  • IMAP polling libraries — Complex, slow, and brittle; parsing multipart MIME is error-prone
  • Mocking the email service — Your tests pass, but production magic links break silently
  • Manual testing only — Slows down iteration and misses regressions

Why Ephemeral Inboxes Solve This

Disposable email inboxes designed for testing give each test run a unique, temporary address. After the test, the inbox expires automatically—no cleanup required, no state leakage.

Key benefits for magic link testing:

  • Unique inbox per test — Parallel test runs never interfere
  • Link extraction built-in — No regex parsing of raw MIME
  • REST API access — Simple polling from any language or CI environment
  • Real email delivery — Tests the full SMTP flow, not a mock
  • Auto-expiry — Old test data never piles up

Implementation Example

Here's how to test a magic link flow with OTPBox:

// Playwright/Puppeteer example: test magic link authentication
const { test, expect } = require('@playwright/test');

async function createInbox() {
  const response = await fetch('https://otpbox.app/api/inbox', {
    method: 'POST'
  });
  const data = await response.json();
  return { token: data.token, email: data.email };
}

async function getMagicLink(token, maxAttempts = 10) {
  for (let i = 0; i < maxAttempts; i++) {
    const response = await fetch(`https://otpbox.app/api/inbox/${token}/latest`);
    
    if (response.ok) {
      const data = await response.json();
      if (data.link) {
        return data.link;
      }
    }
    
    // Wait before retrying (email delivery typically takes 1-5 seconds)
    await new Promise(resolve => setTimeout(resolve, 3000));
  }
  
  throw new Error('Magic link not received after 30 seconds');
}

test('user can log in via magic link', async ({ page }) => {
  // 1. Create a disposable inbox for this test
  const { token, email } = await createInbox();
  console.log(`Test inbox: ${email}`);
  
  // 2. Start magic link flow in the app
  await page.goto('https://yourapp.com/login');
  await page.fill('input[name="email"]', email);
  await page.click('button:has-text("Send Magic Link")');
  
  await expect(page.locator('text=Check your email')).toBeVisible();
  
  // 3. Poll for the magic link
  console.log('Waiting for magic link...');
  const magicLink = await getMagicLink(token);
  console.log(`Received magic link: ${magicLink}`);
  
  // 4. Visit the magic link in the same browser session
  await page.goto(magicLink);
  
  // 5. Verify user is logged in
  await expect(page.locator('text=Dashboard')).toBeVisible();
  await expect(page.locator('text=Logout')).toBeVisible();
});

The critical parts:

  1. POST /api/inbox returns { email, token } as separate fields
  2. GET /api/inbox/{token}/latest returns { link, otp, ... } with extracted magic link
  3. Visit the link in the same browser context to continue the authenticated session

Alternative Strategies (And Their Trade-offs)

Before reaching for disposable email, consider these alternatives:

Local SMTP Catchers

Tools like Mailpit or MailHog run a local SMTP server that captures outgoing email during development and testing.

Pros: Fast, no external dependencies, full control
Cons: Requires setup/Docker in CI; not realistic for testing deliverability; manual link extraction

Public Temp Mail

Services like Mailinator or Guerrilla Mail offer free temporary email addresses.

Pros: Free, no signup
Cons: No official APIs; inboxes are public (anyone can read); often blacklisted by verification services

Paid Inbox APIs

Dedicated services like Mailtrap or Mailosaur provide email testing APIs with advanced features.

Pros: Robust APIs, team features, message search
Cons: Paid plans; overkill for simple magic link extraction

For most teams, a free disposable inbox service with link extraction offers the best balance: real email delivery, simple API, and no cost.

Pattern: Extract and Visit

The standard magic link test pattern:

#!/bin/bash
set -e

# 1. Create inbox
RESP=$(curl -s -X POST https://otpbox.app/api/inbox)
TOKEN=$(echo "$RESP" | jq -r '.token')
EMAIL=$(echo "$RESP" | jq -r '.email')

# 2. Trigger magic link from your app
curl -X POST https://yourapp.com/api/auth/magic-link \
  -H "Content-Type: application/json" \
  -d "{\"email\": \"${EMAIL}\"}"

# 3. Poll for the link
for i in {1..10}; do
  RESP=$(curl -s "https://otpbox.app/api/inbox/${TOKEN}/latest")
  LINK=$(echo "$RESP" | jq -r '.link // empty')
  
  if [ -n "$LINK" ] && [ "$LINK" != "null" ]; then
    echo "✓ Magic link received: $LINK"
    
    # 4. Open link in headless browser or validate token
    # Example: curl "$LINK" or visit in Playwright/Selenium
    exit 0
  fi
  
  echo "Waiting for magic link (attempt $i/10)..."
  sleep 3
done

echo "✗ Timeout: No magic link received"
exit 1

This pattern works for:

  • Magic link authentication
  • Password reset links
  • Email verification links
  • Unsubscribe URLs (if you need to test them)

Best Practices

When testing magic links in CI:

  1. One inbox per test run — Never reuse inboxes; parallel tests will collide
  2. Poll with reasonable timeouts — 30 seconds is usually enough; if delivery takes longer, something is wrong
  3. Test failure cases — Expired links, invalid tokens, rate limits
  4. Log the inbox email — Makes debugging failed tests much easier
  5. Don't commit tokens — Disposable inboxes are temporary by design; no need to store them

When to Use This Approach

Use ephemeral inboxes for magic link testing when:

  • You run E2E tests in CI/CD that need real email delivery
  • Your app sends magic links, password resets, or verification URLs
  • You want to avoid shared test email accounts
  • You need to test cross-device flows (send email, open link elsewhere)

Don't use this approach if you can get away with mocking or if your tests run entirely offline. For unit tests, mock the email service. For integration tests without browsers, this pattern still works—just validate the link format instead of visiting it.

Related Articles

Resources

Testing magic links doesn't require a dedicated Gmail account or complex IMAP libraries. With ephemeral inboxes, your tests receive real email, extract the link automatically, and clean up after themselves—leaving your test suite fast, reliable, and maintainable.