mirror of
https://github.com/xlorepdarkhelm/numinar-coding-project.git
synced 2026-09-09 17:45:35 -04:00
Added tests for bookings router.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
423
backend/tests/routers/test_bookings.py
Normal file
423
backend/tests/routers/test_bookings.py
Normal 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)
|
||||
Reference in New Issue
Block a user