Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
377 changes: 377 additions & 0 deletions tests/test_bots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,377 @@
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
import discord
from discord.ext import commands

from bots.commander import CommanderBot, pool, on_select_pool
from bots.fees import FeesBot
from bots.price import PriceBot
from bots.rewards import RewardsBot
from bots.tvl import TVLBot
from bots.ticker import TickerBot
from bots.data import LiquidityPool, LiquidityPoolEpoch
from bots.helpers import is_address


@pytest.fixture
def mock_interaction():
"""Mock Discord interaction for testing"""
interaction = MagicMock(spec=discord.Interaction)
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
interaction.user = MagicMock()
interaction.user.id = 12345
interaction.client = MagicMock()
interaction.client.emojis = []
return interaction


@pytest.fixture
def mock_pool():
"""Mock LiquidityPool for testing"""
pool = MagicMock(spec=LiquidityPool)
pool.lp = "0x1234567890123456789012345678901234567890"
pool.token0 = MagicMock()
pool.token0.symbol = "WETH"
pool.token1 = MagicMock()
pool.token1.symbol = "USDC"
pool.is_stable = True
pool.pool_fee_percentage = 0.003
pool.volume = 1000000
pool.reserve0 = MagicMock()
pool.reserve0.amount = 100
pool.reserve1 = MagicMock()
pool.reserve1.amount = 200000
return pool


@pytest.fixture
def mock_pool_epoch():
"""Mock LiquidityPoolEpoch for testing"""
epoch = MagicMock(spec=LiquidityPoolEpoch)
epoch.total_fees = 50000
epoch.total_bribes = 25000
return epoch


class TestCommanderBot:
"""Test cases for CommanderBot slash commands"""

@pytest.mark.asyncio
async def test_pool_command_with_valid_address(self, mock_interaction, mock_pool):
"""Test /pool command with a valid pool address"""
with patch('bots.commander.LiquidityPool.by_address', return_value=mock_pool), \
patch('bots.commander.LiquidityPool.tvl', return_value=1000000), \
patch('bots.commander.LiquidityPoolEpoch.fetch_for_pool', return_value=MagicMock()), \
patch('bots.commander.PoolStats') as mock_pool_stats:

mock_embed = MagicMock()
mock_pool_stats.return_value.render = AsyncMock(return_value=mock_embed)

await pool(mock_interaction, "0x1234567890123456789012345678901234567890")

mock_interaction.response.send_message.assert_called_once()
mock_pool_stats.assert_called_once()

@pytest.mark.asyncio
async def test_pool_command_with_invalid_address(self, mock_interaction):
"""Test /pool command with an invalid pool address"""
with patch('bots.commander.LiquidityPool.by_address', return_value=None):
await pool(mock_interaction, "0xinvalid")

mock_interaction.response.send_message.assert_called_once_with(
"No pool found with this address: 0xinvalid"
)

@pytest.mark.asyncio
async def test_pool_command_with_search_query_single_result(self, mock_interaction, mock_pool):
"""Test /pool command with search query that returns single result"""
with patch('bots.commander.is_address', return_value=False), \
patch('bots.commander.LiquidityPool.search', return_value=[mock_pool]), \
patch('bots.commander.LiquidityPool.tvl', return_value=1000000), \
patch('bots.commander.LiquidityPoolEpoch.fetch_for_pool', return_value=MagicMock()), \
patch('bots.commander.PoolStats') as mock_pool_stats:

mock_embed = MagicMock()
mock_pool_stats.return_value.render = AsyncMock(return_value=mock_embed)

await pool(mock_interaction, "WETH")

mock_interaction.response.send_message.assert_called_once()
mock_pool_stats.assert_called_once()

@pytest.mark.asyncio
async def test_pool_command_with_search_query_multiple_results(self, mock_interaction, mock_pool):
"""Test /pool command with search query that returns multiple results"""
pools = [mock_pool, mock_pool] # Two pools

with patch('bots.commander.is_address', return_value=False), \
patch('bots.commander.LiquidityPool.search', return_value=pools), \
patch('bots.commander.PoolsDropdown') as mock_dropdown:

await pool(mock_interaction, "ETH")

mock_interaction.response.send_message.assert_called_once_with(
"Choose a pool:",
view=mock_dropdown.return_value
)
mock_dropdown.assert_called_once()

@pytest.mark.asyncio
async def test_on_select_pool_with_address(self, mock_interaction, mock_pool, mock_pool_epoch):
"""Test on_select_pool callback with pool address"""
with patch('bots.commander.LiquidityPool.by_address', return_value=mock_pool), \
patch('bots.commander.LiquidityPool.tvl', return_value=1000000), \
patch('bots.commander.LiquidityPoolEpoch.fetch_for_pool', return_value=mock_pool_epoch), \
patch('bots.commander.PoolStats') as mock_pool_stats:

