veza/veza-backend-api/internal/handlers/password_reset_handler_test.go

375 lines
11 KiB
Go

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")
}