Refactor data fetching hooks, add page size lint test
- Simplify useFetchData: remove unused URL building logic - Add usePolledData initial implementation - Add router page_size param validation test - Update API reference docs - Clean up tasks doc
This commit is contained in:
@@ -22,7 +22,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Path, status
|
||||
from fastapi import APIRouter, Body, Path, Query, status
|
||||
|
||||
from app.dependencies import (
|
||||
AuthDep,
|
||||
@@ -43,6 +43,7 @@ from app.models.jail import (
|
||||
JailListResponse,
|
||||
)
|
||||
from app.services import jail_service
|
||||
from app.utils.constants import DEFAULT_PAGE_SIZE
|
||||
|
||||
router: APIRouter = APIRouter(prefix="/api/v1/jails", tags=["Jails"])
|
||||
|
||||
@@ -521,8 +522,8 @@ async def get_jail_banned_ips(
|
||||
socket_path: Fail2BanSocketDep,
|
||||
http_session: HttpSessionDep,
|
||||
geo_cache: GeoCacheDep,
|
||||
page: int = 1,
|
||||
page_size: int = 25,
|
||||
page: int = Query(default=1, ge=1, description="1-based page number."),
|
||||
page_size: int = Query(default=DEFAULT_PAGE_SIZE, ge=1, le=100, description="Items per page (max 100)."),
|
||||
search: str | None = None,
|
||||
) -> JailBannedIpsResponse:
|
||||
"""Return a paginated list of IPs currently banned by a specific jail.
|
||||
@@ -539,7 +540,7 @@ async def get_jail_banned_ips(
|
||||
http_session: Shared HTTP session for geolocation.
|
||||
geo_cache: Geolocation cache instance.
|
||||
page: 1-based page number (default 1, min 1).
|
||||
page_size: Items per page (default 25, max 100).
|
||||
page_size: Items per page (default 100, max 100).
|
||||
search: Optional case-insensitive substring filter on the IP address.
|
||||
|
||||
Returns:
|
||||
|
||||
90
backend/tests/test_utils/test_router_page_size_lint.py
Normal file
90
backend/tests/test_utils/test_router_page_size_lint.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Lint test: no hardcoded page_size defaults exist in routers.
|
||||
|
||||
Ensures all paginated endpoints derive their default from
|
||||
:data:`~app.utils.constants.DEFAULT_PAGE_SIZE` rather than
|
||||
hardcoding integer literals.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROUTERS_DIR = Path(__file__).parent.parent.parent / "app" / "routers"
|
||||
|
||||
|
||||
class _PageSizeDefaultChecker(ast.NodeVisitor):
|
||||
"""Visit Python source files and report hardcoded page_size defaults."""
|
||||
|
||||
def __init__(self, source: str) -> None:
|
||||
self.source = source
|
||||
self.lines = source.splitlines()
|
||||
self.violations: list[str] = []
|
||||
|
||||
def _line_for(self, node: ast.AST) -> int:
|
||||
return node.lineno # type: ignore[attr-defined]
|
||||
|
||||
def _extract_assign_value(self, target: ast.expr, value: ast.expr) -> int | None:
|
||||
"""Return int value if *value* is an ast.Constant/ast.Num with an int."""
|
||||
if isinstance(value, ast.Constant) and isinstance(value.value, int):
|
||||
return value.value
|
||||
if isinstance(value, ast.Num) and isinstance(value.n, int):
|
||||
return value.n
|
||||
return None
|
||||
|
||||
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
||||
for default in node.args.defaults:
|
||||
self._check_default(default, node.name)
|
||||
for default in node.args.kw_defaults:
|
||||
if default is not None:
|
||||
self._check_default(default, node.name)
|
||||
self.generic_visit(node)
|
||||
|
||||
def _check_default(self, node: ast.expr, fn_name: str) -> None:
|
||||
# Look for patterns:
|
||||
# page_size: SomeType = 25
|
||||
# page_size: int = 25
|
||||
if isinstance(node, ast.Constant) and isinstance(node.value, int):
|
||||
lineno = node.lineno # type: ignore[attr-defined]
|
||||
col = node.col_offset # type: ignore[attr-defined]
|
||||
self.violations.append(
|
||||
f"line {lineno}:{col} {fn_name} page_size default is hardcoded int ({node.value}); "
|
||||
"use DEFAULT_PAGE_SIZE from app.utils.constants"
|
||||
)
|
||||
|
||||
|
||||
def _check_file(path: Path) -> list[str]:
|
||||
"""Return list of violation messages for *path* (empty if clean)."""
|
||||
try:
|
||||
source = path.read_text()
|
||||
except Exception as exc:
|
||||
return [f"could not read {path}: {exc}"]
|
||||
|
||||
tree = ast.parse(source, filename=str(path))
|
||||
checker = _PageSizeDefaultChecker(source)
|
||||
checker.visit(tree)
|
||||
|
||||
# Filter to only violations that involve page_size.
|
||||
# The checker already visits FunctionDef defaults broadly; we narrow to
|
||||
# those that look like "page_size = <int>" by inspecting the line content.
|
||||
filtered: list[str] = []
|
||||
for msg in checker.violations:
|
||||
# Extract line number from "line N:"
|
||||
lineno = int(msg.split(":")[1])
|
||||
line_text = source.splitlines()[lineno - 1]
|
||||
if "page_size" in line_text:
|
||||
filtered.append(msg)
|
||||
return filtered
|
||||
|
||||
|
||||
@pytest.mark.parametrize("router", sorted(ROUTERS_DIR.glob("*.py")), ids=lambda p: p.name)
|
||||
def test_no_hardcoded_page_size_defaults(router: Path) -> None:
|
||||
"""Router files must not contain hardcoded integer defaults for page_size.
|
||||
|
||||
All paginated endpoints should use ``default=DEFAULT_PAGE_SIZE`` from
|
||||
:data:`~app.utils.constants.DEFAULT_PAGE_SIZE`.
|
||||
"""
|
||||
violations = _check_file(router)
|
||||
assert violations == [], f"Hardcoded page_size defaults found in {router.name}:\n" + "\n".join(violations)
|
||||
Reference in New Issue
Block a user