204 lines
13 KiB
Gherkin
204 lines
13 KiB
Gherkin
Feature: Verify account screen
|
|
|
|
As a user who just clicked the verification link in their email
|
|
I want /register/validate?c=CODE to submit that code automatically, land me on my dashboard once
|
|
it's accepted, tell me plainly when it's wrong or expired, and let me log in inline if I opened
|
|
the link somewhere I'm not logged in
|
|
So that verifying my account is a single click from my inbox, whatever device or session state I
|
|
open the link in
|
|
|
|
Background:
|
|
Given /register/validate is a "public" route (meta.auth "public"), rendered as VerifyPage
|
|
inside AuthShell, per docs/design/routes.md; the router's beforeEach never runs session.check
|
|
for a public route, so VerifyPage starts with no assumption about the session
|
|
And VerifyPage only calls POST /v1/auth/verify, DELETE /v1/auth/register, and POST
|
|
/v1/auth/logout (and, inline, POST /v1/auth/login on a 401)
|
|
And on mount, VerifyPage reads the "c" query parameter once: if it is absent or an empty
|
|
string, no request is ever made and the Missing code state renders immediately; otherwise the
|
|
Verifying state renders and POST /v1/auth/verify is called immediately with { code: c }
|
|
And every call to POST /v1/auth/verify (the initial one and the post-login retry) passes
|
|
{ onUnauthorized: 'passthrough', onForbidden: 'passthrough' } (the same onForbidden
|
|
RequestOptions field added to useApi by features/pending_screen.feature, plus the existing
|
|
onUnauthorized field used the same way getMe() already uses it), so a 401 renders the inline
|
|
log-in state instead of the global redirect to /login, and a 403 renders the Invalid-or-
|
|
expired state instead of the global "unverified session" redirect
|
|
And Verifying and No-session-retrying both render the same full-card spinner: a centered
|
|
"i-lucide-loader-2" icon with a spin animation, the title "Verifying your account…" and the
|
|
line "This only takes a moment.", as the design board's AuthCard spinner draws it,
|
|
role="status", replacing AuthShell's whole card body with no title header shown
|
|
And on a 200, VerifyPage shows a ResultPanel with tone "success", icon "i-lucide-shield-check",
|
|
title "Your account is verified", text "Taking you to your dashboard…", and no actions, while
|
|
a toast (color "success", icon "i-lucide-circle-check", title "Your account is verified",
|
|
description "Welcome to RenewCMS.") is shown immediately over that panel, as the design board
|
|
draws it, while concurrently awaiting session.retry() (needed so /home's route guard sees the
|
|
now-verified session instead of whatever stale state this tab last cached) and an 800 ms
|
|
timer; once both settle, the app navigates to /home
|
|
And on a 403 (user.ErrInvalidVerificationCode or user.ErrVerificationCodeExpired), VerifyPage
|
|
shows a ResultPanel with tone "error", icon "i-lucide-link-2-off", title "This verification
|
|
link doesn't work", text "It may have expired (links last 2 hours) or already been used.",
|
|
and a note (rendered via the default slot) "Cancel your registration and start over to get a
|
|
new link.", with a primary action "Cancel registration and start over" and a secondary action
|
|
"Log out"; unlike Pending's cancel flow, this button opens no ConfirmDialog (verify.md's state
|
|
list has no confirm step for it) and calls DELETE /v1/auth/register directly, passing
|
|
{ onForbidden: 'passthrough' } for the same reason Pending's own DELETE call does
|
|
And "Cancel registration and start over" resolving 200 navigates to /register and shows the
|
|
same toast Pending's own Cancelled state shows (color "success", icon "i-lucide-circle-check",
|
|
title "Registration cancelled", description "You can create the owner account again."),
|
|
reusing that copy since it is the same underlying event
|
|
And "Cancel registration and start over" failing — with a 403 (already verified elsewhere, so
|
|
there is nothing left to cancel), a 500, or a request that never reaches the server — shows an
|
|
alert on the same ResultPanel screen via AuthShell's #notice slot, reusing Register's existing
|
|
server-error pattern: color "error", icon "i-lucide-circle-x", title "Couldn't cancel
|
|
registration", description "Something went wrong on the server. Try again in a moment.",
|
|
variant "soft", not closable, role "alert"; the ResultPanel and its two actions stay exactly
|
|
as they were so the user can try again or choose "Log out" instead; no redirect happens for
|
|
the 403 case here, unlike Pending's own already-verified handling
|
|
And "Log out" (on the Invalid-or-expired state) behaves exactly as it does on Pending: it calls
|
|
POST /v1/auth/logout, which per loginHandler.Logout always responds 200 and clears the jwt
|
|
cookie, then VerifyPage calls session.clear() and navigates to /login, disabling both actions
|
|
while the call is in flight
|
|
And on a 401, VerifyPage replaces the spinner with an inline log-in form: AuthShell title "Log
|
|
in to finish verifying", description "You opened this link somewhere you're not logged in.
|
|
Log in here and we'll verify your account right after.", a Username field (placeholder "Your
|
|
username") and a Password field (password input with a show/hide toggle), and a submit button
|
|
"Log in and verify"; there is no "Forgot password?" hint and no registration-open footer,
|
|
unlike LoginView
|
|
And the inline form's submit button is enabled, as the design board draws it; on submit,
|
|
LoginView's own validation runs first (Username under 3 characters shows "Use at least 3
|
|
characters.", an empty Password shows "Enter your password.", and nothing is sent), and a
|
|
valid form calls POST /v1/auth/login with { username, password }
|
|
And a 200 from that login call transitions to the No-session-retrying spinner and immediately
|
|
retries POST /v1/auth/verify with the same code and the same passthrough options, which then
|
|
follows the 200/403 handling above
|
|
And a 403 (wrong credentials), 429, or 500/network failure from that inline login call reuses
|
|
LoginView's existing alert copy for the same statuses (wrongCredentialsAlert, with LoginView's
|
|
exact title/description; tooManyAttemptsAlert with the same retryAfter countdown behavior as
|
|
LoginView; serverErrorAlert), shown via AuthShell's #notice slot, since verify.md specifies no
|
|
separate copy for this embedded form and states.md's HTTP-status table assigns this copy
|
|
generically to "Log in" contexts
|
|
And while that inline login is submitting, both fields are disabled and the submit button reads
|
|
"Logging in…" with a loading indicator, mirroring LoginView's submitting state
|
|
|
|
Scenario: Verifying — the code submits automatically on load
|
|
When the user navigates to "/register/validate?c=abc123"
|
|
Then the full-card spinner is shown with the text "Verifying your account…"
|
|
And POST /v1/auth/verify is called once, immediately, with body {"code": "abc123"}
|
|
|
|
Scenario: Success — 200 refreshes the session and navigates to /home with a toast
|
|
Given the user is on "/register/validate?c=abc123" in the Verifying state
|
|
When POST /v1/auth/verify resolves 200
|
|
Then a ResultPanel is shown with tone "success", icon "i-lucide-shield-check", title "Your
|
|
account is verified", text "Taking you to your dashboard…", and no actions
|
|
And a toast is shown over it with color "success", icon "i-lucide-circle-check", title "Your
|
|
account is verified", description "Welcome to RenewCMS."
|
|
And session.retry() is awaited
|
|
And after 800 ms and once session.retry() has resolved, the app navigates to /home
|
|
|
|
Scenario: Invalid or expired — 403 offers a way to start over
|
|
Given the user is on "/register/validate?c=abc123" in the Verifying state
|
|
When POST /v1/auth/verify resolves 403
|
|
Then a ResultPanel is shown with tone "error", icon "i-lucide-link-2-off", title "This
|
|
verification link doesn't work", text "It may have expired (links last 2 hours) or already
|
|
been used.", and note "Cancel your registration and start over to get a new link."
|
|
And its actions are a primary "Cancel registration and start over" and a secondary "Log out"
|
|
|
|
Scenario: Cancel registration and start over — success
|
|
Given the ResultPanel from "Invalid or expired" is shown
|
|
When the user activates "Cancel registration and start over"
|
|
Then DELETE /v1/auth/register is sent directly, without any confirmation dialog, with
|
|
{ onForbidden: 'passthrough' }
|
|
When it resolves 200
|
|
Then the app navigates to /register
|
|
And a toast is shown with color "success", icon "i-lucide-circle-check", title "Registration
|
|
cancelled", description "You can create the owner account again."
|
|
|
|
Scenario Outline: Cancel registration and start over — failure shows an alert, not a redirect
|
|
Given the ResultPanel from "Invalid or expired" is shown
|
|
When the user activates "Cancel registration and start over"
|
|
And DELETE /v1/auth/register <failure>
|
|
Then an alert is shown with color "error", icon "i-lucide-circle-x", title "Couldn't cancel
|
|
registration", description "Something went wrong on the server. Try again in a moment."
|
|
And the ResultPanel and its two actions ("Cancel registration and start over", "Log out") are
|
|
still shown, unchanged, so the user can retry or log out instead
|
|
And no navigation happens
|
|
|
|
Examples:
|
|
| failure |
|
|
| resolves 403 with the apperr.AlreadyVerifiedErrorMessage body |
|
|
| fails with a 500 response |
|
|
| never reaches the server |
|
|
|
|
Scenario: Log out from the Invalid-or-expired state
|
|
Given the ResultPanel from "Invalid or expired" is shown
|
|
When the user activates "Log out"
|
|
Then POST /v1/auth/logout is sent
|
|
When it resolves 200
|
|
Then the session store is cleared
|
|
And the app navigates to /login
|
|
|
|
Scenario: No session — the inline log-in form replaces the spinner
|
|
Given the user is on "/register/validate?c=abc123" in the Verifying state
|
|
When POST /v1/auth/verify resolves 401
|
|
Then the spinner is replaced by a form titled "Log in to finish verifying" with description
|
|
"You opened this link somewhere you're not logged in. Log in here and we'll verify your
|
|
account right after."
|
|
And it has a Username field (placeholder "Your username") and a Password field with a show/hide
|
|
toggle, and a submit button "Log in and verify"
|
|
And there is no "Forgot password?" link and no registration-open footer
|
|
|
|
Scenario: No session — empty fields are caught on submit, as on Log in
|
|
Given the inline log-in form from "No session" is shown
|
|
Then the submit button is enabled
|
|
When the user submits with both fields empty
|
|
Then Username shows "Use at least 3 characters." and Password shows "Enter your password."
|
|
And POST /v1/auth/login is not called
|
|
|
|
Scenario: No session — successful login retries verification with the same code
|
|
Given the inline log-in form from "No session" is shown on "/register/validate?c=abc123"
|
|
When the user submits Username "marta" and Password "limewash2026"
|
|
And POST /v1/auth/login resolves 200
|
|
Then the full-card spinner is shown again with the text "Verifying your account…"
|
|
And POST /v1/auth/verify is sent again, with body {"code": "abc123"} and the same passthrough
|
|
options as the first attempt
|
|
|
|
Scenario: No session — wrong credentials during the inline login
|
|
Given the inline log-in form from "No session" is shown
|
|
When the user submits Username "marta" and Password "wrong-password"
|
|
And POST /v1/auth/login resolves 403
|
|
Then an error alert is shown with title "That username and password don't match" and
|
|
description "Check both and try again. Passwords are case-sensitive."
|
|
And the Password field is cleared and refocused
|
|
And POST /v1/auth/verify is not retried
|
|
|
|
Scenario: No session — too many login attempts
|
|
Given the inline log-in form from "No session" is shown
|
|
When the user submits Username "marta" and Password "limewash2026"
|
|
And POST /v1/auth/login resolves 429 with Retry-After 30
|
|
Then an info alert is shown with title "Too many attempts" and description "For your security,
|
|
wait a moment before trying again."
|
|
And the submit button reads "Try again in 0:30" and it and both fields are disabled until the
|
|
countdown ends, mirroring LoginView
|
|
|
|
Scenario Outline: No session — server or network error during the inline login
|
|
Given the inline log-in form from "No session" is shown
|
|
When the user submits Username "marta" and Password "limewash2026"
|
|
And POST /v1/auth/login <failure>
|
|
Then an error alert is shown with title "Couldn't reach RenewCMS" and description "Check your
|
|
connection and try again."
|
|
And the fields keep their values and are re-enabled
|
|
|
|
Examples:
|
|
| failure |
|
|
| fails with a 500 response |
|
|
| never reaches the server |
|
|
|
|
Scenario: Missing code — no request is made
|
|
When the user navigates to "/register/validate" with no "c" query parameter
|
|
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 "The verification code is missing. Open the link from your email again, or
|
|
copy the full address into your browser."
|
|
And its single action is a secondary "Go to log in"
|
|
When the user activates "Go to log in"
|
|
Then the app navigates to /login
|