7 new endpoints under /api/v1/defi/ (all free, no API key):
- GET /defi/tvl/protocols - top DeFi protocols by TVL
- GET /defi/tvl/chains - chain TVL rankings
- GET /defi/tvl/{protocol} - single protocol TVL
- GET /defi/yields - top yield pools (filter by chain/project)
- GET /defi/stablecoins - stablecoin market data
- GET /defi/volumes/dexs - DEX volume overview
- GET /defi/fees - protocol fees/revenue overview
Data source: DefiLlama API (free, no key needed)
58 new tests (33 service + 25 route). All 561 tests passing.
657 lines
20 KiB
Python
657 lines
20 KiB
Python
"""Tests for defi_service.py - DefiLlama API integration.
|
|
|
|
TDD: these tests are written before implementation.
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
import defi_service
|
|
from defi_service import (
|
|
get_chain_tvls,
|
|
get_dex_volumes,
|
|
get_protocol_fees,
|
|
get_protocol_tvl,
|
|
get_stablecoins,
|
|
get_top_protocols,
|
|
get_yield_pools,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
SAMPLE_PROTOCOLS = [
|
|
{
|
|
"name": "Aave",
|
|
"symbol": "AAVE",
|
|
"tvl": 10_000_000_000.0,
|
|
"chain": "Ethereum",
|
|
"chains": ["Ethereum", "Polygon"],
|
|
"category": "Lending",
|
|
"change_1d": 0.5,
|
|
"change_7d": -1.2,
|
|
},
|
|
{
|
|
"name": "Uniswap",
|
|
"symbol": "UNI",
|
|
"tvl": 8_000_000_000.0,
|
|
"chain": "Ethereum",
|
|
"chains": ["Ethereum"],
|
|
"category": "DEX",
|
|
"change_1d": 1.0,
|
|
"change_7d": 2.0,
|
|
},
|
|
]
|
|
|
|
SAMPLE_CHAINS = [
|
|
{"name": "Ethereum", "tvl": 50_000_000_000.0, "tokenSymbol": "ETH"},
|
|
{"name": "BSC", "tvl": 5_000_000_000.0, "tokenSymbol": "BNB"},
|
|
]
|
|
|
|
SAMPLE_POOLS = [
|
|
{
|
|
"pool": "0xabcd",
|
|
"chain": "Ethereum",
|
|
"project": "aave-v3",
|
|
"symbol": "USDC",
|
|
"tvlUsd": 1_000_000_000.0,
|
|
"apy": 3.5,
|
|
"apyBase": 3.0,
|
|
"apyReward": 0.5,
|
|
},
|
|
{
|
|
"pool": "0x1234",
|
|
"chain": "Polygon",
|
|
"project": "curve",
|
|
"symbol": "DAI",
|
|
"tvlUsd": 500_000_000.0,
|
|
"apy": 4.2,
|
|
"apyBase": 2.5,
|
|
"apyReward": 1.7,
|
|
},
|
|
]
|
|
|
|
SAMPLE_STABLECOINS_RESPONSE = {
|
|
"peggedAssets": [
|
|
{
|
|
"name": "Tether",
|
|
"symbol": "USDT",
|
|
"pegType": "peggedUSD",
|
|
"circulating": {"peggedUSD": 100_000_000_000.0},
|
|
"price": 1.0,
|
|
},
|
|
{
|
|
"name": "USD Coin",
|
|
"symbol": "USDC",
|
|
"pegType": "peggedUSD",
|
|
"circulating": {"peggedUSD": 40_000_000_000.0},
|
|
"price": 1.0,
|
|
},
|
|
]
|
|
}
|
|
|
|
SAMPLE_DEX_RESPONSE = {
|
|
"total24h": 5_000_000_000.0,
|
|
"total7d": 30_000_000_000.0,
|
|
"protocols": [
|
|
{"name": "Uniswap", "total24h": 2_000_000_000.0},
|
|
{"name": "Curve", "total24h": 500_000_000.0},
|
|
],
|
|
}
|
|
|
|
SAMPLE_FEES_RESPONSE = {
|
|
"protocols": [
|
|
{"name": "Uniswap", "total24h": 1_000_000.0, "revenue24h": 500_000.0},
|
|
{"name": "Aave", "total24h": 800_000.0, "revenue24h": 800_000.0},
|
|
],
|
|
}
|
|
|
|
|
|
def _make_mock_client(json_return):
|
|
"""Build a fully configured AsyncMock for httpx.AsyncClient context manager.
|
|
|
|
Uses MagicMock for the response object so that resp.json() (a sync method
|
|
in httpx) returns the value directly rather than a coroutine.
|
|
"""
|
|
mock_resp = MagicMock()
|
|
mock_resp.status_code = 200
|
|
mock_resp.raise_for_status = MagicMock()
|
|
mock_resp.json.return_value = json_return
|
|
|
|
mock_client = AsyncMock()
|
|
mock_client.get = AsyncMock(return_value=mock_resp)
|
|
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
mock_client.__aexit__ = AsyncMock(return_value=None)
|
|
return mock_client
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_top_protocols
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_top_protocols_returns_top_20(mock_client_cls):
|
|
raw = [
|
|
{
|
|
"name": f"Protocol{i}",
|
|
"symbol": f"P{i}",
|
|
"tvl": float(100 - i),
|
|
"chain": "Ethereum",
|
|
"chains": ["Ethereum"],
|
|
"category": "Lending",
|
|
"change_1d": 0.1,
|
|
"change_7d": 0.2,
|
|
}
|
|
for i in range(30)
|
|
]
|
|
mock_client_cls.return_value = _make_mock_client(raw)
|
|
|
|
result = await get_top_protocols()
|
|
|
|
assert len(result) == 20
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_top_protocols_returns_correct_fields(mock_client_cls):
|
|
mock_client_cls.return_value = _make_mock_client(SAMPLE_PROTOCOLS)
|
|
|
|
result = await get_top_protocols()
|
|
|
|
assert len(result) == 2
|
|
first = result[0]
|
|
assert first["name"] == "Aave"
|
|
assert first["symbol"] == "AAVE"
|
|
assert first["tvl"] == 10_000_000_000.0
|
|
assert first["chain"] == "Ethereum"
|
|
assert first["chains"] == ["Ethereum", "Polygon"]
|
|
assert first["category"] == "Lending"
|
|
assert first["change_1d"] == 0.5
|
|
assert first["change_7d"] == -1.2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_top_protocols_respects_custom_limit(mock_client_cls):
|
|
raw = [
|
|
{"name": f"P{i}", "symbol": f"S{i}", "tvl": float(i), "chain": "ETH",
|
|
"chains": [], "category": "DEX", "change_1d": 0.0, "change_7d": 0.0}
|
|
for i in range(25)
|
|
]
|
|
mock_client_cls.return_value = _make_mock_client(raw)
|
|
|
|
result = await get_top_protocols(limit=5)
|
|
|
|
assert len(result) == 5
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_top_protocols_handles_missing_fields(mock_client_cls):
|
|
raw = [{"name": "Sparse"}] # missing all optional fields
|
|
mock_client_cls.return_value = _make_mock_client(raw)
|
|
|
|
result = await get_top_protocols()
|
|
|
|
assert len(result) == 1
|
|
assert result[0]["name"] == "Sparse"
|
|
assert result[0]["tvl"] is None
|
|
assert result[0]["symbol"] is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_top_protocols_returns_empty_on_http_error(mock_client_cls):
|
|
mock_resp = MagicMock()
|
|
mock_resp.raise_for_status.side_effect = Exception("HTTP 500")
|
|
mock_client = AsyncMock()
|
|
mock_client.get = AsyncMock(return_value=mock_resp)
|
|
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
mock_client.__aexit__ = AsyncMock(return_value=None)
|
|
mock_client_cls.return_value = mock_client
|
|
|
|
result = await get_top_protocols()
|
|
|
|
assert result == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_top_protocols_calls_correct_url(mock_client_cls):
|
|
mock_client = _make_mock_client([])
|
|
mock_client_cls.return_value = mock_client
|
|
|
|
await get_top_protocols()
|
|
|
|
mock_client.get.assert_called_once_with("https://api.llama.fi/protocols")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_chain_tvls
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_chain_tvls_returns_all_chains(mock_client_cls):
|
|
mock_client_cls.return_value = _make_mock_client(SAMPLE_CHAINS)
|
|
|
|
result = await get_chain_tvls()
|
|
|
|
assert len(result) == 2
|
|
assert result[0]["name"] == "Ethereum"
|
|
assert result[0]["tvl"] == 50_000_000_000.0
|
|
assert result[0]["tokenSymbol"] == "ETH"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_chain_tvls_handles_missing_token_symbol(mock_client_cls):
|
|
raw = [{"name": "SomeChain", "tvl": 100.0}]
|
|
mock_client_cls.return_value = _make_mock_client(raw)
|
|
|
|
result = await get_chain_tvls()
|
|
|
|
assert result[0]["tokenSymbol"] is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_chain_tvls_returns_empty_on_error(mock_client_cls):
|
|
mock_resp = MagicMock()
|
|
mock_resp.raise_for_status.side_effect = Exception("timeout")
|
|
mock_client = AsyncMock()
|
|
mock_client.get = AsyncMock(return_value=mock_resp)
|
|
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
mock_client.__aexit__ = AsyncMock(return_value=None)
|
|
mock_client_cls.return_value = mock_client
|
|
|
|
result = await get_chain_tvls()
|
|
|
|
assert result == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_chain_tvls_calls_correct_url(mock_client_cls):
|
|
mock_client = _make_mock_client([])
|
|
mock_client_cls.return_value = mock_client
|
|
|
|
await get_chain_tvls()
|
|
|
|
mock_client.get.assert_called_once_with("https://api.llama.fi/v2/chains")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_protocol_tvl
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_protocol_tvl_returns_numeric_value(mock_client_cls):
|
|
mock_client_cls.return_value = _make_mock_client(10_000_000_000.0)
|
|
|
|
result = await get_protocol_tvl("aave")
|
|
|
|
assert result == 10_000_000_000.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_protocol_tvl_calls_correct_url(mock_client_cls):
|
|
mock_client = _make_mock_client(1234.0)
|
|
mock_client_cls.return_value = mock_client
|
|
|
|
await get_protocol_tvl("uniswap")
|
|
|
|
mock_client.get.assert_called_once_with("https://api.llama.fi/tvl/uniswap")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_protocol_tvl_returns_none_on_error(mock_client_cls):
|
|
mock_resp = MagicMock()
|
|
mock_resp.raise_for_status.side_effect = Exception("404 not found")
|
|
mock_client = AsyncMock()
|
|
mock_client.get = AsyncMock(return_value=mock_resp)
|
|
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
mock_client.__aexit__ = AsyncMock(return_value=None)
|
|
mock_client_cls.return_value = mock_client
|
|
|
|
result = await get_protocol_tvl("nonexistent")
|
|
|
|
assert result is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_yield_pools
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_yield_pools_returns_top_20_by_tvl(mock_client_cls):
|
|
# Create 30 pools with decreasing TVL
|
|
raw_data = {
|
|
"data": [
|
|
{
|
|
"pool": f"pool{i}",
|
|
"chain": "Ethereum",
|
|
"project": "aave",
|
|
"symbol": "USDC",
|
|
"tvlUsd": float(1000 - i),
|
|
"apy": 3.0,
|
|
"apyBase": 2.5,
|
|
"apyReward": 0.5,
|
|
}
|
|
for i in range(30)
|
|
]
|
|
}
|
|
mock_client_cls.return_value = _make_mock_client(raw_data)
|
|
|
|
result = await get_yield_pools()
|
|
|
|
assert len(result) == 20
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_yield_pools_returns_correct_fields(mock_client_cls):
|
|
mock_client_cls.return_value = _make_mock_client({"data": SAMPLE_POOLS})
|
|
|
|
result = await get_yield_pools()
|
|
|
|
assert len(result) == 2
|
|
first = result[0]
|
|
assert first["pool"] == "0xabcd"
|
|
assert first["chain"] == "Ethereum"
|
|
assert first["project"] == "aave-v3"
|
|
assert first["symbol"] == "USDC"
|
|
assert first["tvlUsd"] == 1_000_000_000.0
|
|
assert first["apy"] == 3.5
|
|
assert first["apyBase"] == 3.0
|
|
assert first["apyReward"] == 0.5
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_yield_pools_filters_by_chain(mock_client_cls):
|
|
mock_client_cls.return_value = _make_mock_client({"data": SAMPLE_POOLS})
|
|
|
|
result = await get_yield_pools(chain="Ethereum")
|
|
|
|
assert len(result) == 1
|
|
assert result[0]["chain"] == "Ethereum"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_yield_pools_filters_by_project(mock_client_cls):
|
|
mock_client_cls.return_value = _make_mock_client({"data": SAMPLE_POOLS})
|
|
|
|
result = await get_yield_pools(project="curve")
|
|
|
|
assert len(result) == 1
|
|
assert result[0]["project"] == "curve"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_yield_pools_filters_by_chain_and_project(mock_client_cls):
|
|
pools = SAMPLE_POOLS + [
|
|
{
|
|
"pool": "0xzzzz",
|
|
"chain": "Ethereum",
|
|
"project": "curve",
|
|
"symbol": "USDT",
|
|
"tvlUsd": 200_000_000.0,
|
|
"apy": 2.0,
|
|
"apyBase": 2.0,
|
|
"apyReward": 0.0,
|
|
}
|
|
]
|
|
mock_client_cls.return_value = _make_mock_client({"data": pools})
|
|
|
|
result = await get_yield_pools(chain="Ethereum", project="curve")
|
|
|
|
assert len(result) == 1
|
|
assert result[0]["pool"] == "0xzzzz"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_yield_pools_returns_empty_on_error(mock_client_cls):
|
|
mock_resp = MagicMock()
|
|
mock_resp.raise_for_status.side_effect = Exception("Connection error")
|
|
mock_client = AsyncMock()
|
|
mock_client.get = AsyncMock(return_value=mock_resp)
|
|
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
mock_client.__aexit__ = AsyncMock(return_value=None)
|
|
mock_client_cls.return_value = mock_client
|
|
|
|
result = await get_yield_pools()
|
|
|
|
assert result == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_yield_pools_calls_correct_url(mock_client_cls):
|
|
mock_client = _make_mock_client({"data": []})
|
|
mock_client_cls.return_value = mock_client
|
|
|
|
await get_yield_pools()
|
|
|
|
mock_client.get.assert_called_once_with("https://yields.llama.fi/pools")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_yield_pools_sorts_by_tvl_descending(mock_client_cls):
|
|
unsorted_pools = [
|
|
{
|
|
"pool": "low", "chain": "Ethereum", "project": "aave",
|
|
"symbol": "DAI", "tvlUsd": 100.0, "apy": 1.0,
|
|
"apyBase": 1.0, "apyReward": 0.0,
|
|
},
|
|
{
|
|
"pool": "high", "chain": "Ethereum", "project": "aave",
|
|
"symbol": "USDC", "tvlUsd": 9000.0, "apy": 2.0,
|
|
"apyBase": 2.0, "apyReward": 0.0,
|
|
},
|
|
]
|
|
mock_client_cls.return_value = _make_mock_client({"data": unsorted_pools})
|
|
|
|
result = await get_yield_pools()
|
|
|
|
assert result[0]["pool"] == "high"
|
|
assert result[1]["pool"] == "low"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_stablecoins
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_stablecoins_returns_correct_fields(mock_client_cls):
|
|
mock_client_cls.return_value = _make_mock_client(SAMPLE_STABLECOINS_RESPONSE)
|
|
|
|
result = await get_stablecoins()
|
|
|
|
assert len(result) == 2
|
|
first = result[0]
|
|
assert first["name"] == "Tether"
|
|
assert first["symbol"] == "USDT"
|
|
assert first["pegType"] == "peggedUSD"
|
|
assert first["circulating"] == 100_000_000_000.0
|
|
assert first["price"] == 1.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_stablecoins_returns_top_20(mock_client_cls):
|
|
assets = [
|
|
{
|
|
"name": f"Stable{i}",
|
|
"symbol": f"S{i}",
|
|
"pegType": "peggedUSD",
|
|
"circulating": {"peggedUSD": float(1000 - i)},
|
|
"price": 1.0,
|
|
}
|
|
for i in range(25)
|
|
]
|
|
mock_client_cls.return_value = _make_mock_client({"peggedAssets": assets})
|
|
|
|
result = await get_stablecoins()
|
|
|
|
assert len(result) == 20
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_stablecoins_handles_missing_circulating(mock_client_cls):
|
|
raw = {"peggedAssets": [{"name": "NoCirc", "symbol": "NC", "pegType": "peggedUSD", "price": 1.0}]}
|
|
mock_client_cls.return_value = _make_mock_client(raw)
|
|
|
|
result = await get_stablecoins()
|
|
|
|
assert result[0]["circulating"] is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_stablecoins_returns_empty_on_error(mock_client_cls):
|
|
mock_resp = MagicMock()
|
|
mock_resp.raise_for_status.side_effect = Exception("timeout")
|
|
mock_client = AsyncMock()
|
|
mock_client.get = AsyncMock(return_value=mock_resp)
|
|
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
mock_client.__aexit__ = AsyncMock(return_value=None)
|
|
mock_client_cls.return_value = mock_client
|
|
|
|
result = await get_stablecoins()
|
|
|
|
assert result == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_stablecoins_calls_correct_url(mock_client_cls):
|
|
mock_client = _make_mock_client({"peggedAssets": []})
|
|
mock_client_cls.return_value = mock_client
|
|
|
|
await get_stablecoins()
|
|
|
|
mock_client.get.assert_called_once_with("https://stablecoins.llama.fi/stablecoins")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_dex_volumes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_dex_volumes_returns_correct_structure(mock_client_cls):
|
|
mock_client_cls.return_value = _make_mock_client(SAMPLE_DEX_RESPONSE)
|
|
|
|
result = await get_dex_volumes()
|
|
|
|
assert result["totalVolume24h"] == 5_000_000_000.0
|
|
assert result["totalVolume7d"] == 30_000_000_000.0
|
|
assert len(result["protocols"]) == 2
|
|
assert result["protocols"][0]["name"] == "Uniswap"
|
|
assert result["protocols"][0]["volume24h"] == 2_000_000_000.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_dex_volumes_returns_none_on_error(mock_client_cls):
|
|
mock_resp = MagicMock()
|
|
mock_resp.raise_for_status.side_effect = Exception("server error")
|
|
mock_client = AsyncMock()
|
|
mock_client.get = AsyncMock(return_value=mock_resp)
|
|
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
mock_client.__aexit__ = AsyncMock(return_value=None)
|
|
mock_client_cls.return_value = mock_client
|
|
|
|
result = await get_dex_volumes()
|
|
|
|
assert result is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_dex_volumes_calls_correct_url(mock_client_cls):
|
|
mock_client = _make_mock_client({"total24h": 0, "total7d": 0, "protocols": []})
|
|
mock_client_cls.return_value = mock_client
|
|
|
|
await get_dex_volumes()
|
|
|
|
mock_client.get.assert_called_once_with("https://api.llama.fi/overview/dexs")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_protocol_fees
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_protocol_fees_returns_correct_structure(mock_client_cls):
|
|
mock_client_cls.return_value = _make_mock_client(SAMPLE_FEES_RESPONSE)
|
|
|
|
result = await get_protocol_fees()
|
|
|
|
assert len(result) == 2
|
|
first = result[0]
|
|
assert first["name"] == "Uniswap"
|
|
assert first["fees24h"] == 1_000_000.0
|
|
assert first["revenue24h"] == 500_000.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_protocol_fees_returns_empty_on_error(mock_client_cls):
|
|
mock_resp = MagicMock()
|
|
mock_resp.raise_for_status.side_effect = Exception("connection reset")
|
|
mock_client = AsyncMock()
|
|
mock_client.get = AsyncMock(return_value=mock_resp)
|
|
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
|
|
mock_client.__aexit__ = AsyncMock(return_value=None)
|
|
mock_client_cls.return_value = mock_client
|
|
|
|
result = await get_protocol_fees()
|
|
|
|
assert result == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_protocol_fees_calls_correct_url(mock_client_cls):
|
|
mock_client = _make_mock_client({"protocols": []})
|
|
mock_client_cls.return_value = mock_client
|
|
|
|
await get_protocol_fees()
|
|
|
|
mock_client.get.assert_called_once_with("https://api.llama.fi/overview/fees")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("defi_service.httpx.AsyncClient")
|
|
async def test_get_protocol_fees_handles_missing_revenue(mock_client_cls):
|
|
raw = {"protocols": [{"name": "SomeProtocol", "total24h": 500_000.0}]}
|
|
mock_client_cls.return_value = _make_mock_client(raw)
|
|
|
|
result = await get_protocol_fees()
|
|
|
|
assert result[0]["revenue24h"] is None
|