>_ emailblackhole

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

  1. Point the app at a dummy SMTP server. Open the sandbox, press Copy as .env, paste it into .env. The app reads SMTP_HOST, SMTP_PORT, SMTP_USER and SMTP_PASS, with STARTTLS. Every email is caught and none is delivered, so the test can use any address.

  2. Note the inbox position (a mark) before the test triggers anything.

  3. Trigger the reset in the UI or through the API, for a unique address such as user-1730000000000@example.com.

  4. 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.json
    

    200 returns the message; 204 means nothing arrived within the timeout, which is itself a test failure worth reporting.

  5. 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

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.