From 5743b580354423b7365c458beb539103595b8845 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:27:34 +0100 Subject: [PATCH] Mock external API calls in price provider tests (#2523) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: k9ert <117085+k9ert@users.noreply.github.com> --- .../specter/util/price_providers.py | 1 - tests/test_util_priceprovider.py | 161 +++++++++++++----- 2 files changed, 123 insertions(+), 39 deletions(-) diff --git a/src/cryptoadvance/specter/util/price_providers.py b/src/cryptoadvance/specter/util/price_providers.py index b9335acdb..4d1d21050 100644 --- a/src/cryptoadvance/specter/util/price_providers.py +++ b/src/cryptoadvance/specter/util/price_providers.py @@ -118,7 +118,6 @@ def get_price_at(specter, timestamp="now"): ) except AttributeError: raise SpecterError(f"Currency not supported: {currency}") - print("MUH" + specter.price_provider) if exchange not in currency_mapping[currency]["support"]: raise SpecterError( f"The currency {currency} is not supported on exchange {exchange}" diff --git a/tests/test_util_priceprovider.py b/tests/test_util_priceprovider.py index 388ab84d0..d07f7e53f 100644 --- a/tests/test_util_priceprovider.py +++ b/tests/test_util_priceprovider.py @@ -9,92 +9,177 @@ from cryptoadvance.specter.util.price_providers import ( _parse_exchange_currency, failsafe_request_get, ) -from mock import MagicMock +from mock import MagicMock, Mock, patch def test_underlying_requests(empty_data_folder): """This test might fail on MacOS see #1512 + Mocked to avoid external API dependency and flaky test failures. """ specter_mock = Specter(data_folder=empty_data_folder, checker_threads=False) requests_session = specter_mock.requests_session() currency = "eur" - price = requests_session.get( - "https://www.bitstamp.net/api/v2/ticker/btc{}".format(currency) - ).json()["last"] - assert type(price) == str - assert float(price) + + # Mock the Bitstamp API response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"last": "45123.50"} + + with patch.object(requests_session, 'get', return_value=mock_response): + price = requests_session.get( + "https://www.bitstamp.net/api/v2/ticker/btc{}".format(currency) + ).json()["last"] + assert type(price) == str + assert float(price) def test_failsafe_request_get(empty_data_folder): specter_mock = Specter(data_folder=empty_data_folder, checker_threads=False) requests_session = specter_mock.requests_session() + + # Test 1: Mock 404 error for invalid currency currency = "notExisting" url = "https://www.bitstamp.net/api/v2/ticker/btc{}".format(currency) - with pytest.raises(SpecterError) as se: - failsafe_request_get(requests_session, url) - assert ( - f"HttpError 404 for https://www.bitstamp.net/api/v2/ticker/btcnotExisting" - in str(se.value) + + mock_404_response = Mock() + mock_404_response.status_code = 404 + mock_404_response.raise_for_status.side_effect = requests.exceptions.HTTPError( + response=Mock(status_code=404) ) + mock_404_response.json.return_value = {"error": "Not found"} + + with patch.object(requests_session, 'get', return_value=mock_404_response): + with pytest.raises(SpecterError) as se: + failsafe_request_get(requests_session, url) + assert ( + f"HttpError 404 for https://www.bitstamp.net/api/v2/ticker/btcnotExisting" + in str(se.value) + ) + # Test 2: Mock JSON error response (response with "errors" key) currency = "usd" - # timestamp most probably in the future url = "https://www.bitstamp.net/api/v2/ohlc/btc{}/?limit=A&step=86400&start={}".format( currency, 6275453759 ) - with pytest.raises(SpecterError) as se: - failsafe_request_get(requests_session, url) - assert str(se.value).startswith("JSON error:") + + mock_json_error_response = Mock() + mock_json_error_response.status_code = 200 + mock_json_error_response.json.return_value = {"errors": ["Invalid parameters"]} + + with patch.object(requests_session, 'get', return_value=mock_json_error_response): + with pytest.raises(SpecterError) as se: + failsafe_request_get(requests_session, url) + assert str(se.value).startswith("JSON error:") - with pytest.raises(SpecterError) as se: - failsafe_request_get(requests_session, "https://httpbin.org/status/404") - # Also allow for Gateway Timeout (504 error), mainly for the CI - assert f"HttpError 404 for https://httpbin.org/status/404" in str( - se.value - ) or f"HttpError 504 for https://httpbin.org/status/404" in str(se.value) + # Test 3: Mock 404 error for httpbin + mock_httpbin_404 = Mock() + mock_httpbin_404.status_code = 404 + mock_httpbin_404.raise_for_status.side_effect = requests.exceptions.HTTPError( + response=Mock(status_code=404) + ) + mock_httpbin_404.json.return_value = {} + + with patch.object(requests_session, 'get', return_value=mock_httpbin_404): + with pytest.raises(SpecterError) as se: + failsafe_request_get(requests_session, "https://httpbin.org/status/404") + # Also allow for Gateway Timeout (504 error), mainly for the CI + assert f"HttpError 404 for https://httpbin.org/status/404" in str( + se.value + ) or f"HttpError 504 for https://httpbin.org/status/404" in str(se.value) - json = failsafe_request_get(requests_session, "https://httpbin.org/json") - assert json["slideshow"] + # Test 4: Mock successful JSON response + mock_success_response = Mock() + mock_success_response.status_code = 200 + mock_success_response.json.return_value = { + "slideshow": { + "author": "Yours Truly", + "date": "date of publication", + "slides": [{"title": "Wake up to WonderWidgets!", "type": "all"}], + "title": "Sample Slide Show", + } + } + + with patch.object(requests_session, 'get', return_value=mock_success_response): + json = failsafe_request_get(requests_session, "https://httpbin.org/json") + assert json["slideshow"] def test_get_price_at(): specter_mock = MagicMock() - # specter_mock = Specter() specter_mock.requests_session.return_value = requests.Session() + specter_mock.price_check = True + + # Mock responses for different providers + def mock_get(url): + response = Mock() + response.status_code = 200 + + # Bitstamp ticker endpoint + if "bitstamp.net/api/v2/ticker" in url: + response.json.return_value = {"last": "45000.50"} + # Coindesk currentprice endpoint + elif "api.coindesk.com/v1/bpi/currentprice" in url: + currency_code = url.split("/")[-1].replace(".json", "") + response.json.return_value = { + "bpi": { + currency_code: {"rate_float": 45000.50} + } + } + else: + response.json.return_value = {} + + return response + + # Test with bitstamp_eur specter_mock.price_provider = "bitstamp_eur" - specter_mock.price_provider = "bitstamp_eur" - # returns a tuple like (True, price, currency_symbol) - mytuple = get_price_at(specter_mock) - assert float(mytuple[0]) - assert mytuple[1] == "€" + with patch('requests.Session.get', side_effect=mock_get): + mytuple = get_price_at(specter_mock) + assert float(mytuple[0]) + assert mytuple[1] == "€" - # Now test them all for bitstamp - # https://www.bitstamp.net/api/#ticker + # Now test them all for bitstamp and coindesk (skip spotbit as it requires Tor) for currency in currency_mapping.keys(): for exchange in currency_mapping[currency]["support"]: if exchange.startswith("spotbit_"): # ignore for now continue specter_mock.price_provider = f"{exchange}_{currency}" - mytuple = get_price_at(specter_mock) - assert float(mytuple[0]) - assert mytuple[1] == currency_mapping[currency]["symbol"] + with patch('requests.Session.get', side_effect=mock_get): + mytuple = get_price_at(specter_mock) + assert float(mytuple[0]) + assert mytuple[1] == currency_mapping[currency]["symbol"] def test_get_price_at_historic(): specter_mock = MagicMock() specter_mock.requests_session.return_value = requests.Session() - # historic-prices: - + specter_mock.price_check = True + + # Mock response for historic prices + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "data": { + "ohlc": [ + { + "close": "62000.00", + "timestamp": "1636551359" + } + ] + } + } + specter_mock.price_provider = f"bitstamp_usd" - mytuple = get_price_at(specter_mock, 1636551359) - assert float(mytuple[0]) > 60000 + with patch('requests.Session.get', return_value=mock_response): + mytuple = get_price_at(specter_mock, 1636551359) + assert float(mytuple[0]) > 60000 def test_get_price_at_errorhandling(): specter_mock = MagicMock() specter_mock.requests_session.return_value = requests.Session() + specter_mock.price_check = True # some unsupported stuff: specter_mock.price_provider = f"bitstamp_ils"