refac: several unverified users can exist at once: an empty reset or verification code is stored as no value so it doesn't break the unique constraint, and deleting a user removes the row so the same username and email can register again
This commit is contained in:
1 parent
0b4e614ec9
commit
6e9e108033
6 files changed
+377
-16
No files matched your search
@@ -0,0 +1,70 @@
|
||||
Feature: Several unverified users can coexist, and deleting a user frees its username and email
|
||||
|
||||
As the maintainer of RenewCMS
|
||||
I want an absent password reset code or verification code stored as no value instead of an empty
|
||||
string, and a deleted user's row actually removed
|
||||
So that several unverified users can register one after another without colliding on the unique
|
||||
reset-code column, and so that deleting a user lets the same username and email register again
|
||||
|
||||
Background:
|
||||
Given internal/infrastructure/persistence/models.User stores PasswordResetCode and VerificationCode
|
||||
as nullable, unique columns, so a domain value of "" is persisted as no value (SQL NULL), and SQL
|
||||
NULL is exempt from the unique constraint
|
||||
And internal/infrastructure/persistence/mappers.UserToModel maps a domain "" for PasswordResetCode or
|
||||
VerificationCode to no value, and UserToDomain maps no value back to ""
|
||||
And internal/infrastructure/persistence/models.User no longer embeds gorm.Model, so
|
||||
UserRepository.Delete removes the row instead of leaving a soft-deleted row that still occupies the
|
||||
unique username and email
|
||||
And POST /v1/auth/register is public, and CreateUser stores a new user with is_verified false and
|
||||
PasswordResetCode "" as soon as the request is valid
|
||||
And Register only answers 403 once at least one existing user is verified (SomeUsersVerified), never
|
||||
because the user table already has rows
|
||||
And DeleteUser has no handler mounted in api/router.go today, so deletion in these scenarios is
|
||||
exercised through UserRepository.Delete / DeleteUserUseCase.DeleteUser directly, not through an
|
||||
HTTP route
|
||||
|
||||
Scenario: Two unverified users can register one after another without a reset-code collision
|
||||
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
|
||||
When the client sends POST /v1/auth/register with username "bob", password "hunter2example12",
|
||||
email "bob@example.com"
|
||||
Then the response status is 200
|
||||
And a jwt cookie is set
|
||||
And the user repository contains both "alice" and "bob", each with is_verified false and an empty
|
||||
password reset code
|
||||
|
||||
Scenario: Creating two users with no active reset code does not violate the unique constraint
|
||||
Given a repository backed by an in-memory SQLite database with models.User migrated
|
||||
When Create is called with username "alice", email "alice@example.com", password_reset_code "" and
|
||||
verification_code "code-alice"
|
||||
And Create is called with username "bob", email "bob@example.com", password_reset_code "" and
|
||||
verification_code "code-bob"
|
||||
Then both calls succeed
|
||||
And GetByUsername "alice" and GetByUsername "bob" each return password_reset_code ""
|
||||
|
||||
Scenario: The unique constraint still rejects two users sharing the same active reset code
|
||||
Given a repository backed by an in-memory SQLite database containing a user "alice" with
|
||||
password_reset_code "shared-code"
|
||||
When Create is called with username "bob", email "bob@example.com" and password_reset_code
|
||||
"shared-code"
|
||||
Then the Create call returns an error
|
||||
And GetByUsername "bob" still returns user.ErrUserNotFound
|
||||
|
||||
Scenario: Deleting a user removes the row so the same username and email can register again
|
||||
Given a repository backed by an in-memory SQLite database containing a user with id 1, username
|
||||
"alice" and email "alice@example.com"
|
||||
When Delete is called with id 1
|
||||
And Create is called with username "alice", email "alice@example.com" and a new password
|
||||
Then the Create call succeeds
|
||||
And the newly created user's id is different from 1
|
||||
And Get with id 1 returns user.ErrUserNotFound
|
||||
|
||||
Scenario: Registering the same username while the first account still exists is still rejected
|
||||
Given a repository backed by an in-memory SQLite database containing a user with username "alice"
|
||||
and email "alice@example.com"
|
||||
When Create is called with username "alice", email "someone-else@example.com" and a new password
|
||||
Then the Create call returns an error
|
||||
And the user repository still contains exactly one user with username "alice"
|
||||
@@ -0,0 +1,50 @@
|
||||
# QA procedure: unverified_users_and_reregistration
|
||||
|
||||
Package: `test` (black-box), extending `test/user_test.go` (mockUserRepo) and reusing the
|
||||
`openInMemoryDb(t)` helper from `test/not_found_errors_test.go`.
|
||||
|
||||
## Setup shared by every case
|
||||
1. Call `validator.InitValidator()` once.
|
||||
2. Repository-level checks use `openInMemoryDb(t)`, `db.AutoMigrate(&models.User{})`, and a real
|
||||
`persistence.NewUserRepository(db)`.
|
||||
3. HTTP-level checks use `mockUserRepo` (from `test/user_test.go`) wired into
|
||||
`useCases.NewCreateUserUseCase(repo, &hashing.BcryptHasher{})`,
|
||||
`useCases.NewListUsersUseCase(repo)`, `useCases.NewGetUserUseCase(repo)`,
|
||||
`useCases.NewAuthenticateUserUseCase(repo, ...)`, and `auth.NewHandler(...)` with a test
|
||||
`jwtauth.New("HS256", []byte("test-secret"), nil)`. Mount `POST /v1/auth/register` and
|
||||
`POST /v1/auth/login` unprotected on a fresh `chi.NewRouter()`, mirroring `api/router.go`. No live
|
||||
server.
|
||||
|
||||
## Repository-level checks (real persistence repository, in-memory SQLite, no HTTP)
|
||||
4. On an empty repository, `Create` a user "alice"/"alice@example.com" with `PasswordResetCode: ""` and
|
||||
`VerificationCode: "code-alice"` → assert `err == nil`. `Create` a second user "bob"/"bob@example.com"
|
||||
with `PasswordResetCode: ""` and `VerificationCode: "code-bob"` → assert `err == nil` (regression: before
|
||||
the fix this fails with a SQLite `UNIQUE constraint failed: users.password_reset_code` error). Then
|
||||
`GetByUsername("alice")` and `GetByUsername("bob")` → assert `PasswordResetCode == ""` for both.
|
||||
5. `Create` a user "alice" with `PasswordResetCode: "shared-code"`. `Create` a user "bob" with the same
|
||||
`PasswordResetCode: "shared-code"` → assert `err != nil` (the unique constraint still applies to an
|
||||
actual shared value). Assert `GetByUsername("bob")` returns `user.ErrUserNotFound` (the row was never
|
||||
created).
|
||||
6. `Create` a user "alice"/"alice@example.com", capture its id. `Delete(id)` → assert `err == nil`.
|
||||
`Create` a new user with the same username "alice" and email "alice@example.com" (a different
|
||||
password and verification code) → assert `err == nil` (regression: before the fix this fails with a
|
||||
SQLite `UNIQUE constraint failed: users.username` error because the row was only soft-deleted). Assert
|
||||
the new user's id differs from the deleted one. Assert `Get(deletedId)` returns `user.ErrUserNotFound`.
|
||||
7. `Create` a user "alice"/"alice@example.com" and do **not** delete it. `Create` a second user with
|
||||
username "alice" and a different email → assert `err != nil` (unchanged: still rejected while the row
|
||||
is live).
|
||||
|
||||
## HTTP checks (fakes, per the shared setup above)
|
||||
8. `POST /v1/auth/register` with `{"username":"alice","password":"hunter2example12","email":"alice@example.com"}`
|
||||
against an empty `mockUserRepo` → assert status `200`, a `Set-Cookie: jwt=...` header present, body
|
||||
decodes to `{"message":"User registered! HTTPonly jwt cookie created"}`.
|
||||
9. `POST /v1/auth/register` again with `{"username":"bob","password":"hunter2example12","email":"bob@example.com"}`
|
||||
(repo now holds unverified "alice") → assert status `200` and a `Set-Cookie: jwt=...` header present
|
||||
(register stays allowed because no user is verified yet).
|
||||
10. Assert `mockUserRepo.users` holds two entries, both `IsVerified == false` and both
|
||||
`PasswordResetCode == ""`.
|
||||
|
||||
## Manual verification (not exercised by the Go test)
|
||||
- None. `DeleteUser` has no HTTP route mounted today, so there is no SPA-visible delete-and-re-register
|
||||
flow to check by hand, and `CreateUser` does not call the mailer, so there is no SMTP involvement in
|
||||
this slice.
|
||||
Reference in new issue
Block a user