Skip to content

Commit e2c8b90

Browse files
authored
Merge pull request #204 from RepoBirdBot/main-5a597c90
Enhance Currency Rate Backend Error Handling & Update Tests/Settings
2 parents 1ac1010 + ad6a618 commit e2c8b90

File tree

2 files changed

+117
-32
lines changed

2 files changed

+117
-32
lines changed
Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import requests
2+
from requests.exceptions import JSONDecodeError
23
from typing import Union
34
from datetime import date
45
from django.utils.formats import date_format
@@ -9,41 +10,54 @@
910

1011

1112
class BankGovUaBackend(BaseBackend):
12-
13+
1314
@classmethod
1415
def get_state_currency(cls):
15-
return STATE_CURRENCY
16+
return STATE_CURRENCY
1617

17-
def __init__(self, currency: str, marketing_currency: str = 'USD',
18+
def __init__(self, currency: str, marketing_currency: str = 'USD',
1819
rate_date: Union[date, None] = None):
1920
self.url = "https://bank.gov.ua/NBUStatService/v1/statdirectory/exchangenew"
2021
self.date_format = "Ymd"
2122
self.error = ''
2223
self.state_currency = self.get_state_currency()
2324
self.currency = currency
2425
self.marketing_currency = marketing_currency
25-
self.rate_date = rate_date
26+
self.rate_date = rate_date if rate_date else date.today()
2627
self.data = self.get_data(marketing_currency)
2728
self.marketing_currency_rate = self.get_marketing_currency_rate()
2829

2930
def get_data(self, currency: str = 'USD') -> list:
3031
date_str = date_format(self.rate_date, format=self.date_format, use_l10n=False)
3132
params = {'date': date_str, 'valcode': currency, 'json': ''}
32-
response = requests.get(self.url, params=params)
33-
return response.json()
34-
35-
def get_marketing_currency_rate(self):
3633
try:
37-
return self.data[0]['rate']
38-
except Exception as e:
39-
self.error = e
34+
response = requests.get(self.url, params=params)
35+
response.raise_for_status()
36+
return response.json()
37+
except JSONDecodeError:
38+
self.error = f"Failed to decode JSON response from API. Status: {response.status_code}. Response text: {response.text[:100]}"
39+
return []
40+
except requests.exceptions.RequestException as e:
41+
self.error = f"API request failed: {e}"
42+
return []
43+
44+
def extract_rate_from_data(self, data: list, currency_code: str):
45+
"""Extracts rate from API data list, handles errors, returns 1 on failure."""
46+
if self.error:
47+
return 1
48+
try:
49+
return data[0]['rate']
50+
except (IndexError, KeyError, TypeError) as e:
51+
self.error = f"Error processing API data for {currency_code}: {e}. Data received: {data}"
4052
return 1
4153

54+
def get_marketing_currency_rate(self):
55+
return self.extract_rate_from_data(self.data, self.marketing_currency)
56+
4257
def get_rate_to_state_currency(self, currency: str = 'USD'):
4358
if self.error:
4459
return 1
45-
try:
46-
return self.get_data(currency)[0]['rate']
47-
except Exception as e:
48-
self.error = e
49-
return 1
60+
61+
62+
currency_data = self.get_data(currency)
63+
return self.extract_rate_from_data(currency_data, currency)
Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
12
from datetime import datetime as dt
3+
from unittest.mock import patch, MagicMock
24
from django.conf import settings
35
from django.test import TestCase
46
from django.utils.module_loading import import_string
5-
7+
import requests
8+
from requests.exceptions import JSONDecodeError, HTTPError, RequestException
69

710
# manage.py test tests.crm.backends.test_currency_rate_backend
811
MARKETING_CURRENCY = 'USD'
@@ -12,23 +15,91 @@ class TestCurrencyRateBackend(TestCase):
1215

1316
def setUp(self):
1417
print(" Run Test Method:", self._testMethodName)
18+
if not settings.LOAD_RATE_BACKEND:
19+
self.skipTest("LOAD_RATE_BACKEND is not set in settings.")
20+
try:
21+
self.be = import_string(settings.LOAD_RATE_BACKEND)
22+
except ImportError:
23+
self.fail(f"Failed to import backend: {settings.LOAD_RATE_BACKEND}")
24+
1525

1626
def test_currency_rate_backend(self):
17-
if not any((settings.LOAD_EXCHANGE_RATE, settings.LOAD_RATE_BACKEND)):
18-
print("Test `CurrencyRateBackend` skipped due to settings.")
19-
return
20-
be = import_string(settings.LOAD_RATE_BACKEND)
27+
# NOTE: This test depends on settings.LOAD_EXCHANGE_RATE = True and a valid LOAD_RATE_BACKEND.
28+
# It might perform a live API call if not mocked.
29+
# Consider mocking if live calls are undesirable.
30+
if not settings.LOAD_EXCHANGE_RATE:
31+
self.skipTest("LOAD_EXCHANGE_RATE is False in settings. Skipping live test.")
32+
2133
today = dt.now().date()
22-
state_currency = be.get_state_currency()
23-
if state_currency != MARKETING_CURRENCY:
24-
backend = be(state_currency, MARKETING_CURRENCY, today)
25-
rate_to_state_currency, rate_to_marketing_currency, error = backend.get_rates()
26-
self.assertEqual('', error)
34+
state_currency = self.be.get_state_currency()
35+
36+
backend_state = self.be(state_currency, MARKETING_CURRENCY, today)
37+
rate_to_state_currency, rate_to_marketing_currency, error_state = backend_state.get_rates()
38+
39+
if error_state:
40+
print(f"Warning: Live API call in test_currency_rate_backend (state) failed: {error_state}")
41+
self.assertNotEqual('', error_state)
42+
else:
2743
self.assertEqual(rate_to_state_currency, 1)
28-
self.assertEqual(type(rate_to_marketing_currency), float)
44+
self.assertIsInstance(rate_to_marketing_currency, (float, int))
45+
if state_currency == MARKETING_CURRENCY:
46+
self.assertEqual(rate_to_marketing_currency, 1)
47+
48+
backend_marketing = self.be(MARKETING_CURRENCY, MARKETING_CURRENCY, today)
49+
rate_to_state_usd, rate_to_marketing_usd, error_marketing = backend_marketing.get_rates()
50+
51+
if error_marketing:
52+
print(f"Warning: Live API call in test_currency_rate_backend (marketing) failed: {error_marketing}")
53+
self.assertNotEqual('', error_marketing)
54+
else:
55+
self.assertEqual(rate_to_marketing_usd, 1)
56+
self.assertIsInstance(rate_to_state_usd, (float, int))
57+
if state_currency == MARKETING_CURRENCY:
58+
self.assertEqual(rate_to_state_usd, 1)
59+
60+
@patch('crm.backends.bank_gov_ua_backend.requests.get')
61+
def test_currency_rate_backend_json_error(self, mock_get):
62+
mock_response = MagicMock(spec=requests.Response)
63+
mock_response.status_code = 200
64+
mock_response.text = 'invalid json'
65+
mock_response.json.side_effect = JSONDecodeError("Expecting value", mock_response.text, 0)
66+
mock_response.raise_for_status = MagicMock()
67+
mock_get.return_value = mock_response
68+
69+
backend = self.be('EUR', MARKETING_CURRENCY, dt.now().date())
70+
rate_to_state_currency, rate_to_marketing_currency, error = backend.get_rates()
71+
72+
self.assertIn("Failed to decode JSON response from API", error)
73+
self.assertIn("Status: 200", error)
74+
self.assertIn("Response text: invalid json", error)
75+
self.assertEqual(rate_to_state_currency, 1)
76+
self.assertEqual(rate_to_marketing_currency, 1)
77+
78+
@patch('crm.backends.bank_gov_ua_backend.requests.get')
79+
def test_currency_rate_backend_http_error(self, mock_get):
80+
mock_response = MagicMock(spec=requests.Response)
81+
mock_response.status_code = 500
82+
mock_response.text = 'Internal Server Error'
83+
http_error = HTTPError("500 Server Error", response=mock_response)
84+
mock_response.raise_for_status.side_effect = http_error
85+
mock_get.return_value = mock_response
86+
87+
backend = self.be('EUR', MARKETING_CURRENCY, dt.now().date())
88+
rate_to_state_currency, rate_to_marketing_currency, error = backend.get_rates()
89+
90+
self.assertIn("API request failed", error)
91+
self.assertIn("500 Server Error", error)
92+
self.assertEqual(rate_to_state_currency, 1)
93+
self.assertEqual(rate_to_marketing_currency, 1)
94+
95+
@patch('crm.backends.bank_gov_ua_backend.requests.get')
96+
def test_currency_rate_backend_request_exception(self, mock_get):
97+
mock_get.side_effect = RequestException("Connection timed out")
98+
99+
backend = self.be('EUR', MARKETING_CURRENCY, dt.now().date())
100+
rate_to_state_currency, rate_to_marketing_currency, error = backend.get_rates()
29101

30-
backend = be(MARKETING_CURRENCY, MARKETING_CURRENCY, today)
31-
rate_to_state_currency, rate_to_marketing_currency, error = backend.get_rates()
32-
self.assertEqual('', error)
33-
self.assertEqual(rate_to_marketing_currency, 1)
34-
self.assertEqual(type(rate_to_state_currency), float)
102+
self.assertIn("API request failed", error)
103+
self.assertIn("Connection timed out", error)
104+
self.assertEqual(rate_to_state_currency, 1)
105+
self.assertEqual(rate_to_marketing_currency, 1)

0 commit comments

Comments
 (0)