from unittest.mock import MagicMock import pytest from pydantic import ValidationError from src.config import get_settings, presigned_url_ttl_seconds @pytest.fixture(autouse=True) def _clear_settings_cache() -> None: get_settings.cache_clear() def _set_required_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("ENVIRONMENT", "dev") monkeypatch.setenv("SQS_QUEUE_URL", "https://sqs.us-east-1.amazonaws.com/123/queue") monkeypatch.setenv("MESSAGE_PROCESSING_TIMEOUT_MINUTES", "30") monkeypatch.setenv("HEARTBEAT_INTERVAL_SECONDS", "60") class TestGetSettings: def test_loads_settings(self, monkeypatch: pytest.MonkeyPatch) -> None: _set_required_env(monkeypatch) monkeypatch.delenv("SENTRY_DSN", raising=False) settings = get_settings() assert settings.ENVIRONMENT == "dev" assert settings.LOG_LEVEL == "INFO" assert settings.SENTRY_DSN is None assert settings.SQS_QUEUE_URL == "https://sqs.us-east-1.amazonaws.com/123/queue" def test_presigned_url_ttl_seconds_is_minutes_times_60(self, monkeypatch: pytest.MonkeyPatch) -> None: _set_required_env(monkeypatch) # MESSAGE_PROCESSING_TIMEOUT_MINUTES = 30 assert presigned_url_ttl_seconds() == 1800 @pytest.mark.parametrize( "missing_var", ["ENVIRONMENT", "SQS_QUEUE_URL", "MESSAGE_PROCESSING_TIMEOUT_MINUTES", "HEARTBEAT_INTERVAL_SECONDS"], ) def test_requires_env_var(self, monkeypatch: pytest.MonkeyPatch, missing_var: str) -> None: _set_required_env(monkeypatch) monkeypatch.delenv(missing_var, raising=False) with pytest.raises(ValidationError): get_settings() def test_rejects_invalid_environment(self, monkeypatch: pytest.MonkeyPatch) -> None: _set_required_env(monkeypatch) monkeypatch.setenv("ENVIRONMENT", "staging") with pytest.raises(ValidationError): get_settings() def test_non_dev_requires_sentry_dsn(self, monkeypatch: pytest.MonkeyPatch) -> None: _set_required_env(monkeypatch) monkeypatch.setenv("ENVIRONMENT", "prod") monkeypatch.delenv("SENTRY_DSN", raising=False) with pytest.raises(ValidationError, match="SENTRY_DSN is required in prod"): get_settings() def test_initializes_sentry_when_dsn_set(self, monkeypatch: pytest.MonkeyPatch) -> None: _set_required_env(monkeypatch) mock_init = MagicMock() monkeypatch.setattr("src.config.sentry_sdk.init", mock_init) monkeypatch.setenv("ENVIRONMENT", "prod") monkeypatch.setenv("SENTRY_DSN", "https://key@sentry.io/123") settings = get_settings() assert settings.ENVIRONMENT == "prod" mock_init.assert_called_once_with( dsn="https://key@sentry.io/123", environment="prod", ) def test_dev_skips_sentry_when_dsn_not_set(self, monkeypatch: pytest.MonkeyPatch) -> None: _set_required_env(monkeypatch) mock_init = MagicMock() monkeypatch.setattr("src.config.sentry_sdk.init", mock_init) monkeypatch.delenv("SENTRY_DSN", raising=False) get_settings() mock_init.assert_not_called() class TestNonEmptyStringValidation: @pytest.mark.parametrize( "value", [ pytest.param("", id="empty"), pytest.param(" ", id="whitespace_only"), ], ) def test_rejects_blank_queue_url(self, monkeypatch: pytest.MonkeyPatch, value: str) -> None: _set_required_env(monkeypatch) monkeypatch.setenv("SQS_QUEUE_URL", value) with pytest.raises(ValidationError): get_settings()