mirror of
https://github.com/xlorepdarkhelm/numinar-coding-project.git
synced 2026-09-08 08:28:18 -04:00
Lots of fixes to lots of things.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2
backend/poetry.lock
generated
2
backend/poetry.lock
generated
@@ -3394,4 +3394,4 @@ tests-strict = ["pytest (==4.6.0) ; python_version < \"3.10.0\" and python_versi
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.13,<4.0"
|
||||
content-hash = "4fed8ee03daf2c52faa530fb0cc25cae226700e71bbabd83d0e40185c58e89de"
|
||||
content-hash = "c8b1a52c4933e45697a4f71396247ff4c23e33621dfdfab3859f8472b62a27e3"
|
||||
|
||||
@@ -51,6 +51,7 @@ xdoctest = {extras = ["colors"], version = ">=0.15.10"}
|
||||
myst-parser = {version = ">=0.16.1"}
|
||||
pytest-asyncio = "^1.1.0"
|
||||
cryptography = ">=45.0.6" # For safety
|
||||
httpx = "^0.28.1"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
backend = "backend.__main__:main"
|
||||
|
||||
@@ -5,6 +5,7 @@ and manages the application lifecycle.
|
||||
"""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI
|
||||
@@ -27,6 +28,7 @@ if ENVIRONMENT not in SHOW_DOCS_ENVIRONMENTS:
|
||||
app_configs["openapi_url"] = None # Disable OpenAPI schema
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
"""Lifespan event to create database tables on startup.
|
||||
|
||||
@@ -38,7 +40,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
after database initialization.
|
||||
"""
|
||||
await create_db_and_tables()
|
||||
yield # This is where the lifespan ends
|
||||
yield
|
||||
|
||||
|
||||
app_configs["lifespan"] = lifespan
|
||||
|
||||
@@ -12,6 +12,9 @@ from sqlalchemy.exc import SQLAlchemyError
|
||||
from backend.dependencies.db import DBSession
|
||||
from backend.models import Booking
|
||||
from backend.models import BookingList
|
||||
|
||||
# Import the SSE publisher from rooms router
|
||||
from backend.routers.rooms import publish_room_availability_event
|
||||
from backend.schemas.bookings import BookingCreate
|
||||
from backend.schemas.bookings import BookingResponse
|
||||
from backend.schemas.bookings import BookingUpdate
|
||||
@@ -115,11 +118,11 @@ async def create_booking(booking: BookingCreate, session: DBSession) -> Booking:
|
||||
"""Create a new booking.
|
||||
|
||||
Args:
|
||||
booking: Data for the new booking.
|
||||
session: Database session.
|
||||
booking (BookingCreate): Data for the new booking.
|
||||
session (DBSession): Database session.
|
||||
|
||||
Returns:
|
||||
Created booking.
|
||||
Booking: Created booking.
|
||||
|
||||
Raises:
|
||||
HTTPException: If a database error or unexpected error occurs.
|
||||
@@ -127,7 +130,9 @@ async def create_booking(booking: BookingCreate, session: DBSession) -> Booking:
|
||||
logger.debug("Received request to create a new booking")
|
||||
try:
|
||||
db_booking = Booking(**booking.model_dump())
|
||||
created_booking = await new_booking(session, db_booking)
|
||||
created_booking = await new_booking(
|
||||
session, db_booking, publish_room_availability_event
|
||||
)
|
||||
logger.info(f"Successfully created booking with id: {created_booking.id}")
|
||||
return created_booking
|
||||
except SQLAlchemyError as e:
|
||||
@@ -151,12 +156,12 @@ async def update_existing_booking(
|
||||
"""Update an existing booking.
|
||||
|
||||
Args:
|
||||
booking_id: ID of the booking to update.
|
||||
booking_update: Updated data for the booking.
|
||||
session: Database session.
|
||||
booking_id (int): ID of the booking to update.
|
||||
booking_update (BookingUpdate): Updated data for the booking.
|
||||
session (DBSession): Database session.
|
||||
|
||||
Returns:
|
||||
Updated booking.
|
||||
Booking: Updated booking.
|
||||
|
||||
Raises:
|
||||
HTTPException: If the booking is not found, a database error occurs,
|
||||
@@ -165,7 +170,9 @@ async def update_existing_booking(
|
||||
logger.debug(f"Received request to update booking with id: {booking_id}")
|
||||
try:
|
||||
booking_params = booking_update.model_dump(exclude_unset=True)
|
||||
updated_booking = await update_booking(session, booking_id, **booking_params)
|
||||
updated_booking = await update_booking(
|
||||
session, booking_id, publish_room_availability_event, **booking_params
|
||||
)
|
||||
logger.info(f"Successfully updated booking with id: {booking_id}")
|
||||
return updated_booking
|
||||
except (NoResultFound, ValueError) as e:
|
||||
@@ -197,8 +204,8 @@ async def delete_existing_booking(booking_id: int, session: DBSession) -> None:
|
||||
"""Delete a booking.
|
||||
|
||||
Args:
|
||||
booking_id: ID of the booking to delete.
|
||||
session: Database session.
|
||||
booking_id (int): ID of the booking to delete.
|
||||
session (DBSession): Database session.
|
||||
|
||||
Raises:
|
||||
HTTPException: If the booking is not found, a database error occurs,
|
||||
@@ -206,7 +213,7 @@ async def delete_existing_booking(booking_id: int, session: DBSession) -> None:
|
||||
"""
|
||||
logger.debug(f"Received request to delete booking with id: {booking_id}")
|
||||
try:
|
||||
await delete_booking(session, booking_id)
|
||||
await delete_booking(session, booking_id, publish_room_availability_event)
|
||||
logger.info(f"Successfully deleted booking with id: {booking_id}")
|
||||
except (NoResultFound, ValueError) as e:
|
||||
logger.warning(f"Booking with id {booking_id} not found")
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
"""Routes for room-related operations in the backend."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
from fastapi import status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
@@ -24,8 +29,70 @@ from backend.services.rooms import update_room
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
router = APIRouter(prefix="/rooms", tags=["rooms"])
|
||||
|
||||
room_availability_subscribers: list[asyncio.Queue[dict[str, Any]]] = []
|
||||
|
||||
|
||||
async def event_generator(
|
||||
request: Any,
|
||||
queue: asyncio.Queue[dict[str, Any]],
|
||||
timeout: float = 15.0,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""Yield SSE events or keep-alive messages from a queue.
|
||||
|
||||
Args:
|
||||
request: The FastAPI request object.
|
||||
queue: The asyncio queue to receive events from.
|
||||
timeout: Timeout for keep-alive messages.
|
||||
|
||||
Yields:
|
||||
SSE-formatted strings.
|
||||
"""
|
||||
import json
|
||||
|
||||
try:
|
||||
while True:
|
||||
if await request.is_disconnected():
|
||||
break
|
||||
try:
|
||||
event = await asyncio.wait_for(queue.get(), timeout=timeout)
|
||||
yield f"data: {json.dumps(event)}\n\n"
|
||||
except asyncio.TimeoutError:
|
||||
yield ": keep-alive\n\n"
|
||||
finally:
|
||||
if queue in room_availability_subscribers:
|
||||
room_availability_subscribers.remove(queue)
|
||||
|
||||
|
||||
async def publish_room_availability_event(event: dict[str, Any]) -> None:
|
||||
"""Publish a room availability event to all subscribers.
|
||||
|
||||
Args:
|
||||
event: The event data to publish.
|
||||
"""
|
||||
for queue in list(room_availability_subscribers):
|
||||
await queue.put(event)
|
||||
|
||||
|
||||
@router.get("/availability/stream")
|
||||
async def stream_room_availability(request: Request) -> StreamingResponse:
|
||||
"""Stream real-time room availability changes using SSE.
|
||||
|
||||
Args:
|
||||
request: The incoming HTTP request.
|
||||
|
||||
Returns:
|
||||
The streaming response for SSE.
|
||||
"""
|
||||
queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
|
||||
room_availability_subscribers.append(queue)
|
||||
|
||||
return StreamingResponse(
|
||||
event_generator(request, queue), media_type="text/event-stream"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", response_model=List[RoomResponse])
|
||||
async def read_rooms(session: DBSession) -> RoomList:
|
||||
|
||||
@@ -32,6 +32,8 @@ from backend.services.rooms import get_room
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EventPublisher = Callable[[dict[str, Any]], Awaitable[None]] | None
|
||||
|
||||
|
||||
class BookingParams(TypedDict):
|
||||
"""Parameters for updating a booking.
|
||||
@@ -98,85 +100,120 @@ async def get_booking(session: AsyncSession, booking_id: int) -> Booking:
|
||||
logger.debug("Exiting get_booking")
|
||||
|
||||
|
||||
async def new_booking(session: AsyncSession, booking: Booking) -> Booking:
|
||||
async def _validate_new_booking_room_exists(session: AsyncSession, room_id: int) -> Any:
|
||||
try:
|
||||
return await get_room(session, room_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Attempted to book non-existent room_id {room_id}")
|
||||
raise ValueError("The selected room does not exist.") from e
|
||||
|
||||
|
||||
def _validate_new_booking_time_constraints(
|
||||
start: datetime, end: datetime, now: datetime
|
||||
) -> None:
|
||||
if start < now or end < now:
|
||||
logger.warning("Attempted to create booking in the past.")
|
||||
raise ValueError("Bookings cannot be made in the past.")
|
||||
if start >= end:
|
||||
logger.warning("Attempted to create booking with start_time >= end_time.")
|
||||
raise ValueError("The booking start time must be before the end time.")
|
||||
|
||||
|
||||
def _validate_new_booking_max_future(
|
||||
start: datetime, end: datetime, now: datetime, max_months: int
|
||||
) -> None:
|
||||
max_future = now + timedelta(days=30 * max_months)
|
||||
if start > max_future or end > max_future:
|
||||
logger.warning("Attempted to create booking too far in the future.")
|
||||
raise ValueError(
|
||||
f"Bookings can only be made up to {max_months} months in advance."
|
||||
)
|
||||
|
||||
|
||||
async def _validate_new_booking_no_overlap(
|
||||
session: AsyncSession, booking: Booking
|
||||
) -> None:
|
||||
overlap_stmt = select(Booking).where(
|
||||
Booking.room_id == booking.room_id,
|
||||
func.tstzrange(Booking.start_time, Booking.end_time, "[]").op("&&")(
|
||||
func.tstzrange(booking.start_time, booking.end_time, "[]")
|
||||
),
|
||||
)
|
||||
result = await session.scalars(overlap_stmt)
|
||||
overlapping = result.first()
|
||||
if overlapping:
|
||||
logger.warning(
|
||||
"Attempted to create overlapping booking for room_id %s",
|
||||
booking.room_id,
|
||||
)
|
||||
raise ValueError(
|
||||
"Booking times overlap with an existing booking for this room."
|
||||
)
|
||||
|
||||
|
||||
async def _validate_new_booking_capacity(
|
||||
session: AsyncSession, booking: Booking, room: Any
|
||||
) -> None:
|
||||
invitees = await get_invitees_for_booking(
|
||||
session, getattr(booking, "id", None) or -1
|
||||
)
|
||||
num_attendees = len(invitees) if invitees else 0
|
||||
if num_attendees > room.capacity:
|
||||
logger.warning(
|
||||
f"Attempted to create booking with {num_attendees} attendees"
|
||||
f" exceeding room capacity {room.capacity}."
|
||||
)
|
||||
raise ValueError(
|
||||
f"Number of attendees ({num_attendees}) exceeds the room"
|
||||
f" capacity ({room.capacity})."
|
||||
)
|
||||
|
||||
|
||||
async def new_booking(
|
||||
session: AsyncSession,
|
||||
booking: Booking,
|
||||
event_publisher: EventPublisher = None,
|
||||
) -> Booking:
|
||||
"""Create a new booking in the database.
|
||||
|
||||
Args:
|
||||
session: Database session.
|
||||
booking: Booking object to create.
|
||||
session (AsyncSession): Database session.
|
||||
booking (Booking): Booking object to create.
|
||||
event_publisher (EventPublisher): Optional async callback to publish booking events.
|
||||
|
||||
Returns:
|
||||
Created booking.
|
||||
Booking: Created booking.
|
||||
|
||||
Raises:
|
||||
ValueError: If any booking constraint is violated.
|
||||
Exception: If any unexpected error occurs.
|
||||
"""
|
||||
logger.debug("Entering new_booking")
|
||||
try:
|
||||
# Constraint: Room must exist
|
||||
try:
|
||||
room = await get_room(session, booking.room_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Attempted to book non-existent room_id {booking.room_id}")
|
||||
raise ValueError("The selected room does not exist.") from e
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Constraint: No bookings in the past
|
||||
if booking.start_time < now or booking.end_time < now:
|
||||
logger.warning("Attempted to create booking in the past.")
|
||||
raise ValueError("Bookings cannot be made in the past.")
|
||||
|
||||
# Constraint: Start date < end date
|
||||
if booking.start_time >= booking.end_time:
|
||||
logger.warning("Attempted to create booking with start_time >= end_time.")
|
||||
raise ValueError("The booking start time must be before the end time.")
|
||||
|
||||
# Constraint: Max months in future
|
||||
room = await _validate_new_booking_room_exists(session, booking.room_id)
|
||||
_validate_new_booking_time_constraints(
|
||||
booking.start_time, booking.end_time, now
|
||||
)
|
||||
max_months = config("BOOKING_MAX_MONTHS", default=12, cast=int)
|
||||
max_future = now + timedelta(days=30 * max_months)
|
||||
if booking.start_time > max_future or booking.end_time > max_future:
|
||||
logger.warning("Attempted to create booking too far in the future.")
|
||||
raise ValueError(
|
||||
f"Bookings can only be made up to {max_months} months in advance."
|
||||
)
|
||||
|
||||
# Check for overlapping bookings for the same room
|
||||
overlap_stmt = select(Booking).where(
|
||||
Booking.room_id == booking.room_id,
|
||||
func.tstzrange(Booking.start_time, Booking.end_time, "[]").op("&&")(
|
||||
func.tstzrange(booking.start_time, booking.end_time, "[]")
|
||||
),
|
||||
_validate_new_booking_max_future(
|
||||
booking.start_time, booking.end_time, now, max_months
|
||||
)
|
||||
result = await session.scalars(overlap_stmt)
|
||||
overlapping = result.first()
|
||||
if overlapping:
|
||||
logger.warning(
|
||||
"Attempted to create overlapping booking for room_id %s",
|
||||
booking.room_id,
|
||||
)
|
||||
raise ValueError(
|
||||
"Booking times overlap with an existing booking for this room."
|
||||
)
|
||||
|
||||
# Constraint: Attendees <= room capacity
|
||||
invitees = await get_invitees_for_booking(
|
||||
session, getattr(booking, "id", None) or -1
|
||||
)
|
||||
num_attendees = len(invitees) if invitees else 0
|
||||
if num_attendees > room.capacity:
|
||||
logger.warning(
|
||||
f"Attempted to create booking with {num_attendees} attendees"
|
||||
f" exceeding room capacity {room.capacity}."
|
||||
)
|
||||
raise ValueError(
|
||||
f"Number of attendees ({num_attendees}) exceeds the room"
|
||||
f" capacity ({room.capacity})."
|
||||
)
|
||||
await _validate_new_booking_no_overlap(session, booking)
|
||||
await _validate_new_booking_capacity(session, booking, room)
|
||||
|
||||
session.add(booking)
|
||||
await session.commit()
|
||||
logger.info(f"Successfully created new booking with id: {booking.id}")
|
||||
if event_publisher:
|
||||
await event_publisher(
|
||||
{
|
||||
"action": "created",
|
||||
"room_id": booking.room_id,
|
||||
"booking_id": booking.id,
|
||||
"start_time": str(booking.start_time),
|
||||
"end_time": str(booking.end_time),
|
||||
}
|
||||
)
|
||||
return booking
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create new booking: {str(e)}")
|
||||
@@ -255,17 +292,21 @@ def _validate_attendee_count(invitees: Sequence[Any], room: Any) -> None:
|
||||
async def update_booking(
|
||||
session: AsyncSession,
|
||||
booking_id: int,
|
||||
event_publisher: EventPublisher = None,
|
||||
**kwargs: Unpack[BookingParams],
|
||||
) -> Booking:
|
||||
"""Update an existing booking.
|
||||
|
||||
Args:
|
||||
session: Database session.
|
||||
booking_id: ID of the booking to update.
|
||||
kwargs: Updated booking parameters (room ID, start time, end time).
|
||||
session (AsyncSession): Database session.
|
||||
booking_id (int): ID of the booking to update.
|
||||
event_publisher (EventPublisher): Optional async callback to publish
|
||||
booking events.
|
||||
kwargs (Unpack[BookingParams]): Updated booking parameters
|
||||
(room ID, start time, end time).
|
||||
|
||||
Returns:
|
||||
Updated booking.
|
||||
Booking: Updated booking.
|
||||
|
||||
Raises:
|
||||
ValueError: If no booking with the specified ID is found.
|
||||
@@ -295,6 +336,16 @@ async def update_booking(
|
||||
await session.commit()
|
||||
booking = await get_booking(session, booking_id)
|
||||
logger.info(f"Successfully updated booking with id: {booking_id}")
|
||||
if event_publisher:
|
||||
await event_publisher(
|
||||
{
|
||||
"action": "updated",
|
||||
"room_id": booking.room_id,
|
||||
"booking_id": booking.id,
|
||||
"start_time": str(booking.start_time),
|
||||
"end_time": str(booking.end_time),
|
||||
}
|
||||
)
|
||||
return booking
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update booking with id {booking_id}: {str(e)}")
|
||||
@@ -304,18 +355,25 @@ async def update_booking(
|
||||
logger.debug("Exiting update_booking")
|
||||
|
||||
|
||||
async def delete_booking(session: AsyncSession, booking_id: int) -> None:
|
||||
async def delete_booking(
|
||||
session: AsyncSession,
|
||||
booking_id: int,
|
||||
event_publisher: EventPublisher = None,
|
||||
) -> None:
|
||||
"""Delete a booking from the database.
|
||||
|
||||
Args:
|
||||
session: Database session.
|
||||
booking_id: ID of the booking to delete.
|
||||
session (AsyncSession): Database session.
|
||||
booking_id (int): ID of the booking to delete.
|
||||
event_publisher (EventPublisher): Optional async callback to publish booking events.
|
||||
|
||||
Raises:
|
||||
ValueError: If no booking with the specified ID is found.
|
||||
"""
|
||||
logger.debug(f"Entering delete_booking with booking_id: {booking_id}")
|
||||
try:
|
||||
# Get booking before delete to get room_id and times
|
||||
booking = await get_booking(session, booking_id)
|
||||
stmt = delete(Booking).where(Booking.id == booking_id)
|
||||
result = await session.execute(stmt)
|
||||
if result.rowcount == 0:
|
||||
@@ -323,6 +381,16 @@ async def delete_booking(session: AsyncSession, booking_id: int) -> None:
|
||||
raise ValueError(f"No booking found with id {booking_id}")
|
||||
await session.commit()
|
||||
logger.info(f"Successfully deleted booking with id: {booking_id}")
|
||||
if event_publisher:
|
||||
await event_publisher(
|
||||
{
|
||||
"action": "deleted",
|
||||
"room_id": booking.room_id,
|
||||
"booking_id": booking.id,
|
||||
"start_time": str(booking.start_time),
|
||||
"end_time": str(booking.end_time),
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete booking with id {booking_id}: {str(e)}")
|
||||
await session.rollback()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Test configuration and fixtures for the backend tests."""
|
||||
|
||||
import logging
|
||||
from collections.abc import AsyncGenerator
|
||||
from collections.abc import Generator
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
@@ -10,7 +11,9 @@ from unittest.mock import MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
import pytest_asyncio
|
||||
from httpx import ASGITransport
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.main import app
|
||||
@@ -33,14 +36,16 @@ def async_session() -> AsyncSession:
|
||||
return AsyncMock(spec=AsyncSession)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client() -> TestClient:
|
||||
"""Fixture to provide a FastAPI TestClient for testing endpoints.
|
||||
@pytest_asyncio.fixture
|
||||
async def client() -> AsyncGenerator[AsyncClient, None]:
|
||||
"""Fixture to provide an AsyncClient for async endpoint testing.
|
||||
|
||||
Returns:
|
||||
A FastAPI TestClient instance for making HTTP requests.
|
||||
Yields:
|
||||
An HTTPX AsyncClient instance for making async HTTP requests.
|
||||
"""
|
||||
return TestClient(app)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
yield ac
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -55,19 +60,7 @@ def mock_logger(request: pytest.FixtureRequest) -> Generator[MagicMock, None, No
|
||||
|
||||
Usage:
|
||||
To use this fixture in a test module, parameterize it with the module path
|
||||
where the logger is defined. For example, to mock the logger in
|
||||
`backend.services.users`, use the `@pytest.mark.parametrize` decorator:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.services.users"], indirect=True)
|
||||
async def test_example(mock_logger):
|
||||
# Test code here, with mock_logger patched for backend.services.users
|
||||
|
||||
The `indirect=True` argument ensures the fixture uses the provided module path.
|
||||
The fixture patches the logger at the specified path
|
||||
(e.g., `backend.services.users.logger`) and yields a `MagicMock` instance that can be
|
||||
used to verify logging behavior.
|
||||
where the logger is defined.
|
||||
"""
|
||||
# Default path if none provided, or use the parameterized path
|
||||
module_path = getattr(request, "param", "backend") + ".logger"
|
||||
@@ -77,6 +70,16 @@ def mock_logger(request: pytest.FixtureRequest) -> Generator[MagicMock, None, No
|
||||
yield mock_logger_instance
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def event_publisher() -> AsyncMock:
|
||||
"""Fixture for an AsyncMock event publisher for event publishing tests.
|
||||
|
||||
Returns:
|
||||
An async mock event publisher callable.
|
||||
"""
|
||||
return AsyncMock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_users() -> UserList:
|
||||
"""Fixture to provide sample User objects for testing.
|
||||
|
||||
@@ -8,7 +8,7 @@ from unittest.mock import MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
@@ -20,7 +20,7 @@ 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: TestClient, sample_bookings: BookingList, mock_logger: MagicMock
|
||||
client: AsyncClient, sample_bookings: BookingList, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test successful retrieval of all bookings for a room via GET /bookings/room/{room_id}.
|
||||
|
||||
@@ -37,7 +37,7 @@ async def test_get_bookings_for_room_success(
|
||||
) as mock_get_bookings:
|
||||
mock_get_bookings.return_value = sample_bookings
|
||||
|
||||
response = client.get(f"/bookings/room/{room_id}")
|
||||
response = await client.get(f"/bookings/room/{room_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [
|
||||
@@ -55,7 +55,7 @@ async def test_get_bookings_for_room_success(
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
||||
async def test_get_bookings_for_room_empty(
|
||||
client: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test retrieval of bookings when none exist for a room via GET /bookings/room/{room_id}.
|
||||
|
||||
@@ -71,7 +71,7 @@ async def test_get_bookings_for_room_empty(
|
||||
) as mock_get_bookings:
|
||||
mock_get_bookings.return_value = []
|
||||
|
||||
response = client.get(f"/bookings/room/{room_id}")
|
||||
response = await client.get(f"/bookings/room/{room_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
@@ -81,7 +81,7 @@ async def test_get_bookings_for_room_empty(
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
||||
async def test_get_bookings_for_room_database_error(
|
||||
client: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of database errors in GET /bookings/room/{room_id}.
|
||||
|
||||
@@ -97,7 +97,7 @@ async def test_get_bookings_for_room_database_error(
|
||||
) as mock_get_bookings:
|
||||
mock_get_bookings.side_effect = SQLAlchemyError()
|
||||
|
||||
response = client.get(f"/bookings/room/{room_id}")
|
||||
response = await client.get(f"/bookings/room/{room_id}")
|
||||
|
||||
assert response.status_code == 500
|
||||
assert "detail" in response.json()
|
||||
@@ -107,7 +107,7 @@ async def test_get_bookings_for_room_database_error(
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
||||
async def test_get_booking_success(
|
||||
client: TestClient, sample_room: Room, mock_logger: MagicMock
|
||||
client: AsyncClient, sample_room: Room, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test successful retrieval of a booking by ID via GET /bookings/{booking_id}.
|
||||
|
||||
@@ -131,7 +131,7 @@ async def test_get_booking_success(
|
||||
) as mock_get_booking:
|
||||
mock_get_booking.return_value = booking
|
||||
|
||||
response = client.get(f"/bookings/{booking_id}")
|
||||
response = await client.get(f"/bookings/{booking_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
@@ -146,7 +146,7 @@ async def test_get_booking_success(
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
||||
async def test_get_booking_not_found(
|
||||
client: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of non-existent booking in GET /bookings/{booking_id}.
|
||||
|
||||
@@ -162,7 +162,7 @@ async def test_get_booking_not_found(
|
||||
) as mock_get_booking:
|
||||
mock_get_booking.side_effect = NoResultFound()
|
||||
|
||||
response = client.get(f"/bookings/{booking_id}")
|
||||
response = await client.get(f"/bookings/{booking_id}")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert "detail" in response.json()
|
||||
@@ -172,7 +172,7 @@ async def test_get_booking_not_found(
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
||||
async def test_get_booking_database_error(
|
||||
client: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of database errors in GET /bookings/{booking_id}.
|
||||
|
||||
@@ -188,7 +188,7 @@ async def test_get_booking_database_error(
|
||||
) as mock_get_booking:
|
||||
mock_get_booking.side_effect = SQLAlchemyError()
|
||||
|
||||
response = client.get(f"/bookings/{booking_id}")
|
||||
response = await client.get(f"/bookings/{booking_id}")
|
||||
|
||||
assert response.status_code == 500
|
||||
assert "detail" in response.json()
|
||||
@@ -198,7 +198,7 @@ async def test_get_booking_database_error(
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
||||
async def test_create_booking_success(
|
||||
client: TestClient, sample_room: Room, mock_logger: MagicMock
|
||||
client: AsyncClient, sample_room: Room, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test successful creation of a new booking via POST /bookings/.
|
||||
|
||||
@@ -222,7 +222,7 @@ async def test_create_booking_success(
|
||||
) as mock_new_booking:
|
||||
mock_new_booking.return_value = created_booking
|
||||
|
||||
response = client.post("/bookings/", json=booking_data)
|
||||
response = await client.post("/bookings/", json=booking_data)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert response.json() == {
|
||||
@@ -237,7 +237,7 @@ async def test_create_booking_success(
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
||||
async def test_create_booking_database_error(
|
||||
client: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of database errors in POST /bookings/.
|
||||
|
||||
@@ -257,7 +257,7 @@ async def test_create_booking_database_error(
|
||||
) as mock_new_booking:
|
||||
mock_new_booking.side_effect = SQLAlchemyError()
|
||||
|
||||
response = client.post("/bookings/", json=booking_data)
|
||||
response = await client.post("/bookings/", json=booking_data)
|
||||
|
||||
assert response.status_code == 500
|
||||
assert "detail" in response.json()
|
||||
@@ -267,7 +267,7 @@ async def test_create_booking_database_error(
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.bookings"], indirect=True)
|
||||
async def test_update_booking_success(
|
||||
client: TestClient, sample_room: Room, mock_logger: MagicMock
|
||||
client: AsyncClient, sample_room: Room, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test successful update of a booking via PUT /bookings/{booking_id}.
|
||||
|
||||
@@ -292,7 +292,7 @@ async def test_update_booking_success(
|
||||
) as mock_update_booking:
|
||||
mock_update_booking.return_value = updated_booking
|
||||
|
||||
response = client.put(f"/bookings/{booking_id}", json=update_data)
|
||||
response = await client.put(f"/bookings/{booking_id}", json=update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
@@ -303,13 +303,13 @@ async def test_update_booking_success(
|
||||
}
|
||||
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)
|
||||
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: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of non-existent booking in PUT /bookings/{booking_id}.
|
||||
|
||||
@@ -330,19 +330,19 @@ async def test_update_booking_not_found(
|
||||
) as mock_update_booking:
|
||||
mock_update_booking.side_effect = NoResultFound()
|
||||
|
||||
response = client.put(f"/bookings/{booking_id}", json=update_data)
|
||||
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, **update_data)
|
||||
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: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of database errors in PUT /bookings/{booking_id}.
|
||||
|
||||
@@ -363,19 +363,19 @@ async def test_update_booking_database_error(
|
||||
) as mock_update_booking:
|
||||
mock_update_booking.side_effect = SQLAlchemyError()
|
||||
|
||||
response = client.put(f"/bookings/{booking_id}", json=update_data)
|
||||
response = await 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)
|
||||
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_delete_booking_success(
|
||||
client: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test successful deletion of a booking via DELETE /bookings/{booking_id}.
|
||||
|
||||
@@ -389,17 +389,17 @@ async def test_delete_booking_success(
|
||||
with patch(
|
||||
"backend.routers.bookings.delete_booking", new=AsyncMock()
|
||||
) as mock_delete_booking:
|
||||
response = client.delete(f"/bookings/{booking_id}")
|
||||
response = await client.delete(f"/bookings/{booking_id}")
|
||||
|
||||
assert response.status_code == 204
|
||||
assert response.text == ""
|
||||
mock_delete_booking.assert_called_once_with(ANY, booking_id)
|
||||
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_not_found(
|
||||
client: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of non-existent booking in DELETE /bookings/{booking_id}.
|
||||
|
||||
@@ -415,17 +415,17 @@ async def test_delete_booking_not_found(
|
||||
) as mock_delete_booking:
|
||||
mock_delete_booking.side_effect = NoResultFound()
|
||||
|
||||
response = client.delete(f"/bookings/{booking_id}")
|
||||
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)
|
||||
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: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of database errors in DELETE /bookings/{booking_id}.
|
||||
|
||||
@@ -441,8 +441,8 @@ async def test_delete_booking_database_error(
|
||||
) as mock_delete_booking:
|
||||
mock_delete_booking.side_effect = SQLAlchemyError()
|
||||
|
||||
response = client.delete(f"/bookings/{booking_id}")
|
||||
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)
|
||||
mock_delete_booking.assert_called_once_with(ANY, booking_id, ANY)
|
||||
|
||||
@@ -7,7 +7,7 @@ from unittest.mock import MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from backend.models import Booking
|
||||
@@ -18,7 +18,7 @@ from backend.models import InviteeList
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.invitees"], indirect=True)
|
||||
async def test_get_invitees_for_booking_success(
|
||||
client: TestClient,
|
||||
client: AsyncClient,
|
||||
sample_booking: Booking,
|
||||
sample_invitees: InviteeList,
|
||||
mock_logger: MagicMock,
|
||||
@@ -38,9 +38,7 @@ async def test_get_invitees_for_booking_success(
|
||||
"backend.routers.invitees.get_invitees_for_booking", new=AsyncMock()
|
||||
) as mock_get_invitees:
|
||||
mock_get_invitees.return_value = sample_invitees
|
||||
|
||||
response = client.get(f"/invitees/booking/{sample_booking.id}")
|
||||
|
||||
response = await client.get(f"/invitees/booking/{sample_booking.id}")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [
|
||||
{
|
||||
@@ -57,7 +55,7 @@ async def test_get_invitees_for_booking_success(
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.invitees"], indirect=True)
|
||||
async def test_get_invitees_for_booking_empty(
|
||||
client: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test retrieval of invitees when none exist for a booking.
|
||||
|
||||
@@ -73,9 +71,7 @@ async def test_get_invitees_for_booking_empty(
|
||||
"backend.routers.invitees.get_invitees_for_booking", new=AsyncMock()
|
||||
) as mock_get_invitees:
|
||||
mock_get_invitees.return_value = []
|
||||
|
||||
response = client.get(f"/invitees/booking/{booking_id}")
|
||||
|
||||
response = await client.get(f"/invitees/booking/{booking_id}")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
mock_get_invitees.assert_called_once_with(ANY, booking_id)
|
||||
@@ -84,7 +80,7 @@ async def test_get_invitees_for_booking_empty(
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.invitees"], indirect=True)
|
||||
async def test_get_invitees_for_booking_database_error(
|
||||
client: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of database errors in GET /invitees/booking/{booking_id}.
|
||||
|
||||
@@ -99,9 +95,7 @@ async def test_get_invitees_for_booking_database_error(
|
||||
"backend.routers.invitees.get_invitees_for_booking", new=AsyncMock()
|
||||
) as mock_get_invitees:
|
||||
mock_get_invitees.side_effect = SQLAlchemyError()
|
||||
|
||||
response = client.get(f"/invitees/booking/{booking_id}")
|
||||
|
||||
response = await client.get(f"/invitees/booking/{booking_id}")
|
||||
assert response.status_code == 500
|
||||
assert "detail" in response.json()
|
||||
mock_get_invitees.assert_called_once_with(ANY, booking_id)
|
||||
@@ -110,7 +104,7 @@ async def test_get_invitees_for_booking_database_error(
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.invitees"], indirect=True)
|
||||
async def test_add_invitee_success(
|
||||
client: TestClient,
|
||||
client: AsyncClient,
|
||||
sample_invitee: Invitee,
|
||||
mock_logger: MagicMock,
|
||||
) -> None:
|
||||
@@ -132,11 +126,9 @@ async def test_add_invitee_success(
|
||||
"backend.routers.invitees.add_invitee_to_booking", new=AsyncMock()
|
||||
) as mock_add_invitee:
|
||||
mock_add_invitee.return_value = sample_invitee
|
||||
|
||||
response = client.post(
|
||||
response = await client.post(
|
||||
f"/invitees/booking/{sample_invitee.booking_id}", json=invitee_data
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
result = response.json()
|
||||
assert result["booking_id"] == sample_invitee.booking_id
|
||||
@@ -149,7 +141,7 @@ async def test_add_invitee_success(
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.invitees"], indirect=True)
|
||||
async def test_add_invitee_database_error(
|
||||
client: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of database errors in POST /invitees/booking/{booking_id}.
|
||||
|
||||
@@ -168,9 +160,9 @@ async def test_add_invitee_database_error(
|
||||
"backend.routers.invitees.add_invitee_to_booking", new=AsyncMock()
|
||||
) as mock_add_invitee:
|
||||
mock_add_invitee.side_effect = SQLAlchemyError()
|
||||
|
||||
response = client.post(f"/invitees/booking/{booking_id}", json=invitee_data)
|
||||
|
||||
response = await client.post(
|
||||
f"/invitees/booking/{booking_id}", json=invitee_data
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "detail" in response.json()
|
||||
mock_add_invitee.assert_called_once_with(
|
||||
@@ -181,7 +173,7 @@ async def test_add_invitee_database_error(
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.invitees"], indirect=True)
|
||||
async def test_remove_invitee_success(
|
||||
client: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test successful removal of an invitee from a booking.
|
||||
|
||||
@@ -197,8 +189,9 @@ async def test_remove_invitee_success(
|
||||
with patch(
|
||||
"backend.routers.invitees.remove_invitee_from_booking", new=AsyncMock()
|
||||
) as mock_remove_invitee:
|
||||
response = client.delete(f"/invitees/booking/{booking_id}/user/{user_email}")
|
||||
|
||||
response = await client.delete(
|
||||
f"/invitees/booking/{booking_id}/user/{user_email}"
|
||||
)
|
||||
assert response.status_code == 204
|
||||
assert response.text == ""
|
||||
mock_remove_invitee.assert_called_once_with(ANY, booking_id, user_email)
|
||||
@@ -207,7 +200,7 @@ async def test_remove_invitee_success(
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.invitees"], indirect=True)
|
||||
async def test_remove_invitee_database_error(
|
||||
client: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of database errors for delete.
|
||||
|
||||
@@ -224,9 +217,9 @@ async def test_remove_invitee_database_error(
|
||||
"backend.routers.invitees.remove_invitee_from_booking", new=AsyncMock()
|
||||
) as mock_remove_invitee:
|
||||
mock_remove_invitee.side_effect = SQLAlchemyError()
|
||||
|
||||
response = client.delete(f"/invitees/booking/{booking_id}/user/{user_email}")
|
||||
|
||||
response = await client.delete(
|
||||
f"/invitees/booking/{booking_id}/user/{user_email}"
|
||||
)
|
||||
assert response.status_code == 500
|
||||
assert "detail" in response.json()
|
||||
mock_remove_invitee.assert_called_once_with(ANY, booking_id, user_email)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Unit tests for the backend.routers.rooms module."""
|
||||
"""Tests for backend.routers.rooms in the Numinar coding project backend."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import ANY
|
||||
@@ -7,7 +7,7 @@ from unittest.mock import MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
|
||||
from backend.models import Room
|
||||
@@ -17,7 +17,7 @@ from backend.models import RoomList
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.rooms"], indirect=True)
|
||||
async def test_get_rooms_success(
|
||||
client: TestClient, sample_rooms: RoomList, mock_logger: MagicMock
|
||||
client: AsyncClient, sample_rooms: RoomList, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test successful retrieval of all rooms via GET /rooms/.
|
||||
|
||||
@@ -31,7 +31,7 @@ async def test_get_rooms_success(
|
||||
with patch("backend.routers.rooms.get_rooms", new=AsyncMock()) as mock_get_rooms:
|
||||
mock_get_rooms.return_value = sample_rooms
|
||||
|
||||
response = client.get("/rooms/")
|
||||
response = await client.get("/rooms/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [
|
||||
@@ -55,7 +55,7 @@ async def test_get_rooms_success(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.rooms"], indirect=True)
|
||||
async def test_get_rooms_empty(client: TestClient, mock_logger: MagicMock) -> None:
|
||||
async def test_get_rooms_empty(client: AsyncClient, mock_logger: MagicMock) -> None:
|
||||
"""Test retrieval of rooms when none exist via GET /rooms/.
|
||||
|
||||
Verifies that the endpoint returns an empty list with a 200 status.
|
||||
@@ -67,7 +67,7 @@ async def test_get_rooms_empty(client: TestClient, mock_logger: MagicMock) -> No
|
||||
with patch("backend.routers.rooms.get_rooms", new=AsyncMock()) as mock_get_rooms:
|
||||
mock_get_rooms.return_value = []
|
||||
|
||||
response = client.get("/rooms/")
|
||||
response = await client.get("/rooms/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
@@ -76,7 +76,7 @@ async def test_get_rooms_empty(client: TestClient, mock_logger: MagicMock) -> No
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.rooms"], indirect=True)
|
||||
async def test_get_room_success(client: TestClient, mock_logger: MagicMock) -> None:
|
||||
async def test_get_room_success(client: AsyncClient, mock_logger: MagicMock) -> None:
|
||||
"""Test successful retrieval of a room by ID via GET /rooms/{room_id}.
|
||||
|
||||
Verifies that the endpoint returns the correct room with a 200 status.
|
||||
@@ -96,7 +96,7 @@ async def test_get_room_success(client: TestClient, mock_logger: MagicMock) -> N
|
||||
with patch("backend.routers.rooms.get_room", new=AsyncMock()) as mock_get_room:
|
||||
mock_get_room.return_value = room
|
||||
|
||||
response = client.get(f"/rooms/{room_id}")
|
||||
response = await client.get(f"/rooms/{room_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
@@ -111,7 +111,7 @@ async def test_get_room_success(client: TestClient, mock_logger: MagicMock) -> N
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.rooms"], indirect=True)
|
||||
async def test_get_room_not_found(client: TestClient, mock_logger: MagicMock) -> None:
|
||||
async def test_get_room_not_found(client: AsyncClient, mock_logger: MagicMock) -> None:
|
||||
"""Test handling of non-existent room in GET /rooms/{room_id}.
|
||||
|
||||
Verifies that the endpoint returns a 404 status when the room is not found.
|
||||
@@ -124,7 +124,7 @@ async def test_get_room_not_found(client: TestClient, mock_logger: MagicMock) ->
|
||||
with patch("backend.routers.rooms.get_room", new=AsyncMock()) as mock_get_room:
|
||||
mock_get_room.side_effect = NoResultFound(f"Room with id {room_id} not found")
|
||||
|
||||
response = client.get(f"/rooms/{room_id}")
|
||||
response = await client.get(f"/rooms/{room_id}")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert "detail" in response.json()
|
||||
@@ -133,7 +133,7 @@ async def test_get_room_not_found(client: TestClient, mock_logger: MagicMock) ->
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.rooms"], indirect=True)
|
||||
async def test_new_room_success(client: TestClient, mock_logger: MagicMock) -> None:
|
||||
async def test_new_room_success(client: AsyncClient, mock_logger: MagicMock) -> None:
|
||||
"""Test successful creation of a new room via POST /rooms/.
|
||||
|
||||
Verifies that the endpoint returns the created room with a 201 status.
|
||||
@@ -153,7 +153,7 @@ async def test_new_room_success(client: TestClient, mock_logger: MagicMock) -> N
|
||||
with patch("backend.routers.rooms.new_room", new=AsyncMock()) as mock_new_room:
|
||||
mock_new_room.return_value = created_room
|
||||
|
||||
response = client.post("/rooms/", json=room_data)
|
||||
response = await client.post("/rooms/", json=room_data)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert response.json() == room_data
|
||||
@@ -162,7 +162,7 @@ async def test_new_room_success(client: TestClient, mock_logger: MagicMock) -> N
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.rooms"], indirect=True)
|
||||
async def test_update_room_success(client: TestClient, mock_logger: MagicMock) -> None:
|
||||
async def test_update_room_success(client: AsyncClient, mock_logger: MagicMock) -> None:
|
||||
"""Test successful update of a room via PUT /rooms/{room_id}.
|
||||
|
||||
Verifies that the endpoint returns the updated room with a 200 status.
|
||||
@@ -184,7 +184,7 @@ async def test_update_room_success(client: TestClient, mock_logger: MagicMock) -
|
||||
) as mock_update_room:
|
||||
mock_update_room.return_value = updated_room
|
||||
|
||||
response = client.put(f"/rooms/{room_id}", json=update_data)
|
||||
response = await client.put(f"/rooms/{room_id}", json=update_data)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
@@ -200,7 +200,7 @@ async def test_update_room_success(client: TestClient, mock_logger: MagicMock) -
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.rooms"], indirect=True)
|
||||
async def test_update_room_not_found(
|
||||
client: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of non-existent room in PUT /rooms/{room_id}.
|
||||
|
||||
@@ -222,7 +222,7 @@ async def test_update_room_not_found(
|
||||
) as mock_update_room:
|
||||
mock_update_room.side_effect = ValueError()
|
||||
|
||||
response = client.put(f"/rooms/{room_id}", json=update_data)
|
||||
response = await client.put(f"/rooms/{room_id}", json=update_data)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert "detail" in response.json()
|
||||
@@ -231,7 +231,7 @@ async def test_update_room_not_found(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.rooms"], indirect=True)
|
||||
async def test_delete_room_success(client: TestClient, mock_logger: MagicMock) -> None:
|
||||
async def test_delete_room_success(client: AsyncClient, mock_logger: MagicMock) -> None:
|
||||
"""Test successful deletion of a room via DELETE /rooms/{room_id}.
|
||||
|
||||
Verifies that the endpoint returns a 204 status on successful deletion.
|
||||
@@ -244,7 +244,7 @@ async def test_delete_room_success(client: TestClient, mock_logger: MagicMock) -
|
||||
with patch(
|
||||
"backend.routers.rooms.delete_room", new=AsyncMock()
|
||||
) as mock_delete_room:
|
||||
response = client.delete(f"/rooms/{room_id}")
|
||||
response = await client.delete(f"/rooms/{room_id}")
|
||||
|
||||
assert response.status_code == 204
|
||||
assert response.text == ""
|
||||
@@ -254,7 +254,7 @@ async def test_delete_room_success(client: TestClient, mock_logger: MagicMock) -
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.rooms"], indirect=True)
|
||||
async def test_delete_room_not_found(
|
||||
client: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of non-existent room in DELETE /rooms/{room_id}.
|
||||
|
||||
@@ -270,7 +270,7 @@ async def test_delete_room_not_found(
|
||||
) as mock_delete_room:
|
||||
mock_delete_room.side_effect = ValueError()
|
||||
|
||||
response = client.delete(f"/rooms/{room_id}")
|
||||
response = await client.delete(f"/rooms/{room_id}")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert "detail" in response.json()
|
||||
|
||||
@@ -6,7 +6,7 @@ from unittest.mock import MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
@@ -17,7 +17,7 @@ from backend.models import UserList
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.users"], indirect=True)
|
||||
async def test_read_users_success(
|
||||
client: TestClient, sample_users: UserList, mock_logger: MagicMock
|
||||
client: AsyncClient, sample_users: UserList, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test successful retrieval of all users via GET /users/.
|
||||
|
||||
@@ -31,7 +31,7 @@ async def test_read_users_success(
|
||||
with patch("backend.routers.users.get_users", new=AsyncMock()) as mock_get_users:
|
||||
mock_get_users.return_value = sample_users
|
||||
|
||||
response = client.get("/users/")
|
||||
response = await client.get("/users/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [
|
||||
@@ -43,7 +43,7 @@ async def test_read_users_success(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.users"], indirect=True)
|
||||
async def test_read_users_empty(client: TestClient, mock_logger: MagicMock) -> None:
|
||||
async def test_read_users_empty(client: AsyncClient, mock_logger: MagicMock) -> None:
|
||||
"""Test retrieval of users when none exist via GET /users/.
|
||||
|
||||
Verifies that the endpoint returns an empty list with a 200 status.
|
||||
@@ -55,7 +55,7 @@ async def test_read_users_empty(client: TestClient, mock_logger: MagicMock) -> N
|
||||
with patch("backend.routers.users.get_users", new=AsyncMock()) as mock_get_users:
|
||||
mock_get_users.return_value = []
|
||||
|
||||
response = client.get("/users/")
|
||||
response = await client.get("/users/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
@@ -65,7 +65,7 @@ async def test_read_users_empty(client: TestClient, mock_logger: MagicMock) -> N
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.users"], indirect=True)
|
||||
async def test_read_users_database_error(
|
||||
client: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of database errors in GET /users/.
|
||||
|
||||
@@ -78,7 +78,7 @@ async def test_read_users_database_error(
|
||||
with patch("backend.routers.users.get_users", new=AsyncMock()) as mock_get_users:
|
||||
mock_get_users.side_effect = SQLAlchemyError()
|
||||
|
||||
response = client.get("/users/")
|
||||
response = await client.get("/users/")
|
||||
|
||||
assert response.status_code == 500
|
||||
assert "detail" in response.json()
|
||||
@@ -96,7 +96,7 @@ async def test_read_users_database_error(
|
||||
ids=["user1", "user2"],
|
||||
)
|
||||
async def test_read_user_success(
|
||||
client: TestClient, email: str, expected_name: str, mock_logger: MagicMock
|
||||
client: AsyncClient, email: str, expected_name: str, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test successful retrieval of a user by email via GET /users/{email}.
|
||||
|
||||
@@ -112,7 +112,7 @@ async def test_read_user_success(
|
||||
with patch("backend.routers.users.get_user", new=AsyncMock()) as mock_get_user:
|
||||
mock_get_user.return_value = user
|
||||
|
||||
response = client.get(f"/users/{email}")
|
||||
response = await client.get(f"/users/{email}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"email": email, "name": expected_name}
|
||||
@@ -127,7 +127,7 @@ async def test_read_user_success(
|
||||
ids=["nonexistent_email", "invalid_email"],
|
||||
)
|
||||
async def test_read_user_not_found(
|
||||
client: TestClient, email: str, mock_logger: MagicMock
|
||||
client: AsyncClient, email: str, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of non-existent user in GET /users/{email}.
|
||||
|
||||
@@ -141,7 +141,7 @@ async def test_read_user_not_found(
|
||||
with patch("backend.routers.users.get_user", new=AsyncMock()) as mock_get_user:
|
||||
mock_get_user.side_effect = NoResultFound()
|
||||
|
||||
response = client.get(f"/users/{email}")
|
||||
response = await client.get(f"/users/{email}")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert "detail" in response.json()
|
||||
@@ -151,7 +151,7 @@ async def test_read_user_not_found(
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.routers.users"], indirect=True)
|
||||
async def test_read_user_database_error(
|
||||
client: TestClient, mock_logger: MagicMock
|
||||
client: AsyncClient, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test handling of database errors in GET /users/{email}.
|
||||
|
||||
@@ -165,7 +165,7 @@ async def test_read_user_database_error(
|
||||
with patch("backend.routers.users.get_user", new=AsyncMock()) as mock_get_user:
|
||||
mock_get_user.side_effect = SQLAlchemyError()
|
||||
|
||||
response = client.get(f"/users/{email}")
|
||||
response = await client.get(f"/users/{email}")
|
||||
|
||||
assert response.status_code == 500
|
||||
assert "detail" in response.json()
|
||||
|
||||
Reference in New Issue
Block a user