major-domo-v2/tests/test_transactions_integration.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

453 lines
19 KiB
Python

"""
Integration tests for Transaction functionality
Tests the complete flow from API through services to Discord commands.
"""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
import asyncio
from models.transaction import Transaction, RosterValidation
from models.team import Team
from models.roster import TeamRoster
from services.transaction_service import transaction_service
from commands.transactions.management import TransactionCommands
class TestTransactionIntegration:
"""Integration tests for the complete transaction system."""
@pytest.fixture
def realistic_api_data(self):
"""Create realistic API response data based on actual structure."""
return [
{
'id': 27787,
'week': 10,
'player': {
'id': 12472,
'name': 'Yordan Alvarez',
'wara': 2.47,
'image': 'https://sba-cards-2024.s3.us-east-1.amazonaws.com/2024-cards/yordan-alvarez.png',
'image2': None,
'team': {
'id': 508,
'abbrev': 'NYD',
'sname': 'Diamonds',
'lname': 'New York Diamonds',
'manager_legacy': None,
'division_legacy': None,
'gmid': '143034072787058688',
'gmid2': None,
'season': 12
},
'season': 12,
'pitcher_injury': None,
'pos_1': 'LF',
'pos_2': None,
'last_game': None,
'il_return': None,
'demotion_week': 1,
'headshot': None,
'strat_code': 'Alvarez,Y',
'bbref_id': 'alvaryo01',
'injury_rating': '1p65'
},
'oldteam': {
'id': 508,
'abbrev': 'NYD',
'sname': 'Diamonds',
'lname': 'New York Diamonds',
'manager_legacy': None,
'division_legacy': None,
'gmid': '143034072787058688',
'gmid2': None,
'season': 12
},
'newteam': {
'id': 499,
'abbrev': 'WV',
'sname': 'Black Bears',
'lname': 'West Virginia Black Bears',
'manager_legacy': None,
'division_legacy': None,
'gmid': '258104532423147520',
'gmid2': None,
'season': 12
},
'season': 12,
'moveid': 'Season-012-Week-10-19-13:04:41',
'cancelled': False,
'frozen': False
},
{
'id': 27788,
'week': 10,
'player': {
'id': 12473,
'name': 'Ronald Acuna Jr.',
'wara': 3.12,
'season': 12,
'pos_1': 'OF'
},
'oldteam': {
'id': 499,
'abbrev': 'WV',
'sname': 'Black Bears',
'lname': 'West Virginia Black Bears',
'season': 12
},
'newteam': {
'id': 501,
'abbrev': 'ATL',
'sname': 'Braves',
'lname': 'Atlanta Braves',
'season': 12
},
'season': 12,
'moveid': 'Season-012-Week-10-20-14:22:15',
'cancelled': False,
'frozen': True
},
{
'id': 27789,
'week': 9,
'player': {
'id': 12474,
'name': 'Mike Trout',
'wara': 2.89,
'season': 12,
'pos_1': 'CF'
},
'oldteam': {
'id': 502,
'abbrev': 'LAA',
'sname': 'Angels',
'lname': 'Los Angeles Angels',
'season': 12
},
'newteam': {
'id': 503,
'abbrev': 'FA',
'sname': 'Free Agents',
'lname': 'Free Agency',
'season': 12
},
'season': 12,
'moveid': 'Season-012-Week-09-18-11:45:33',
'cancelled': True,
'frozen': False
}
]
@pytest.mark.asyncio
async def test_api_to_model_conversion(self, realistic_api_data):
"""Test that realistic API data converts correctly to Transaction models."""
transactions = [Transaction.from_api_data(data) for data in realistic_api_data]
assert len(transactions) == 3
# Test first transaction (pending)
tx1 = transactions[0]
assert tx1.id == 27787
assert tx1.player.name == 'Yordan Alvarez'
assert tx1.player.wara == 2.47
assert tx1.player.bbref_id == 'alvaryo01'
assert tx1.oldteam.abbrev == 'NYD'
assert tx1.newteam.abbrev == 'WV'
assert tx1.is_pending is True
assert tx1.is_major_league_move is True
# Test second transaction (frozen)
tx2 = transactions[1]
assert tx2.id == 27788
assert tx2.is_frozen is True
assert tx2.is_pending is False
# Test third transaction (cancelled)
tx3 = transactions[2]
assert tx3.id == 27789
assert tx3.is_cancelled is True
assert tx3.newteam.abbrev == 'FA' # Move to free agency
@pytest.mark.asyncio
async def test_service_layer_integration(self, realistic_api_data):
"""Test service layer with realistic data processing."""
service = transaction_service
with patch.object(service, 'get_all_items', new_callable=AsyncMock) as mock_get:
# Mock API returns realistic data
mock_get.return_value = [Transaction.from_api_data(data) for data in realistic_api_data]
# Test team transactions
result = await service.get_team_transactions('WV', 12)
# Should sort by week, then moveid
assert result[0].week == 9 # Week 9 first
assert result[1].week == 10 # Then week 10 transactions
assert result[2].week == 10
# Test filtering
pending = await service.get_pending_transactions('WV', 12)
frozen = await service.get_frozen_transactions('WV', 12)
# Verify filtering works correctly
with patch.object(service, 'get_team_transactions', new_callable=AsyncMock) as mock_team_tx:
mock_team_tx.return_value = [tx for tx in result if tx.is_pending]
pending_filtered = await service.get_pending_transactions('WV', 12)
mock_team_tx.assert_called_with('WV', 12, cancelled=False, frozen=False)
@pytest.mark.asyncio
async def test_command_layer_integration(self, realistic_api_data):
"""Test Discord command layer with realistic transaction data."""
mock_bot = MagicMock()
commands_cog = TransactionCommands(mock_bot)
mock_interaction = AsyncMock()
mock_interaction.user.id = 258104532423147520 # WV owner ID from API data
mock_team = Team.from_api_data({
'id': 499,
'abbrev': 'WV',
'sname': 'Black Bears',
'lname': 'West Virginia Black Bears',
'season': 12,
'thumbnail': 'https://example.com/wv.png'
})
transactions = [Transaction.from_api_data(data) for data in realistic_api_data]
with patch('commands.transactions.management.team_service') as mock_team_service:
with patch('commands.transactions.management.transaction_service') as mock_tx_service:
# Setup service mocks
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
# Filter transactions by status
pending_tx = [tx for tx in transactions if tx.is_pending]
frozen_tx = [tx for tx in transactions if tx.is_frozen]
cancelled_tx = [tx for tx in transactions if tx.is_cancelled]
mock_tx_service.get_pending_transactions = AsyncMock(return_value=pending_tx)
mock_tx_service.get_frozen_transactions = AsyncMock(return_value=frozen_tx)
mock_tx_service.get_processed_transactions = AsyncMock(return_value=[])
# Execute command
await commands_cog.my_moves.callback(commands_cog, mock_interaction, show_cancelled=False)
# Verify embed creation
embed_call = mock_interaction.followup.send.call_args
embed = embed_call.kwargs['embed']
# Check embed contains realistic data
assert 'WV' in embed.title
assert 'West Virginia Black Bears' in embed.description
# Check transaction descriptions in fields
pending_field = next(f for f in embed.fields if 'Pending' in f.name)
assert 'Yordan Alvarez: NYD → WV' in pending_field.value
@pytest.mark.asyncio
async def test_error_propagation_integration(self):
"""Test that errors propagate correctly through all layers."""
service = transaction_service
mock_bot = MagicMock()
commands_cog = TransactionCommands(mock_bot)
mock_interaction = AsyncMock()
mock_interaction.user.id = 258104532423147520
# Test API error propagation
with patch.object(service, 'get_all_items', new_callable=AsyncMock) as mock_get:
# Mock API failure
mock_get.side_effect = Exception("Database connection failed")
with patch('commands.transactions.management.team_service') as mock_team_service:
mock_team = Team.from_api_data({
'id': 499,
'abbrev': 'WV',
'sname': 'Black Bears',
'lname': 'West Virginia Black Bears',
'season': 12
})
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
# Should propagate exception
with pytest.raises(Exception) as exc_info:
await commands_cog.my_moves.callback(commands_cog, mock_interaction)
assert "Database connection failed" in str(exc_info.value)
@pytest.mark.asyncio
async def test_performance_integration(self, realistic_api_data):
"""Test system performance with realistic data volumes."""
# Scale up the data to simulate production load
large_dataset = []
for week in range(1, 19): # 18 weeks
for i in range(20): # 20 transactions per week
tx_data = {
**realistic_api_data[0],
'id': (week * 100) + i,
'week': week,
'moveid': f'Season-012-Week-{week:02d}-{i:02d}',
'player': {
**realistic_api_data[0]['player'],
'id': (week * 100) + i,
'name': f'Player {(week * 100) + i}'
},
'cancelled': i % 10 == 0, # 10% cancelled
'frozen': i % 7 == 0 # ~14% frozen
}
large_dataset.append(tx_data)
service = transaction_service
with patch.object(service, 'get_all_items', new_callable=AsyncMock) as mock_get:
mock_get.return_value = [Transaction.from_api_data(data) for data in large_dataset]
import time
start_time = time.time()
# Test various service operations
all_transactions = await service.get_team_transactions('WV', 12)
pending = await service.get_pending_transactions('WV', 12)
frozen = await service.get_frozen_transactions('WV', 12)
end_time = time.time()
processing_time = end_time - start_time
# Performance assertions
assert len(all_transactions) == 360 # 18 weeks * 20 transactions
assert len(pending) > 0
assert len(frozen) > 0
assert processing_time < 0.5 # Should process quickly
# Verify sorting performance
for i in range(len(all_transactions) - 1):
current_tx = all_transactions[i]
next_tx = all_transactions[i + 1]
assert current_tx.week <= next_tx.week
@pytest.mark.asyncio
async def test_concurrent_operations_integration(self, realistic_api_data):
"""Test concurrent operations across the entire system."""
service = transaction_service
mock_bot = MagicMock()
# Create multiple command instances (simulating multiple users)
command_instances = [TransactionCommands(mock_bot) for _ in range(5)]
mock_interactions = []
for i in range(5):
interaction = AsyncMock()
interaction.user.id = 258104532423147520 + i
mock_interactions.append(interaction)
transactions = [Transaction.from_api_data(data) for data in realistic_api_data]
with patch.object(service, 'get_all_items', new_callable=AsyncMock) as mock_get:
mock_get.return_value = transactions
with patch('commands.transactions.management.team_service') as mock_team_service:
with patch('commands.transactions.management.transaction_service', service):
mock_team = Team.from_api_data({
'id': 499,
'abbrev': 'WV',
'sname': 'Black Bears',
'lname': 'West Virginia Black Bears',
'season': 12
})
mock_team_service.get_teams_by_owner = AsyncMock(return_value=[mock_team])
# Execute concurrent operations
tasks = []
for i, (cmd, interaction) in enumerate(zip(command_instances, mock_interactions)):
tasks.append(cmd.my_moves.callback(cmd, interaction, show_cancelled=(i % 2 == 0)))
# Wait for all operations to complete
results = await asyncio.gather(*tasks, return_exceptions=True)
# All should complete successfully
successful_results = [r for r in results if not isinstance(r, Exception)]
assert len(successful_results) == 5
# All interactions should have received responses
for interaction in mock_interactions:
interaction.followup.send.assert_called_once()
@pytest.mark.asyncio
async def test_data_consistency_integration(self, realistic_api_data):
"""Test data consistency across service operations."""
service = transaction_service
transactions = [Transaction.from_api_data(data) for data in realistic_api_data]
with patch.object(service, 'get_all_items', new_callable=AsyncMock) as mock_get:
mock_get.return_value = transactions
# Get transactions through different service methods
all_tx = await service.get_team_transactions('WV', 12)
pending_tx = await service.get_pending_transactions('WV', 12)
frozen_tx = await service.get_frozen_transactions('WV', 12)
# Verify data consistency
total_by_status = len(pending_tx) + len(frozen_tx)
# Count cancelled transactions separately
cancelled_count = len([tx for tx in all_tx if tx.is_cancelled])
# Total should match when accounting for all statuses
assert len(all_tx) == total_by_status + cancelled_count
# Verify no transaction appears in multiple status lists
pending_ids = {tx.id for tx in pending_tx}
frozen_ids = {tx.id for tx in frozen_tx}
assert len(pending_ids.intersection(frozen_ids)) == 0 # No overlap
# Verify transaction properties match their categorization
for tx in pending_tx:
assert tx.is_pending is True
assert tx.is_frozen is False
assert tx.is_cancelled is False
for tx in frozen_tx:
assert tx.is_frozen is True
assert tx.is_pending is False
assert tx.is_cancelled is False
@pytest.mark.asyncio
async def test_validation_integration(self, realistic_api_data):
"""Test transaction validation integration."""
service = transaction_service
transactions = [Transaction.from_api_data(data) for data in realistic_api_data]
# Test validation for each transaction
for tx in transactions:
validation = await service.validate_transaction(tx)
assert isinstance(validation, RosterValidation)
# Basic validation should pass for well-formed transactions
assert validation.is_legal is True
assert len(validation.errors) == 0
# Test validation with problematic transaction (simulated)
problematic_tx = transactions[0]
# Mock validation failure
with patch.object(service, 'validate_transaction') as mock_validate:
mock_validate.return_value = RosterValidation(
is_legal=False,
errors=['Player not eligible for move', 'Roster size violation'],
warnings=['Team WARA below threshold']
)
validation = await service.validate_transaction(problematic_tx)
assert validation.is_legal is False
assert len(validation.errors) == 2
assert len(validation.warnings) == 1
assert validation.status_emoji == ''