Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
184 lines
5.8 KiB
Python
184 lines
5.8 KiB
Python
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
|
# For license information, please see license.txt
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import frappe
|
|
from frappe.tests.utils import FrappeTestCase
|
|
|
|
from crm.api.exchange_rate import (
|
|
_fetch_from_exchangerate_api,
|
|
_fetch_from_exchangerate_host,
|
|
_fetch_from_fawaz_api,
|
|
_fetch_from_frankfurter,
|
|
get_exchange_rate,
|
|
)
|
|
|
|
|
|
def _make_response(ok: bool, json_data: dict) -> MagicMock:
|
|
"""Helper: build a fake requests.Response."""
|
|
mock = MagicMock()
|
|
mock.ok = ok
|
|
mock.json.return_value = json_data
|
|
return mock
|
|
|
|
|
|
def _mock_settings(provider: str, access_key: str = "") -> MagicMock:
|
|
"""Helper: build a fake FCRM Settings single document."""
|
|
settings = MagicMock()
|
|
settings.service_provider = provider
|
|
settings.access_key = access_key
|
|
return settings
|
|
|
|
|
|
class TestFetchFromFrankfurter(FrappeTestCase):
|
|
@patch("crm.api.exchange_rate.requests.get")
|
|
def test_returns_rate_on_success(self, mock_get):
|
|
mock_get.return_value = _make_response(True, {"rates": {"INR": 83.5}})
|
|
|
|
rate = _fetch_from_frankfurter("USD", "INR", "latest")
|
|
|
|
self.assertEqual(rate, 83.5)
|
|
mock_get.assert_called_once()
|
|
|
|
@patch("crm.api.exchange_rate.requests.get")
|
|
def test_returns_none_when_response_not_ok(self, mock_get):
|
|
mock_get.return_value = _make_response(False, {})
|
|
|
|
rate = _fetch_from_frankfurter("USD", "INR", "latest")
|
|
|
|
self.assertIsNone(rate)
|
|
|
|
@patch("crm.api.exchange_rate.requests.get")
|
|
def test_returns_none_on_missing_currency_key(self, mock_get):
|
|
# response is ok but to_currency key is absent — should not raise KeyError
|
|
mock_get.return_value = _make_response(True, {"rates": {}})
|
|
|
|
rate = _fetch_from_frankfurter("USD", "INR", "latest")
|
|
|
|
self.assertIsNone(rate)
|
|
|
|
@patch(
|
|
"crm.api.exchange_rate.requests.get",
|
|
side_effect=Exception("Connection refused"),
|
|
)
|
|
def test_returns_none_on_network_error(self, mock_get):
|
|
rate = _fetch_from_frankfurter("USD", "INR", "latest")
|
|
|
|
self.assertIsNone(rate)
|
|
|
|
|
|
class TestFetchFromFawazApi(FrappeTestCase):
|
|
@patch("crm.api.exchange_rate.requests.get")
|
|
def test_returns_rate_on_success(self, mock_get):
|
|
mock_get.return_value = _make_response(True, {"usd": {"inr": 83.5}})
|
|
|
|
rate = _fetch_from_fawaz_api("USD", "INR", "latest")
|
|
|
|
self.assertEqual(rate, 83.5)
|
|
|
|
@patch("crm.api.exchange_rate.requests.get")
|
|
def test_falls_back_to_second_url_when_first_fails(self, mock_get):
|
|
# First URL raises, second URL succeeds
|
|
mock_get.side_effect = [
|
|
Exception("CDN timeout"),
|
|
_make_response(True, {"usd": {"inr": 83.5}}),
|
|
]
|
|
|
|
rate = _fetch_from_fawaz_api("USD", "INR", "latest")
|
|
|
|
self.assertEqual(rate, 83.5)
|
|
self.assertEqual(mock_get.call_count, 2)
|
|
|
|
@patch(
|
|
"crm.api.exchange_rate.requests.get",
|
|
side_effect=Exception("Network error"),
|
|
)
|
|
def test_returns_none_when_all_urls_fail(self, mock_get):
|
|
rate = _fetch_from_fawaz_api("USD", "INR", "latest")
|
|
|
|
self.assertIsNone(rate)
|
|
|
|
|
|
class TestFetchFromExchangeRateHost(FrappeTestCase):
|
|
@patch("crm.api.exchange_rate.requests.get")
|
|
def test_returns_rate_on_success(self, mock_get):
|
|
mock_get.return_value = _make_response(True, {"result": 83.5})
|
|
settings = _mock_settings("exchangerate.host", access_key="test_key")
|
|
|
|
rate = _fetch_from_exchangerate_host(settings, "USD", "INR", "latest")
|
|
|
|
self.assertEqual(rate, 83.5)
|
|
|
|
def test_raises_when_access_key_missing(self):
|
|
settings = _mock_settings("exchangerate.host", access_key="")
|
|
|
|
with self.assertRaises(frappe.exceptions.ValidationError):
|
|
_fetch_from_exchangerate_host(settings, "USD", "INR", "latest")
|
|
|
|
@patch("crm.api.exchange_rate.requests.get")
|
|
def test_returns_none_when_response_not_ok(self, mock_get):
|
|
mock_get.return_value = _make_response(False, {})
|
|
settings = _mock_settings("exchangerate.host", access_key="test_key")
|
|
|
|
rate = _fetch_from_exchangerate_host(settings, "USD", "INR", "latest")
|
|
|
|
self.assertIsNone(rate)
|
|
|
|
|
|
class TestFetchFromExchangeRateApi(FrappeTestCase):
|
|
@patch("crm.api.exchange_rate.requests.get")
|
|
def test_returns_rate_on_success(self, mock_get):
|
|
mock_get.return_value = _make_response(True, {"result": "success", "conversion_rate": 83.5})
|
|
settings = _mock_settings("exchangerate-api", access_key="test_key")
|
|
|
|
rate = _fetch_from_exchangerate_api(settings, "USD", "INR")
|
|
|
|
self.assertEqual(rate, 83.5)
|
|
|
|
def test_raises_when_access_key_missing(self):
|
|
settings = _mock_settings("exchangerate-api", access_key="")
|
|
|
|
with self.assertRaises(frappe.exceptions.ValidationError):
|
|
_fetch_from_exchangerate_api(settings, "USD", "INR")
|
|
|
|
@patch("crm.api.exchange_rate.requests.get")
|
|
def test_returns_none_when_api_result_is_not_success(self, mock_get):
|
|
mock_get.return_value = _make_response(True, {"result": "error", "error-type": "invalid-key"})
|
|
settings = _mock_settings("exchangerate-api", access_key="bad_key")
|
|
|
|
rate = _fetch_from_exchangerate_api(settings, "USD", "INR")
|
|
|
|
self.assertIsNone(rate)
|
|
|
|
|
|
class TestGetExchangeRate(FrappeTestCase):
|
|
def setUp(self):
|
|
frappe.cache().delete_value("exchange_rate_USD_INR_latest")
|
|
frappe.cache().delete_value(f"exchange_rate_USD_INR_{frappe.utils.today()}")
|
|
|
|
@patch("crm.api.exchange_rate._fetch_exchange_rate")
|
|
def test_returns_rate_on_success(self, mock_fetch):
|
|
mock_fetch.return_value = (83.5, "frankfurter")
|
|
|
|
rate = get_exchange_rate("USD", "INR")
|
|
|
|
self.assertEqual(rate, 83.5)
|
|
|
|
@patch("crm.api.exchange_rate._fetch_exchange_rate")
|
|
def test_caches_result_and_skips_second_fetch(self, mock_fetch):
|
|
mock_fetch.return_value = (83.5, "frankfurter")
|
|
|
|
get_exchange_rate("USD", "INR")
|
|
get_exchange_rate("USD", "INR")
|
|
|
|
# _fetch_exchange_rate should only be called once; second call hits cache
|
|
mock_fetch.assert_called_once()
|
|
|
|
@patch("crm.api.exchange_rate._fetch_exchange_rate")
|
|
def test_raises_when_all_providers_fail(self, mock_fetch):
|
|
mock_fetch.return_value = (None, "frankfurter")
|
|
|
|
with self.assertRaises(frappe.exceptions.ValidationError):
|
|
get_exchange_rate("USD", "XYZ")
|