"""Typed and bounded configuration validation tests.""" import math import sys from pathlib import Path import pytest sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from trading.config_validation import ( ConfigurationError, assert_safe_for_startup, validate_configuration, validate_trading_env, ) def test_default_is_safe(monkeypatch): for key in ( "TRADING_MODE", "HERMES_EXECUTION_ENABLED", "FUTURES_API_KEY", "FUTURES_API_SECRET", "HERMES_NONPAPER_EXECUTION_ENABLED", ): monkeypatch.delenv(key, raising=False) safe, msgs = validate_trading_env() assert safe is True assert any("new entries are blocked" in m for m in msgs) def test_invalid_mode_raises(monkeypatch): monkeypatch.setenv("TRADING_MODE", "bogus") with pytest.raises(ConfigurationError, match="TRADING_MODE"): assert_safe_for_startup() @pytest.mark.parametrize("value", ["0", "-1", "nan", "inf", "-inf", "0.5"]) def test_sync_interval_rejects_zero_negative_nonfinite_and_fractional(value): report = validate_configuration({"TRADING_MODE": "paper", "SYNC_INTERVAL": value}) assert not report.safe assert any("SYNC_INTERVAL" in e for e in report.errors) @pytest.mark.parametrize("key,value", [ ("HERMES_MAX_RISK_PERCENT", "0"), ("HERMES_MAX_RISK_PERCENT", "11"), ("HERMES_MAX_LEVERAGE", "126"), ("HERMES_MAX_OPEN_POSITIONS", "0"), ("HERMES_PROVIDER_MAX_AGE_S", "nan"), ]) def test_critical_numeric_bounds(key, value): report = validate_configuration({"TRADING_MODE": "paper", key: value}) assert not report.safe assert any(key in e for e in report.errors) def test_invalid_boolean_is_rejected(): report = validate_configuration({"TRADING_MODE": "paper", "HERMES_EXECUTION_ENABLED": "perhaps"}) assert not report.safe assert "HERMES_EXECUTION_ENABLED must be a boolean" in report.errors def test_default_leverage_cannot_exceed_maximum(): report = validate_configuration({ "TRADING_MODE": "paper", "FUTURES_DEFAULT_LEVERAGE": "20", "HERMES_MAX_LEVERAGE": "10", }) assert not report.safe assert any("must not exceed" in e for e in report.errors) def test_nonpaper_fully_armed_is_rejected_without_secret_leak(): report = validate_configuration({ "TRADING_MODE": "live", "HERMES_EXECUTION_ENABLED": "true", "HERMES_NONPAPER_EXECUTION_ENABLED": "true", "FUTURES_API_KEY": "super-secret-key", "FUTURES_API_SECRET": "super-secret-value", }) assert not report.safe rendered = " ".join(report.messages) assert "super-secret" not in rendered assert "non-Paper execution is prohibited" in rendered def test_production_requires_session_admin_home_and_origins(): report = validate_configuration({"TRADING_MODE": "paper", "HERMES_ENV": "production"}) assert not report.safe for key in ("HERMES_ADMIN_PASSWORD", "HERMES_SESSION_SECRET", "HERMES_HOME", "HERMES_ALLOWED_ORIGINS"): assert any(key in e for e in report.errors) def test_paper_with_execution_enabled_remains_safe(): report = validate_configuration({"TRADING_MODE": "paper", "HERMES_EXECUTION_ENABLED": "true"}) assert report.safe def test_invalid_url_and_wildcard_origin_are_rejected(): report = validate_configuration({ "TRADING_MODE": "paper", "DS4_BASE_URL": "file:///tmp/data", "HERMES_ALLOWED_ORIGINS": "*", }) assert not report.safe assert any("DS4_BASE_URL" in e for e in report.errors) assert any("wildcard" in e for e in report.errors) @pytest.mark.parametrize("key,value", [ ("HERMES_ENV", "prod-ish"), ("FUTURES_EXCHANGE_ID", "binance"), ("TELEGRAM_MODE", "polling"), ("HERMES_SESSION_TTL_SECONDS", "59"), ("HERMES_RECONCILIATION_INTERVAL_SECONDS", "0"), ]) def test_execution_critical_enums_and_intervals_are_bounded(key, value): report = validate_configuration({"TRADING_MODE": "paper", key: value}) assert not report.safe assert any(key in error for error in report.errors) def test_any_nonpaper_mode_is_rejected_even_without_credentials(): report = validate_configuration({"TRADING_MODE": "testnet"}) assert not report.safe assert any("non-Paper trading modes are disabled" in error for error in report.errors) def test_production_requires_authoritative_database_and_provider(): report = validate_configuration({ "TRADING_MODE": "paper", "HERMES_ENV": "production", "HERMES_ADMIN_PASSWORD": "not-a-real-secret", "HERMES_SESSION_SECRET": "x" * 32, "HERMES_HOME": "/opt/data", "HERMES_ALLOWED_ORIGINS": "https://example.invalid", }) assert not report.safe assert any("HERMES_DATABASE_URL" in error for error in report.errors) assert any("DS4_BASE_URL" in error for error in report.errors) def _production_env(database_url: str) -> dict[str, str]: return { "TRADING_MODE": "paper", "HERMES_ENV": "production", "HERMES_ADMIN_PASSWORD": "not-a-real-secret", "HERMES_SESSION_SECRET": "x" * 32, "HERMES_AUDIT_SIGNING_SECRET": "a" * 32, "HERMES_CSRF_SIGNING_SECRET": "c" * 32, "HERMES_HOME": "/opt/data", "HERMES_ALLOWED_ORIGINS": "https://example.invalid", "HERMES_DATABASE_URL": database_url, "DS4_BASE_URL": "https://provider.example.invalid", } def test_production_rejects_sqlite_even_for_paper_mode(): report = validate_configuration(_production_env("sqlite:////opt/data/hermes.sqlite3")) assert not report.safe assert "production requires a PostgreSQL HERMES_DATABASE_URL" in report.errors def test_production_accepts_postgresql_architecture_with_execution_disabled(): report = validate_configuration( _production_env("postgresql://hermes:placeholder@db.example.invalid/hermes") ) assert report.safe assert any("new entries are blocked" in warning for warning in report.warnings) def test_database_dialect_must_match_url(): report = validate_configuration({ "TRADING_MODE": "paper", "HERMES_DATABASE_DIALECT": "postgresql", "HERMES_DATABASE_URL": "sqlite:////tmp/hermes.sqlite3", }) assert not report.safe assert "HERMES_DATABASE_DIALECT must match HERMES_DATABASE_URL" in report.errors def test_in_memory_sqlite_is_rejected_for_durable_state(): report = validate_configuration({ "TRADING_MODE": "paper", "HERMES_DATABASE_URL": "sqlite:///:memory:", }) assert not report.safe assert any("in-memory SQLite" in error for error in report.errors)