409 lines
13 KiB
Python
409 lines
13 KiB
Python
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()
|