mock_embed = MagicMock()
mock_pool_stats.return_value.render = AsyncMock(return_value=mock_embed)

await on_select_pool(mock_interaction, mock_pool.lp)

mock_interaction.response.send_message.assert_called_once_with(embed=mock_embed)


class TestTickerBots:
"""Test cases for all TickerBot implementations"""

@pytest.mark.asyncio
async def test_tvl_bot_on_ready(self):
"""Test TVL bot initialization"""
with patch('bots.tvl.TickerBot.update_presence') as mock_update_presence:
bot = TVLBot(command_prefix="!", protocol_name="Velodrome")
bot.user = MagicMock()
bot.user.id = 12345

await bot.on_ready()

mock_update_presence.assert_called_once_with("TVL Velodrome")

@pytest.mark.asyncio
async def test_tvl_bot_ticker_success(self):
"""Test TVL bot ticker update success"""
mock_pools = [MagicMock()]
mock_tokens = [MagicMock()]

with patch('bots.tvl.LiquidityPool.get_pools', return_value=mock_pools), \
patch('bots.tvl.LiquidityPool.tvl', return_value=5000000), \
patch('bots.tvl.Token.get_all_listed_tokens', return_value=mock_tokens), \
patch('bots.tvl.TickerBot.update_nick_for_all_servers') as mock_update_nick, \
patch('bots.tvl.TickerBot.update_presence') as mock_update_presence:

bot = TVLBot(command_prefix="!", protocol_name="Velodrome")

await bot.ticker()

mock_update_nick.assert_called_once_with("TVL ~$5.00M")
mock_update_presence.assert_called_once_with("Based on 1 listed tokens")

@pytest.mark.asyncio
async def test_tvl_bot_ticker_error_handling(self):
"""Test TVL bot ticker error handling"""
with patch('bots.tvl.LiquidityPool.get_pools', side_effect=Exception("API Error")), \
patch('bots.tvl.LOGGER') as mock_logger:

bot = TVLBot(command_prefix="!", protocol_name="Velodrome")

await bot.ticker()

mock_logger.error.assert_called_once()

@pytest.mark.asyncio
async def test_fees_bot_on_ready(self):
"""Test Fees bot initialization"""
with patch('bots.fees.TickerBot.update_presence') as mock_update_presence:
bot = FeesBot(command_prefix="!", protocol_name="Velodrome")
bot.user = MagicMock()
bot.user.id = 12345

await bot.on_ready()

mock_update_presence.assert_called_once_with("Watching fees for Velodrome")

@pytest.mark.asyncio
async def test_fees_bot_ticker_success(self):
"""Test Fees bot ticker update success"""
mock_pools = [
MagicMock(total_fees=1000),
MagicMock(total_fees=2000),
MagicMock(total_fees=1500)
]

with patch('bots.fees.LiquidityPool.get_pools', return_value=mock_pools), \
patch('bots.fees.TickerBot.update_nick_for_all_servers') as mock_update_nick, \
patch('bots.fees.TickerBot.update_presence') as mock_update_presence:

bot = FeesBot(command_prefix="!", protocol_name="Velodrome")

await bot.ticker()

mock_update_nick.assert_called_once_with("Fees ~$4.5K")
mock_update_presence.assert_called_once_with("Based on 3 pools this epoch")

@pytest.mark.asyncio
async def test_fees_bot_ticker_empty_pools(self):
"""Test Fees bot with no pools"""
with patch('bots.fees.LiquidityPool.get_pools', return_value=[]), \
patch('bots.fees.TickerBot.update_nick_for_all_servers') as mock_update_nick, \
patch('bots.fees.TickerBot.update_presence') as mock_update_presence:

bot = FeesBot(command_prefix="!", protocol_name="Velodrome")

await bot.ticker()

mock_update_nick.assert_called_once_with("Fees ~$0")
mock_update_presence.assert_called_once_with("Based on 0 pools this epoch")

@pytest.mark.asyncio
async def test_price_bot_initialization(self):
"""Test Price bot basic functionality"""
with patch('bots.price.TickerBot.update_presence') as mock_update_presence:
bot = PriceBot(command_prefix="!", protocol_name="Velodrome", token_symbol="VELO")
bot.user = MagicMock()
bot.user.id = 12345

await bot.on_ready()

mock_update_presence.assert_called_once_with("VELO price on Velodrome")

