Ensure env configuration takes priority over DB overrides

This commit is contained in:
Egor
2025-11-01 02:50:24 +03:00
parent c2b6b4e370
commit 17768c303f
3 changed files with 207 additions and 5 deletions
+1
View File
@@ -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",
+43 -5
View File
@@ -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")