Files
RenewCMS/test/unverified_users_and_reregistration_test.go
T
2026-10-06 22:25:23 +02:00

229 lines
8.4 KiB
Go

package test
import (
"RenewCMS/api/handlers/auth"
validator "RenewCMS/api/pkg/validator"
domainUser "RenewCMS/internal/domain/user"
"RenewCMS/internal/infrastructure/hashing"
"RenewCMS/internal/infrastructure/persistence/models"
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
"RenewCMS/internal/infrastructure/useCases"
"bytes"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth/v5"
)
func newMigratedUserRepository(t *testing.T) *persistence.UserRepository {
t.Helper()
db := openInMemoryDb(t)
if err := db.AutoMigrate(&models.User{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
return persistence.NewUserRepository(db)
}
func TestUnverifiedUsersAndReregistrationRepository(t *testing.T) {
t.Run("Creating two users with no active reset code does not violate the unique constraint", func(t *testing.T) {
repo := newMigratedUserRepository(t)
if _, err := repo.Create(domainUser.User{Username: "alice", Email: "alice@example.com", Password: "hunter2example12", PasswordResetCode: "", VerificationCode: "code-alice"}); err != nil {
t.Fatalf("expected alice creation to succeed, got %v", err)
}
if _, err := repo.Create(domainUser.User{Username: "bob", Email: "bob@example.com", Password: "hunter2example12", PasswordResetCode: "", VerificationCode: "code-bob"}); err != nil {
t.Fatalf("expected bob creation to succeed, got %v", err)
}
alice, err := repo.GetByUsername("alice")
if err != nil {
t.Fatalf("failed to get alice: %v", err)
}
if alice.PasswordResetCode != "" {
t.Errorf("expected empty password reset code for alice, got %q", alice.PasswordResetCode)
}
bob, err := repo.GetByUsername("bob")
if err != nil {
t.Fatalf("failed to get bob: %v", err)
}
if bob.PasswordResetCode != "" {
t.Errorf("expected empty password reset code for bob, got %q", bob.PasswordResetCode)
}
})
t.Run("The unique constraint still rejects two users sharing the same active reset code", func(t *testing.T) {
repo := newMigratedUserRepository(t)
if _, err := repo.Create(domainUser.User{Username: "alice", Email: "alice@example.com", Password: "hunter2example12", PasswordResetCode: "shared-code", VerificationCode: "code-alice"}); err != nil {
t.Fatalf("expected alice creation to succeed, got %v", err)
}
_, err := repo.Create(domainUser.User{Username: "bob", Email: "bob@example.com", Password: "hunter2example12", PasswordResetCode: "shared-code", VerificationCode: "code-bob"})
if err == nil {
t.Fatal("expected Create for bob to fail because of the shared reset code")
}
_, err = repo.GetByUsername("bob")
if !errors.Is(err, domainUser.ErrUserNotFound) {
t.Errorf("expected user.ErrUserNotFound, got %v", err)
}
})
t.Run("Deleting a user removes the row so the same username and email can register again", func(t *testing.T) {
repo := newMigratedUserRepository(t)
created, err := repo.Create(domainUser.User{Username: "alice", Email: "alice@example.com", Password: "hunter2example12", VerificationCode: "code-alice"})
if err != nil {
t.Fatalf("failed to seed alice: %v", err)
}
if err := repo.Delete(created.ID); err != nil {
t.Fatalf("expected delete to succeed, got %v", err)
}
recreated, err := repo.Create(domainUser.User{Username: "alice", Email: "alice@example.com", Password: "differentpassword12", VerificationCode: "code-alice-2"})
if err != nil {
t.Fatalf("expected alice to be re-registrable after deletion, got %v", err)
}
if recreated.ID == created.ID {
t.Errorf("expected the recreated user to have a different id than %d", created.ID)
}
_, err = repo.Get(created.ID)
if !errors.Is(err, domainUser.ErrUserNotFound) {
t.Errorf("expected user.ErrUserNotFound for the deleted id, got %v", err)
}
})
t.Run("Registering the same username while the first account still exists is still rejected", func(t *testing.T) {
repo := newMigratedUserRepository(t)
if _, err := repo.Create(domainUser.User{Username: "alice", Email: "alice@example.com", Password: "hunter2example12", VerificationCode: "code-alice"}); err != nil {
t.Fatalf("failed to seed alice: %v", err)
}
_, err := repo.Create(domainUser.User{Username: "alice", Email: "someone-else@example.com", Password: "anotherpassword12", VerificationCode: "code-alice-2"})
if err == nil {
t.Fatal("expected Create to fail because the username is still in use")
}
users := repo.GetAll()
count := 0
for _, u := range users {
if u.Username == "alice" {
count++
}
}
if count != 1 {
t.Errorf("expected exactly one user with username alice, got %d", count)
}
})
t.Run("Updating a user's reset code to one already active for another user is rejected", func(t *testing.T) {
repo := newMigratedUserRepository(t)
if _, err := repo.Create(domainUser.User{Username: "alice", Email: "alice@example.com", Password: "hunter2example12", PasswordResetCode: "alice-code", VerificationCode: "code-alice"}); err != nil {
t.Fatalf("expected alice creation to succeed, got %v", err)
}
bob, err := repo.Create(domainUser.User{Username: "bob", Email: "bob@example.com", Password: "hunter2example12", VerificationCode: "code-bob"})
if err != nil {
t.Fatalf("expected bob creation to succeed, got %v", err)
}
_, err = repo.UpdatePasswordResetCode(bob.ID, "alice-code", time.Now().Add(time.Hour))
if err == nil {
t.Fatal("expected UpdatePasswordResetCode for bob to fail because the code is already active for alice")
}
reloadedBob, err := repo.Get(bob.ID)
if err != nil {
t.Fatalf("failed to reload bob: %v", err)
}
if reloadedBob.PasswordResetCode != "" {
t.Errorf("expected bob's reset code to remain unset, got %q", reloadedBob.PasswordResetCode)
}
})
}
func newUnverifiedRegistrationRouter(userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
hasher := hashing.NewBcryptHasher()
loginHandler := auth.NewLoginHandler(
token,
useCases.NewCreateUserUseCase(userRepo, hasher, newMockMailRepo(), "https://cms.example.com", discardRecorder),
useCases.NewAuthenticateUserUseCase(userRepo, hasher, discardRecorder),
true,
)
r := chi.NewRouter()
r.Route("/v1", func(r chi.Router) {
r.Route("/auth", func(r chi.Router) {
r.Post("/register", loginHandler.Register)
})
})
return r
}
func registerViaHttp(t *testing.T, router *chi.Mux, username, password, email string) *httptest.ResponseRecorder {
t.Helper()
body, err := json.Marshal(map[string]string{"username": username, "password": password, "email": email})
if err != nil {
t.Fatalf("failed to marshal body: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/v1/auth/register", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
return rec
}
func TestUnverifiedUsersAndReregistrationHttp(t *testing.T) {
validator.InitValidator()
token := jwtauth.New("HS256", []byte("test-secret"), nil)
t.Run("Two unverified users can register one after another without a reset-code collision", func(t *testing.T) {
repo := newMockUserRepo()
router := newUnverifiedRegistrationRouter(repo, token)
aliceRec := registerViaHttp(t, router, "alice", "hunter2example12", "alice@example.com")
if aliceRec.Code != http.StatusOK {
t.Fatalf("expected status 200 for alice, got %d: %s", aliceRec.Code, aliceRec.Body.String())
}
if aliceRec.Header().Get("Set-Cookie") == "" {
t.Errorf("expected a jwt cookie to be set for alice")
}
var aliceBody map[string]string
if err := json.Unmarshal(aliceRec.Body.Bytes(), &aliceBody); err != nil {
t.Fatalf("failed to decode alice response body: %v", err)
}
if aliceBody["message"] != "User registered! HTTPonly jwt cookie created" {
t.Errorf("unexpected response body for alice: %v", aliceBody)
}
bobRec := registerViaHttp(t, router, "bob", "hunter2example12", "bob@example.com")
if bobRec.Code != http.StatusOK {
t.Fatalf("expected status 200 for bob, got %d: %s", bobRec.Code, bobRec.Body.String())
}
if bobRec.Header().Get("Set-Cookie") == "" {
t.Errorf("expected a jwt cookie to be set for bob")
}
if len(repo.users) != 2 {
t.Fatalf("expected 2 users in the repository, got %d", len(repo.users))
}
for _, u := range repo.users {
if u.IsVerified {
t.Errorf("expected user %q to be unverified", u.Username)
}
if u.PasswordResetCode != "" {
t.Errorf("expected user %q to have an empty password reset code, got %q", u.Username, u.PasswordResetCode)
}
}
})
}