Implements comprehensive dice rolling system for gameplay: ## New Features - `/roll` and `!roll` commands for XdY dice notation with multiple roll support - `/ab` and `!atbat` commands for baseball at-bat dice shortcuts (1d6;2d6;1d20) - `/fielding` and `!f` commands for Super Advanced fielding with full position charts ## Technical Implementation - Complete dice command package in commands/dice/ - Full range and error charts for all 8 defensive positions (1B,2B,3B,SS,LF,RF,CF,C) - Pre-populated position choices for user-friendly slash command interface - Backwards compatibility with prefix commands (!roll, !r, !dice, !ab, !atbat, !f, !fielding, !saf) - Type-safe implementation following "Raise or Return" pattern ## Testing & Quality - 30 comprehensive tests with 100% pass rate - Complete test coverage for all dice functionality, parsing, validation, and error handling - Integration with bot.py command loading system - Maintainable data structures replacing verbose original implementation ## User Experience - Consistent embed formatting across all commands - Detailed fielding results with range and error analysis - Support for complex dice combinations and multiple roll formats - Clear error messages for invalid inputs 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
393 lines
17 KiB
Python
393 lines
17 KiB
Python
"""
|
|
Tests for /dropadd Discord Commands
|
|
|
|
Validates the Discord command interface, autocomplete, and user interactions.
|
|
"""
|
|
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
import discord
|
|
from discord import app_commands
|
|
|
|
from commands.transactions.dropadd import DropAddCommands
|
|
from services.transaction_builder import TransactionBuilder
|
|
from models.team import RosterType
|
|
from models.team import Team
|
|
from models.player import Player
|
|
|
|
|
|
class TestDropAddCommands:
|
|
"""Test DropAddCommands Discord command functionality."""
|
|
|
|
@pytest.fixture
|
|
def mock_bot(self):
|
|
"""Create mock Discord bot."""
|
|
bot = MagicMock()
|
|
bot.user = MagicMock()
|
|
bot.user.id = 123456789
|
|
return bot
|
|
|
|
@pytest.fixture
|
|
def commands_cog(self, mock_bot):
|
|
"""Create DropAddCommands cog instance."""
|
|
return DropAddCommands(mock_bot)
|
|
|
|
@pytest.fixture
|
|
def mock_interaction(self):
|
|
"""Create mock Discord interaction."""
|
|
interaction = AsyncMock()
|
|
interaction.user = MagicMock()
|
|
interaction.user.id = 258104532423147520
|
|
interaction.response = AsyncMock()
|
|
interaction.followup = AsyncMock()
|
|
interaction.client = MagicMock()
|
|
interaction.client.user = MagicMock()
|
|
interaction.channel = MagicMock()
|
|
return interaction
|
|
|
|
@pytest.fixture
|
|
def mock_team(self):
|
|
"""Create mock team data."""
|
|
return Team(
|
|
id=499,
|
|
abbrev='WV',
|
|
sname='Black Bears',
|
|
lname='West Virginia Black Bears',
|
|
season=12
|
|
)
|
|
|
|
@pytest.fixture
|
|
def mock_player(self):
|
|
"""Create mock player data."""
|
|
return Player(
|
|
id=12472,
|
|
name='Mike Trout',
|
|
season=12,
|
|
primary_position='CF'
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_player_autocomplete_success(self, commands_cog, mock_interaction):
|
|
"""Test successful player autocomplete."""
|
|
mock_players = [
|
|
Player(id=1, name='Mike Trout', season=12, primary_position='CF'),
|
|
Player(id=2, name='Ronald Acuna Jr.', season=12, primary_position='OF')
|
|
]
|
|
|
|
with patch('commands.transactions.dropadd.player_service') as mock_service:
|
|
mock_service.get_players_by_name.return_value = mock_players
|
|
|
|
choices = await commands_cog.player_autocomplete(mock_interaction, 'Trout')
|
|
|
|
assert len(choices) == 2
|
|
assert choices[0].name == 'Mike Trout (CF)'
|
|
assert choices[0].value == 'Mike Trout'
|
|
assert choices[1].name == 'Ronald Acuna Jr. (OF)'
|
|
assert choices[1].value == 'Ronald Acuna Jr.'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_player_autocomplete_with_team(self, commands_cog, mock_interaction):
|
|
"""Test player autocomplete with team information."""
|
|
mock_team = Team(id=499, abbrev='LAA', sname='Angels', lname='Los Angeles Angels', season=12)
|
|
mock_player = Player(
|
|
id=1,
|
|
name='Mike Trout',
|
|
season=12,
|
|
primary_position='CF'
|
|
)
|
|
mock_player.team = mock_team # Add team info
|
|
|
|
with patch('commands.transactions.dropadd.player_service') as mock_service:
|
|
mock_service.get_players_by_name.return_value = [mock_player]
|
|
|
|
choices = await commands_cog.player_autocomplete(mock_interaction, 'Trout')
|
|
|
|
assert len(choices) == 1
|
|
assert choices[0].name == 'Mike Trout (CF - LAA)'
|
|
assert choices[0].value == 'Mike Trout'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_player_autocomplete_short_input(self, commands_cog, mock_interaction):
|
|
"""Test player autocomplete with short input returns empty."""
|
|
choices = await commands_cog.player_autocomplete(mock_interaction, 'T')
|
|
assert len(choices) == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_player_autocomplete_error_handling(self, commands_cog, mock_interaction):
|
|
"""Test player autocomplete error handling."""
|
|
with patch('commands.transactions.dropadd.player_service') as mock_service:
|
|
mock_service.get_players_by_name.side_effect = Exception("API Error")
|
|
|
|
choices = await commands_cog.player_autocomplete(mock_interaction, 'Trout')
|
|
assert len(choices) == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dropadd_command_no_team(self, commands_cog, mock_interaction):
|
|
"""Test /dropadd command when user has no team."""
|
|
with patch('commands.transactions.dropadd.team_service') as mock_service:
|
|
mock_service.get_teams_by_owner.return_value = []
|
|
|
|
await commands_cog.dropadd(mock_interaction)
|
|
|
|
mock_interaction.response.defer.assert_called_once()
|
|
mock_interaction.followup.send.assert_called_once()
|
|
|
|
# Check error message
|
|
call_args = mock_interaction.followup.send.call_args
|
|
assert "don't appear to own a team" in call_args[0][0]
|
|
assert call_args[1]['ephemeral'] is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dropadd_command_success_no_params(self, commands_cog, mock_interaction, mock_team):
|
|
"""Test /dropadd command success without parameters."""
|
|
with patch('commands.transactions.dropadd.team_service') as mock_team_service:
|
|
with patch('commands.transactions.dropadd.get_transaction_builder') as mock_get_builder:
|
|
with patch('commands.transactions.dropadd.create_transaction_embed') as mock_create_embed:
|
|
mock_team_service.get_teams_by_owner.return_value = [mock_team]
|
|
|
|
mock_builder = MagicMock()
|
|
mock_builder.team = mock_team
|
|
mock_get_builder.return_value = mock_builder
|
|
|
|
mock_embed = MagicMock()
|
|
mock_create_embed.return_value = mock_embed
|
|
|
|
await commands_cog.dropadd(mock_interaction)
|
|
|
|
# Verify flow
|
|
mock_interaction.response.defer.assert_called_once()
|
|
mock_team_service.get_teams_by_owner.assert_called_once_with(
|
|
mock_interaction.user.id, 12
|
|
)
|
|
mock_get_builder.assert_called_once_with(mock_interaction.user.id, mock_team)
|
|
mock_create_embed.assert_called_once_with(mock_builder)
|
|
mock_interaction.followup.send.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dropadd_command_with_quick_move(self, commands_cog, mock_interaction, mock_team):
|
|
"""Test /dropadd command with quick move parameters."""
|
|
with patch('commands.transactions.dropadd.team_service') as mock_team_service:
|
|
with patch('commands.transactions.dropadd.get_transaction_builder') as mock_get_builder:
|
|
with patch.object(commands_cog, '_add_quick_move') as mock_add_quick:
|
|
with patch('commands.transactions.dropadd.create_transaction_embed') as mock_create_embed:
|
|
mock_team_service.get_teams_by_owner.return_value = [mock_team]
|
|
|
|
mock_builder = MagicMock()
|
|
mock_get_builder.return_value = mock_builder
|
|
mock_add_quick.return_value = True
|
|
mock_create_embed.return_value = MagicMock()
|
|
|
|
await commands_cog.dropadd(
|
|
mock_interaction,
|
|
player='Mike Trout',
|
|
action='add',
|
|
destination='ml'
|
|
)
|
|
|
|
# Verify quick move was attempted
|
|
mock_add_quick.assert_called_once_with(
|
|
mock_builder, 'Mike Trout', 'add', 'ml'
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_quick_move_success(self, commands_cog, mock_team, mock_player):
|
|
"""Test successful quick move addition."""
|
|
mock_builder = MagicMock()
|
|
mock_builder.team = mock_team
|
|
mock_builder.add_move.return_value = True
|
|
|
|
with patch('commands.transactions.dropadd.player_service') as mock_service:
|
|
mock_service.get_players_by_name.return_value = [mock_player]
|
|
|
|
success = await commands_cog._add_quick_move(
|
|
mock_builder, 'Mike Trout', 'add', 'ml'
|
|
)
|
|
|
|
assert success is True
|
|
mock_service.get_players_by_name.assert_called_once_with('Mike Trout', 12)
|
|
mock_builder.add_move.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_quick_move_player_not_found(self, commands_cog, mock_team):
|
|
"""Test quick move when player not found."""
|
|
mock_builder = MagicMock()
|
|
mock_builder.team = mock_team
|
|
|
|
with patch('commands.transactions.dropadd.player_service') as mock_service:
|
|
mock_service.get_players_by_name.return_value = []
|
|
|
|
success = await commands_cog._add_quick_move(
|
|
mock_builder, 'Nonexistent Player', 'add', 'ml'
|
|
)
|
|
|
|
assert success is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_quick_move_invalid_action(self, commands_cog, mock_team):
|
|
"""Test quick move with invalid action."""
|
|
mock_builder = MagicMock()
|
|
mock_builder.team = mock_team
|
|
|
|
success = await commands_cog._add_quick_move(
|
|
mock_builder, 'Mike Trout', 'invalid_action', 'ml'
|
|
)
|
|
|
|
assert success is False
|
|
|
|
# TODO: These tests are for obsolete MoveAction-based functionality
|
|
# The transaction system now uses from_roster/to_roster directly
|
|
# def test_determine_roster_types_add(self, commands_cog):
|
|
# def test_determine_roster_types_drop(self, commands_cog):
|
|
# def test_determine_roster_types_recall(self, commands_cog):
|
|
# def test_determine_roster_types_demote(self, commands_cog):
|
|
pass # Placeholder
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clear_transaction_command(self, commands_cog, mock_interaction):
|
|
"""Test /cleartransaction command."""
|
|
with patch('commands.transactions.dropadd.clear_transaction_builder') as mock_clear:
|
|
await commands_cog.clear_transaction(mock_interaction)
|
|
|
|
mock_clear.assert_called_once_with(mock_interaction.user.id)
|
|
mock_interaction.response.send_message.assert_called_once()
|
|
|
|
# Check success message
|
|
call_args = mock_interaction.response.send_message.call_args
|
|
assert "transaction builder has been cleared" in call_args[0][0]
|
|
assert call_args[1]['ephemeral'] is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transaction_status_no_team(self, commands_cog, mock_interaction):
|
|
"""Test /transactionstatus when user has no team."""
|
|
with patch('commands.transactions.dropadd.team_service') as mock_service:
|
|
mock_service.get_teams_by_owner.return_value = []
|
|
|
|
await commands_cog.transaction_status(mock_interaction)
|
|
|
|
mock_interaction.response.defer.assert_called_once_with(ephemeral=True)
|
|
mock_interaction.followup.send.assert_called_once()
|
|
|
|
call_args = mock_interaction.followup.send.call_args
|
|
assert "don't appear to own a team" in call_args[0][0]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transaction_status_empty_builder(self, commands_cog, mock_interaction, mock_team):
|
|
"""Test /transactionstatus with empty builder."""
|
|
with patch('commands.transactions.dropadd.team_service') as mock_team_service:
|
|
with patch('commands.transactions.dropadd.get_transaction_builder') as mock_get_builder:
|
|
mock_team_service.get_teams_by_owner.return_value = [mock_team]
|
|
|
|
mock_builder = MagicMock()
|
|
mock_builder.is_empty = True
|
|
mock_get_builder.return_value = mock_builder
|
|
|
|
await commands_cog.transaction_status(mock_interaction)
|
|
|
|
call_args = mock_interaction.followup.send.call_args
|
|
assert "transaction builder is empty" in call_args[0][0]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transaction_status_with_moves(self, commands_cog, mock_interaction, mock_team):
|
|
"""Test /transactionstatus with moves in builder."""
|
|
from services.transaction_builder import RosterValidationResult
|
|
|
|
with patch('commands.transactions.dropadd.team_service') as mock_team_service:
|
|
with patch('commands.transactions.dropadd.get_transaction_builder') as mock_get_builder:
|
|
mock_team_service.get_teams_by_owner.return_value = [mock_team]
|
|
|
|
mock_builder = MagicMock()
|
|
mock_builder.is_empty = False
|
|
mock_builder.move_count = 2
|
|
mock_builder.validate_transaction = AsyncMock(return_value=RosterValidationResult(
|
|
is_legal=True,
|
|
major_league_count=25,
|
|
minor_league_count=10,
|
|
warnings=[],
|
|
errors=[],
|
|
suggestions=[]
|
|
))
|
|
mock_get_builder.return_value = mock_builder
|
|
|
|
await commands_cog.transaction_status(mock_interaction)
|
|
|
|
call_args = mock_interaction.followup.send.call_args
|
|
status_msg = call_args[0][0]
|
|
assert "Moves:** 2" in status_msg
|
|
assert "✅ Legal" in status_msg
|
|
|
|
|
|
class TestDropAddCommandsIntegration:
|
|
"""Integration tests for dropadd commands with real-like data flows."""
|
|
|
|
@pytest.fixture
|
|
def mock_bot(self):
|
|
"""Create mock Discord bot."""
|
|
return MagicMock()
|
|
|
|
@pytest.fixture
|
|
def commands_cog(self, mock_bot):
|
|
"""Create DropAddCommands cog instance."""
|
|
return DropAddCommands(mock_bot)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_dropadd_workflow(self, commands_cog):
|
|
"""Test complete dropadd workflow from command to builder creation."""
|
|
mock_interaction = AsyncMock()
|
|
mock_interaction.user.id = 123456789
|
|
|
|
mock_team = Team(
|
|
id=499,
|
|
abbrev='WV',
|
|
sname='Black Bears',
|
|
lname='West Virginia Black Bears',
|
|
season=12
|
|
)
|
|
|
|
mock_player = Player(
|
|
id=12472,
|
|
name='Mike Trout',
|
|
season=12,
|
|
primary_position='CF'
|
|
)
|
|
|
|
with patch('commands.transactions.dropadd.team_service') as mock_team_service:
|
|
with patch('commands.transactions.dropadd.player_service') as mock_player_service:
|
|
with patch('commands.transactions.dropadd.get_transaction_builder') as mock_get_builder:
|
|
with patch('commands.transactions.dropadd.create_transaction_embed') as mock_create_embed:
|
|
# Setup mocks
|
|
mock_team_service.get_teams_by_owner.return_value = [mock_team]
|
|
mock_player_service.get_players_by_name.return_value = [mock_player]
|
|
|
|
mock_builder = TransactionBuilder(mock_team, 123456789, 12)
|
|
mock_get_builder.return_value = mock_builder
|
|
mock_create_embed.return_value = MagicMock()
|
|
|
|
# Execute command with parameters
|
|
await commands_cog.dropadd(
|
|
mock_interaction,
|
|
player='Mike Trout',
|
|
action='add',
|
|
destination='ml'
|
|
)
|
|
|
|
# Verify the builder has the move
|
|
assert mock_builder.move_count == 1
|
|
move = mock_builder.moves[0]
|
|
assert move.player == mock_player
|
|
# Note: TransactionMove no longer has 'action' field - uses from_roster/to_roster instead
|
|
assert move.to_roster == RosterType.MAJOR_LEAGUE
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_error_recovery_in_workflow(self, commands_cog):
|
|
"""Test error recovery in dropadd workflow."""
|
|
mock_interaction = AsyncMock()
|
|
mock_interaction.user.id = 123456789
|
|
|
|
with patch('commands.transactions.dropadd.team_service') as mock_service:
|
|
# Simulate API error
|
|
mock_service.get_teams_by_owner.side_effect = Exception("API Error")
|
|
|
|
# Should not raise exception, should handle gracefully
|
|
await commands_cog.dropadd(mock_interaction)
|
|
|
|
# Should have deferred and attempted to send error (which will also fail gracefully)
|
|
mock_interaction.response.defer.assert_called_once() |