From c5415725b03f69e08d80f8486773e3ee4120a57c Mon Sep 17 00:00:00 2001 From: Saturday-boyi <2174084306@qq.com> Date: Sat, 19 Sep 2026 18:16:26 +0800 Subject: [PATCH] fix: return empty election lists with HTTP 200 --- src/elections/urls.py | 7 +--- tests/unit/test_elections.py | 71 ++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 tests/unit/test_elections.py diff --git a/src/elections/urls.py b/src/elections/urls.py index 68e34e9..e0005a6 100644 --- a/src/elections/urls.py +++ b/src/elections/urls.py @@ -74,9 +74,8 @@ def _raise_if_bad_election_data( @router.get( "", - description="Return a list of all elections, their statuses and nominees (if requested)", + description="Return a list of all elections, their statuses and nominees (if requested), or an empty list if none exist", response_model=list[ElectionResponse], - responses={status.HTTP_404_NOT_FOUND: {"description": "No election found", "model": DetailModel}}, operation_id="get_all_elections", ) async def list_elections( @@ -91,15 +90,11 @@ async def list_elections( election_responses = await elections.crud.get_all_elections_with_nominees( db_session, current_time, has_permission ) - if not election_responses: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="no election found") election_metadata_list = [ election.model_dump(mode="json", exclude_none=True) for election in election_responses ] else: election_list = await elections.crud.get_all_elections(db_session) - if election_list is None or len(election_list) == 0: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="no election found") election_metadata_list = [] for election in election_list: if has_permission: diff --git a/tests/unit/test_elections.py b/tests/unit/test_elections.py new file mode 100644 index 0000000..0104fa5 --- /dev/null +++ b/tests/unit/test_elections.py @@ -0,0 +1,71 @@ +from unittest.mock import AsyncMock + +import pytest +import pytest_asyncio +from fastapi import FastAPI, status +from httpx import ASGITransport, AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession + +import elections.crud +from database import get_db_session +from elections.urls import router + +pytestmark = pytest.mark.unit + + +@pytest_asyncio.fixture +async def election_client(): + app = FastAPI() + app.include_router(router) + + async def override_get_db_session(): + yield AsyncMock(spec=AsyncSession) + + app.dependency_overrides[get_db_session] = override_get_db_session + async with AsyncClient(transport=ASGITransport(app), base_url="http://test") as client: + yield client + + +@pytest.mark.parametrize( + ("query", "with_nominees"), + [("", False), ("?with_nominees=false", False), ("?with_nominees=true", True)], +) +async def test__empty_election_list_returns_ok( + election_client: AsyncClient, monkeypatch: pytest.MonkeyPatch, query, with_nominees +): + get_all = AsyncMock(return_value=[]) + get_with_nominees = AsyncMock(return_value=[]) + monkeypatch.setattr(elections.crud, "get_all_elections", get_all) + monkeypatch.setattr(elections.crud, "get_all_elections_with_nominees", get_with_nominees) + + response = await election_client.get(f"/election{query}") + + assert response.status_code == status.HTTP_200_OK + assert response.json() == [] + if with_nominees: + get_with_nominees.assert_awaited_once() + get_all.assert_not_awaited() + else: + get_all.assert_awaited_once() + get_with_nominees.assert_not_awaited() + + +async def test__missing_election_still_returns_not_found(election_client: AsyncClient, monkeypatch: pytest.MonkeyPatch): + get_election = AsyncMock(return_value=None) + monkeypatch.setattr(elections.crud, "get_election", get_election) + + response = await election_client.get("/election/missing") + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.json() == {"detail": "election with slug missing does not exist"} + get_election.assert_awaited_once() + + +async def test__election_list_schema_does_not_advertise_not_found(election_client: AsyncClient): + response = await election_client.get("/openapi.json") + assert response.status_code == status.HTTP_200_OK + paths = response.json()["paths"] + + assert "200" in paths["/election"]["get"]["responses"] + assert "404" not in paths["/election"]["get"]["responses"] + assert "404" in paths["/election/{election_name}"]["get"]["responses"]