Spaces:
Sleeping
Sleeping
| """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() | |
| 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) | |
| 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) | |
| 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) | |