Spaces:
Sleeping
Sleeping
File size: 6,634 Bytes
2e658e7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | """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)
|