Files
RenewCMS/features/registration_gate_and_verification_email.feature
T
2026-09-26 05:41:34 +02:00

113 lines
7.4 KiB
Gherkin

Feature: Registration is only open while no verified account exists, and it sends a time-limited verification email
As the maintainer of RenewCMS
I want the "is registration still open" rule enforced by the create-user use case instead of the
auth handler, and a verification email sent on every successful registration whose link uses the
configured public URL and whose code expires two hours after creation
So that the business rule is testable independently of HTTP, and a newly registered user always
receives a working, time-limited verification link built from the server's own configuration
Background:
Given POST /v1/auth/register is public, decodes RegisterCredentials (username, password, email)
and validates them with validator.Validate.Struct before anything else
And today, the gate lives in api/handlers/auth.Handler.SomeUsersVerified, which calls
ListUsersUseCase.ListUsers and loops over IsVerified before the body is even decoded; this slice
moves that check into CreateUserUseCase.CreateUser, which consults user.Repository.GetAll itself
and returns the sentinel user.ErrRegistrationClosed when at least one existing user already has
IsVerified true
And the handler stops performing the check itself and stops calling ListUsersUseCase for this
purpose: it calls CreateUserUseCase.CreateUser unconditionally and maps user.ErrRegistrationClosed
to a 403 response with the same text it returns today, "You are not allowed to create a user. Log
in or reset database.", now defined as a named constant in api/pkg/apperr (e.g.
apperr.RegistrationClosedErrorMessage) instead of an inline literal in the handler
And CreateUserUseCase.CreateUser already sets VerificationExpiration to time.Now().Add(2 * time.Hour)
and returns the raw (unhashed) verification code while storing only its hash; this slice does not
change that expiry computation
And the email send moves into CreateUserUseCase itself: NewCreateUserUseCase gains a mail.Repository
(the existing domain interface) and the configured public URL string as constructor parameters,
both injected from config.Settings.PublicURL in api/dependecyInjection.go; the handler no longer
sends mail and no longer holds a SendMailUseCase or PublicURL itself
And after user.Repository.Create succeeds, CreateUserUseCase.CreateUser calls
mail.Repository.Send with the user's email, template "verification"
(internal/infrastructure/mailer/templates/verification.html, which renders a link from
"{{.Host}}/register/validate?c={{.VerificationCode}}"), and data whose Host is the injected public
URL and whose VerificationCode is the raw (unhashed) code
And if mail.Repository.Send returns an error, CreateUserUseCase.CreateUser deletes the
just-created user through user.Repository.Delete and returns an error wrapping the send failure,
so no half-registered user is left behind and the same username and email can be used to register
again
And the handler maps that CreateUser error to a 500 response and never sets the jwt cookie in that
case, since CreateUser itself never returned a created user
Scenario: Registration succeeds when no verified user exists and sends the verification email
Given the user repository is empty
When the client sends POST /v1/auth/register with username "alice", password "hunter2example12",
email "alice@example.com"
Then the response status is 200
And a jwt cookie is set
And exactly one email was sent to "alice@example.com" using template "verification"
And that email's data has VerificationCode equal to the raw code returned by CreateUser, not its
stored hash
Scenario: The verification email's Host reflects the injected public URL
Given a config.Settings value with PublicURL "https://cms.example.com"
And the registration flow is built from that settings value
When the client sends POST /v1/auth/register with username "alice", password "hunter2example12",
email "alice@example.com"
Then the email sent to "alice@example.com" has data.Host equal to "https://cms.example.com"
And changing the process environment variable PUBLIC_URL afterward does not change that value on a
second registration built from the same settings value
Scenario: The stored verification code expires two hours after creation
Given the user repository is empty
When the client sends POST /v1/auth/register with username "alice", password "hunter2example12",
email "alice@example.com"
Then the created user's VerificationExpiration is within 5 seconds of time.Now().Add(2 * time.Hour)
Scenario: Registration is rejected once a verified user exists, and no email is sent
Given the user repository contains a user "alice" with IsVerified true
When the client sends POST /v1/auth/register with username "bob", password "hunter2example12",
email "bob@example.com"
Then the response status is 403
And the response body is "You are not allowed to create a user. Log in or reset database."
And the user repository still contains only "alice"
And no email was sent
Scenario: A failed verification email rolls back the just-created user
Given the user repository is empty
And the mail repository is set to fail on its next Send call
When the client sends POST /v1/auth/register with username "alice", password "hunter2example12",
email "alice@example.com"
Then the response status is 500
And no jwt cookie is set
And the user repository no longer contains a user named "alice"
When the client then sends POST /v1/auth/register again with username "alice",
password "hunter2example12", email "alice@example.com", and the mail repository now succeeds
Then the response status is 200
And a jwt cookie is set
Scenario: The create-user use case itself enforces the gate, independently of any handler
Given a user.Repository fake containing one user with IsVerified true
And a CreateUserUseCase built directly over that fake, a hasher and a mail.Repository fake
When CreateUser is called with username "bob", password "hunter2example12", email "bob@example.com"
Then the call returns user.ErrRegistrationClosed, asserted with errors.Is
And the fake repository's Create method was never invoked
And no email was sent
Scenario: The create-user use case allows registration while every existing user is unverified
Given a user.Repository fake containing two users, both with IsVerified false
And a CreateUserUseCase built directly over that fake, a hasher and a mail.Repository fake
When CreateUser is called with username "carol", password "hunter2example12",
email "carol@example.com"
Then the call succeeds and returns a user with IsVerified false
And exactly one email was sent to "carol@example.com" using template "verification"
Scenario: The create-user use case rolls back the user when the mail repository fails
Given an empty user.Repository fake
And a CreateUserUseCase built directly over that fake, a hasher and a mail.Repository fake set to
fail on its next Send call
When CreateUser is called with username "alice", password "hunter2example12",
email "alice@example.com"
Then the call returns an error wrapping the mail repository's failure
And the fake repository no longer contains a user named "alice"