Added tests for bookings router.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2025-08-25 20:21:40 -04:00
parent ddc80e361c
commit 519974a07f
4 changed files with 430 additions and 13 deletions

View File

@@ -98,13 +98,13 @@ async def create_booking(booking: BookingCreate, session: DBSession):
except SQLAlchemyError as e:
logger.error(f"Database error while creating booking: {str(e)}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Database error occurred",
) from e
except Exception as e:
logger.error(f"Unexpected error while creating booking: {str(e)}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="An unexpected error occurred",
) from e
@@ -116,9 +116,7 @@ async def update_existing_booking(
"""Endpoint to update an existing booking."""
logger.debug(f"Received request to update booking with id: {booking_id}")
try:
booking_params = {
k: v for k, v in booking_update.model_dump(exclude_unset=True).items()
}
booking_params = booking_update.model_dump(exclude_unset=True)
updated_booking = await update_booking(session, booking_id, **booking_params)
logger.info(f"Successfully updated booking with id: {booking_id}")
return updated_booking
@@ -133,7 +131,7 @@ async def update_existing_booking(
f"Database error while updating booking with id {booking_id}: {str(e)}"
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Database error occurred",
) from e
except Exception as e:
@@ -141,7 +139,7 @@ async def update_existing_booking(
f"Unexpected error while updating booking with id {booking_id}: {str(e)}"
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="An unexpected error occurred",
) from e

View File

@@ -107,9 +107,7 @@ async def update_existing_room(
"""Endpoint to update an existing room."""
logger.debug(f"Received request to update room with id: {room_id}")
try:
room_params = {
k: v for k, v in room_update.model_dump(exclude_unset=True).items()
}
room_params = room_update.model_dump(exclude_unset=True)
updated_room = await update_room(session, room_id, **room_params)
logger.info(f"Successfully updated room with id: {room_id}")
return updated_room

View File

@@ -5,8 +5,6 @@ from datetime import datetime
from pydantic import BaseModel
from pydantic import ConfigDict
from backend.schemas.rooms import RoomResponse
class BookingBase(BaseModel):
"""Base schema for Booking."""
@@ -25,6 +23,7 @@ class BookingCreate(BookingBase):
class BookingUpdate(BaseModel):
"""Schema for updating an existing Booking."""
id: int | None = None
room_id: int | None = None
start_time: datetime | None = None
end_time: datetime | None = None
@@ -36,6 +35,5 @@ class BookingResponse(BookingBase):
"""Response schema for Booking, including ID and related Room."""
id: int
room: RoomResponse
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,423 @@
"""Unit tests for the backend.routers.bookings module."""
import logging
from collections.abc import Generator
from datetime import datetime
from typing import Any
from unittest.mock import ANY
from unittest.mock import AsyncMock
from unittest.mock import MagicMock
from unittest.mock import patch
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.exc import NoResultFound
from sqlalchemy.exc import SQLAlchemyError
from backend.models import Booking
from backend.models import Room
from backend.services.bookings import BookingList
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.routers.bookings.logger", mock_logger_instance):
yield mock_logger_instance
@pytest.fixture
def sample_room() -> Room:
"""Fixture to provide a sample Room object for testing."""
return Room(
id=1,
name="Test Room",
location="Building A",
equipment="Projector, Whiteboard",
capacity=10,
)
@pytest.fixture
def sample_bookings(sample_room: Room) -> BookingList:
"""Fixture to provide sample Booking objects for testing."""
booking1 = Booking(
room_id=1,
start_time=datetime.fromisoformat("2025-08-25T10:00:00"),
end_time=datetime.fromisoformat("2025-08-25T11:00:00"),
)
booking1.id = 1 # Manually set ID for testing purposes
booking1.room = sample_room # Attach room relationship
booking2 = Booking(
room_id=1,
start_time=datetime.fromisoformat("2025-08-25T12:00:00"),
end_time=datetime.fromisoformat("2025-08-25T13:00:00"),
)
booking2.id = 2 # Manually set ID for testing purposes
booking2.room = sample_room # Attach room relationship
return [booking1, booking2]
@pytest.mark.asyncio
async def test_get_bookings_for_room_success(
client: TestClient, sample_bookings: BookingList, mock_logger: MockLogger
) -> None:
"""Test successful retrieval of all bookings for a room via GET /bookings/room/{room_id}.
Verifies that the endpoint returns the expected list of bookings with a 200 status.
"""
room_id = 1
with patch(
"backend.routers.bookings.get_bookings_for_room", new=AsyncMock()
) as mock_get_bookings:
mock_get_bookings.return_value = sample_bookings
response = client.get(f"/bookings/room/{room_id}")
assert response.status_code == 200
assert response.json() == [
{
"id": 1,
"room_id": 1,
"start_time": "2025-08-25T10:00:00",
"end_time": "2025-08-25T11:00:00",
},
{
"id": 2,
"room_id": 1,
"start_time": "2025-08-25T12:00:00",
"end_time": "2025-08-25T13:00:00",
},
]
mock_get_bookings.assert_called_once_with(ANY, room_id)
@pytest.mark.asyncio
async def test_get_bookings_for_room_empty(
client: TestClient, mock_logger: MockLogger
) -> None:
"""Test retrieval of bookings when none exist for a room via GET /bookings/room/{room_id}.
Verifies that the endpoint returns an empty list with a 200 status.
"""
room_id = 1
with patch(
"backend.routers.bookings.get_bookings_for_room", new=AsyncMock()
) as mock_get_bookings:
mock_get_bookings.return_value = []
response = client.get(f"/bookings/room/{room_id}")
assert response.status_code == 200
assert response.json() == []
mock_get_bookings.assert_called_once_with(ANY, room_id)
@pytest.mark.asyncio
async def test_get_bookings_for_room_database_error(
client: TestClient, mock_logger: MockLogger
) -> None:
"""Test handling of database errors in GET /bookings/room/{room_id}.
Verifies that the endpoint returns a 500 status on database failure.
"""
room_id = 1
with patch(
"backend.routers.bookings.get_bookings_for_room", new=AsyncMock()
) as mock_get_bookings:
mock_get_bookings.side_effect = SQLAlchemyError()
response = client.get(f"/bookings/room/{room_id}")
assert response.status_code == 500
assert "detail" in response.json()
mock_get_bookings.assert_called_once_with(ANY, room_id)
@pytest.mark.asyncio
async def test_get_booking_success(
client: TestClient, sample_room: Room, mock_logger: MockLogger
) -> None:
"""Test successful retrieval of a booking by ID via GET /bookings/{booking_id}.
Verifies that the endpoint returns the correct booking with a 200 status.
"""
booking_id = 1
booking = Booking(
room_id=1,
start_time=datetime.fromisoformat("2025-08-25T10:00:00"),
end_time=datetime.fromisoformat("2025-08-25T11:00:00"),
)
booking.id = booking_id
booking.room = sample_room # Attach room relationship
with patch(
"backend.routers.bookings.get_booking", new=AsyncMock()
) as mock_get_booking:
mock_get_booking.return_value = booking
response = client.get(f"/bookings/{booking_id}")
assert response.status_code == 200
assert response.json() == {
"id": booking_id,
"room_id": 1,
"start_time": "2025-08-25T10:00:00",
"end_time": "2025-08-25T11:00:00",
}
mock_get_booking.assert_called_once_with(ANY, booking_id)
@pytest.mark.asyncio
async def test_get_booking_not_found(
client: TestClient, mock_logger: MockLogger
) -> None:
"""Test handling of non-existent booking in GET /bookings/{booking_id}.
Verifies that the endpoint returns a 404 status when the booking is not found.
"""
booking_id = 999
with patch(
"backend.routers.bookings.get_booking", new=AsyncMock()
) as mock_get_booking:
mock_get_booking.side_effect = NoResultFound()
response = client.get(f"/bookings/{booking_id}")
assert response.status_code == 404
assert "detail" in response.json()
mock_get_booking.assert_called_once_with(ANY, booking_id)
@pytest.mark.asyncio
async def test_get_booking_database_error(
client: TestClient, mock_logger: MockLogger
) -> None:
"""Test handling of database errors in GET /bookings/{booking_id}.
Verifies that the endpoint returns a 500 status on database failure.
"""
booking_id = 1
with patch(
"backend.routers.bookings.get_booking", new=AsyncMock()
) as mock_get_booking:
mock_get_booking.side_effect = SQLAlchemyError()
response = client.get(f"/bookings/{booking_id}")
assert response.status_code == 500
assert "detail" in response.json()
mock_get_booking.assert_called_once_with(ANY, booking_id)
@pytest.mark.asyncio
async def test_create_booking_success(
client: TestClient, sample_room: Room, mock_logger: MockLogger
) -> None:
"""Test successful creation of a new booking via POST /bookings/.
Verifies that the endpoint returns the created booking with a 201 status.
"""
booking_data: dict[str, Any] = {
"room_id": 1,
"start_time": "2025-08-25T10:00:00",
"end_time": "2025-08-25T11:00:00",
}
created_booking = Booking(**booking_data)
created_booking.id = 3
created_booking.room = sample_room # Attach room relationship
with patch(
"backend.routers.bookings.new_booking", new=AsyncMock()
) as mock_new_booking:
mock_new_booking.return_value = created_booking
response = client.post("/bookings/", json=booking_data)
assert response.status_code == 201
assert response.json() == {
"id": 3,
"room_id": 1,
"start_time": "2025-08-25T10:00:00",
"end_time": "2025-08-25T11:00:00",
}
mock_new_booking.assert_called_once()
@pytest.mark.asyncio
async def test_create_booking_database_error(
client: TestClient, mock_logger: MockLogger
) -> None:
"""Test handling of database errors in POST /bookings/.
Verifies that the endpoint returns a 400 status on database failure.
"""
booking_data: dict[str, Any] = {
"room_id": 1,
"start_time": "2025-08-25T10:00:00",
"end_time": "2025-08-25T11:00:00",
}
with patch(
"backend.routers.bookings.new_booking", new=AsyncMock()
) as mock_new_booking:
mock_new_booking.side_effect = SQLAlchemyError()
response = client.post("/bookings/", json=booking_data)
assert response.status_code == 500
assert "detail" in response.json()
mock_new_booking.assert_called_once()
@pytest.mark.asyncio
async def test_update_booking_success(
client: TestClient, sample_room: Room, mock_logger: MockLogger
) -> None:
"""Test successful update of a booking via PUT /bookings/{booking_id}.
Verifies that the endpoint returns the updated booking with a 200 status.
"""
booking_id = 1
update_data: dict[str, Any] = {
"room_id": 2,
"start_time": "2025-08-25T14:00:00",
"end_time": "2025-08-25T15:00:00",
}
updated_booking = Booking(**update_data)
updated_booking.id = booking_id
updated_booking.room = sample_room # Attach room relationship
with patch(
"backend.routers.bookings.update_booking", new=AsyncMock()
) as mock_update_booking:
mock_update_booking.return_value = updated_booking
response = client.put(f"/bookings/{booking_id}", json=update_data)
assert response.status_code == 200
assert response.json() == {
"id": booking_id,
"room_id": 2,
"start_time": "2025-08-25T14:00:00",
"end_time": "2025-08-25T15:00:00",
}
update_data["start_time"] = datetime.fromisoformat(update_data["start_time"])
update_data["end_time"] = datetime.fromisoformat(update_data["end_time"])
mock_update_booking.assert_called_once_with(ANY, booking_id, **update_data)
@pytest.mark.asyncio
async def test_update_booking_not_found(
client: TestClient, mock_logger: MockLogger
) -> None:
"""Test handling of non-existent booking in PUT /bookings/{booking_id}.
Verifies that the endpoint returns a 404 status when the booking is not found.
"""
booking_id = 999
update_data: dict[str, Any] = {
"room_id": 2,
"start_time": "2025-08-25T14:00:00",
"end_time": "2025-08-25T15:00:00",
}
with patch(
"backend.routers.bookings.update_booking", new=AsyncMock()
) as mock_update_booking:
mock_update_booking.side_effect = NoResultFound()
response = client.put(f"/bookings/{booking_id}", json=update_data)
assert response.status_code == 404
assert "detail" in response.json()
update_data["start_time"] = datetime.fromisoformat(update_data["start_time"])
update_data["end_time"] = datetime.fromisoformat(update_data["end_time"])
mock_update_booking.assert_called_once_with(ANY, booking_id, **update_data)
@pytest.mark.asyncio
async def test_update_booking_database_error(
client: TestClient, mock_logger: MockLogger
) -> None:
"""Test handling of database errors in PUT /bookings/{booking_id}.
Verifies that the endpoint returns a 500 status on database failure.
"""
booking_id = 1
update_data: dict[str, Any] = {
"room_id": 2,
"start_time": "2025-08-25T14:00:00",
"end_time": "2025-08-25T15:00:00",
}
with patch(
"backend.routers.bookings.update_booking", new=AsyncMock()
) as mock_update_booking:
mock_update_booking.side_effect = SQLAlchemyError()
response = client.put(f"/bookings/{booking_id}", json=update_data)
assert response.status_code == 500
assert "detail" in response.json()
update_data["start_time"] = datetime.fromisoformat(update_data["start_time"])
update_data["end_time"] = datetime.fromisoformat(update_data["end_time"])
mock_update_booking.assert_called_once_with(ANY, booking_id, **update_data)
@pytest.mark.asyncio
async def test_delete_booking_success(
client: TestClient, mock_logger: MockLogger
) -> None:
"""Test successful deletion of a booking via DELETE /bookings/{booking_id}.
Verifies that the endpoint returns a 204 status on successful deletion.
"""
booking_id = 1
with patch(
"backend.routers.bookings.delete_booking", new=AsyncMock()
) as mock_delete_booking:
response = client.delete(f"/bookings/{booking_id}")
assert response.status_code == 204
assert response.text == ""
mock_delete_booking.assert_called_once_with(ANY, booking_id)
@pytest.mark.asyncio
async def test_delete_booking_not_found(
client: TestClient, mock_logger: MockLogger
) -> None:
"""Test handling of non-existent booking in DELETE /bookings/{booking_id}.
Verifies that the endpoint returns a 404 status when the booking is not found.
"""
booking_id = 999
with patch(
"backend.routers.bookings.delete_booking", new=AsyncMock()
) as mock_delete_booking:
mock_delete_booking.side_effect = NoResultFound()
response = client.delete(f"/bookings/{booking_id}")
assert response.status_code == 404
assert "detail" in response.json()
mock_delete_booking.assert_called_once_with(ANY, booking_id)
@pytest.mark.asyncio
async def test_delete_booking_database_error(
client: TestClient, mock_logger: MockLogger
) -> None:
"""Test handling of database errors in DELETE /bookings/{booking_id}.
Verifies that the endpoint returns a 500 status on database failure.
"""
booking_id = 1
with patch(
"backend.routers.bookings.delete_booking", new=AsyncMock()
) as mock_delete_booking:
mock_delete_booking.side_effect = SQLAlchemyError()
response = client.delete(f"/bookings/{booking_id}")
assert response.status_code == 500
assert "detail" in response.json()
mock_delete_booking.assert_called_once_with(ANY, booking_id)