"""Validation de la configuration au demarrage. Une valeur illisible doit arreter l'application avec un message nommant la variable fautive, jamais desactiver silencieusement une protection. """ from __future__ import annotations import pytest from pydantic import ValidationError from app.config import ConfigurationError, Settings, get_settings, parse_cidrs from app.services.rate_limit import RateLimitSyntaxError, parse_rate @pytest.mark.parametrize( ("spec", "limit", "window"), [ ("10/hour", 10, 3600), ("120/minute", 120, 60), ("5/second", 5, 1), ("2/day", 2, 86400), ("10/h", 10, 3600), (" 30/MINUTE ", 30, 60), ("3", 3, 60), ], ) def test_valid_rate_specs(spec: str, limit: int, window: int) -> None: rule = parse_rate(spec) assert rule is not None assert rule.limit == limit assert rule.window_seconds == window @pytest.mark.parametrize("spec", ["", "0", "off", "none", "disabled", " OFF "]) def test_explicit_deactivation(spec: str) -> None: """Desactiver une limite reste possible, mais doit etre explicite.""" assert parse_rate(spec) is None @pytest.mark.parametrize( "spec", ["10/hours", "abc", "-1/hour", "10/banana", "10/", "/hour", "dix/hour", "10 par heure"], ) def test_invalid_rate_specs_are_refused(spec: str) -> None: with pytest.raises(RateLimitSyntaxError): parse_rate(spec) def test_invalid_rate_message_is_actionable() -> None: with pytest.raises(RateLimitSyntaxError) as error: parse_rate("10/hours") message = str(error.value) assert "10/hours" in message assert "hours" in message assert "second" in message and "hour" in message class TestSettingsValidation: def test_valid_configuration_loads(self) -> None: settings = Settings( share_create_rate_limit="10/hour", share_read_rate_limit="120/minute", share_write_rate_limit="off", ) assert settings.share_write_rate_limit == "off" @pytest.mark.parametrize( "field", ["share_create_rate_limit", "share_read_rate_limit", "share_write_rate_limit"], ) def test_each_rate_variable_is_validated(self, field: str) -> None: with pytest.raises(Exception) as error: Settings(**{field: "10/banana"}) assert "banana" in str(error.value) def test_negative_shared_map_limit_is_refused(self) -> None: with pytest.raises(ValidationError): Settings(max_shared_maps=-1) def test_negative_share_size_is_refused(self) -> None: with pytest.raises(ValidationError): Settings(max_share_size_mb=-2) def test_zero_share_size_means_inherit(self) -> None: settings = Settings(max_share_size_mb=0, max_map_size_mb=10) assert settings.max_share_size_bytes == settings.max_map_size_bytes def test_negative_expiry_is_refused(self) -> None: with pytest.raises(ValidationError): Settings(default_share_expiry_days=-1) def test_zero_expiry_is_allowed(self) -> None: assert Settings(default_share_expiry_days=0).default_share_expiry_days == 0 def test_zero_map_size_is_refused(self) -> None: with pytest.raises(ValidationError): Settings(max_map_size_mb=0) class TestTrustedProxyConfiguration: def test_valid_cidrs(self) -> None: networks = parse_cidrs("172.16.0.0/12, 10.0.0.0/8 ,2001:db8::/32") assert len(networks) == 3 assert str(networks[0]) == "172.16.0.0/12" def test_single_address_becomes_a_host_network(self) -> None: assert str(parse_cidrs("192.0.2.7")[0]) == "192.0.2.7/32" def test_empty_value_is_allowed(self) -> None: assert parse_cidrs("") == [] assert parse_cidrs(" , ") == [] @pytest.mark.parametrize("value", ["999.1.1.1/8", "10.0.0.0/64", "pas-un-reseau", "10.0.0.0/"]) def test_invalid_cidr_is_refused(self, value: str) -> None: with pytest.raises(ValueError, match="reseau valide"): parse_cidrs(value) def test_invalid_cidr_stops_settings(self) -> None: with pytest.raises(Exception) as error: Settings(trusted_proxy_cidrs="10.0.0.0/8,pas-un-reseau") assert "pas-un-reseau" in str(error.value) def test_trusting_proxies_without_declaring_them_is_refused(self) -> None: """Faire confiance sans dire a qui n'appliquerait aucune confiance.""" with pytest.raises(Exception) as error: Settings(trust_proxy_headers=True, trusted_proxy_cidrs="") assert "TRUSTED_PROXY_CIDRS" in str(error.value) def test_trusting_proxies_with_networks_is_accepted(self) -> None: settings = Settings(trust_proxy_headers=True, trusted_proxy_cidrs="172.16.0.0/12") assert len(settings.trusted_proxy_networks) == 1 class TestStartupFailure: def test_bad_rate_stops_startup_with_a_readable_message(self, monkeypatch) -> None: monkeypatch.setenv("SHARE_CREATE_RATE_LIMIT", "10/banana") get_settings.cache_clear() with pytest.raises(ConfigurationError) as error: get_settings() message = str(error.value) assert "Configuration invalide" in message assert "SHARE_CREATE_RATE_LIMIT" in message assert "banana" in message get_settings.cache_clear() def test_bad_cidr_stops_startup(self, monkeypatch) -> None: monkeypatch.setenv("TRUSTED_PROXY_CIDRS", "10.0.0.0/8,nawak") get_settings.cache_clear() with pytest.raises(ConfigurationError) as error: get_settings() assert "TRUSTED_PROXY_CIDRS" in str(error.value) get_settings.cache_clear()