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
264 lines
9.1 KiB
Python
264 lines
9.1 KiB
Python
"""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")
|