Backend (tasks 1.1, 1.5–1.8): - pyproject.toml with FastAPI, Pydantic v2, aiosqlite, APScheduler 3.x, structlog, bcrypt; ruff + mypy strict configured - Pydantic Settings (BANGUI_ prefix env vars, fail-fast validation) - SQLite schema: settings, sessions, blocklist_sources, import_log; WAL mode + foreign keys; idempotent init_db() - FastAPI app factory with lifespan (DB, aiohttp session, scheduler), CORS, unhandled-exception handler, GET /api/health - Fail2BanClient: async Unix-socket wrapper using run_in_executor, custom error types, async context manager - Utility modules: ip_utils, time_utils, constants - 47 tests; ruff 0 errors; mypy --strict 0 errors Frontend (tasks 1.2–1.4): - Vite + React 18 + TypeScript strict; Fluent UI v9; ESLint + Prettier - Custom brand theme (#0F6CBD, WCAG AA contrast) with light/dark variants - Typed fetch API client (ApiError, get/post/put/del) + endpoints constants - tsc --noEmit 0 errors
80 lines
2.6 KiB
Python
80 lines
2.6 KiB
Python
"""Tests for app.utils.time_utils."""
|
|
|
|
import datetime
|
|
|
|
from app.utils.time_utils import add_minutes, hours_ago, is_expired, utc_from_timestamp, utc_now
|
|
|
|
|
|
class TestUtcNow:
|
|
"""Tests for :func:`utc_now`."""
|
|
|
|
def test_utc_now_returns_timezone_aware_datetime(self) -> None:
|
|
result = utc_now()
|
|
assert result.tzinfo is not None
|
|
|
|
def test_utc_now_timezone_is_utc(self) -> None:
|
|
result = utc_now()
|
|
assert result.tzinfo == datetime.UTC
|
|
|
|
def test_utc_now_is_recent(self) -> None:
|
|
before = datetime.datetime.now(datetime.UTC)
|
|
result = utc_now()
|
|
after = datetime.datetime.now(datetime.UTC)
|
|
assert before <= result <= after
|
|
|
|
|
|
class TestUtcFromTimestamp:
|
|
"""Tests for :func:`utc_from_timestamp`."""
|
|
|
|
def test_utc_from_timestamp_epoch_returns_utc_epoch(self) -> None:
|
|
result = utc_from_timestamp(0.0)
|
|
assert result == datetime.datetime(1970, 1, 1, tzinfo=datetime.UTC)
|
|
|
|
def test_utc_from_timestamp_returns_aware_datetime(self) -> None:
|
|
result = utc_from_timestamp(1_000_000_000.0)
|
|
assert result.tzinfo is not None
|
|
|
|
|
|
class TestAddMinutes:
|
|
"""Tests for :func:`add_minutes`."""
|
|
|
|
def test_add_minutes_positive(self) -> None:
|
|
dt = datetime.datetime(2024, 1, 1, 12, 0, 0, tzinfo=datetime.UTC)
|
|
result = add_minutes(dt, 30)
|
|
expected = datetime.datetime(2024, 1, 1, 12, 30, 0, tzinfo=datetime.UTC)
|
|
assert result == expected
|
|
|
|
def test_add_minutes_negative(self) -> None:
|
|
dt = datetime.datetime(2024, 1, 1, 12, 0, 0, tzinfo=datetime.UTC)
|
|
result = add_minutes(dt, -60)
|
|
expected = datetime.datetime(2024, 1, 1, 11, 0, 0, tzinfo=datetime.UTC)
|
|
assert result == expected
|
|
|
|
|
|
class TestIsExpired:
|
|
"""Tests for :func:`is_expired`."""
|
|
|
|
def test_is_expired_past_timestamp_returns_true(self) -> None:
|
|
past = datetime.datetime(2000, 1, 1, tzinfo=datetime.UTC)
|
|
assert is_expired(past) is True
|
|
|
|
def test_is_expired_future_timestamp_returns_false(self) -> None:
|
|
future = datetime.datetime(2099, 1, 1, tzinfo=datetime.UTC)
|
|
assert is_expired(future) is False
|
|
|
|
|
|
class TestHoursAgo:
|
|
"""Tests for :func:`hours_ago`."""
|
|
|
|
def test_hours_ago_returns_past_datetime(self) -> None:
|
|
result = hours_ago(24)
|
|
assert result < utc_now()
|
|
|
|
def test_hours_ago_correct_delta(self) -> None:
|
|
before = utc_now()
|
|
result = hours_ago(1)
|
|
after = utc_now()
|
|
expected_min = before - datetime.timedelta(hours=1, seconds=1)
|
|
expected_max = after - datetime.timedelta(hours=1) + datetime.timedelta(seconds=1)
|
|
assert expected_min <= result <= expected_max
|