Files
wehub-resource-sync 7a0da7932b
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
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

973 lines
33 KiB
Python

"""
Comprehensive tests for SocketIOService.
Tests singleton pattern, event emission, subscriptions, and error handling.
"""
import pytest
from unittest.mock import Mock, MagicMock, patch
class TestSocketIOServiceSingleton:
"""Tests for SocketIOService singleton pattern.
Note: Singleton reset is handled by the global reset_all_singletons fixture
in tests/conftest.py.
"""
def test_requires_app_on_first_creation(self):
"""Test that Flask app is required on first creation."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
with pytest.raises(ValueError) as exc_info:
SocketIOService()
assert "Flask app must be specified" in str(exc_info.value)
def test_creates_instance_with_app(self, mock_flask_app):
"""Test that instance is created with Flask app."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
with patch(
"local_deep_research.web.services.socket_service.SocketIO"
) as mock_socketio:
service = SocketIOService(app=mock_flask_app)
assert service is not None
mock_socketio.assert_called_once()
def test_returns_same_instance_on_second_call(self, mock_flask_app):
"""Test that same instance is returned on subsequent calls."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
with patch("local_deep_research.web.services.socket_service.SocketIO"):
service1 = SocketIOService(app=mock_flask_app)
service2 = SocketIOService()
assert service1 is service2
def test_ignores_app_on_second_call(self, mock_flask_app):
"""Test that app parameter is ignored after singleton creation."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
with patch("local_deep_research.web.services.socket_service.SocketIO"):
service1 = SocketIOService(app=mock_flask_app)
other_app = Mock()
service2 = SocketIOService(app=other_app)
assert service1 is service2
class TestSocketIOServiceInit:
"""Tests for SocketIOService initialization.
Note: Singleton reset is handled by the global reset_all_singletons fixture
in tests/conftest.py.
"""
def test_initializes_socketio_with_correct_params(self, mock_flask_app):
"""Test that SocketIO is initialized with correct parameters."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
with patch(
"local_deep_research.web.services.socket_service.SocketIO"
) as mock_socketio:
SocketIOService(app=mock_flask_app)
mock_socketio.assert_called_once_with(
mock_flask_app,
cors_allowed_origins=None,
async_mode="threading",
path="/socket.io",
logger=False,
engineio_logger=False,
ping_timeout=20,
ping_interval=5,
)
def test_registers_connect_handler(self, mock_flask_app):
"""Test that connect handler is registered."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
mock_socketio = MagicMock()
with patch(
"local_deep_research.web.services.socket_service.SocketIO",
return_value=mock_socketio,
):
SocketIOService(app=mock_flask_app)
# Check that on("connect") was called
mock_socketio.on.assert_any_call("connect")
def test_registers_disconnect_handler(self, mock_flask_app):
"""Test that disconnect handler is registered."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
mock_socketio = MagicMock()
with patch(
"local_deep_research.web.services.socket_service.SocketIO",
return_value=mock_socketio,
):
SocketIOService(app=mock_flask_app)
mock_socketio.on.assert_any_call("disconnect")
def test_registers_subscribe_handler(self, mock_flask_app):
"""Test that subscribe_to_research handler is registered."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
mock_socketio = MagicMock()
with patch(
"local_deep_research.web.services.socket_service.SocketIO",
return_value=mock_socketio,
):
SocketIOService(app=mock_flask_app)
mock_socketio.on.assert_any_call("subscribe_to_research")
def test_registers_join_alias_handler(self, mock_flask_app):
"""Test that the 'join' alias handler is registered.
The JS client emits 'join' on subscribe; without this alias, the
catch-up snapshot in __handle_subscribe never fires for fresh page
loads and per-client targeting falls through to broadcast.
"""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
mock_socketio = MagicMock()
with patch(
"local_deep_research.web.services.socket_service.SocketIO",
return_value=mock_socketio,
):
SocketIOService(app=mock_flask_app)
mock_socketio.on.assert_any_call("join")
def test_registers_leave_alias_handler(self, mock_flask_app):
"""Test that the 'leave' alias handler is registered."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
mock_socketio = MagicMock()
with patch(
"local_deep_research.web.services.socket_service.SocketIO",
return_value=mock_socketio,
):
SocketIOService(app=mock_flask_app)
mock_socketio.on.assert_any_call("leave")
def test_registers_unsubscribe_handler(self, mock_flask_app):
"""Test that unsubscribe_from_research handler is registered."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
mock_socketio = MagicMock()
with patch(
"local_deep_research.web.services.socket_service.SocketIO",
return_value=mock_socketio,
):
SocketIOService(app=mock_flask_app)
mock_socketio.on.assert_any_call("unsubscribe_from_research")
class TestSocketIOServiceEmitSocketEvent:
"""Tests for emit_socket_event method.
Note: Singleton reset is handled by the global reset_all_singletons fixture
in tests/conftest.py.
"""
@pytest.fixture
def service(self, mock_flask_app):
"""Create SocketIOService instance with mocked SocketIO."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
mock_socketio = MagicMock()
with patch(
"local_deep_research.web.services.socket_service.SocketIO",
return_value=mock_socketio,
):
service = SocketIOService(app=mock_flask_app)
service._SocketIOService__socketio = mock_socketio
return service, mock_socketio
def test_emits_to_all_when_no_room(self, service):
"""Test emitting to all clients when no room specified."""
svc, mock_socketio = service
result = svc.emit_socket_event("test_event", {"key": "value"})
assert result is True
mock_socketio.emit.assert_called_once_with(
"test_event", {"key": "value"}
)
def test_emits_to_specific_room(self, service):
"""Test emitting to specific room."""
svc, mock_socketio = service
result = svc.emit_socket_event(
"test_event", {"key": "value"}, room="room-123"
)
assert result is True
mock_socketio.emit.assert_called_once_with(
"test_event", {"key": "value"}, room="room-123"
)
def test_returns_false_on_exception(self, service):
"""Test that False is returned on emission error."""
svc, mock_socketio = service
mock_socketio.emit.side_effect = Exception("Emission failed")
result = svc.emit_socket_event("test_event", {"key": "value"})
assert result is False
def test_logs_exception_on_error(self, service):
"""Test that exception is logged on error."""
svc, mock_socketio = service
mock_socketio.emit.side_effect = Exception("Emission failed")
with patch(
"local_deep_research.web.services.socket_service.logger"
) as mock_logger:
svc.emit_socket_event("test_event", {"key": "value"})
mock_logger.exception.assert_called_once()
class TestSocketIOServiceEmitToSubscribers:
"""Tests for emit_to_subscribers method.
Note: Singleton reset is handled by the global reset_all_singletons fixture
in tests/conftest.py.
"""
@pytest.fixture
def service(self, mock_flask_app):
"""Create SocketIOService instance with mocked SocketIO."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
mock_socketio = MagicMock()
with patch(
"local_deep_research.web.services.socket_service.SocketIO",
return_value=mock_socketio,
):
service = SocketIOService(app=mock_flask_app)
service._SocketIOService__socketio = mock_socketio
return service, mock_socketio
def test_emits_formatted_event(self, service, sample_research_id):
"""Test that event is emitted with formatted name to subscribers."""
svc, mock_socketio = service
# Targeted delivery is the only path emit_to_subscribers takes;
# without a registered subscriber the event is dropped (the
# no-subscribers test pins that contract separately).
svc._SocketIOService__socket_subscriptions = {
sample_research_id: {"sid-1"}
}
svc.emit_to_subscribers(
"research_update", sample_research_id, {"data": "test"}
)
# Should emit with formatted event name, scoped to the room.
mock_socketio.emit.assert_any_call(
f"research_update_{sample_research_id}",
{"data": "test"},
room="sid-1",
)
def test_emits_to_individual_subscribers(self, service, sample_research_id):
"""Test that event is emitted to each subscriber."""
svc, mock_socketio = service
# Add subscribers
svc._SocketIOService__socket_subscriptions = {
sample_research_id: {"sid-1", "sid-2"}
}
svc.emit_to_subscribers(
"research_update", sample_research_id, {"data": "test"}
)
# Should emit to each subscriber
calls = mock_socketio.emit.call_args_list
room_calls = [c for c in calls if "room" in c.kwargs]
assert len(room_calls) == 2
def test_returns_true_on_success(self, service, sample_research_id):
"""Test that True is returned on success."""
svc, mock_socketio = service
result = svc.emit_to_subscribers(
"research_update", sample_research_id, {"data": "test"}
)
assert result is True
def test_returns_false_on_exception(self, service, sample_research_id):
"""Test that False is returned on error.
Per-subscriber emit failures are now caught inside the inner
loop (so other subscribers still receive the event), so the
method returns False only when something outside the loop
raises — simulated here by raising during ``__lock`` acquisition.
"""
svc, mock_socketio = service
# Make the lock context manager raise to force the outer
# except path. The lock is a private attribute on the service.
broken_lock = MagicMock()
broken_lock.__enter__ = MagicMock(side_effect=Exception("Failed"))
broken_lock.__exit__ = MagicMock(return_value=False)
svc._SocketIOService__lock = broken_lock
result = svc.emit_to_subscribers(
"research_update", sample_research_id, {"data": "test"}
)
assert result is False
def test_disables_logging_when_requested(self, service, sample_research_id):
"""Test that logging can be disabled."""
svc, mock_socketio = service
svc.emit_to_subscribers(
"research_update",
sample_research_id,
{"data": "test"},
enable_logging=False,
)
# After call, logging should be re-enabled
assert svc._SocketIOService__logging_enabled is True
def test_re_enables_logging_after_exception(
self, service, sample_research_id
):
"""Test that logging is re-enabled even after exception."""
svc, mock_socketio = service
mock_socketio.emit.side_effect = Exception("Failed")
svc.emit_to_subscribers(
"research_update",
sample_research_id,
{"data": "test"},
enable_logging=False,
)
# Logging should be re-enabled after exception
assert svc._SocketIOService__logging_enabled is True
def test_handles_no_subscribers(self, service, sample_research_id):
"""No-subscribers: drop the event rather than broadcasting.
A prior implementation broadcast room-less to all clients in
this case, which leaked research payloads across users. Early
events are recovered by the catch-up snapshot in
``__handle_subscribe``; no cross-user broadcast is needed.
"""
svc, mock_socketio = service
result = svc.emit_to_subscribers(
"research_update", sample_research_id, {"data": "test"}
)
assert result is True
# MUST NOT emit when there are no targeted subscribers — the
# absence of a room= argument would broadcast to every client.
mock_socketio.emit.assert_not_called()
def test_handles_subscriber_emit_error(self, service, sample_research_id):
"""Test that individual subscriber errors don't stop other emissions."""
svc, mock_socketio = service
# Add subscribers
svc._SocketIOService__socket_subscriptions = {
sample_research_id: {"sid-1", "sid-2"}
}
# First call succeeds, second fails
call_count = [0]
def emit_side_effect(*args, **kwargs):
call_count[0] += 1
if call_count[0] == 2: # Fail on second subscriber
raise Exception("Subscriber error")
mock_socketio.emit.side_effect = emit_side_effect
result = svc.emit_to_subscribers(
"research_update", sample_research_id, {"data": "test"}
)
# Should still return True (overall success)
assert result is True
class TestSocketIOServiceSubscriptionManagement:
"""Tests for subscription management.
Note: Singleton reset is handled by the global reset_all_singletons fixture
in tests/conftest.py.
"""
@pytest.fixture
def service_with_mocks(self, mock_flask_app):
"""Create service with accessible internal state."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
mock_socketio = MagicMock()
with patch(
"local_deep_research.web.services.socket_service.SocketIO",
return_value=mock_socketio,
):
service = SocketIOService(app=mock_flask_app)
service._SocketIOService__socketio = mock_socketio
return service, mock_socketio
def test_subscribe_adds_client(
self, service_with_mocks, mock_request, sample_research_id
):
"""Test that subscribing adds client to subscription set."""
svc, _ = service_with_mocks
data = {"research_id": sample_research_id}
svc._SocketIOService__handle_subscribe(data, mock_request)
subscriptions = svc._SocketIOService__socket_subscriptions
assert sample_research_id in subscriptions
assert mock_request.sid in subscriptions[sample_research_id]
def test_subscribe_creates_set_for_new_research(
self, service_with_mocks, mock_request
):
"""Test that subscribing creates new set for new research."""
svc, _ = service_with_mocks
data = {"research_id": "new-research-id"}
svc._SocketIOService__handle_subscribe(data, mock_request)
subscriptions = svc._SocketIOService__socket_subscriptions
assert "new-research-id" in subscriptions
assert isinstance(subscriptions["new-research-id"], set)
def test_subscribe_ignores_empty_research_id(
self, service_with_mocks, mock_request
):
"""Test that empty research_id is ignored."""
svc, _ = service_with_mocks
data = {"research_id": ""}
svc._SocketIOService__handle_subscribe(data, mock_request)
subscriptions = svc._SocketIOService__socket_subscriptions
assert "" not in subscriptions
def test_subscribe_ignores_missing_research_id(
self, service_with_mocks, mock_request
):
"""Test that missing research_id is ignored."""
svc, _ = service_with_mocks
data = {}
svc._SocketIOService__handle_subscribe(data, mock_request)
subscriptions = svc._SocketIOService__socket_subscriptions
assert len(subscriptions) == 0
def test_disconnect_removes_subscription(
self, service_with_mocks, mock_request
):
"""Test that disconnecting removes client from all research subscriptions.
Schema: __socket_subscriptions is research_id → set of sids.
Disconnect must discard the sid from every research_id's set.
"""
svc, _ = service_with_mocks
# Set up correct schema: research_id → {sids}
svc._SocketIOService__socket_subscriptions = {
"research-1": {mock_request.sid, "other-sid"},
"research-2": {mock_request.sid},
}
svc._SocketIOService__handle_disconnect(
mock_request, "client disconnect"
)
subscriptions = svc._SocketIOService__socket_subscriptions
# sid should be removed from research-1, leaving only other-sid
assert mock_request.sid not in subscriptions.get("research-1", set())
assert "other-sid" in subscriptions["research-1"]
# research-2 had only this sid, so the key should be cleaned up
assert "research-2" not in subscriptions
def test_disconnect_handles_no_subscription(
self, service_with_mocks, mock_request
):
"""Test that disconnect handles non-subscribed client."""
svc, _ = service_with_mocks
# No subscriptions exist
svc._SocketIOService__socket_subscriptions = {}
# Should not raise
svc._SocketIOService__handle_disconnect(
mock_request, "client disconnect"
)
def test_disconnect_leaves_other_clients_intact(
self, service_with_mocks, mock_request
):
"""Test that disconnect only removes the disconnecting client."""
svc, _ = service_with_mocks
svc._SocketIOService__socket_subscriptions = {
"research-1": {mock_request.sid, "sid-A", "sid-B"},
}
svc._SocketIOService__handle_disconnect(
mock_request, "client disconnect"
)
subscriptions = svc._SocketIOService__socket_subscriptions
assert subscriptions["research-1"] == {"sid-A", "sid-B"}
def test_disconnect_noop_when_sid_not_subscribed(
self, service_with_mocks, mock_request
):
"""Test that disconnect is a no-op when sid has no subscriptions."""
svc, _ = service_with_mocks
svc._SocketIOService__socket_subscriptions = {
"research-1": {"other-sid-1", "other-sid-2"},
}
svc._SocketIOService__handle_disconnect(
mock_request, "client disconnect"
)
# Nothing should change
subscriptions = svc._SocketIOService__socket_subscriptions
assert subscriptions["research-1"] == {"other-sid-1", "other-sid-2"}
def test_subscribe_then_disconnect_round_trip(
self, service_with_mocks, mock_request
):
"""Test full subscribe → disconnect cycle uses consistent schema."""
svc, _ = service_with_mocks
# Subscribe to two research IDs
svc._SocketIOService__handle_subscribe(
{"research_id": "r1"}, mock_request
)
svc._SocketIOService__handle_subscribe(
{"research_id": "r2"}, mock_request
)
subscriptions = svc._SocketIOService__socket_subscriptions
assert mock_request.sid in subscriptions["r1"]
assert mock_request.sid in subscriptions["r2"]
# Disconnect should clean up both
svc._SocketIOService__handle_disconnect(
mock_request, "client disconnect"
)
assert mock_request.sid not in subscriptions.get("r1", set())
assert mock_request.sid not in subscriptions.get("r2", set())
def test_unsubscribe_discards_sid(
self, service_with_mocks, mock_request, sample_research_id
):
"""Unsubscribe handler removes the sid from the subscription set."""
svc, _ = service_with_mocks
svc._SocketIOService__socket_subscriptions = {
sample_research_id: {mock_request.sid, "other-sid"},
}
svc._SocketIOService__handle_unsubscribe(
{"research_id": sample_research_id}, mock_request
)
subscriptions = svc._SocketIOService__socket_subscriptions
assert mock_request.sid not in subscriptions[sample_research_id]
# Other clients are untouched
assert "other-sid" in subscriptions[sample_research_id]
def test_unsubscribe_ignores_missing_research_id(
self, service_with_mocks, mock_request
):
"""Unsubscribe with missing research_id is a no-op."""
svc, _ = service_with_mocks
svc._SocketIOService__socket_subscriptions = {
"research-1": {"sid-A"},
}
# Should not raise
svc._SocketIOService__handle_unsubscribe({}, mock_request)
svc._SocketIOService__handle_unsubscribe(None, mock_request)
# Existing subscriptions are unchanged
subscriptions = svc._SocketIOService__socket_subscriptions
assert subscriptions["research-1"] == {"sid-A"}
def test_unsubscribe_handles_unknown_research_id(
self, service_with_mocks, mock_request
):
"""Unsubscribe for a research_id with no subscribers is a no-op."""
svc, _ = service_with_mocks
svc._SocketIOService__socket_subscriptions = {}
# Should not raise
svc._SocketIOService__handle_unsubscribe(
{"research_id": "never-subscribed"}, mock_request
)
def test_unsubscribe_prunes_empty_subscription_set(
self, service_with_mocks, mock_request, sample_research_id
):
"""Removing the last sid for a research_id deletes the dict entry.
Otherwise __socket_subscriptions accumulates stale keys for every
research_id seen over a long-running server, even after all clients
have left.
"""
svc, _ = service_with_mocks
svc._SocketIOService__socket_subscriptions = {
sample_research_id: {mock_request.sid},
}
svc._SocketIOService__handle_unsubscribe(
{"research_id": sample_research_id}, mock_request
)
assert (
sample_research_id not in svc._SocketIOService__socket_subscriptions
)
def test_unsubscribe_keeps_set_when_other_clients_remain(
self, service_with_mocks, mock_request, sample_research_id
):
"""Unsubscribe must not delete the set while other sids are present."""
svc, _ = service_with_mocks
svc._SocketIOService__socket_subscriptions = {
sample_research_id: {mock_request.sid, "other-sid"},
}
svc._SocketIOService__handle_unsubscribe(
{"research_id": sample_research_id}, mock_request
)
assert svc._SocketIOService__socket_subscriptions[
sample_research_id
] == {"other-sid"}
class TestSocketIOServiceErrorHandling:
"""Tests for error handling methods.
Note: Singleton reset is handled by the global reset_all_singletons fixture
in tests/conftest.py.
"""
@pytest.fixture
def service(self, mock_flask_app):
"""Create SocketIOService instance."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
mock_socketio = MagicMock()
with patch(
"local_deep_research.web.services.socket_service.SocketIO",
return_value=mock_socketio,
):
service = SocketIOService(app=mock_flask_app)
return service
def test_socket_error_returns_false(self, service):
"""Test that socket error handler returns False."""
result = service._SocketIOService__handle_socket_error(
Exception("Test error")
)
assert result is False
def test_default_error_returns_false(self, service):
"""Test that default error handler returns False."""
result = service._SocketIOService__handle_default_error(
Exception("Test error")
)
assert result is False
def test_connect_handler_rejects_unauthenticated(
self, service, mock_request
):
"""Connect handler rejects when no session username."""
with patch(
"local_deep_research.web.services.socket_service.session", {}
):
assert (
service._SocketIOService__handle_connect(mock_request) is False
)
def test_connect_handler_rejects_no_db_session_and_no_password(
self, service, mock_request
):
"""Connect handler rejects when user has no active DB session and no stored password."""
with (
patch(
"local_deep_research.web.services.socket_service.session",
{"username": "alice", "session_id": "sess-1"},
),
patch(
"local_deep_research.web.services.socket_service.db_manager"
) as mock_db,
patch(
"local_deep_research.web.services.socket_service.session_password_store"
) as mock_store,
):
mock_db.is_user_connected.return_value = False
mock_store.get_session_password.return_value = None
assert (
service._SocketIOService__handle_connect(mock_request) is False
)
mock_db.open_user_database.assert_not_called()
def test_connect_handler_lazy_opens_when_password_available(
self, service, mock_request
):
"""Connect handler lazy-opens the DB when the engine isn't open but a password is stored."""
with (
patch(
"local_deep_research.web.services.socket_service.session",
{"username": "alice", "session_id": "sess-1"},
),
patch(
"local_deep_research.web.services.socket_service.db_manager"
) as mock_db,
patch(
"local_deep_research.web.services.socket_service.session_password_store"
) as mock_store,
patch("local_deep_research.web.services.socket_service.join_room"),
):
mock_db.is_user_connected.return_value = False
mock_store.get_session_password.return_value = "pw"
assert (
service._SocketIOService__handle_connect(mock_request) is True
)
mock_db.open_user_database.assert_called_once_with("alice", "pw")
def test_connect_handler_accepts_authenticated(self, service, mock_request):
"""Connect handler accepts authenticated users with active DB session."""
with (
patch(
"local_deep_research.web.services.socket_service.session",
{"username": "alice"},
),
patch(
"local_deep_research.web.services.socket_service.db_manager"
) as mock_db,
patch(
"local_deep_research.web.services.socket_service.logger"
) as mock_logger,
patch("local_deep_research.web.services.socket_service.join_room"),
):
mock_db.is_user_connected.return_value = True
assert (
service._SocketIOService__handle_connect(mock_request) is True
)
mock_logger.info.assert_called()
call_args = mock_logger.info.call_args[0][0]
assert mock_request.sid in call_args
class TestSocketIOServiceRun:
"""Tests for run method.
Note: Singleton reset is handled by the global reset_all_singletons fixture
in tests/conftest.py.
"""
@pytest.fixture
def service(self, mock_flask_app):
"""Create SocketIOService instance."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
mock_socketio = MagicMock()
with patch(
"local_deep_research.web.services.socket_service.SocketIO",
return_value=mock_socketio,
):
service = SocketIOService(app=mock_flask_app)
service._SocketIOService__socketio = mock_socketio
return service, mock_socketio, mock_flask_app
def test_run_calls_socketio_run(self, service):
"""Test that run method calls socketio.run."""
svc, mock_socketio, mock_app = service
svc.run("0.0.0.0", 5000)
mock_socketio.run.assert_called_once()
def test_run_passes_correct_params(self, service):
"""Test that run passes correct parameters."""
svc, mock_socketio, mock_app = service
svc.run("localhost", 8080, debug=True)
mock_socketio.run.assert_called_once_with(
mock_app,
debug=True,
host="localhost",
port=8080,
allow_unsafe_werkzeug=True,
use_reloader=False,
)
def test_run_defaults_debug_to_false(self, service):
"""Test that debug defaults to False."""
svc, mock_socketio, mock_app = service
svc.run("0.0.0.0", 5000)
call_kwargs = mock_socketio.run.call_args[1]
assert call_kwargs["debug"] is False
class TestSocketIOServiceLogging:
"""Tests for logging behavior.
Note: Singleton reset is handled by the global reset_all_singletons fixture
in tests/conftest.py.
"""
@pytest.fixture
def service(self, mock_flask_app):
"""Create SocketIOService instance."""
from local_deep_research.web.services.socket_service import (
SocketIOService,
)
mock_socketio = MagicMock()
with patch(
"local_deep_research.web.services.socket_service.SocketIO",
return_value=mock_socketio,
):
service = SocketIOService(app=mock_flask_app)
return service
def test_logging_enabled_by_default(self, service):
"""Test that logging is enabled by default."""
assert service._SocketIOService__logging_enabled is True
def test_log_info_logs_when_enabled(self, service):
"""Test that __log_info logs when enabled."""
with patch(
"local_deep_research.web.services.socket_service.logger"
) as mock_logger:
service._SocketIOService__log_info("Test message")
mock_logger.info.assert_called_once_with("Test message")
def test_log_info_skips_when_disabled(self, service):
"""Test that __log_info skips when disabled."""
service._SocketIOService__logging_enabled = False
with patch(
"local_deep_research.web.services.socket_service.logger"
) as mock_logger:
service._SocketIOService__log_info("Test message")
mock_logger.info.assert_not_called()
def test_log_error_logs_when_enabled(self, service):
"""Test that __log_error logs when enabled."""
with patch(
"local_deep_research.web.services.socket_service.logger"
) as mock_logger:
service._SocketIOService__log_error("Error message")
mock_logger.error.assert_called_once_with("Error message")
def test_log_error_skips_when_disabled(self, service):
"""Test that __log_error skips when disabled."""
service._SocketIOService__logging_enabled = False
with patch(
"local_deep_research.web.services.socket_service.logger"
) as mock_logger:
service._SocketIOService__log_error("Error message")
mock_logger.error.assert_not_called()
def test_log_exception_logs_when_enabled(self, service):
"""Test that __log_exception logs when enabled."""
with patch(
"local_deep_research.web.services.socket_service.logger"
) as mock_logger:
service._SocketIOService__log_exception("Exception message")
mock_logger.exception.assert_called_once_with("Exception message")
def test_log_exception_skips_when_disabled(self, service):
"""Test that __log_exception skips when disabled."""
service._SocketIOService__logging_enabled = False
with patch(
"local_deep_research.web.services.socket_service.logger"
) as mock_logger:
service._SocketIOService__log_exception("Exception message")
mock_logger.exception.assert_not_called()