mirror of
https://github.com/xlorepdarkhelm/numinar-coding-project.git
synced 2026-09-06 23:48:26 -04:00
495 lines
17 KiB
Python
495 lines
17 KiB
Python
"""Unit tests for the backend.routers.bookings module."""
|
|
|
|
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 httpx import AsyncClient
|
|
from sqlalchemy.exc import NoResultFound
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
|
|
from backend.models import Booking
|
|
from backend.models import BookingList
|
|
from backend.models import Room
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
|
async def test_get_bookings_for_room_success(
|
|
client: AsyncClient, sample_bookings: BookingList, mock_logger: MagicMock
|
|
) -> 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.
|
|
|
|
Args:
|
|
client: The FastAPI test client for making HTTP requests.
|
|
sample_bookings: The mocked list of bookings to return.
|
|
mock_logger: The mocked logger instance.
|
|
"""
|
|
room_id = 1
|
|
with (
|
|
patch(
|
|
"backend.routers.bookings.get_bookings_for_room", new=AsyncMock()
|
|
) as mock_get_bookings,
|
|
patch(
|
|
"backend.routers.bookings.get_invitees_for_booking", new=AsyncMock()
|
|
) as mock_get_invitees,
|
|
patch("backend.routers.bookings.get_room", new=AsyncMock()) as mock_get_room,
|
|
):
|
|
mock_get_bookings.return_value = sample_bookings
|
|
mock_get_invitees.return_value = []
|
|
mock_get_room.return_value = None
|
|
|
|
response = await client.get(f"/bookings/room/{room_id}")
|
|
|
|
assert response.status_code == 200
|
|
actual = response.json()
|
|
expected: list[dict[str, Any]] = [
|
|
{
|
|
"id": booking.id,
|
|
"room_id": booking.room_id,
|
|
"start_time": booking.start_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
|
"end_time": booking.end_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
|
"title": booking.title,
|
|
"invitees": [],
|
|
}
|
|
for booking in sample_bookings
|
|
]
|
|
# Remove 'room' key if present
|
|
for item in actual:
|
|
item.pop("room", None)
|
|
assert actual == expected
|
|
mock_get_bookings.assert_called_once_with(ANY, room_id, None)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
|
async def test_get_bookings_for_room_empty(
|
|
client: AsyncClient, mock_logger: MagicMock
|
|
) -> 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.
|
|
|
|
Args:
|
|
client: The FastAPI test client for making HTTP requests.
|
|
mock_logger: The mocked logger instance.
|
|
"""
|
|
room_id = 1
|
|
with patch(
|
|
"backend.routers.bookings.get_bookings_for_room", new=AsyncMock()
|
|
) as mock_get_bookings:
|
|
mock_get_bookings.return_value = []
|
|
|
|
response = await 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, None)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
|
async def test_get_bookings_for_room_database_error(
|
|
client: AsyncClient, mock_logger: MagicMock
|
|
) -> None:
|
|
"""Test handling of database errors in GET /bookings/room/{room_id}.
|
|
|
|
Verifies that the endpoint returns a 500 status on database failure.
|
|
|
|
Args:
|
|
client: The FastAPI test client for making HTTP requests.
|
|
mock_logger: The mocked logger instance.
|
|
"""
|
|
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 = await 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, None)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
|
async def test_get_booking_success(
|
|
client: AsyncClient, sample_room: Room, mock_logger: MagicMock
|
|
) -> 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.
|
|
|
|
Args:
|
|
client: The FastAPI test client for making HTTP requests.
|
|
sample_room: The mocked room object.
|
|
mock_logger: The mocked logger instance.
|
|
"""
|
|
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,
|
|
patch(
|
|
"backend.routers.bookings.get_invitees_for_booking", new=AsyncMock()
|
|
) as mock_get_invitees,
|
|
patch("backend.routers.bookings.get_room", new=AsyncMock()) as mock_get_room,
|
|
):
|
|
mock_get_booking.return_value = booking
|
|
mock_get_invitees.return_value = []
|
|
mock_get_room.return_value = sample_room
|
|
|
|
response = await client.get(f"/bookings/{booking_id}")
|
|
|
|
assert response.status_code == 200
|
|
actual = response.json()
|
|
actual.pop("room", None)
|
|
assert actual == {
|
|
"id": booking_id,
|
|
"room_id": 1,
|
|
"start_time": "2025-08-25T10:00:00",
|
|
"end_time": "2025-08-25T11:00:00",
|
|
"title": None,
|
|
"invitees": [],
|
|
}
|
|
mock_get_booking.assert_called_once_with(ANY, booking_id)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
|
async def test_get_booking_not_found(
|
|
client: AsyncClient, mock_logger: MagicMock
|
|
) -> 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.
|
|
|
|
Args:
|
|
client: The FastAPI test client for making HTTP requests.
|
|
mock_logger: The mocked logger instance.
|
|
"""
|
|
booking_id = 999
|
|
with patch(
|
|
"backend.routers.bookings.get_booking", new=AsyncMock()
|
|
) as mock_get_booking:
|
|
mock_get_booking.side_effect = NoResultFound()
|
|
|
|
response = await 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
|
|
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
|
async def test_get_booking_database_error(
|
|
client: AsyncClient, mock_logger: MagicMock
|
|
) -> None:
|
|
"""Test handling of database errors in GET /bookings/{booking_id}.
|
|
|
|
Verifies that the endpoint returns a 500 status on database failure.
|
|
|
|
Args:
|
|
client: The FastAPI test client for making HTTP requests.
|
|
mock_logger: The mocked logger instance.
|
|
"""
|
|
booking_id = 1
|
|
with patch(
|
|
"backend.routers.bookings.get_booking", new=AsyncMock()
|
|
) as mock_get_booking:
|
|
mock_get_booking.side_effect = SQLAlchemyError()
|
|
|
|
response = await 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
|
|
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
|
async def test_create_booking_success(
|
|
client: AsyncClient, sample_room: Room, mock_logger: MagicMock
|
|
) -> None:
|
|
"""Test successful creation of a new booking via POST /bookings/.
|
|
|
|
Verifies that the endpoint returns the created booking with a 201 status.
|
|
|
|
Args:
|
|
client: The FastAPI test client for making HTTP requests.
|
|
sample_room: The mocked room object.
|
|
mock_logger: The mocked logger instance.
|
|
"""
|
|
booking_data: dict[str, Any] = {
|
|
"room_id": 1,
|
|
"start_time": "2025-08-25T10:00:00",
|
|
"end_time": "2025-08-25T11:00:00",
|
|
"title": "Test Booking Title",
|
|
}
|
|
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,
|
|
patch(
|
|
"backend.routers.bookings.get_invitees_for_booking", new=AsyncMock()
|
|
) as mock_get_invitees,
|
|
patch("backend.routers.bookings.get_room", new=AsyncMock()) as mock_get_room,
|
|
):
|
|
mock_new_booking.return_value = created_booking
|
|
mock_get_invitees.return_value = []
|
|
mock_get_room.return_value = sample_room
|
|
|
|
response = await client.post("/bookings/", json=booking_data)
|
|
|
|
assert response.status_code == 201
|
|
actual = response.json()
|
|
actual.pop("room", None)
|
|
assert actual == {
|
|
"id": 3,
|
|
"room_id": 1,
|
|
"start_time": "2025-08-25T10:00:00",
|
|
"end_time": "2025-08-25T11:00:00",
|
|
"title": "Test Booking Title",
|
|
"invitees": [],
|
|
}
|
|
mock_new_booking.assert_called_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
|
async def test_create_booking_database_error(
|
|
client: AsyncClient, mock_logger: MagicMock
|
|
) -> None:
|
|
"""Test handling of database errors in POST /bookings/.
|
|
|
|
Verifies that the endpoint returns a 500 status on database failure.
|
|
|
|
Args:
|
|
client: The FastAPI test client for making HTTP requests.
|
|
mock_logger: The mocked logger instance.
|
|
"""
|
|
booking_data: dict[str, Any] = {
|
|
"room_id": 1,
|
|
"start_time": "2025-08-25T10:00:00",
|
|
"end_time": "2025-08-25T11:00:00",
|
|
}
|
|
|
|
async def raise_sqlalchemy_error(*args, **kwargs): # type: ignore
|
|
raise SQLAlchemyError()
|
|
|
|
with (
|
|
patch(
|
|
"backend.routers.bookings.new_booking",
|
|
new=AsyncMock(side_effect=raise_sqlalchemy_error),
|
|
) as mock_new_booking,
|
|
patch(
|
|
"backend.routers.bookings.get_invitees_for_booking",
|
|
new=AsyncMock(return_value=[]),
|
|
),
|
|
patch("backend.routers.bookings.get_room", new=AsyncMock(return_value=None)),
|
|
):
|
|
response = await 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
|
|
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
|
async def test_update_booking_success(
|
|
client: AsyncClient, sample_room: Room, mock_logger: MagicMock
|
|
) -> None:
|
|
"""Test successful update of a booking via PUT /bookings/{booking_id}.
|
|
|
|
Verifies that the endpoint returns the updated booking with a 200 status.
|
|
|
|
Args:
|
|
client: The FastAPI test client for making HTTP requests.
|
|
sample_room: The mocked room object.
|
|
mock_logger: The mocked logger instance.
|
|
"""
|
|
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",
|
|
"title": "Updated Test Booking Title",
|
|
}
|
|
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,
|
|
patch(
|
|
"backend.routers.bookings.get_invitees_for_booking", new=AsyncMock()
|
|
) as mock_get_invitees,
|
|
patch("backend.routers.bookings.get_room", new=AsyncMock()) as mock_get_room,
|
|
):
|
|
mock_update_booking.return_value = updated_booking
|
|
mock_get_invitees.return_value = []
|
|
mock_get_room.return_value = sample_room
|
|
|
|
response = await client.put(f"/bookings/{booking_id}", json=update_data)
|
|
|
|
assert response.status_code == 200
|
|
actual = response.json()
|
|
actual.pop("room", None)
|
|
assert actual == {
|
|
"id": booking_id,
|
|
"room_id": 2,
|
|
"start_time": "2025-08-25T14:00:00",
|
|
"end_time": "2025-08-25T15:00:00",
|
|
"title": "Updated Test Booking Title",
|
|
"invitees": [],
|
|
}
|
|
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, ANY, **update_data)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
|
async def test_update_booking_not_found(
|
|
client: AsyncClient, mock_logger: MagicMock
|
|
) -> 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.
|
|
|
|
Args:
|
|
client: The FastAPI test client for making HTTP requests.
|
|
mock_logger: The mocked logger instance.
|
|
"""
|
|
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",
|
|
"title": "Updated Test Booking Title",
|
|
}
|
|
with patch(
|
|
"backend.routers.bookings.update_booking", new=AsyncMock()
|
|
) as mock_update_booking:
|
|
mock_update_booking.side_effect = NoResultFound()
|
|
|
|
response = await 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, ANY, **update_data)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
|
async def test_update_booking_database_error(
|
|
client: AsyncClient, mock_logger: MagicMock
|
|
) -> None:
|
|
"""Test handling of database errors in PUT /bookings/{booking_id}.
|
|
|
|
Verifies that the endpoint returns a 500 status on database failure.
|
|
|
|
Args:
|
|
client: The FastAPI test client for making HTTP requests.
|
|
mock_logger: The mocked logger instance.
|
|
"""
|
|
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",
|
|
}
|
|
|
|
async def raise_sqlalchemy_error(*args, **kwargs): # type: ignore
|
|
raise SQLAlchemyError()
|
|
|
|
with (
|
|
patch(
|
|
"backend.routers.bookings.update_booking",
|
|
new=AsyncMock(side_effect=raise_sqlalchemy_error),
|
|
) as mock_update_booking,
|
|
patch(
|
|
"backend.routers.bookings.get_invitees_for_booking",
|
|
new=AsyncMock(return_value=[]),
|
|
),
|
|
patch("backend.routers.bookings.get_room", new=AsyncMock(return_value=None)),
|
|
):
|
|
response = await client.put(f"/bookings/{booking_id}", json=update_data)
|
|
assert response.status_code == 500
|
|
assert "detail" in response.json()
|
|
mock_update_booking.assert_called_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
|
async def test_delete_booking_not_found(
|
|
client: AsyncClient, mock_logger: MagicMock
|
|
) -> 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.
|
|
|
|
Args:
|
|
client: The FastAPI test client for making HTTP requests.
|
|
mock_logger: The mocked logger instance.
|
|
"""
|
|
booking_id = 999
|
|
with patch(
|
|
"backend.routers.bookings.delete_booking", new=AsyncMock()
|
|
) as mock_delete_booking:
|
|
mock_delete_booking.side_effect = NoResultFound()
|
|
|
|
response = await 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, ANY)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
|
async def test_delete_booking_database_error(
|
|
client: AsyncClient, mock_logger: MagicMock
|
|
) -> None:
|
|
"""Test handling of database errors in DELETE /bookings/{booking_id}.
|
|
|
|
Verifies that the endpoint returns a 500 status on database failure.
|
|
|
|
Args:
|
|
client: The FastAPI test client for making HTTP requests.
|
|
mock_logger: The mocked logger instance.
|
|
"""
|
|
booking_id = 1
|
|
with patch(
|
|
"backend.routers.bookings.delete_booking", new=AsyncMock()
|
|
) as mock_delete_booking:
|
|
mock_delete_booking.side_effect = SQLAlchemyError()
|
|
|
|
response = await 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, ANY)
|