| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- """Test authentication security to prevent user enumeration."""
- from unittest.mock import MagicMock, patch
- import pytest
- from flask import Flask
- from flask_restx import Api
- import services.errors.account
- from controllers.console.auth.error import AuthenticationFailedError
- from controllers.console.auth.login import LoginApi
- from controllers.console.error import AccountNotFound
- class TestAuthenticationSecurity:
- """Test authentication endpoints for security against user enumeration."""
- def setup_method(self):
- """Set up test fixtures."""
- self.app = Flask(__name__)
- self.api = Api(self.app)
- self.api.add_resource(LoginApi, "/login")
- self.client = self.app.test_client()
- self.app.config["TESTING"] = True
- @patch("controllers.console.wraps.db")
- @patch("controllers.console.auth.login.FeatureService.get_system_features")
- @patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
- @patch("controllers.console.auth.login.AccountService.authenticate")
- @patch("controllers.console.auth.login.AccountService.send_reset_password_email")
- @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
- @patch("controllers.console.auth.login.RegisterService.get_invitation_if_token_valid")
- def test_login_invalid_email_with_registration_allowed(
- self, mock_get_invitation, mock_send_email, mock_authenticate, mock_is_rate_limit, mock_features, mock_db
- ):
- """Test that invalid email sends reset password email when registration is allowed."""
- # Arrange
- mock_is_rate_limit.return_value = False
- mock_get_invitation.return_value = None
- mock_authenticate.side_effect = services.errors.account.AccountNotFoundError("Account not found")
- mock_db.session.query.return_value.first.return_value = MagicMock() # Mock setup exists
- mock_features.return_value.is_allow_register = True
- mock_send_email.return_value = "token123"
- # Act
- with self.app.test_request_context(
- "/login", method="POST", json={"email": "nonexistent@example.com", "password": "WrongPass123!"}
- ):
- login_api = LoginApi()
- result = login_api.post()
- # Assert
- assert result == {"result": "fail", "data": "token123", "code": "account_not_found"}
- mock_send_email.assert_called_once_with(email="nonexistent@example.com", language="en-US")
- @patch("controllers.console.wraps.db")
- @patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
- @patch("controllers.console.auth.login.AccountService.authenticate")
- @patch("controllers.console.auth.login.AccountService.add_login_error_rate_limit")
- @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
- @patch("controllers.console.auth.login.RegisterService.get_invitation_if_token_valid")
- def test_login_wrong_password_returns_error(
- self, mock_get_invitation, mock_add_rate_limit, mock_authenticate, mock_is_rate_limit, mock_db
- ):
- """Test that wrong password returns AuthenticationFailedError."""
- # Arrange
- mock_is_rate_limit.return_value = False
- mock_get_invitation.return_value = None
- mock_authenticate.side_effect = services.errors.account.AccountPasswordError("Wrong password")
- mock_db.session.query.return_value.first.return_value = MagicMock() # Mock setup exists
- # Act
- with self.app.test_request_context(
- "/login", method="POST", json={"email": "existing@example.com", "password": "WrongPass123!"}
- ):
- login_api = LoginApi()
- # Assert
- with pytest.raises(AuthenticationFailedError) as exc_info:
- login_api.post()
- assert exc_info.value.error_code == "authentication_failed"
- assert exc_info.value.description == "Invalid email or password."
- mock_add_rate_limit.assert_called_once_with("existing@example.com")
- @patch("controllers.console.wraps.db")
- @patch("controllers.console.auth.login.FeatureService.get_system_features")
- @patch("controllers.console.auth.login.AccountService.is_login_error_rate_limit")
- @patch("controllers.console.auth.login.AccountService.authenticate")
- @patch("controllers.console.auth.login.dify_config.BILLING_ENABLED", False)
- @patch("controllers.console.auth.login.RegisterService.get_invitation_if_token_valid")
- def test_login_invalid_email_with_registration_disabled(
- self, mock_get_invitation, mock_authenticate, mock_is_rate_limit, mock_features, mock_db
- ):
- """Test that invalid email raises AccountNotFound when registration is disabled."""
- # Arrange
- mock_is_rate_limit.return_value = False
- mock_get_invitation.return_value = None
- mock_authenticate.side_effect = services.errors.account.AccountNotFoundError("Account not found")
- mock_db.session.query.return_value.first.return_value = MagicMock() # Mock setup exists
- mock_features.return_value.is_allow_register = False
- # Act
- with self.app.test_request_context(
- "/login", method="POST", json={"email": "nonexistent@example.com", "password": "WrongPass123!"}
- ):
- login_api = LoginApi()
- # Assert
- with pytest.raises(AccountNotFound) as exc_info:
- login_api.post()
- assert exc_info.value.error_code == "account_not_found"
- @patch("controllers.console.wraps.db")
- @patch("controllers.console.auth.login.FeatureService.get_system_features")
- @patch("controllers.console.auth.login.AccountService.get_user_through_email")
- @patch("controllers.console.auth.login.AccountService.send_reset_password_email")
- def test_reset_password_with_existing_account(self, mock_send_email, mock_get_user, mock_features, mock_db):
- """Test that reset password returns success with token for existing accounts."""
- # Mock the setup check
- mock_db.session.query.return_value.first.return_value = MagicMock() # Mock setup exists
- # Test with existing account
- mock_get_user.return_value = MagicMock(email="existing@example.com")
- mock_send_email.return_value = "token123"
- with self.app.test_request_context("/reset-password", method="POST", json={"email": "existing@example.com"}):
- from controllers.console.auth.login import ResetPasswordSendEmailApi
- api = ResetPasswordSendEmailApi()
- result = api.post()
- assert result == {"result": "success", "data": "token123"}
|