Files
BanGUI/backend/app/utils/time_utils.py
Lukas 0221e423f2 Fix pagination metadata return structure and test assertions
The API pagination infrastructure was already correctly implemented with:
- PaginatedListResponse base model containing 'items' and 'pagination' fields
- PaginationMetadata object with all required fields (page, page_size, total, total_pages, has_next_page, has_prev_page)
- All services correctly calling create_pagination_metadata()

However, there were two bugs preventing tests from passing:

1. IMPORT BUG: time_utils.py was importing TIME_RANGE_SECONDS from app.models.ban
   when it's actually defined in app.models._common. This caused import errors
   in tests that exercise time-range filtering.

2. TEST BUG: Test assertions were using outdated API structure, accessing
   .total, .page, .page_size directly on paginated responses instead of
   through the .pagination object.

   Fixed locations:
   - test_mappers/test_ban_mappers.py: 3 assertions updated to use .pagination.*
   - test_services/test_blocklist_service.py: 6 assertions updated
   - test_services/test_history_service.py: 14 assertions updated

All paginated API endpoints now correctly return pagination metadata:
- GET /api/history
- GET /api/history/archive
- GET /api/dashboard/bans
- GET /api/jails/{name}/banned
- GET /api/blocklists/log

Verified with 24 passing pagination tests demonstrating correct behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-01 15:42:05 +02:00

97 lines
3.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Timezone-aware datetime helpers.
All datetimes in BanGUI are stored and transmitted in UTC.
Conversion to the user's display timezone happens only at the presentation
layer (frontend). These utilities provide a consistent, safe foundation
for working with time throughout the backend.
"""
import datetime
import time
def utc_now() -> datetime.datetime:
"""Return the current UTC time as a timezone-aware :class:`datetime.datetime`.
Returns:
Current UTC datetime with ``tzinfo=datetime.UTC``.
"""
return datetime.datetime.now(datetime.UTC)
def utc_from_timestamp(ts: float) -> datetime.datetime:
"""Convert a POSIX timestamp to a timezone-aware UTC datetime.
Args:
ts: POSIX timestamp (seconds since Unix epoch).
Returns:
Timezone-aware UTC :class:`datetime.datetime`.
"""
return datetime.datetime.fromtimestamp(ts, tz=datetime.UTC)
def add_minutes(dt: datetime.datetime, minutes: int) -> datetime.datetime:
"""Return a new datetime that is *minutes* ahead of *dt*.
Args:
dt: The source datetime (must be timezone-aware).
minutes: Number of minutes to add. May be negative.
Returns:
A new timezone-aware :class:`datetime.datetime`.
"""
return dt + datetime.timedelta(minutes=minutes)
def is_expired(expires_at: datetime.datetime) -> bool:
"""Return ``True`` if *expires_at* is in the past relative to UTC now.
Args:
expires_at: The expiry timestamp to check (must be timezone-aware).
Returns:
``True`` when the timestamp is past, ``False`` otherwise.
"""
return utc_now() >= expires_at
def hours_ago(hours: int) -> datetime.datetime:
"""Return a timezone-aware UTC datetime *hours* before now.
Args:
hours: Number of hours to subtract from the current time.
Returns:
Timezone-aware UTC :class:`datetime.datetime`.
"""
return utc_now() - datetime.timedelta(hours=hours)
def since_unix(range_: str) -> int:
"""Return the Unix timestamp for the start of a time-range window.
Uses :func:`time.time` (always UTC epoch seconds on all platforms) to be
consistent with how fail2ban stores ``timeofban`` values in its SQLite
database. fail2ban records :func:`time.time()` values directly, so using
a timezone-aware :func:`datetime.datetime.now`\\ ``(UTC).timestamp()``
would theoretically produce the same result but using :func:`time.time`
avoids any timezone-aware datetime pitfalls on misconfigured systems.
A 60-second slack window is applied to accommodate clock drift and
test seeding delays. This ensures consistent query windows across services
(e.g., dashboard vs. history).
Args:
range_: One of the supported time-range presets (e.g., ``"24h"``).
Returns:
Unix timestamp (seconds since epoch) representing the start of the
time window: *now range_ slack*.
"""
from app.models._common import TIME_RANGE_SECONDS
from app.utils.constants import TIME_RANGE_SLACK_SECONDS
seconds: int = TIME_RANGE_SECONDS[range_]
return int(time.time()) - seconds - TIME_RANGE_SLACK_SECONDS