Feature: Password and code hashing go through a domain Hasher interface As the maintainer of RenewCMS I want password and verification/reset-code hashing done by a domain hash.Hasher interface, used only by use cases So that internal/infrastructure/persistence/repositories/userRepository.go and api/handlers/auth/authHandler.go stop calling bcrypt directly, and a use case can be tested against a fake hasher instead of a real bcrypt cost factor Background: Given internal/domain/hash defines the interface Hasher with Hash(value string) (string, error) and Compare(hashed string, value string) error And internal/domain/user defines the sentinel user.ErrInvalidCredentials alongside the existing user.ErrUserNotFound And internal/infrastructure/hashing.BcryptHasher implements hash.Hasher and is asserted with var _ hash.Hasher = &BcryptHasher{} And api/dependecyInjection.go constructs one BcryptHasher and passes it to every use case that hashes or compares a secret: CreateUserUseCase, AuthenticateUserUseCase, VerifyUserUseCase, RequestPasswordResetUseCase and ResetPasswordUseCase And UserRepository.Create, UpdatePassword and UpdatePasswordResetCode store the string they are given verbatim and never call bcrypt And the auth Handler no longer imports golang.org/x/crypto/bcrypt; Login calls AuthenticateUserUseCase.Authenticate instead of comparing a hash itself Scenario: Creating a user stores a hashed password and a hashed verification code Given a fake hasher that hashes any value "x" to "hashed:x" When CreateUserUseCase.CreateUser is called with username "alice", password "s3cretPassw0rd" and email "alice@example.com" Then the repository's Create is called with a user whose Password is "hashed:s3cretPassw0rd" And that user's VerificationCode is "hashed:", not the raw code And the User returned by the use case has VerificationCode equal to the raw generated code, not the hash Scenario: A hashing failure while creating a user persists nothing Given a fake hasher whose Hash always returns an error When CreateUserUseCase.CreateUser is called with username "alice", password "s3cretPassw0rd" and email "alice@example.com" Then the use case returns that error And the repository's Create is never called Scenario: Authenticating with the correct password returns the user Given a user repository containing a user "alice" with stored password "hashed:hunter2example" And a fake hasher whose Compare returns nil when given "hashed:hunter2example" and "hunter2example" When AuthenticateUserUseCase.Authenticate is called with username "alice" and password "hunter2example" Then the result is that user, with no error Scenario: Authenticating with the wrong password returns a domain sentinel error Given a user repository containing a user "alice" with stored password "hashed:hunter2example" And a fake hasher whose Compare returns an error for that password When AuthenticateUserUseCase.Authenticate is called with username "alice" and password "wrongpassword12" Then the result is the error user.ErrInvalidCredentials And errors.Is(err, user.ErrInvalidCredentials) is true Scenario: Authenticating an unknown username never calls the hasher Given a user repository with no user "bob" When AuthenticateUserUseCase.Authenticate is called with username "bob" and password "hunter2example" Then the result is the error user.ErrUserNotFound And the hasher's Compare is never called Scenario Outline: Login over HTTP still can't distinguish a bad password from an unknown username Given a user repository containing a user "alice" with stored password "hashed:hunter2example" And a real BcryptHasher When the client sends POST /v1/auth/login with username "" and password "" Then the response status is 403 And the response body is the text apperr.LogsErrorMessage And no jwt cookie is set Examples: | username | password | case | | alice | wrongpassword12 | existing user, bad pass | | bob | hunter2example | unknown username | Scenario: Registering a user stores the hashed password, never the plaintext Given an empty user repository And a real BcryptHasher When the client sends POST /v1/auth/register with username "alice", password "s3cretPassw0rd" and email "alice@example.com" Then the response status is 200 And a jwt cookie is set And the user repository's stored Password for "alice" is not "s3cretPassw0rd" And BcryptHasher.Compare("", "s3cretPassw0rd") returns nil Scenario: A hashing failure during registration surfaces as 500 and sets no cookie Given an empty user repository And a fake hasher whose Hash always returns an error When the client sends POST /v1/auth/register with username "alice", password "s3cretPassw0rd" and email "alice@example.com" Then the response status is 500 And no jwt cookie is set