TRUSTED_PROXY_IPS now accepts CIDR notation (e.g., 192.168.1.0/24) in addition to exact IP addresses. Supports both IPv4 and IPv6. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
401 lines
14 KiB
Python
401 lines
14 KiB
Python
# ABOUTME: Tests for the configuration module.
|
|
# ABOUTME: Validates environment variable loading, defaults, and validation.
|
|
|
|
import os
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
|
|
class TestConfigDefaults:
|
|
"""Test that config loads with correct default values."""
|
|
|
|
def test_default_port(self):
|
|
"""Default port should be 3366 (EGG in leet)."""
|
|
with patch.dict(os.environ, {"CSRF_SECRET": "test-secret"}, clear=True):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.port == 3366
|
|
|
|
def test_default_db_path(self):
|
|
"""Default DB path should be animaltrack.db."""
|
|
with patch.dict(os.environ, {"CSRF_SECRET": "test-secret"}, clear=True):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.db_path == "animaltrack.db"
|
|
|
|
def test_default_auth_header_name(self):
|
|
"""Default auth header should be X-Oidc-Username."""
|
|
with patch.dict(os.environ, {"CSRF_SECRET": "test-secret"}, clear=True):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.auth_header_name == "X-Oidc-Username"
|
|
|
|
def test_default_trusted_proxy_ips_empty(self):
|
|
"""Default trusted proxy IPs should be empty list."""
|
|
with patch.dict(os.environ, {"CSRF_SECRET": "test-secret"}, clear=True):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.trusted_proxy_ips == []
|
|
|
|
def test_default_csrf_cookie_name(self):
|
|
"""Default CSRF cookie name should be csrf_token."""
|
|
with patch.dict(os.environ, {"CSRF_SECRET": "test-secret"}, clear=True):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.csrf_cookie_name == "csrf_token"
|
|
|
|
def test_default_base_path(self):
|
|
"""Default base path should be /."""
|
|
with patch.dict(os.environ, {"CSRF_SECRET": "test-secret"}, clear=True):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.base_path == "/"
|
|
|
|
def test_default_seed_on_start(self):
|
|
"""Default seed on start should be False."""
|
|
with patch.dict(os.environ, {"CSRF_SECRET": "test-secret"}, clear=True):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.seed_on_start is False
|
|
|
|
def test_default_log_level(self):
|
|
"""Default log level should be INFO."""
|
|
with patch.dict(os.environ, {"CSRF_SECRET": "test-secret"}, clear=True):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.log_level == "INFO"
|
|
|
|
def test_default_metrics_enabled(self):
|
|
"""Default metrics enabled should be False."""
|
|
with patch.dict(os.environ, {"CSRF_SECRET": "test-secret"}, clear=True):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.metrics_enabled is True
|
|
|
|
|
|
class TestConfigEnvOverrides:
|
|
"""Test that environment variables override defaults."""
|
|
|
|
def test_port_override(self):
|
|
"""PORT env var should override default."""
|
|
with patch.dict(os.environ, {"PORT": "8080", "CSRF_SECRET": "test-secret"}, clear=True):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.port == 8080
|
|
|
|
def test_db_path_override(self):
|
|
"""DB_PATH env var should override default."""
|
|
with patch.dict(
|
|
os.environ, {"DB_PATH": "/data/custom.db", "CSRF_SECRET": "test-secret"}, clear=True
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.db_path == "/data/custom.db"
|
|
|
|
def test_auth_header_name_override(self):
|
|
"""AUTH_HEADER_NAME env var should override default."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{"AUTH_HEADER_NAME": "X-Custom-User", "CSRF_SECRET": "test-secret"},
|
|
clear=True,
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.auth_header_name == "X-Custom-User"
|
|
|
|
def test_csrf_cookie_name_override(self):
|
|
"""CSRF_COOKIE_NAME env var should override default."""
|
|
with patch.dict(
|
|
os.environ, {"CSRF_COOKIE_NAME": "my_csrf", "CSRF_SECRET": "test-secret"}, clear=True
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.csrf_cookie_name == "my_csrf"
|
|
|
|
def test_base_path_override(self):
|
|
"""BASE_PATH env var should override default."""
|
|
with patch.dict(
|
|
os.environ, {"BASE_PATH": "/app", "CSRF_SECRET": "test-secret"}, clear=True
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.base_path == "/app"
|
|
|
|
def test_seed_on_start_true(self):
|
|
"""SEED_ON_START=true should set to True."""
|
|
with patch.dict(
|
|
os.environ, {"SEED_ON_START": "true", "CSRF_SECRET": "test-secret"}, clear=True
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.seed_on_start is True
|
|
|
|
def test_seed_on_start_false(self):
|
|
"""SEED_ON_START=false should set to False."""
|
|
with patch.dict(
|
|
os.environ, {"SEED_ON_START": "false", "CSRF_SECRET": "test-secret"}, clear=True
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.seed_on_start is False
|
|
|
|
def test_log_level_override(self):
|
|
"""LOG_LEVEL env var should override default."""
|
|
with patch.dict(
|
|
os.environ, {"LOG_LEVEL": "DEBUG", "CSRF_SECRET": "test-secret"}, clear=True
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.log_level == "DEBUG"
|
|
|
|
def test_metrics_enabled_true(self):
|
|
"""METRICS_ENABLED=true should set to True."""
|
|
with patch.dict(
|
|
os.environ, {"METRICS_ENABLED": "true", "CSRF_SECRET": "test-secret"}, clear=True
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.metrics_enabled is True
|
|
|
|
|
|
class TestTrustedProxyIPs:
|
|
"""Test TRUSTED_PROXY_IPS parsing."""
|
|
|
|
def test_single_ip(self):
|
|
"""Single IP should be parsed as list with one element."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{"TRUSTED_PROXY_IPS": "192.168.1.1", "CSRF_SECRET": "test-secret"},
|
|
clear=True,
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.trusted_proxy_ips == ["192.168.1.1"]
|
|
|
|
def test_multiple_ips_comma_separated(self):
|
|
"""Multiple IPs should be parsed from comma-separated string."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{"TRUSTED_PROXY_IPS": "192.168.1.1,10.0.0.1,172.16.0.1", "CSRF_SECRET": "test-secret"},
|
|
clear=True,
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.trusted_proxy_ips == ["192.168.1.1", "10.0.0.1", "172.16.0.1"]
|
|
|
|
def test_ips_with_spaces_trimmed(self):
|
|
"""Spaces around IPs should be trimmed."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{"TRUSTED_PROXY_IPS": " 192.168.1.1 , 10.0.0.1 ", "CSRF_SECRET": "test-secret"},
|
|
clear=True,
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.trusted_proxy_ips == ["192.168.1.1", "10.0.0.1"]
|
|
|
|
def test_empty_string_gives_empty_list(self):
|
|
"""Empty string should give empty list."""
|
|
with patch.dict(
|
|
os.environ, {"TRUSTED_PROXY_IPS": "", "CSRF_SECRET": "test-secret"}, clear=True
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.trusted_proxy_ips == []
|
|
|
|
def test_cidr_notation_parsed(self):
|
|
"""CIDR notation should be parsed into networks."""
|
|
import ipaddress
|
|
|
|
with patch.dict(
|
|
os.environ,
|
|
{"TRUSTED_PROXY_IPS": "192.168.1.0/24", "CSRF_SECRET": "test-secret"},
|
|
clear=True,
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
networks = settings.trusted_proxy_networks
|
|
assert len(networks) == 1
|
|
assert isinstance(networks[0], ipaddress.IPv4Network)
|
|
assert str(networks[0]) == "192.168.1.0/24"
|
|
|
|
def test_plain_ip_parsed_as_single_host_network(self):
|
|
"""Plain IP should be parsed as /32 network."""
|
|
import ipaddress
|
|
|
|
with patch.dict(
|
|
os.environ,
|
|
{"TRUSTED_PROXY_IPS": "10.0.0.1", "CSRF_SECRET": "test-secret"},
|
|
clear=True,
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
networks = settings.trusted_proxy_networks
|
|
assert len(networks) == 1
|
|
assert isinstance(networks[0], ipaddress.IPv4Network)
|
|
# Plain IP becomes /32 network
|
|
assert networks[0].num_addresses == 1
|
|
|
|
def test_mixed_ips_and_cidrs(self):
|
|
"""Mix of plain IPs and CIDR notation should all be parsed."""
|
|
import ipaddress
|
|
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"TRUSTED_PROXY_IPS": "10.0.0.1,192.168.0.0/16,172.16.0.0/12",
|
|
"CSRF_SECRET": "test-secret",
|
|
},
|
|
clear=True,
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
networks = settings.trusted_proxy_networks
|
|
assert len(networks) == 3
|
|
# All should be network objects
|
|
assert all(
|
|
isinstance(n, (ipaddress.IPv4Network, ipaddress.IPv6Network)) for n in networks
|
|
)
|
|
|
|
def test_ipv6_cidr_supported(self):
|
|
"""IPv6 CIDR notation should be supported."""
|
|
import ipaddress
|
|
|
|
with patch.dict(
|
|
os.environ,
|
|
{"TRUSTED_PROXY_IPS": "::1,fd00::/8", "CSRF_SECRET": "test-secret"},
|
|
clear=True,
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
networks = settings.trusted_proxy_networks
|
|
assert len(networks) == 2
|
|
assert any(isinstance(n, ipaddress.IPv6Network) for n in networks)
|
|
|
|
def test_invalid_cidr_becomes_literal(self):
|
|
"""Invalid CIDR notation should become a literal for string matching."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{"TRUSTED_PROXY_IPS": "192.168.1.0/33", "CSRF_SECRET": "test-secret"},
|
|
clear=True,
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
# Invalid CIDR should not appear in networks
|
|
assert len(settings.trusted_proxy_networks) == 0
|
|
# But should appear in literals
|
|
assert "192.168.1.0/33" in settings.trusted_proxy_literals
|
|
|
|
def test_invalid_ip_becomes_literal(self):
|
|
"""Invalid IP address should become a literal for string matching."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{"TRUSTED_PROXY_IPS": "not-an-ip", "CSRF_SECRET": "test-secret"},
|
|
clear=True,
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
# Invalid IP should not appear in networks
|
|
assert len(settings.trusted_proxy_networks) == 0
|
|
# But should appear in literals
|
|
assert "not-an-ip" in settings.trusted_proxy_literals
|
|
|
|
|
|
class TestCsrfSecretRequired:
|
|
"""Test that CSRF_SECRET is required."""
|
|
|
|
def test_missing_csrf_secret_raises_error(self):
|
|
"""Missing CSRF_SECRET should raise ValidationError."""
|
|
with patch.dict(os.environ, {}, clear=True):
|
|
from animaltrack.config import Settings
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Settings()
|
|
# Check that the error is about csrf_secret
|
|
errors = exc_info.value.errors()
|
|
assert any(e["loc"] == ("csrf_secret",) for e in errors)
|
|
|
|
def test_csrf_secret_provided(self):
|
|
"""Provided CSRF_SECRET should be accessible."""
|
|
with patch.dict(os.environ, {"CSRF_SECRET": "my-super-secret-key"}, clear=True):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.csrf_secret == "my-super-secret-key"
|
|
|
|
|
|
class TestValidation:
|
|
"""Test validation of config values."""
|
|
|
|
def test_invalid_port_not_integer(self):
|
|
"""Non-integer PORT should raise ValidationError."""
|
|
with patch.dict(
|
|
os.environ, {"PORT": "not-a-number", "CSRF_SECRET": "test-secret"}, clear=True
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
with pytest.raises(ValidationError):
|
|
Settings()
|
|
|
|
def test_invalid_log_level(self):
|
|
"""Invalid LOG_LEVEL should raise ValidationError."""
|
|
with patch.dict(
|
|
os.environ, {"LOG_LEVEL": "INVALID_LEVEL", "CSRF_SECRET": "test-secret"}, clear=True
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
with pytest.raises(ValidationError):
|
|
Settings()
|
|
|
|
def test_valid_log_levels(self):
|
|
"""All standard log levels should be valid."""
|
|
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
for level in valid_levels:
|
|
with patch.dict(
|
|
os.environ, {"LOG_LEVEL": level, "CSRF_SECRET": "test-secret"}, clear=True
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.log_level == level
|
|
|
|
def test_log_level_case_insensitive(self):
|
|
"""Log level should be case-insensitive (normalized to uppercase)."""
|
|
with patch.dict(
|
|
os.environ, {"LOG_LEVEL": "debug", "CSRF_SECRET": "test-secret"}, clear=True
|
|
):
|
|
from animaltrack.config import Settings
|
|
|
|
settings = Settings()
|
|
assert settings.log_level == "DEBUG"
|