diff --git a/requirements-dev.txt b/requirements-dev.txt index 7703670..21a7017 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,3 +5,4 @@ requests==2.32.3 # Dev only packages. black==24.4.2 +coverage==7.5.3 diff --git a/run_tests.py b/run_tests.py index f695555..174d6b3 100644 --- a/run_tests.py +++ b/run_tests.py @@ -2,8 +2,17 @@ def main(): - subprocess.call(["python", "-m", "unittest"]) + # Run tests with coverage + subprocess.call(["python", "-m", "coverage", "run", "-m", "unittest", "discover"]) + + # Generate the coverage report in the terminal + subprocess.call(["python", "-m", "coverage", "report", "-m"]) + + # Generate the HTML coverage report + subprocess.call(["python", "-m", "coverage", "html"]) + print("\n\nTests completed.") + print("HTML report generated at 'htmlcov/index.html'") if __name__ == "__main__": diff --git a/tests/test_rates.py b/tests/test_rates.py index 54dd666..aa7d7c7 100644 --- a/tests/test_rates.py +++ b/tests/test_rates.py @@ -1,4 +1,9 @@ +import json import unittest +from unittest.mock import patch, MagicMock + +import requests + from tipbot import rates from .samples import UPDATE @@ -6,4 +11,48 @@ class TestRate(unittest.TestCase): def test_get_rate(self): rate = rates.get_rate(UPDATE) - self.assertIs(type(rate), float) + self.assertIsInstance(rate, float) + + def test_get_rate_unsupported_currency(self): + mock_update = MagicMock() + rates.get_rate(mock_update, "XYZ") + + mock_update.message.reply_text.assert_called_once_with( + "XYZ is not a supported currency." + ) + + @patch("tipbot.rates.requests.get") + def test_get_rate_api_failure(self, mock_requests_get): + mock_requests_get.side_effect = requests.exceptions.RequestException + mock_update = MagicMock() + rates.get_rate(mock_update, "USD") + + mock_update.message.reply_text.assert_called_once_with( + f"Unable to contact {rates.RATE_API}" + ) + + @patch("tipbot.rates.requests.get") + def test_get_rate_http_error(self, mock_requests_get): + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError + mock_response.text = "Error message" + mock_requests_get.return_value = mock_response + mock_update = MagicMock() + rates.get_rate(mock_update, "USD") + + mock_update.message.reply_text.assert_called_once_with( + f"Unable to contact {rates.RATE_API}" + ) + + @patch("tipbot.rates.requests.get") + def test_get_rate_parse_failure(self, mock_requests_get): + mock_response = MagicMock() + mock_response.raise_for_status = MagicMock() + mock_response.json.side_effect = json.JSONDecodeError("Expecting value", "", 0) + mock_requests_get.return_value = mock_response + mock_update = MagicMock() + rates.get_rate(mock_update, "USD") + + mock_update.message.reply_text.assert_called_once_with( + f"Unable to parse rate data: {mock_response.text}" + ) diff --git a/tipbot/rates.py b/tipbot/rates.py index 1940536..48ac163 100644 --- a/tipbot/rates.py +++ b/tipbot/rates.py @@ -1,6 +1,12 @@ +import json +import logging + import requests +logger = logging.getLogger(__name__) + + RATE_API = "https://bitpay.com/rates/BCH/" CURRENCY_CODE = { "BTC": 0, @@ -173,7 +179,7 @@ } -def get_rate(update, currency="USD"): +def get_rate(update, currency: str = "USD") -> float: """Returns the BCH price fetching BitPay API API documentation: @@ -182,13 +188,20 @@ def get_rate(update, currency="USD"): currency = currency.upper() if currency not in CURRENCY_CODE: - return update.message.reply_text(f"{currency} is not a supported " "currency.") + return update.message.reply_text(f"{currency} is not a supported currency.") - r = requests.get(RATE_API) - if r.status_code != 200: # pragma: no cover + try: + response = requests.get(RATE_API) + response.raise_for_status() + data = response.json()["data"] + rate = data[CURRENCY_CODE[currency]]["rate"] + return rate + except requests.exceptions.RequestException as e: + logger.exception("Failed to fetch rate") + if isinstance(e, requests.exceptions.HTTPError): + logger.info(response.text) return update.message.reply_text(f"Unable to contact {RATE_API}") - - data = r.json()["data"] - rate = data[CURRENCY_CODE[currency]]["rate"] - - return rate + except (json.JSONDecodeError, KeyError): + message = f"Unable to parse rate data: {response.text}" + logger.exception(message) + return update.message.reply_text(message)