from decimal import Decimal from pathlib import Path import pytest from pydantic import ValidationError from app import config as config_module from app.config import Settings, get_settings PROJECT_ROOT = Path(__file__).resolve().parents[2] TEST_CONFIG_FILE = PROJECT_ROOT / "config.test.yaml" MISSING_REQUIRED_SECTION_CONFIG_FILE = ( PROJECT_ROOT / "tests" / "config" / "fixtures" / "config.missing_adapter.yaml" ) MISSING_ADDRESS_SUGGESTIONS_CONFIG_FILE = ( PROJECT_ROOT / "tests" / "config" / "fixtures" / "config.missing_address_suggestions.yaml" ) DEFAULT_CONFIG_FILE_FIXTURE = ( PROJECT_ROOT / "tests" / "config" / "fixtures" / "config.default.yaml" ) TEST_OVERRIDE_CONFIG_FILE_FIXTURE = ( PROJECT_ROOT / "tests" / "config" / "fixtures" / "config.test.override.yaml" ) INVALID_PRICE_MULTIPLIER_CONFIG_FILE = ( PROJECT_ROOT / "tests" / "config" / "fixtures" / "config.invalid_price_multiplier.yaml" ) MISSING_PRICE_MULTIPLIER_CONFIG_FILE = ( PROJECT_ROOT / "tests" / "config" / "fixtures" / "config.missing_price_multiplier.yaml" ) MISSING_OBSERVABILITY_CONFIG_FILE = ( PROJECT_ROOT / "tests" / "config" / "fixtures" / "config.missing_observability.yaml" ) MISSING_OBSERVABILITY_ENDPOINT_CONFIG_FILE = ( PROJECT_ROOT / "tests" / "config" / "fixtures" / "config.missing_observability_endpoint.yaml" ) _DADATA_COUNTRIES = ("RU", "BY", "KZ") _YANDEX_COUNTRIES = ("AM", "AZ", "KG", "MD", "TJ", "TM", "UZ") _TOMTOM_COUNTRIES = ( "AL", "AT", "BE", "BG", "CH", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LV", "NL", "NO", "PL", "PT", "RO", "RS", "SE", "SI", "SK", "UA", ) def _expected_country_mapping() -> dict[str, str]: return { **{country_code: "dadata" for country_code in _DADATA_COUNTRIES}, **{ country_code: "yandex_geosuggest" for country_code in _YANDEX_COUNTRIES }, **{country_code: "tomtom" for country_code in _TOMTOM_COUNTRIES}, } def _use_runtime_config_files( monkeypatch: pytest.MonkeyPatch, *, test_config_file: Path, default_config_file: Path | None = None, ) -> None: monkeypatch.setattr(config_module, "TEST_CONFIG_FILE", str(test_config_file)) if default_config_file is not None: monkeypatch.setattr( config_module, "DEFAULT_CONFIG_FILE", str(default_config_file), ) get_settings.cache_clear() def _write_tbank_url_validation_config( tmp_path: Path, *, include_notification_url: bool = True, include_success_url: bool = True, ) -> Path: notification_url = ( ' notification_url: "https://merchant.test/tbank/notification"\n' if include_notification_url else "" ) success_url = ( ' success_url: "https://merchant.test/payment/success"\n' if include_success_url else "" ) config_file = tmp_path / "config.yaml" config_file.write_text( f""" controller: {{}} service: {{}} business_logic: provider_price_multiplier: 1.0 repository: {{}} adapter: {{}} tbank_payment: init_url: "https://securepay.tinkoff.ru/v2/Init" {notification_url}{success_url} auth: terminal_key: "test-terminal-key" password: "test-password" postgres: dsn: "postgresql+asyncpg://postgres:postgres@localhost:5432/config_test" address_suggestions: {{}} observability: service_name: "config-test" otlp_endpoint: "http://collector:4317" """.strip(), encoding="utf-8", ) return config_file def test_configuration_sections_are_loaded_from_yaml_file( monkeypatch: pytest.MonkeyPatch, ) -> None: _use_runtime_config_files(monkeypatch, test_config_file=TEST_CONFIG_FILE) settings = Settings() assert settings.controller.api_prefix == "/api/v1" assert settings.controller.request_id_header == "X-Request-ID" assert settings.service.provider_timeout_seconds == 10.0 assert settings.service.max_parallel_providers == 8 assert settings.business_logic.weight_round_scale == 2 assert settings.business_logic.provider_price_multiplier == Decimal("1.0") assert settings.repository.redis_dsn == "redis://localhost:6379/0" assert settings.repository.price_cache_ttl_seconds == 900 assert settings.adapter.cdek_base_url == "https://api.cdek.ru/v2" assert settings.adapter.cdek_client_id == "test-client-id" assert settings.adapter.cdek_client_secret == "test-client-secret" assert settings.adapter.cdek_retry_attempts == 2 assert settings.adapter.cdek_retry_backoff_seconds == 0.2 assert settings.adapter.cdek_timeout_seconds == 10.0 assert settings.adapter.cdek_cache_ttl_seconds == 900 assert settings.tbank_payment.init_url == "https://securepay.tinkoff.ru/v2/Init" assert ( settings.tbank_payment.notification_url == "https://merchant.test/tbank/notification" ) assert settings.tbank_payment.success_url == "https://merchant.test/payment/success" assert settings.tbank_payment.auth.terminal_key == "test-terminal-key" assert settings.tbank_payment.auth.password == "test-password" assert settings.tbank_payment.timeout_seconds == 10.0 assert settings.tbank_payment.retry_attempts == 2 assert settings.tbank_payment.retry_backoff_seconds == 0.2 assert ( settings.postgres.dsn == "postgresql+asyncpg://postgres:postgres@localhost:5432/g2s_aggregator_test" ) assert settings.address_suggestions.country_to_provider == _expected_country_mapping() assert ( settings.address_suggestions.dadata.url == "https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address" ) assert settings.address_suggestions.dadata.api_key == "test-dadata-api-key" assert settings.address_suggestions.dadata.timeout_seconds == 7.5 assert ( settings.address_suggestions.yandex_geosuggest.url == "https://suggest-maps.yandex.ru/v1/suggest" ) assert ( settings.address_suggestions.yandex_geosuggest.api_key == "test-yandex-geosuggest-api-key" ) assert settings.address_suggestions.yandex_geosuggest.timeout_seconds == 6.5 assert settings.address_suggestions.tomtom.url == "https://api.tomtom.com/search/2/search" assert settings.address_suggestions.tomtom.api_key == "test-tomtom-api-key" assert settings.address_suggestions.tomtom.timeout_seconds == 5.5 assert settings.observability.enabled is False assert settings.observability.service_name == "g2s-aggregator-test" assert settings.observability.otlp_endpoint == "http://localhost:4317" assert settings.observability.otlp_insecure is True def test_get_settings_fails_when_required_yaml_section_is_missing( monkeypatch: pytest.MonkeyPatch, ) -> None: _use_runtime_config_files( monkeypatch, test_config_file=MISSING_REQUIRED_SECTION_CONFIG_FILE, ) with pytest.raises(ValidationError) as error: get_settings() locations = {tuple(item["loc"]) for item in error.value.errors()} assert ("adapter",) in locations get_settings.cache_clear() def test_get_settings_fails_when_address_suggestions_section_is_missing( monkeypatch: pytest.MonkeyPatch, ) -> None: _use_runtime_config_files( monkeypatch, test_config_file=MISSING_ADDRESS_SUGGESTIONS_CONFIG_FILE, ) with pytest.raises(ValidationError) as error: get_settings() locations = {tuple(item["loc"]) for item in error.value.errors()} assert ("address_suggestions",) in locations get_settings.cache_clear() def test_get_settings_returns_cached_instance(monkeypatch: pytest.MonkeyPatch) -> None: _use_runtime_config_files(monkeypatch, test_config_file=TEST_CONFIG_FILE) first = get_settings() second = get_settings() assert first is second assert first.service.provider_timeout_seconds == 10.0 assert first.business_logic.provider_price_multiplier == Decimal("1.0") assert first.tbank_payment.auth.terminal_key == "test-terminal-key" assert ( first.tbank_payment.notification_url == "https://merchant.test/tbank/notification" ) assert first.postgres.dsn.endswith("/g2s_aggregator_test") assert first.address_suggestions.country_to_provider["RU"] == "dadata" assert first.observability.service_name == "g2s-aggregator-test" get_settings.cache_clear() def test_get_settings_uses_config_test_yaml_in_pytest_environment( monkeypatch: pytest.MonkeyPatch, ) -> None: _use_runtime_config_files( monkeypatch, test_config_file=TEST_OVERRIDE_CONFIG_FILE_FIXTURE, default_config_file=DEFAULT_CONFIG_FILE_FIXTURE, ) settings = get_settings() assert settings.service.provider_timeout_seconds == 17.0 assert settings.controller.api_prefix == "/from-config-test-yaml" assert settings.business_logic.provider_price_multiplier == Decimal("1.25") assert settings.adapter.cdek_client_id == "test-id" assert settings.tbank_payment.auth.terminal_key == "override-terminal-key" assert settings.tbank_payment.auth.password == "override-password" assert ( settings.tbank_payment.notification_url == "https://override.test/tbank/notification" ) assert settings.tbank_payment.success_url == "https://override.test/payment/success" assert settings.tbank_payment.timeout_seconds == 4.25 assert settings.tbank_payment.retry_attempts == 1 assert settings.tbank_payment.retry_backoff_seconds == 0.05 assert ( settings.postgres.dsn == "postgresql+asyncpg://postgres:postgres@localhost:5432/override" ) assert settings.address_suggestions.country_to_provider == { "RU": "dadata", "AM": "yandex_geosuggest", "DE": "tomtom", } assert settings.address_suggestions.dadata.api_key == "override-dadata-key" assert settings.address_suggestions.dadata.timeout_seconds == 3.0 assert settings.address_suggestions.yandex_geosuggest.api_key == "override-yandex-key" assert settings.address_suggestions.yandex_geosuggest.timeout_seconds == 4.5 assert settings.address_suggestions.tomtom.api_key == "override-tomtom-key" assert settings.address_suggestions.tomtom.timeout_seconds == 5.25 assert settings.observability.enabled is True assert settings.observability.service_name == "override-config-service" assert settings.observability.otlp_endpoint == "http://override-collector:4317" assert settings.observability.otlp_insecure is False get_settings.cache_clear() def test_get_settings_fails_when_provider_price_multiplier_is_invalid( monkeypatch: pytest.MonkeyPatch, ) -> None: _use_runtime_config_files( monkeypatch, test_config_file=INVALID_PRICE_MULTIPLIER_CONFIG_FILE, ) with pytest.raises(ValidationError) as error: get_settings() locations = {tuple(item["loc"]) for item in error.value.errors()} assert ("business_logic", "provider_price_multiplier") in locations get_settings.cache_clear() def test_get_settings_fails_when_provider_price_multiplier_is_missing( monkeypatch: pytest.MonkeyPatch, ) -> None: _use_runtime_config_files( monkeypatch, test_config_file=MISSING_PRICE_MULTIPLIER_CONFIG_FILE, ) with pytest.raises(ValidationError) as error: get_settings() locations = {tuple(item["loc"]) for item in error.value.errors()} assert ("business_logic", "provider_price_multiplier") in locations get_settings.cache_clear() @pytest.mark.parametrize( ("include_notification_url", "include_success_url", "expected_location"), ( (False, True, ("tbank_payment", "notification_url")), (True, False, ("tbank_payment", "success_url")), ), ) def test_get_settings_fails_when_tbank_payment_url_is_missing( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, include_notification_url: bool, include_success_url: bool, expected_location: tuple[str, str], ) -> None: config_file = _write_tbank_url_validation_config( tmp_path, include_notification_url=include_notification_url, include_success_url=include_success_url, ) _use_runtime_config_files(monkeypatch, test_config_file=config_file) with pytest.raises(ValidationError) as error: get_settings() locations = {tuple(item["loc"]) for item in error.value.errors()} assert expected_location in locations get_settings.cache_clear() def test_get_settings_fails_when_observability_section_is_missing( monkeypatch: pytest.MonkeyPatch, ) -> None: _use_runtime_config_files( monkeypatch, test_config_file=MISSING_OBSERVABILITY_CONFIG_FILE, ) with pytest.raises(ValidationError) as error: get_settings() locations = {tuple(item["loc"]) for item in error.value.errors()} assert ("observability",) in locations get_settings.cache_clear() def test_get_settings_fails_when_observability_endpoint_is_missing( monkeypatch: pytest.MonkeyPatch, ) -> None: _use_runtime_config_files( monkeypatch, test_config_file=MISSING_OBSERVABILITY_ENDPOINT_CONFIG_FILE, ) with pytest.raises(ValidationError) as error: get_settings() locations = {tuple(item["loc"]) for item in error.value.errors()} assert ("observability", "otlp_endpoint") in locations get_settings.cache_clear()