major-domo-v2/tests/test_views_transaction_embed.py
Cal Corum f007c5b870 Fix frozen flag bug and add Transaction Thaw Report for admins
Bug Fix:
- Fixed /dropadd transactions being marked frozen=True during thaw period
- Now uses current_state.freeze to set frozen flag correctly
- Transactions entered Sat-Sun are now unfrozen and execute Monday

New Feature - Transaction Thaw Report:
- Added data structures for thaw reporting (ThawReport, ThawedMove,
  CancelledMove, ConflictResolution, ConflictContender)
- Modified resolve_contested_transactions() to return conflict details
- Added _post_thaw_report() to post formatted report to admin channel
- Report shows thawed moves, cancelled moves, and conflict resolution
- Handles Discord's 2000 char limit with _send_long_report()

Tests:
- Updated test_views_transaction_embed.py for frozen flag behavior
- Added test for thaw period (freeze=False) scenario
- Updated test_tasks_transaction_freeze.py for new return values
- All tests passing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 13:35:48 -06:00

457 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Tests for Transaction Embed Views
Validates Discord UI components, modals, and interactive elements.
"""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
import discord
from views.transaction_embed import (
TransactionEmbedView,
RemoveMoveView,
RemoveMoveSelect,
SubmitConfirmationModal,
create_transaction_embed
)
from services.transaction_builder import (
TransactionBuilder,
TransactionMove,
RosterValidationResult
)
from models.team import Team, RosterType
from models.player import Player
class TestTransactionEmbedView:
"""Test TransactionEmbedView Discord UI component."""
@pytest.fixture
def mock_builder(self):
"""Create mock TransactionBuilder."""
team = Team(id=499, abbrev='WV', sname='Black Bears', lname='West Virginia Black Bears', season=12)
builder = MagicMock(spec=TransactionBuilder)
builder.team = team
builder.user_id = 123456789
builder.season = 12
builder.is_empty = False
builder.move_count = 2
builder.moves = []
builder.created_at = MagicMock()
builder.created_at.strftime.return_value = "10:30:15"
return builder
# Don't create view as fixture - create in test methods to ensure event loop is running
@pytest.fixture
def mock_interaction(self):
"""Create mock Discord interaction."""
interaction = AsyncMock()
interaction.user = MagicMock()
interaction.user.id = 123456789
interaction.response = AsyncMock()
interaction.followup = AsyncMock()
interaction.client = MagicMock()
interaction.channel = MagicMock()
return interaction
@pytest.mark.asyncio
async def test_interaction_check_correct_user(self, mock_builder, mock_interaction):
"""Test interaction check passes for correct user."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
result = await view.interaction_check(mock_interaction)
assert result is True
@pytest.mark.asyncio
async def test_interaction_check_wrong_user(self, mock_builder, mock_interaction):
"""Test interaction check fails for wrong user."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
mock_interaction.user.id = 999999999 # Different user
result = await view.interaction_check(mock_interaction)
assert result is False
mock_interaction.response.send_message.assert_called_once()
call_args = mock_interaction.response.send_message.call_args
assert "don't have permission" in call_args[0][0]
assert call_args[1]['ephemeral'] is True
@pytest.mark.asyncio
async def test_remove_move_button_empty_builder(self, mock_builder, mock_interaction):
"""Test remove move button with empty builder."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
view.builder.is_empty = True
await view.remove_move_button.callback(mock_interaction)
mock_interaction.response.send_message.assert_called_once()
call_args = mock_interaction.response.send_message.call_args
assert "No moves to remove" in call_args[0][0]
assert call_args[1]['ephemeral'] is True
@pytest.mark.asyncio
async def test_remove_move_button_with_moves(self, mock_builder, mock_interaction):
"""Test remove move button with moves available."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
view.builder.is_empty = False
with patch('views.transaction_embed.create_transaction_embed') as mock_create_embed:
mock_create_embed.return_value = MagicMock()
await view.remove_move_button.callback(mock_interaction)
mock_interaction.response.edit_message.assert_called_once()
# Check that view is RemoveMoveView
call_args = mock_interaction.response.edit_message.call_args
view_arg = call_args[1]['view']
assert isinstance(view_arg, RemoveMoveView)
@pytest.mark.asyncio
async def test_submit_button_empty_builder(self, mock_builder, mock_interaction):
"""Test submit button with empty builder."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
view.builder.is_empty = True
await view.submit_button.callback(mock_interaction)
mock_interaction.response.send_message.assert_called_once()
call_args = mock_interaction.response.send_message.call_args
assert "Cannot submit empty transaction" in call_args[0][0]
assert call_args[1]['ephemeral'] is True
@pytest.mark.asyncio
async def test_submit_button_illegal_transaction(self, mock_builder, mock_interaction):
"""Test submit button with illegal transaction."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
view.builder.is_empty = False
view.builder.validate_transaction = AsyncMock(return_value=RosterValidationResult(
is_legal=False,
major_league_count=26,
minor_league_count=10,
warnings=[],
errors=["Too many players"],
suggestions=["Drop 1 player"]
))
await view.submit_button.callback(mock_interaction)
mock_interaction.response.send_message.assert_called_once()
call_args = mock_interaction.response.send_message.call_args
message = call_args[0][0]
assert "Cannot submit illegal transaction" in message
assert "Too many players" in message
assert "Drop 1 player" in message
assert call_args[1]['ephemeral'] is True
@pytest.mark.asyncio
async def test_submit_button_legal_transaction(self, mock_builder, mock_interaction):
"""Test submit button with legal transaction."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
view.builder.is_empty = False
view.builder.validate_transaction = AsyncMock(return_value=RosterValidationResult(
is_legal=True,
major_league_count=25,
minor_league_count=10,
warnings=[],
errors=[],
suggestions=[]
))
await view.submit_button.callback(mock_interaction)
# Should send confirmation modal
mock_interaction.response.send_modal.assert_called_once()
modal_arg = mock_interaction.response.send_modal.call_args[0][0]
assert isinstance(modal_arg, SubmitConfirmationModal)
@pytest.mark.asyncio
async def test_cancel_button(self, mock_builder, mock_interaction):
"""Test cancel button clears moves and disables view."""
view = TransactionEmbedView(mock_builder, user_id=123456789)
with patch('views.transaction_embed.create_transaction_embed') as mock_create_embed:
mock_create_embed.return_value = MagicMock()
await view.cancel_button.callback(mock_interaction)
# Should clear moves
view.builder.clear_moves.assert_called_once()
# Should edit message with disabled view
mock_interaction.response.edit_message.assert_called_once()
call_args = mock_interaction.response.edit_message.call_args
assert "Transaction cancelled" in call_args[1]['content']
class TestSubmitConfirmationModal:
"""Test SubmitConfirmationModal functionality."""
@pytest.fixture
def mock_builder(self):
"""Create mock TransactionBuilder."""
team = Team(id=499, abbrev='WV', sname='Black Bears', lname='West Virginia Black Bears', season=12)
builder = MagicMock(spec=TransactionBuilder)
builder.team = team
builder.moves = []
return builder
@pytest.fixture
def modal(self, mock_builder):
"""Create SubmitConfirmationModal instance."""
return SubmitConfirmationModal(mock_builder)
@pytest.fixture
def mock_interaction(self):
"""Create mock Discord interaction."""
interaction = AsyncMock()
interaction.user = MagicMock()
interaction.user.id = 123456789
interaction.response = AsyncMock()
interaction.followup = AsyncMock()
interaction.client = MagicMock()
interaction.channel = MagicMock()
# Mock message history
mock_message = MagicMock()
mock_message.author = interaction.client.user
mock_message.embeds = [MagicMock()]
mock_message.embeds[0].title = "📋 Transaction Builder"
mock_message.edit = AsyncMock()
interaction.channel.history.return_value.__aiter__ = AsyncMock(return_value=iter([mock_message]))
return interaction
@pytest.mark.asyncio
async def test_modal_submit_wrong_confirmation(self, mock_builder, mock_interaction):
"""Test modal submission with wrong confirmation text."""
modal = SubmitConfirmationModal(mock_builder)
# Mock the TextInput values
modal.confirmation = MagicMock()
modal.confirmation.value = 'WRONG'
await modal.on_submit(mock_interaction)
mock_interaction.response.send_message.assert_called_once()
call_args = mock_interaction.response.send_message.call_args
assert "must type 'CONFIRM' exactly" in call_args[0][0]
assert call_args[1]['ephemeral'] is True
@pytest.mark.asyncio
async def test_modal_submit_correct_confirmation(self, mock_builder, mock_interaction):
"""Test modal submission with correct confirmation."""
modal = SubmitConfirmationModal(mock_builder)
# Mock the TextInput values
modal.confirmation = MagicMock()
modal.confirmation.value = 'CONFIRM'
mock_transaction = MagicMock()
mock_transaction.moveid = 'Season-012-Week-11-123456789'
mock_transaction.week = 11
mock_transaction.frozen = False # Will be set to True
with patch('services.league_service.league_service') as mock_league_service:
mock_current_state = MagicMock()
mock_current_state.week = 10
mock_current_state.freeze = True # Simulate Monday-Friday freeze period
mock_league_service.get_current_state = AsyncMock(return_value=mock_current_state)
modal.builder.submit_transaction = AsyncMock(return_value=[mock_transaction])
with patch('services.transaction_service.transaction_service') as mock_transaction_service:
# Mock the create_transaction_batch call
mock_transaction_service.create_transaction_batch = AsyncMock(return_value=[mock_transaction])
with patch('utils.transaction_logging.post_transaction_to_log') as mock_post_log:
mock_post_log.return_value = AsyncMock()
with patch('services.transaction_builder.clear_transaction_builder') as mock_clear:
await modal.on_submit(mock_interaction)
# Should defer response
mock_interaction.response.defer.assert_called_once_with(ephemeral=True)
# Should get current state
mock_league_service.get_current_state.assert_called_once()
# Should submit transaction for next week
modal.builder.submit_transaction.assert_called_once_with(week=11)
# Should mark transaction as frozen (based on current_state.freeze)
assert mock_transaction.frozen is True
# Should POST to database
mock_transaction_service.create_transaction_batch.assert_called_once()
# Should clear builder
mock_clear.assert_called_once_with(123456789)
# Should send success message
mock_interaction.followup.send.assert_called_once()
call_args = mock_interaction.followup.send.call_args
assert "Transaction Submitted Successfully" in call_args[0][0]
assert mock_transaction.moveid in call_args[0][0]
@pytest.mark.asyncio
async def test_modal_submit_during_thaw_period(self, mock_builder, mock_interaction):
"""Test modal submission during thaw period (Saturday-Sunday) sets frozen=False.
When freeze=False (Saturday-Sunday), transactions should NOT be frozen
because they should execute immediately on Monday morning without waiting
for the Saturday conflict resolution process.
"""
modal = SubmitConfirmationModal(mock_builder)
# Mock the TextInput values
modal.confirmation = MagicMock()
modal.confirmation.value = 'CONFIRM'
mock_transaction = MagicMock()
mock_transaction.moveid = 'Season-012-Week-11-123456789'
mock_transaction.week = 11
mock_transaction.frozen = True # Will be set to False during thaw
with patch('services.league_service.league_service') as mock_league_service:
mock_current_state = MagicMock()
mock_current_state.week = 10
mock_current_state.freeze = False # Simulate Saturday-Sunday thaw period
mock_league_service.get_current_state = AsyncMock(return_value=mock_current_state)
modal.builder.submit_transaction = AsyncMock(return_value=[mock_transaction])
with patch('services.transaction_service.transaction_service') as mock_transaction_service:
# Mock the create_transaction_batch call
mock_transaction_service.create_transaction_batch = AsyncMock(return_value=[mock_transaction])
with patch('utils.transaction_logging.post_transaction_to_log') as mock_post_log:
mock_post_log.return_value = AsyncMock()
with patch('services.transaction_builder.clear_transaction_builder') as mock_clear:
await modal.on_submit(mock_interaction)
# Should mark transaction as NOT frozen (thaw period)
assert mock_transaction.frozen is False
@pytest.mark.asyncio
async def test_modal_submit_no_current_state(self, mock_builder, mock_interaction):
"""Test modal submission when current state unavailable."""
modal = SubmitConfirmationModal(mock_builder)
# Mock the TextInput values
modal.confirmation = MagicMock()
modal.confirmation.value = 'CONFIRM'
with patch('services.league_service.league_service') as mock_league_service:
mock_league_service.get_current_state = AsyncMock(return_value=None)
await modal.on_submit(mock_interaction)
mock_interaction.followup.send.assert_called_once()
call_args = mock_interaction.followup.send.call_args
assert "Could not get current league state" in call_args[0][0]
assert call_args[1]['ephemeral'] is True
class TestEmbedCreation:
"""Test embed creation functions."""
@pytest.fixture
def mock_builder_empty(self):
"""Create empty mock TransactionBuilder."""
team = Team(id=499, abbrev='WV', sname='Black Bears', lname='West Virginia Black Bears', season=12)
builder = MagicMock(spec=TransactionBuilder)
builder.team = team
builder.is_empty = True
builder.move_count = 0
builder.moves = []
builder.created_at = MagicMock()
builder.created_at.strftime.return_value = "10:30:15"
builder.validate_transaction = AsyncMock(return_value=RosterValidationResult(
is_legal=True,
major_league_count=24,
minor_league_count=10,
warnings=[],
errors=[],
suggestions=["Add player moves to build your transaction"]
))
return builder
@pytest.fixture
def mock_builder_with_moves(self):
"""Create mock TransactionBuilder with moves."""
team = Team(id=499, abbrev='WV', sname='Black Bears', lname='West Virginia Black Bears', season=12)
builder = MagicMock(spec=TransactionBuilder)
builder.team = team
builder.is_empty = False
builder.move_count = 2
mock_moves = []
for i in range(2):
move = MagicMock()
move.description = f"Move {i+1}: Player → Team"
mock_moves.append(move)
builder.moves = mock_moves
builder.created_at = MagicMock()
builder.created_at.strftime.return_value = "10:30:15"
builder.validate_transaction = AsyncMock(return_value=RosterValidationResult(
is_legal=False,
major_league_count=26,
minor_league_count=10,
warnings=["Warning message"],
errors=["Error message"],
suggestions=["Suggestion message"]
))
return builder
@pytest.mark.asyncio
async def test_create_transaction_embed_empty(self, mock_builder_empty):
"""Test creating embed for empty transaction."""
embed = await create_transaction_embed(mock_builder_empty)
assert isinstance(embed, discord.Embed)
assert "Transaction Builder - WV" in embed.title
assert "📋" in embed.title
# Should have fields for empty state
field_names = [field.name for field in embed.fields]
assert "Current Moves" in field_names
assert "Roster Status" in field_names
assert "Suggestions" in field_names
# Check empty moves message
moves_field = next(field for field in embed.fields if field.name == "Current Moves")
assert "No moves yet" in moves_field.value
# Check for Add More Moves instruction field
field_names = [field.name for field in embed.fields]
assert " Add More Moves" in field_names
add_moves_field = next(field for field in embed.fields if field.name == " Add More Moves")
assert "/dropadd" in add_moves_field.value
@pytest.mark.asyncio
async def test_create_transaction_embed_with_moves(self, mock_builder_with_moves):
"""Test creating embed for transaction with moves."""
embed = await create_transaction_embed(mock_builder_with_moves)
assert isinstance(embed, discord.Embed)
assert "Transaction Builder - WV" in embed.title
# Should have all fields
field_names = [field.name for field in embed.fields]
assert "Current Moves (2)" in field_names
assert "Roster Status" in field_names
assert "❌ Errors" in field_names
assert "Suggestions" in field_names
# Check moves content
moves_field = next(field for field in embed.fields if "Current Moves" in field.name)
assert "Move 1: Player → Team" in moves_field.value
assert "Move 2: Player → Team" in moves_field.value
# Check for Add More Moves instruction field
field_names = [field.name for field in embed.fields]
assert " Add More Moves" in field_names
add_moves_field = next(field for field in embed.fields if field.name == " Add More Moves")
assert "/dropadd" in add_moves_field.value