Files
wehub-resource-sync 7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

618 lines
21 KiB
Python

"""
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")