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)