major-domo-v2/tests/test_commands_charts.py
Cal Corum 7aa454f619 CLAUDE: Reorganize data storage and enhance team/roster displays
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>
2025-10-15 19:05:51 -05:00

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'