- Fix TOCTOU race conditions by wrapping read-modify-write cycles under single-file locks in execution_state, portfolio_service, precheck_state, state_manager, and precheck_service. - Add missing test coverage (96 tests total): - test_review_service.py (15 tests) - test_check_api.py (6 tests) - test_external_gate.py main branches (+10 tests) - test_trade_execution.py new commands (+8 tests) - Unify all agent-consumed JSON messages to English. - Config-ize hardcoded values (volume filter, schema_version) via get_user_config with sensible defaults. - Add 1-hour TTL to exchange cache with force_new override. - Add ruff and mypy to dev dependencies; fix all type errors. - Add __all__ declarations to 11 service modules. - Sync README with new commands, config tuning docs, and PyPI badge. - Publish package as coinhunter==1.0.0 on PyPI with MIT license. - Deprecate coinhunter-cli==1.0.1 with runtime warning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
99 lines
3.0 KiB
Python
99 lines
3.0 KiB
Python
"""Tests for exchange helpers and caching."""
|
|
|
|
import pytest
|
|
|
|
from coinhunter.services import exchange_service as es
|
|
|
|
|
|
class TestNormSymbol:
|
|
def test_already_normalized(self):
|
|
assert es.norm_symbol("BTC/USDT") == "BTC/USDT"
|
|
|
|
def test_raw_symbol(self):
|
|
assert es.norm_symbol("BTCUSDT") == "BTC/USDT"
|
|
|
|
def test_lowercase(self):
|
|
assert es.norm_symbol("btcusdt") == "BTC/USDT"
|
|
|
|
def test_dash_separator(self):
|
|
assert es.norm_symbol("BTC-USDT") == "BTC/USDT"
|
|
|
|
def test_underscore_separator(self):
|
|
assert es.norm_symbol("BTC_USDT") == "BTC/USDT"
|
|
|
|
def test_unsupported_symbol(self):
|
|
with pytest.raises(ValueError):
|
|
es.norm_symbol("BTC")
|
|
|
|
|
|
class TestStorageSymbol:
|
|
def test_converts_to_flat(self):
|
|
assert es.storage_symbol("BTC/USDT") == "BTCUSDT"
|
|
|
|
def test_raw_input(self):
|
|
assert es.storage_symbol("ETHUSDT") == "ETHUSDT"
|
|
|
|
|
|
class TestFloorToStep:
|
|
def test_no_step(self):
|
|
assert es.floor_to_step(1.234, 0) == 1.234
|
|
|
|
def test_floors_to_step(self):
|
|
assert es.floor_to_step(1.234, 0.1) == pytest.approx(1.2)
|
|
|
|
def test_exact_multiple(self):
|
|
assert es.floor_to_step(12, 1) == 12
|
|
|
|
|
|
class TestGetExchangeCaching:
|
|
def test_returns_cached_instance(self, monkeypatch):
|
|
monkeypatch.setenv("BINANCE_API_KEY", "test_key")
|
|
monkeypatch.setenv("BINANCE_API_SECRET", "test_secret")
|
|
|
|
class FakeEx:
|
|
def load_markets(self):
|
|
pass
|
|
|
|
# Reset cache and patch ccxt.binance
|
|
original_cache = es._exchange_cache
|
|
original_cached_at = es._exchange_cached_at
|
|
try:
|
|
es._exchange_cache = None
|
|
es._exchange_cached_at = None
|
|
monkeypatch.setattr(es.ccxt, "binance", lambda *a, **kw: FakeEx())
|
|
|
|
first = es.get_exchange()
|
|
second = es.get_exchange()
|
|
assert first is second
|
|
|
|
third = es.get_exchange(force_new=True)
|
|
assert third is not first
|
|
finally:
|
|
es._exchange_cache = original_cache
|
|
es._exchange_cached_at = original_cached_at
|
|
|
|
def test_cache_expires_after_ttl(self, monkeypatch):
|
|
monkeypatch.setenv("BINANCE_API_KEY", "test_key")
|
|
monkeypatch.setenv("BINANCE_API_SECRET", "test_secret")
|
|
|
|
class FakeEx:
|
|
def load_markets(self):
|
|
pass
|
|
|
|
original_cache = es._exchange_cache
|
|
original_cached_at = es._exchange_cached_at
|
|
try:
|
|
es._exchange_cache = None
|
|
es._exchange_cached_at = None
|
|
monkeypatch.setattr(es.ccxt, "binance", lambda *a, **kw: FakeEx())
|
|
monkeypatch.setattr(es, "load_env", lambda: None)
|
|
|
|
first = es.get_exchange()
|
|
# Simulate time passing beyond TTL
|
|
es._exchange_cached_at -= es.CACHE_TTL_SECONDS + 1
|
|
second = es.get_exchange()
|
|
assert first is not second
|
|
finally:
|
|
es._exchange_cache = original_cache
|
|
es._exchange_cached_at = original_cached_at
|