# 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 == [] 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"