"""Extra coverage tests for socket_service.py — private event handlers and logging.""" from unittest.mock import MagicMock, patch MODULE = "local_deep_research.web.services.socket_service" def _get_service_with_patched_init(): """Get a SocketIOService instance bypassing singleton + Flask requirement.""" from local_deep_research.web.services.socket_service import SocketIOService with patch.object( SocketIOService, "__new__", lambda cls, *a, **kw: object.__new__(cls) ): svc = object.__new__(SocketIOService) svc._SocketIOService__socket_subscriptions = {} svc._SocketIOService__lock = __import__("threading").Lock() svc._SocketIOService__socketio = MagicMock() svc._SocketIOService__logging_enabled = True return svc # =========================================================================== # __handle_connect # =========================================================================== class TestHandleConnect: def test_rejects_unauthenticated(self): svc = _get_service_with_patched_init() mock_request = MagicMock() mock_request.sid = "client-123" with patch(f"{MODULE}.session", {}): assert svc._SocketIOService__handle_connect(mock_request) is False def test_rejects_when_no_db_session(self): svc = _get_service_with_patched_init() mock_request = MagicMock() mock_request.sid = "client-123" with ( patch(f"{MODULE}.session", {"username": "alice"}), patch(f"{MODULE}.db_manager") as mock_db, ): mock_db.is_user_connected.return_value = False assert svc._SocketIOService__handle_connect(mock_request) is False def test_accepts_authenticated(self): svc = _get_service_with_patched_init() mock_request = MagicMock() mock_request.sid = "client-123" with ( patch(f"{MODULE}.session", {"username": "alice"}), patch(f"{MODULE}.db_manager") as mock_db, patch(f"{MODULE}.join_room"), ): mock_db.is_user_connected.return_value = True assert svc._SocketIOService__handle_connect(mock_request) is True # =========================================================================== # __handle_disconnect # =========================================================================== class TestHandleDisconnect: def test_removes_subscription(self): svc = _get_service_with_patched_init() svc._SocketIOService__socket_subscriptions = { "res-1": {"client-1", "client-2"}, "res-2": {"client-1"}, } mock_request = MagicMock() mock_request.sid = "client-1" with patch(f"{MODULE}.cleanup_current_thread", create=True): svc._SocketIOService__handle_disconnect( mock_request, "transport close" ) assert "client-1" not in svc._SocketIOService__socket_subscriptions.get( "res-1", set() ) # res-2 should be cleaned up entirely since no clients left assert "res-2" not in svc._SocketIOService__socket_subscriptions def test_empty_subscriptions_handled(self): svc = _get_service_with_patched_init() mock_request = MagicMock() mock_request.sid = "unknown-client" with patch(f"{MODULE}.cleanup_current_thread", create=True): svc._SocketIOService__handle_disconnect(mock_request, "timeout") def test_cleanup_import_error_swallowed(self): svc = _get_service_with_patched_init() mock_request = MagicMock() mock_request.sid = "client-1" with patch.dict( "sys.modules", {"local_deep_research.database.thread_local_session": None}, ): svc._SocketIOService__handle_disconnect(mock_request, "close") def test_cleanup_exception_swallowed(self): svc = _get_service_with_patched_init() mock_request = MagicMock() mock_request.sid = "client-1" mock_mod = MagicMock() mock_mod.cleanup_current_thread.side_effect = RuntimeError( "cleanup fail" ) with patch.dict( "sys.modules", {"local_deep_research.database.thread_local_session": mock_mod}, ): svc._SocketIOService__handle_disconnect(mock_request, "close") def test_outer_exception_swallowed(self): svc = _get_service_with_patched_init() # Make the lock acquisition fail svc._SocketIOService__lock = MagicMock() svc._SocketIOService__lock.__enter__ = MagicMock( side_effect=RuntimeError("lock fail") ) svc._SocketIOService__lock.__exit__ = MagicMock(return_value=False) mock_request = MagicMock() mock_request.sid = "client-1" # Should not raise svc._SocketIOService__handle_disconnect(mock_request, "error") # =========================================================================== # __handle_subscribe # =========================================================================== class TestHandleSubscribe: def test_adds_subscriber(self): svc = _get_service_with_patched_init() mock_request = MagicMock() mock_request.sid = "client-1" with patch(f"{MODULE}.get_active_research_snapshot", return_value=None): svc._SocketIOService__handle_subscribe( {"research_id": "res-1"}, mock_request ) assert "client-1" in svc._SocketIOService__socket_subscriptions["res-1"] def test_sends_current_status_when_available(self): svc = _get_service_with_patched_init() mock_request = MagicMock() mock_request.sid = "client-1" snapshot = { "progress": 50, "log": [{"message": "Searching...", "phase": "search"}], } with patch( f"{MODULE}.get_active_research_snapshot", return_value=snapshot ): svc._SocketIOService__handle_subscribe( {"research_id": "res-1"}, mock_request ) svc._SocketIOService__socketio.emit.assert_called() def test_no_status_when_snapshot_none(self): svc = _get_service_with_patched_init() mock_request = MagicMock() mock_request.sid = "client-1" with patch(f"{MODULE}.get_active_research_snapshot", return_value=None): svc._SocketIOService__handle_subscribe( {"research_id": "res-1"}, mock_request ) svc._SocketIOService__socketio.emit.assert_not_called() def test_empty_log_no_emit(self): svc = _get_service_with_patched_init() mock_request = MagicMock() mock_request.sid = "client-1" snapshot = {"progress": 0, "log": []} with patch( f"{MODULE}.get_active_research_snapshot", return_value=snapshot ): svc._SocketIOService__handle_subscribe( {"research_id": "res-1"}, mock_request ) svc._SocketIOService__socketio.emit.assert_not_called() def test_no_research_id_ignored(self): svc = _get_service_with_patched_init() mock_request = MagicMock() svc._SocketIOService__handle_subscribe({}, mock_request) assert len(svc._SocketIOService__socket_subscriptions) == 0 # =========================================================================== # __handle_socket_error / __handle_default_error # =========================================================================== class TestErrorHandlers: def test_socket_error_returns_false(self): svc = _get_service_with_patched_init() result = svc._SocketIOService__handle_socket_error(RuntimeError("test")) assert result is False def test_default_error_returns_false(self): svc = _get_service_with_patched_init() result = svc._SocketIOService__handle_default_error(ValueError("test")) assert result is False # =========================================================================== # __log_info / __log_error / __log_exception — logging control # =========================================================================== class TestLoggingControl: def test_log_info_when_enabled(self): svc = _get_service_with_patched_init() svc._SocketIOService__logging_enabled = True svc._SocketIOService__log_info("test message") def test_log_info_when_disabled(self): svc = _get_service_with_patched_init() svc._SocketIOService__logging_enabled = False svc._SocketIOService__log_info("should not log") def test_log_error_when_enabled(self): svc = _get_service_with_patched_init() svc._SocketIOService__logging_enabled = True svc._SocketIOService__log_error("error message") def test_log_exception_when_enabled(self): svc = _get_service_with_patched_init() svc._SocketIOService__logging_enabled = True svc._SocketIOService__log_exception("exception message") def test_log_exception_when_disabled(self): svc = _get_service_with_patched_init() svc._SocketIOService__logging_enabled = False svc._SocketIOService__log_exception("should not log")