Pagination contract is not standardized across endpoints
This commit is contained in:
@@ -85,7 +85,7 @@ def _make_import_result() -> ImportRunResult:
|
||||
|
||||
def _make_log_response() -> ImportLogListResponse:
|
||||
return ImportLogListResponse(
|
||||
items=[], total=0, page=1, page_size=50, total_pages=1
|
||||
items=[], total=0, page=1, page_size=50
|
||||
)
|
||||
|
||||
|
||||
@@ -457,10 +457,10 @@ class TestImportLog:
|
||||
assert resp.status_code == 200
|
||||
|
||||
async def test_log_response_shape(self, bl_client: AsyncClient) -> None:
|
||||
"""Log response has items, total, page, page_size, total_pages."""
|
||||
"""Log response has items, total, page, page_size."""
|
||||
resp = await bl_client.get("/api/blocklists/log")
|
||||
body = resp.json()
|
||||
for key in ("items", "total", "page", "page_size", "total_pages"):
|
||||
for key in ("items", "total", "page", "page_size"):
|
||||
assert key in body
|
||||
|
||||
async def test_log_empty_when_no_runs(self, bl_client: AsyncClient) -> None:
|
||||
|
||||
@@ -529,7 +529,6 @@ class TestImportLogPagination:
|
||||
assert resp.total == 0
|
||||
assert resp.page == 1
|
||||
assert resp.page_size == 10
|
||||
assert resp.total_pages == 1
|
||||
|
||||
async def test_list_import_logs_paginates(self, db: aiosqlite.Connection) -> None:
|
||||
"""list_import_logs computes total pages and returns the correct subset."""
|
||||
@@ -549,7 +548,6 @@ class TestImportLogPagination:
|
||||
db, source_id=None, page=2, page_size=2
|
||||
)
|
||||
assert resp.total == 3
|
||||
assert resp.total_pages == 2
|
||||
assert resp.page == 2
|
||||
assert resp.page_size == 2
|
||||
assert len(resp.items) == 1
|
||||
|
||||
104
backend/tests/test_utils/test_pagination.py
Normal file
104
backend/tests/test_utils/test_pagination.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""Tests for pagination utilities.
|
||||
|
||||
Validates the pagination helper functions used across all paginated endpoints.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.utils.pagination import get_offset, compute_total_pages, PAGINATION_DEFAULTS
|
||||
|
||||
|
||||
class TestPaginationDefaults:
|
||||
"""Test pagination default constants."""
|
||||
|
||||
def test_pagination_defaults_structure(self) -> None:
|
||||
"""PAGINATION_DEFAULTS contains required keys."""
|
||||
required_keys = {"page", "page_size", "max_page_size"}
|
||||
assert required_keys.issubset(PAGINATION_DEFAULTS.keys())
|
||||
|
||||
def test_pagination_defaults_values(self) -> None:
|
||||
"""PAGINATION_DEFAULTS have expected values."""
|
||||
assert PAGINATION_DEFAULTS["page"] == 1
|
||||
assert PAGINATION_DEFAULTS["page_size"] == 100
|
||||
assert PAGINATION_DEFAULTS["max_page_size"] == 500
|
||||
|
||||
|
||||
class TestGetOffset:
|
||||
"""Test get_offset calculation."""
|
||||
|
||||
def test_first_page(self) -> None:
|
||||
"""First page (page=1) produces offset=0."""
|
||||
assert get_offset(1, 10) == 0
|
||||
|
||||
def test_second_page(self) -> None:
|
||||
"""Second page (page=2) produces offset=page_size."""
|
||||
assert get_offset(2, 10) == 10
|
||||
|
||||
def test_arbitrary_page(self) -> None:
|
||||
"""Arbitrary pages produce correct offsets."""
|
||||
assert get_offset(3, 50) == 100
|
||||
assert get_offset(5, 25) == 100
|
||||
assert get_offset(10, 100) == 900
|
||||
|
||||
def test_page_size_variations(self) -> None:
|
||||
"""Offsets scale correctly with page_size."""
|
||||
assert get_offset(2, 1) == 1
|
||||
assert get_offset(2, 10) == 10
|
||||
assert get_offset(2, 100) == 100
|
||||
assert get_offset(2, 500) == 500
|
||||
|
||||
def test_invalid_page_raises(self) -> None:
|
||||
"""Invalid page numbers raise ValueError."""
|
||||
with pytest.raises(ValueError, match="page must be >= 1"):
|
||||
get_offset(0, 10)
|
||||
|
||||
with pytest.raises(ValueError, match="page must be >= 1"):
|
||||
get_offset(-1, 10)
|
||||
|
||||
def test_invalid_page_size_raises(self) -> None:
|
||||
"""Invalid page sizes raise ValueError."""
|
||||
with pytest.raises(ValueError, match="page_size must be >= 1"):
|
||||
get_offset(1, 0)
|
||||
|
||||
with pytest.raises(ValueError, match="page_size must be >= 1"):
|
||||
get_offset(1, -1)
|
||||
|
||||
|
||||
class TestComputeTotalPages:
|
||||
"""Test compute_total_pages calculation."""
|
||||
|
||||
def test_empty_collection(self) -> None:
|
||||
"""Empty collection (total=0) produces at least 1 page."""
|
||||
assert compute_total_pages(0, 10) == 1
|
||||
|
||||
def test_exact_page_fit(self) -> None:
|
||||
"""Totals that fit exactly produce expected page count."""
|
||||
assert compute_total_pages(10, 10) == 1
|
||||
assert compute_total_pages(20, 10) == 2
|
||||
assert compute_total_pages(50, 10) == 5
|
||||
|
||||
def test_partial_page(self) -> None:
|
||||
"""Totals that overflow a page produce ceil(total / page_size)."""
|
||||
assert compute_total_pages(11, 10) == 2
|
||||
assert compute_total_pages(25, 5) == 5
|
||||
assert compute_total_pages(101, 10) == 11
|
||||
|
||||
def test_single_item(self) -> None:
|
||||
"""Single item produces 1 page regardless of page_size."""
|
||||
assert compute_total_pages(1, 1) == 1
|
||||
assert compute_total_pages(1, 10) == 1
|
||||
assert compute_total_pages(1, 500) == 1
|
||||
|
||||
def test_large_page_size(self) -> None:
|
||||
"""Large page sizes still compute correctly."""
|
||||
assert compute_total_pages(1, 500) == 1
|
||||
assert compute_total_pages(501, 500) == 2
|
||||
assert compute_total_pages(1000, 500) == 2
|
||||
|
||||
def test_invalid_page_size_raises(self) -> None:
|
||||
"""Invalid page sizes raise ValueError."""
|
||||
with pytest.raises(ValueError, match="page_size must be >= 1"):
|
||||
compute_total_pages(100, 0)
|
||||
|
||||
with pytest.raises(ValueError, match="page_size must be >= 1"):
|
||||
compute_total_pages(100, -1)
|
||||
Reference in New Issue
Block a user