import pytest from unittest.mock import AsyncMock, MagicMock from sqlalchemy.exc import IntegrityError, OperationalError from app.crud.user import get_user_by_email, create_user from app.schemas.user import UserCreate from app.models import User as UserModel from app.core.exceptions import ( UserCreationError, EmailAlreadyRegisteredError, DatabaseConnectionError, DatabaseIntegrityError, DatabaseQueryError, DatabaseTransactionError ) # Fixtures @pytest.fixture def mock_db_session(): return AsyncMock() @pytest.fixture def user_create_data(): return UserCreate(email="test@example.com", password="password123", name="Test User") @pytest.fixture def existing_user_data(): return UserModel(id=1, email="exists@example.com", password_hash="hashed_password", name="Existing User") # Tests for get_user_by_email @pytest.mark.asyncio async def test_get_user_by_email_found(mock_db_session, existing_user_data): mock_db_session.execute.return_value.scalars.return_value.first.return_value = existing_user_data user = await get_user_by_email(mock_db_session, "exists@example.com") assert user is not None assert user.email == "exists@example.com" mock_db_session.execute.assert_called_once() @pytest.mark.asyncio async def test_get_user_by_email_not_found(mock_db_session): mock_db_session.execute.return_value.scalars.return_value.first.return_value = None user = await get_user_by_email(mock_db_session, "nonexistent@example.com") assert user is None mock_db_session.execute.assert_called_once() @pytest.mark.asyncio async def test_get_user_by_email_db_connection_error(mock_db_session): mock_db_session.execute.side_effect = OperationalError("mock_op_error", "params", "orig") with pytest.raises(DatabaseConnectionError): await get_user_by_email(mock_db_session, "test@example.com") @pytest.mark.asyncio async def test_get_user_by_email_db_query_error(mock_db_session): # Simulate a generic SQLAlchemyError that is not OperationalError mock_db_session.execute.side_effect = IntegrityError("mock_sql_error", "params", "orig") # Using IntegrityError as an example of SQLAlchemyError with pytest.raises(DatabaseQueryError): await get_user_by_email(mock_db_session, "test@example.com") # Tests for create_user @pytest.mark.asyncio async def test_create_user_success(mock_db_session, user_create_data): # The actual user object returned would be created by SQLAlchemy based on db_user # We mock the process: db.add is called, then db.flush, then db.refresh updates db_user async def mock_refresh(user_model_instance): user_model_instance.id = 1 # Simulate DB assigning an ID # Simulate other db-generated fields if necessary return None mock_db_session.refresh = AsyncMock(side_effect=mock_refresh) mock_db_session.flush = AsyncMock() mock_db_session.add = MagicMock() created_user = await create_user(mock_db_session, user_create_data) mock_db_session.add.assert_called_once() mock_db_session.flush.assert_called_once() mock_db_session.refresh.assert_called_once() assert created_user is not None assert created_user.email == user_create_data.email assert created_user.name == user_create_data.name assert hasattr(created_user, 'id') # Check if ID was assigned (simulated by mock_refresh) # Password hash check would be more involved, ensure hash_password was called correctly # For now, we assume hash_password works as intended and is tested elsewhere. @pytest.mark.asyncio async def test_create_user_email_already_registered(mock_db_session, user_create_data): mock_db_session.flush.side_effect = IntegrityError("mock error (unique constraint)", "params", "orig") with pytest.raises(EmailAlreadyRegisteredError): await create_user(mock_db_session, user_create_data) @pytest.mark.asyncio async def test_create_user_db_integrity_error_not_unique(mock_db_session, user_create_data): # Simulate an IntegrityError that is not related to a unique constraint mock_db_session.flush.side_effect = IntegrityError("mock error (not unique constraint)", "params", "orig") with pytest.raises(DatabaseIntegrityError): await create_user(mock_db_session, user_create_data) @pytest.mark.asyncio async def test_create_user_db_connection_error(mock_db_session, user_create_data): mock_db_session.begin.side_effect = OperationalError("mock_op_error", "params", "orig") with pytest.raises(DatabaseConnectionError): await create_user(mock_db_session, user_create_data) # also test OperationalError on flush mock_db_session.begin.side_effect = None # reset side effect mock_db_session.flush.side_effect = OperationalError("mock_op_error", "params", "orig") with pytest.raises(DatabaseConnectionError): await create_user(mock_db_session, user_create_data) @pytest.mark.asyncio async def test_create_user_db_transaction_error(mock_db_session, user_create_data): # Simulate a generic SQLAlchemyError on flush that is not IntegrityError or OperationalError mock_db_session.flush.side_effect = UserCreationError("Simulated non-specific SQLAlchemyError") # Or any other SQLAlchemyError with pytest.raises(DatabaseTransactionError): await create_user(mock_db_session, user_create_data)