"""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()