Files
conference-room-booking-system/backend/tests/services/test_bookings.py
Cliff Hill 4b4017d7c6 Fixing things.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
2025-08-26 17:15:56 -04:00

465 lines
16 KiB
Python

"""Unit tests for the backend.services.bookings module."""
from datetime import datetime
from typing import Any
from unittest.mock import AsyncMock
from unittest.mock import MagicMock
import pytest
from sqlalchemy import delete
from sqlalchemy import select
from sqlalchemy import update
from sqlalchemy.exc import NoResultFound
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
from backend.models import Booking
from backend.models import BookingList
from backend.services.bookings import delete_booking
from backend.services.bookings import get_booking
from backend.services.bookings import get_bookings_for_room
from backend.services.bookings import new_booking
from backend.services.bookings import update_booking
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
async def test_get_bookings_for_room_success(
async_session: AsyncSession, sample_bookings: BookingList, mock_logger: MagicMock
) -> None:
"""Test successful retrieval of all bookings for a room.
Verifies that get_bookings_for_room returns the expected list of bookings and constructs
the correct SQLAlchemy query.
Args:
async_session: The asynchronous database session.
sample_bookings: The mocked list of bookings to return.
mock_logger: The mocked logger instance.
"""
room_id = 1
mock_scalars_result = AsyncMock()
mock_scalars_result.all = MagicMock(return_value=sample_bookings)
mock_scalars = AsyncMock(return_value=mock_scalars_result)
async_session.scalars = mock_scalars # type: ignore [method-assign]
result: BookingList = await get_bookings_for_room(async_session, room_id)
assert isinstance(result, list)
assert len(result) == 2
assert result == sample_bookings
async_session.scalars.assert_called_once()
assert async_session.scalars.call_args.args[0].compare(
select(Booking).where(Booking.room_id == room_id)
)
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
async def test_get_bookings_for_room_empty(
async_session: AsyncSession, mock_logger: MagicMock
) -> None:
"""Test retrieval of bookings when none exist for the room.
Verifies that get_bookings_for_room returns an empty list when no bookings are found.
Args:
async_session: The asynchronous database session.
mock_logger: The mocked logger instance.
"""
room_id = 1
mock_scalars_result = AsyncMock()
mock_scalars_result.all = MagicMock(return_value=[])
mock_scalars = AsyncMock(return_value=mock_scalars_result)
async_session.scalars = mock_scalars # type: ignore [method-assign]
result: BookingList = await get_bookings_for_room(async_session, room_id)
assert isinstance(result, list)
assert len(result) == 0
async_session.scalars.assert_called_once()
assert async_session.scalars.call_args.args[0].compare(
select(Booking).where(Booking.room_id == room_id)
)
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
async def test_get_bookings_for_room_database_error(
async_session: AsyncSession, mock_logger: MagicMock
) -> None:
"""Test handling of database errors in get_bookings_for_room.
Verifies that get_bookings_for_room raises an exception on database failure.
Args:
async_session: The asynchronous database session.
mock_logger: The mocked logger instance.
"""
room_id = 1
async_session.scalars = AsyncMock( # type: ignore [method-assign]
side_effect=SQLAlchemyError("Database error")
)
with pytest.raises(SQLAlchemyError):
await get_bookings_for_room(async_session, room_id)
async_session.scalars.assert_called_once()
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
@pytest.mark.parametrize(
"booking_id, expected_room_id",
[
(1, 1),
(2, 2),
],
ids=["booking1", "booking2"],
)
async def test_get_booking_success(
async_session: AsyncSession,
booking_id: int,
expected_room_id: int,
mock_logger: MagicMock,
) -> None:
"""Test successful retrieval of a booking by ID.
Verifies that get_booking returns the correct booking and constructs the correct query.
Args:
async_session: The asynchronous database session.
booking_id: The ID of the booking to retrieve.
expected_room_id: The expected room ID of the booking.
mock_logger: The mocked logger instance.
"""
booking = Booking(
room_id=expected_room_id,
start_time=datetime(2025, 8, 24, 10, 0),
end_time=datetime(2025, 8, 24, 12, 0),
)
booking.id = booking_id
async_session.scalar = AsyncMock(return_value=booking) # type: ignore [method-assign]
result: Booking = await get_booking(async_session, booking_id)
assert result.id == booking_id
assert result.room_id == expected_room_id
async_session.scalar.assert_called_once()
assert async_session.scalar.call_args.args[0].compare(
select(Booking).where(Booking.id == booking_id)
)
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
@pytest.mark.parametrize(
"booking_id",
[999, -1],
ids=["nonexistent_id", "invalid_id"],
)
async def test_get_booking_not_found(
async_session: AsyncSession, mock_logger: MagicMock, booking_id: int
) -> None:
"""Test handling of non-existent booking in get_booking.
Verifies that get_booking raises NoResultFound when the booking is not found.
Args:
async_session: The asynchronous database session.
mock_logger: The mocked logger instance.
booking_id: The ID of the booking to retrieve.
"""
async_session.scalar = AsyncMock(return_value=None) # type: ignore [method-assign]
with pytest.raises(NoResultFound):
await get_booking(async_session, booking_id)
async_session.scalar.assert_called_once()
assert async_session.scalar.call_args.args[0].compare(
select(Booking).where(Booking.id == booking_id)
)
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
async def test_get_booking_database_error(
async_session: AsyncSession, mock_logger: MagicMock
) -> None:
"""Test handling of database errors in get_booking.
Verifies that get_booking raises an exception on database failure.
Args:
async_session: The asynchronous database session.
mock_logger: The mocked logger instance.
"""
booking_id = 1
async_session.scalar = AsyncMock( # type: ignore [method-assign]
side_effect=SQLAlchemyError("Database error")
)
with pytest.raises(SQLAlchemyError):
await get_booking(async_session, booking_id)
async_session.scalar.assert_called_once()
assert async_session.scalar.call_args.args[0].compare(
select(Booking).where(Booking.id == booking_id)
)
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
async def test_new_booking_success(
async_session: AsyncSession, mock_logger: MagicMock
) -> None:
"""Test successful creation of a new booking.
Verifies that new_booking adds the booking, commits the session, and returns the booking.
Args:
async_session: The asynchronous database session.
mock_logger: The mocked logger instance.
"""
booking = Booking(
room_id=1,
start_time=datetime(2025, 8, 24, 10, 0),
end_time=datetime(2025, 8, 24, 12, 0),
)
booking.id = 1
async_session.add = MagicMock() # type: ignore [method-assign]
async_session.commit = AsyncMock() # type: ignore [method-assign]
result: Booking = await new_booking(async_session, booking)
assert result == booking
async_session.add.assert_called_once_with(booking)
async_session.commit.assert_called_once()
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
async def test_new_booking_database_error(
async_session: AsyncSession, mock_logger: MagicMock
) -> None:
"""Test handling of database errors in new_booking.
Verifies that new_booking rolls back the session on database failure.
Args:
async_session: The asynchronous database session.
mock_logger: The mocked logger instance.
"""
booking = Booking(
room_id=1,
start_time=datetime(2025, 8, 24, 10, 0),
end_time=datetime(2025, 8, 24, 12, 0),
)
booking.id = 1
async_session.add = MagicMock() # type: ignore [method-assign]
async_session.commit = AsyncMock( # type: ignore [method-assign]
side_effect=SQLAlchemyError("Database error")
)
async_session.rollback = AsyncMock() # type: ignore [method-assign]
with pytest.raises(SQLAlchemyError):
await new_booking(async_session, booking)
async_session.add.assert_called_once_with(booking)
async_session.commit.assert_called_once()
async_session.rollback.assert_called_once()
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
async def test_update_booking_success(
async_session: AsyncSession, mock_logger: MagicMock
) -> None:
"""Test successful update of a booking.
Verifies that update_booking updates the booking, commits the session, and
returns the updated booking.
Args:
async_session: The asynchronous database session.
mock_logger: The mocked logger instance.
"""
booking_id = 1
update_params: dict[str, Any] = {
"room_id": 2,
"start_time": datetime(2025, 8, 24, 13, 0),
"end_time": datetime(2025, 8, 24, 15, 0),
}
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]
updated_booking = Booking(**update_params)
updated_booking.id = booking_id
async_session.scalar = AsyncMock( # type: ignore [method-assign]
return_value=updated_booking
)
result: Booking = await update_booking(async_session, booking_id, **update_params)
assert result == updated_booking
async_session.execute.assert_called_once()
assert async_session.execute.call_args.args[0].compare(
update(Booking).where(Booking.id == booking_id).values(**update_params)
)
async_session.commit.assert_called_once()
async_session.scalar.assert_called_once()
assert async_session.scalar.call_args.args[0].compare(
select(Booking).where(Booking.id == booking_id)
)
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
async def test_update_booking_not_found(
async_session: AsyncSession, mock_logger: MagicMock
) -> None:
"""Test handling of non-existent booking in update_booking.
Verifies that update_booking raises ValueError when the booking is not found.
Args:
async_session: The asynchronous database session.
mock_logger: The mocked logger instance.
"""
booking_id = 999
update_params: dict[str, Any] = {
"room_id": 2,
"start_time": datetime(2025, 8, 24, 13, 0),
"end_time": datetime(2025, 8, 24, 15, 0),
}
mock_execute_result = MagicMock(rowcount=0)
async_session.execute = AsyncMock( # type: ignore [method-assign]
return_value=mock_execute_result
)
async_session.rollback = AsyncMock() # type: ignore [method-assign]
with pytest.raises(ValueError):
await update_booking(async_session, booking_id, **update_params)
async_session.execute.assert_called_once()
assert async_session.execute.call_args.args[0].compare(
update(Booking).where(Booking.id == booking_id).values(**update_params)
)
async_session.rollback.assert_called_once()
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
async def test_update_booking_database_error(
async_session: AsyncSession, mock_logger: MagicMock
) -> None:
"""Test handling of database errors in update_booking.
Verifies that update_booking rolls back the session on database failure.
Args:
async_session: The asynchronous database session.
mock_logger: The mocked logger instance.
"""
booking_id = 1
update_params: dict[str, Any] = {
"room_id": 2,
"start_time": datetime(2025, 8, 24, 13, 0),
"end_time": datetime(2025, 8, 24, 15, 0),
}
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 update_booking(async_session, booking_id, **update_params)
async_session.execute.assert_called_once()
assert async_session.execute.call_args.args[0].compare(
update(Booking).where(Booking.id == booking_id).values(**update_params)
)
async_session.rollback.assert_called_once()
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
async def test_delete_booking_success(
async_session: AsyncSession, mock_logger: MagicMock
) -> None:
"""Test successful deletion of a booking.
Verifies that delete_booking deletes the booking and commits the session.
Args:
async_session: The asynchronous database session.
mock_logger: The mocked logger instance.
"""
booking_id = 1
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 delete_booking(async_session, booking_id)
async_session.execute.assert_called_once()
assert async_session.execute.call_args.args[0].compare(
delete(Booking).where(Booking.id == booking_id)
)
async_session.commit.assert_called_once()
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
async def test_delete_booking_not_found(
async_session: AsyncSession, mock_logger: MagicMock
) -> None:
"""Test handling of non-existent booking in delete_booking.
Verifies that delete_booking raises ValueError when the booking is not found.
Args:
async_session: The asynchronous database session.
mock_logger: The mocked logger instance.
"""
booking_id = 999
mock_execute_result = MagicMock(rowcount=0)
async_session.execute = AsyncMock( # type: ignore [method-assign]
return_value=mock_execute_result
)
async_session.rollback = AsyncMock() # type: ignore [method-assign]
with pytest.raises(ValueError):
await delete_booking(async_session, booking_id)
async_session.execute.assert_called_once()
assert async_session.execute.call_args.args[0].compare(
delete(Booking).where(Booking.id == booking_id)
)
async_session.rollback.assert_called_once()
@pytest.mark.asyncio
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
async def test_delete_booking_database_error(
async_session: AsyncSession, mock_logger: MagicMock
) -> None:
"""Test handling of database errors in delete_booking.
Verifies that delete_booking rolls back the session on database failure.
Args:
async_session: The asynchronous database session.
mock_logger: The mocked logger instance.
"""
booking_id = 1
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 delete_booking(async_session, booking_id)
async_session.execute.assert_called_once()
assert async_session.execute.call_args.args[0].compare(
delete(Booking).where(Booking.id == booking_id)
)
async_session.rollback.assert_called_once()