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"), 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) } } }) }