From 2f47b389b6b5ca5154eef32015047ed1e1724872 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Sun, 24 Aug 2025 16:37:21 -0400 Subject: [PATCH] Added tests for the users router. Signed-off-by: Cliff Hill --- backend/src/backend/routers/users.py | 16 ++- backend/tests/routers/__init__.py | 1 + backend/tests/routers/test_users.py | 166 +++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 backend/tests/routers/__init__.py create mode 100644 backend/tests/routers/test_users.py diff --git a/backend/src/backend/routers/users.py b/backend/src/backend/routers/users.py index d0356dc3..fef86c9c 100644 --- a/backend/src/backend/routers/users.py +++ b/backend/src/backend/routers/users.py @@ -4,6 +4,7 @@ from fastapi import APIRouter from fastapi import HTTPException from pydantic import EmailStr from sqlalchemy.exc import NoResultFound +from sqlalchemy.exc import SQLAlchemyError from backend.dependencies.db import DBSession from backend.schemas.users import UserResponse @@ -17,7 +18,14 @@ router = APIRouter(prefix="/users", tags=["users"]) @router.get("/", response_model=list[UserResponse]) async def read_users(session: DBSession): """Endpoint to retrieve all users.""" - users = await get_users(session) + try: + users = await get_users(session) + except SQLAlchemyError as e: + raise HTTPException(status_code=500, detail="Database error occurred") from e + except Exception as e: + raise HTTPException( + status_code=500, detail="An unexpected error occurred" + ) from e return users @@ -30,4 +38,10 @@ async def read_user(email: EmailStr, session: DBSession): raise HTTPException( status_code=404, detail=f"User with email {email} not found" ) from e + except SQLAlchemyError as e: + raise HTTPException(status_code=500, detail="Database error occurred") from e + except Exception as e: + raise HTTPException( + status_code=500, detail="An unexpected error occurred" + ) from e return user diff --git a/backend/tests/routers/__init__.py b/backend/tests/routers/__init__.py new file mode 100644 index 00000000..d6e7f141 --- /dev/null +++ b/backend/tests/routers/__init__.py @@ -0,0 +1 @@ +"""Tests for the routers package.""" diff --git a/backend/tests/routers/test_users.py b/backend/tests/routers/test_users.py new file mode 100644 index 00000000..c905441c --- /dev/null +++ b/backend/tests/routers/test_users.py @@ -0,0 +1,166 @@ +"""Unit tests for the backend.routers.users module.""" + +import logging +from collections.abc import Generator +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 sqlalchemy.exc import SQLAlchemyError + +from backend.main import app +from backend.models import User +from backend.services.users import UserList + +from ..conftest import MockLogger + + +@pytest.fixture(autouse=True) +def mock_logger() -> Generator[MockLogger, None, None]: + """Fixture to mock the logger used in the users service.""" + mock_logger_instance = MagicMock(spec=logging.Logger) + with patch("backend.services.users.logger", mock_logger_instance): + yield mock_logger_instance + + +@pytest.fixture +def sample_users() -> UserList: + """Fixture to provide sample User objects for testing.""" + return [ + User(email="user1@example.com", name="User One"), + User(email="user2@example.com", name="User Two"), + ] + + +@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 +) -> None: + """Test successful retrieval of all users via GET /users/. + + Verifies that the endpoint returns the expected list of users with a 200 status. + """ + with patch("backend.routers.users.get_users", new=AsyncMock()) as mock_get_users: + mock_get_users.return_value = sample_users + + response = client.get("/users/") + + assert response.status_code == 200 + assert response.json() == [ + {"email": "user1@example.com", "name": "User One"}, + {"email": "user2@example.com", "name": "User Two"}, + ] + mock_get_users.assert_called_once() + + +@pytest.mark.asyncio +async def test_read_users_empty(client: TestClient, mock_logger: MockLogger) -> None: + """Test retrieval of users when none exist via GET /users/. + + Verifies that the endpoint returns an empty list with a 200 status. + """ + with patch("backend.routers.users.get_users", new=AsyncMock()) as mock_get_users: + mock_get_users.return_value = [] + + response = client.get("/users/") + + assert response.status_code == 200 + assert response.json() == [] + mock_get_users.assert_called_once() + + +@pytest.mark.asyncio +async def test_read_users_database_error( + client: TestClient, mock_logger: MockLogger +) -> None: + """Test handling of database errors in GET /users/. + + 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") + + response = client.get("/users/") + + assert response.status_code == 500 + assert response.json() == {"detail": "Database error occurred"} + mock_get_users.assert_called_once() + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "email, expected_name", + [ + ("user1@example.com", "User One"), + ("user2@example.com", "User Two"), + ], + ids=["user1", "user2"], +) +async def test_read_user_success( + client: TestClient, email: str, expected_name: str, mock_logger: MockLogger +) -> None: + """Test successful retrieval of a user by email via GET /users/{email}. + + Verifies that the endpoint returns the correct user with a 200 status. + """ + user = User(email=email, name=expected_name) + 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}") + + assert response.status_code == 200 + assert response.json() == {"email": email, "name": expected_name} + mock_get_user.assert_called_once_with(ANY, email) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "email", + ["nonexistent@example.com", "invalid@domain.com"], + ids=["nonexistent_email", "invalid_email"], +) +async def test_read_user_not_found( + client: TestClient, email: str, mock_logger: MockLogger +) -> None: + """Test handling of non-existent user in GET /users/{email}. + + 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") + + response = client.get(f"/users/{email}") + + assert response.status_code == 404 + assert response.json() == {"detail": f"User with email {email} not found"} + mock_get_user.assert_called_once_with(ANY, email) + + +@pytest.mark.asyncio +async def test_read_user_database_error( + client: TestClient, mock_logger: MockLogger +) -> None: + """Test handling of database errors in GET /users/{email}. + + Verifies that the endpoint returns a 500 status on database failure. + """ + 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") + + response = client.get(f"/users/{email}") + + assert response.status_code == 500 + assert response.json() == {"detail": "Database error occurred"} + mock_get_user.assert_called_once_with(ANY, email)