major-domo-v2/tests/test_commands_dropadd.py
Cal Corum 1dd930e4b3 CLAUDE: Complete dice command system with fielding mechanics
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>
2025-09-24 22:30:31 -05:00

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()