Skip to content

Commit a6c9abe

Browse files
Fix aiohttp mocking to prevent real API calls to cryptocurrency exchanges
Co-authored-by: kiarashplusplus <1780945+kiarashplusplus@users.noreply.github.com>
1 parent e25f37d commit a6c9abe

1 file changed

Lines changed: 103 additions & 89 deletions

File tree

tests/conftest.py

Lines changed: 103 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -589,10 +589,13 @@ def __getattr__(self, name):
589589
@pytest.fixture(autouse=True)
590590
def mock_aiohttp_for_providers(request):
591591
"""
592-
Mock aiohttp.ClientSession.get for provider API calls to external services.
592+
Mock aiohttp.ClientSession for provider API calls to external services.
593593
594594
This prevents tests from making real HTTP requests to third-party APIs
595-
like CoinGecko, NewsAPI, Polygon, Finnhub, etc.
595+
like CoinGecko, NewsAPI, Polygon, Finnhub, and cryptocurrency exchanges.
596+
597+
The fixture mocks all HTTP methods (GET, POST, etc.) used by aiohttp
598+
to ensure CCXT and other libraries can't make real network calls.
596599
597600
Note: This only mocks specific financial data API domains, not all HTTP calls.
598601
Tests that need real API calls should use the @pytest.mark.live marker.
@@ -626,115 +629,126 @@ def mock_aiohttp_for_providers(request):
626629
"query1.finance.yahoo.com",
627630
"query2.finance.yahoo.com",
628631
"finance.yahoo.com",
629-
# Cryptocurrency exchanges
632+
# Cryptocurrency exchanges (CCXT uses these)
630633
"api.binance.com",
631634
"api.coinbase.com",
632635
"api.kraken.com",
633636
"api.bybit.com",
634637
"api.huobi.pro",
635638
"www.okx.com",
636-
# Additional CCXT exchanges
637639
"api.kucoin.com",
638640
"api.gateio.ws",
639641
"api.bitget.com",
642+
# Additional exchange domains that CCXT may use
643+
"api.exchange.coinbase.com",
644+
"api-pub.bitfinex.com",
645+
"api.gemini.com",
646+
"ftx.com",
647+
"api.pro.coinbase.com",
640648
]
641649

642-
# Create a mock response
643-
mock_response = MagicMock()
644-
mock_response.status = 200
645-
mock_response.json = AsyncMock(return_value={"status": "ok", "data": {}})
646-
mock_response.text = AsyncMock(return_value='{"status": "ok"}')
647-
mock_response.__aenter__ = AsyncMock(return_value=mock_response)
648-
mock_response.__aexit__ = AsyncMock(return_value=None)
649-
650-
original_get = None
651-
652-
def selective_mock_get(self, url, *args, **kwargs):
653-
"""Only mock calls to financial data provider domains"""
654-
url_str = str(url)
655-
656-
# Check if this is a call to a provider domain
650+
def create_mock_response():
651+
"""Create a fresh mock response for each call."""
652+
mock_resp = MagicMock()
653+
mock_resp.status = 200
654+
mock_resp.json = AsyncMock(return_value={"status": "ok", "data": {}})
655+
mock_resp.text = AsyncMock(return_value='{"status": "ok"}')
656+
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
657+
mock_resp.__aexit__ = AsyncMock(return_value=None)
658+
return mock_resp
659+
660+
def should_mock_url(url_str):
661+
"""Check if URL should be mocked based on domain."""
657662
for domain in provider_domains:
658663
if domain in url_str:
659-
# Return provider-specific mock data
660-
if "polygon.io" in url_str:
661-
mock_response.json.return_value = {
662-
"status": "OK",
663-
"results": [
664-
{
665-
"c": 150.0,
666-
"h": 155.0,
667-
"l": 145.0,
668-
"o": 148.0,
669-
"v": 1000000,
670-
"t": 1630000000000,
671-
}
672-
],
673-
}
674-
elif "alphavantage.co" in url_str:
675-
mock_response.json.return_value = {
676-
"Global Quote": {
677-
"01. symbol": "TSLA",
678-
"05. price": "150.00",
679-
"09. change": "2.00",
680-
"10. change percent": "1.5%",
681-
}
682-
}
683-
elif "finnhub.io" in url_str:
684-
mock_response.json.return_value = {
664+
return True
665+
return False
666+
667+
def get_mock_data_for_url(url_str):
668+
"""Get provider-specific mock data based on URL."""
669+
if "polygon.io" in url_str:
670+
return {
671+
"status": "OK",
672+
"results": [
673+
{
685674
"c": 150.0,
686-
"d": 2.0,
687-
"dp": 1.5,
688675
"h": 155.0,
689676
"l": 145.0,
690677
"o": 148.0,
691-
"pc": 148.0,
692-
"t": 1630000000,
678+
"v": 1000000,
679+
"t": 1630000000000,
693680
}
694-
elif "financialmodelingprep.com" in url_str:
695-
mock_response.json.return_value = [
696-
{
697-
"symbol": "TSLA",
698-
"price": 150.0,
699-
"changesPercentage": 1.5,
700-
"change": 2.0,
701-
"dayLow": 145.0,
702-
"dayHigh": 155.0,
703-
"yearHigh": 200.0,
704-
"yearLow": 100.0,
705-
"marketCap": 500000000000,
706-
"priceAvg50": 145.0,
707-
"priceAvg200": 140.0,
708-
"volume": 1000000,
709-
"avgVolume": 1000000,
710-
"exchange": "NASDAQ",
711-
"open": 148.0,
712-
"previousClose": 148.0,
713-
"eps": 5.0,
714-
"pe": 30.0,
715-
"earningsAnnouncement": "2023-01-01",
716-
"sharesOutstanding": 1000000000,
717-
"timestamp": 1630000000,
718-
}
719-
]
720-
else:
721-
# Default generic response
722-
mock_response.json.return_value = {"status": "ok", "data": {}}
723-
724-
return mock_response
725-
726-
# For all other calls, use the original method
727-
if original_get is not None:
728-
return original_get(self, url, *args, **kwargs)
729-
730-
# Fallback: return mock response to be safe
731-
return mock_response
681+
],
682+
}
683+
elif "alphavantage.co" in url_str:
684+
return {
685+
"Global Quote": {
686+
"01. symbol": "TSLA",
687+
"05. price": "150.00",
688+
"09. change": "2.00",
689+
"10. change percent": "1.5%",
690+
}
691+
}
692+
elif "finnhub.io" in url_str:
693+
return {
694+
"c": 150.0,
695+
"d": 2.0,
696+
"dp": 1.5,
697+
"h": 155.0,
698+
"l": 145.0,
699+
"o": 148.0,
700+
"pc": 148.0,
701+
"t": 1630000000,
702+
}
703+
elif "financialmodelingprep.com" in url_str:
704+
return [
705+
{
706+
"symbol": "TSLA",
707+
"price": 150.0,
708+
"changesPercentage": 1.5,
709+
"change": 2.0,
710+
"dayLow": 145.0,
711+
"dayHigh": 155.0,
712+
"yearHigh": 200.0,
713+
"yearLow": 100.0,
714+
"marketCap": 500000000000,
715+
"priceAvg50": 145.0,
716+
"priceAvg200": 140.0,
717+
"volume": 1000000,
718+
"avgVolume": 1000000,
719+
"exchange": "NASDAQ",
720+
"open": 148.0,
721+
"previousClose": 148.0,
722+
"eps": 5.0,
723+
"pe": 30.0,
724+
"earningsAnnouncement": "2023-01-01",
725+
"sharesOutstanding": 1000000000,
726+
"timestamp": 1630000000,
727+
}
728+
]
729+
# Default response for cryptocurrency exchanges
730+
return {"status": "ok", "data": {}}
732731

733732
import aiohttp
734733

735-
original_get = aiohttp.ClientSession.get
734+
# Store original methods
735+
original_request = aiohttp.ClientSession._request
736+
737+
async def mock_request(self, method, url, *args, **kwargs):
738+
"""Mock _request method which is the base for all HTTP methods."""
739+
url_str = str(url)
740+
741+
if should_mock_url(url_str):
742+
mock_resp = create_mock_response()
743+
mock_data = get_mock_data_for_url(url_str)
744+
mock_resp.json = AsyncMock(return_value=mock_data)
745+
mock_resp.text = AsyncMock(return_value='{"status": "ok"}')
746+
return mock_resp
747+
748+
# For non-mocked URLs, use the original method
749+
return await original_request(self, method, url, *args, **kwargs)
736750

737-
with patch.object(aiohttp.ClientSession, "get", selective_mock_get):
751+
with patch.object(aiohttp.ClientSession, "_request", mock_request):
738752
yield
739753

740754

0 commit comments

Comments
 (0)