How to Test a Password Reset Flow End-to-End
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
- User requests a reset (enters their email).
- App emails a reset link with a one-time token.
- User clicks the link → lands on a “set new password” page.
- User sets a new password; the token is consumed.
- 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:
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.primaryLinkprimaryLink 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:
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
resetLinkagain 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:
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.