@@ -589,10 +589,13 @@ def __getattr__(self, name):
589589@pytest .fixture (autouse = True )
590590def 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