diff --git a/api/src/core/config.py b/api/src/core/config.py index 360182b..47f093d 100644 --- a/api/src/core/config.py +++ b/api/src/core/config.py @@ -1,5 +1,6 @@ import os +from pydantic import NonNegativeInt, PositiveInt from pydantic_settings import BaseSettings, SettingsConfigDict @@ -68,9 +69,12 @@ class Settings(BaseSettings): # NATS и outbox/inbox обмена recognition_job NATS_ENABLED: bool = False - OUTBOX_POLL_SECONDS: int = 5 # период опроса outbox - ACK_TIMEOUT_SECONDS: int = 30 # ожидание ack сервиса распознавания до повторной публикации - OUTBOX_MAX_REPUBLISH: int = 3 # число повторных публикаций до перевода в dlq + # 0 и отрицательные значения отвергаются при старте: период 0 — горячий цикл, ack-таймаут 0 — повтор каждый проход + OUTBOX_POLL_SECONDS: PositiveInt = 5 # период опроса outbox + ACK_TIMEOUT_SECONDS: PositiveInt = 30 # ожидание ack сервиса распознавания до повторной публикации + OUTBOX_MAX_REPUBLISH: NonNegativeInt = ( + 3 # число повторных публикаций до перевода в dlq (0 — сразу dlq без повторов) + ) model_config = SettingsConfigDict( extra="ignore", diff --git a/api/tests/test_config.py b/api/tests/test_config.py index 907f606..f413f61 100644 --- a/api/tests/test_config.py +++ b/api/tests/test_config.py @@ -4,6 +4,7 @@ import os from pathlib import Path import pytest +from pydantic import ValidationError from src.core.config import Settings @@ -67,3 +68,40 @@ def test_empty_nats_seed_and_jwt_allowed(clean_env: pytest.MonkeyPatch) -> None: assert s.OPENBAO.NATS.NATS_JWT == "" assert s.NATS_ENABLED is True assert s.OPENBAO.SMB.SMB_HOST == "" + + +REQUIRED_ENV = { + "OPENBAO__DATABASE__DATABASE_URL": "postgresql+asyncpg://u:p@localhost:5432/d", + "OPENBAO__MINIO__MINIO_URL": "localhost:9000", + "OPENBAO__MINIO__MINIO_ADMIN_LOGIN": "login", + "OPENBAO__MINIO__MINIO_ADMIN_PASSWORD": "password", + "OPENBAO__MINIO__MINIO_BUCKET": "bucket", + "OPENBAO__NATS__NATS_URL": "nats://localhost:4222", +} + + +@pytest.mark.parametrize( + ("key", "value"), + [ + ("OUTBOX_POLL_SECONDS", "0"), + ("OUTBOX_POLL_SECONDS", "-1"), + ("ACK_TIMEOUT_SECONDS", "0"), + ("ACK_TIMEOUT_SECONDS", "-5"), + ("OUTBOX_MAX_REPUBLISH", "-1"), + ], +) +def test_exchange_settings_reject_invalid(clean_env: pytest.MonkeyPatch, key: str, value: str) -> None: + """Период опроса и ack-таймаут — строго положительные, число повторов — неотрицательное (финальное ревью M-4).""" + for name, required in REQUIRED_ENV.items(): + clean_env.setenv(name, required) + clean_env.setenv(key, value) + with pytest.raises(ValidationError, match=key): + Settings(_env_file=None) # type: ignore[call-arg] + + +def test_zero_republish_allowed(clean_env: pytest.MonkeyPatch) -> None: + """OUTBOX_MAX_REPUBLISH=0 допустим: без повторов, сразу dlq после ack-таймаута.""" + for name, required in REQUIRED_ENV.items(): + clean_env.setenv(name, required) + clean_env.setenv("OUTBOX_MAX_REPUBLISH", "0") + assert Settings(_env_file=None).OUTBOX_MAX_REPUBLISH == 0 # type: ignore[call-arg]