Files
upstream-frappe-crm/crm/tests/test_exchange_rate.py
upstream-mirror db69de1c11
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
snapshot of https://github.com/frappe/crm.git (develop, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:36:01 +05:00

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")