From 11e3b3c82bde75a6d83aaea00857153caa91ea0f Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Wed, 27 Aug 2025 15:51:05 -0400 Subject: [PATCH] Lots of fixes to lots of things. Signed-off-by: Cliff Hill --- backend/poetry.lock | 2 +- backend/pyproject.toml | 1 + backend/src/backend/main.py | 4 +- backend/src/backend/routers/bookings.py | 31 ++-- backend/src/backend/routers/rooms.py | 67 ++++++++ backend/src/backend/services/bookings.py | 206 +++++++++++++++-------- backend/tests/conftest.py | 43 ++--- backend/tests/routers/test_bookings.py | 70 ++++---- backend/tests/routers/test_invitees.py | 49 +++--- backend/tests/routers/test_rooms.py | 40 ++--- backend/tests/routers/test_users.py | 26 +-- 11 files changed, 340 insertions(+), 199 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index 83308b25..d142d927 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -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" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 44dbbfef..aa108bf6 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -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" diff --git a/backend/src/backend/main.py b/backend/src/backend/main.py index ff11b0e9..9c9099a1 100644 --- a/backend/src/backend/main.py +++ b/backend/src/backend/main.py @@ -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 diff --git a/backend/src/backend/routers/bookings.py b/backend/src/backend/routers/bookings.py index c3215304..9d8be42d 100644 --- a/backend/src/backend/routers/bookings.py +++ b/backend/src/backend/routers/bookings.py @@ -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") diff --git a/backend/src/backend/routers/rooms.py b/backend/src/backend/routers/rooms.py index f3d23247..1e341c1d 100644 --- a/backend/src/backend/routers/rooms.py +++ b/backend/src/backend/routers/rooms.py @@ -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: diff --git a/backend/src/backend/services/bookings.py b/backend/src/backend/services/bookings.py index 09caac2a..7e6278f8 100644 --- a/backend/src/backend/services/bookings.py +++ b/backend/src/backend/services/bookings.py @@ -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() diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index b5a668fc..b8ab94c6 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -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. diff --git a/backend/tests/routers/test_bookings.py b/backend/tests/routers/test_bookings.py index 53ad1e0b..501dc5fa 100644 --- a/backend/tests/routers/test_bookings.py +++ b/backend/tests/routers/test_bookings.py @@ -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) diff --git a/backend/tests/routers/test_invitees.py b/backend/tests/routers/test_invitees.py index 941b8bf2..146257a4 100644 --- a/backend/tests/routers/test_invitees.py +++ b/backend/tests/routers/test_invitees.py @@ -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) diff --git a/backend/tests/routers/test_rooms.py b/backend/tests/routers/test_rooms.py index ea03c6d9..d3ca36e2 100644 --- a/backend/tests/routers/test_rooms.py +++ b/backend/tests/routers/test_rooms.py @@ -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() diff --git a/backend/tests/routers/test_users.py b/backend/tests/routers/test_users.py index d95fc265..f78bc257 100644 --- a/backend/tests/routers/test_users.py +++ b/backend/tests/routers/test_users.py @@ -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()