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
107 lines
3.4 KiB
Python
107 lines
3.4 KiB
Python
"""Tests for app.utils.ip_utils."""
|
|
|
|
import pytest
|
|
|
|
from app.utils.ip_utils import (
|
|
ip_version,
|
|
is_valid_ip,
|
|
is_valid_ip_or_network,
|
|
is_valid_network,
|
|
normalise_ip,
|
|
normalise_network,
|
|
)
|
|
|
|
|
|
class TestIsValidIp:
|
|
"""Tests for :func:`is_valid_ip`."""
|
|
|
|
def test_is_valid_ip_with_valid_ipv4_returns_true(self) -> None:
|
|
assert is_valid_ip("192.168.1.1") is True
|
|
|
|
def test_is_valid_ip_with_valid_ipv6_returns_true(self) -> None:
|
|
assert is_valid_ip("2001:db8::1") is True
|
|
|
|
def test_is_valid_ip_with_cidr_returns_false(self) -> None:
|
|
assert is_valid_ip("10.0.0.0/8") is False
|
|
|
|
def test_is_valid_ip_with_empty_string_returns_false(self) -> None:
|
|
assert is_valid_ip("") is False
|
|
|
|
def test_is_valid_ip_with_hostname_returns_false(self) -> None:
|
|
assert is_valid_ip("example.com") is False
|
|
|
|
def test_is_valid_ip_with_loopback_returns_true(self) -> None:
|
|
assert is_valid_ip("127.0.0.1") is True
|
|
|
|
|
|
class TestIsValidNetwork:
|
|
"""Tests for :func:`is_valid_network`."""
|
|
|
|
def test_is_valid_network_with_valid_cidr_returns_true(self) -> None:
|
|
assert is_valid_network("192.168.0.0/24") is True
|
|
|
|
def test_is_valid_network_with_host_bits_set_returns_true(self) -> None:
|
|
# strict=False means host bits being set is allowed.
|
|
assert is_valid_network("192.168.0.1/24") is True
|
|
|
|
def test_is_valid_network_with_plain_ip_returns_true(self) -> None:
|
|
# A bare IP is treated as a host-only /32 network — this is valid.
|
|
assert is_valid_network("192.168.0.1") is True
|
|
|
|
def test_is_valid_network_with_hostname_returns_false(self) -> None:
|
|
assert is_valid_network("example.com") is False
|
|
|
|
def test_is_valid_network_with_invalid_prefix_returns_false(self) -> None:
|
|
assert is_valid_network("10.0.0.0/99") is False
|
|
|
|
|
|
class TestIsValidIpOrNetwork:
|
|
"""Tests for :func:`is_valid_ip_or_network`."""
|
|
|
|
def test_accepts_plain_ip(self) -> None:
|
|
assert is_valid_ip_or_network("1.2.3.4") is True
|
|
|
|
def test_accepts_cidr(self) -> None:
|
|
assert is_valid_ip_or_network("10.0.0.0/8") is True
|
|
|
|
def test_rejects_garbage(self) -> None:
|
|
assert is_valid_ip_or_network("not-an-ip") is False
|
|
|
|
|
|
class TestNormaliseIp:
|
|
"""Tests for :func:`normalise_ip`."""
|
|
|
|
def test_normalise_ip_ipv4_unchanged(self) -> None:
|
|
assert normalise_ip("10.20.30.40") == "10.20.30.40"
|
|
|
|
def test_normalise_ip_ipv6_compressed(self) -> None:
|
|
assert normalise_ip("2001:0db8:0000:0000:0000:0000:0000:0001") == "2001:db8::1"
|
|
|
|
def test_normalise_ip_invalid_raises_value_error(self) -> None:
|
|
with pytest.raises(ValueError):
|
|
normalise_ip("not-an-ip")
|
|
|
|
|
|
class TestNormaliseNetwork:
|
|
"""Tests for :func:`normalise_network`."""
|
|
|
|
def test_normalise_network_masks_host_bits(self) -> None:
|
|
assert normalise_network("192.168.1.5/24") == "192.168.1.0/24"
|
|
|
|
def test_normalise_network_already_canonical(self) -> None:
|
|
assert normalise_network("10.0.0.0/8") == "10.0.0.0/8"
|
|
|
|
|
|
class TestIpVersion:
|
|
"""Tests for :func:`ip_version`."""
|
|
|
|
def test_ip_version_ipv4_returns_4(self) -> None:
|
|
assert ip_version("8.8.8.8") == 4
|
|
|
|
def test_ip_version_ipv6_returns_6(self) -> None:
|
|
assert ip_version("::1") == 6
|
|
|
|
def test_ip_version_invalid_raises_value_error(self) -> None:
|
|
with pytest.raises(ValueError):
|
|
ip_version("garbage")
|