"""Unit tests for app.config (SETUP-03). These tests exercise `app.config.load_settings()` directly, using only monkeypatched (fake) environment values -- never the project's real `.env` or real secrets. """ from __future__ import annotations import pytest from app.config import ConfigError, load_settings VALID_ENV = { "DB_HOST": "test-db-host.invalid", "DB_PORT": "3306", "DB_NAME": "test_db", "DB_USER": "test_user", "DB_PASSWORD": "test-password-not-real", "DATABASE_URL": ( "mysql+pymysql://test_user:test-password-not-real" "@test-db-host.invalid:3306/test_db" ), "APP_PORT": "8000", "ALLOWED_ORIGINS": "https://example.test,https://other.example.test", "FRAUD_ALERT_THRESHOLD": "0.5", "MODEL_PATH": "training/artifacts/model_v1.pkl", "SEED_OFFICER_EMAIL": "officer@example.test", "SEED_OFFICER_PASSWORD": "fake-seed-password-not-real", "SESSION_EXPIRE_MINUTES": "1440", "KAGGLE_USERNAME": "fake-kaggle-user", "KAGGLE_KEY": "fake-kaggle-key-not-real", } @pytest.fixture def valid_env(monkeypatch: pytest.MonkeyPatch) -> None: """Set a complete, fully-fake, valid environment for each test.""" for key, value in VALID_ENV.items(): monkeypatch.setenv(key, value) def test_complete_env_yields_typed_settings(valid_env: None) -> None: settings = load_settings() assert settings.DB_HOST == VALID_ENV["DB_HOST"] assert settings.DB_PORT == 3306 assert isinstance(settings.DB_PORT, int) assert settings.DB_NAME == VALID_ENV["DB_NAME"] assert settings.DB_USER == VALID_ENV["DB_USER"] assert settings.DB_PASSWORD == VALID_ENV["DB_PASSWORD"] assert settings.DATABASE_URL == VALID_ENV["DATABASE_URL"] assert settings.APP_PORT == 8000 assert isinstance(settings.APP_PORT, int) assert settings.ALLOWED_ORIGINS == [ "https://example.test", "https://other.example.test", ] assert settings.FRAUD_ALERT_THRESHOLD == pytest.approx(0.5) assert isinstance(settings.FRAUD_ALERT_THRESHOLD, float) assert settings.MODEL_PATH == VALID_ENV["MODEL_PATH"] assert settings.SEED_OFFICER_EMAIL == VALID_ENV["SEED_OFFICER_EMAIL"] assert settings.SEED_OFFICER_PASSWORD == VALID_ENV["SEED_OFFICER_PASSWORD"] assert settings.SESSION_EXPIRE_MINUTES == 1440 assert isinstance(settings.SESSION_EXPIRE_MINUTES, int) assert settings.KAGGLE_USERNAME == VALID_ENV["KAGGLE_USERNAME"] assert settings.KAGGLE_KEY == VALID_ENV["KAGGLE_KEY"] @pytest.mark.parametrize( "missing_var", [ "DB_HOST", "DB_NAME", "DB_USER", "DB_PASSWORD", "DATABASE_URL", "ALLOWED_ORIGINS", "FRAUD_ALERT_THRESHOLD", "MODEL_PATH", "DB_PORT", "SEED_OFFICER_EMAIL", "SEED_OFFICER_PASSWORD", ], ) def test_missing_required_var_raises_config_error_naming_the_variable( valid_env: None, monkeypatch: pytest.MonkeyPatch, missing_var: str ) -> None: monkeypatch.delenv(missing_var, raising=False) with pytest.raises(ConfigError) as exc_info: load_settings() message = str(exc_info.value) assert missing_var in message # Never echo secret-ish values in the error message. assert VALID_ENV["DB_PASSWORD"] not in message assert VALID_ENV["KAGGLE_KEY"] not in message def test_out_of_range_fraud_alert_threshold_raises_config_error( valid_env: None, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("FRAUD_ALERT_THRESHOLD", "1.5") with pytest.raises(ConfigError) as exc_info: load_settings() assert "FRAUD_ALERT_THRESHOLD" in str(exc_info.value) def test_non_numeric_fraud_alert_threshold_raises_config_error( valid_env: None, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("FRAUD_ALERT_THRESHOLD", "not-a-number") with pytest.raises(ConfigError) as exc_info: load_settings() assert "FRAUD_ALERT_THRESHOLD" in str(exc_info.value) def test_unparseable_db_port_raises_config_error( valid_env: None, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("DB_PORT", "not-an-int") with pytest.raises(ConfigError) as exc_info: load_settings() assert "DB_PORT" in str(exc_info.value) def test_missing_app_port_falls_back_to_default( valid_env: None, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.delenv("APP_PORT", raising=False) settings = load_settings() assert settings.APP_PORT == 8000 def test_unparseable_app_port_raises_config_error( valid_env: None, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv("APP_PORT", "not-an-int") with pytest.raises(ConfigError) as exc_info: load_settings() assert "APP_PORT" in str(exc_info.value) def test_missing_session_expire_minutes_falls_back_to_default( valid_env: None, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.delenv("SESSION_EXPIRE_MINUTES", raising=False) settings = load_settings() assert settings.SESSION_EXPIRE_MINUTES == 1440