From bd567ab0c65d8164f5d154c81f7be957b34e1fa5 Mon Sep 17 00:00:00 2001 From: Ian Beck Date: Fri, 29 Aug 2025 14:20:40 -0700 Subject: [PATCH 1/2] Force pagination to max out at the maximum limit If the user passes in a number in excess of the max limit, we just use the max. This ensures that the endpoint returns without a validation error getting generated by the internal Pydantic logic. Closes #88. --- api/schemas/pagination.py | 18 +++++++++++++++--- api/tests/cards/test_cards.py | 13 ++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/api/schemas/pagination.py b/api/schemas/pagination.py index 8e510da..0175b78 100644 --- a/api/schemas/pagination.py +++ b/api/schemas/pagination.py @@ -1,7 +1,7 @@ from enum import Enum -from typing import Any +from typing import Any, Annotated -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, BeforeValidator from api.environment import settings @@ -13,10 +13,22 @@ class PaginationOrderOptions(str, Enum): desc = "desc" +def limit_to_max_pagination_value(value: Any) -> int: + """Ensures that users cannot exceed the maximum pagination limit + + Needs to be run prior to normal validation to prevent a validation error from getting thrown + by the framework. + """ + value = int(value) + if value > settings.pagination_max_limit: + value = settings.pagination_max_limit + return value + + class PaginationOptions(BaseModel): """Query string options to adjust pagination""" - limit: int = Field( + limit: Annotated[int, BeforeValidator(limit_to_max_pagination_value)] = Field( settings.pagination_default_limit, gt=0, le=settings.pagination_max_limit ) offset: int = 0 diff --git a/api/tests/cards/test_cards.py b/api/tests/cards/test_cards.py index e3d4c77..ec3658c 100644 --- a/api/tests/cards/test_cards.py +++ b/api/tests/cards/test_cards.py @@ -6,7 +6,7 @@ from api.models.release import Release, UserRelease from api.services.card import create_card -from ..utils import create_user_token +from ..utils import create_user_token, monkeypatch_settings def names_from_results(response): @@ -210,6 +210,17 @@ def test_pagination_paging(client: TestClient, session: db.Session): assert data["next"] is not None +def test_pagination_exceed_limit(client: TestClient, session: db.Session, monkeypatch): + """Exceeding the max pagination limit results in getting the max limit back""" + monkeypatch_settings(monkeypatch, {"pagination_max_limit": 4, "pagination_default_limit": 2}) + response = client.get("/v2/cards", params={"limit": 5}) + assert response.status_code == 200 + data = response.json() + assert data["count"] == 10 + assert len(data["results"]) == 4 + assert data["next"] is not None + + def test_pagination_negative_offsets(client: TestClient, session: db.Session): """A number that would result in a negative offset must result in no offset""" # Verify that previous links work properly with arbitrary offsets that would make them negative From 7f750f40b94ffb16d6142afb1fd0ddc61e44ec14 Mon Sep 17 00:00:00 2001 From: Ian Beck Date: Fri, 29 Aug 2025 14:37:39 -0700 Subject: [PATCH 2/2] Fixed formatting --- api/schemas/pagination.py | 4 ++-- api/tests/cards/test_cards.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/api/schemas/pagination.py b/api/schemas/pagination.py index 0175b78..29c5717 100644 --- a/api/schemas/pagination.py +++ b/api/schemas/pagination.py @@ -1,7 +1,7 @@ from enum import Enum -from typing import Any, Annotated +from typing import Annotated, Any -from pydantic import BaseModel, Field, BeforeValidator +from pydantic import BaseModel, BeforeValidator, Field from api.environment import settings diff --git a/api/tests/cards/test_cards.py b/api/tests/cards/test_cards.py index ec3658c..e0fd2f8 100644 --- a/api/tests/cards/test_cards.py +++ b/api/tests/cards/test_cards.py @@ -212,7 +212,9 @@ def test_pagination_paging(client: TestClient, session: db.Session): def test_pagination_exceed_limit(client: TestClient, session: db.Session, monkeypatch): """Exceeding the max pagination limit results in getting the max limit back""" - monkeypatch_settings(monkeypatch, {"pagination_max_limit": 4, "pagination_default_limit": 2}) + monkeypatch_settings( + monkeypatch, {"pagination_max_limit": 4, "pagination_default_limit": 2} + ) response = client.get("/v2/cards", params={"limit": 5}) assert response.status_code == 200 data = response.json()