SimpleChatbot / hermes_overlay /tests /test_config_validation.py
Amin
Deploy: HermesFace finalized project to HF Space
2e658e7
Raw
History Blame Contribute Delete
6.63 kB
"""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)