mirror of
https://github.com/xlorepdarkhelm/numinar-coding-project.git
synced 2026-08-24 00:06:26 -04:00
@@ -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
|
||||
|
||||
401
backend/tests/services/test_bookings.py
Normal file
401
backend/tests/services/test_bookings.py
Normal 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()
|
||||
Reference in New Issue
Block a user