Files
upo-app-agents/tests/agents/test_market.py
Simone Garau dfca44c9d5 ToDo:
1. Aggiungere un aggregator per i dati recuperati dai provider.
2. Lavorare effettivamente all'issue

Done:
1. creati test per i provider
2. creato market_providers_api_demo.py per mostrare i dati recuperati dalle api dei providers
3. aggiornato i provider
4. creato il provider binance sia pubblico che con chiave
5. creato error_handler.py per gestire decoratori e utilità: retry automatico, gestione timeout...
2025-09-29 21:28:41 +02:00

597 lines
23 KiB
Python

"""
Test suite completo per il sistema di mercato.
Questo modulo testa approfonditamente tutte le implementazioni di BaseWrapper
e verifica la conformità all'interfaccia definita in base.py.
"""
import os
import pytest
from unittest.mock import Mock, patch, MagicMock
from typing import Type, List
# Import delle classi da testare
from app.markets.base import BaseWrapper, ProductInfo, Price
from app.markets.coinbase import CoinBaseWrapper
from app.markets.cryptocompare import CryptoCompareWrapper
from app.markets.binance import BinanceWrapper
from app.markets.binance_public import PublicBinanceAgent
from app.markets import MarketAPIs
class TestBaseWrapperInterface:
"""Test per verificare che tutte le implementazioni rispettino l'interfaccia BaseWrapper."""
def test_all_wrappers_extend_basewrapper(self):
"""Verifica che tutte le classi wrapper estendano BaseWrapper."""
wrapper_classes = [
CoinBaseWrapper,
CryptoCompareWrapper,
BinanceWrapper,
PublicBinanceAgent,
MarketAPIs
]
for wrapper_class in wrapper_classes:
assert issubclass(wrapper_class, BaseWrapper), f"{wrapper_class.__name__} deve estendere BaseWrapper"
def test_all_wrappers_implement_required_methods(self):
"""Verifica che tutte le classi implementino i metodi richiesti dall'interfaccia."""
wrapper_classes = [
CoinBaseWrapper,
CryptoCompareWrapper,
BinanceWrapper,
PublicBinanceAgent,
MarketAPIs
]
required_methods = ['get_product', 'get_products', 'get_all_products', 'get_historical_prices']
for wrapper_class in wrapper_classes:
for method in required_methods:
assert hasattr(wrapper_class, method), f"{wrapper_class.__name__} deve implementare {method}"
assert callable(getattr(wrapper_class, method)), f"{method} deve essere callable in {wrapper_class.__name__}"
class TestProductInfoModel:
"""Test per la classe ProductInfo e i suoi metodi di conversione."""
def test_productinfo_initialization(self):
"""Test inizializzazione di ProductInfo."""
product = ProductInfo()
assert product.id == ""
assert product.symbol == ""
assert product.price == 0.0
assert product.volume_24h == 0.0
assert product.status == ""
assert product.quote_currency == ""
def test_productinfo_with_data(self):
"""Test ProductInfo con dati specifici."""
product = ProductInfo(
id="BTC-USD",
symbol="BTC",
price=50000.0,
volume_24h=1000000.0,
status="TRADING",
quote_currency="USD"
)
assert product.id == "BTC-USD"
assert product.symbol == "BTC"
assert product.price == 50000.0
assert product.volume_24h == 1000000.0
assert product.status == "TRADING"
assert product.quote_currency == "USD"
def test_productinfo_from_cryptocompare(self):
"""Test conversione da dati CryptoCompare."""
mock_data = {
'FROMSYMBOL': 'BTC',
'TOSYMBOL': 'USD',
'PRICE': 50000.0,
'VOLUME24HOUR': 1000000.0
}
product = ProductInfo.from_cryptocompare(mock_data)
assert product.id == "BTC-USD"
assert product.symbol == "BTC"
assert product.price == 50000.0
assert product.volume_24h == 1000000.0
assert product.status == ""
def test_productinfo_from_binance(self):
"""Test conversione da dati Binance."""
ticker_data = {'symbol': 'BTCUSDT', 'price': '50000.0'}
ticker_24h_data = {'volume': '1000000.0'}
product = ProductInfo.from_binance(ticker_data, ticker_24h_data)
assert product.id == "BTCUSDT"
assert product.symbol == "BTC"
assert product.price == 50000.0
assert product.volume_24h == 1000000.0
assert product.status == "TRADING"
assert product.quote_currency == "USDT"
class TestPriceModel:
"""Test per la classe Price e i suoi metodi di conversione."""
def test_price_initialization(self):
"""Test inizializzazione di Price."""
price = Price()
assert price.high == 0.0
assert price.low == 0.0
assert price.open == 0.0
assert price.close == 0.0
assert price.volume == 0.0
assert price.time == ""
def test_price_with_data(self):
"""Test Price con dati specifici."""
price = Price(
high=51000.0,
low=49000.0,
open=50000.0,
close=50500.0,
volume=1000.0,
time="2024-01-01T00:00:00Z"
)
assert price.high == 51000.0
assert price.low == 49000.0
assert price.open == 50000.0
assert price.close == 50500.0
assert price.volume == 1000.0
assert price.time == "2024-01-01T00:00:00Z"
def test_price_from_cryptocompare(self):
"""Test conversione da dati CryptoCompare."""
mock_data = {
'high': 51000.0,
'low': 49000.0,
'open': 50000.0,
'close': 50500.0,
'volumeto': 1000.0,
'time': 1704067200
}
price = Price.from_cryptocompare(mock_data)
assert price.high == 51000.0
assert price.low == 49000.0
assert price.open == 50000.0
assert price.close == 50500.0
assert price.volume == 1000.0
assert price.time == "1704067200"
class TestCoinBaseWrapper:
"""Test specifici per CoinBaseWrapper."""
@pytest.mark.skipif(
not (os.getenv('COINBASE_API_KEY') and os.getenv('COINBASE_API_SECRET')),
reason="Credenziali Coinbase non configurate"
)
def test_coinbase_initialization_with_env_vars(self):
"""Test inizializzazione con variabili d'ambiente."""
wrapper = CoinBaseWrapper(currency="USD")
assert wrapper.currency == "USD"
assert wrapper.client is not None
@patch.dict(os.environ, {}, clear=True)
def test_coinbase_initialization_with_params(self):
"""Test inizializzazione con parametri espliciti quando non ci sono variabili d'ambiente."""
with pytest.raises(AssertionError, match="API key is required"):
CoinBaseWrapper(api_key=None, api_private_key=None)
@patch('app.markets.coinbase.RESTClient')
def test_coinbase_asset_formatting_behavior(self, mock_client):
"""Test comportamento di formattazione asset ID attraverso get_product."""
mock_response = Mock()
mock_response.product_id = "BTC-USD"
mock_response.base_currency_id = "BTC"
mock_response.price = "50000.0"
mock_response.volume_24h = "1000000.0"
mock_response.status = "TRADING"
mock_client_instance = Mock()
mock_client_instance.get_product.return_value = mock_response
mock_client.return_value = mock_client_instance
wrapper = CoinBaseWrapper(api_key="test", api_private_key="test")
# Test che entrambi i formati funzionino
wrapper.get_product("BTC")
wrapper.get_product("BTC-USD")
# Verifica che get_product sia stato chiamato con il formato corretto
assert mock_client_instance.get_product.call_count == 2
@patch('app.markets.coinbase.RESTClient')
def test_coinbase_get_product(self, mock_client):
"""Test get_product con mock."""
mock_response = Mock()
mock_response.product_id = "BTC-USD"
mock_response.base_currency_id = "BTC"
mock_response.price = "50000.0"
mock_response.volume_24h = "1000000.0"
mock_response.status = "TRADING"
mock_client_instance = Mock()
mock_client_instance.get_product.return_value = mock_response
mock_client.return_value = mock_client_instance
wrapper = CoinBaseWrapper(api_key="test", api_private_key="test")
product = wrapper.get_product("BTC")
assert isinstance(product, ProductInfo)
assert product.symbol == "BTC"
mock_client_instance.get_product.assert_called_once_with("BTC-USD")
class TestCryptoCompareWrapper:
"""Test specifici per CryptoCompareWrapper."""
@pytest.mark.skipif(
not os.getenv('CRYPTOCOMPARE_API_KEY'),
reason="CRYPTOCOMPARE_API_KEY non configurata"
)
def test_cryptocompare_initialization_with_env_var(self):
"""Test inizializzazione con variabile d'ambiente."""
wrapper = CryptoCompareWrapper(currency="USD")
assert wrapper.currency == "USD"
assert wrapper.api_key is not None
def test_cryptocompare_initialization_with_param(self):
"""Test inizializzazione con parametro esplicito."""
wrapper = CryptoCompareWrapper(api_key="test_key", currency="EUR")
assert wrapper.api_key == "test_key"
assert wrapper.currency == "EUR"
@patch('app.markets.cryptocompare.requests.get')
def test_cryptocompare_get_product(self, mock_get):
"""Test get_product con mock."""
mock_response = Mock()
mock_response.json.return_value = {
'RAW': {
'BTC': {
'USD': {
'FROMSYMBOL': 'BTC',
'TOSYMBOL': 'USD',
'PRICE': 50000.0,
'VOLUME24HOUR': 1000000.0
}
}
}
}
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
wrapper = CryptoCompareWrapper(api_key="test_key")
product = wrapper.get_product("BTC")
assert isinstance(product, ProductInfo)
assert product.symbol == "BTC"
assert product.price == 50000.0
def test_cryptocompare_get_all_products_workaround(self):
"""Test che get_all_products funzioni con il workaround implementato."""
wrapper = CryptoCompareWrapper(api_key="test_key")
# Il metodo ora dovrebbe restituire una lista di ProductInfo invece di sollevare NotImplementedError
products = wrapper.get_all_products()
assert isinstance(products, list)
# Verifica che la lista non sia vuota (dovrebbe contenere almeno alcuni asset popolari)
assert len(products) > 0
# Verifica che ogni elemento sia un ProductInfo
for product in products:
assert isinstance(product, ProductInfo)
class TestBinanceWrapper:
"""Test specifici per BinanceWrapper."""
def test_binance_initialization_without_credentials(self):
"""Test che l'inizializzazione fallisca senza credenziali."""
# Assicuriamoci che le variabili d'ambiente siano vuote per questo test
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(AssertionError, match="API key is required"):
BinanceWrapper(api_key=None, api_secret="test")
with pytest.raises(AssertionError, match="API secret is required"):
BinanceWrapper(api_key="test", api_secret=None)
@patch('app.markets.binance.Client')
def test_binance_symbol_formatting_behavior(self, mock_client):
"""Test comportamento di formattazione simbolo attraverso get_product."""
mock_client_instance = Mock()
mock_client_instance.get_symbol_ticker.return_value = {
'symbol': 'BTCUSDT',
'price': '50000.0'
}
mock_client_instance.get_ticker.return_value = {
'volume': '1000000.0'
}
mock_client.return_value = mock_client_instance
wrapper = BinanceWrapper(api_key="test", api_secret="test")
# Test che entrambi i formati funzionino
wrapper.get_product("BTC")
wrapper.get_product("BTCUSDT")
# Verifica che i metodi siano stati chiamati
assert mock_client_instance.get_symbol_ticker.call_count == 2
@patch('app.markets.binance.Client')
def test_binance_get_product(self, mock_client):
"""Test get_product con mock."""
mock_client_instance = Mock()
mock_client_instance.get_symbol_ticker.return_value = {
'symbol': 'BTCUSDT',
'price': '50000.0'
}
mock_client_instance.get_ticker.return_value = {
'volume': '1000000.0'
}
mock_client.return_value = mock_client_instance
wrapper = BinanceWrapper(api_key="test", api_secret="test")
product = wrapper.get_product("BTC")
assert isinstance(product, ProductInfo)
assert product.symbol == "BTC"
assert product.price == 50000.0
class TestPublicBinanceAgent:
"""Test specifici per PublicBinanceAgent."""
@patch('app.markets.binance_public.Client')
def test_public_binance_initialization(self, mock_client):
"""Test inizializzazione senza credenziali."""
agent = PublicBinanceAgent()
assert agent.client is not None
mock_client.assert_called_once_with()
@patch('app.markets.binance_public.Client')
def test_public_binance_symbol_formatting_behavior(self, mock_client):
"""Test comportamento di formattazione simbolo attraverso get_product."""
mock_client_instance = Mock()
mock_client_instance.get_symbol_ticker.return_value = {
'symbol': 'BTCUSDT',
'price': '50000.0'
}
mock_client_instance.get_ticker.return_value = {
'volume': '1000000.0'
}
mock_client.return_value = mock_client_instance
agent = PublicBinanceAgent()
# Test che entrambi i formati funzionino
agent.get_product("BTC")
agent.get_product("BTCUSDT")
# Verifica che i metodi siano stati chiamati
assert mock_client_instance.get_symbol_ticker.call_count == 2
@patch('app.markets.binance_public.Client')
def test_public_binance_get_product(self, mock_client):
"""Test get_product con mock."""
mock_client_instance = Mock()
mock_client_instance.get_symbol_ticker.return_value = {
'symbol': 'BTCUSDT',
'price': '50000.0'
}
mock_client_instance.get_ticker.return_value = {
'volume': '1000000.0'
}
mock_client.return_value = mock_client_instance
agent = PublicBinanceAgent()
product = agent.get_product("BTC")
assert isinstance(product, ProductInfo)
assert product.symbol == "BTC"
assert product.price == 50000.0
@patch('app.markets.binance_public.Client')
def test_public_binance_get_all_products(self, mock_client):
"""Test get_all_products restituisce asset principali."""
mock_client_instance = Mock()
mock_client_instance.get_symbol_ticker.return_value = {
'symbol': 'BTCUSDT',
'price': '50000.0'
}
mock_client_instance.get_ticker.return_value = {
'volume': '1000000.0'
}
mock_client.return_value = mock_client_instance
agent = PublicBinanceAgent()
products = agent.get_all_products()
assert isinstance(products, list)
assert len(products) == 8 # Numero di asset principali definiti
for product in products:
assert isinstance(product, ProductInfo)
@patch('app.markets.binance_public.Client')
def test_public_binance_get_public_prices(self, mock_client):
"""Test metodo specifico get_public_prices."""
mock_client_instance = Mock()
mock_client_instance.get_symbol_ticker.return_value = {'price': '50000.0'}
mock_client_instance.get_server_time.return_value = {'serverTime': 1704067200000}
mock_client.return_value = mock_client_instance
agent = PublicBinanceAgent()
prices = agent.get_public_prices(["BTCUSDT"])
assert isinstance(prices, dict)
assert 'BTC_USD' in prices
assert prices['BTC_USD'] == 50000.0
assert 'source' in prices
assert prices['source'] == 'binance_public'
class TestMarketAPIs:
"""Test per la classe MarketAPIs che aggrega i wrapper."""
def test_market_apis_initialization_no_providers(self):
"""Test che l'inizializzazione fallisca senza provider disponibili."""
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(AssertionError, match="No market API keys"):
MarketAPIs("USD")
@patch('app.markets.CoinBaseWrapper')
def test_market_apis_with_coinbase_only(self, mock_coinbase):
"""Test con solo Coinbase disponibile."""
mock_instance = Mock()
mock_coinbase.return_value = mock_instance
with patch('app.markets.CryptoCompareWrapper', side_effect=Exception("No API key")):
apis = MarketAPIs("USD")
assert len(apis.wrappers) == 1
assert apis.wrappers[0] == mock_instance
@patch('app.markets.CoinBaseWrapper')
@patch('app.markets.CryptoCompareWrapper')
def test_market_apis_delegation(self, mock_crypto, mock_coinbase):
"""Test che i metodi vengano delegati al primo wrapper disponibile."""
mock_coinbase_instance = Mock()
mock_crypto_instance = Mock()
mock_coinbase.return_value = mock_coinbase_instance
mock_crypto.return_value = mock_crypto_instance
apis = MarketAPIs("USD")
# Test delegazione get_product
apis.get_product("BTC")
mock_coinbase_instance.get_product.assert_called_once_with("BTC")
# Test delegazione get_products
apis.get_products(["BTC", "ETH"])
mock_coinbase_instance.get_products.assert_called_once_with(["BTC", "ETH"])
# Test delegazione get_all_products
apis.get_all_products()
mock_coinbase_instance.get_all_products.assert_called_once()
# Test delegazione get_historical_prices
apis.get_historical_prices("BTC")
mock_coinbase_instance.get_historical_prices.assert_called_once_with("BTC")
class TestErrorHandling:
"""Test per la gestione degli errori in tutti i wrapper."""
@patch('app.markets.binance_public.Client')
def test_public_binance_error_handling(self, mock_client):
"""Test gestione errori in PublicBinanceAgent."""
mock_client_instance = Mock()
mock_client_instance.get_symbol_ticker.side_effect = Exception("API Error")
mock_client.return_value = mock_client_instance
agent = PublicBinanceAgent()
product = agent.get_product("INVALID")
# Dovrebbe restituire un ProductInfo vuoto invece di sollevare eccezione
assert isinstance(product, ProductInfo)
assert product.id == "INVALID"
assert product.symbol == "INVALID"
@patch('app.markets.cryptocompare.requests.get')
def test_cryptocompare_network_error(self, mock_get):
"""Test gestione errori di rete in CryptoCompareWrapper."""
mock_get.side_effect = Exception("Network Error")
wrapper = CryptoCompareWrapper(api_key="test")
with pytest.raises(Exception):
wrapper.get_product("BTC")
@patch('app.markets.binance.Client')
def test_binance_api_error_in_get_products(self, mock_client):
"""Test gestione errori in BinanceWrapper.get_products."""
mock_client_instance = Mock()
mock_client_instance.get_symbol_ticker.side_effect = Exception("API Error")
mock_client.return_value = mock_client_instance
wrapper = BinanceWrapper(api_key="test", api_secret="test")
products = wrapper.get_products(["BTC", "ETH"])
# Dovrebbe restituire lista vuota invece di sollevare eccezione
assert isinstance(products, list)
assert len(products) == 0
class TestIntegrationScenarios:
"""Test di integrazione per scenari reali."""
def test_wrapper_method_signatures(self):
"""Verifica che tutti i wrapper abbiano le stesse signature dei metodi."""
wrapper_classes = [CoinBaseWrapper, CryptoCompareWrapper, BinanceWrapper, PublicBinanceAgent]
for wrapper_class in wrapper_classes:
# Verifica get_product
assert hasattr(wrapper_class, 'get_product')
# Verifica get_products
assert hasattr(wrapper_class, 'get_products')
# Verifica get_all_products
assert hasattr(wrapper_class, 'get_all_products')
# Verifica get_historical_prices
assert hasattr(wrapper_class, 'get_historical_prices')
def test_productinfo_consistency(self):
"""Test che tutti i metodi from_* di ProductInfo restituiscano oggetti consistenti."""
# Test from_cryptocompare
crypto_data = {
'FROMSYMBOL': 'BTC',
'TOSYMBOL': 'USD',
'PRICE': 50000.0,
'VOLUME24HOUR': 1000000.0
}
crypto_product = ProductInfo.from_cryptocompare(crypto_data)
# Test from_binance
binance_ticker = {'symbol': 'BTCUSDT', 'price': '50000.0'}
binance_24h = {'volume': '1000000.0'}
binance_product = ProductInfo.from_binance(binance_ticker, binance_24h)
# Verifica che entrambi abbiano gli stessi campi
assert hasattr(crypto_product, 'id')
assert hasattr(crypto_product, 'symbol')
assert hasattr(crypto_product, 'price')
assert hasattr(crypto_product, 'volume_24h')
assert hasattr(binance_product, 'id')
assert hasattr(binance_product, 'symbol')
assert hasattr(binance_product, 'price')
assert hasattr(binance_product, 'volume_24h')
def test_price_consistency(self):
"""Test che tutti i metodi from_* di Price restituiscano oggetti consistenti."""
# Test from_cryptocompare
crypto_data = {
'high': 51000.0,
'low': 49000.0,
'open': 50000.0,
'close': 50500.0,
'volumeto': 1000.0,
'time': 1704067200
}
crypto_price = Price.from_cryptocompare(crypto_data)
# Verifica che abbia tutti i campi richiesti
assert hasattr(crypto_price, 'high')
assert hasattr(crypto_price, 'low')
assert hasattr(crypto_price, 'open')
assert hasattr(crypto_price, 'close')
assert hasattr(crypto_price, 'volume')
assert hasattr(crypto_price, 'time')
if __name__ == "__main__":
pytest.main([__file__, "-v"])