Ensure env configuration takes priority over DB overrides
This commit is contained in:
@@ -1429,6 +1429,7 @@ class Settings(BaseSettings):
|
||||
|
||||
|
||||
settings = Settings()
|
||||
ENV_OVERRIDE_KEYS = set(settings.model_fields_set)
|
||||
|
||||
_PERIOD_PRICE_FIELDS: Dict[int, str] = {
|
||||
14: "PRICE_14_DAYS",
|
||||
|
||||
@@ -9,7 +9,13 @@ from app.database.universal_migration import ensure_default_web_api_token
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import Settings, settings, refresh_period_prices, refresh_traffic_prices
|
||||
from app.config import (
|
||||
Settings,
|
||||
settings,
|
||||
refresh_period_prices,
|
||||
refresh_traffic_prices,
|
||||
ENV_OVERRIDE_KEYS,
|
||||
)
|
||||
from app.database.crud.system_setting import (
|
||||
delete_system_setting,
|
||||
upsert_system_setting,
|
||||
@@ -631,6 +637,10 @@ class BotConfigurationService:
|
||||
def is_read_only(cls, key: str) -> bool:
|
||||
return key in cls.READ_ONLY_KEYS
|
||||
|
||||
@classmethod
|
||||
def _is_env_override(cls, key: str) -> bool:
|
||||
return key in cls._env_override_keys
|
||||
|
||||
@classmethod
|
||||
def _format_numeric_with_unit(cls, key: str, value: Union[int, float]) -> Optional[str]:
|
||||
if isinstance(value, bool):
|
||||
@@ -744,6 +754,7 @@ class BotConfigurationService:
|
||||
_definitions: Dict[str, SettingDefinition] = {}
|
||||
_original_values: Dict[str, Any] = settings.model_dump()
|
||||
_overrides_raw: Dict[str, Optional[str]] = {}
|
||||
_env_override_keys: set[str] = set(ENV_OVERRIDE_KEYS)
|
||||
_callback_tokens: Dict[str, str] = {}
|
||||
_token_to_key: Dict[str, str] = {}
|
||||
_choice_tokens: Dict[str, Dict[Any, str]] = {}
|
||||
@@ -867,6 +878,8 @@ class BotConfigurationService:
|
||||
|
||||
@classmethod
|
||||
def has_override(cls, key: str) -> bool:
|
||||
if cls._is_env_override(key):
|
||||
return False
|
||||
return key in cls._overrides_raw
|
||||
|
||||
@classmethod
|
||||
@@ -1162,6 +1175,12 @@ class BotConfigurationService:
|
||||
overrides[row.key] = row.value
|
||||
|
||||
for key, raw_value in overrides.items():
|
||||
if cls._is_env_override(key):
|
||||
logger.debug(
|
||||
"Пропускаем настройку %s из БД: используется значение из окружения",
|
||||
key,
|
||||
)
|
||||
continue
|
||||
try:
|
||||
parsed_value = cls.deserialize_value(key, raw_value)
|
||||
except Exception as error:
|
||||
@@ -1281,8 +1300,15 @@ class BotConfigurationService:
|
||||
|
||||
raw_value = cls.serialize_value(key, value)
|
||||
await upsert_system_setting(db, key, raw_value)
|
||||
cls._overrides_raw[key] = raw_value
|
||||
cls._apply_to_settings(key, value)
|
||||
if cls._is_env_override(key):
|
||||
logger.info(
|
||||
"Настройка %s сохранена в БД, но не применена: значение задаётся через окружение",
|
||||
key,
|
||||
)
|
||||
cls._overrides_raw.pop(key, None)
|
||||
else:
|
||||
cls._overrides_raw[key] = raw_value
|
||||
cls._apply_to_settings(key, value)
|
||||
|
||||
if key in {"WEB_API_DEFAULT_TOKEN", "WEB_API_DEFAULT_TOKEN_NAME"}:
|
||||
await cls._sync_default_web_api_token()
|
||||
@@ -1300,14 +1326,26 @@ class BotConfigurationService:
|
||||
|
||||
await delete_system_setting(db, key)
|
||||
cls._overrides_raw.pop(key, None)
|
||||
original = cls.get_original_value(key)
|
||||
cls._apply_to_settings(key, original)
|
||||
if cls._is_env_override(key):
|
||||
logger.info(
|
||||
"Настройка %s сброшена в БД, используется значение из окружения",
|
||||
key,
|
||||
)
|
||||
else:
|
||||
original = cls.get_original_value(key)
|
||||
cls._apply_to_settings(key, original)
|
||||
|
||||
if key in {"WEB_API_DEFAULT_TOKEN", "WEB_API_DEFAULT_TOKEN_NAME"}:
|
||||
await cls._sync_default_web_api_token()
|
||||
|
||||
@classmethod
|
||||
def _apply_to_settings(cls, key: str, value: Any) -> None:
|
||||
if cls._is_env_override(key):
|
||||
logger.debug(
|
||||
"Пропуск применения настройки %s: значение задано через окружение",
|
||||
key,
|
||||
)
|
||||
return
|
||||
try:
|
||||
setattr(settings, key, value)
|
||||
if key in {
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
from types import SimpleNamespace
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
from app.config import settings
|
||||
from app.services.system_settings_service import bot_configuration_service
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_env_override_prevents_set_value(monkeypatch):
|
||||
bot_configuration_service.initialize_definitions()
|
||||
|
||||
env_value = "env_support"
|
||||
monkeypatch.setattr(settings, "SUPPORT_USERNAME", env_value)
|
||||
original_values = dict(bot_configuration_service._original_values)
|
||||
original_values["SUPPORT_USERNAME"] = env_value
|
||||
monkeypatch.setattr(bot_configuration_service, "_original_values", original_values)
|
||||
|
||||
env_keys = set(bot_configuration_service._env_override_keys)
|
||||
env_keys.add("SUPPORT_USERNAME")
|
||||
monkeypatch.setattr(bot_configuration_service, "_env_override_keys", env_keys)
|
||||
monkeypatch.setattr(bot_configuration_service, "_overrides_raw", {})
|
||||
|
||||
async def fake_upsert(db, key, value, description=None): # noqa: ANN001
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.system_settings_service.upsert_system_setting",
|
||||
fake_upsert,
|
||||
)
|
||||
|
||||
await bot_configuration_service.set_value(
|
||||
object(),
|
||||
"SUPPORT_USERNAME",
|
||||
"db_support",
|
||||
)
|
||||
|
||||
assert settings.SUPPORT_USERNAME == env_value
|
||||
assert not bot_configuration_service.has_override("SUPPORT_USERNAME")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_env_override_prevents_reset_value(monkeypatch):
|
||||
bot_configuration_service.initialize_definitions()
|
||||
|
||||
env_value = "env_support"
|
||||
monkeypatch.setattr(settings, "SUPPORT_USERNAME", env_value)
|
||||
original_values = dict(bot_configuration_service._original_values)
|
||||
original_values["SUPPORT_USERNAME"] = env_value
|
||||
monkeypatch.setattr(bot_configuration_service, "_original_values", original_values)
|
||||
|
||||
env_keys = set(bot_configuration_service._env_override_keys)
|
||||
env_keys.add("SUPPORT_USERNAME")
|
||||
monkeypatch.setattr(bot_configuration_service, "_env_override_keys", env_keys)
|
||||
monkeypatch.setattr(bot_configuration_service, "_overrides_raw", {"SUPPORT_USERNAME": "db"})
|
||||
|
||||
async def fake_delete(db, key): # noqa: ANN001
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.system_settings_service.delete_system_setting",
|
||||
fake_delete,
|
||||
)
|
||||
|
||||
await bot_configuration_service.reset_value(
|
||||
object(),
|
||||
"SUPPORT_USERNAME",
|
||||
)
|
||||
|
||||
assert settings.SUPPORT_USERNAME == env_value
|
||||
assert not bot_configuration_service.has_override("SUPPORT_USERNAME")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize_skips_db_value_for_env_override(monkeypatch):
|
||||
bot_configuration_service.initialize_definitions()
|
||||
|
||||
env_value = "env_support"
|
||||
monkeypatch.setattr(settings, "SUPPORT_USERNAME", env_value)
|
||||
original_values = dict(bot_configuration_service._original_values)
|
||||
original_values["SUPPORT_USERNAME"] = env_value
|
||||
monkeypatch.setattr(bot_configuration_service, "_original_values", original_values)
|
||||
|
||||
env_keys = set(bot_configuration_service._env_override_keys)
|
||||
env_keys.add("SUPPORT_USERNAME")
|
||||
monkeypatch.setattr(bot_configuration_service, "_env_override_keys", env_keys)
|
||||
monkeypatch.setattr(bot_configuration_service, "_overrides_raw", {})
|
||||
|
||||
class DummyResult:
|
||||
def scalars(self):
|
||||
return self
|
||||
|
||||
def all(self):
|
||||
return [SimpleNamespace(key="SUPPORT_USERNAME", value="db_support")]
|
||||
|
||||
class DummySession:
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb): # noqa: ANN001
|
||||
return False
|
||||
|
||||
async def execute(self, query): # noqa: ANN001
|
||||
return DummyResult()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.system_settings_service.AsyncSessionLocal",
|
||||
lambda: DummySession(),
|
||||
)
|
||||
|
||||
async def fake_sync():
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.system_settings_service.ensure_default_web_api_token",
|
||||
fake_sync,
|
||||
raising=False,
|
||||
)
|
||||
|
||||
await bot_configuration_service.initialize()
|
||||
|
||||
assert settings.SUPPORT_USERNAME == env_value
|
||||
assert "SUPPORT_USERNAME" not in bot_configuration_service._overrides_raw
|
||||
assert not bot_configuration_service.has_override("SUPPORT_USERNAME")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_value_applies_without_env_override(monkeypatch):
|
||||
bot_configuration_service.initialize_definitions()
|
||||
|
||||
monkeypatch.setattr(bot_configuration_service, "_env_override_keys", set())
|
||||
monkeypatch.setattr(bot_configuration_service, "_overrides_raw", {})
|
||||
|
||||
initial_value = True
|
||||
target_value = False
|
||||
|
||||
monkeypatch.setattr(settings, "SUPPORT_MENU_ENABLED", initial_value)
|
||||
original_values = dict(bot_configuration_service._original_values)
|
||||
original_values["SUPPORT_MENU_ENABLED"] = initial_value
|
||||
monkeypatch.setattr(bot_configuration_service, "_original_values", original_values)
|
||||
|
||||
async def fake_upsert(db, key, value, description=None): # noqa: ANN001
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.system_settings_service.upsert_system_setting",
|
||||
fake_upsert,
|
||||
)
|
||||
|
||||
await bot_configuration_service.set_value(
|
||||
object(),
|
||||
"SUPPORT_MENU_ENABLED",
|
||||
target_value,
|
||||
)
|
||||
|
||||
assert settings.SUPPORT_MENU_ENABLED is target_value
|
||||
assert bot_configuration_service.has_override("SUPPORT_MENU_ENABLED")
|
||||
Reference in New Issue
Block a user