import httpx from fastapi.testclient import TestClient from app import main class StubAsyncClient: def __init__(self, response=None, error=None): self.response = response self.error = error self.requested_url = None self.timeout = None async def __aenter__(self): return self async def __aexit__(self, exc_type, exc, traceback): return None async def get(self, url, timeout): self.requested_url = url self.timeout = timeout if self.error: raise self.error return self.response def test_health_is_independent_of_btcdata(): response = TestClient(main.app).get("/api/health") assert response.status_code == 200 assert response.json() == {"status": "up"} def test_home_updates_title_from_latest_price(): response = TestClient(main.app).get("/") assert response.status_code == 200 assert "new MutationObserver(updateTitleFromPrice)" in response.text assert "currentPrice - midnightPrice" in response.text assert "document.title = price + ' | ' + formattedChange" in response.text assert '

BTC Price

' in response.text assert "page-title" not in response.text assert 'id="midnight-change"' in response.text assert "midnightChange.textContent = formattedChange" in response.text assert "since 00:00" not in response.text assert "change > 0 ? 'text-green-600' : change < 0 ? 'text-red-600'" in response.text assert "formatPriceChange(change)" in response.text assert "midnight.setHours(0, 0, 0, 0)" in response.text def test_price_fragment_exposes_formatted_price_for_title(): html = main.price_json_to_html( '{"price": 67432.1, "prev_price": 67400, "recorded_at": ""}' ) assert 'data-title-price="$67.432,10"' in html assert 'data-price-value="67432.1"' in html assert ">$67.432,10

" in html def test_ready_when_btcdata_is_healthy(monkeypatch): request = httpx.Request("GET", "http://btcdata.test/api/health") upstream = httpx.Response(200, request=request) client = StubAsyncClient(response=upstream) monkeypatch.setattr(main, "BTCDATA_URL", "http://btcdata.test") monkeypatch.setattr(main.httpx, "AsyncClient", lambda: client) response = TestClient(main.app).get("/api/ready") assert response.status_code == 200 assert response.json() == {"status": "ready"} assert client.requested_url == "http://btcdata.test/api/health" assert client.timeout == main.READINESS_TIMEOUT_SECONDS def test_not_ready_when_btcdata_is_unavailable(monkeypatch): request = httpx.Request("GET", "http://btcdata.test/api/health") error = httpx.ConnectError("connection refused", request=request) client = StubAsyncClient(error=error) monkeypatch.setattr(main, "BTCDATA_URL", "http://btcdata.test") monkeypatch.setattr(main.httpx, "AsyncClient", lambda: client) response = TestClient(main.app).get("/api/ready") assert response.status_code == 503 assert response.json() == {"status": "not ready"} assert "btcdata.test" not in response.text def test_not_ready_when_btcdata_returns_an_error(monkeypatch): request = httpx.Request("GET", "http://btcdata.test/api/health") upstream = httpx.Response(503, request=request) client = StubAsyncClient(response=upstream) monkeypatch.setattr(main.httpx, "AsyncClient", lambda: client) response = TestClient(main.app).get("/api/ready") assert response.status_code == 503 assert response.json() == {"status": "not ready"} def test_version_endpoint(monkeypatch): monkeypatch.setattr(main, "APP_VERSION", "abc1234") response = TestClient(main.app).get("/api/version") assert response.status_code == 200 assert response.json() == {"version": "abc1234"} def test_24_hour_chart_uses_half_hour_candles(): response = TestClient(main.app).get("/") assert "24h Candles (30m)" in response.text assert "initChart('chart-hour', '/api/kraken-half-hour', 5 * 60 * 1000, updateMidnightPrice)" in response.text assert "initChart('chart-minute', '/api/kraken-minute', 60 * 1000)" in response.text def test_half_hour_proxy_preserves_candles_and_upstream_errors(monkeypatch): for status, payload in [(200, [{"ts": 1800, "open": "10", "high": "12", "low": "9", "close": "11"}]), (503, {"error": "database unavailable"})]: request = httpx.Request("GET", "http://btcdata.test/api/kraken-half-hour") client = StubAsyncClient(response=httpx.Response(status, json=payload, request=request)) monkeypatch.setattr(main, "BTCDATA_URL", "http://btcdata.test") monkeypatch.setattr(main.httpx, "AsyncClient", lambda: client) response = TestClient(main.app).get("/api/kraken-half-hour") assert response.status_code == status assert response.json() == payload assert client.requested_url == "http://btcdata.test/api/kraken-half-hour"