refac: register only open while no verified acc exists, enforced by a use case rather than handler & sends verification email whose link uses the conf pub URL whose code expires after 2h
This commit is contained in:
1 parent
6e9e108033
commit
6d510d2805
14 files changed
+575
-30
No files matched your search
@@ -0,0 +1,112 @@
|
||||
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 "mailValidation"
|
||||
(internal/infrastructure/mailer/templates/mailValidation.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 "mailValidation"
|
||||
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 "mailValidation"
|
||||
|
||||
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"
|
||||
Reference in new issue
Block a user