Standardize data file locations to data/ directory and improve command organization with better UI for team rosters, pagination for team lists, and refactored chart commands into logical command groups. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
541 lines
20 KiB
Python
541 lines
20 KiB
Python
"""
|
|
Tests for chart display and management commands.
|
|
"""
|
|
import pytest
|
|
import json
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
from discord import app_commands
|
|
|
|
from commands.utilities.charts import (
|
|
ChartCommands, ChartManageGroup, ChartCategoryGroup,
|
|
chart_autocomplete, category_autocomplete
|
|
)
|
|
from services.chart_service import ChartService, Chart, get_chart_service
|
|
from exceptions import BotException
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_charts_file():
|
|
"""Create a temporary charts.json file for testing."""
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
|
charts_data = {
|
|
"charts": {
|
|
"rest": {
|
|
"name": "Pitcher Rest",
|
|
"category": "gameplay",
|
|
"description": "Pitcher rest and endurance rules",
|
|
"urls": ["https://example.com/rest.png"]
|
|
},
|
|
"defense": {
|
|
"name": "Defense Chart",
|
|
"category": "defense",
|
|
"description": "General defensive play chart",
|
|
"urls": ["https://example.com/defense.png"]
|
|
},
|
|
"multi-image": {
|
|
"name": "Multi Image Chart",
|
|
"category": "gameplay",
|
|
"description": "Chart with multiple images",
|
|
"urls": [
|
|
"https://example.com/image1.png",
|
|
"https://example.com/image2.png",
|
|
"https://example.com/image3.png"
|
|
]
|
|
}
|
|
},
|
|
"categories": {
|
|
"gameplay": "Gameplay Mechanics",
|
|
"defense": "Defensive Play",
|
|
"reference": "Reference Charts"
|
|
}
|
|
}
|
|
json.dump(charts_data, f)
|
|
temp_path = Path(f.name)
|
|
|
|
yield temp_path
|
|
|
|
# Cleanup
|
|
if temp_path.exists():
|
|
temp_path.unlink()
|
|
|
|
|
|
@pytest.fixture
|
|
def chart_service(temp_charts_file):
|
|
"""Create a chart service with test data."""
|
|
with patch.object(ChartService, 'CHARTS_FILE', temp_charts_file):
|
|
service = ChartService()
|
|
return service
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_interaction():
|
|
"""Create a mock Discord interaction."""
|
|
interaction = AsyncMock()
|
|
interaction.response = AsyncMock()
|
|
interaction.followup = AsyncMock()
|
|
interaction.user = MagicMock()
|
|
interaction.user.id = 123456789
|
|
interaction.user.display_name = "TestUser"
|
|
interaction.guild = MagicMock()
|
|
interaction.guild.id = 987654321
|
|
interaction.channel = MagicMock()
|
|
interaction.channel.name = "test-channel"
|
|
return interaction
|
|
|
|
|
|
class TestChartService:
|
|
"""Tests for ChartService class."""
|
|
|
|
def test_load_charts(self, chart_service):
|
|
"""Test loading charts from file."""
|
|
assert len(chart_service._charts) == 3
|
|
assert 'rest' in chart_service._charts
|
|
assert 'defense' in chart_service._charts
|
|
assert 'multi-image' in chart_service._charts
|
|
|
|
def test_get_chart(self, chart_service):
|
|
"""Test getting a chart by key."""
|
|
chart = chart_service.get_chart('rest')
|
|
assert chart is not None
|
|
assert chart.key == 'rest'
|
|
assert chart.name == 'Pitcher Rest'
|
|
assert chart.category == 'gameplay'
|
|
assert len(chart.urls) == 1
|
|
|
|
def test_get_chart_not_found(self, chart_service):
|
|
"""Test getting a non-existent chart."""
|
|
chart = chart_service.get_chart('nonexistent')
|
|
assert chart is None
|
|
|
|
def test_get_all_charts(self, chart_service):
|
|
"""Test getting all charts."""
|
|
charts = chart_service.get_all_charts()
|
|
assert len(charts) == 3
|
|
assert all(isinstance(c, Chart) for c in charts)
|
|
|
|
def test_get_charts_by_category(self, chart_service):
|
|
"""Test getting charts by category."""
|
|
gameplay_charts = chart_service.get_charts_by_category('gameplay')
|
|
assert len(gameplay_charts) == 2
|
|
assert all(c.category == 'gameplay' for c in gameplay_charts)
|
|
|
|
defense_charts = chart_service.get_charts_by_category('defense')
|
|
assert len(defense_charts) == 1
|
|
assert defense_charts[0].key == 'defense'
|
|
|
|
def test_get_chart_keys(self, chart_service):
|
|
"""Test getting chart keys for autocomplete."""
|
|
keys = chart_service.get_chart_keys()
|
|
assert keys == ['defense', 'multi-image', 'rest'] # Sorted
|
|
|
|
def test_get_categories(self, chart_service):
|
|
"""Test getting categories."""
|
|
categories = chart_service.get_categories()
|
|
assert 'gameplay' in categories
|
|
assert 'defense' in categories
|
|
assert categories['gameplay'] == 'Gameplay Mechanics'
|
|
|
|
def test_add_chart(self, chart_service):
|
|
"""Test adding a new chart."""
|
|
chart_service.add_chart(
|
|
key='new-chart',
|
|
name='New Chart',
|
|
category='reference',
|
|
urls=['https://example.com/new.png'],
|
|
description='A new chart'
|
|
)
|
|
|
|
chart = chart_service.get_chart('new-chart')
|
|
assert chart is not None
|
|
assert chart.name == 'New Chart'
|
|
assert chart.category == 'reference'
|
|
|
|
def test_add_duplicate_chart(self, chart_service):
|
|
"""Test adding a duplicate chart raises exception."""
|
|
with pytest.raises(BotException, match="already exists"):
|
|
chart_service.add_chart(
|
|
key='rest', # Already exists
|
|
name='Duplicate',
|
|
category='gameplay',
|
|
urls=['https://example.com/dup.png']
|
|
)
|
|
|
|
def test_update_chart(self, chart_service):
|
|
"""Test updating an existing chart."""
|
|
chart_service.update_chart(
|
|
key='rest',
|
|
name='Updated Rest Chart',
|
|
description='Updated description'
|
|
)
|
|
|
|
chart = chart_service.get_chart('rest')
|
|
assert chart.name == 'Updated Rest Chart'
|
|
assert chart.description == 'Updated description'
|
|
assert chart.category == 'gameplay' # Unchanged
|
|
|
|
def test_update_nonexistent_chart(self, chart_service):
|
|
"""Test updating a non-existent chart raises exception."""
|
|
with pytest.raises(BotException, match="not found"):
|
|
chart_service.update_chart(
|
|
key='nonexistent',
|
|
name='New Name'
|
|
)
|
|
|
|
def test_remove_chart(self, chart_service):
|
|
"""Test removing a chart."""
|
|
chart_service.remove_chart('rest')
|
|
assert chart_service.get_chart('rest') is None
|
|
assert len(chart_service._charts) == 2
|
|
|
|
def test_remove_nonexistent_chart(self, chart_service):
|
|
"""Test removing a non-existent chart raises exception."""
|
|
with pytest.raises(BotException, match="not found"):
|
|
chart_service.remove_chart('nonexistent')
|
|
|
|
def test_add_category(self, chart_service):
|
|
"""Test adding a new category."""
|
|
chart_service.add_category(key='stats', display_name='Statistics Charts')
|
|
|
|
categories = chart_service.get_categories()
|
|
assert 'stats' in categories
|
|
assert categories['stats'] == 'Statistics Charts'
|
|
|
|
def test_add_duplicate_category(self, chart_service):
|
|
"""Test adding a duplicate category raises exception."""
|
|
with pytest.raises(BotException, match="already exists"):
|
|
chart_service.add_category(key='gameplay', display_name='Duplicate')
|
|
|
|
def test_remove_category(self, chart_service):
|
|
"""Test removing an unused category."""
|
|
chart_service.remove_category('reference')
|
|
|
|
categories = chart_service.get_categories()
|
|
assert 'reference' not in categories
|
|
|
|
def test_remove_nonexistent_category(self, chart_service):
|
|
"""Test removing a non-existent category raises exception."""
|
|
with pytest.raises(BotException, match="not found"):
|
|
chart_service.remove_category('nonexistent')
|
|
|
|
def test_remove_category_with_charts(self, chart_service):
|
|
"""Test removing a category that charts are using raises exception."""
|
|
with pytest.raises(BotException, match="Cannot remove category"):
|
|
chart_service.remove_category('gameplay')
|
|
|
|
def test_update_category(self, chart_service):
|
|
"""Test updating a category display name."""
|
|
chart_service.update_category(key='gameplay', display_name='Updated Gameplay')
|
|
|
|
categories = chart_service.get_categories()
|
|
assert categories['gameplay'] == 'Updated Gameplay'
|
|
|
|
def test_update_nonexistent_category(self, chart_service):
|
|
"""Test updating a non-existent category raises exception."""
|
|
with pytest.raises(BotException, match="not found"):
|
|
chart_service.update_category(key='nonexistent', display_name='New Name')
|
|
|
|
|
|
class TestChartCommands:
|
|
"""Tests for ChartCommands class."""
|
|
|
|
@pytest.fixture
|
|
def chart_cog(self, chart_service):
|
|
"""Create ChartCommands cog with mocked service."""
|
|
bot = AsyncMock()
|
|
cog = ChartCommands(bot)
|
|
|
|
with patch.object(cog, 'chart_service', chart_service):
|
|
yield cog
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_charts_command_single_image(self, chart_cog, mock_interaction):
|
|
"""Test displaying a chart with a single image."""
|
|
await chart_cog.charts.callback(chart_cog, mock_interaction, 'rest')
|
|
|
|
# Verify response was sent with embed
|
|
mock_interaction.response.send_message.assert_called_once()
|
|
call_kwargs = mock_interaction.response.send_message.call_args[1]
|
|
embed = call_kwargs['embed']
|
|
|
|
assert '📊 Pitcher Rest' in embed.title
|
|
assert embed.description == 'Pitcher rest and endurance rules'
|
|
assert embed.image.url == 'https://example.com/rest.png'
|
|
|
|
# Verify no followups for single image
|
|
mock_interaction.followup.send.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_charts_command_multiple_images(self, chart_cog, mock_interaction):
|
|
"""Test displaying a chart with multiple images."""
|
|
await chart_cog.charts.callback(chart_cog, mock_interaction, 'multi-image')
|
|
|
|
# Verify initial response
|
|
mock_interaction.response.send_message.assert_called_once()
|
|
call_kwargs = mock_interaction.response.send_message.call_args[1]
|
|
embed = call_kwargs['embed']
|
|
|
|
assert '📊 Multi Image Chart' in embed.title
|
|
assert embed.image.url == 'https://example.com/image1.png'
|
|
|
|
# Verify followup messages for additional images
|
|
assert mock_interaction.followup.send.call_count == 2
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_charts_command_not_found(self, chart_cog, mock_interaction):
|
|
"""Test displaying a non-existent chart."""
|
|
with pytest.raises(BotException, match="not found"):
|
|
await chart_cog.charts.callback(chart_cog, mock_interaction, 'nonexistent')
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_chart_autocomplete(self, chart_service, mock_interaction):
|
|
"""Test chart autocomplete functionality."""
|
|
# Patch get_chart_service to return our test service
|
|
with patch('commands.utilities.charts.get_chart_service', return_value=chart_service):
|
|
# Test with empty current input
|
|
choices = await chart_autocomplete(mock_interaction, '')
|
|
assert len(choices) == 3
|
|
|
|
# Test with partial match
|
|
choices = await chart_autocomplete(mock_interaction, 'def')
|
|
assert len(choices) == 1
|
|
assert choices[0].value == 'defense'
|
|
|
|
# Test with no match
|
|
choices = await chart_autocomplete(mock_interaction, 'xyz')
|
|
assert len(choices) == 0
|
|
|
|
|
|
class TestChartManageGroup:
|
|
"""Tests for ChartManageGroup command group."""
|
|
|
|
@pytest.fixture
|
|
def manage_group(self, chart_service):
|
|
"""Create ChartManageGroup with mocked service."""
|
|
group = ChartManageGroup()
|
|
|
|
with patch.object(group, 'chart_service', chart_service):
|
|
yield group
|
|
|
|
@pytest.fixture
|
|
def mock_admin_interaction(self, mock_interaction):
|
|
"""Create a mock interaction with admin permissions."""
|
|
mock_interaction.user.guild_permissions.administrator = True
|
|
return mock_interaction
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_chart_add_command(self, manage_group, mock_admin_interaction):
|
|
"""Test adding a new chart via command."""
|
|
await manage_group.add.callback(
|
|
manage_group,
|
|
mock_admin_interaction,
|
|
key='new-chart',
|
|
name='New Chart',
|
|
category='gameplay',
|
|
url='https://example.com/new.png',
|
|
description='Test chart'
|
|
)
|
|
|
|
# Verify success response
|
|
mock_admin_interaction.response.send_message.assert_called_once()
|
|
call_kwargs = mock_admin_interaction.response.send_message.call_args[1]
|
|
embed = call_kwargs['embed']
|
|
|
|
assert '✅ Chart Added' in embed.title
|
|
assert call_kwargs['ephemeral'] is True
|
|
|
|
# Verify chart was added
|
|
chart = manage_group.chart_service.get_chart('new-chart')
|
|
assert chart is not None
|
|
assert chart.name == 'New Chart'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_chart_add_invalid_category(self, manage_group, mock_admin_interaction):
|
|
"""Test adding a chart with invalid category."""
|
|
with pytest.raises(BotException, match="Invalid category"):
|
|
await manage_group.add.callback(
|
|
manage_group,
|
|
mock_admin_interaction,
|
|
key='new-chart',
|
|
name='New Chart',
|
|
category='invalid-category',
|
|
url='https://example.com/new.png',
|
|
description=None
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_chart_remove_command(self, manage_group, mock_admin_interaction):
|
|
"""Test removing a chart via command."""
|
|
await manage_group.remove.callback(manage_group, mock_admin_interaction, 'rest')
|
|
|
|
# Verify success response
|
|
mock_admin_interaction.response.send_message.assert_called_once()
|
|
call_kwargs = mock_admin_interaction.response.send_message.call_args[1]
|
|
embed = call_kwargs['embed']
|
|
|
|
assert '✅ Chart Removed' in embed.title
|
|
assert call_kwargs['ephemeral'] is True
|
|
|
|
# Verify chart was removed
|
|
chart = manage_group.chart_service.get_chart('rest')
|
|
assert chart is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_chart_remove_not_found(self, manage_group, mock_admin_interaction):
|
|
"""Test removing a non-existent chart."""
|
|
with pytest.raises(BotException, match="not found"):
|
|
await manage_group.remove.callback(manage_group, mock_admin_interaction, 'nonexistent')
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_chart_update_command(self, manage_group, mock_admin_interaction):
|
|
"""Test updating a chart via command."""
|
|
await manage_group.update.callback(
|
|
manage_group,
|
|
mock_admin_interaction,
|
|
key='rest',
|
|
name='Updated Rest Chart',
|
|
category=None,
|
|
url=None,
|
|
description='Updated description'
|
|
)
|
|
|
|
# Verify success response
|
|
mock_admin_interaction.response.send_message.assert_called_once()
|
|
call_kwargs = mock_admin_interaction.response.send_message.call_args[1]
|
|
embed = call_kwargs['embed']
|
|
|
|
assert '✅ Chart Updated' in embed.title
|
|
|
|
# Verify chart was updated
|
|
chart = manage_group.chart_service.get_chart('rest')
|
|
assert chart.name == 'Updated Rest Chart'
|
|
assert chart.description == 'Updated description'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_chart_update_no_fields(self, manage_group, mock_admin_interaction):
|
|
"""Test updating with no fields raises exception."""
|
|
with pytest.raises(BotException, match="Must provide at least one field"):
|
|
await manage_group.update.callback(
|
|
manage_group,
|
|
mock_admin_interaction,
|
|
key='rest',
|
|
name=None,
|
|
category=None,
|
|
url=None,
|
|
description=None
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_chart_update_invalid_category(self, manage_group, mock_admin_interaction):
|
|
"""Test updating with invalid category."""
|
|
with pytest.raises(BotException, match="Invalid category"):
|
|
await manage_group.update.callback(
|
|
manage_group,
|
|
mock_admin_interaction,
|
|
key='rest',
|
|
name=None,
|
|
category='invalid-category',
|
|
url=None,
|
|
description=None
|
|
)
|
|
|
|
|
|
class TestChartCategoryGroup:
|
|
"""Tests for ChartCategoryGroup command group."""
|
|
|
|
@pytest.fixture
|
|
def category_group(self, chart_service):
|
|
"""Create ChartCategoryGroup with mocked service."""
|
|
group = ChartCategoryGroup()
|
|
|
|
with patch.object(group, 'chart_service', chart_service):
|
|
yield group
|
|
|
|
@pytest.fixture
|
|
def mock_admin_interaction(self, mock_interaction):
|
|
"""Create a mock interaction with admin permissions."""
|
|
mock_interaction.user.guild_permissions.administrator = True
|
|
return mock_interaction
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_categories(self, category_group, mock_admin_interaction):
|
|
"""Test listing all categories."""
|
|
await category_group.list_categories.callback(
|
|
category_group,
|
|
mock_admin_interaction
|
|
)
|
|
|
|
# Verify response
|
|
mock_admin_interaction.response.send_message.assert_called_once()
|
|
call_kwargs = mock_admin_interaction.response.send_message.call_args[1]
|
|
embed = call_kwargs['embed']
|
|
|
|
assert '📊 Chart Categories' in embed.title
|
|
assert call_kwargs['ephemeral'] is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_category(self, category_group, mock_admin_interaction):
|
|
"""Test adding a new category."""
|
|
await category_group.add_category.callback(
|
|
category_group,
|
|
mock_admin_interaction,
|
|
key='stats',
|
|
display_name='Statistics Charts'
|
|
)
|
|
|
|
# Verify success response
|
|
mock_admin_interaction.response.send_message.assert_called_once()
|
|
call_kwargs = mock_admin_interaction.response.send_message.call_args[1]
|
|
embed = call_kwargs['embed']
|
|
|
|
assert '✅ Category Added' in embed.title
|
|
assert call_kwargs['ephemeral'] is True
|
|
|
|
# Verify category was added
|
|
categories = category_group.chart_service.get_categories()
|
|
assert 'stats' in categories
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_remove_category(self, category_group, mock_admin_interaction):
|
|
"""Test removing a category."""
|
|
await category_group.remove_category.callback(
|
|
category_group,
|
|
mock_admin_interaction,
|
|
key='reference'
|
|
)
|
|
|
|
# Verify success response
|
|
mock_admin_interaction.response.send_message.assert_called_once()
|
|
call_kwargs = mock_admin_interaction.response.send_message.call_args[1]
|
|
embed = call_kwargs['embed']
|
|
|
|
assert '✅ Category Removed' in embed.title
|
|
assert call_kwargs['ephemeral'] is True
|
|
|
|
# Verify category was removed
|
|
categories = category_group.chart_service.get_categories()
|
|
assert 'reference' not in categories
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rename_category(self, category_group, mock_admin_interaction):
|
|
"""Test renaming a category."""
|
|
await category_group.rename_category.callback(
|
|
category_group,
|
|
mock_admin_interaction,
|
|
key='gameplay',
|
|
new_display_name='Updated Gameplay'
|
|
)
|
|
|
|
# Verify success response
|
|
mock_admin_interaction.response.send_message.assert_called_once()
|
|
call_kwargs = mock_admin_interaction.response.send_message.call_args[1]
|
|
embed = call_kwargs['embed']
|
|
|
|
assert '✅ Category Renamed' in embed.title
|
|
assert call_kwargs['ephemeral'] is True
|
|
|
|
# Verify category was renamed
|
|
categories = category_group.chart_service.get_categories()
|
|
assert categories['gameplay'] == 'Updated Gameplay'
|