Added tests for rooms endpoints.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2025-08-24 21:20:24 -04:00
parent 3b525c3eec
commit ddc80e361c
7 changed files with 286 additions and 26 deletions

View File

@@ -38,9 +38,3 @@ app.include_router(bookings.router)
app.include_router(invitees.router)
setup_logging(app)
@app.get("/")
async def root():
"""Root endpoint to verify the service is running."""
return {"message": "Welcome to the Numinar Coding Project Backend!"}

View File

@@ -41,7 +41,7 @@ class Room(Base):
__tablename__ = "rooms"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(index=True, unique=True)
location: Mapped[str]
equipment: Mapped[str]

View File

@@ -56,7 +56,7 @@ async def read_room(room_id: int, session: DBSession):
room = await get_room(session, room_id)
logger.info(f"Successfully retrieved room with id: {room_id}")
return room
except NoResultFound as e:
except (NoResultFound, ValueError) as e:
logger.warning(f"Room with id {room_id} not found")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -113,7 +113,7 @@ async def update_existing_room(
updated_room = await update_room(session, room_id, **room_params)
logger.info(f"Successfully updated room with id: {room_id}")
return updated_room
except NoResultFound as e:
except (NoResultFound, ValueError) as e:
logger.warning(f"Room with id {room_id} not found")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -141,7 +141,7 @@ async def delete_existing_room(room_id: int, session: DBSession):
try:
await delete_room(session, room_id)
logger.info(f"Successfully deleted room with id: {room_id}")
except NoResultFound as e:
except (NoResultFound, ValueError) as e:
logger.warning(f"Room with id {room_id} not found")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,

View File

@@ -7,6 +7,7 @@ from pydantic import ConfigDict
class RoomBase(BaseModel):
"""Base schema for Room."""
id: int
name: str
location: str
equipment: str
@@ -22,6 +23,7 @@ class RoomCreate(RoomBase):
class RoomUpdate(BaseModel):
"""Schema for updating an existing Room."""
id: int | None = None
name: str | None = None
location: str | None = None
equipment: str | None = None
@@ -31,8 +33,6 @@ class RoomUpdate(BaseModel):
class RoomResponse(RoomBase):
"""Response schema for Room, including ID."""
id: int
"""Response schema for Room."""
model_config = ConfigDict(from_attributes=True)

View File

@@ -4,8 +4,11 @@ from unittest.mock import AsyncMock
from unittest.mock import MagicMock
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.ext.asyncio import AsyncSession
from backend.main import app
type MockLogger = MagicMock
@@ -14,3 +17,9 @@ type MockLogger = MagicMock
def async_session() -> AsyncSession:
"""Fixture to provide a mocked AsyncSession for database interactions."""
return AsyncMock(spec=AsyncSession)
@pytest.fixture
def client() -> TestClient:
"""Fixture to provide a FastAPI TestClient for testing endpoints."""
return TestClient(app)

View File

@@ -0,0 +1,264 @@
"""Unit tests for the backend.routers.rooms module."""
import logging
from collections.abc import Generator
from typing import Any
from unittest.mock import ANY
from unittest.mock import AsyncMock
from unittest.mock import MagicMock
from unittest.mock import patch
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.exc import NoResultFound
from backend.models import Room
from backend.services.rooms import RoomList
from ..conftest import MockLogger
@pytest.fixture(autouse=True)
def mock_logger() -> Generator[MockLogger, None, None]:
"""Fixture to mock the logger used in the rooms service."""
mock_logger_instance = MagicMock(spec=logging.Logger)
with patch("backend.routers.rooms.logger", mock_logger_instance):
yield mock_logger_instance
@pytest.fixture
def sample_rooms() -> RoomList:
"""Fixture to provide sample Room objects for testing."""
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.mark.asyncio
async def test_get_rooms_success(
client: TestClient, sample_rooms: RoomList, mock_logger: MockLogger
) -> None:
"""Test successful retrieval of all rooms via GET /rooms/.
Verifies that the endpoint returns the expected list of rooms with a 200 status.
"""
with patch("backend.routers.rooms.get_rooms", new=AsyncMock()) as mock_get_rooms:
mock_get_rooms.return_value = sample_rooms
response = client.get("/rooms/")
assert response.status_code == 200
assert response.json() == [
{
"id": 1,
"name": "Room 1",
"location": "Building A",
"equipment": "Projector",
"capacity": 10,
},
{
"id": 2,
"name": "Room 2",
"location": "Building B",
"equipment": "Whiteboard",
"capacity": 15,
},
]
mock_get_rooms.assert_called_once()
@pytest.mark.asyncio
async def test_get_rooms_empty(client: TestClient, mock_logger: MockLogger) -> None:
"""Test retrieval of rooms when none exist via GET /rooms/.
Verifies that the endpoint returns an empty list with a 200 status.
"""
with patch("backend.routers.rooms.get_rooms", new=AsyncMock()) as mock_get_rooms:
mock_get_rooms.return_value = []
response = client.get("/rooms/")
assert response.status_code == 200
assert response.json() == []
mock_get_rooms.assert_called_once()
@pytest.mark.asyncio
async def test_get_room_success(client: TestClient, mock_logger: MockLogger) -> 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.
"""
room_id = 1
room = Room(
id=room_id,
name="Room 1",
location="Building A",
equipment="Projector",
capacity=10,
)
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}")
assert response.status_code == 200
assert response.json() == {
"id": room_id,
"name": "Room 1",
"location": "Building A",
"equipment": "Projector",
"capacity": 10,
}
mock_get_room.assert_called_once_with(ANY, room_id)
@pytest.mark.asyncio
async def test_get_room_not_found(client: TestClient, mock_logger: MockLogger) -> 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.
"""
room_id = 999
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}")
assert response.status_code == 404
assert "detail" in response.json()
mock_get_room.assert_called_once_with(ANY, room_id)
@pytest.mark.asyncio
async def test_new_room_success(client: TestClient, mock_logger: MockLogger) -> None:
"""Test successful creation of a new room via POST /rooms/.
Verifies that the endpoint returns the created room with a 201 status.
"""
room_data: dict[str, Any] = {
"id": 3,
"name": "Room 3",
"location": "Building C",
"equipment": "TV",
"capacity": 20,
}
created_room = Room(**room_data)
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)
assert response.status_code == 201
assert response.json() == room_data
mock_new_room.assert_called_once()
@pytest.mark.asyncio
async def test_update_room_success(client: TestClient, mock_logger: MockLogger) -> None:
"""Test successful update of a room via PUT /rooms/{room_id}.
Verifies that the endpoint returns the updated room with a 200 status.
"""
room_id = 1
update_data: dict[str, Any] = {
"name": "Updated Room",
"location": "Building D",
"equipment": "Monitor",
"capacity": 25,
}
updated_room = Room(id=room_id, **update_data)
with patch(
"backend.routers.rooms.update_room", new=AsyncMock()
) as mock_update_room:
mock_update_room.return_value = updated_room
response = client.put(f"/rooms/{room_id}", json=update_data)
assert response.status_code == 200
assert response.json() == {
"id": room_id,
"name": "Updated Room",
"location": "Building D",
"equipment": "Monitor",
"capacity": 25,
}
mock_update_room.assert_called_once_with(ANY, room_id, **update_data)
@pytest.mark.asyncio
async def test_update_room_not_found(
client: TestClient, mock_logger: MockLogger
) -> None:
"""Test handling of non-existent room in PUT /rooms/{room_id}.
Verifies that the endpoint returns a 404 status when the room is not found.
"""
room_id = 999
update_data: dict[str, Any] = {
"name": "Updated Room",
"location": "Building D",
"equipment": "Monitor",
"capacity": 25,
}
with patch(
"backend.routers.rooms.update_room", new=AsyncMock()
) as mock_update_room:
mock_update_room.side_effect = ValueError()
response = client.put(f"/rooms/{room_id}", json=update_data)
assert response.status_code == 404
assert "detail" in response.json()
mock_update_room.assert_called_once_with(ANY, room_id, **update_data)
@pytest.mark.asyncio
async def test_delete_room_success(client: TestClient, mock_logger: MockLogger) -> None:
"""Test successful deletion of a room via DELETE /rooms/{room_id}.
Verifies that the endpoint returns a 204 status on successful deletion.
"""
room_id = 1
with patch(
"backend.routers.rooms.delete_room", new=AsyncMock()
) as mock_delete_room:
response = client.delete(f"/rooms/{room_id}")
assert response.status_code == 204
assert response.text == ""
mock_delete_room.assert_called_once_with(ANY, room_id)
@pytest.mark.asyncio
async def test_delete_room_not_found(
client: TestClient, mock_logger: MockLogger
) -> None:
"""Test handling of non-existent room in DELETE /rooms/{room_id}.
Verifies that the endpoint returns a 404 status when the room is not found.
"""
room_id = 999
with patch(
"backend.routers.rooms.delete_room", new=AsyncMock()
) as mock_delete_room:
mock_delete_room.side_effect = ValueError()
response = client.delete(f"/rooms/{room_id}")
assert response.status_code == 404
assert "detail" in response.json()
mock_delete_room.assert_called_once_with(ANY, room_id)

