refactor: restructure API pagination metadata for better frontend usability

- Create PaginationMetadata model with computed derived fields (total_pages, has_next_page, has_prev_page)
- Update PaginatedListResponse to embed pagination metadata in a separate 'pagination' object
- Add create_pagination_metadata() factory function in utils/pagination.py for consistent computation
- Update all paginated service functions to use new structure:
  - history_service.list_history()
  - blocklist_service.get_import_logs()
  - jail_service.get_jail_banned_ips()
  - ban_mappers.map_domain_dashboard_ban_list_to_response()
- Update response model docstrings with new structure examples
- Update Backend-Development.md documentation with new pagination patterns
- Update test fixtures to work with new response structure

Response shape changes from:
  {"items": [...], "total": 100, "page": 1, "page_size": 50}
To:
  {"items": [...], "pagination": {"page": 1, "page_size": 50, "total": 100, "total_pages": 2, "has_next_page": true, "has_prev_page": false}}

Benefits:
- Frontend receives all pagination state needed for UI controls
- No need for frontend to calculate total_pages or page navigation logic
- Consolidated pagination metadata reduces field sprawl
- OpenAPI schema automatically reflects changes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-30 22:24:42 +02:00
parent 05c3b564ae
commit 73021429f7
9 changed files with 156 additions and 96 deletions

View File

@@ -24,7 +24,7 @@ from app.models.history import (
# ---------------------------------------------------------------------------
_SETUP_PAYLOAD = {
"master_password": "testpassword1",
"master_password": "Mysecretpass1!",
"database_path": "bangui.db",
"fail2ban_socket": "/var/run/fail2ban/fail2ban.sock",
"timezone": "UTC",
@@ -50,8 +50,11 @@ def _make_history_item(ip: str = "1.2.3.4", jail: str = "sshd") -> HistoryBanIte
def _make_history_list(n: int = 2) -> HistoryListResponse:
"""Build a mock ``HistoryListResponse`` with *n* items."""
from app.utils.pagination import create_pagination_metadata
items = [_make_history_item(ip=f"1.2.3.{i}") for i in range(n)]
return HistoryListResponse(items=items, total=n, page=1, page_size=100)
pagination = create_pagination_metadata(total=n, page=1, page_size=100)
return HistoryListResponse(items=items, pagination=pagination)
def _make_ip_detail(ip: str = "1.2.3.4") -> IpDetailResponse:
@@ -96,7 +99,7 @@ async def history_client(tmp_path: Path) -> AsyncClient: # type: ignore[misc]
settings = Settings(
database_path=str(tmp_path / "history_test.db"),
fail2ban_socket="/tmp/fake_fail2ban.sock",
session_secret="test-history-secret",
session_secret="test-history-secret-32chars-long!!",
session_duration_minutes=60,
timezone="UTC",
log_level="debug",
@@ -163,10 +166,15 @@ class TestHistoryList:
body = response.json()
assert "items" in body
assert "total" in body
assert "page" in body
assert "page_size" in body
assert body["total"] == 1
assert "pagination" in body
pagination = body["pagination"]
assert "total" in pagination
assert "page" in pagination
assert "page_size" in pagination
assert "total_pages" in pagination
assert "has_next_page" in pagination
assert "has_prev_page" in pagination
assert pagination["total"] == 1
item = body["items"][0]
assert "ip" in item
@@ -253,17 +261,22 @@ class TestHistoryList:
async def test_empty_result(self, history_client: AsyncClient) -> None:
"""An empty history returns items=[] and total=0."""
from app.utils.pagination import create_pagination_metadata
with patch(
"app.routers.history.history_service.list_history",
new=AsyncMock(
return_value=HistoryListResponse(items=[], total=0, page=1, page_size=100)
return_value=HistoryListResponse(
items=[],
pagination=create_pagination_metadata(total=0, page=1, page_size=100),
)
),
):
response = await history_client.get("/api/history")
body = response.json()
assert body["items"] == []
assert body["total"] == 0
assert body["pagination"]["total"] == 0
# ---------------------------------------------------------------------------