"""Unit tests for the backend.services.users module.""" import logging from collections.abc import Generator from unittest.mock import AsyncMock from unittest.mock import MagicMock from unittest.mock import patch import pytest from sqlalchemy import select from sqlalchemy.exc import NoResultFound from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession from backend.models import User from backend.services.users import UserList from backend.services.users import get_user from backend.services.users import get_users from ..conftest import MockLogger @pytest.fixture(autouse=True) def mock_logger() -> Generator[MockLogger, None, None]: """Fixture to mock the logger used in the users service.""" mock_logger_instance = MagicMock(spec=logging.Logger) with patch("backend.services.users.logger", mock_logger_instance): yield mock_logger_instance @pytest.fixture def sample_users() -> UserList: """Fixture to provide sample User objects for testing.""" return [ User(email="user1@example.com", name="User One"), User(email="user2@example.com", name="User Two"), ] @pytest.mark.asyncio async def test_get_users_success( async_session: AsyncSession, sample_users: UserList, mock_logger: MockLogger ) -> None: """Test successful retrieval of all users from the database. Verifies that get_users returns the expected list of users and constructs the correct SQLAlchemy query. """ mock_scalars_result = AsyncMock() mock_scalars_result.all = MagicMock(return_value=sample_users) mock_scalars = AsyncMock(return_value=mock_scalars_result) async_session.scalars = mock_scalars result: UserList = await get_users(async_session) assert isinstance(result, list) assert len(result) == 2 assert result == sample_users async_session.scalars.assert_called_once() assert async_session.scalars.call_args.args[0].compare(select(User)) @pytest.mark.asyncio async def test_get_users_empty( async_session: AsyncSession, mock_logger: MockLogger ) -> None: """Test retrieval of users when the database is empty. Verifies that get_users returns an empty list when no users are found. """ mock_scalars_result = AsyncMock() mock_scalars_result.all = MagicMock(return_value=[]) mock_scalars = AsyncMock(return_value=mock_scalars_result) async_session.scalars = mock_scalars result: UserList = await get_users(async_session) assert isinstance(result, list) assert len(result) == 0 async_session.scalars.assert_called_once() assert async_session.scalars.call_args.args[0].compare(select(User)) @pytest.mark.asyncio async def test_get_users_database_error( async_session: AsyncSession, mock_logger: MockLogger ) -> None: """Test handling of database errors in get_users. Verifies that get_users raises an exception and logs an error on database failure. """ async_session.scalars = AsyncMock(side_effect=SQLAlchemyError("Database error")) with pytest.raises(SQLAlchemyError): await get_users(async_session) async_session.scalars.assert_called_once() @pytest.mark.asyncio @pytest.mark.parametrize( "email, expected_name", [ ("user1@example.com", "User One"), ("user2@example.com", "User Two"), ], ids=["user1", "user2"], ) async def test_get_user_success( async_session: AsyncSession, email: str, expected_name: str, mock_logger: MockLogger ) -> None: """Test successful retrieval of a user by email. Verifies that get_user returns the correct user and constructs the correct query. """ user = User(email=email, name=expected_name) async_session.scalar = AsyncMock(return_value=user) result: User = await get_user(async_session, email) assert result.email == email assert result.name == expected_name async_session.scalar.assert_called_once() assert async_session.scalar.call_args.args[0].compare( select(User).where(User.email == email) ) @pytest.mark.asyncio @pytest.mark.parametrize( "email", [ "nonexistent@example.com", "invalid@domain.com", ], ids=["nonexistent_email", "invalid_email"], ) async def test_get_user_not_found( async_session: AsyncSession, mock_logger: MockLogger, email: str ) -> None: """Test handling of non-existent user in get_user. Verifies that get_user raises NoResultFound and logs an error when the user is not found. """ async_session.scalar = AsyncMock(return_value=None) with pytest.raises(NoResultFound): await get_user(async_session, email) async_session.scalar.assert_called_once() assert async_session.scalar.call_args.args[0].compare( select(User).where(User.email == email) ) @pytest.mark.asyncio async def test_get_user_database_error( async_session: AsyncSession, mock_logger: MockLogger ) -> None: """Test handling of database errors in get_user. Verifies that get_user raises an exception and logs an error on database failure. """ email: str = "user1@example.com" async_session.scalar = AsyncMock(side_effect=SQLAlchemyError("Database error")) with pytest.raises(SQLAlchemyError): await get_user(async_session, email) async_session.scalar.assert_called_once() assert async_session.scalar.call_args.args[0].compare( select(User).where(User.email == email) )