All posts
    Guides

    How to Test a Password Reset Flow End-to-End

    The Devmailr Team6 min read

    The forgot-password flow touches auth, email, tokens, and expiry — and it’s the flow users hit when they’re already frustrated. Yet it’s rarely tested end-to-end, because the reset link arrives by email. Here’s how to test the whole thing automatically.

    What the flow looks like

    1. User requests a reset (enters their email).
    2. App emails a reset link with a one-time token.
    3. User clicks the link → lands on a “set new password” page.
    4. User sets a new password; the token is consumed.
    5. User logs in with the new password.

    A good test exercises all five — including that the email actually sends and the link actually works.

    Catch the reset email

    Create a disposable Devmailr inbox, register a user with that address, request a reset, then wait for the email and grab the link:

    javascript
    const inbox = await createInbox(`reset-${Date.now()}`)
    await registerUser(inbox.address, 'old-password')   // your app
    
    // request the reset
    await requestPasswordReset(inbox.address)
    
    // wait for the email and read the reset link
    const email = await waitForEmail(inbox._id, 'subject=' + encodeURIComponent('Reset your password'))
    const resetLink = email.primaryLink

    primaryLink picks the main call-to-action — the reset URL — over footer/unsubscribe links. If your reset uses a code instead, read extractedCodes[0] or a match regex. (The createInbox/waitForEmail helpers come from the CI/CD guide.)

    Follow the link and set a new password

    With Playwright (or any driver), navigate to the link and complete the form:

    javascript
    await page.goto(resetLink)
    await page.getByLabel('New password').fill('new-password-123')
    await page.getByRole('button', { name: 'Set password' }).click()
    await expect(page.getByText('Password updated')).toBeVisible()

    Assert the important edge cases

    Reset flows fail in specific, testable ways. Worth covering:

    • Login works with the new password — and not the old one.
    • The token is single-use — visiting resetLink again should fail.
    • Expired tokens are rejected — if you can mint an old token, assert a clear error.
    • No account enumeration — requesting a reset for an unknown email returns the same generic response.

    A pure-API version

    You don’t need a browser to test the core — extract the token from the link and POST it:

    javascript
    const token = new URL(email.primaryLink).searchParams.get('token')
    const res = await fetch('https://staging.myapp.com/api/reset-password', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token, password: 'new-password-123' }),
    })
    expect(res.status).toBe(200)

    Related reading

    Try it in two minutes

    Create a disposable prefix@devmailr.app inbox over the API, send mail to it from your app, and read it in your tests. Free plan, no card required.