Files
conference-room-booking-system/backend/tests/conftest.py

265 lines
7.5 KiB
Python

"""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
from datetime import timezone
from pathlib import Path
from unittest.mock import AsyncMock
from unittest.mock import MagicMock
from unittest.mock import patch
import pytest
import pytest_asyncio
from dotenv import load_dotenv
from fastapi.responses import JSONResponse
from httpx import ASGITransport
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
# Ensure .env is loaded from base or parent directory before any project
# imports
def _load_env_from_base_or_parent() -> None:
base = Path(__file__).parent.parent # project root
env_path = base / ".env"
if env_path.exists():
load_dotenv(dotenv_path=env_path)
else:
parent_env = base.parent / ".env"
if parent_env.exists():
load_dotenv(dotenv_path=parent_env)
_load_env_from_base_or_parent()
# Project imports must be placed after this:
from backend.main import app # noqa: E402
from backend.models import Booking # noqa: E402
from backend.models import BookingList # noqa: E402
from backend.models import Invitee # noqa: E402
from backend.models import Room # noqa: E402
from backend.models import RoomList # noqa: E402
from backend.models import User # noqa: E402
from backend.models import UserList # noqa: E402
@pytest.fixture
def async_session() -> AsyncSession:
"""Fixture to provide a mocked AsyncSession for database interactions.
Returns:
A mocked AsyncSession object for database interactions.
"""
return AsyncMock(spec=AsyncSession)
@pytest_asyncio.fixture
async def client() -> AsyncGenerator[AsyncClient, None]:
"""Fixture to provide an AsyncClient for async endpoint testing.
Yields:
An HTTPX AsyncClient instance for making async HTTP requests.
"""
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
@pytest.fixture
def mock_logger(request: pytest.FixtureRequest) -> Generator[MagicMock, None, None]:
"""Fixture to mock a logger for a specified module path.
Args:
request: Pytest request object, used to pass the module path for patching.
Yields:
MagicMock: A mocked logger instance for the specified module path.
Raises:
ValueError: If the module path parameter is not provided.
Usage:
To use this fixture in a test module, parameterize it with the module path
where the logger is defined.
"""
# Require explicit module path for logger
if not hasattr(request, "param"):
raise ValueError(
"mock_logger fixture requires a module path parameter, e.g."
" 'backend.routers.invitees'"
)
module_path = request.param + ".logger"
mock_logger_instance = MagicMock(spec=logging.Logger)
with patch(module_path, mock_logger_instance):
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.
Returns:
A list of sample User objects.
"""
return [
User(email="user1@example.com", name="User One"),
User(email="user2@example.com", name="User Two"),
]
@pytest.fixture
def sample_rooms() -> RoomList:
"""Fixture to provide sample Room objects for testing.
Returns:
A list of sample Room objects.
"""
return [
Room(
id=1,
name="Room 1",
location="Building A",
equipment="Projector",
capacity=10,
),
Room(
id=2,
name="Room 2",
location="Building B",
equipment="Whiteboard",
capacity=15,
),
]
@pytest.fixture
def sample_room(sample_rooms: RoomList) -> Room:
"""Fixture to provide a sample Room object for testing.
Args:
sample_rooms: The list of sample Room objects.
Returns:
A single sample Room object.
"""
return sample_rooms[0]
@pytest.fixture
def sample_bookings(sample_room: Room) -> BookingList:
"""Fixture to provide sample Booking objects for testing.
Args:
sample_room: The sample Room object to attach to bookings.
Returns:
A list of sample Booking objects with attached room relationships.
"""
booking1 = Booking(
room_id=1,
start_time=datetime.now(timezone.utc) + timedelta(hours=3),
end_time=datetime.now(timezone.utc) + timedelta(hours=4),
title="Sample Booking 1",
)
booking1.id = 1 # Manually set ID for testing purposes
booking1.room = sample_room # Attach room relationship
booking2 = Booking(
room_id=1,
start_time=datetime.now(timezone.utc) + timedelta(hours=12),
end_time=datetime.now(timezone.utc) + timedelta(hours=13),
title="Sample Booking 2",
)
booking2.id = 2 # Manually set ID for testing purposes
booking2.room = sample_room # Attach room relationship
return [booking1, booking2]
@pytest.fixture
def sample_booking(sample_bookings: BookingList) -> Booking:
"""Fixture to provide a sample Booking object for testing.
Args:
sample_bookings: The list of sample Booking objects.
Returns:
A single sample Booking object.
"""
return sample_bookings[0]
@pytest.fixture
def sample_invitees(sample_booking: Booking, sample_users: UserList) -> list[Invitee]:
"""Fixture to provide sample Invitee objects for testing.
Args:
sample_booking: The sample Booking object to attach to invitees.
sample_users: The list of sample User objects to attach to invitees.
Returns:
A list of sample Invitee objects with attached booking and user relationships.
"""
invitee1 = Invitee(booking_id=1, user_email="user1@example.com")
invitee1.id = 1
invitee1.booking = sample_booking
invitee1.user = sample_users[0]
invitee2 = Invitee(booking_id=1, user_email="user2@example.com")
invitee2.id = 2
invitee2.booking = sample_booking
invitee2.user = sample_users[1]
return [invitee1, invitee2]
@pytest.fixture
def sample_invitee(sample_invitees: list[Invitee]) -> Invitee:
"""Fixture to provide a sample Invitee object for testing.
Args:
sample_invitees: The list of sample Invitee objects.
Returns:
A single sample Invitee object.
"""
return sample_invitees[0]
@pytest.fixture(autouse=True)
def override_exception_handler() -> Generator[None, None, None]:
"""Override the default exception handler for testing.
Yields:
None
"""
from backend.main import app
# Save the original handler
original_handler = app.exception_handlers.get(Exception)
from fastapi import Request
async def test_exception_handler(request: Request, exc: Exception) -> JSONResponse:
return JSONResponse(
status_code=500, content={"detail": "Internal server error"}
)
app.add_exception_handler(Exception, test_exception_handler)
yield
# Restore the original handler
if original_handler is not None:
app.add_exception_handler(Exception, original_handler)
else:
del app.exception_handlers[Exception]