375 lines
11 KiB
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")
|
|
}
|