from datetime import datetime, timezone import httpx import pytest from app.services.metals.bullionvault_service import BullionVaultService def _csv_response(rows: list[str]) -> str: header = ( '"Date","High (kg)","Low (kg)","Close (kg)",,"High (troy oz)","Low (troy oz)","Close (troy oz)",\n' ) return header + "".join(f"{row}\n" for row in rows) def _mock_client(handler): transport = httpx.MockTransport(handler) return httpx.AsyncClient(transport=transport) @pytest.mark.asyncio async def test_get_current_gold_price_parses_csv() -> None: csv = _csv_response( [ '"12:00:00 23-Nov-2025",130702.99,130600.11,130650.10,,4065.32,4040.32,4050.55,', '"11:50:00 23-Nov-2025",130600.99,130500.11,130550.10,,4055.32,4030.32,4040.55,', ] ) async def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(200, text=csv) async with _mock_client(handler) as client: service = BullionVaultService(client=client, base_url="https://chart-data.bullionvault.com") result = await service.get_current_gold_price("usd") assert result["currency"] == "USD" assert result["price"] == pytest.approx(4050.55, rel=0.001) assert result["price_kg"] == pytest.approx(130650.10, rel=0.001) assert result["data_points"] == 2 assert datetime.fromisoformat(result["timestamp"]).tzinfo == timezone.utc @pytest.mark.asyncio async def test_get_current_gold_price_raises_on_empty_data() -> None: async def handler(_: httpx.Request) -> httpx.Response: return httpx.Response(200, text='"Date",\n') async with _mock_client(handler) as client: service = BullionVaultService(client=client) with pytest.raises(ValueError, match="No price data available"): await service.get_current_gold_price() @pytest.mark.asyncio async def test_get_gold_history_applies_limit() -> None: csv = _csv_response( [ '"12:00:00 23-Nov-2025",130702.99,130600.11,130650.10,,4065.32,4040.32,4050.55,', '"11:50:00 23-Nov-2025",130600.99,130500.11,130550.10,,4055.32,4030.32,4040.55,', '"11:40:00 23-Nov-2025",130500.99,130400.11,130450.10,,4045.32,4020.32,4030.55,', ] ) async def handler(_: httpx.Request) -> httpx.Response: return httpx.Response(200, text=csv) async with _mock_client(handler) as client: service = BullionVaultService(client=client) history = await service.get_gold_history(limit=2) assert len(history) == 2 assert history[0]["close"] == pytest.approx(4050.55, rel=0.001) assert history[1]["close"] == pytest.approx(4040.55, rel=0.001) @pytest.mark.asyncio async def test_get_multi_currency_prices_skips_failures() -> None: csv = _csv_response( [ '"12:00:00 23-Nov-2025",130702.99,130600.11,130650.10,,4065.32,4040.32,4050.55,', ] ) retry_counts: dict[str, int] = {} async def handler(request: httpx.Request) -> httpx.Response: currency = request.url.path.split("/")[4] retry_counts[currency] = retry_counts.get(currency, 0) + 1 if currency == "GBP": return httpx.Response(500, text="error") return httpx.Response(200, text=csv) async with _mock_client(handler) as client: service = BullionVaultService(client=client, max_retries=1) prices = await service.get_multi_currency_prices() # Only currencies served by successful responses should be included assert "USD" in prices assert "EUR" in prices assert "GBP" not in prices assert retry_counts["GBP"] == 1