113 lines
7.4 KiB
Gherkin
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"
|