Added bookings tests.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2025-08-24 16:23:05 -04:00
parent ce49cd9a1e
commit f3a50d343f
2 changed files with 404 additions and 4 deletions

View File

@@ -48,11 +48,10 @@ async def get_booking(session: AsyncSession, booking_id: int) -> Booking:
try:
stmt = select(Booking).where(Booking.id == booking_id)
booking = await session.scalar(stmt)
if booking is None:
raise NoResultFound(f"Booking with id {booking_id} not found")
logger.info(f"Successfully retrieved booking with id: {booking_id}")
return cast(Booking, booking)
except NoResultFound:
logger.error(f"Booking with id {booking_id} not found")
raise
return booking
except Exception as e:
logger.error(f"Failed to retrieve booking with id {booking_id}: {str(e)}")
raise

View File

@@ -0,0 +1,401 @@
"""Unit tests for the backend.services.bookings module."""
import logging
from collections.abc import Generator
from datetime import datetime
from typing import Any
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 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.services.bookings 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
from ..conftest import MockLogger
@pytest.fixture(autouse=True)
def mock_logger() -> Generator[MockLogger, None, None]:
"""Fixture to mock the logger used in the bookings service."""
mock_logger_instance = MagicMock(spec=logging.Logger)
with patch("backend.services.bookings.logger", mock_logger_instance):
yield mock_logger_instance
@pytest.fixture
def sample_bookings() -> BookingList:
"""Fixture to provide sample Booking objects for testing."""
booking1 = Booking(
room_id=1,
start_time=datetime(2025, 8, 24, 10, 0),
end_time=datetime(2025, 8, 24, 12, 0),
)
booking1.id = 1
booking2 = Booking(
room_id=1,
start_time=datetime(2025, 8, 24, 13, 0),
end_time=datetime(2025, 8, 24, 15, 0),
)
booking2.id = 2
return [booking1, booking2]
@pytest.mark.asyncio
async def test_get_bookings_for_room_success(
async_session: AsyncSession, sample_bookings: BookingList, mock_logger: MockLogger
) -> 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.
"""
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
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
async def test_get_bookings_for_room_empty(
async_session: AsyncSession, mock_logger: MockLogger
) -> 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.
"""
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
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
async def test_get_bookings_for_room_database_error(
async_session: AsyncSession, mock_logger: MockLogger
) -> None:
"""Test handling of database errors in get_bookings_for_room.
Verifies that get_bookings_for_room raises an exception on database failure.
"""
room_id = 1
async_session.scalars = AsyncMock(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(
"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: MockLogger,
) -> None:
"""Test successful retrieval of a booking by ID.
Verifies that get_booking returns the correct booking and constructs the correct query.
"""
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)
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(
"booking_id",
[999, -1],
ids=["nonexistent_id", "invalid_id"],
)
async def test_get_booking_not_found(
async_session: AsyncSession, mock_logger: MockLogger, 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.
"""
async_session.scalar = AsyncMock(return_value=None)
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
async def test_get_booking_database_error(
async_session: AsyncSession, mock_logger: MockLogger
) -> None:
"""Test handling of database errors in get_booking.
Verifies that get_booking raises an exception on database failure.
"""
booking_id = 1
async_session.scalar = AsyncMock(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
async def test_new_booking_success(
async_session: AsyncSession, mock_logger: MockLogger
) -> None:
"""Test successful creation of a new booking.
Verifies that new_booking adds the booking, commits the session, and returns the booking.
"""
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()
async_session.commit = AsyncMock()
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
async def test_new_booking_database_error(
async_session: AsyncSession, mock_logger: MockLogger
) -> None:
"""Test handling of database errors in new_booking.
Verifies that new_booking rolls back the session on database failure.
"""
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()
async_session.commit = AsyncMock(side_effect=SQLAlchemyError("Database error"))
async_session.rollback = AsyncMock()
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
async def test_update_booking_success(
async_session: AsyncSession, mock_logger: MockLogger
) -> None:
"""Test successful update of a booking.
Verifies that update_booking updates the booking, commits the session, and
returns the updated booking.
"""
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(return_value=mock_execute_result)
async_session.commit = AsyncMock()
updated_booking = Booking(**update_params)
updated_booking.id = booking_id
async_session.scalar = AsyncMock(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
async def test_update_booking_not_found(
async_session: AsyncSession, mock_logger: MockLogger
) -> None:
"""Test handling of non-existent booking in update_booking.
Verifies that update_booking raises ValueError when the booking is not found.
"""
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(return_value=mock_execute_result)
async_session.rollback = AsyncMock()
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
async def test_update_booking_database_error(
async_session: AsyncSession, mock_logger: MockLogger
) -> None:
"""Test handling of database errors in update_booking.
Verifies that update_booking rolls back the session on database failure.
"""
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(side_effect=SQLAlchemyError("Database error"))
async_session.rollback = AsyncMock()
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
async def test_delete_booking_success(
async_session: AsyncSession, mock_logger: MockLogger
) -> None:
"""Test successful deletion of a booking.
Verifies that delete_booking deletes the booking and commits the session.
"""
booking_id = 1
mock_execute_result = MagicMock(rowcount=1)
async_session.execute = AsyncMock(return_value=mock_execute_result)
async_session.commit = AsyncMock()
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
async def test_delete_booking_not_found(
async_session: AsyncSession, mock_logger: MockLogger
) -> None:
"""Test handling of non-existent booking in delete_booking.
Verifies that delete_booking raises ValueError when the booking is not found.
"""
booking_id = 999
mock_execute_result = MagicMock(rowcount=0)
async_session.execute = AsyncMock(return_value=mock_execute_result)
async_session.rollback = AsyncMock()
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
async def test_delete_booking_database_error(
async_session: AsyncSession, mock_logger: MockLogger
) -> None:
"""Test handling of database errors in delete_booking.
Verifies that delete_booking rolls back the session on database failure.
"""
booking_id = 1
async_session.execute = AsyncMock(side_effect=SQLAlchemyError("Database error"))
async_session.rollback = AsyncMock()
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()