View File

@@ -12,7 +12,6 @@ from fastapi.testclient import TestClient
from sqlalchemy.exc import NoResultFound
from sqlalchemy.exc import SQLAlchemyError
from backend.main import app
from backend.models import User
from backend.services.users import UserList
@@ -36,12 +35,6 @@ def sample_users() -> UserList:
]
@pytest.fixture
def client() -> TestClient:
"""Fixture to provide a FastAPI TestClient for testing endpoints."""
return TestClient(app)
@pytest.mark.asyncio
async def test_read_users_success(
client: TestClient, sample_users: UserList, mock_logger: MockLogger
@@ -88,12 +81,12 @@ async def test_read_users_database_error(
Verifies that the endpoint returns a 500 status on database failure.
"""
with patch("backend.routers.users.get_users", new=AsyncMock()) as mock_get_users:
mock_get_users.side_effect = SQLAlchemyError("Database error")
mock_get_users.side_effect = SQLAlchemyError()
response = client.get("/users/")
assert response.status_code == 500
assert response.json() == {"detail": "Database error occurred"}
assert "detail" in response.json()
mock_get_users.assert_called_once()
@@ -138,12 +131,12 @@ async def test_read_user_not_found(
Verifies that the endpoint returns a 404 status when the user is not found.
"""
with patch("backend.routers.users.get_user", new=AsyncMock()) as mock_get_user:
mock_get_user.side_effect = NoResultFound(f"User with email {email} not found")
mock_get_user.side_effect = NoResultFound()
response = client.get(f"/users/{email}")
assert response.status_code == 404
assert response.json() == {"detail": f"User with email {email} not found"}
assert "detail" in response.json()
mock_get_user.assert_called_once_with(ANY, email)
@@ -157,10 +150,10 @@ async def test_read_user_database_error(
"""
email = "user1@example.com"
with patch("backend.routers.users.get_user", new=AsyncMock()) as mock_get_user:
mock_get_user.side_effect = SQLAlchemyError("Database error")
mock_get_user.side_effect = SQLAlchemyError()
response = client.get(f"/users/{email}")
assert response.status_code == 500
assert response.json() == {"detail": "Database error occurred"}
assert "detail" in response.json()
mock_get_user.assert_called_once_with(ANY, email)