strat-gameplay-webapp/backend/tests/unit/websocket/test_substitution_handlers.py
Cal Corum 4253b71db9 CLAUDE: Enhance WebSocket handlers with comprehensive test coverage
WebSocket Infrastructure:
- Connection manager: Improved connection/disconnection handling
- Handlers: Enhanced event handlers for game operations

Test Coverage (148 new tests):
- test_connection_handlers.py: Connection lifecycle tests
- test_connection_manager.py: Manager operations tests
- test_handler_locking.py: Concurrency/locking tests
- test_query_handlers.py: Game query handler tests
- test_rate_limiting.py: Rate limit enforcement tests
- test_substitution_handlers.py: Player substitution tests
- test_manual_outcome_handlers.py: Manual outcome workflow tests
- conftest.py: Shared WebSocket test fixtures

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 12:08:43 -06:00

999 lines
34 KiB
Python

"""
Tests for WebSocket substitution handlers.
Covers all three substitution types:
- Pinch hitter (batting substitution)
- Defensive replacement (field substitution)
- Pitching change (pitcher substitution)
Each handler follows the same pattern: validate input, acquire lock,
call SubstitutionManager, broadcast results.
Author: Claude
Date: 2025-01-27
"""
import asyncio
import pytest
from contextlib import asynccontextmanager
from uuid import uuid4
from unittest.mock import AsyncMock, MagicMock, patch
from app.core.substitution_manager import SubstitutionResult
# ============================================================================
# TEST FIXTURES
# ============================================================================
@pytest.fixture
def mock_sub_result_success():
"""Create a successful substitution result."""
return SubstitutionResult(
success=True,
player_out_lineup_id=1,
player_in_card_id=999,
new_lineup_id=100,
new_position="CF",
new_batting_order=1,
error_message=None,
error_code=None,
)
@pytest.fixture
def mock_sub_result_failure():
"""Create a failed substitution result."""
return SubstitutionResult(
success=False,
player_out_lineup_id=1,
player_in_card_id=999,
new_lineup_id=None,
new_position=None,
new_batting_order=None,
error_message="Player not found in roster",
error_code="PLAYER_NOT_IN_ROSTER",
)
@pytest.fixture
def base_sub_data():
"""Base data for substitution requests."""
return {
"game_id": str(uuid4()),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"team_id": 1,
}
# ============================================================================
# PINCH HITTER TESTS
# ============================================================================
class TestPinchHitter:
"""Tests for request_pinch_hitter handler."""
@pytest.mark.asyncio
async def test_pinch_hitter_success(
self, mock_manager, mock_game_state, mock_sub_result_success
):
"""
Verify successful pinch hitter substitution.
Should call SubstitutionManager.pinch_hit(), broadcast player_substituted,
and confirm to requester.
"""
from socketio import AsyncServer
sio = AsyncServer()
@asynccontextmanager
async def mock_lock(game_id, timeout=30.0):
yield
with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \
patch("app.websocket.handlers.SubstitutionManager") as MockSubMgr:
mock_state_mgr.get_state.return_value = mock_game_state
mock_state_mgr.game_lock = mock_lock
mock_sub_instance = MagicMock()
mock_sub_instance.pinch_hit = AsyncMock(return_value=mock_sub_result_success)
MockSubMgr.return_value = mock_sub_instance
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pinch_hitter"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"team_id": 1,
},
)
# Verify SubstitutionManager called
mock_sub_instance.pinch_hit.assert_called_once()
# Verify broadcast
mock_manager.broadcast_to_game.assert_called()
broadcast_call = mock_manager.broadcast_to_game.call_args
assert broadcast_call[0][1] == "player_substituted"
assert broadcast_call[0][2]["type"] == "pinch_hitter"
# Verify confirmation to requester
mock_manager.emit_to_user.assert_called()
confirm_call = mock_manager.emit_to_user.call_args
assert confirm_call[0][1] == "substitution_confirmed"
@pytest.mark.asyncio
async def test_pinch_hitter_missing_game_id(self, mock_manager):
"""Verify error when game_id is missing."""
from socketio import AsyncServer
from app.websocket.handlers import register_handlers
sio = AsyncServer()
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pinch_hitter"]
await handler(
"test_sid",
{"player_out_lineup_id": 1, "player_in_card_id": 999, "team_id": 1},
)
mock_manager.emit_to_user.assert_called_once()
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "substitution_error"
assert "MISSING_FIELD" in call_args[0][2]["code"]
@pytest.mark.asyncio
async def test_pinch_hitter_invalid_game_id(self, mock_manager):
"""Verify error when game_id is invalid UUID."""
from socketio import AsyncServer
from app.websocket.handlers import register_handlers
sio = AsyncServer()
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pinch_hitter"]
await handler(
"test_sid",
{
"game_id": "not-a-uuid",
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "substitution_error"
assert "INVALID_FORMAT" in call_args[0][2]["code"]
@pytest.mark.asyncio
async def test_pinch_hitter_game_not_found(self, mock_manager):
"""Verify error when game doesn't exist."""
from socketio import AsyncServer
sio = AsyncServer()
with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
mock_state_mgr.get_state.return_value = None
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pinch_hitter"]
await handler(
"test_sid",
{
"game_id": str(uuid4()),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert "not found" in call_args[0][2]["message"]
@pytest.mark.asyncio
async def test_pinch_hitter_missing_player_out(self, mock_manager, mock_game_state):
"""Verify error when player_out_lineup_id is missing."""
from socketio import AsyncServer
sio = AsyncServer()
with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
mock_state_mgr.get_state.return_value = mock_game_state
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pinch_hitter"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_in_card_id": 999,
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "substitution_error"
assert "player_out" in call_args[0][2]["message"].lower()
@pytest.mark.asyncio
async def test_pinch_hitter_missing_player_in(self, mock_manager, mock_game_state):
"""Verify error when player_in_card_id is missing."""
from socketio import AsyncServer
sio = AsyncServer()
with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
mock_state_mgr.get_state.return_value = mock_game_state
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pinch_hitter"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_out_lineup_id": 1,
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "substitution_error"
assert "player_in" in call_args[0][2]["message"].lower()
@pytest.mark.asyncio
async def test_pinch_hitter_missing_team_id(self, mock_manager, mock_game_state):
"""Verify error when team_id is missing."""
from socketio import AsyncServer
sio = AsyncServer()
with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
mock_state_mgr.get_state.return_value = mock_game_state
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pinch_hitter"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
},
)
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "substitution_error"
assert "team_id" in call_args[0][2]["message"].lower()
@pytest.mark.asyncio
async def test_pinch_hitter_validation_failure(
self, mock_manager, mock_game_state, mock_sub_result_failure
):
"""Verify SubstitutionManager failure is propagated to user."""
from socketio import AsyncServer
sio = AsyncServer()
@asynccontextmanager
async def mock_lock(game_id, timeout=30.0):
yield
with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \
patch("app.websocket.handlers.SubstitutionManager") as MockSubMgr:
mock_state_mgr.get_state.return_value = mock_game_state
mock_state_mgr.game_lock = mock_lock
mock_sub_instance = MagicMock()
mock_sub_instance.pinch_hit = AsyncMock(return_value=mock_sub_result_failure)
MockSubMgr.return_value = mock_sub_instance
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pinch_hitter"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "substitution_error"
assert call_args[0][2]["code"] == "PLAYER_NOT_IN_ROSTER"
@pytest.mark.asyncio
async def test_pinch_hitter_lock_timeout(self, mock_manager, mock_game_state):
"""Verify lock timeout returns server busy message."""
from socketio import AsyncServer
sio = AsyncServer()
@asynccontextmanager
async def timeout_lock(game_id, timeout=30.0):
raise asyncio.TimeoutError("Lock timeout")
yield
with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
mock_state_mgr.get_state.return_value = mock_game_state
mock_state_mgr.game_lock = timeout_lock
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pinch_hitter"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "error"
assert "busy" in call_args[0][2]["message"].lower()
# ============================================================================
# DEFENSIVE REPLACEMENT TESTS
# ============================================================================
class TestDefensiveReplacement:
"""Tests for request_defensive_replacement handler."""
@pytest.mark.asyncio
async def test_defensive_replacement_success(
self, mock_manager, mock_game_state, mock_sub_result_success
):
"""Verify successful defensive replacement."""
from socketio import AsyncServer
sio = AsyncServer()
@asynccontextmanager
async def mock_lock(game_id, timeout=30.0):
yield
with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \
patch("app.websocket.handlers.SubstitutionManager") as MockSubMgr:
mock_state_mgr.get_state.return_value = mock_game_state
mock_state_mgr.game_lock = mock_lock
mock_sub_instance = MagicMock()
mock_sub_instance.defensive_replace = AsyncMock(
return_value=mock_sub_result_success
)
MockSubMgr.return_value = mock_sub_instance
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_defensive_replacement"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"new_position": "CF",
"team_id": 1,
},
)
# Verify SubstitutionManager called with position
mock_sub_instance.defensive_replace.assert_called_once()
call_kwargs = mock_sub_instance.defensive_replace.call_args[1]
assert call_kwargs["new_position"] == "CF"
# Verify broadcast type
broadcast_call = mock_manager.broadcast_to_game.call_args
assert broadcast_call[0][2]["type"] == "defensive_replacement"
@pytest.mark.asyncio
async def test_defensive_replacement_missing_game_id(self, mock_manager):
"""Verify error when game_id is missing."""
from socketio import AsyncServer
from app.websocket.handlers import register_handlers
sio = AsyncServer()
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_defensive_replacement"]
await handler(
"test_sid",
{
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"new_position": "CF",
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "substitution_error"
assert "MISSING_FIELD" in call_args[0][2]["code"]
@pytest.mark.asyncio
async def test_defensive_replacement_invalid_game_id(self, mock_manager):
"""Verify error when game_id is invalid."""
from socketio import AsyncServer
from app.websocket.handlers import register_handlers
sio = AsyncServer()
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_defensive_replacement"]
await handler(
"test_sid",
{
"game_id": "invalid",
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"new_position": "CF",
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert "INVALID_FORMAT" in call_args[0][2]["code"]
@pytest.mark.asyncio
async def test_defensive_replacement_game_not_found(self, mock_manager):
"""Verify error when game doesn't exist."""
from socketio import AsyncServer
sio = AsyncServer()
with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
mock_state_mgr.get_state.return_value = None
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_defensive_replacement"]
await handler(
"test_sid",
{
"game_id": str(uuid4()),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"new_position": "CF",
"team_id": 1,
},
)
assert "not found" in mock_manager.emit_to_user.call_args[0][2]["message"]
@pytest.mark.asyncio
async def test_defensive_replacement_missing_position(
self, mock_manager, mock_game_state
):
"""Verify error when new_position is missing."""
from socketio import AsyncServer
sio = AsyncServer()
with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
mock_state_mgr.get_state.return_value = mock_game_state
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_defensive_replacement"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "substitution_error"
assert "position" in call_args[0][2]["message"].lower()
@pytest.mark.asyncio
async def test_defensive_replacement_validation_failure(
self, mock_manager, mock_game_state, mock_sub_result_failure
):
"""Verify SubstitutionManager failure is propagated."""
from socketio import AsyncServer
sio = AsyncServer()
@asynccontextmanager
async def mock_lock(game_id, timeout=30.0):
yield
with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \
patch("app.websocket.handlers.SubstitutionManager") as MockSubMgr:
mock_state_mgr.get_state.return_value = mock_game_state
mock_state_mgr.game_lock = mock_lock
mock_sub_instance = MagicMock()
mock_sub_instance.defensive_replace = AsyncMock(
return_value=mock_sub_result_failure
)
MockSubMgr.return_value = mock_sub_instance
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_defensive_replacement"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"new_position": "CF",
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "substitution_error"
@pytest.mark.asyncio
async def test_defensive_replacement_lock_timeout(
self, mock_manager, mock_game_state
):
"""Verify lock timeout returns server busy message."""
from socketio import AsyncServer
sio = AsyncServer()
@asynccontextmanager
async def timeout_lock(game_id, timeout=30.0):
raise asyncio.TimeoutError("Lock timeout")
yield
with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
mock_state_mgr.get_state.return_value = mock_game_state
mock_state_mgr.game_lock = timeout_lock
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_defensive_replacement"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"new_position": "CF",
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert "busy" in call_args[0][2]["message"].lower()
@pytest.mark.asyncio
async def test_defensive_replacement_missing_player_out(
self, mock_manager, mock_game_state
):
"""
Verify error when player_out_lineup_id is missing.
Defensive replacement requires specifying which player is being replaced.
"""
from socketio import AsyncServer
sio = AsyncServer()
with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
mock_state_mgr.get_state.return_value = mock_game_state
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_defensive_replacement"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_in_card_id": 999,
"new_position": "CF",
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "substitution_error"
assert "player_out" in call_args[0][2]["message"].lower()
@pytest.mark.asyncio
async def test_defensive_replacement_missing_player_in(
self, mock_manager, mock_game_state
):
"""
Verify error when player_in_card_id is missing.
Defensive replacement requires specifying which player is entering.
"""
from socketio import AsyncServer
sio = AsyncServer()
with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
mock_state_mgr.get_state.return_value = mock_game_state
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_defensive_replacement"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_out_lineup_id": 1,
"new_position": "CF",
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "substitution_error"
assert "player_in" in call_args[0][2]["message"].lower()
@pytest.mark.asyncio
async def test_defensive_replacement_missing_team_id(
self, mock_manager, mock_game_state
):
"""
Verify error when team_id is missing.
Defensive replacement must specify which team is making the substitution.
"""
from socketio import AsyncServer
sio = AsyncServer()
with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
mock_state_mgr.get_state.return_value = mock_game_state
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_defensive_replacement"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"new_position": "CF",
},
)
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "substitution_error"
assert "team_id" in call_args[0][2]["message"].lower()
# ============================================================================
# PITCHING CHANGE TESTS
# ============================================================================
class TestPitchingChange:
"""Tests for request_pitching_change handler."""
@pytest.mark.asyncio
async def test_pitching_change_success(
self, mock_manager, mock_game_state, mock_sub_result_success
):
"""Verify successful pitching change."""
from socketio import AsyncServer
sio = AsyncServer()
mock_sub_result_success.new_position = "P" # Override for pitcher
@asynccontextmanager
async def mock_lock(game_id, timeout=30.0):
yield
with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \
patch("app.websocket.handlers.SubstitutionManager") as MockSubMgr:
mock_state_mgr.get_state.return_value = mock_game_state
mock_state_mgr.game_lock = mock_lock
mock_sub_instance = MagicMock()
mock_sub_instance.change_pitcher = AsyncMock(
return_value=mock_sub_result_success
)
MockSubMgr.return_value = mock_sub_instance
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pitching_change"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"team_id": 1,
},
)
mock_sub_instance.change_pitcher.assert_called_once()
broadcast_call = mock_manager.broadcast_to_game.call_args
assert broadcast_call[0][2]["type"] == "pitching_change"
@pytest.mark.asyncio
async def test_pitching_change_missing_game_id(self, mock_manager):
"""Verify error when game_id is missing."""
from socketio import AsyncServer
from app.websocket.handlers import register_handlers
sio = AsyncServer()
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pitching_change"]
await handler(
"test_sid",
{"player_out_lineup_id": 1, "player_in_card_id": 999, "team_id": 1},
)
call_args = mock_manager.emit_to_user.call_args
assert "MISSING_FIELD" in call_args[0][2]["code"]
@pytest.mark.asyncio
async def test_pitching_change_invalid_game_id(self, mock_manager):
"""Verify error when game_id is invalid."""
from socketio import AsyncServer
from app.websocket.handlers import register_handlers
sio = AsyncServer()
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pitching_change"]
await handler(
"test_sid",
{
"game_id": "bad-uuid",
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert "INVALID_FORMAT" in call_args[0][2]["code"]
@pytest.mark.asyncio
async def test_pitching_change_game_not_found(self, mock_manager):
"""Verify error when game doesn't exist."""
from socketio import AsyncServer
sio = AsyncServer()
with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
mock_state_mgr.get_state.return_value = None
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pitching_change"]
await handler(
"test_sid",
{
"game_id": str(uuid4()),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"team_id": 1,
},
)
assert "not found" in mock_manager.emit_to_user.call_args[0][2]["message"]
@pytest.mark.asyncio
async def test_pitching_change_missing_player_out(
self, mock_manager, mock_game_state
):
"""Verify error when player_out_lineup_id is missing."""
from socketio import AsyncServer
sio = AsyncServer()
with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
mock_state_mgr.get_state.return_value = mock_game_state
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pitching_change"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_in_card_id": 999,
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "substitution_error"
@pytest.mark.asyncio
async def test_pitching_change_missing_player_in(
self, mock_manager, mock_game_state
):
"""Verify error when player_in_card_id is missing."""
from socketio import AsyncServer
sio = AsyncServer()
with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
mock_state_mgr.get_state.return_value = mock_game_state
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pitching_change"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_out_lineup_id": 1,
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "substitution_error"
@pytest.mark.asyncio
async def test_pitching_change_missing_team_id(self, mock_manager, mock_game_state):
"""Verify error when team_id is missing."""
from socketio import AsyncServer
sio = AsyncServer()
with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
mock_state_mgr.get_state.return_value = mock_game_state
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pitching_change"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
},
)
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "substitution_error"
@pytest.mark.asyncio
async def test_pitching_change_validation_failure(
self, mock_manager, mock_game_state, mock_sub_result_failure
):
"""Verify SubstitutionManager failure is propagated."""
from socketio import AsyncServer
sio = AsyncServer()
@asynccontextmanager
async def mock_lock(game_id, timeout=30.0):
yield
with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \
patch("app.websocket.handlers.SubstitutionManager") as MockSubMgr:
mock_state_mgr.get_state.return_value = mock_game_state
mock_state_mgr.game_lock = mock_lock
mock_sub_instance = MagicMock()
mock_sub_instance.change_pitcher = AsyncMock(
return_value=mock_sub_result_failure
)
MockSubMgr.return_value = mock_sub_instance
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pitching_change"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert call_args[0][1] == "substitution_error"
@pytest.mark.asyncio
async def test_pitching_change_lock_timeout(self, mock_manager, mock_game_state):
"""Verify lock timeout returns server busy message."""
from socketio import AsyncServer
sio = AsyncServer()
@asynccontextmanager
async def timeout_lock(game_id, timeout=30.0):
raise asyncio.TimeoutError("Lock timeout")
yield
with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
mock_state_mgr.get_state.return_value = mock_game_state
mock_state_mgr.game_lock = timeout_lock
from app.websocket.handlers import register_handlers
register_handlers(sio, mock_manager)
handler = sio.handlers["/"]["request_pitching_change"]
await handler(
"test_sid",
{
"game_id": str(mock_game_state.game_id),
"player_out_lineup_id": 1,
"player_in_card_id": 999,
"team_id": 1,
},
)
call_args = mock_manager.emit_to_user.call_args
assert "busy" in call_args[0][2]["message"].lower()