.. _testing: Testing ======= Backend tests are written with **pytest** and live in ``backend/tests/``. All tests mock the Supabase client to avoid hitting the real database. Test Configuration ------------------ ``backend/pytest.ini``: .. code-block:: ini [pytest] pythonpath = . testpaths = tests python_files = test_*.py asyncio_mode = strict asyncio_default_fixture_loop_scope = function - ``asyncio_mode = strict`` — every async test must be marked with ``@pytest.mark.asyncio`` or use a fixture that handles the event loop. - ``pythonpath = .`` — allows ``from app.xxx import ...`` without installing the package. Running Tests ------------- .. code-block:: bash cd backend # All tests pytest # Single file pytest tests/unit/test_auth_module.py # Single test by name pattern pytest -k "test_signup" # Verbose output pytest -v # With coverage (requires pytest-cov) pytest --cov=app --cov-report=html Directory Structure ------------------- .. code-block:: text backend/tests/ ├── conftest.py # Shared fixtures ├── unit/ │ └── test_auth_module.py # AuthService unit tests (8 tests) └── integration/ └── test_auth.py # API endpoint integration tests (18 tests) Fixtures (``conftest.py``) -------------------------- A session-scoped autouse fixture patches the Supabase client with a ``unittest.mock.MagicMock`` before every test, preventing real database calls. .. code-block:: python @pytest.fixture(autouse=True) def mock_supabase(mocker): mocker.patch("app.core.database.supabase", new=MagicMock()) All tests inherit this fixture automatically — no explicit declaration needed. Unit Tests (``tests/unit/test_auth_module.py``) ------------------------------------------------ Unit tests call ``AuthService`` methods directly and assert on the return values or raised exceptions. The Supabase client calls made inside the service are all intercepted by the autouse mock. .. list-table:: :header-rows: 1 :widths: 40 60 * - Test - What it verifies * - ``test_signup_success`` - Successful signup returns a ``TokenResponse`` with an access token. * - ``test_signup_failure`` - Supabase ``AuthApiError`` is re-raised as ``AuthServiceError``. * - ``test_login_success`` - Valid credentials return a ``TokenResponse``. * - ``test_login_failure`` - Wrong credentials raise ``AuthServiceError`` with status 401. * - ``test_google_oauth_url`` - Returns a dict containing a ``url`` key. * - ``test_refresh_session`` - Valid refresh token returns a new ``TokenResponse``. * - ``test_logout`` - Calls ``supabase.auth.sign_out()`` and returns a success message. * - ``test_get_user_from_token`` - Returns a ``UserResponse`` for a valid access token. * - ``test_update_onboarding`` - Updates ``profiles`` row and returns a success message. Integration Tests (``tests/integration/test_auth.py``) ------------------------------------------------------- Integration tests use FastAPI's ``TestClient`` and mock ``AuthService`` at the router level, so the full HTTP request/response cycle is exercised without real Supabase calls. .. code-block:: python from fastapi.testclient import TestClient from app.main import app client = TestClient(app) .. list-table:: :header-rows: 1 :widths: 40 60 * - Test - What it verifies * - ``test_signup_success`` - ``POST /api/auth/signup`` returns ``201`` and a token response. * - ``test_signup_duplicate`` - Duplicate email returns ``400``. * - ``test_login_success`` - ``POST /api/auth/login`` returns ``200`` and a token response. * - ``test_login_wrong_password`` - Wrong password returns ``400``. * - ``test_google_oauth_url`` - ``POST /api/auth/google/url`` returns ``200`` with ``url`` field. * - ``test_google_callback`` - ``POST /api/auth/google/callback`` with valid code returns ``200``. * - ``test_google_token`` - ``POST /api/auth/google/token`` with valid id_token returns ``200``. * - ``test_refresh_token`` - ``POST /api/auth/refresh`` with valid refresh token returns ``200``. * - ``test_logout`` - ``POST /api/auth/logout`` with valid auth header returns ``200``. * - ``test_get_current_user`` - ``GET /api/auth/me`` returns ``200`` and user data. * - ``test_get_current_user_no_auth`` - Missing header returns ``403``. * - ``test_get_current_user_invalid_token`` - Invalid token returns ``401``. * - ``test_update_onboarding`` - ``PUT /api/auth/onboarding`` with valid body returns ``200``. * - ``test_update_onboarding_no_auth`` - Missing auth returns ``403``. * - … (18 total) - Writing New Tests ----------------- Unit test skeleton ~~~~~~~~~~~~~~~~~~ .. code-block:: python import pytest from unittest.mock import MagicMock from app.modules.auth.service import AuthService, AuthServiceError @pytest.mark.asyncio async def test_my_service_method(mock_supabase): # Arrange — configure the mock return value mock_supabase.auth.some_method.return_value = MagicMock( session=MagicMock(access_token="token123"), user=MagicMock(id="uuid", email="a@b.com"), ) # Act result = await AuthService.some_method("arg") # Assert assert result.access_token == "token123" Integration test skeleton ~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python import pytest from fastapi.testclient import TestClient from unittest.mock import patch, MagicMock from app.main import app client = TestClient(app) def test_my_endpoint(mocker): mocker.patch( "app.modules.auth.router.AuthService.some_method", return_value=MagicMock(access_token="token123"), ) response = client.post("/api/auth/some-endpoint", json={"key": "value"}) assert response.status_code == 200 assert response.json()["access_token"] == "token123" Test Dependencies ----------------- .. list-table:: :header-rows: 1 :widths: 30 15 55 * - Package - Version - Purpose * - ``pytest`` - 9.0.2 - Test runner * - ``pytest-asyncio`` - 1.3.0 - Async test support (``asyncio_mode = strict``) * - ``pytest-mock`` - 3.15.1 - ``mocker`` fixture for ``unittest.mock`` integration * - ``httpx`` - (transitive) - Required by FastAPI ``TestClient``