agenda/tests/test_fx.py

207 lines
6.6 KiB
Python

"""Tests for foreign exchange functionality."""
import json
import os
import tempfile
from datetime import datetime
from decimal import Decimal
from unittest.mock import patch
from agenda.fx import get_rates, read_cached_rates
class TestReadCachedRates:
"""Test the read_cached_rates function."""
def test_read_cached_rates_none_filename(self) -> None:
"""Test with None filename returns empty dict."""
result = read_cached_rates(None, ["USD", "EUR"])
assert result == {}
def test_read_cached_rates_valid_file(self) -> None:
"""Test reading valid cached rates file."""
currencies = ["USD", "EUR", "JPY"]
data = {
"quotes": {
"GBPUSD": 1.25,
"GBPEUR": 1.15,
"GBPJPY": 150.0,
"GBPCAD": 1.70, # Not requested
}
}
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
json.dump(data, f)
filepath = f.name
try:
result = read_cached_rates(filepath, currencies)
assert len(result) == 3
assert result["USD"] == Decimal("1.25")
assert result["EUR"] == Decimal("1.15")
assert result["JPY"] == Decimal("150.0")
assert "CAD" not in result # Not requested
finally:
os.unlink(filepath)
def test_read_cached_rates_missing_currencies(self) -> None:
"""Test with some currencies missing from data."""
currencies = ["USD", "EUR", "CHF"] # CHF not in data
data = {
"quotes": {
"GBPUSD": 1.25,
"GBPEUR": 1.15,
# GBPCHF missing
}
}
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
json.dump(data, f)
filepath = f.name
try:
result = read_cached_rates(filepath, currencies)
assert len(result) == 2
assert result["USD"] == Decimal("1.25")
assert result["EUR"] == Decimal("1.15")
assert "CHF" not in result # Missing from data
finally:
os.unlink(filepath)
def test_read_cached_rates_empty_currencies_list(self) -> None:
"""Test with empty currencies list."""
data = {
"quotes": {
"GBPUSD": 1.25,
"GBPEUR": 1.15,
}
}
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
json.dump(data, f)
filepath = f.name
try:
result = read_cached_rates(filepath, [])
assert result == {}
finally:
os.unlink(filepath)
def test_read_cached_rates_no_quotes_key(self) -> None:
"""Test with data missing quotes key."""
currencies = ["USD", "EUR"]
data = {"other_key": "value"} # No quotes key
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
json.dump(data, f)
filepath = f.name
try:
assert read_cached_rates(filepath, currencies) == {}
finally:
os.unlink(filepath)
def test_read_cached_rates_decimal_precision(self) -> None:
"""Test that rates are returned as Decimal with proper precision."""
currencies = ["USD"]
data = {"quotes": {"GBPUSD": 1.234567890123456789}} # High precision
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
json.dump(data, f)
filepath = f.name
try:
result = read_cached_rates(filepath, currencies)
assert isinstance(result["USD"], Decimal)
# Should preserve reasonable precision from the JSON
# Python's JSON precision may be limited to float precision
expected = Decimal("1.234567890123456789")
assert abs(result["USD"] - expected) < Decimal("0.0000000000000001")
finally:
os.unlink(filepath)
def test_read_cached_rates_various_currency_codes(self) -> None:
"""Test with various currency codes."""
currencies = ["USD", "EUR", "JPY", "CHF", "CAD", "AUD"]
data = {
"quotes": {
"GBPUSD": 1.25,
"GBPEUR": 1.15,
"GBPJPY": 150.0,
"GBPCHF": 1.12,
"GBPCAD": 1.70,
"GBPAUD": 1.85,
}
}
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
json.dump(data, f)
filepath = f.name
try:
result = read_cached_rates(filepath, currencies)
assert len(result) == 6
for currency in currencies:
assert currency in result
assert isinstance(result[currency], Decimal)
finally:
os.unlink(filepath)
def test_get_rates_uses_older_valid_cache_when_latest_attempt_is_usage_limit() -> None:
"""A usage-limit API response should not replace usable cached rates."""
with tempfile.TemporaryDirectory() as temp_dir:
fx_dir = os.path.join(temp_dir, "fx")
os.mkdir(fx_dir)
suffix = "EUR,USD_to_GBP.json"
valid_file = os.path.join(fx_dir, f"2026-06-19_23:35_{suffix}")
usage_limit_file = os.path.join(fx_dir, f"2026-06-20_11:35_{suffix}")
with open(valid_file, "w") as f:
json.dump(
{"success": True, "quotes": {"GBPEUR": 1.15, "GBPUSD": 1.25}},
f,
)
with open(usage_limit_file, "w") as f:
json.dump(
{
"success": False,
"error": {
"code": 104,
"type": "usage_limit_reached",
"info": "Your monthly usage limit has been reached.",
},
},
f,
)
config = {
"CURRENCIES": ["USD", "EUR"],
"DATA_DIR": temp_dir,
"EXCHANGERATE_ACCESS_KEY": "key",
"OFFLINE_MODE": False,
"FX_CACHE_TTL_HOURS": 1,
"FX_FAILURE_RETRY_HOURS": 24,
}
with patch("agenda.fx.datetime") as mock_datetime, patch(
"agenda.fx.httpx.Client"
) as mock_client:
mock_datetime.now.return_value = datetime(2026, 6, 20, 12, 35)
mock_datetime.strptime.side_effect = datetime.strptime
result = get_rates(config)
assert result == {"USD": Decimal("1.25"), "EUR": Decimal("1.15")}
mock_client.assert_not_called()