- Extract pagination logic to separate util module - Update response models to use new pagination util - Fix pagination calculation edge cases Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
140 lines
5.4 KiB
Python
140 lines
5.4 KiB
Python
"""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)
|
|
|
|
|
|
class TestPaginationMetadataMode:
|
|
"""Test pagination_mode field in create_pagination_metadata."""
|
|
|
|
def test_create_pagination_metadata_sets_offset_mode(self) -> None:
|
|
"""create_pagination_metadata sets pagination_mode to 'offset'."""
|
|
from app.utils.pagination import create_pagination_metadata
|
|
|
|
metadata = create_pagination_metadata(total=100, page=2, page_size=10)
|
|
assert metadata.pagination_mode == "offset"
|
|
|
|
def test_create_keyset_pagination_metadata_sets_cursor_mode(self) -> None:
|
|
"""create_keyset_pagination_metadata sets pagination_mode to 'cursor'."""
|
|
from app.utils.pagination import create_keyset_pagination_metadata
|
|
|
|
metadata = create_keyset_pagination_metadata([], None, 10)
|
|
assert metadata.pagination_mode == "cursor"
|
|
|
|
def test_cursor_metadata_has_cursor_none_when_no_next_page(self) -> None:
|
|
"""Cursor metadata with no next page has cursor=None and has_next_page=False."""
|
|
from app.utils.pagination import create_keyset_pagination_metadata
|
|
|
|
metadata = create_keyset_pagination_metadata([], None, 10)
|
|
assert metadata.has_next_page is False
|
|
assert metadata.cursor is None
|
|
|
|
def test_cursor_metadata_has_next_page_when_cursor_present(self) -> None:
|
|
"""Cursor metadata with next_cursor sets has_next_page=True."""
|
|
from app.utils.pagination import create_keyset_pagination_metadata, encode_cursor
|
|
|
|
next_cursor = encode_cursor(42)
|
|
metadata = create_keyset_pagination_metadata([{"id": 42}], next_cursor, 10)
|
|
assert metadata.has_next_page is True
|
|
assert metadata.cursor == next_cursor
|