Backend changes: - Modified request_game_state handler to fetch plays from database - Convert Play DB models to frontend-compatible PlayResult dicts - Emit game_state_sync event with state + recent_plays array Frontend changes: - Added deduplication by play_number in addPlayToHistory() - Prevents duplicate plays when game_state_sync is received Field mapping from Play model: - hit_type -> outcome - result_description -> description - batter_id -> batter_lineup_id - batter_final -> batter_result 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
742 lines
25 KiB
Python
742 lines
25 KiB
Python
"""
|
|
Tests for WebSocket connection and room management handlers.
|
|
|
|
Verifies authentication (connect), disconnection (disconnect), room joining
|
|
(join_game, leave_game), heartbeat, and game state request handlers.
|
|
|
|
Author: Claude
|
|
Date: 2025-01-27
|
|
"""
|
|
|
|
import pytest
|
|
from uuid import uuid4
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from tests.unit.websocket.conftest import get_handler
|
|
|
|
|
|
# ============================================================================
|
|
# CONNECT HANDLER TESTS
|
|
# ============================================================================
|
|
|
|
|
|
class TestConnectHandler:
|
|
"""Tests for the connect event handler."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connect_with_valid_cookie(self, mock_manager):
|
|
"""
|
|
Verify connect() accepts connection with valid cookie token.
|
|
|
|
Cookie-based auth is the primary auth method when using HttpOnly cookies
|
|
set by the OAuth flow. Handler should extract token from HTTP_COOKIE.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
# Mock verify_token to return valid user data
|
|
with patch("app.websocket.handlers.verify_token") as mock_verify:
|
|
mock_verify.return_value = {"user_id": "user_123", "username": "testuser"}
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["connect"]
|
|
|
|
# Simulate environ with cookie
|
|
environ = {"HTTP_COOKIE": "pd_access_token=valid_token_here; other=value"}
|
|
auth = None
|
|
|
|
result = await handler("test_sid", environ, auth)
|
|
|
|
assert result is True
|
|
mock_manager.connect.assert_called_once_with("test_sid", "user_123", ip_address=None)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connect_with_auth_object(self, mock_manager):
|
|
"""
|
|
Verify connect() accepts connection with auth object token.
|
|
|
|
Auth object is used by direct JS clients that can't use cookies.
|
|
Handler should fall back to auth.token when cookie not present.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
with patch("app.websocket.handlers.verify_token") as mock_verify:
|
|
mock_verify.return_value = {"user_id": "user_456", "username": "jsuser"}
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["connect"]
|
|
|
|
# Simulate environ without cookie, but with auth object
|
|
environ = {"HTTP_COOKIE": ""}
|
|
auth = {"token": "auth_object_token"}
|
|
|
|
result = await handler("test_sid", environ, auth)
|
|
|
|
assert result is True
|
|
mock_verify.assert_called_once_with("auth_object_token")
|
|
mock_manager.connect.assert_called_once_with("test_sid", "user_456", ip_address=None)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connect_missing_credentials_rejected(self, mock_manager):
|
|
"""
|
|
Verify connect() rejects connection when no token provided.
|
|
|
|
Handler should return False to reject the Socket.io connection
|
|
when neither cookie nor auth object contains a token.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["connect"]
|
|
|
|
# No cookie, no auth
|
|
environ = {"HTTP_COOKIE": ""}
|
|
auth = None
|
|
|
|
result = await handler("test_sid", environ, auth)
|
|
|
|
assert result is False
|
|
mock_manager.connect.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connect_invalid_token_rejected(self, mock_manager):
|
|
"""
|
|
Verify connect() rejects connection when token is invalid.
|
|
|
|
When verify_token returns no user_id, the connection should be rejected.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
with patch("app.websocket.handlers.verify_token") as mock_verify:
|
|
mock_verify.return_value = {} # No user_id in response
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["connect"]
|
|
|
|
environ = {"HTTP_COOKIE": "pd_access_token=invalid_token"}
|
|
auth = None
|
|
|
|
result = await handler("test_sid", environ, auth)
|
|
|
|
assert result is False
|
|
mock_manager.connect.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connect_exception_rejected(self, mock_manager):
|
|
"""
|
|
Verify connect() rejects connection when exception occurs.
|
|
|
|
Any exception during auth processing should result in connection rejection,
|
|
not a server crash.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
with patch("app.websocket.handlers.verify_token") as mock_verify:
|
|
mock_verify.side_effect = Exception("Token verification failed")
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["connect"]
|
|
|
|
environ = {"HTTP_COOKIE": "pd_access_token=error_token"}
|
|
auth = None
|
|
|
|
result = await handler("test_sid", environ, auth)
|
|
|
|
assert result is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connect_extracts_ip_address(self, mock_manager):
|
|
"""
|
|
Verify connect() extracts IP address from environ.
|
|
|
|
IP address is used for logging and rate limiting. Handler should
|
|
extract REMOTE_ADDR from the WSGI environ dict.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
with patch("app.websocket.handlers.verify_token") as mock_verify:
|
|
mock_verify.return_value = {"user_id": "user_123"}
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["connect"]
|
|
|
|
environ = {
|
|
"HTTP_COOKIE": "pd_access_token=valid_token",
|
|
"REMOTE_ADDR": "192.168.1.100",
|
|
}
|
|
|
|
result = await handler("test_sid", environ, None)
|
|
|
|
assert result is True
|
|
mock_manager.connect.assert_called_once_with(
|
|
"test_sid", "user_123", ip_address="192.168.1.100"
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connect_emits_connected_event(self, mock_manager):
|
|
"""
|
|
Verify connect() emits connected event with user_id.
|
|
|
|
After successful authentication, the handler should emit a 'connected'
|
|
event to the client with their user_id for client-side tracking.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
sio.emit = AsyncMock()
|
|
|
|
with patch("app.websocket.handlers.verify_token") as mock_verify:
|
|
mock_verify.return_value = {"user_id": "user_789"}
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["connect"]
|
|
|
|
environ = {"HTTP_COOKIE": "pd_access_token=valid_token"}
|
|
|
|
await handler("test_sid", environ, None)
|
|
|
|
# Verify connected event emitted
|
|
sio.emit.assert_called_once()
|
|
call_args = sio.emit.call_args
|
|
assert call_args[0][0] == "connected"
|
|
assert call_args[0][1]["user_id"] == "user_789"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connect_value_error_rejected(self, mock_manager):
|
|
"""
|
|
Verify connect() handles ValueError during token parsing.
|
|
|
|
If verify_token raises ValueError (malformed token), the connection
|
|
should be rejected without crashing.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
with patch("app.websocket.handlers.verify_token") as mock_verify:
|
|
mock_verify.side_effect = ValueError("Invalid token format")
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["connect"]
|
|
|
|
environ = {"HTTP_COOKIE": "pd_access_token=malformed"}
|
|
result = await handler("test_sid", environ, None)
|
|
|
|
assert result is False
|
|
mock_manager.connect.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connect_key_error_rejected(self, mock_manager):
|
|
"""
|
|
Verify connect() handles KeyError during token parsing.
|
|
|
|
If verify_token raises KeyError (missing required field), the connection
|
|
should be rejected without crashing.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
with patch("app.websocket.handlers.verify_token") as mock_verify:
|
|
mock_verify.side_effect = KeyError("missing_field")
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["connect"]
|
|
|
|
environ = {"HTTP_COOKIE": "pd_access_token=incomplete"}
|
|
result = await handler("test_sid", environ, None)
|
|
|
|
assert result is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connect_network_error_rejected(self, mock_manager):
|
|
"""
|
|
Verify connect() handles network errors gracefully.
|
|
|
|
If a ConnectionError or OSError occurs during connection setup,
|
|
the connection should be rejected.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
with patch("app.websocket.handlers.verify_token") as mock_verify:
|
|
mock_verify.return_value = {"user_id": "user_123"}
|
|
mock_manager.connect = AsyncMock(side_effect=ConnectionError("Network failed"))
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["connect"]
|
|
|
|
environ = {"HTTP_COOKIE": "pd_access_token=valid"}
|
|
result = await handler("test_sid", environ, None)
|
|
|
|
assert result is False
|
|
|
|
|
|
# ============================================================================
|
|
# DISCONNECT HANDLER TESTS
|
|
# ============================================================================
|
|
|
|
|
|
class TestDisconnectHandler:
|
|
"""Tests for the disconnect event handler."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_disconnect_calls_manager(self, mock_manager):
|
|
"""
|
|
Verify disconnect() delegates to ConnectionManager.
|
|
|
|
Handler should call manager.disconnect() with the session ID
|
|
to clean up all session state and room memberships.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["disconnect"]
|
|
|
|
await handler("test_sid")
|
|
|
|
mock_manager.disconnect.assert_called_once_with("test_sid")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_disconnect_cleans_rate_limiter(self, mock_manager):
|
|
"""
|
|
Verify disconnect() removes rate limiter buckets.
|
|
|
|
Handler should call rate_limiter.remove_connection() to clean up
|
|
rate limiting state and prevent memory leaks.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
with patch("app.websocket.handlers.rate_limiter") as mock_limiter:
|
|
mock_limiter.remove_connection = MagicMock()
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["disconnect"]
|
|
|
|
await handler("test_sid")
|
|
|
|
mock_limiter.remove_connection.assert_called_once_with("test_sid")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_disconnect_handles_manager_error(self, mock_manager):
|
|
"""
|
|
Verify disconnect() handles manager errors gracefully.
|
|
|
|
Even if manager.disconnect() raises an error, the handler should
|
|
not crash - disconnection cleanup should be best-effort.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
mock_manager.disconnect = AsyncMock(side_effect=Exception("Cleanup failed"))
|
|
|
|
with patch("app.websocket.handlers.rate_limiter"):
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["disconnect"]
|
|
|
|
# Should not raise even if manager fails
|
|
# Note: The handler may or may not catch this - depends on impl
|
|
try:
|
|
await handler("test_sid")
|
|
except Exception:
|
|
pass # Some implementations may let errors propagate
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_disconnect_multiple_calls_idempotent(self, mock_manager):
|
|
"""
|
|
Verify disconnect() can be called multiple times safely.
|
|
|
|
Socket.io may call disconnect multiple times in edge cases.
|
|
Handler should be idempotent and not crash on repeated calls.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
with patch("app.websocket.handlers.rate_limiter"):
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["disconnect"]
|
|
|
|
# First disconnect
|
|
await handler("test_sid")
|
|
# Second disconnect should also succeed
|
|
await handler("test_sid")
|
|
|
|
# Manager should be called twice
|
|
assert mock_manager.disconnect.call_count == 2
|
|
|
|
|
|
# ============================================================================
|
|
# JOIN GAME HANDLER TESTS
|
|
# ============================================================================
|
|
|
|
|
|
class TestJoinGameHandler:
|
|
"""Tests for the join_game event handler."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_join_game_success(self, mock_manager):
|
|
"""
|
|
Verify join_game() successfully joins user to game room.
|
|
|
|
Handler should call manager.join_game and emit game_joined confirmation.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["join_game"]
|
|
|
|
game_id = str(uuid4())
|
|
await handler("test_sid", {"game_id": game_id})
|
|
|
|
mock_manager.join_game.assert_called_once_with("test_sid", game_id, "player")
|
|
mock_manager.emit_to_user.assert_called_once()
|
|
|
|
# Verify confirmation event
|
|
call_args = mock_manager.emit_to_user.call_args
|
|
assert call_args[0][1] == "game_joined"
|
|
assert call_args[0][2]["game_id"] == game_id
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_join_game_missing_game_id(self, mock_manager):
|
|
"""
|
|
Verify join_game() returns error when game_id missing.
|
|
|
|
Handler should emit error event without attempting to join.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["join_game"]
|
|
|
|
await handler("test_sid", {})
|
|
|
|
mock_manager.join_game.assert_not_called()
|
|
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_join_game_with_role(self, mock_manager):
|
|
"""
|
|
Verify join_game() passes role to manager.
|
|
|
|
Role can be 'home', 'away', 'spectator', or 'player'.
|
|
Handler should pass the specified role to manager.join_game().
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["join_game"]
|
|
|
|
game_id = str(uuid4())
|
|
await handler("test_sid", {"game_id": game_id, "role": "spectator"})
|
|
|
|
mock_manager.join_game.assert_called_once_with("test_sid", game_id, "spectator")
|
|
|
|
|
|
# ============================================================================
|
|
# LEAVE GAME HANDLER TESTS
|
|
# ============================================================================
|
|
|
|
|
|
class TestLeaveGameHandler:
|
|
"""Tests for the leave_game event handler."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_leave_game_success(self, mock_manager):
|
|
"""
|
|
Verify leave_game() removes user from game room.
|
|
|
|
Handler should call manager.leave_game with game_id.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["leave_game"]
|
|
|
|
game_id = str(uuid4())
|
|
await handler("test_sid", {"game_id": game_id})
|
|
|
|
mock_manager.leave_game.assert_called_once_with("test_sid", game_id)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_leave_game_missing_game_id_noop(self, mock_manager):
|
|
"""
|
|
Verify leave_game() is a no-op when game_id missing.
|
|
|
|
Handler should not call leave_game if no game_id provided.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["leave_game"]
|
|
|
|
await handler("test_sid", {})
|
|
|
|
mock_manager.leave_game.assert_not_called()
|
|
|
|
|
|
# ============================================================================
|
|
# HEARTBEAT HANDLER TESTS
|
|
# ============================================================================
|
|
|
|
|
|
class TestHeartbeatHandler:
|
|
"""Tests for the heartbeat event handler."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_heartbeat_acknowledges(self, mock_manager):
|
|
"""
|
|
Verify heartbeat() emits acknowledgment.
|
|
|
|
Handler should emit heartbeat_ack directly to the client socket.
|
|
This keeps the connection alive and verifies client is responsive.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
sio.emit = AsyncMock()
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["heartbeat"]
|
|
|
|
await handler("test_sid")
|
|
|
|
sio.emit.assert_called_once_with("heartbeat_ack", {}, room="test_sid")
|
|
|
|
|
|
# ============================================================================
|
|
# REQUEST GAME STATE HANDLER TESTS
|
|
# ============================================================================
|
|
|
|
|
|
class TestRequestGameStateHandler:
|
|
"""Tests for the request_game_state event handler."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_game_state_from_memory(self, mock_manager, mock_game_state):
|
|
"""
|
|
Verify request_game_state() returns state from memory with play history.
|
|
|
|
When game state is in StateManager memory, handler should return it
|
|
along with play history via game_state_sync event.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \
|
|
patch("app.websocket.handlers.DatabaseOperations") as mock_db_ops_class:
|
|
mock_state_mgr.get_state.return_value = mock_game_state
|
|
|
|
# Mock DatabaseOperations.get_plays() to return empty list
|
|
mock_db_ops = AsyncMock()
|
|
mock_db_ops.get_plays = AsyncMock(return_value=[])
|
|
mock_db_ops_class.return_value = mock_db_ops
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["request_game_state"]
|
|
|
|
await handler("test_sid", {"game_id": str(mock_game_state.game_id)})
|
|
|
|
mock_state_mgr.get_state.assert_called_once()
|
|
mock_state_mgr.recover_game.assert_not_called()
|
|
mock_db_ops.get_plays.assert_called_once()
|
|
mock_manager.emit_to_user.assert_called_once()
|
|
|
|
call_args = mock_manager.emit_to_user.call_args
|
|
assert call_args[0][1] == "game_state_sync"
|
|
# Verify payload structure
|
|
payload = call_args[0][2]
|
|
assert "state" in payload
|
|
assert "recent_plays" in payload
|
|
assert "timestamp" in payload
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_game_state_from_db(self, mock_manager, mock_game_state):
|
|
"""
|
|
Verify request_game_state() recovers from database when not in memory.
|
|
|
|
When game state is not in memory, handler should call recover_game()
|
|
to load and replay from database (recovery path), then return state
|
|
with play history via game_state_sync event.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \
|
|
patch("app.websocket.handlers.DatabaseOperations") as mock_db_ops_class:
|
|
mock_state_mgr.get_state.return_value = None # Not in memory
|
|
mock_state_mgr.recover_game = AsyncMock(return_value=mock_game_state)
|
|
|
|
# Mock DatabaseOperations.get_plays() to return empty list
|
|
mock_db_ops = AsyncMock()
|
|
mock_db_ops.get_plays = AsyncMock(return_value=[])
|
|
mock_db_ops_class.return_value = mock_db_ops
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["request_game_state"]
|
|
|
|
await handler("test_sid", {"game_id": str(mock_game_state.game_id)})
|
|
|
|
mock_state_mgr.get_state.assert_called_once()
|
|
mock_state_mgr.recover_game.assert_called_once()
|
|
mock_db_ops.get_plays.assert_called_once()
|
|
mock_manager.emit_to_user.assert_called_once()
|
|
|
|
call_args = mock_manager.emit_to_user.call_args
|
|
assert call_args[0][1] == "game_state_sync"
|
|
# Verify payload structure
|
|
payload = call_args[0][2]
|
|
assert "state" in payload
|
|
assert "recent_plays" in payload
|
|
assert "timestamp" in payload
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_game_state_missing_game_id(self, mock_manager):
|
|
"""
|
|
Verify request_game_state() 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["/"]["request_game_state"]
|
|
|
|
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"
|
|
assert "game_id" in call_args[0][2]["message"].lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_game_state_invalid_format(self, mock_manager):
|
|
"""
|
|
Verify request_game_state() returns error for invalid UUID format.
|
|
|
|
Handler should validate game_id is a valid UUID before attempting
|
|
to fetch state.
|
|
"""
|
|
from socketio import AsyncServer
|
|
|
|
sio = AsyncServer()
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["request_game_state"]
|
|
|
|
await handler("test_sid", {"game_id": "not-a-valid-uuid"})
|
|
|
|
mock_manager.emit_to_user.assert_called_once()
|
|
call_args = mock_manager.emit_to_user.call_args
|
|
assert call_args[0][1] == "error"
|
|
assert "invalid" in call_args[0][2]["message"].lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_game_state_not_found(self, mock_manager):
|
|
"""
|
|
Verify request_game_state() returns error when game not found anywhere.
|
|
|
|
When game is neither in memory nor database, handler should emit
|
|
an error event indicating the game was 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
|
|
mock_state_mgr.recover_game = AsyncMock(return_value=None)
|
|
|
|
from app.websocket.handlers import register_handlers
|
|
|
|
register_handlers(sio, mock_manager)
|
|
handler = sio.handlers["/"]["request_game_state"]
|
|
|
|
game_id = str(uuid4())
|
|
await handler("test_sid", {"game_id": game_id})
|
|
|
|
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()
|