Most signup flows send a verification email with a link or a code. That step is easy to leave untested because it needs a real inbox — so teams stub it out, and then a broken template or a dead link ships to production. You can test it for real instead: create a throwaway address, run your signup, and read the email back over HTTP.
receivemail.dev for a fresh address. You get back the address and a
per-mailbox secret used to read it later.# 1. create the mailbox
resp=$(curl -s -X POST https://receivemail.dev/mailboxes)
addr=$(echo "$resp" | jq -r .address)
secret=$(echo "$resp" | jq -r .secret)
# 2. run your signup against $addr (your app / your API call here)
curl -s -X POST https://your-app.example/api/signup \
-H 'content-type: application/json' \
-d "{\"email\":\"$addr\",\"password\":\"test-Pa55w0rd\"}"
# 3. poll for the verification mail (up to ~30s)
for i in $(seq 1 15); do
body=$(curl -s "https://receivemail.dev/mailboxes/$addr/messages" \
-H "Authorization: Bearer $secret")
link=$(echo "$body" | jq -r '.messages[0].body_text // ""' \
| grep -oE 'https://your-app\.example/verify\?token=[A-Za-z0-9._-]+' | head -1)
[ -n "$link" ] && break
sleep 2
done
# 4. hit the verification link
test -n "$link" && curl -s -o /dev/null -w '%{http_code}\n' "$link"
import { test, expect, request } from '@playwright/test';
test('new user can verify their email', async ({ page }) => {
const api = await request.newContext();
// fresh inbox
const mb = await (await api.post('https://receivemail.dev/mailboxes')).json();
// sign up through the real UI
await page.goto('/signup');
await page.getByLabel('Email').fill(mb.address);
await page.getByLabel('Password').fill('test-Pa55w0rd');
await page.getByRole('button', { name: 'Create account' }).click();
// wait for the verification email to land
let link: string | undefined;
await expect.poll(async () => {
const res = await api.get(
`https://receivemail.dev/mailboxes/${mb.address}/messages`,
{ headers: { Authorization: `Bearer ${mb.secret}` } },
);
const msg = (await res.json()).messages[0];
link = msg?.body_text?.match(/https:\/\/[^\s"]+verify[^\s"]*/)?.[0];
return Boolean(link);
}, { timeout: 30_000 }).toBe(true);
await page.goto(link!);
await expect(page.getByText('Email verified')).toBeVisible();
});
The polling boilerplate is packaged up so you don't have to copy it:
npm i -D playwright-receivemail # or: cypress-receivemail
import { test, expect } from 'playwright-receivemail';
test('new user can verify their email', async ({ page, mailbox }) => {
await page.goto('/signup');
await page.getByLabel('Email').fill(mailbox.address);
await page.getByLabel('Password').fill('test-Pa55w0rd');
await page.getByRole('button', { name: 'Create account' }).click();
const link = await mailbox.waitForMatch(/https:\/\/[^\s"]+verify[^\s"]*/, {
subjectContains: 'verify',
});
await page.goto(link);
await expect(page.getByText('Email verified')).toBeVisible();
});
Cypress: cy.createMailbox(), then
cy.getEmailMatch(/\d{6}/) for a code or a link regex. See
playwright-receivemail
/ cypress-receivemail.
secret from the create response.@receivemail.dev accepts mail, so you can also hard-code a
known address for a fixture, then read it with the admin token.