Files
RenewCMS/features/verify_screen.feature

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