parent
d620ffd389
commit
a369c44ae9
2 changed files with 149 additions and 77 deletions
149
tests/test_fx.py
149
tests/test_fx.py
|
|
@ -3,12 +3,11 @@
|
|||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from agenda.fx import read_cached_rates
|
||||
from agenda.fx import get_rates, read_cached_rates
|
||||
|
||||
|
||||
class TestReadCachedRates:
|
||||
|
|
@ -27,23 +26,23 @@ class TestReadCachedRates:
|
|||
"GBPUSD": 1.25,
|
||||
"GBPEUR": 1.15,
|
||||
"GBPJPY": 150.0,
|
||||
"GBPCAD": 1.70 # Not requested
|
||||
"GBPCAD": 1.70, # Not requested
|
||||
}
|
||||
}
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
|
||||
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)
|
||||
|
||||
|
|
@ -53,23 +52,23 @@ class TestReadCachedRates:
|
|||
data = {
|
||||
"quotes": {
|
||||
"GBPUSD": 1.25,
|
||||
"GBPEUR": 1.15
|
||||
"GBPEUR": 1.15,
|
||||
# GBPCHF missing
|
||||
}
|
||||
}
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
|
||||
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)
|
||||
|
||||
|
|
@ -78,18 +77,18 @@ class TestReadCachedRates:
|
|||
data = {
|
||||
"quotes": {
|
||||
"GBPUSD": 1.25,
|
||||
"GBPEUR": 1.15
|
||||
"GBPEUR": 1.15,
|
||||
}
|
||||
}
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
|
||||
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)
|
||||
|
||||
|
|
@ -97,58 +96,35 @@ class TestReadCachedRates:
|
|||
"""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:
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
||||
json.dump(data, f)
|
||||
filepath = f.name
|
||||
|
||||
try:
|
||||
with pytest.raises(KeyError):
|
||||
read_cached_rates(filepath, currencies)
|
||||
|
||||
finally:
|
||||
os.unlink(filepath)
|
||||
|
||||
def test_read_cached_rates_file_not_found(self) -> None:
|
||||
"""Test error handling when file doesn't exist."""
|
||||
with pytest.raises(FileNotFoundError):
|
||||
read_cached_rates("/nonexistent/file.json", ["USD"])
|
||||
|
||||
def test_read_cached_rates_invalid_json(self) -> None:
|
||||
"""Test error handling with invalid JSON."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
f.write("invalid json content")
|
||||
filepath = f.name
|
||||
|
||||
try:
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
read_cached_rates(filepath, ["USD"])
|
||||
|
||||
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:
|
||||
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
|
||||
# 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)
|
||||
|
||||
|
|
@ -162,21 +138,70 @@ class TestReadCachedRates:
|
|||
"GBPJPY": 150.0,
|
||||
"GBPCHF": 1.12,
|
||||
"GBPCAD": 1.70,
|
||||
"GBPAUD": 1.85
|
||||
"GBPAUD": 1.85,
|
||||
}
|
||||
}
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
|
||||
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)
|
||||
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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue