"""Tests for routes_defi.py - DeFi API routes. TDD: these tests are written before implementation. """ from unittest.mock import AsyncMock, patch import pytest from httpx import ASGITransport, AsyncClient from main import app @pytest.fixture async def client(): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as c: yield c # --------------------------------------------------------------------------- # GET /api/v1/defi/tvl/protocols # --------------------------------------------------------------------------- @pytest.mark.asyncio @patch("routes_defi.defi_service.get_top_protocols", new_callable=AsyncMock) async def test_tvl_protocols_happy_path(mock_fn, client): mock_fn.return_value = [ { "name": "Aave", "symbol": "AAVE", "tvl": 10_000_000_000.0, "chain": "Ethereum", "chains": ["Ethereum"], "category": "Lending", "change_1d": 0.5, "change_7d": -1.2, } ] resp = await client.get("/api/v1/defi/tvl/protocols") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert len(data["data"]) == 1 assert data["data"][0]["name"] == "Aave" assert data["data"][0]["tvl"] == 10_000_000_000.0 @pytest.mark.asyncio @patch("routes_defi.defi_service.get_top_protocols", new_callable=AsyncMock) async def test_tvl_protocols_empty(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/defi/tvl/protocols") assert resp.status_code == 200 assert resp.json()["data"] == [] @pytest.mark.asyncio @patch("routes_defi.defi_service.get_top_protocols", new_callable=AsyncMock) async def test_tvl_protocols_service_error_returns_502(mock_fn, client): mock_fn.side_effect = RuntimeError("DefiLlama unavailable") resp = await client.get("/api/v1/defi/tvl/protocols") assert resp.status_code == 502 # --------------------------------------------------------------------------- # GET /api/v1/defi/tvl/chains # --------------------------------------------------------------------------- @pytest.mark.asyncio @patch("routes_defi.defi_service.get_chain_tvls", new_callable=AsyncMock) async def test_tvl_chains_happy_path(mock_fn, client): mock_fn.return_value = [ {"name": "Ethereum", "tvl": 50_000_000_000.0, "tokenSymbol": "ETH"}, {"name": "BSC", "tvl": 5_000_000_000.0, "tokenSymbol": "BNB"}, ] resp = await client.get("/api/v1/defi/tvl/chains") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert len(data["data"]) == 2 assert data["data"][0]["name"] == "Ethereum" assert data["data"][0]["tokenSymbol"] == "ETH" @pytest.mark.asyncio @patch("routes_defi.defi_service.get_chain_tvls", new_callable=AsyncMock) async def test_tvl_chains_empty(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/defi/tvl/chains") assert resp.status_code == 200 assert resp.json()["data"] == [] @pytest.mark.asyncio @patch("routes_defi.defi_service.get_chain_tvls", new_callable=AsyncMock) async def test_tvl_chains_service_error_returns_502(mock_fn, client): mock_fn.side_effect = RuntimeError("upstream error") resp = await client.get("/api/v1/defi/tvl/chains") assert resp.status_code == 502 # --------------------------------------------------------------------------- # GET /api/v1/defi/tvl/{protocol} # --------------------------------------------------------------------------- @pytest.mark.asyncio @patch("routes_defi.defi_service.get_protocol_tvl", new_callable=AsyncMock) async def test_protocol_tvl_happy_path(mock_fn, client): mock_fn.return_value = 10_000_000_000.0 resp = await client.get("/api/v1/defi/tvl/aave") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert data["data"]["protocol"] == "aave" assert data["data"]["tvl"] == 10_000_000_000.0 @pytest.mark.asyncio @patch("routes_defi.defi_service.get_protocol_tvl", new_callable=AsyncMock) async def test_protocol_tvl_not_found_returns_404(mock_fn, client): mock_fn.return_value = None resp = await client.get("/api/v1/defi/tvl/nonexistent-protocol") assert resp.status_code == 404 @pytest.mark.asyncio @patch("routes_defi.defi_service.get_protocol_tvl", new_callable=AsyncMock) async def test_protocol_tvl_service_error_returns_502(mock_fn, client): mock_fn.side_effect = RuntimeError("HTTP error") resp = await client.get("/api/v1/defi/tvl/aave") assert resp.status_code == 502 @pytest.mark.asyncio @patch("routes_defi.defi_service.get_protocol_tvl", new_callable=AsyncMock) async def test_protocol_tvl_passes_slug_to_service(mock_fn, client): mock_fn.return_value = 5_000_000_000.0 await client.get("/api/v1/defi/tvl/uniswap-v3") mock_fn.assert_called_once_with("uniswap-v3") # --------------------------------------------------------------------------- # GET /api/v1/defi/yields # --------------------------------------------------------------------------- @pytest.mark.asyncio @patch("routes_defi.defi_service.get_yield_pools", new_callable=AsyncMock) async def test_yields_happy_path(mock_fn, client): mock_fn.return_value = [ { "pool": "0xabcd", "chain": "Ethereum", "project": "aave-v3", "symbol": "USDC", "tvlUsd": 1_000_000_000.0, "apy": 3.5, "apyBase": 3.0, "apyReward": 0.5, } ] resp = await client.get("/api/v1/defi/yields") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert len(data["data"]) == 1 assert data["data"][0]["pool"] == "0xabcd" assert data["data"][0]["apy"] == 3.5 @pytest.mark.asyncio @patch("routes_defi.defi_service.get_yield_pools", new_callable=AsyncMock) async def test_yields_with_chain_filter(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/defi/yields?chain=Ethereum") assert resp.status_code == 200 mock_fn.assert_called_once_with(chain="Ethereum", project=None) @pytest.mark.asyncio @patch("routes_defi.defi_service.get_yield_pools", new_callable=AsyncMock) async def test_yields_with_project_filter(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/defi/yields?project=aave-v3") assert resp.status_code == 200 mock_fn.assert_called_once_with(chain=None, project="aave-v3") @pytest.mark.asyncio @patch("routes_defi.defi_service.get_yield_pools", new_callable=AsyncMock) async def test_yields_with_chain_and_project_filter(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/defi/yields?chain=Polygon&project=curve") assert resp.status_code == 200 mock_fn.assert_called_once_with(chain="Polygon", project="curve") @pytest.mark.asyncio @patch("routes_defi.defi_service.get_yield_pools", new_callable=AsyncMock) async def test_yields_empty(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/defi/yields") assert resp.status_code == 200 assert resp.json()["data"] == [] @pytest.mark.asyncio @patch("routes_defi.defi_service.get_yield_pools", new_callable=AsyncMock) async def test_yields_service_error_returns_502(mock_fn, client): mock_fn.side_effect = RuntimeError("yields API down") resp = await client.get("/api/v1/defi/yields") assert resp.status_code == 502 # --------------------------------------------------------------------------- # GET /api/v1/defi/stablecoins # --------------------------------------------------------------------------- @pytest.mark.asyncio @patch("routes_defi.defi_service.get_stablecoins", new_callable=AsyncMock) async def test_stablecoins_happy_path(mock_fn, client): mock_fn.return_value = [ { "name": "Tether", "symbol": "USDT", "pegType": "peggedUSD", "circulating": 100_000_000_000.0, "price": 1.0, } ] resp = await client.get("/api/v1/defi/stablecoins") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert len(data["data"]) == 1 assert data["data"][0]["symbol"] == "USDT" assert data["data"][0]["circulating"] == 100_000_000_000.0 @pytest.mark.asyncio @patch("routes_defi.defi_service.get_stablecoins", new_callable=AsyncMock) async def test_stablecoins_empty(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/defi/stablecoins") assert resp.status_code == 200 assert resp.json()["data"] == [] @pytest.mark.asyncio @patch("routes_defi.defi_service.get_stablecoins", new_callable=AsyncMock) async def test_stablecoins_service_error_returns_502(mock_fn, client): mock_fn.side_effect = RuntimeError("stables API error") resp = await client.get("/api/v1/defi/stablecoins") assert resp.status_code == 502 # --------------------------------------------------------------------------- # GET /api/v1/defi/volumes/dexs # --------------------------------------------------------------------------- @pytest.mark.asyncio @patch("routes_defi.defi_service.get_dex_volumes", new_callable=AsyncMock) async def test_dex_volumes_happy_path(mock_fn, client): mock_fn.return_value = { "totalVolume24h": 5_000_000_000.0, "totalVolume7d": 30_000_000_000.0, "protocols": [ {"name": "Uniswap", "volume24h": 2_000_000_000.0}, ], } resp = await client.get("/api/v1/defi/volumes/dexs") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert data["data"]["totalVolume24h"] == 5_000_000_000.0 assert data["data"]["totalVolume7d"] == 30_000_000_000.0 assert len(data["data"]["protocols"]) == 1 @pytest.mark.asyncio @patch("routes_defi.defi_service.get_dex_volumes", new_callable=AsyncMock) async def test_dex_volumes_returns_502_when_service_returns_none(mock_fn, client): mock_fn.return_value = None resp = await client.get("/api/v1/defi/volumes/dexs") assert resp.status_code == 502 @pytest.mark.asyncio @patch("routes_defi.defi_service.get_dex_volumes", new_callable=AsyncMock) async def test_dex_volumes_service_error_returns_502(mock_fn, client): mock_fn.side_effect = RuntimeError("volume API error") resp = await client.get("/api/v1/defi/volumes/dexs") assert resp.status_code == 502 # --------------------------------------------------------------------------- # GET /api/v1/defi/fees # --------------------------------------------------------------------------- @pytest.mark.asyncio @patch("routes_defi.defi_service.get_protocol_fees", new_callable=AsyncMock) async def test_fees_happy_path(mock_fn, client): mock_fn.return_value = [ {"name": "Uniswap", "fees24h": 1_000_000.0, "revenue24h": 500_000.0}, {"name": "Aave", "fees24h": 800_000.0, "revenue24h": 800_000.0}, ] resp = await client.get("/api/v1/defi/fees") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert len(data["data"]) == 2 assert data["data"][0]["name"] == "Uniswap" assert data["data"][0]["fees24h"] == 1_000_000.0 assert data["data"][0]["revenue24h"] == 500_000.0 @pytest.mark.asyncio @patch("routes_defi.defi_service.get_protocol_fees", new_callable=AsyncMock) async def test_fees_empty(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/defi/fees") assert resp.status_code == 200 assert resp.json()["data"] == [] @pytest.mark.asyncio @patch("routes_defi.defi_service.get_protocol_fees", new_callable=AsyncMock) async def test_fees_service_error_returns_502(mock_fn, client): mock_fn.side_effect = RuntimeError("fees API error") resp = await client.get("/api/v1/defi/fees") assert resp.status_code == 502