"""Unit tests for the backend.services.invitees module.""" from unittest.mock import AsyncMock from unittest.mock import MagicMock from unittest.mock import patch import pytest from sqlalchemy import delete from sqlalchemy import select from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession from backend.models import Invitee from backend.models import InviteeList from backend.schemas.invitees import InviteeResponse from backend.schemas.users import UserResponse from backend.services.invitees import add_invitee_to_booking from backend.services.invitees import get_invitees_for_booking from backend.services.invitees import remove_invitee_from_booking @pytest.mark.asyncio @pytest.mark.parametrize("mock_logger", ["backend.services.invitees"], indirect=True) async def test_get_invitees_for_booking_success( async_session: AsyncSession, sample_invitees: InviteeList, mock_logger: MagicMock ) -> None: """Test successful retrieval of all invitees for a booking. Verifies that get_invitees_for_booking returns the expected list of users and constructs the correct SQLAlchemy query. Args: async_session: The asynchronous database session. sample_invitees: The mocked list of invitees to return. mock_logger: The mocked logger instance. """ booking_id = 1 mock_scalars_result = AsyncMock() mock_scalars_result.all = MagicMock(return_value=sample_invitees) mock_scalars = AsyncMock(return_value=mock_scalars_result) with patch.object(async_session, "scalars", mock_scalars): result = await get_invitees_for_booking(async_session, booking_id) assert isinstance(result, list) assert len(result) == 2 # Convert sample_invitees (Invitee) to InviteeResponse for comparison expected = [ InviteeResponse.model_validate( { **inv.__dict__, "user": UserResponse.model_validate(inv.user) if inv.user else None, }, ) for inv in sample_invitees ] assert result == expected mock_scalars.assert_called_once() # Compare SQL string representations for reliability actual_sql = str(mock_scalars.call_args.args[0]) expected_sql = str(select(Invitee).where(Invitee.booking_id == booking_id)) assert actual_sql == expected_sql @pytest.mark.asyncio @pytest.mark.parametrize("mock_logger", ["backend.services.invitees"], indirect=True) async def test_get_invitees_for_booking_empty( async_session: AsyncSession, mock_logger: MagicMock ) -> None: """Test retrieval of invitees when none exist for the booking. Verifies that get_invitees_for_booking returns an empty list when no invitees are found. Args: async_session: The asynchronous database session. mock_logger: The mocked logger instance. """ booking_id = 1 mock_scalars_result = AsyncMock() mock_scalars_result.all = AsyncMock(return_value=[]) mock_scalars = AsyncMock(return_value=mock_scalars_result) with patch.object(async_session, "scalars", mock_scalars): result = await get_invitees_for_booking(async_session, booking_id) assert isinstance(result, list) assert len(result) == 0 mock_scalars.assert_called_once() # Compare SQL string representations for reliability actual_sql = str(mock_scalars.call_args.args[0]) expected_sql = str(select(Invitee).where(Invitee.booking_id == booking_id)) assert actual_sql == expected_sql @pytest.mark.asyncio @pytest.mark.parametrize("mock_logger", ["backend.services.invitees"], indirect=True) async def test_get_invitees_for_booking_database_error( async_session: AsyncSession, mock_logger: MagicMock ) -> None: """Test handling of database errors in get_invitees_for_booking. Verifies that get_invitees_for_booking raises an exception on database failure. Args: async_session: The asynchronous database session. mock_logger: The mocked logger instance. """ booking_id = 1 mock_scalars = AsyncMock(side_effect=SQLAlchemyError("Database error")) with patch.object(async_session, "scalars", mock_scalars): with pytest.raises(SQLAlchemyError): await get_invitees_for_booking(async_session, booking_id) mock_scalars.assert_called_once() @pytest.mark.asyncio @pytest.mark.parametrize("mock_logger", ["backend.services.invitees"], indirect=True) async def test_add_invitee_to_booking_success( async_session: AsyncSession, sample_invitee: Invitee, mock_logger: MagicMock, ) -> None: """Test successful addition of an invitee to a booking. Verifies that add_invitee_to_booking adds the invitee, commits the session, and returns the invitee. Args: async_session: The asynchronous database session. sample_invitee: The mocked invitee object. mock_logger: The mocked logger instance. """ mock_execute_result = MagicMock() mock_execute_result.scalar_one = MagicMock(return_value=0) with ( patch.object(async_session, "add", new_callable=MagicMock) as mock_add, patch.object(async_session, "commit", new_callable=AsyncMock) as mock_commit, patch.object( async_session, "execute", AsyncMock(return_value=mock_execute_result) ), patch( "backend.services.bookings.get_booking", new=AsyncMock(return_value=MagicMock(room_id=1)), ), patch( "backend.services.rooms.get_room", new=AsyncMock(return_value=MagicMock(capacity=10)), ), patch( "backend.services.invitees.get_invitees_for_booking", new=AsyncMock(return_value=[]), ), ): result = await add_invitee_to_booking( async_session, sample_invitee.booking_id, sample_invitee.user_email ) assert result.booking_id == sample_invitee.booking_id assert result.user_email == sample_invitee.user_email mock_add.assert_called_once() mock_commit.assert_called_once() @pytest.mark.asyncio @pytest.mark.parametrize("mock_logger", ["backend.services.invitees"], indirect=True) async def test_add_invitee_to_booking_database_error( async_session: AsyncSession, mock_logger: MagicMock ) -> None: """Test handling of database errors in add_invitee_to_booking. Verifies that add_invitee_to_booking rolls back the session on database failure. Args: async_session: The asynchronous database session. mock_logger: The mocked logger instance. """ booking_id = 1 user_email = "user1@example.com" invitee = Invitee(booking_id=booking_id, user_email=user_email) mock_execute_result = MagicMock() mock_execute_result.scalar_one = MagicMock(return_value=0) with ( patch.object(async_session, "add", MagicMock()) as mock_add, patch.object( async_session, "commit", AsyncMock(side_effect=SQLAlchemyError("Database error")), ) as mock_commit, patch.object(async_session, "rollback", AsyncMock()) as mock_rollback, patch.object( async_session, "execute", AsyncMock(return_value=mock_execute_result) ), patch( "backend.services.bookings.get_booking", new=AsyncMock(return_value=MagicMock(room_id=1)), ), patch( "backend.services.rooms.get_room", new=AsyncMock(return_value=MagicMock(capacity=10)), ), patch( "backend.services.invitees.get_invitees_for_booking", new=AsyncMock(return_value=[]), ), ): with pytest.raises(SQLAlchemyError): await add_invitee_to_booking(async_session, booking_id, user_email) mock_add.assert_called_once_with(invitee) mock_commit.assert_called_once() mock_rollback.assert_called_once() @pytest.mark.asyncio @pytest.mark.parametrize("mock_logger", ["backend.services.invitees"], indirect=True) @pytest.mark.parametrize( "booking_id, user_email", [ (1, "user1@example.com"), (2, "user2@example.com"), ], ids=["invitee1", "invitee2"], ) async def test_remove_invitee_from_booking_success( async_session: AsyncSession, booking_id: int, user_email: str, mock_logger: MagicMock, ) -> None: """Test successful removal of an invitee from a booking. Verifies that remove_invitee_from_booking deletes the invitee and commits the session. Args: async_session: The asynchronous database session. booking_id: The ID of the booking to remove the invitee from. user_email: The email of the user to remove as an invitee. mock_logger: The mocked logger instance. """ mock_execute_result = MagicMock(rowcount=1) async_session.execute = AsyncMock( # type: ignore [method-assign] return_value=mock_execute_result ) async_session.commit = AsyncMock() # type: ignore [method-assign] await remove_invitee_from_booking(async_session, booking_id, user_email) async_session.execute.assert_called_once() assert async_session.execute.call_args.args[0].compare( delete(Invitee).where( Invitee.booking_id == booking_id, Invitee.user_email == user_email ) ) async_session.commit.assert_called_once() @pytest.mark.asyncio @pytest.mark.parametrize("mock_logger", ["backend.services.invitees"], indirect=True) async def test_remove_invitee_from_booking_database_error( async_session: AsyncSession, mock_logger: MagicMock ) -> None: """Test handling of database errors in remove_invitee_from_booking. Verifies that remove_invitee_from_booking rolls back the session on database failure. Args: async_session: The asynchronous database session. mock_logger: The mocked logger instance. """ booking_id = 1 user_email = "user1@example.com" async_session.execute = AsyncMock( # type: ignore [method-assign] side_effect=SQLAlchemyError("Database error") ) async_session.rollback = AsyncMock() # type: ignore [method-assign] with pytest.raises(SQLAlchemyError): await remove_invitee_from_booking(async_session, booking_id, user_email) async_session.execute.assert_called_once() assert async_session.execute.call_args.args[0].compare( delete(Invitee).where( Invitee.booking_id == booking_id, Invitee.user_email == user_email ) ) async_session.rollback.assert_called_once()