Files
RenewCMS/features/reset_confirm_screen.feature

133 lines
8.2 KiB
Gherkin

Feature: Password reset confirm screen
As a user who opened the reset link from their email
I want /register/reset/validate?c=CODE&email=EMAIL to let me choose a new password via POST
/v1/auth/password-reset/confirm, checking length and match in the browser first, and to tell
me plainly when the link is incomplete, the code is wrong or expired, or the server failed
So that finishing a password reset is a single form from the email, with the same neutral
"invalid or expired" message whether the code was wrong, reused, or the account doesn't exist
Background:
Given /register/reset/validate is a public route (meta.auth "public") rendered as
ResetConfirmPage inside AuthShell, per docs/design/routes.md
And ResetConfirmPage only calls POST /v1/auth/password-reset/confirm
And on mount, ResetConfirmPage reads the "c" and "email" query parameters once: if either is
absent or an empty string, no request is ever made and the Missing parameters state renders
immediately instead of the form
And when both are present, the form has a read-only Email field (value from the "email" query
parameter, help text "From your reset link."), a New password field (password input with
show/hide, a live checklist item "At least 8 characters" that starts unmet/gray, turns
error/red only after a failed submit, and turns met/green once satisfied), and a Confirm new
password field (password input with show/hide, a live checklist item "Passwords match"
with the same unmet/error/met states)
And New password's length is checked on submit only: fewer than 8 characters leaves its
checklist item in its error state and blocks the request
And Confirm new password's match against New password is checked on blur of that field and
again on submit: a mismatch shows the error "The passwords don't match." and leaves the
"Passwords match" checklist item in its error state, and blocks the request
And a submit that passes both checks calls POST /v1/auth/password-reset/confirm with
{ email, code: c, password: <New password> }; the Confirm new password value is never sent
And on a 200 response, the app navigates to "/login?reason=password-changed", whose
rendering is covered by features/login_screen.feature's "Password changed" scenario, not
re-specified here
And a 403 response (per features/reset_password.feature, covering an unknown email, a wrong
code, an already-used code, or an expired code alike) replaces the whole form with a
ResultPanel
And a 500 response, or a request that never reaches the server, is surfaced with the app's
generic network/server-error alert — icon "i-lucide-circle-x", title "Couldn't reach
RenewCMS", description "Check your connection and try again.", the same copy LoginPage and
RegisterPage already use for the same failure; the form is not replaced, both password
fields keep whatever the user typed, and the form re-enables, since
docs/design/screens/reset-password.md has no state of its own for this response and this
is the app's generic pattern for it (per the user's decision on this slice)
Scenario: Default — both parameters present, an untouched form
When the user navigates to "/register/reset/validate?c=abc123&email=marta%40renew.example"
Then the heading is "Choose a new password"
And the description is "Pick something you haven't used here before. You'll log in with it
next."
And the Email field shows "marta@renew.example", read-only, with help "From your reset link."
And the New password field shows no value, with checklist item "At least 8 characters" in its
unmet state
And the Confirm new password field shows no value, with checklist item "Passwords match" in
its unmet state
And the submit button reads "Change password"
Scenario: Too short — fewer than 8 characters blocks submission
Given the user is on the reset-confirm form with New password "lime" and Confirm new
password "lime"
When the user submits
Then POST /v1/auth/password-reset/confirm is never called
And the New password checklist item "At least 8 characters" shows its error state
And the Confirm new password checklist item "Passwords match" shows its met state, since both
fields hold the same value
Scenario: Mismatch — caught on blur and on submit
Given the user is on the reset-confirm form with New password "limewash2026"
When the user types "limewash2025" into Confirm new password and blurs the field
Then the Confirm new password field shows the error "The passwords don't match." and its
checklist item "Passwords match" shows its error state
When the user submits with these same values
Then POST /v1/auth/password-reset/confirm is never called
And the same mismatch error is still shown
Scenario: Submitting — fields and button are disabled while the request is in flight
Given the user is on the reset-confirm form with New password "limewash2026" and Confirm new
password "limewash2026"
When the user submits and POST /v1/auth/password-reset/confirm has not yet resolved
Then the New password and Confirm new password fields are disabled
And the submit button reads "Changing password…" and shows a loading state
Scenario: Success — a 200 hands off to the log-in screen
Given the user is on "/register/reset/validate?c=abc123&email=marta%40renew.example" with
New password "limewash2026" and Confirm new password "limewash2026"
When the user submits and POST /v1/auth/password-reset/confirm resolves 200
Then the app navigates to "/login?reason=password-changed"
Scenario: Invalid or expired — a 403 replaces the form with a ResultPanel
Given the user is on "/register/reset/validate?c=abc123&email=marta%40renew.example" with
New password "limewash2026" and Confirm new password "limewash2026"
When the user submits and POST /v1/auth/password-reset/confirm resolves 403 with the
apperr.PasswordResetFailedErrorMessage body
Then the form is no longer shown
And a ResultPanel is shown with tone "error", icon "i-lucide-link-2-off", title "This reset
link has expired", text "Reset links work once, for 1 hour. Request a new one and use it
within the hour."
And its actions are a primary "Request a new link" and a secondary "Back to log in"
When the user activates "Request a new link"
Then the app navigates to /register/reset/request
When instead the user activates "Back to log in"
Then the app navigates to /login
Scenario Outline: Server or network error — the form keeps its values and re-enables
Given the user is on "/register/reset/validate?c=abc123&email=marta%40renew.example" with
New password "limewash2026" and Confirm new password "limewash2026"
When the user submits and POST /v1/auth/password-reset/confirm <failure>
Then an error alert reads "Couldn't reach RenewCMS" with description "Check your connection
and try again."
And the form is still shown, not replaced by a ResultPanel
And the New password field still shows "limewash2026" and the Confirm new password field
still shows "limewash2026", both re-enabled
And the submit button reads "Change password" again, no longer loading
Examples:
| failure |
| fails with a 500 response |
| never reaches the server |
Scenario Outline: Missing parameters — either missing parameter blocks the request
When the user navigates to <path>
Then no request is made
And a ResultPanel is shown with tone "error", icon "i-lucide-link-2-off", title "This link
is incomplete", text "Part of the reset link is missing. Open it from your email again, or
request a new one."
And its single action is a primary "Request a new link"
When the user activates "Request a new link"
Then the app navigates to /register/reset/request
Examples:
| path |
| "/register/reset/validate?email=marta%40renew.example" |
| "/register/reset/validate?c=abc123" |
| "/register/reset/validate" |