diff --git a/bin/evening_detective_server b/bin/evening_detective_server index a36a71c..0997946 100755 Binary files a/bin/evening_detective_server and b/bin/evening_detective_server differ diff --git a/internal/services/users_service/change_password_test.go b/internal/services/users_service/change_password_test.go index 5009cba..843f1af 100644 --- a/internal/services/users_service/change_password_test.go +++ b/internal/services/users_service/change_password_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "evening_detective_server/internal/modules/processor_jwt" "evening_detective_server/internal/repos" "evening_detective_server/internal/repos/users_repo" "golang.org/x/crypto/bcrypt" @@ -61,3 +62,34 @@ func TestChangeUserPassword(t *testing.T) { }) } } + +// Stop after successful password verification without using a real token database. +type loginPasswordCheck struct { + processor_jwt.IProcessorJWT + err error +} + +func (p loginPasswordCheck) GenerateAccessToken(int, string, []string) (string, error) { + return "", p.err +} + +func TestLoginAfterPasswordChangePreservesEmailCase(t *testing.T) { + repo := newFakeRepo() + repo.seed("admin@example.com", "unused") + repo.byID[1].Roles = []string{"admin"} + repo.seed("User@Example.COM", "old-hash") + svc := &UsersService{usersRepo: repo, processorJWT: loginPasswordCheck{err: errors.New("stop before token storage")}} + ctx := context.Background() + if err := svc.ChangeUserPassword(ctx, 1, 2, "new password 123"); err != nil { + t.Fatal(err) + } + // This error can only occur after email lookup and bcrypt verification succeed. + _, _, err := svc.Login(ctx, " User@Example.COM ", "new password 123") + if err == nil || err.Error() != "Не получилось получить доступ" { + t.Fatalf("lookup/password verification failed: %v", err) + } + _, _, err = svc.Login(ctx, "user@example.com", "new password 123") + if !errors.Is(err, users_repo.ErrUserNotFound) { + t.Fatalf("email case was changed: %v", err) + } +} diff --git a/internal/services/users_service/service.go b/internal/services/users_service/service.go index ef3c44f..5dd1181 100644 --- a/internal/services/users_service/service.go +++ b/internal/services/users_service/service.go @@ -98,7 +98,7 @@ func (s *UsersService) AddUser( return ErrTermsNotAccepted } - email = normalizeEmail(email) + email = strings.TrimSpace(email) if err := validateEmail(email); err != nil { return err } @@ -182,7 +182,7 @@ func (s *UsersService) RefreshPassword( ctx context.Context, email string, ) error { - email = normalizeEmail(email) + email = strings.TrimSpace(email) if err := validateEmail(email); err != nil { return err } @@ -222,12 +222,6 @@ func (s *UsersService) RefreshPassword( return s.usersRepo.UpdateUserPassword(ctx, user.Email, string(hashedPassword)) } -// normalizeEmail приводит адрес к каноническому виду (без пробелов, нижний -// регистр): "User@Example.COM " и "user@example.com" — один аккаунт. -func normalizeEmail(email string) string { - return strings.ToLower(strings.TrimSpace(email)) -} - // validateEmail проверяет, что строка является одиночным email-адресом. func validateEmail(email string) error { if email == "" { @@ -249,11 +243,10 @@ func (s *UsersService) Login( email string, password string, ) (string, string, error) { - // Email нормализуется так же, как при регистрации: иначе пользователь, - // зарегистрировавшийся как user@example.com, не войдёт с User@Example.COM. + // Сохраняем регистр email, как при регистрации; убираем только внешние пробелы. user, err := s.usersRepo.GetUserByEmail( ctx, - normalizeEmail(email), + strings.TrimSpace(email), ) if err != nil { return "", "", err diff --git a/internal/services/users_service/service_test.go b/internal/services/users_service/service_test.go index 7338b25..86ff47a 100644 --- a/internal/services/users_service/service_test.go +++ b/internal/services/users_service/service_test.go @@ -175,17 +175,17 @@ func TestAddUserSuccess(t *testing.T) { t.Fatalf("AddUser: %v", err) } - // Email нормализован: пробелы убраны, регистр приведён. - if _, ok := repo.users["user@example.com"]; !ok { - t.Errorf("пользователь не создан с нормализованным email: %+v", repo.users) + // Убираем внешние пробелы, сохраняя исходный регистр email. + if _, ok := repo.users["User@Example.COM"]; !ok { + t.Errorf("пользователь не создан с исходным регистром email: %+v", repo.users) } msgs := sender.sent() if len(msgs) != 1 { t.Fatalf("ожидалось 1 письмо, отправлено %d", len(msgs)) } - if msgs[0].To != "user@example.com" { - t.Errorf("письмо ушло на %q, want user@example.com", msgs[0].To) + if msgs[0].To != "User@Example.COM" { + t.Errorf("письмо ушло на %q, want User@Example.COM", msgs[0].To) } if !strings.Contains(msgs[0].Subject, "Вечернему детективу") { t.Errorf("неожиданная тема письма: %q", msgs[0].Subject) @@ -284,11 +284,11 @@ func TestAddUserRequiresTerms(t *testing.T) { func TestRefreshPasswordSuccess(t *testing.T) { repo := newFakeRepo() - repo.seed("user@example.com", "old-hash") + repo.seed("User@Example.COM", "old-hash") sender := &fakeSender{} svc := newTestService(repo, sender) - // Ввод с пробелами и другим регистром — нормализуется. + // Внешние пробелы убираются, регистр остаётся исходным. err := svc.RefreshPassword(context.Background(), " User@Example.COM ") if err != nil { t.Fatalf("RefreshPassword: %v", err) @@ -297,7 +297,7 @@ func TestRefreshPasswordSuccess(t *testing.T) { if len(sender.sent()) != 1 { t.Fatalf("ожидалось 1 письмо, отправлено %d", len(sender.sent())) } - u, _ := repo.GetUserByEmail(context.Background(), "user@example.com") + u, _ := repo.GetUserByEmail(context.Background(), "User@Example.COM") if u.PasswordHash == "old-hash" { t.Error("пароль не изменён") }