Add tests for RequestPasswordResetHandler and fix test compilation errors

- Add comprehensive tests for RequestPasswordResetHandler covering success and error cases
- Fix timezone-related test failures after removal of User.Timezone field:
  - Remove Timezone assertions from login_user_test.go
  - Remove Timezone field from RegisterRequest in integration tests
  - Update ValidateRegistration test cases (no longer validates timezone)
  - Update migrations_test to check current user table schema
- Fix syntax errors in user_repository_test.go (duplicate closing braces on lines 114 and 210)
This commit is contained in:
2025-12-01 23:04:40 +01:00
parent 5d92820591
commit f780c69806
7 changed files with 338 additions and 37 deletions
@@ -0,0 +1,336 @@
package commands
import (
"context"
"testing"
"time"
"apocapoc-api/internal/domain/entities"
"apocapoc-api/internal/domain/services"
"apocapoc-api/internal/shared/errors"
)
type mockRequestResetUserRepo struct {
findByEmailFunc func(ctx context.Context, email string) (*entities.User, error)
users map[string]*entities.User
}
func (m *mockRequestResetUserRepo) FindByEmail(ctx context.Context, email string) (*entities.User, error) {
if m.findByEmailFunc != nil {
return m.findByEmailFunc(ctx, email)
}
if user, ok := m.users[email]; ok {
return user, nil
}
return nil, errors.ErrNotFound
}
func (m *mockRequestResetUserRepo) FindByID(ctx context.Context, id string) (*entities.User, error) {
return nil, errors.ErrNotFound
}
func (m *mockRequestResetUserRepo) FindByVerificationToken(ctx context.Context, token string) (*entities.User, error) {
return nil, errors.ErrNotFound
}
func (m *mockRequestResetUserRepo) Create(ctx context.Context, user *entities.User) error {
return nil
}
func (m *mockRequestResetUserRepo) Update(ctx context.Context, user *entities.User) error {
return nil
}
func (m *mockRequestResetUserRepo) Delete(ctx context.Context, id string) error {
return nil
}
type mockRequestResetTokenRepo struct {
createFunc func(ctx context.Context, token *entities.PasswordResetToken) error
tokens []*entities.PasswordResetToken
}
func (m *mockRequestResetTokenRepo) Create(ctx context.Context, token *entities.PasswordResetToken) error {
if m.createFunc != nil {
return m.createFunc(ctx, token)
}
m.tokens = append(m.tokens, token)
return nil
}
func (m *mockRequestResetTokenRepo) FindByToken(ctx context.Context, token string) (*entities.PasswordResetToken, error) {
return nil, errors.ErrNotFound
}
func (m *mockRequestResetTokenRepo) Update(ctx context.Context, token *entities.PasswordResetToken) error {
return nil
}
func (m *mockRequestResetTokenRepo) DeleteExpired(ctx context.Context) error {
return nil
}
type mockRequestResetEmailService struct {
sendFunc func(message services.EmailMessage) error
sentMessages []services.EmailMessage
}
func (m *mockRequestResetEmailService) Send(message services.EmailMessage) error {
if m.sendFunc != nil {
return m.sendFunc(message)
}
m.sentMessages = append(m.sentMessages, message)
return nil
}
func TestRequestPasswordResetHandler_Success(t *testing.T) {
user := entities.NewUser("test@example.com", "hash")
user.ID = "user-123"
user.EmailVerified = true
userRepo := &mockRequestResetUserRepo{
users: map[string]*entities.User{
user.Email: user,
},
}
tokenRepo := &mockRequestResetTokenRepo{
tokens: []*entities.PasswordResetToken{},
}
emailService := &mockRequestResetEmailService{
sentMessages: []services.EmailMessage{},
}
handler := NewRequestPasswordResetHandler(userRepo, tokenRepo, emailService, "http://localhost:8080")
cmd := RequestPasswordResetCommand{
Email: user.Email,
}
err := handler.Handle(context.Background(), cmd)
if err != nil {
t.Fatalf("Handle() unexpected error = %v", err)
}
if len(tokenRepo.tokens) != 1 {
t.Fatalf("Expected 1 token created, got %d", len(tokenRepo.tokens))
}
createdToken := tokenRepo.tokens[0]
if createdToken.UserID != user.ID {
t.Errorf("Token UserID = %v, want %v", createdToken.UserID, user.ID)
}
if createdToken.Token == "" {
t.Error("Token string is empty")
}
if createdToken.ExpiresAt.Before(time.Now()) {
t.Error("Token already expired")
}
expectedExpiry := time.Now().Add(1 * time.Hour)
diff := createdToken.ExpiresAt.Sub(expectedExpiry)
if diff > time.Minute || diff < -time.Minute {
t.Errorf("Token expiry = %v, expected around %v", createdToken.ExpiresAt, expectedExpiry)
}
if len(emailService.sentMessages) != 1 {
t.Fatalf("Expected 1 email sent, got %d", len(emailService.sentMessages))
}
sentEmail := emailService.sentMessages[0]
if sentEmail.To != user.Email {
t.Errorf("Email To = %v, want %v", sentEmail.To, user.Email)
}
if sentEmail.Subject != "Password Reset Request" {
t.Errorf("Email Subject = %v, want %v", sentEmail.Subject, "Password Reset Request")
}
if !sentEmail.IsHTML {
t.Error("Email should be HTML")
}
if sentEmail.Body == "" {
t.Error("Email body is empty")
}
}
func TestRequestPasswordResetHandler_EmptyEmail(t *testing.T) {
handler := NewRequestPasswordResetHandler(
&mockRequestResetUserRepo{users: make(map[string]*entities.User)},
&mockRequestResetTokenRepo{tokens: []*entities.PasswordResetToken{}},
&mockRequestResetEmailService{},
"http://localhost:8080",
)
cmd := RequestPasswordResetCommand{
Email: "",
}
err := handler.Handle(context.Background(), cmd)
if err != errors.ErrInvalidInput {
t.Errorf("Handle() error = %v, want %v", err, errors.ErrInvalidInput)
}
}
func TestRequestPasswordResetHandler_UserNotFound(t *testing.T) {
handler := NewRequestPasswordResetHandler(
&mockRequestResetUserRepo{users: make(map[string]*entities.User)},
&mockRequestResetTokenRepo{tokens: []*entities.PasswordResetToken{}},
&mockRequestResetEmailService{},
"http://localhost:8080",
)
cmd := RequestPasswordResetCommand{
Email: "nonexistent@example.com",
}
err := handler.Handle(context.Background(), cmd)
if err != errors.ErrNotFound {
t.Errorf("Handle() error = %v, want %v", err, errors.ErrNotFound)
}
}
func TestRequestPasswordResetHandler_EmailNotVerified(t *testing.T) {
user := entities.NewUser("test@example.com", "hash")
user.ID = "user-123"
user.EmailVerified = false
userRepo := &mockRequestResetUserRepo{
users: map[string]*entities.User{
user.Email: user,
},
}
handler := NewRequestPasswordResetHandler(
userRepo,
&mockRequestResetTokenRepo{tokens: []*entities.PasswordResetToken{}},
&mockRequestResetEmailService{},
"http://localhost:8080",
)
cmd := RequestPasswordResetCommand{
Email: user.Email,
}
err := handler.Handle(context.Background(), cmd)
if err != errors.ErrEmailNotVerified {
t.Errorf("Handle() error = %v, want %v", err, errors.ErrEmailNotVerified)
}
}
func TestRequestPasswordResetHandler_TokenCreationFailure(t *testing.T) {
user := entities.NewUser("test@example.com", "hash")
user.ID = "user-123"
user.EmailVerified = true
userRepo := &mockRequestResetUserRepo{
users: map[string]*entities.User{
user.Email: user,
},
}
tokenRepo := &mockRequestResetTokenRepo{
createFunc: func(ctx context.Context, token *entities.PasswordResetToken) error {
return errors.ErrInvalidInput
},
}
emailService := &mockRequestResetEmailService{}
handler := NewRequestPasswordResetHandler(userRepo, tokenRepo, emailService, "http://localhost:8080")
cmd := RequestPasswordResetCommand{
Email: user.Email,
}
err := handler.Handle(context.Background(), cmd)
if err == nil {
t.Fatal("Handle() expected error but got nil")
}
if len(emailService.sentMessages) != 0 {
t.Error("Email should not be sent if token creation fails")
}
}
func TestRequestPasswordResetHandler_EmailSendFailure(t *testing.T) {
user := entities.NewUser("test@example.com", "hash")
user.ID = "user-123"
user.EmailVerified = true
userRepo := &mockRequestResetUserRepo{
users: map[string]*entities.User{
user.Email: user,
},
}
tokenRepo := &mockRequestResetTokenRepo{
tokens: []*entities.PasswordResetToken{},
}
emailService := &mockRequestResetEmailService{
sendFunc: func(message services.EmailMessage) error {
return errors.ErrInvalidInput
},
}
handler := NewRequestPasswordResetHandler(userRepo, tokenRepo, emailService, "http://localhost:8080")
cmd := RequestPasswordResetCommand{
Email: user.Email,
}
err := handler.Handle(context.Background(), cmd)
if err == nil {
t.Fatal("Handle() expected error but got nil")
}
if len(tokenRepo.tokens) != 1 {
t.Error("Token should be created even if email sending fails")
}
}
func TestRequestPasswordResetHandler_ResetLinkFormat(t *testing.T) {
user := entities.NewUser("test@example.com", "hash")
user.ID = "user-123"
user.EmailVerified = true
userRepo := &mockRequestResetUserRepo{
users: map[string]*entities.User{
user.Email: user,
},
}
tokenRepo := &mockRequestResetTokenRepo{
tokens: []*entities.PasswordResetToken{},
}
emailService := &mockRequestResetEmailService{
sentMessages: []services.EmailMessage{},
}
appURL := "https://myapp.com"
handler := NewRequestPasswordResetHandler(userRepo, tokenRepo, emailService, appURL)
cmd := RequestPasswordResetCommand{
Email: user.Email,
}
err := handler.Handle(context.Background(), cmd)
if err != nil {
t.Fatalf("Handle() unexpected error = %v", err)
}
if len(emailService.sentMessages) != 1 {
t.Fatal("Expected 1 email sent")
}
sentEmail := emailService.sentMessages[0]
if sentEmail.Body == "" {
t.Fatal("Email body is empty")
}
}
@@ -90,10 +90,6 @@ func TestLoginUserHandler_Success(t *testing.T) {
if result.Email != "test@example.com" { if result.Email != "test@example.com" {
t.Errorf("Email = %v, want %v", result.Email, "test@example.com") t.Errorf("Email = %v, want %v", result.Email, "test@example.com")
} }
if result.Timezone != "UTC" {
t.Errorf("Timezone = %v, want %v", result.Timezone, "UTC")
}
} }
func TestLoginUserHandler_EmptyEmail(t *testing.T) { func TestLoginUserHandler_EmptyEmail(t *testing.T) {
@@ -13,7 +13,6 @@ func TestAuthFlow(t *testing.T) {
reqBody := RegisterRequest{ reqBody := RegisterRequest{
Email: "test@example.com", Email: "test@example.com",
Password: "Password123!", Password: "Password123!",
Timezone: "UTC",
} }
rr := makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", reqBody, "") rr := makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", reqBody, "")
@@ -37,7 +36,6 @@ func TestAuthFlow(t *testing.T) {
reqBody := RegisterRequest{ reqBody := RegisterRequest{
Email: "duplicate@example.com", Email: "duplicate@example.com",
Password: "Password123!", Password: "Password123!",
Timezone: "UTC",
} }
makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", reqBody, "") makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", reqBody, "")
@@ -53,7 +51,6 @@ func TestAuthFlow(t *testing.T) {
reqBody := RegisterRequest{ reqBody := RegisterRequest{
Email: "invalid-email", Email: "invalid-email",
Password: "Password123!", Password: "Password123!",
Timezone: "UTC",
} }
rr := makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", reqBody, "") rr := makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", reqBody, "")
@@ -67,7 +64,6 @@ func TestAuthFlow(t *testing.T) {
reqBody := RegisterRequest{ reqBody := RegisterRequest{
Email: "short@example.com", Email: "short@example.com",
Password: "123", Password: "123",
Timezone: "UTC",
} }
rr := makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", reqBody, "") rr := makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", reqBody, "")
@@ -81,7 +77,6 @@ func TestAuthFlow(t *testing.T) {
registerBody := RegisterRequest{ registerBody := RegisterRequest{
Email: "login@example.com", Email: "login@example.com",
Password: "Password123!", Password: "Password123!",
Timezone: "UTC",
} }
makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", registerBody, "") makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", registerBody, "")
@@ -108,7 +103,6 @@ func TestAuthFlow(t *testing.T) {
registerBody := RegisterRequest{ registerBody := RegisterRequest{
Email: "wrongpass@example.com", Email: "wrongpass@example.com",
Password: "Password123!", Password: "Password123!",
Timezone: "UTC",
} }
makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", registerBody, "") makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", registerBody, "")
@@ -120,7 +120,6 @@ func registerAndLogin(t *testing.T, router http.Handler, email, password string)
registerBody := RegisterRequest{ registerBody := RegisterRequest{
Email: email, Email: email,
Password: password, Password: password,
Timezone: "UTC",
} }
makeRequest(t, router, "POST", "/api/v1/auth/register", registerBody, "") makeRequest(t, router, "POST", "/api/v1/auth/register", registerBody, "")
@@ -43,7 +43,7 @@ func TestUsersTableSchema(t *testing.T) {
t.Fatalf("RunMigrations failed: %v", err) t.Fatalf("RunMigrations failed: %v", err)
} }
columns := []string{"id", "email", "password_hash", "timezone", "created_at", "updated_at"} columns := []string{"id", "email", "password_hash", "email_verified", "email_verification_token", "email_verification_expiry", "created_at", "updated_at"}
for _, col := range columns { for _, col := range columns {
query := "SELECT " + col + " FROM users LIMIT 0" query := "SELECT " + col + " FROM users LIMIT 0"
rows, err := db.Query(query) rows, err := db.Query(query)
@@ -111,7 +111,6 @@ func TestUserRepositoryFindByID(t *testing.T) {
if found.Email != user.Email { if found.Email != user.Email {
t.Errorf("Expected email %s, got %s", user.Email, found.Email) t.Errorf("Expected email %s, got %s", user.Email, found.Email)
} }
}
} }
func TestUserRepositoryFindByIDNotFound(t *testing.T) { func TestUserRepositoryFindByIDNotFound(t *testing.T) {
@@ -207,7 +206,6 @@ func TestUserRepositoryUpdate(t *testing.T) {
if found.Email != "updated@example.com" { if found.Email != "updated@example.com" {
t.Errorf("Expected email updated@example.com, got %s", found.Email) t.Errorf("Expected email updated@example.com, got %s", found.Email)
} }
}
} }
func TestUserRepositoryUpdateNotFound(t *testing.T) { func TestUserRepositoryUpdateNotFound(t *testing.T) {
+1 -23
View File
@@ -118,77 +118,55 @@ func TestValidateRegistration(t *testing.T) {
name string name string
email string email string
password string password string
timezone string
wantErr bool wantErr bool
}{ }{
{ {
"valid registration", "valid registration",
"user@example.com", "user@example.com",
"Passw0rd!", "Passw0rd!",
"UTC",
false, false,
}, },
{ {
"valid with complex email", "valid with complex email",
"user.name+tag@example.co.uk", "user.name+tag@example.co.uk",
"MyS3cur3P@ss", "MyS3cur3P@ss",
"Europe/Madrid",
false, false,
}, },
{ {
"invalid email", "invalid email",
"invalid-email", "invalid-email",
"Passw0rd!", "Passw0rd!",
"UTC",
true, true,
}, },
{ {
"invalid password", "invalid password",
"user@example.com", "user@example.com",
"weak", "weak",
"UTC",
true,
},
{
"invalid timezone",
"user@example.com",
"Passw0rd!",
"InvalidTZ",
true, true,
}, },
{ {
"all invalid", "all invalid",
"not-an-email", "not-an-email",
"weak", "weak",
"bad-tz",
true, true,
}, },
{ {
"empty email", "empty email",
"", "",
"Passw0rd!", "Passw0rd!",
"UTC",
true, true,
}, },
{ {
"empty password", "empty password",
"user@example.com", "user@example.com",
"", "",
"UTC",
true,
},
{
"empty timezone",
"user@example.com",
"Passw0rd!",
"",
true, true,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err := ValidateRegistration(tt.email, tt.password, tt.timezone) err := ValidateRegistration(tt.email, tt.password)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("ValidateRegistration() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("ValidateRegistration() error = %v, wantErr %v", err, tt.wantErr)
} }