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
This commit is contained in:
Edward Betts 2026-10-01 13:25:52 +01:00
parent 22976868ce
commit 6b253f2d04
2 changed files with 106 additions and 5 deletions

View file

@ -0,0 +1,93 @@
"""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()