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:
@@ -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" {
|
||||
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) {
|
||||
|
||||
@@ -13,7 +13,6 @@ func TestAuthFlow(t *testing.T) {
|
||||
reqBody := RegisterRequest{
|
||||
Email: "test@example.com",
|
||||
Password: "Password123!",
|
||||
Timezone: "UTC",
|
||||
}
|
||||
|
||||
rr := makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", reqBody, "")
|
||||
@@ -37,7 +36,6 @@ func TestAuthFlow(t *testing.T) {
|
||||
reqBody := RegisterRequest{
|
||||
Email: "duplicate@example.com",
|
||||
Password: "Password123!",
|
||||
Timezone: "UTC",
|
||||
}
|
||||
|
||||
makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", reqBody, "")
|
||||
@@ -53,7 +51,6 @@ func TestAuthFlow(t *testing.T) {
|
||||
reqBody := RegisterRequest{
|
||||
Email: "invalid-email",
|
||||
Password: "Password123!",
|
||||
Timezone: "UTC",
|
||||
}
|
||||
|
||||
rr := makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", reqBody, "")
|
||||
@@ -67,7 +64,6 @@ func TestAuthFlow(t *testing.T) {
|
||||
reqBody := RegisterRequest{
|
||||
Email: "short@example.com",
|
||||
Password: "123",
|
||||
Timezone: "UTC",
|
||||
}
|
||||
|
||||
rr := makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", reqBody, "")
|
||||
@@ -81,7 +77,6 @@ func TestAuthFlow(t *testing.T) {
|
||||
registerBody := RegisterRequest{
|
||||
Email: "login@example.com",
|
||||
Password: "Password123!",
|
||||
Timezone: "UTC",
|
||||
}
|
||||
makeRequest(t, *ts.Router, "POST", "/api/v1/auth/register", registerBody, "")
|
||||
|
||||
@@ -108,7 +103,6 @@ func TestAuthFlow(t *testing.T) {
|
||||
registerBody := RegisterRequest{
|
||||
Email: "wrongpass@example.com",
|
||||
Password: "Password123!",
|
||||
Timezone: "UTC",
|
||||
}
|
||||
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{
|
||||
Email: email,
|
||||
Password: password,
|
||||
Timezone: "UTC",
|
||||
}
|
||||
makeRequest(t, router, "POST", "/api/v1/auth/register", registerBody, "")
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestUsersTableSchema(t *testing.T) {
|
||||
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 {
|
||||
query := "SELECT " + col + " FROM users LIMIT 0"
|
||||
rows, err := db.Query(query)
|
||||
|
||||
@@ -111,7 +111,6 @@ func TestUserRepositoryFindByID(t *testing.T) {
|
||||
if found.Email != user.Email {
|
||||
t.Errorf("Expected email %s, got %s", user.Email, found.Email)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserRepositoryFindByIDNotFound(t *testing.T) {
|
||||
@@ -207,7 +206,6 @@ func TestUserRepositoryUpdate(t *testing.T) {
|
||||
if found.Email != "updated@example.com" {
|
||||
t.Errorf("Expected email updated@example.com, got %s", found.Email)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserRepositoryUpdateNotFound(t *testing.T) {
|
||||
|
||||
@@ -118,77 +118,55 @@ func TestValidateRegistration(t *testing.T) {
|
||||
name string
|
||||
email string
|
||||
password string
|
||||
timezone string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"valid registration",
|
||||
"user@example.com",
|
||||
"Passw0rd!",
|
||||
"UTC",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"valid with complex email",
|
||||
"user.name+tag@example.co.uk",
|
||||
"MyS3cur3P@ss",
|
||||
"Europe/Madrid",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid email",
|
||||
"invalid-email",
|
||||
"Passw0rd!",
|
||||
"UTC",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"invalid password",
|
||||
"user@example.com",
|
||||
"weak",
|
||||
"UTC",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"invalid timezone",
|
||||
"user@example.com",
|
||||
"Passw0rd!",
|
||||
"InvalidTZ",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"all invalid",
|
||||
"not-an-email",
|
||||
"weak",
|
||||
"bad-tz",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty email",
|
||||
"",
|
||||
"Passw0rd!",
|
||||
"UTC",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty password",
|
||||
"user@example.com",
|
||||
"",
|
||||
"UTC",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty timezone",
|
||||
"user@example.com",
|
||||
"Passw0rd!",
|
||||
"",
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
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 {
|
||||
t.Errorf("ValidateRegistration() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user