@pytest.mark.asyncio
async def test_rewards_bot_initialization(self):
"""Test Rewards bot basic functionality"""
with patch('bots.rewards.TickerBot.update_presence') as mock_update_presence:
bot = RewardsBot(command_prefix="!", protocol_name="Velodrome")
bot.user = MagicMock()
bot.user.id = 12345

await bot.on_ready()

mock_update_presence.assert_called_once_with("Rewards for Velodrome")


class TestHelperFunctions:
"""Test cases for helper functions"""

@pytest.mark.parametrize("address,expected", [
("0x1234567890123456789012345678901234567890", True),
("0x123", False),
("not_an_address", False),
("", False),
])
def test_is_address(self, address, expected):
"""Test address validation"""
assert is_address(address) == expected


class TestBotIntegration:
"""Integration tests for bot functionality"""

@pytest.mark.asyncio
async def test_commander_bot_singleton(self):
"""Test that CommanderBot returns singleton instance"""
bot1 = CommanderBot()
bot2 = CommanderBot()

assert bot1 is bot2
assert isinstance(bot1, commands.Bot)

@pytest.mark.asyncio
async def test_bot_initialization(self):
"""Test bot initialization with proper intents"""
bot = CommanderBot()

assert bot.command_prefix == "/"
assert bot.intents.message_content is True
assert bot.intents.guilds is True # Default intent


class TestErrorHandling:
"""Test error handling scenarios"""

@pytest.mark.asyncio
async def test_pool_command_network_error(self, mock_interaction):
"""Test pool command handling of network/API errors"""
with patch('bots.commander.LiquidityPool.by_address', side_effect=Exception("Network error")):
await pool(mock_interaction, "0x1234567890123456789012345678901234567890")

# Should handle error gracefully
mock_interaction.response.send_message.assert_called_once()

@pytest.mark.asyncio
async def test_ticker_network_error(self):
"""Test ticker error handling for network issues"""
with patch('bots.tvl.LiquidityPool.get_pools', side_effect=Exception("API timeout")), \
patch('bots.tvl.LOGGER') as mock_logger:

bot = TVLBot(command_prefix="!", protocol_name="Velodrome")

await bot.ticker()

mock_logger.error.assert_called_once_with("Ticker failed with API timeout")


class TestDataValidation:
"""Test data validation and edge cases"""

@pytest.mark.asyncio
async def test_empty_search_results(self, mock_interaction):
"""Test handling of empty search results"""
with patch('bots.commander.is_address', return_value=False), \
patch('bots.commander.LiquidityPool.search', return_value=[]):

await pool(mock_interaction, "nonexistent")

mock_interaction.response.send_message.assert_called_once_with(
"Choose a pool:",
view=mock_interaction.client.PoolsDropdown.return_value
)

@pytest.mark.asyncio
async def test_pool_stats_rendering(self, mock_interaction, mock_pool, mock_pool_epoch):
"""Test pool stats rendering with various data"""
with patch('bots.commander.LiquidityPool.by_address', return_value=mock_pool), \
patch('bots.commander.LiquidityPool.tvl', return_value=5000000), \
patch('bots.commander.LiquidityPoolEpoch.fetch_for_pool', return_value=mock_pool_epoch), \
patch('bots.commander.PoolStats') as mock_pool_stats:

mock_embed = MagicMock()
mock_pool_stats.return_value.render = AsyncMock(return_value=mock_embed)

await pool(mock_interaction, mock_pool.lp)

# Verify PoolStats was called with correct parameters
mock_pool_stats.assert_called_once()
render_call = mock_pool_stats.return_value.render.call_args
assert render_call[0][0] == mock_pool # pool
assert render_call[0][1] == 5000000 # tvl
assert render_call[0][2] == mock_pool_epoch # pool_epoch


class TestConcurrency:
"""Test concurrent operations and race conditions"""

@pytest.mark.asyncio
async def test_multiple_pool_commands(self, mock_interaction, mock_pool):
"""Test handling multiple simultaneous pool commands"""
interactions = [mock_interaction, mock_interaction, mock_interaction]

with patch('bots.commander.LiquidityPool.by_address', return_value=mock_pool), \
patch('bots.commander.LiquidityPool.tvl', return_value=1000000), \
patch('bots.commander.LiquidityPoolEpoch.fetch_for_pool', return_value=MagicMock()), \
patch('bots.commander.PoolStats') as mock_pool_stats:

mock_embed = MagicMock()
mock_pool_stats.return_value.render = AsyncMock(return_value=mock_embed)

# Execute multiple commands concurrently
await asyncio.gather(*[
pool(interaction, mock_pool.lp) for interaction in interactions
])

# Each interaction should have been handled
assert mock_interaction.response.send_message.call_count == 3


# Import here to avoid circular imports in test file
import asyncio