🧪 Test Hub
testing: Demo target v3.0.0 · built-in demo site

DEMO-023 Freight: password reset through the email

Edit Load test Duplicate

Requests a reset, waits for the email in the demo's mailbox, follows the link, sets a random new password, signs in with it -- then checks the link no longer works.

demoemailfreightsecurity version: 1.0 DEMO-023__freight_password_reset_email.py group: Freight extrasgroup: Python and TypeScript, side by side

Runs
7
Pass rate
86%
6 passed / 1 failed
Avg duration
10s
p95 11s
Estimate
10s
avg of last 10 judged runs
Flakiness
0.33
0 stable · 1 alternates
Current streak
3
passed

Last 7 results newest on the right — click a square to open that run

Duration per run point color = outcome; dashed = 7-run average; click a point to open the run

Step timing trends the same step compared across the last 7 runs — slowest steps first, one line each

How long it takes distribution of 7 runs — two humps mean two different behaviours hiding behind one average

fastest 9s · slowest 11s

Why this test failed grouped by message, last 90 days

timesmessagelast
1 AssertionError: the reset link worked a SECOND time: any old reset em… Sep 24 open

Outcomes by target version

What this test does plain language, derived from the code

  1. site = Freight(page, ctx)
  2. site.open("login/")
  3. Click #forgot-link
  4. page.fill("#forgot-login", ACCOUNT)
  5. Click #forgot-btn
  6. ref = page.inner_text("#forgot-ref").strip()
  7. ctx.log(f"reset requested, reference {ref}")
  8. ⏱ Timed phase: wait for the email
  9. site.open(f"mailbox/?to={ADDRESS}")
  10. subject = f"#mail-list a.mail-subject:has-text('ref {ref}')"
  11. page.wait_for_selector(subject, timeout=60_000)
  12. page.click(subject)
  13. page.wait_for_selector(RESET_LINK)
  14. message = page.url
  15. Take a screenshot (“the reset email”)
  16. new_password = "Pw-" + secrets.token_urlsafe(12)
  17. ⏱ Timed phase: choose a new password
  18. page.click(RESET_LINK)
  19. page.fill("#new-password", new_password)
  20. page.fill("#confirm-password", new_password)
  21. Click #reset-btn
  22. Wait until #login-btn appears
  23. ⏱ Timed phase: sign in with the new password
  24. page.fill("#username", ACCOUNT)
  25. page.fill("#password", new_password)
  26. Click #login-btn
  27. Wait until #signed-in-user appears
  28. Note in the log: “signed in with the new password”
  29. Click #sign-out
  30. Wait until #login-btn appears
  31. ⏱ Timed phase: follow the same link again
  32. page.goto(message)
  33. page.click(RESET_LINK)
  34. Wait until #reset-invalid, #reset-form appears
  35. Take a screenshot (“the same link, a second time”)
  36. assert page.locator("#reset-form").count() == 0, (
  37. "the reset link worked a SECOND time: any old reset email is still a key to the "
  38. "account -- a reset link must stop working once it has been used")
  39. ctx.log(page.inner_text("#reset-invalid-why"))
Show the code
"""A password reset, end to end -- through the email.

The flow people test by hand because "it goes through email": ask for a
reset, WAIT for the email (a mail queue is not instant), open it, follow the
link, choose a new password, sign in with it -- and then check the link is
dead, because a reset link that works twice turns every old email into a key
to the account.

The demo catches its outgoing mail in a Mailbox page (as MailHog or Mailpit
do in a real test environment), and the test correlates by the request's
reference number: other resets may be in the mailbox too. The reset link
carries a secret token, so the test CLICKS it in the email rather than
navigating to it -- a navigation would write the token into the run's log.
The new password is random per run and never logged.
"""
import secrets

from _lib.freight import Freight

ACCOUNT = "driver"                               # tester/manager are shared: never reset
ADDRESS = "driver@acme-freight.example"
RESET_LINK = "#mail-body a[href*='/login/reset/']"


def run(page, ctx):
    site = Freight(page, ctx)
    site.open("login/")
    page.click("#forgot-link")
    page.fill("#forgot-login", ACCOUNT)
    page.click("#forgot-btn")
    ref = page.inner_text("#forgot-ref").strip()
    ctx.log(f"reset requested, reference {ref}")

    with ctx.timed("wait for the email"):
        site.open(f"mailbox/?to={ADDRESS}")
        # the mailbox checks for new mail every two seconds and reloads itself
        subject = f"#mail-list a.mail-subject:has-text('ref {ref}')"
        page.wait_for_selector(subject, timeout=60_000)
    page.click(subject)
    page.wait_for_selector(RESET_LINK)
    message = page.url
    ctx.screenshot("the reset email")

    new_password = "Pw-" + secrets.token_urlsafe(12)
    with ctx.timed("choose a new password"):
        page.click(RESET_LINK)
        page.fill("#new-password", new_password)
        page.fill("#confirm-password", new_password)
        page.click("#reset-btn")
        page.wait_for_selector("#login-btn")
    with ctx.timed("sign in with the new password"):
        page.fill("#username", ACCOUNT)
        page.fill("#password", new_password)
        page.click("#login-btn")
        page.wait_for_selector("#signed-in-user")
    ctx.log("signed in with the new password")
    page.click("#sign-out")
    page.wait_for_selector("#login-btn")

    with ctx.timed("follow the same link again"):
        page.goto(message)
        page.click(RESET_LINK)
        page.wait_for_selector("#reset-invalid, #reset-form")
    ctx.screenshot("the same link, a second time")
    assert page.locator("#reset-form").count() == 0, (
        "the reset link worked a SECOND time: any old reset email is still a key to the "
        "account -- a reset link must stop working once it has been used")
    ctx.log(page.inner_text("#reset-invalid-why"))

All runs

RunStatusQueuedDuration VersionTriggerBatch
#473 passed 2026-09-25 00:30:11 9s 3.0.0 group group: Python and TypeScript,…
#363 passed 2026-09-24 22:05:07 11s 3.0.0 group group: Freight extras
#348 passed 2026-09-24 20:41:19 9s 3.0.0 cli-adopted 3 tests (terminal)
#345 failed 2026-09-24 20:41:04 9s 2.1.0 cli-adopted 3 tests (terminal)
#342 passed 2026-09-24 20:40:49 11s 2.0.0 cli-adopted 3 tests (terminal)
#339 passed 2026-09-24 20:40:35 11s 1.1.0 cli-adopted 3 tests (terminal)
#336 passed 2026-09-24 20:40:21 9s 1.0.0 cli-adopted 3 tests (terminal)