""" Tests: Uncapped Hit WebSocket Handlers Verifies the 5 new WebSocket event handlers for uncapped hit decisions: - submit_uncapped_lead_advance - submit_uncapped_defensive_throw - submit_uncapped_trail_advance - submit_uncapped_throw_target - submit_uncapped_safe_out Tests cover: - Missing/invalid game_id handling - Missing/invalid field-specific input validation - Successful submission forwarding to game engine - State broadcast after successful submission - ValueError propagation from game engine Author: Claude Date: 2025-02-11 """ import pytest from uuid import uuid4 from unittest.mock import AsyncMock, MagicMock, patch from app.models.game_models import GameState, LineupPlayerState from .conftest import get_handler, sio_with_mocks # ============================================================================= # Fixtures # ============================================================================= @pytest.fixture def mock_game_state(): """Create a mock active game state for handler tests.""" return GameState( game_id=uuid4(), league_id="sba", home_team_id=1, away_team_id=2, current_batter=LineupPlayerState( lineup_id=1, card_id=100, position="CF", batting_order=1 ), status="active", inning=1, half="top", outs=0, ) # ============================================================================= # Tests: submit_uncapped_lead_advance # ============================================================================= class TestSubmitUncappedLeadAdvance: """Tests for the submit_uncapped_lead_advance WebSocket handler.""" @pytest.mark.asyncio async def test_missing_game_id(self, sio_with_mocks): """Handler emits error when game_id is not provided.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_lead_advance") await handler("test_sid", {"advance": True}) mocks["manager"].emit_to_user.assert_called_once() call_args = mocks["manager"].emit_to_user.call_args[0] assert call_args[1] == "error" assert "game_id" in call_args[2]["message"].lower() @pytest.mark.asyncio async def test_invalid_game_id(self, sio_with_mocks): """Handler emits error when game_id is not a valid UUID.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_lead_advance") await handler("test_sid", {"game_id": "not-a-uuid", "advance": True}) mocks["manager"].emit_to_user.assert_called() call_args = mocks["manager"].emit_to_user.call_args[0] assert call_args[1] == "error" @pytest.mark.asyncio async def test_missing_advance_field(self, sio_with_mocks): """Handler emits error when advance field is missing.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_lead_advance") game_id = str(uuid4()) await handler("test_sid", {"game_id": game_id}) mocks["manager"].emit_to_user.assert_called() call_args = mocks["manager"].emit_to_user.call_args[0] assert call_args[1] == "error" assert "advance" in call_args[2]["message"].lower() @pytest.mark.asyncio async def test_invalid_advance_type(self, sio_with_mocks): """Handler emits error when advance is not a bool.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_lead_advance") game_id = str(uuid4()) await handler("test_sid", {"game_id": game_id, "advance": "yes"}) mocks["manager"].emit_to_user.assert_called() call_args = mocks["manager"].emit_to_user.call_args[0] assert call_args[1] == "error" @pytest.mark.asyncio async def test_successful_submission(self, sio_with_mocks, mock_game_state): """Handler calls game_engine and broadcasts state on success.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_lead_advance") game_id = str(mock_game_state.game_id) mocks["game_engine"].submit_uncapped_lead_advance = AsyncMock() mocks["state_manager"].get_state = MagicMock(return_value=mock_game_state) await handler("test_sid", {"game_id": game_id, "advance": True}) mocks["game_engine"].submit_uncapped_lead_advance.assert_called_once_with( mock_game_state.game_id, True ) mocks["manager"].broadcast_to_game.assert_called_once() @pytest.mark.asyncio async def test_value_error_from_engine(self, sio_with_mocks): """Handler emits error when game engine raises ValueError.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_lead_advance") game_id = str(uuid4()) mocks["game_engine"].submit_uncapped_lead_advance = AsyncMock( side_effect=ValueError("Wrong phase") ) await handler("test_sid", {"game_id": game_id, "advance": True}) mocks["manager"].emit_to_user.assert_called() call_args = mocks["manager"].emit_to_user.call_args[0] assert call_args[1] == "error" assert "Wrong phase" in call_args[2]["message"] # ============================================================================= # Tests: submit_uncapped_defensive_throw # ============================================================================= class TestSubmitUncappedDefensiveThrow: """Tests for the submit_uncapped_defensive_throw WebSocket handler.""" @pytest.mark.asyncio async def test_missing_game_id(self, sio_with_mocks): """Handler emits error when game_id is not provided.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_defensive_throw") await handler("test_sid", {"will_throw": True}) mocks["manager"].emit_to_user.assert_called_once() call_args = mocks["manager"].emit_to_user.call_args[0] assert call_args[1] == "error" @pytest.mark.asyncio async def test_missing_will_throw_field(self, sio_with_mocks): """Handler emits error when will_throw field is missing.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_defensive_throw") game_id = str(uuid4()) await handler("test_sid", {"game_id": game_id}) mocks["manager"].emit_to_user.assert_called() call_args = mocks["manager"].emit_to_user.call_args[0] assert "will_throw" in call_args[2]["message"].lower() @pytest.mark.asyncio async def test_successful_submission(self, sio_with_mocks, mock_game_state): """Handler calls game_engine and broadcasts state on success.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_defensive_throw") game_id = str(mock_game_state.game_id) mocks["game_engine"].submit_uncapped_defensive_throw = AsyncMock() mocks["state_manager"].get_state = MagicMock(return_value=mock_game_state) await handler("test_sid", {"game_id": game_id, "will_throw": False}) mocks["game_engine"].submit_uncapped_defensive_throw.assert_called_once_with( mock_game_state.game_id, False ) mocks["manager"].broadcast_to_game.assert_called_once() # ============================================================================= # Tests: submit_uncapped_trail_advance # ============================================================================= class TestSubmitUncappedTrailAdvance: """Tests for the submit_uncapped_trail_advance WebSocket handler.""" @pytest.mark.asyncio async def test_missing_game_id(self, sio_with_mocks): """Handler emits error when game_id is not provided.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_trail_advance") await handler("test_sid", {"advance": True}) mocks["manager"].emit_to_user.assert_called_once() @pytest.mark.asyncio async def test_missing_advance_field(self, sio_with_mocks): """Handler emits error when advance field is missing.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_trail_advance") game_id = str(uuid4()) await handler("test_sid", {"game_id": game_id}) mocks["manager"].emit_to_user.assert_called() call_args = mocks["manager"].emit_to_user.call_args[0] assert "advance" in call_args[2]["message"].lower() @pytest.mark.asyncio async def test_successful_submission(self, sio_with_mocks, mock_game_state): """Handler calls game_engine and broadcasts state on success.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_trail_advance") game_id = str(mock_game_state.game_id) mocks["game_engine"].submit_uncapped_trail_advance = AsyncMock() mocks["state_manager"].get_state = MagicMock(return_value=mock_game_state) await handler("test_sid", {"game_id": game_id, "advance": True}) mocks["game_engine"].submit_uncapped_trail_advance.assert_called_once_with( mock_game_state.game_id, True ) # ============================================================================= # Tests: submit_uncapped_throw_target # ============================================================================= class TestSubmitUncappedThrowTarget: """Tests for the submit_uncapped_throw_target WebSocket handler.""" @pytest.mark.asyncio async def test_missing_game_id(self, sio_with_mocks): """Handler emits error when game_id is not provided.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_throw_target") await handler("test_sid", {"target": "lead"}) mocks["manager"].emit_to_user.assert_called_once() @pytest.mark.asyncio async def test_invalid_target(self, sio_with_mocks): """Handler emits error when target is not 'lead' or 'trail'.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_throw_target") game_id = str(uuid4()) await handler("test_sid", {"game_id": game_id, "target": "middle"}) mocks["manager"].emit_to_user.assert_called() call_args = mocks["manager"].emit_to_user.call_args[0] assert call_args[1] == "error" assert "lead" in call_args[2]["message"] or "trail" in call_args[2]["message"] @pytest.mark.asyncio async def test_missing_target(self, sio_with_mocks): """Handler emits error when target field is missing.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_throw_target") game_id = str(uuid4()) await handler("test_sid", {"game_id": game_id}) mocks["manager"].emit_to_user.assert_called() @pytest.mark.asyncio async def test_successful_submission_lead(self, sio_with_mocks, mock_game_state): """Handler calls game_engine with target='lead' and broadcasts state.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_throw_target") game_id = str(mock_game_state.game_id) mocks["game_engine"].submit_uncapped_throw_target = AsyncMock() mocks["state_manager"].get_state = MagicMock(return_value=mock_game_state) await handler("test_sid", {"game_id": game_id, "target": "lead"}) mocks["game_engine"].submit_uncapped_throw_target.assert_called_once_with( mock_game_state.game_id, "lead" ) @pytest.mark.asyncio async def test_successful_submission_trail(self, sio_with_mocks, mock_game_state): """Handler calls game_engine with target='trail' and broadcasts state.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_throw_target") game_id = str(mock_game_state.game_id) mocks["game_engine"].submit_uncapped_throw_target = AsyncMock() mocks["state_manager"].get_state = MagicMock(return_value=mock_game_state) await handler("test_sid", {"game_id": game_id, "target": "trail"}) mocks["game_engine"].submit_uncapped_throw_target.assert_called_once_with( mock_game_state.game_id, "trail" ) # ============================================================================= # Tests: submit_uncapped_safe_out # ============================================================================= class TestSubmitUncappedSafeOut: """Tests for the submit_uncapped_safe_out WebSocket handler.""" @pytest.mark.asyncio async def test_missing_game_id(self, sio_with_mocks): """Handler emits error when game_id is not provided.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_safe_out") await handler("test_sid", {"result": "safe"}) mocks["manager"].emit_to_user.assert_called_once() @pytest.mark.asyncio async def test_invalid_result(self, sio_with_mocks): """Handler emits error when result is not 'safe' or 'out'.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_safe_out") game_id = str(uuid4()) await handler("test_sid", {"game_id": game_id, "result": "maybe"}) mocks["manager"].emit_to_user.assert_called() call_args = mocks["manager"].emit_to_user.call_args[0] assert call_args[1] == "error" @pytest.mark.asyncio async def test_missing_result(self, sio_with_mocks): """Handler emits error when result field is missing.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_safe_out") game_id = str(uuid4()) await handler("test_sid", {"game_id": game_id}) mocks["manager"].emit_to_user.assert_called() @pytest.mark.asyncio async def test_successful_safe(self, sio_with_mocks, mock_game_state): """Handler calls game_engine with result='safe' and broadcasts state.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_safe_out") game_id = str(mock_game_state.game_id) mocks["game_engine"].submit_uncapped_safe_out = AsyncMock() mocks["state_manager"].get_state = MagicMock(return_value=mock_game_state) await handler("test_sid", {"game_id": game_id, "result": "safe"}) mocks["game_engine"].submit_uncapped_safe_out.assert_called_once_with( mock_game_state.game_id, "safe" ) mocks["manager"].broadcast_to_game.assert_called_once() @pytest.mark.asyncio async def test_successful_out(self, sio_with_mocks, mock_game_state): """Handler calls game_engine with result='out' and broadcasts state.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_safe_out") game_id = str(mock_game_state.game_id) mocks["game_engine"].submit_uncapped_safe_out = AsyncMock() mocks["state_manager"].get_state = MagicMock(return_value=mock_game_state) await handler("test_sid", {"game_id": game_id, "result": "out"}) mocks["game_engine"].submit_uncapped_safe_out.assert_called_once_with( mock_game_state.game_id, "out" ) @pytest.mark.asyncio async def test_value_error_propagation(self, sio_with_mocks): """Handler emits error when game engine raises ValueError.""" sio, mocks = sio_with_mocks handler = get_handler(sio, "submit_uncapped_safe_out") game_id = str(uuid4()) mocks["game_engine"].submit_uncapped_safe_out = AsyncMock( side_effect=ValueError("No pending uncapped hit") ) await handler("test_sid", {"game_id": game_id, "result": "safe"}) mocks["manager"].emit_to_user.assert_called() call_args = mocks["manager"].emit_to_user.call_args[0] assert "No pending uncapped hit" in call_args[2]["message"]