"""Tests for socket service.""" from unittest.mock import AsyncMock, patch import pytest import socketio from app.services.socket import SocketManager class TestSocketManager: """Test socket manager service.""" @pytest.fixture def socket_manager(self): """Create a fresh socket manager for testing.""" return SocketManager() @pytest.fixture def mock_sio(self, socket_manager): """Mock the socket.io server.""" socket_manager.sio = AsyncMock(spec=socketio.AsyncServer) return socket_manager.sio def test_init_creates_socket_server(self): """Test that socket manager initializes with proper configuration.""" manager = SocketManager() assert manager.sio is not None assert isinstance(manager.user_rooms, dict) assert isinstance(manager.socket_users, dict) assert len(manager.user_rooms) == 0 assert len(manager.socket_users) == 0 @pytest.mark.asyncio async def test_send_to_user_success(self, socket_manager, mock_sio): """Test sending message to connected user.""" user_id = "123" room_id = "user_123" socket_manager.user_rooms[user_id] = room_id event = "test_event" data = {"message": "hello"} result = await socket_manager.send_to_user(user_id, event, data) assert result is True mock_sio.emit.assert_called_once_with(event, data, room=room_id) @pytest.mark.asyncio async def test_send_to_user_not_connected(self, socket_manager, mock_sio): """Test sending message to user who is not connected.""" user_id = "999" event = "test_event" data = {"message": "hello"} result = await socket_manager.send_to_user(user_id, event, data) assert result is False mock_sio.emit.assert_not_called() @pytest.mark.asyncio async def test_broadcast_to_all(self, socket_manager, mock_sio): """Test broadcasting message to all users.""" event = "broadcast_event" data = {"message": "announcement"} await socket_manager.broadcast_to_all(event, data) mock_sio.emit.assert_called_once_with(event, data) def test_get_connected_users(self, socket_manager): """Test getting list of connected users.""" # Add some users socket_manager.user_rooms["1"] = "user_1" socket_manager.user_rooms["2"] = "user_2" socket_manager.user_rooms["3"] = "user_3" connected_users = socket_manager.get_connected_users() assert len(connected_users) == 3 assert "1" in connected_users assert "2" in connected_users assert "3" in connected_users def test_get_room_info(self, socket_manager): """Test getting room information.""" # Add some users socket_manager.user_rooms["1"] = "user_1" socket_manager.user_rooms["2"] = "user_2" room_info = socket_manager.get_room_info() assert room_info["total_users"] == 2 assert room_info["connected_users"] == ["1", "2"] @pytest.mark.asyncio @patch("app.services.socket.extract_access_token_from_cookies") @patch("app.services.socket.JWTUtils.decode_access_token") async def test_connect_handler_success( self, mock_decode, mock_extract_token, socket_manager, mock_sio ): """Test successful connection with valid token.""" # Setup mocks mock_extract_token.return_value = "valid_token" mock_decode.return_value = {"sub": "123"} # Mock environment environ = {"HTTP_COOKIE": "access_token=valid_token"} # Access the connect handler directly handlers = {} original_event = socket_manager.sio.event def mock_event(func): handlers[func.__name__] = func return func socket_manager.sio.event = mock_event socket_manager._setup_handlers() # Call the connect handler await handlers["connect"]("test_sid", environ) # Verify token extraction and validation mock_extract_token.assert_called_once_with("access_token=valid_token") mock_decode.assert_called_once_with("valid_token") # Verify user tracking assert socket_manager.socket_users["test_sid"] == "123" assert socket_manager.user_rooms["123"] == "user_123" @pytest.mark.asyncio @patch("app.services.socket.extract_access_token_from_cookies") async def test_connect_handler_no_token( self, mock_extract_token, socket_manager, mock_sio ): """Test connection with no access token.""" # Setup mocks mock_extract_token.return_value = None # Mock environment environ = {"HTTP_COOKIE": ""} # Access the connect handler directly handlers = {} original_event = socket_manager.sio.event def mock_event(func): handlers[func.__name__] = func return func socket_manager.sio.event = mock_event socket_manager._setup_handlers() # Call the connect handler await handlers["connect"]("test_sid", environ) # Verify disconnection mock_sio.disconnect.assert_called_once_with("test_sid") # Verify no user tracking assert "test_sid" not in socket_manager.socket_users assert len(socket_manager.user_rooms) == 0 @pytest.mark.asyncio @patch("app.services.socket.extract_access_token_from_cookies") @patch("app.services.socket.JWTUtils.decode_access_token") async def test_connect_handler_invalid_token( self, mock_decode, mock_extract_token, socket_manager, mock_sio ): """Test connection with invalid token.""" # Setup mocks mock_extract_token.return_value = "invalid_token" mock_decode.side_effect = Exception("Invalid token") # Mock environment environ = {"HTTP_COOKIE": "access_token=invalid_token"} # Access the connect handler directly handlers = {} original_event = socket_manager.sio.event def mock_event(func): handlers[func.__name__] = func return func socket_manager.sio.event = mock_event socket_manager._setup_handlers() # Call the connect handler await handlers["connect"]("test_sid", environ) # Verify disconnection mock_sio.disconnect.assert_called_once_with("test_sid") # Verify no user tracking assert "test_sid" not in socket_manager.socket_users assert len(socket_manager.user_rooms) == 0 @pytest.mark.asyncio @patch("app.services.socket.extract_access_token_from_cookies") @patch("app.services.socket.JWTUtils.decode_access_token") async def test_connect_handler_missing_user_id( self, mock_decode, mock_extract_token, socket_manager, mock_sio ): """Test connection with token missing user ID.""" # Setup mocks mock_extract_token.return_value = "token_without_user_id" mock_decode.return_value = {"other_field": "value"} # Missing 'sub' # Mock environment environ = {"HTTP_COOKIE": "access_token=token_without_user_id"} # Access the connect handler directly handlers = {} original_event = socket_manager.sio.event def mock_event(func): handlers[func.__name__] = func return func socket_manager.sio.event = mock_event socket_manager._setup_handlers() # Call the connect handler await handlers["connect"]("test_sid", environ) # Verify disconnection mock_sio.disconnect.assert_called_once_with("test_sid") # Verify no user tracking assert "test_sid" not in socket_manager.socket_users assert len(socket_manager.user_rooms) == 0 @pytest.mark.asyncio async def test_disconnect_handler(self, socket_manager, mock_sio): """Test disconnect handler.""" # Setup initial state socket_manager.socket_users["test_sid"] = "123" socket_manager.user_rooms["123"] = "user_123" # Access the disconnect handler directly handlers = {} original_event = socket_manager.sio.event def mock_event(func): handlers[func.__name__] = func return func socket_manager.sio.event = mock_event socket_manager._setup_handlers() # Call the disconnect handler await handlers["disconnect"]("test_sid") # Verify cleanup assert "test_sid" not in socket_manager.socket_users assert "123" not in socket_manager.user_rooms @pytest.mark.asyncio async def test_disconnect_handler_unknown_socket(self, socket_manager, mock_sio): """Test disconnect handler with unknown socket.""" # Access the disconnect handler directly handlers = {} original_event = socket_manager.sio.event def mock_event(func): handlers[func.__name__] = func return func socket_manager.sio.event = mock_event socket_manager._setup_handlers() # Call the disconnect handler with unknown socket await handlers["disconnect"]("unknown_sid") # Should not raise any errors and state should remain clean assert len(socket_manager.socket_users) == 0 assert len(socket_manager.user_rooms) == 0