Files
conference-room-booking-system/backend/tests/services/test_invitees.py
2025-09-03 18:16:52 -04:00

280 lines
10 KiB
Python

"""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()