""" Coverage tests for SocketIOService — targeting ~17 previously uncovered emission failure paths and edge-case branches. Focuses on: - emit_socket_event failure when room IS specified - emit_to_subscribers with enable_logging=False (success path) - remove_subscriptions_for_research when nothing was stored (None branch) - __handle_connect direct invocation - __handle_subscribe with no research_id (falsy) - __handle_subscribe snapshot with empty log (no emit) - __handle_subscribe snapshot with log entry (emit path) - __handle_socket_error and __handle_default_error return values - run() when werkzeug import fails (ImportError branch) - __handle_disconnect cleanup_current_thread raises generic Exception - __log_info / __log_error / __log_exception silenced when logging disabled - emit_to_subscribers per-subscriber exception with logging disabled """ from unittest.mock import patch, MagicMock, Mock MODULE = "local_deep_research.web.services.socket_service" # --------------------------------------------------------------------------- # Helper: create a fresh SocketIOService with MockSocketIO injected # --------------------------------------------------------------------------- class _MockSocketIO: """Minimal SocketIO stand-in that records emitted events.""" def __init__(self, app=None, **kwargs): self.emitted_events = [] self._handlers = {} def emit(self, event, data, room=None): self.emitted_events.append({"event": event, "data": data, "room": room}) def on(self, event): def decorator(f): self._handlers[event] = f return f return decorator @property def on_error(self): def decorator(f): self._handlers["error"] = f return f return decorator @property def on_error_default(self): def decorator(f): self._handlers["error_default"] = f return f return decorator def run(self, app, **kwargs): pass def _make_service(): from local_deep_research.web.services.socket_service import SocketIOService SocketIOService._instance = None mock_app = Mock() mock_app.config = {} mock_app.debug = False with ( patch(f"{MODULE}.SocketIO", _MockSocketIO), patch( "local_deep_research.settings.env_registry.get_env_setting", return_value=None, ), ): service = SocketIOService(app=mock_app) return service class TestEmitSocketEventRoomFailure: """emit_socket_event returns False when room is specified and emit raises.""" def setup_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) self._orig = SocketIOService._instance def teardown_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) SocketIOService._instance = self._orig def test_emit_with_room_raises_returns_false(self): """When room is set and socketio.emit raises, returns False.""" service = _make_service() service._SocketIOService__socketio.emit = Mock( side_effect=RuntimeError("room disconnected") ) result = service.emit_socket_event( "test_event", {"x": 1}, room="sid_abc" ) assert result is False def test_emit_with_room_exception_does_not_propagate(self): """Exception from room-targeted emit is swallowed, not re-raised.""" service = _make_service() service._SocketIOService__socketio.emit = Mock( side_effect=OSError("network gone") ) try: result = service.emit_socket_event("ev", {}, room="r1") except Exception: assert False, ( "Exception should not propagate from emit_socket_event" ) assert result is False class TestEmitToSubscribersLoggingDisabled: """emit_to_subscribers with enable_logging=False (success path).""" def setup_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) self._orig = SocketIOService._instance def teardown_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) SocketIOService._instance = self._orig def test_logging_disabled_then_restored_on_success(self): """Logging flag is False during call and True again after.""" service = _make_service() rid = "r_logging" with service._SocketIOService__lock: service._SocketIOService__socket_subscriptions[rid] = {"s1"} logging_during_call = [] original_emit = service._SocketIOService__socketio.emit def capturing_emit(event, data, room=None): logging_during_call.append( service._SocketIOService__logging_enabled ) original_emit(event, data, room=room) service._SocketIOService__socketio.emit = capturing_emit result = service.emit_to_subscribers( "ev", rid, {}, enable_logging=False ) assert result is True # During call logging was disabled assert logging_during_call == [False] # After call logging is restored assert service._SocketIOService__logging_enabled is True def test_logging_disabled_broadcast_path_restored(self): """Logging is restored even via the broadcast (no-subscriber) path.""" service = _make_service() result = service.emit_to_subscribers( "ev", "no_such_research", {}, enable_logging=False ) assert result is True assert service._SocketIOService__logging_enabled is True class TestRemoveSubscriptionsNoneBranch: """remove_subscriptions_for_research when nothing exists (removed=None).""" def setup_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) self._orig = SocketIOService._instance def teardown_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) SocketIOService._instance = self._orig def test_remove_nonexistent_does_not_log(self): """No log_info call when removed is None (pop returns None).""" service = _make_service() with patch.object(service, "_SocketIOService__log_info") as mock_log: service.remove_subscriptions_for_research("totally_unknown") # __log_info should NOT be called since removed is None mock_log.assert_not_called() def test_remove_existing_does_log(self): """__log_info IS called when subscriptions were present.""" service = _make_service() rid = "r_exists" with service._SocketIOService__lock: service._SocketIOService__socket_subscriptions[rid] = { "sid_a", "sid_b", } with patch.object(service, "_SocketIOService__log_info") as mock_log: service.remove_subscriptions_for_research(rid) mock_log.assert_called_once() assert rid not in service._SocketIOService__socket_subscriptions class TestHandleConnect: """Direct invocation of __handle_connect.""" def setup_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) self._orig = SocketIOService._instance def teardown_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) SocketIOService._instance = self._orig def test_handle_connect_rejects_unauthenticated(self): """__handle_connect returns False when no session username.""" service = _make_service() mock_request = MagicMock() mock_request.sid = "connect_client_999" with patch(f"{MODULE}.session", {}): result = service._SocketIOService__handle_connect(mock_request) assert result is False def test_handle_connect_rejects_when_no_db_session_and_no_password(self): """No active DB session AND no stored password → reject.""" service = _make_service() mock_request = MagicMock() mock_request.sid = "connect_client_999" with ( patch( f"{MODULE}.session", {"username": "alice", "session_id": "sess-1"}, ), patch(f"{MODULE}.db_manager") as mock_db, patch(f"{MODULE}.session_password_store") as mock_store, ): mock_db.is_user_connected.return_value = False mock_store.get_session_password.return_value = None result = service._SocketIOService__handle_connect(mock_request) assert result is False mock_db.open_user_database.assert_not_called() def test_handle_connect_lazy_opens_db_when_password_available(self): """No active DB session but password stored → lazy-open and accept.""" service = _make_service() mock_request = MagicMock() mock_request.sid = "connect_client_999" with ( patch( f"{MODULE}.session", {"username": "alice", "session_id": "sess-1"}, ), patch(f"{MODULE}.db_manager") as mock_db, patch(f"{MODULE}.session_password_store") as mock_store, patch(f"{MODULE}.join_room"), ): mock_db.is_user_connected.return_value = False mock_store.get_session_password.return_value = "pw" result = service._SocketIOService__handle_connect(mock_request) assert result is True mock_db.open_user_database.assert_called_once_with("alice", "pw") def test_handle_connect_rejects_when_lazy_open_raises(self): """Lazy open raising (e.g., wrong password) → reject.""" service = _make_service() mock_request = MagicMock() mock_request.sid = "connect_client_999" with ( patch( f"{MODULE}.session", {"username": "alice", "session_id": "sess-1"}, ), patch(f"{MODULE}.db_manager") as mock_db, patch(f"{MODULE}.session_password_store") as mock_store, patch.object(service, "_SocketIOService__log_error"), ): mock_db.is_user_connected.return_value = False mock_store.get_session_password.return_value = "pw" mock_db.open_user_database.side_effect = ValueError("bad key") result = service._SocketIOService__handle_connect(mock_request) assert result is False def test_handle_connect_accepts_authenticated(self): """__handle_connect returns True for authenticated users with DB session.""" service = _make_service() mock_request = MagicMock() mock_request.sid = "connect_client_999" with ( patch(f"{MODULE}.session", {"username": "alice"}), patch(f"{MODULE}.db_manager") as mock_db, patch(f"{MODULE}.join_room"), patch.object(service, "_SocketIOService__log_info") as mock_log, ): mock_db.is_user_connected.return_value = True result = service._SocketIOService__handle_connect(mock_request) assert result is True mock_log.assert_called_once() assert "connect_client_999" in str(mock_log.call_args) def test_handle_connect_joins_per_user_room(self): """Each connected socket joins its owner's per-user room, so user-scoped events (settings_changed, which carries raw setting values) reach only that user's tabs and are never broadcast to other connected clients.""" from local_deep_research.web.services.socket_service import ( SocketIOService, ) service = _make_service() mock_request = MagicMock() mock_request.sid = "connect_client_999" with ( patch(f"{MODULE}.session", {"username": "alice"}), patch(f"{MODULE}.db_manager") as mock_db, patch(f"{MODULE}.join_room") as mock_join, patch.object(service, "_SocketIOService__log_info"), ): mock_db.is_user_connected.return_value = True service._SocketIOService__handle_connect(mock_request) mock_join.assert_called_once_with(SocketIOService.user_room("alice")) assert SocketIOService.user_room("alice") == "user:alice" class TestHandleSubscribeEdgeCases: """Edge cases in __handle_subscribe.""" def setup_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) self._orig = SocketIOService._instance def teardown_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) SocketIOService._instance = self._orig def test_subscribe_with_none_research_id_no_op(self): """No subscription added when research_id is absent/None.""" service = _make_service() mock_request = MagicMock() mock_request.sid = "client_no_id" service._SocketIOService__handle_subscribe({}, mock_request) with service._SocketIOService__lock: assert len(service._SocketIOService__socket_subscriptions) == 0 @patch(f"{MODULE}.get_active_research_snapshot") def test_subscribe_snapshot_empty_log_no_emit(self, mock_snapshot): """When snapshot exists but log is empty, no socket event is emitted.""" service = _make_service() mock_snapshot.return_value = {"progress": 42, "log": []} mock_request = MagicMock() mock_request.sid = "client_empty_log" service._SocketIOService__handle_subscribe( {"research_id": "r_empty_log"}, mock_request ) # No emit should happen since latest_log is None/falsy assert len(service._SocketIOService__socketio.emitted_events) == 0 @patch(f"{MODULE}.get_active_research_snapshot") def test_subscribe_snapshot_with_log_emits_event(self, mock_snapshot): """When snapshot has a log entry, emit_socket_event is called.""" service = _make_service() mock_snapshot.return_value = { "progress": 75, "log": [{"message": "Searching...", "type": "info"}], } mock_request = MagicMock() mock_request.sid = "client_with_log" with patch.object( service, "emit_socket_event", return_value=True ) as mock_emit: service._SocketIOService__handle_subscribe( {"research_id": "r_with_log"}, mock_request ) mock_emit.assert_called_once() call_args = mock_emit.call_args assert ( "r_with_log" in call_args[0][0] ) # event name contains research_id assert call_args[1]["room"] == "client_with_log" class TestHandleSocketErrors: """__handle_socket_error and __handle_default_error return False.""" def setup_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) self._orig = SocketIOService._instance def teardown_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) SocketIOService._instance = self._orig def test_handle_socket_error_returns_false(self): """__handle_socket_error returns False to avoid crashing the server.""" service = _make_service() result = service._SocketIOService__handle_socket_error( Exception("socket error") ) assert result is False def test_handle_default_error_returns_false(self): """__handle_default_error returns False to avoid crashing the server.""" service = _make_service() result = service._SocketIOService__handle_default_error( Exception("unhandled error") ) assert result is False class TestRunWerkzeugImportError: """run() when werkzeug cannot be imported.""" def setup_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) self._orig = SocketIOService._instance def teardown_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) SocketIOService._instance = self._orig def test_run_handles_werkzeug_import_error(self): """run() logs a warning but still starts when werkzeug is missing.""" service = _make_service() service._SocketIOService__socketio.run = Mock() import builtins real_import = builtins.__import__ def mock_import(name, *args, **kwargs): if name == "werkzeug.serving": raise ImportError("werkzeug not installed") return real_import(name, *args, **kwargs) with patch("builtins.__import__", side_effect=mock_import): service.run(host="127.0.0.1", port=5000, debug=False) service._SocketIOService__socketio.run.assert_called_once() class TestHandleDisconnectCleanupException: """__handle_disconnect when cleanup_current_thread raises a generic Exception.""" def setup_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) self._orig = SocketIOService._instance def teardown_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) SocketIOService._instance = self._orig def test_cleanup_exception_is_caught_and_logged(self): """Generic Exception from cleanup_current_thread is caught, not re-raised.""" service = _make_service() mock_request = MagicMock() mock_request.sid = "client_cleanup_exc" def boom_cleanup(): raise RuntimeError("session cleanup failed") mock_module = MagicMock() mock_module.cleanup_current_thread = boom_cleanup with patch.dict( "sys.modules", { "local_deep_research.database.thread_local_session": mock_module, }, ): with patch.object( service, "_SocketIOService__log_exception" ) as mock_exc: # Should NOT raise service._SocketIOService__handle_disconnect( mock_request, "cleanup exc test" ) # __log_exception should have been called for the cleanup failure assert mock_exc.called class TestLoggingMethods: """__log_info, __log_error, __log_exception are no-ops when logging disabled.""" def setup_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) self._orig = SocketIOService._instance def teardown_method(self): from local_deep_research.web.services.socket_service import ( SocketIOService, ) SocketIOService._instance = self._orig def test_log_info_silent_when_disabled(self): """__log_info does not call logger.info when __logging_enabled is False.""" service = _make_service() service._SocketIOService__logging_enabled = False with patch(f"{MODULE}.logger") as mock_logger: service._SocketIOService__log_info("should be silent") mock_logger.info.assert_not_called() def test_log_error_silent_when_disabled(self): """__log_error does not call logger.error when __logging_enabled is False.""" service = _make_service() service._SocketIOService__logging_enabled = False with patch(f"{MODULE}.logger") as mock_logger: service._SocketIOService__log_error("should be silent") mock_logger.error.assert_not_called() def test_log_exception_silent_when_disabled(self): """__log_exception does not call logger.exception when __logging_enabled is False.""" service = _make_service() service._SocketIOService__logging_enabled = False with patch(f"{MODULE}.logger") as mock_logger: service._SocketIOService__log_exception("should be silent") mock_logger.exception.assert_not_called() def test_log_info_active_when_enabled(self): """__log_info calls logger.info when __logging_enabled is True.""" service = _make_service() service._SocketIOService__logging_enabled = True with patch(f"{MODULE}.logger") as mock_logger: service._SocketIOService__log_info("active message") mock_logger.info.assert_called_once_with("active message")