Skip to content

Enhance Currency Rate Backend Error Handling & Update Tests/Settings #204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 32 additions & 16 deletions crm/backends/bank_gov_ua_backend.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import requests
from requests.exceptions import JSONDecodeError
from typing import Union
from datetime import date
from django.utils.formats import date_format
Expand All @@ -9,41 +10,56 @@


class BankGovUaBackend(BaseBackend):

@classmethod
def get_state_currency(cls):
return STATE_CURRENCY
return STATE_CURRENCY

def __init__(self, currency: str, marketing_currency: str = 'USD',
def __init__(self, currency: str, marketing_currency: str = 'USD',
rate_date: Union[date, None] = None):
self.url = "https://bank.gov.ua/NBUStatService/v1/statdirectory/exchangenew"
self.date_format = "Ymd"
self.error = ''
self.state_currency = self.get_state_currency()
self.currency = currency
self.marketing_currency = marketing_currency
self.rate_date = rate_date
self.rate_date = rate_date if rate_date else date.today()
self.data = self.get_data(marketing_currency)
self.marketing_currency_rate = self.get_marketing_currency_rate()

def get_data(self, currency: str = 'USD') -> list:
date_str = date_format(self.rate_date, format=self.date_format, use_l10n=False)
params = {'date': date_str, 'valcode': currency, 'json': ''}
response = requests.get(self.url, params=params)
return response.json()
try:
response = requests.get(self.url, params=params)
response.raise_for_status()
return response.json()
except JSONDecodeError:
self.error = f"Failed to decode JSON response from API. Status: {response.status_code}. Response text: {response.text[:100]}"
return []
except requests.exceptions.RequestException as e:
self.error = f"API request failed: {e}"
return []

def get_marketing_currency_rate(self):
def extract_rate_from_data(self, data: list, currency_code: str):
"""Extracts rate from API data list, handles errors, returns 1 on failure."""
if not data:
return 1
try:
return self.data[0]['rate']
except Exception as e:
self.error = e
return data[0]['rate']
except (IndexError, KeyError, TypeError) as e:
self.error = f"Error processing API data for {currency_code}: {e}. Data received: {data}"
return 1

def get_marketing_currency_rate(self):
return self.extract_rate_from_data(self.data, self.marketing_currency)

def get_rate_to_state_currency(self, currency: str = 'USD'):
if self.error:
return 1
try:
return self.get_data(currency)[0]['rate']
except Exception as e:
self.error = e
if self.error and not self.data:
return 1

if currency == self.marketing_currency:
return self.get_marketing_currency_rate()

specific_data = self.get_data(currency)
return self.extract_rate_from_data(specific_data, currency)
103 changes: 88 additions & 15 deletions tests/crm/backends/test_currency_rate_backend.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@

from datetime import datetime as dt
from unittest.mock import patch, MagicMock
from django.conf import settings
from django.test import TestCase
from django.utils.module_loading import import_string

import requests
from requests.exceptions import JSONDecodeError, HTTPError, RequestException

# manage.py test tests.crm.backends.test_currency_rate_backend
MARKETING_CURRENCY = 'USD'
Expand All @@ -11,24 +14,94 @@
class TestCurrencyRateBackend(TestCase):

def setUp(self):
if not settings.LOAD_RATE_BACKEND:
self.skipTest("LOAD_RATE_BACKEND is not set in settings.")
return
try:
self.be = import_string(settings.LOAD_RATE_BACKEND)
except ImportError:
self.fail(f"Failed to import backend: {settings.LOAD_RATE_BACKEND}")
print(" Run Test Method:", self._testMethodName)


def test_currency_rate_backend(self):
if not any((settings.LOAD_EXCHANGE_RATE, settings.LOAD_RATE_BACKEND)):
print("Test `CurrencyRateBackend` skipped due to settings.")
# NOTE: This test depends on settings.LOAD_EXCHANGE_RATE = True and a valid LOAD_RATE_BACKEND.
# It might perform a live API call if not mocked.
# Consider mocking if live calls are undesirable.
if not settings.LOAD_EXCHANGE_RATE:
self.skipTest("LOAD_EXCHANGE_RATE is False in settings. Skipping live test.")
return
be = import_string(settings.LOAD_RATE_BACKEND)

today = dt.now().date()
state_currency = be.get_state_currency()
if state_currency != MARKETING_CURRENCY:
backend = be(state_currency, MARKETING_CURRENCY, today)
rate_to_state_currency, rate_to_marketing_currency, error = backend.get_rates()
self.assertEqual('', error)
state_currency = self.be.get_state_currency()

backend_state = self.be(state_currency, MARKETING_CURRENCY, today)
rate_to_state_currency, rate_to_marketing_currency, error_state = backend_state.get_rates()

if error_state:
print(f"Warning: Live API call in test_currency_rate_backend (state) failed: {error_state}")
self.assertNotEqual('', error_state)
else:
self.assertEqual(rate_to_state_currency, 1)
self.assertEqual(type(rate_to_marketing_currency), float)
self.assertIsInstance(rate_to_marketing_currency, (float, int))
if state_currency == MARKETING_CURRENCY:
self.assertEqual(rate_to_marketing_currency, 1)

backend_marketing = self.be(MARKETING_CURRENCY, MARKETING_CURRENCY, today)
rate_to_state_usd, rate_to_marketing_usd, error_marketing = backend_marketing.get_rates()

if error_marketing:
print(f"Warning: Live API call in test_currency_rate_backend (marketing) failed: {error_marketing}")
self.assertNotEqual('', error_marketing)
else:
self.assertEqual(rate_to_marketing_usd, 1)
self.assertIsInstance(rate_to_state_usd, (float, int))
if state_currency == MARKETING_CURRENCY:
self.assertEqual(rate_to_state_usd, 1)

@patch('crm.backends.bank_gov_ua_backend.requests.get')
def test_currency_rate_backend_json_error(self, mock_get):
mock_response = MagicMock(spec=requests.Response)
mock_response.status_code = 200
mock_response.text = 'invalid json'
mock_response.json.side_effect = JSONDecodeError("Expecting value", mock_response.text, 0)
mock_response.raise_for_status = MagicMock()
mock_get.return_value = mock_response

backend = self.be('EUR', MARKETING_CURRENCY, dt.now().date())
rate_to_state_currency, rate_to_marketing_currency, error = backend.get_rates()

self.assertIn("Failed to decode JSON response from API", error)
self.assertIn("Status: 200", error)
self.assertIn("Response text: invalid json", error)
self.assertEqual(rate_to_state_currency, 1)
self.assertEqual(rate_to_marketing_currency, 1)

@patch('crm.backends.bank_gov_ua_backend.requests.get')
def test_currency_rate_backend_http_error(self, mock_get):
mock_response = MagicMock(spec=requests.Response)
mock_response.status_code = 500
mock_response.text = 'Internal Server Error'
http_error = HTTPError("500 Server Error", response=mock_response)
mock_response.raise_for_status.side_effect = http_error
mock_get.return_value = mock_response

backend = self.be('EUR', MARKETING_CURRENCY, dt.now().date())
rate_to_state_currency, rate_to_marketing_currency, error = backend.get_rates()

self.assertIn("API request failed", error)
self.assertIn("500 Server Error", error)
self.assertEqual(rate_to_state_currency, 1)
self.assertEqual(rate_to_marketing_currency, 1)

@patch('crm.backends.bank_gov_ua_backend.requests.get')
def test_currency_rate_backend_request_exception(self, mock_get):
mock_get.side_effect = RequestException("Connection timed out")

backend = self.be('EUR', MARKETING_CURRENCY, dt.now().date())
rate_to_state_currency, rate_to_marketing_currency, error = backend.get_rates()

backend = be(MARKETING_CURRENCY, MARKETING_CURRENCY, today)
rate_to_state_currency, rate_to_marketing_currency, error = backend.get_rates()
self.assertEqual('', error)
self.assertEqual(rate_to_marketing_currency, 1)
self.assertEqual(type(rate_to_state_currency), float)
self.assertIn("API request failed", error)
self.assertIn("Connection timed out", error)
self.assertEqual(rate_to_state_currency, 1)
self.assertEqual(rate_to_marketing_currency, 1)