package handlers import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "veza-backend-api/internal/services" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "go.uber.org/zap" ) // MockPasswordResetService mocks PasswordResetService type MockPasswordResetService struct { mock.Mock } func (m *MockPasswordResetService) GenerateToken() (string, error) { args := m.Called() return args.String(0), args.Error(1) } func (m *MockPasswordResetService) StoreToken(userID uuid.UUID, token string) error { args := m.Called(userID, token) return args.Error(0) } func (m *MockPasswordResetService) VerifyToken(token string) (uuid.UUID, error) { args := m.Called(token) return args.Get(0).(uuid.UUID), args.Error(1) } func (m *MockPasswordResetService) MarkTokenAsUsed(token string) error { args := m.Called(token) return args.Error(0) } func (m *MockPasswordResetService) InvalidateOldTokens(userID uuid.UUID) error { args := m.Called(userID) return args.Error(0) } // MockPasswordService mocks PasswordService type MockPasswordService struct { mock.Mock } func (m *MockPasswordService) GetUserByEmail(email string) (*services.UserInfo, error) { args := m.Called(email) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*services.UserInfo), args.Error(1) } func (m *MockPasswordService) ValidatePassword(password string) error { args := m.Called(password) return args.Error(0) } func (m *MockPasswordService) UpdatePassword(userID uuid.UUID, password string) error { args := m.Called(userID, password) return args.Error(0) } // MockEmailService mocks EmailService type MockEmailService struct { mock.Mock } func (m *MockEmailService) SendPasswordResetEmail(userID uuid.UUID, email, token string) error { args := m.Called(userID, email, token) return args.Error(0) } // MockAuditService mocks AuditService type MockAuditService struct { mock.Mock } func (m *MockAuditService) LogPasswordResetRequest(ctx context.Context, userID *uuid.UUID, email, ip, userAgent string) error { args := m.Called(ctx, userID, email, ip, userAgent) return args.Error(0) } func (m *MockAuditService) LogPasswordReset(ctx context.Context, userID uuid.UUID, success bool, ip, userAgent string) error { args := m.Called(ctx, userID, success, ip, userAgent) return args.Error(0) } // MockAuthService mocks AuthService type MockAuthService struct { mock.Mock } func (m *MockAuthService) InvalidateAllUserSessions(ctx context.Context, userID uuid.UUID, sessionService SessionServiceInterface) error { args := m.Called(ctx, userID, sessionService) return args.Error(0) } func setupTestPasswordResetRouter( mockPasswordResetService *MockPasswordResetService, mockPasswordService *MockPasswordService, mockEmailService *MockEmailService, mockAuditService *MockAuditService, mockAuthService *MockAuthService, ) *gin.Engine { gin.SetMode(gin.TestMode) router := gin.New() logger := zap.NewNop() api := router.Group("/api/v1/auth/password") { api.POST("/reset-request", RequestPasswordResetWithInterfaces( mockPasswordResetService, mockPasswordService, mockEmailService, mockAuditService, logger, )) api.POST("/reset", ResetPasswordWithInterfaces( mockPasswordResetService, mockPasswordService, mockAuthService, nil, // sessionService - can be nil for these tests mockAuditService, logger, )) } return router } func TestRequestPasswordReset_Success(t *testing.T) { // Setup mockPasswordResetService := new(MockPasswordResetService) mockPasswordService := new(MockPasswordService) mockEmailService := new(MockEmailService) mockAuditService := new(MockAuditService) mockAuthService := new(MockAuthService) router := setupTestPasswordResetRouter( mockPasswordResetService, mockPasswordService, mockEmailService, mockAuditService, mockAuthService, ) userID := uuid.New() mockUser := &services.UserInfo{ ID: userID, Email: "test@example.com", } token := "test-reset-token" reqBody := RequestPasswordResetRequest{ Email: "test@example.com", } mockPasswordService.On("GetUserByEmail", "test@example.com").Return(mockUser, nil) mockPasswordResetService.On("InvalidateOldTokens", userID).Return(nil) mockPasswordResetService.On("GenerateToken").Return(token, nil) mockPasswordResetService.On("StoreToken", userID, token).Return(nil) mockEmailService.On("SendPasswordResetEmail", userID, "test@example.com", token).Return(nil) mockAuditService.On("LogPasswordResetRequest", mock.Anything, &userID, "test@example.com", mock.Anything, mock.Anything).Return(nil) body, _ := json.Marshal(reqBody) // Execute req, _ := http.NewRequest("POST", "/api/v1/auth/password/reset-request", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusOK, w.Code) mockPasswordService.AssertExpectations(t) mockPasswordResetService.AssertExpectations(t) } func TestRequestPasswordReset_UserNotFound(t *testing.T) { // Setup mockPasswordResetService := new(MockPasswordResetService) mockPasswordService := new(MockPasswordService) mockEmailService := new(MockEmailService) mockAuditService := new(MockAuditService) mockAuthService := new(MockAuthService) router := setupTestPasswordResetRouter( mockPasswordResetService, mockPasswordService, mockEmailService, mockAuditService, mockAuthService, ) reqBody := RequestPasswordResetRequest{ Email: "notfound@example.com", } mockPasswordService.On("GetUserByEmail", "notfound@example.com").Return(nil, assert.AnError) body, _ := json.Marshal(reqBody) // Execute req, _ := http.NewRequest("POST", "/api/v1/auth/password/reset-request", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert - Should return success for security (prevent email enumeration) assert.Equal(t, http.StatusOK, w.Code) mockPasswordResetService.AssertNotCalled(t, "GenerateToken") } func TestRequestPasswordReset_InvalidEmail(t *testing.T) { // Setup mockPasswordResetService := new(MockPasswordResetService) mockPasswordService := new(MockPasswordService) mockEmailService := new(MockEmailService) mockAuditService := new(MockAuditService) mockAuthService := new(MockAuthService) router := setupTestPasswordResetRouter( mockPasswordResetService, mockPasswordService, mockEmailService, mockAuditService, mockAuthService, ) reqBody := map[string]string{"email": "invalid-email"} body, _ := json.Marshal(reqBody) // Execute req, _ := http.NewRequest("POST", "/api/v1/auth/password/reset-request", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusBadRequest, w.Code) mockPasswordService.AssertNotCalled(t, "GetUserByEmail") } func TestResetPassword_Success(t *testing.T) { // Setup mockPasswordResetService := new(MockPasswordResetService) mockPasswordService := new(MockPasswordService) mockEmailService := new(MockEmailService) mockAuditService := new(MockAuditService) mockAuthService := new(MockAuthService) router := setupTestPasswordResetRouter( mockPasswordResetService, mockPasswordService, mockEmailService, mockAuditService, mockAuthService, ) userID := uuid.New() token := "valid-token" newPassword := "newPassword123" reqBody := ResetPasswordRequest{ Token: token, NewPassword: newPassword, } mockPasswordResetService.On("VerifyToken", token).Return(userID, nil) mockPasswordService.On("ValidatePassword", newPassword).Return(nil) mockPasswordService.On("UpdatePassword", userID, newPassword).Return(nil) mockPasswordResetService.On("MarkTokenAsUsed", token).Return(nil) mockAuthService.On("InvalidateAllUserSessions", mock.Anything, userID, mock.Anything).Return(nil) mockAuditService.On("LogPasswordReset", mock.Anything, userID, true, mock.Anything, mock.Anything).Return(nil) body, _ := json.Marshal(reqBody) // Execute req, _ := http.NewRequest("POST", "/api/v1/auth/password/reset", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusOK, w.Code) mockPasswordResetService.AssertExpectations(t) mockPasswordService.AssertExpectations(t) } func TestResetPassword_InvalidToken(t *testing.T) { // Setup mockPasswordResetService := new(MockPasswordResetService) mockPasswordService := new(MockPasswordService) mockEmailService := new(MockEmailService) mockAuditService := new(MockAuditService) mockAuthService := new(MockAuthService) router := setupTestPasswordResetRouter( mockPasswordResetService, mockPasswordService, mockEmailService, mockAuditService, mockAuthService, ) token := "invalid-token" newPassword := "newPassword123" reqBody := ResetPasswordRequest{ Token: token, NewPassword: newPassword, } mockPasswordResetService.On("VerifyToken", token).Return(uuid.Nil, assert.AnError) mockAuditService.On("LogPasswordReset", mock.Anything, uuid.Nil, false, mock.Anything, mock.Anything).Return(nil) body, _ := json.Marshal(reqBody) // Execute req, _ := http.NewRequest("POST", "/api/v1/auth/password/reset", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusBadRequest, w.Code) mockPasswordService.AssertNotCalled(t, "UpdatePassword") } func TestResetPassword_InvalidPassword(t *testing.T) { // Setup mockPasswordResetService := new(MockPasswordResetService) mockPasswordService := new(MockPasswordService) mockEmailService := new(MockEmailService) mockAuditService := new(MockAuditService) mockAuthService := new(MockAuthService) router := setupTestPasswordResetRouter( mockPasswordResetService, mockPasswordService, mockEmailService, mockAuditService, mockAuthService, ) userID := uuid.New() token := "valid-token" newPassword := "short" reqBody := ResetPasswordRequest{ Token: token, NewPassword: newPassword, } mockPasswordResetService.On("VerifyToken", token).Return(userID, nil) mockPasswordService.On("ValidatePassword", newPassword).Return(assert.AnError) body, _ := json.Marshal(reqBody) // Execute req, _ := http.NewRequest("POST", "/api/v1/auth/password/reset", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Assert assert.Equal(t, http.StatusBadRequest, w.Code) mockPasswordService.AssertNotCalled(t, "UpdatePassword") }