agenda/tests/test_fx_currency_changes.py
Edward Betts 6b253f2d04 Refresh FX rates when configured currencies change
Compare the latest cached currency list before applying cache TTL and match cache filenames exactly. Preserve offline mode and failure retry backoff.

Closes #170
2026-10-01 13:25:52 +01:00

93 lines
3.2 KiB
Python

"""Currency configuration changes must bypass the FX cache interval."""
import json
from datetime import datetime
from pathlib import Path
from unittest.mock import patch
import flask
import pytest
from agenda.fx import get_rates
@pytest.mark.parametrize(
("previous", "current", "fetch"),
[
(["USD"], ["USD", "EUR"], True),
(["EUR", "USD"], ["USD"], True),
(["EUR", "USD"], ["USD", "EUR"], False),
],
)
def test_currency_changes(
tmp_path: Path, previous: list[str], current: list[str], fetch: bool
) -> None:
"""Fetch additions/removals even if an older matching cache is still fresh."""
fx_dir = tmp_path / "fx"
fx_dir.mkdir()
def cache(timestamp: str, currencies: list[str]) -> None:
suffix = ",".join(sorted(currencies))
filename = fx_dir / f"{timestamp}_frankfurter_{suffix}_to_GBP.json"
filename.write_text(
json.dumps([{"quote": c, "rate": 1.25} for c in currencies])
)
cache("2026-10-01_10:00", current)
cache("2026-10-01_11:00", previous)
config = flask.config.Config(
"/",
{
"CURRENCIES": current,
"DATA_DIR": str(tmp_path),
"OFFLINE_MODE": False,
"FX_CACHE_TTL_HOURS": 12,
},
)
with (
patch("agenda.fx.datetime") as clock,
patch("agenda.fx.httpx.Client") as client,
):
clock.now.return_value = datetime(2026, 10, 1, 12)
clock.strptime.side_effect = datetime.strptime
get = client.return_value.__enter__.return_value.get
get.return_value.text = json.dumps([{"quote": c, "rate": 1.3} for c in current])
result = get_rates(config)
assert set(result) == set(current)
assert get.call_count == int(fetch)
# The same configuration must use the newly fetched cache.
get_rates(config)
assert get.call_count == int(fetch)
def test_changed_currencies_preserve_failure_backoff(tmp_path: Path) -> None:
"""A failed fetch for the new list should not be retried on every cron run."""
fx_dir = tmp_path / "fx"
fx_dir.mkdir()
(fx_dir / "2026-10-01_11:00_frankfurter_EUR,USD_to_GBP.json").write_text(
json.dumps([{"quote": "EUR", "rate": 1.15}, {"quote": "USD", "rate": 1.25}])
)
config = flask.config.Config(
"/", {"CURRENCIES": ["USD"], "DATA_DIR": str(tmp_path), "OFFLINE_MODE": False}
)
with (
patch("agenda.fx.datetime") as clock,
patch("agenda.fx.httpx.Client") as client,
):
clock.now.return_value = datetime(2026, 10, 1, 12)
clock.strptime.side_effect = datetime.strptime
get = client.return_value.__enter__.return_value.get
get.return_value.text = "[]"
assert get_rates(config) == {}
assert get_rates(config) == {}
assert get.call_count == 1
def test_currency_changes_respect_offline_mode(tmp_path: Path) -> None:
"""Changing configuration must not trigger network requests when offline."""
config = flask.config.Config(
"/", {"CURRENCIES": ["USD"], "DATA_DIR": str(tmp_path), "OFFLINE_MODE": True}
)
with patch("agenda.fx.httpx.Client") as client:
assert get_rates(config) == {}
client.assert_not_called()