""" Tests for WebSocket query and decision handlers. Verifies lineup retrieval (get_lineup), box score (get_box_score), and strategic decision submission handlers (submit_defensive_decision, submit_offensive_decision). Author: Claude Date: 2025-01-27 """ import pytest from uuid import uuid4 from unittest.mock import AsyncMock, MagicMock, patch # ============================================================================ # GET LINEUP HANDLER TESTS # ============================================================================ class TestGetLineupHandler: """Tests for the get_lineup event handler.""" @pytest.mark.asyncio async def test_get_lineup_from_cache(self, mock_manager, mock_lineup_state): """ Verify get_lineup() returns cached lineup when available. StateManager caches lineups for fast O(1) lookup. When lineup is in cache, handler should return it without hitting database. """ from socketio import AsyncServer sio = AsyncServer() game_id = uuid4() with patch("app.websocket.handlers.state_manager") as mock_state_mgr: mock_state_mgr.get_lineup.return_value = mock_lineup_state from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["get_lineup"] await handler("test_sid", {"game_id": str(game_id), "team_id": 1}) mock_state_mgr.get_lineup.assert_called_once_with(game_id, 1) mock_manager.emit_to_user.assert_called_once() call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "lineup_data" assert call_args[0][2]["team_id"] == 1 @pytest.mark.asyncio async def test_get_lineup_from_db_when_not_cached( self, mock_manager, mock_lineup_state, mock_game_state ): """ Verify get_lineup() loads from database when not in cache. When lineup is not cached, handler should load from database via lineup_service and cache the result. """ from socketio import AsyncServer sio = AsyncServer() game_id = uuid4() with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \ patch("app.websocket.handlers.lineup_service") as mock_lineup_svc: mock_state_mgr.get_lineup.return_value = None # Not cached mock_state_mgr.get_state.return_value = mock_game_state mock_lineup_svc.load_team_lineup_with_player_data = AsyncMock( return_value=mock_lineup_state ) from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["get_lineup"] await handler("test_sid", {"game_id": str(game_id), "team_id": 1}) mock_lineup_svc.load_team_lineup_with_player_data.assert_called_once() mock_state_mgr.set_lineup.assert_called_once() # Cache it @pytest.mark.asyncio async def test_get_lineup_missing_game_id(self, mock_manager): """ Verify get_lineup() returns error when game_id missing. """ from socketio import AsyncServer sio = AsyncServer() from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["get_lineup"] await handler("test_sid", {"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] == "error" assert "game_id" in call_args[0][2]["message"].lower() @pytest.mark.asyncio async def test_get_lineup_missing_team_id(self, mock_manager): """ Verify get_lineup() returns error when team_id missing. """ from socketio import AsyncServer sio = AsyncServer() from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["get_lineup"] await handler("test_sid", {"game_id": str(uuid4())}) mock_manager.emit_to_user.assert_called_once() call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "error" assert "team_id" in call_args[0][2]["message"].lower() # ============================================================================ # GET BOX SCORE HANDLER TESTS # ============================================================================ class TestGetBoxScoreHandler: """Tests for the get_box_score event handler.""" @pytest.mark.asyncio async def test_get_box_score_success(self, mock_manager): """ Verify get_box_score() returns box score data from service. Handler should fetch box score from materialized views via box_score_service and emit to requester. """ from socketio import AsyncServer sio = AsyncServer() game_id = uuid4() mock_box_score = { "home_team": {"runs": 5, "hits": 10, "errors": 1}, "away_team": {"runs": 3, "hits": 7, "errors": 2}, } # box_score_service is imported inside the handler via: # from app.services import box_score_service with patch("app.services.box_score_service") as mock_service: mock_service.get_box_score = AsyncMock(return_value=mock_box_score) from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["get_box_score"] await handler("test_sid", {"game_id": str(game_id)}) mock_service.get_box_score.assert_called_once_with(game_id) mock_manager.emit_to_user.assert_called_once() call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "box_score_data" assert call_args[0][2]["box_score"] == mock_box_score @pytest.mark.asyncio async def test_get_box_score_not_found(self, mock_manager): """ Verify get_box_score() returns error when no data found. When materialized views return no data (e.g., game hasn't started), handler should emit error with helpful hint about migrations. """ from socketio import AsyncServer sio = AsyncServer() # box_score_service is imported inside the handler with patch("app.services.box_score_service") as mock_service: mock_service.get_box_score = AsyncMock(return_value=None) from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["get_box_score"] await handler("test_sid", {"game_id": str(uuid4())}) mock_manager.emit_to_user.assert_called_once() call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "error" @pytest.mark.asyncio async def test_get_box_score_missing_game_id(self, mock_manager): """ Verify get_box_score() returns error when game_id missing. """ from socketio import AsyncServer sio = AsyncServer() from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["get_box_score"] await handler("test_sid", {}) mock_manager.emit_to_user.assert_called_once() call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "error" # ============================================================================ # SUBMIT DEFENSIVE DECISION HANDLER TESTS # ============================================================================ class TestSubmitDefensiveDecisionHandler: """Tests for the submit_defensive_decision event handler.""" @pytest.mark.asyncio async def test_submit_defensive_decision_success( self, mock_manager, mock_game_state ): """ Verify submit_defensive_decision() processes and broadcasts decision. Handler should create DefensiveDecision, submit to game engine, and broadcast to game room so both teams see the defense strategy. Valid values: - infield_depth: infield_in, normal, corners_in - outfield_depth: normal, shallow """ from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \ patch("app.websocket.handlers.game_engine") as mock_engine: mock_state_mgr.get_state.return_value = mock_game_state updated_state = MagicMock() updated_state.pending_decision = None mock_engine.submit_defensive_decision = AsyncMock(return_value=updated_state) from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["submit_defensive_decision"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "infield_depth": "infield_in", # Valid: infield_in, normal, corners_in "outfield_depth": "shallow", # Valid: normal, shallow "hold_runners": [1], }, ) mock_engine.submit_defensive_decision.assert_called_once() mock_manager.broadcast_to_game.assert_called_once() call_args = mock_manager.broadcast_to_game.call_args assert call_args[0][1] == "defensive_decision_submitted" assert call_args[0][2]["decision"]["infield_depth"] == "infield_in" @pytest.mark.asyncio async def test_submit_defensive_decision_uses_defaults( self, mock_manager, mock_game_state ): """ Verify submit_defensive_decision() uses default values when not provided. Handler should default to normal alignment, normal depth, no hold runners when these fields are omitted from the request. """ from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \ patch("app.websocket.handlers.game_engine") as mock_engine: mock_state_mgr.get_state.return_value = mock_game_state updated_state = MagicMock() updated_state.pending_decision = None mock_engine.submit_defensive_decision = AsyncMock(return_value=updated_state) from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["submit_defensive_decision"] await handler("test_sid", {"game_id": str(mock_game_state.game_id)}) call_args = mock_manager.broadcast_to_game.call_args decision = call_args[0][2]["decision"] assert decision["alignment"] == "normal" assert decision["infield_depth"] == "normal" assert decision["outfield_depth"] == "normal" assert decision["hold_runners"] == [] @pytest.mark.asyncio async def test_submit_defensive_decision_game_not_found(self, mock_manager): """ Verify submit_defensive_decision() returns error when game not found. """ 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["/"]["submit_defensive_decision"] await handler("test_sid", {"game_id": str(uuid4())}) mock_manager.emit_to_user.assert_called_once() call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "error" assert "not found" in call_args[0][2]["message"].lower() # ============================================================================ # SUBMIT OFFENSIVE DECISION HANDLER TESTS # ============================================================================ class TestSubmitOffensiveDecisionHandler: """Tests for the submit_offensive_decision event handler.""" @pytest.mark.asyncio async def test_submit_offensive_decision_success( self, mock_manager, mock_game_state ): """ Verify submit_offensive_decision() processes and broadcasts decision. Handler should create OffensiveDecision with action and steal attempts, submit to game engine, and broadcast to game room. """ from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \ patch("app.websocket.handlers.game_engine") as mock_engine: mock_state_mgr.get_state.return_value = mock_game_state updated_state = MagicMock() updated_state.pending_decision = None mock_engine.submit_offensive_decision = AsyncMock(return_value=updated_state) from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["submit_offensive_decision"] await handler( "test_sid", { "game_id": str(mock_game_state.game_id), "action": "steal", "steal_attempts": [2], }, ) mock_engine.submit_offensive_decision.assert_called_once() mock_manager.broadcast_to_game.assert_called_once() call_args = mock_manager.broadcast_to_game.call_args assert call_args[0][1] == "offensive_decision_submitted" assert call_args[0][2]["decision"]["action"] == "steal" assert call_args[0][2]["decision"]["steal_attempts"] == [2] @pytest.mark.asyncio async def test_submit_offensive_decision_defaults_to_swing_away( self, mock_manager, mock_game_state ): """ Verify submit_offensive_decision() defaults to swing_away action. When no action is provided, handler should default to swing_away which is the most common offensive approach. """ from socketio import AsyncServer sio = AsyncServer() with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \ patch("app.websocket.handlers.game_engine") as mock_engine: mock_state_mgr.get_state.return_value = mock_game_state updated_state = MagicMock() updated_state.pending_decision = None mock_engine.submit_offensive_decision = AsyncMock(return_value=updated_state) from app.websocket.handlers import register_handlers register_handlers(sio, mock_manager) handler = sio.handlers["/"]["submit_offensive_decision"] await handler("test_sid", {"game_id": str(mock_game_state.game_id)}) call_args = mock_manager.broadcast_to_game.call_args decision = call_args[0][2]["decision"] assert decision["action"] == "swing_away" assert decision["steal_attempts"] == [] @pytest.mark.asyncio async def test_submit_offensive_decision_game_not_found(self, mock_manager): """ Verify submit_offensive_decision() returns error when game not found. """ 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["/"]["submit_offensive_decision"] await handler("test_sid", {"game_id": str(uuid4())}) mock_manager.emit_to_user.assert_called_once() call_args = mock_manager.emit_to_user.call_args assert call_args[0][1] == "error" assert "not found" in call_args[0][2]["message"].lower()