Test a password-reset email end to end
A password reset touches four things: the form, the email, the link in it, and the page the link opens. Mocking the email hides the part that breaks most often (wrong host in the link, expired token, missing template variable). This is the way to test the real email without a real mailbox.
The flow
Point the app at a dummy SMTP server. Open the sandbox, press Copy as .env, paste it into
.env. The app readsSMTP_HOST,SMTP_PORT,SMTP_USERandSMTP_PASS, with STARTTLS. Every email is caught and none is delivered, so the test can use any address.Note the inbox position (a mark) before the test triggers anything.
Trigger the reset in the UI or through the API, for a unique address such as
user-1730000000000@example.com.Wait for the email, filtered by that address and by the subject:
AUTH="Authorization: Bearer $EBH_SECRET" BASE="$EBH_URL/api/sessions/$EBH_ID" MARK=$(curl -fsS -H "$AUTH" "$BASE/messages" | jq '[.messages[].seq] | max // 0') # ... the test submits the "forgot password" form for user@example.com ... curl -fsS -H "$AUTH" \ "$BASE/wait?after=$MARK&to=user@example.com&subject=reset&timeout=60" > reset.json jq -r '.messages[0].links[0]' reset.json200returns the message;204means nothing arrived within the timeout, which is itself a test failure worth reporting.Open the link and finish the reset. Then assert the new password works and that the same link no longer works (reset links must be single-use).
The same steps in a browser test are in Playwright and Cypress.
What to assert about the email
| Check | How |
|---|---|
| It arrived, once | exactly one message for that address after the mark |
| Right subject and sender | subject, from in the message JSON |
| The link points at the right host | links[0] starts with your app's URL, not localhost or a staging host |
| The token works once | open links[0], set a password, open it again and expect an error |
| Expiry | if your tokens expire, test the expired case with your own clock or a short lifetime |
| Plain-text part exists | text is not empty, for clients that do not show HTML |
| One-time codes | codes[0] holds a code already extracted from the body |
Magic links and sign-up confirmation
Identical flow: only the subject filter and what you assert after opening the link change. For a six-digit code instead of a link, type codes[0] into the form.
When it should also pass the real internet
The dummy SMTP server shows you what your app produced. To see whether a real provider's mail would be accepted by a strict receiver, send it to the inbox's public address instead and read the SPF, DKIM and DMARC results on the message. See Getting started.
Related: Test email in CI, API reference, limits.