Files
goyco/internal/services/password_reset_service_test.go

418 lines
11 KiB
Go

package services
import (
"errors"
"testing"
"time"
"golang.org/x/crypto/bcrypt"
"goyco/internal/database"
"goyco/internal/testutils"
)
func TestNewPasswordResetService(t *testing.T) {
userRepo := testutils.NewMockUserRepository()
emailService, _ := NewEmailService(testutils.AppTestConfig, &testutils.MockEmailSender{})
service := NewPasswordResetService(userRepo, emailService)
if service == nil {
t.Fatal("expected service to be created")
}
if service.userRepo != userRepo {
t.Error("expected userRepo to be set")
}
if service.emailService != emailService {
t.Error("expected emailService to be set")
}
}
func TestPasswordResetService_RequestPasswordReset(t *testing.T) {
tests := []struct {
name string
usernameOrEmail string
setupMocks func() (*testutils.MockUserRepository, EmailSender)
expectedError bool
shouldSendEmail bool
}{
{
name: "successful request by username",
usernameOrEmail: "testuser",
setupMocks: func() (*testutils.MockUserRepository, EmailSender) {
userRepo := testutils.NewMockUserRepository()
user := &database.User{
ID: 1,
Username: "testuser",
Email: "test@example.com",
}
userRepo.Create(user)
emailSender := &testutils.MockEmailSender{}
return userRepo, emailSender
},
expectedError: false,
shouldSendEmail: true,
},
{
name: "successful request by email",
usernameOrEmail: "test@example.com",
setupMocks: func() (*testutils.MockUserRepository, EmailSender) {
userRepo := testutils.NewMockUserRepository()
user := &database.User{
ID: 1,
Username: "testuser",
Email: "test@example.com",
}
userRepo.Create(user)
emailSender := &testutils.MockEmailSender{}
return userRepo, emailSender
},
expectedError: false,
shouldSendEmail: true,
},
{
name: "empty input",
usernameOrEmail: "",
setupMocks: func() (*testutils.MockUserRepository, EmailSender) {
return testutils.NewMockUserRepository(), &testutils.MockEmailSender{}
},
expectedError: true,
shouldSendEmail: false,
},
{
name: "whitespace only input",
usernameOrEmail: " ",
setupMocks: func() (*testutils.MockUserRepository, EmailSender) {
return testutils.NewMockUserRepository(), &testutils.MockEmailSender{}
},
expectedError: true,
shouldSendEmail: false,
},
{
name: "user not found",
usernameOrEmail: "nonexistent",
setupMocks: func() (*testutils.MockUserRepository, EmailSender) {
return testutils.NewMockUserRepository(), &testutils.MockEmailSender{}
},
expectedError: false,
shouldSendEmail: false,
},
{
name: "email service error",
usernameOrEmail: "testuser",
setupMocks: func() (*testutils.MockUserRepository, EmailSender) {
userRepo := testutils.NewMockUserRepository()
user := &database.User{
ID: 1,
Username: "testuser",
Email: "test@example.com",
}
userRepo.Create(user)
var errorSender errorEmailSender
errorSender.err = errors.New("email service error")
emailSender := &errorSender
return userRepo, emailSender
},
expectedError: true,
shouldSendEmail: false,
},
{
name: "prefers email over username",
usernameOrEmail: "test@example.com",
setupMocks: func() (*testutils.MockUserRepository, EmailSender) {
userRepo := testutils.NewMockUserRepository()
user := &database.User{
ID: 1,
Username: "testuser",
Email: "test@example.com",
}
userRepo.Create(user)
emailSender := &testutils.MockEmailSender{}
return userRepo, emailSender
},
expectedError: false,
shouldSendEmail: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
userRepo, emailSender := tt.setupMocks()
emailService, _ := NewEmailService(testutils.AppTestConfig, emailSender)
service := NewPasswordResetService(userRepo, emailService)
err := service.RequestPasswordReset(tt.usernameOrEmail)
if tt.expectedError {
if err == nil {
t.Error("expected error but got none")
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if tt.shouldSendEmail {
user, _ := userRepo.GetByUsername("testuser")
if user == nil {
user, _ = userRepo.GetByEmail("test@example.com")
}
if user != nil && user.PasswordResetToken == "" {
t.Error("expected password reset token to be set")
}
}
}
})
}
}
func TestPasswordResetService_ResetPassword(t *testing.T) {
tests := []struct {
name string
token string
newPassword string
setupMocks func() (*testutils.MockUserRepository, EmailSender)
expectedError bool
verifyPassword bool
}{
{
name: "successful password reset",
token: "valid-token",
newPassword: "NewSecurePass123!",
setupMocks: func() (*testutils.MockUserRepository, EmailSender) {
userRepo := testutils.NewMockUserRepository()
expiresAt := time.Now().Add(time.Hour)
user := &database.User{
ID: 1,
Username: "testuser",
Email: "test@example.com",
PasswordResetToken: HashVerificationToken("valid-token"),
PasswordResetExpiresAt: &expiresAt,
}
userRepo.Create(user)
return userRepo, &testutils.MockEmailSender{}
},
expectedError: false,
verifyPassword: true,
},
{
name: "empty token",
token: "",
newPassword: "NewSecurePass123!",
setupMocks: func() (*testutils.MockUserRepository, EmailSender) {
return testutils.NewMockUserRepository(), &testutils.MockEmailSender{}
},
expectedError: true,
verifyPassword: false,
},
{
name: "whitespace only token",
token: " ",
newPassword: "NewSecurePass123!",
setupMocks: func() (*testutils.MockUserRepository, EmailSender) {
return testutils.NewMockUserRepository(), &testutils.MockEmailSender{}
},
expectedError: true,
verifyPassword: false,
},
{
name: "invalid token",
token: "invalid-token",
newPassword: "NewSecurePass123!",
setupMocks: func() (*testutils.MockUserRepository, EmailSender) {
return testutils.NewMockUserRepository(), &testutils.MockEmailSender{}
},
expectedError: true,
verifyPassword: false,
},
{
name: "expired token",
token: "expired-token",
newPassword: "NewSecurePass123!",
setupMocks: func() (*testutils.MockUserRepository, EmailSender) {
userRepo := testutils.NewMockUserRepository()
expiresAt := time.Now().Add(-time.Hour)
user := &database.User{
ID: 1,
Username: "testuser",
Email: "test@example.com",
PasswordResetToken: HashVerificationToken("expired-token"),
PasswordResetExpiresAt: &expiresAt,
}
userRepo.Create(user)
return userRepo, &testutils.MockEmailSender{}
},
expectedError: true,
verifyPassword: false,
},
{
name: "nil expiration date",
token: "valid-token",
newPassword: "NewSecurePass123!",
setupMocks: func() (*testutils.MockUserRepository, EmailSender) {
userRepo := testutils.NewMockUserRepository()
user := &database.User{
ID: 1,
Username: "testuser",
Email: "test@example.com",
PasswordResetToken: HashVerificationToken("valid-token"),
}
userRepo.Create(user)
return userRepo, &testutils.MockEmailSender{}
},
expectedError: true,
verifyPassword: false,
},
{
name: "invalid password",
token: "valid-token",
newPassword: "short",
setupMocks: func() (*testutils.MockUserRepository, EmailSender) {
userRepo := testutils.NewMockUserRepository()
expiresAt := time.Now().Add(time.Hour)
user := &database.User{
ID: 1,
Username: "testuser",
Email: "test@example.com",
PasswordResetToken: HashVerificationToken("valid-token"),
PasswordResetExpiresAt: &expiresAt,
}
userRepo.Create(user)
return userRepo, &testutils.MockEmailSender{}
},
expectedError: true,
verifyPassword: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
userRepo, emailSender := tt.setupMocks()
emailService, _ := NewEmailService(testutils.AppTestConfig, emailSender)
service := NewPasswordResetService(userRepo, emailService)
err := service.ResetPassword(tt.token, tt.newPassword)
if tt.expectedError {
if err == nil {
t.Error("expected error but got none")
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if tt.verifyPassword {
user, _ := userRepo.GetByUsername("testuser")
if user == nil {
t.Fatal("expected user to exist")
}
if user.PasswordResetToken != "" {
t.Error("expected password reset token to be cleared")
}
if user.PasswordResetExpiresAt != nil {
t.Error("expected password reset expiration to be cleared")
}
if user.Password == "" {
t.Error("expected password to be set")
}
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(tt.newPassword))
if err != nil {
t.Errorf("password hash verification failed: %v", err)
}
}
}
})
}
}
func TestPasswordResetService_ResetPassword_TokenClearedAfterExpiration(t *testing.T) {
userRepo := testutils.NewMockUserRepository()
expiresAt := time.Now().Add(-time.Hour)
user := &database.User{
ID: 1,
Username: "testuser",
Email: "test@example.com",
PasswordResetToken: HashVerificationToken("expired-token"),
PasswordResetExpiresAt: &expiresAt,
}
userRepo.Create(user)
emailService, _ := NewEmailService(testutils.AppTestConfig, &testutils.MockEmailSender{})
service := NewPasswordResetService(userRepo, emailService)
err := service.ResetPassword("expired-token", "NewSecurePass123!")
if err == nil {
t.Error("expected error for expired token")
}
updatedUser, _ := userRepo.GetByID(1)
if updatedUser == nil {
t.Fatal("expected user to exist")
}
if updatedUser.PasswordResetToken != "" {
t.Error("expected password reset token to be cleared after expiration")
}
if updatedUser.PasswordResetExpiresAt != nil {
t.Error("expected password reset expiration to be cleared after expiration")
}
}
func TestPasswordResetService_RequestPasswordReset_EmailFailureRollback(t *testing.T) {
userRepo := testutils.NewMockUserRepository()
user := &database.User{
ID: 1,
Username: "testuser",
Email: "test@example.com",
}
userRepo.Create(user)
emailSender := &errorEmailSender{err: errors.New("email service error")}
emailService, _ := NewEmailService(testutils.AppTestConfig, emailSender)
service := NewPasswordResetService(userRepo, emailService)
err := service.RequestPasswordReset("testuser")
if err == nil {
t.Error("expected error when email fails")
}
updatedUser, _ := userRepo.GetByID(1)
if updatedUser == nil {
t.Fatal("expected user to exist")
}
if updatedUser.PasswordResetToken != "" {
t.Error("expected password reset token to be rolled back on email failure")
}
if updatedUser.PasswordResetSentAt != nil {
t.Error("expected password reset sent at to be rolled back on email failure")
}
if updatedUser.PasswordResetExpiresAt != nil {
t.Error("expected password reset expiration to be rolled back on email failure")
}
}