refac: GET /v1/auth/registration
This commit is contained in:
1 parent
61828783a1
commit
2a64991916
6 files changed
+329
No files matched your search
@@ -0,0 +1,80 @@
|
||||
Feature: The SPA can ask whether registration is currently open, without exposing anything else
|
||||
|
||||
As the maintainer of RenewCMS
|
||||
I want a public, unauthenticated GET endpoint that reports whether registration is open, driven by
|
||||
the exact same rule CreateUserUseCase already enforces on POST /v1/auth/register, and that reveals
|
||||
nothing beyond that single boolean
|
||||
So that the SPA can decide whether to show a registration form before the user submits anything,
|
||||
without a second, drifting copy of the "is registration closed" rule and without leaking how many
|
||||
accounts exist or who they belong to
|
||||
|
||||
Background:
|
||||
Given CreateUserUseCase already has a private registrationClosed() bool method that loops over
|
||||
user.Repository.GetAll() and returns true as soon as any existing user has IsVerified true; this
|
||||
slice exports that same check as a new method on CreateUserUseCase (e.g. IsRegistrationOpen() bool,
|
||||
returning the negation) so it can be called from outside the use case, and CreateUser itself keeps
|
||||
calling it exactly as before
|
||||
And this slice adds GET /v1/auth/registration, handled from the same handler package as
|
||||
POST /v1/auth/register (api/handlers/auth), reusing the same CreateUserUseCase instance the
|
||||
register handler already holds; no new use case, no new domain method beyond the exported check,
|
||||
and no second loop over user.Repository.GetAll anywhere in the handler
|
||||
And GET /v1/auth/registration is mounted in api/router.go's NewAuthRouter at the top level, outside
|
||||
every r.Group: not inside the jwtauth.Verifier/Authenticator group that guards /verify,
|
||||
DELETE /register and /me, and not inside either middleware.NewRateLimiter group that guards /login
|
||||
and /password-reset; no jwt cookie is ever read, parsed or validated for this route
|
||||
And the handler decodes no request body and reads no cookie and no claim; it calls
|
||||
CreateUserUseCase.IsRegistrationOpen(), sets the response header Cache-Control: no-store, and
|
||||
responds 200 with a JSON body containing exactly one field, "open", and nothing else: no user
|
||||
count, no usernames, no emails, no is_verified values
|
||||
|
||||
Scenario: Registration is reported open while no verified account exists
|
||||
Given the user repository is empty
|
||||
When the client sends GET /v1/auth/registration with no jwt cookie
|
||||
Then the response status is 200
|
||||
And the response header Cache-Control is "no-store"
|
||||
And the response body is exactly {"open": true}
|
||||
|
||||
Scenario: Registration is reported open while every existing account is unverified
|
||||
Given the user repository contains a user "alice" with IsVerified false
|
||||
When the client sends GET /v1/auth/registration with no jwt cookie
|
||||
Then the response status is 200
|
||||
And the response body is exactly {"open": true}
|
||||
|
||||
Scenario: Registration is reported closed once a verified account exists
|
||||
Given the user repository contains a user "alice" with IsVerified true
|
||||
When the client sends GET /v1/auth/registration with no jwt cookie
|
||||
Then the response status is 200
|
||||
And the response header Cache-Control is "no-store"
|
||||
And the response body is exactly {"open": false}
|
||||
|
||||
Scenario: An expired, forged, or otherwise invalid jwt cookie makes no difference to the response
|
||||
Given the user repository contains a user "alice" with IsVerified true
|
||||
And the client holds a jwt cookie that is expired, forged, or otherwise invalid and would be
|
||||
rejected by jwtauth.Authenticator on any protected route
|
||||
When the client sends GET /v1/auth/registration with that cookie attached
|
||||
Then the response status is 200
|
||||
And the response body is exactly {"open": false}, the same as a request with no cookie at all
|
||||
And the response does not set or clear any cookie
|
||||
And this proves the route never even attempts to parse or validate the cookie, not merely that a
|
||||
valid one would be ignored
|
||||
|
||||
Scenario: The endpoint reveals nothing beyond the open flag
|
||||
Given the user repository contains three users, two verified and one unverified
|
||||
When the client sends GET /v1/auth/registration
|
||||
Then the response body's only top-level field is "open"
|
||||
And the response body contains none of: a user count, a username, an email, or an is_verified value
|
||||
|
||||
Scenario: The route is never rate-limited, unlike login and password reset
|
||||
Given the user repository is empty
|
||||
When the client sends GET /v1/auth/registration 20 times in immediate succession from the same
|
||||
client
|
||||
Then every response status is 200
|
||||
And none of the responses is 429
|
||||
|
||||
Scenario: The same rule CreateUser enforces is what answers this endpoint
|
||||
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 IsRegistrationOpen is called
|
||||
Then it returns false
|
||||
And calling CreateUser on the same use case with a new username/email/password returns
|
||||
user.ErrRegistrationClosed, asserted with errors.Is
|
||||
@@ -0,0 +1,58 @@
|
||||
# QA procedure: registration_status_endpoint
|
||||
|
||||
Package: `test` (black-box), extending `test/user_test.go` (`mockUserRepo`, `stubHasher`) and
|
||||
`test/mail_test.go` (`mockMailRepo`), and following the router wiring pattern in
|
||||
`test/registration_gate_and_verification_email_http_test.go` /
|
||||
`test/get_current_user_http_test.go`.
|
||||
|
||||
## Setup shared by every case
|
||||
|
||||
1. Call `validator.InitValidator()` once.
|
||||
2. Build `createUserUseCase := useCases.NewCreateUserUseCase(repo, &stubHasher{}, mailRepo,
|
||||
"https://cms.example.com")` where `repo` is a `mockUserRepo` and `mailRepo` is a `mockMailRepo`.
|
||||
3. Build the auth handler(s) the same way `NewAuthRouter` does, but only mount what each case needs:
|
||||
`GET /v1/auth/registration` unprotected, at the router's top level (no `jwtauth.Verifier`, no
|
||||
`jwtauth.Authenticator`, no `middleware.NewRateLimiter`), on a fresh `chi.NewRouter()`. No live
|
||||
server, no real SQLite file, no real SMTP.
|
||||
|
||||
## HTTP-level checks
|
||||
|
||||
4. Empty `mockUserRepo`. `GET /v1/auth/registration` with no cookie → assert status `200`,
|
||||
`Cache-Control` header equals `"no-store"`, and the decoded JSON body is exactly `{"open": true}`
|
||||
(assert there is no other top-level key by decoding into `map[string]any` and checking `len(body)
|
||||
== 1`).
|
||||
5. `mockUserRepo` seeded with one user, `IsVerified: false`. `GET /v1/auth/registration` → assert
|
||||
status `200` and body exactly `{"open": true}`.
|
||||
6. `mockUserRepo` seeded with one user, `IsVerified: true`. `GET /v1/auth/registration` → assert
|
||||
status `200`, `Cache-Control: no-store`, and body exactly `{"open": false}`.
|
||||
7. Same seed as step 6. Attach a jwt cookie whose value is invalid/expired/garbage — e.g. a plain
|
||||
string like `"jwt=not-a-real-token"`, or a token minted with a different signing key, or one built
|
||||
with `token.Encode` and then manually expired/truncated — deliberately NOT a cookie freshly minted
|
||||
with `token.Encode` against the router's own signing key. `GET /v1/auth/registration` with that
|
||||
cookie attached → assert status `200`, body exactly `{"open": false}` (identical to step 6's
|
||||
cookie-less result), and no `Set-Cookie` header on the response. This demonstrates the route never
|
||||
parses or validates the cookie at all, since an invalid/expired one produces byte-identical output
|
||||
to no cookie.
|
||||
8. `mockUserRepo` seeded with three users (two `IsVerified: true`, one `IsVerified: false`).
|
||||
`GET /v1/auth/registration` → decode the body into `map[string]any`, assert its only key is
|
||||
`"open"`, and assert the raw response body (as a string) does not contain any of the seeded
|
||||
usernames or emails, and does not contain a numeric user count anywhere but inside the boolean
|
||||
`open` value.
|
||||
9. Empty `mockUserRepo`. Send `GET /v1/auth/registration` 20 times in a row against the same
|
||||
router/repo (a plain loop of `httptest.NewRequest` + `ServeHTTP`, no external rate limiter
|
||||
middleware mounted on this route) → assert every response status is `200` and none is `429`.
|
||||
|
||||
## Use-case-level check (no HTTP)
|
||||
|
||||
10. Build a `user.Repository` fake (`mockUserRepo`) containing one user with `IsVerified: true`. Build
|
||||
`CreateUserUseCase` over that fake, a `stubHasher`, and a `mockMailRepo`. Call the new exported
|
||||
method (e.g. `IsRegistrationOpen()`) and assert it returns `false`. Then call `CreateUser` with a
|
||||
new username/email/password on the same use case instance and assert
|
||||
`errors.Is(err, user.ErrRegistrationClosed)` — proving both entry points consult the same
|
||||
underlying check rather than two independently-maintained copies.
|
||||
|
||||
## Manual verification (not exercised by the Go test)
|
||||
|
||||
- The SPA actually calling `GET /v1/auth/registration` before rendering the registration form (rather
|
||||
than always showing it and relying on the 403 from POST /v1/auth/register) is out of scope for this
|
||||
slice's automated test and should be checked by hand against a running dev build.
|
||||
Reference in new issue
Block a user