Compare the latest cached currency list before applying cache TTL and match cache filenames exactly. Preserve offline mode and failure retry backoff. Closes #170
93 lines
3.2 KiB
Python
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()
|