"""Integration tests for multi-user session isolation in remote-hosted mode.
These tests compose PluginRegistry + PluginHub to verify that users
cannot see or interact with each other's Unity instances.
"""
import asyncio
from unittest.mock import AsyncMock
import pytest
from core.config import config
from transport.plugin_hub import NoUnitySessionError, PluginHub
from transport.plugin_registry import PluginRegistry
@pytest.fixture(autouse=True)
def _reset_plugin_hub():
old_registry = PluginHub._registry
old_connections = PluginHub._connections.copy()
old_pending = PluginHub._pending.copy()
old_lock = PluginHub._lock
old_loop = PluginHub._loop
yield
PluginHub._registry = old_registry
PluginHub._connections = old_connections
PluginHub._pending = old_pending
PluginHub._lock = old_lock
PluginHub._loop = old_loop
async def _setup_two_user_registry():
"""Set up a registry with two users, each having Unity instances.
Returns the configured registry. Also configures PluginHub to use it.
"""
registry = PluginRegistry()
loop = asyncio.get_running_loop()
PluginHub.configure(registry, loop)
await registry.register("sess-A1", "ProjectAlpha", "hashA1", "2022.3", user_id="userA")
await registry.register("sess-B1", "ProjectBeta", "hashB1", "2022.3", user_id="userB")
await registry.register("sess-A2", "ProjectGamma", "hashA2", "2022.3", user_id="userA")
return registry
class TestMultiUserSessionFiltering:
@pytest.mark.asyncio
async def test_get_sessions_filters_by_user(self):
"""PluginHub.get_sessions(user_id=X) returns only X's sessions."""
await _setup_two_user_registry()
sessions_a = await PluginHub.get_sessions(user_id="userA")
assert len(sessions_a.sessions) == 2
project_names = {s.project for s in sessions_a.sessions.values()}
assert project_names == {"ProjectAlpha", "ProjectGamma"}
sessions_b = await PluginHub.get_sessions(user_id="userB")
assert len(sessions_b.sessions) == 1
assert next(iter(sessions_b.sessions.values())
).project == "ProjectBeta"
@pytest.mark.asyncio
async def test_get_sessions_no_filter_returns_all_in_local_mode(self):
"""In local mode, PluginHub.get_sessions() without user_id returns everything."""
await _setup_two_user_registry()
all_sessions = await PluginHub.get_sessions()
assert len(all_sessions.sessions) == 3
@pytest.mark.asyncio
async def test_get_sessions_no_filter_raises_in_remote_hosted(self, monkeypatch):
"""In remote-hosted mode, PluginHub.get_sessions() without user_id raises."""
monkeypatch.setattr(config, "http_remote_hosted", True)
await _setup_two_user_registry()
with pytest.raises(ValueError, match="requires user_id"):
await PluginHub.get_sessions()
class TestResolveSessionIdIsolation:
@pytest.mark.asyncio
async def test_resolve_session_for_own_hash(self, monkeypatch):
"""User A can resolve their own project hash."""
monkeypatch.setattr(config, "http_remote_hosted", True)
await _setup_two_user_registry()
session_id = await PluginHub._resolve_session_id("hashA1", user_id="userA")
assert session_id == "sess-A1"
@pytest.mark.asyncio
async def test_cannot_resolve_other_users_hash(self, monkeypatch):
"""User A cannot resolve User B's project hash."""
monkeypatch.setattr(config, "http_remote_hosted", True)
monkeypatch.setenv("UNITY_MCP_SESSION_RESOLVE_MAX_WAIT_S", "0.1")
await _setup_two_user_registry()
# userA tries to resolve userB's hash -> should not find it
with pytest.raises(NoUnitySessionError):
await PluginHub._resolve_session_id("hashB1", user_id="userA")
class TestInstanceListResourceIsolation:
@pytest.mark.asyncio
async def test_unity_instances_resource_filters_by_user(self, monkeypatch):
"""The unity_instances resource should pass user_id and return filtered results."""
monkeypatch.setattr(config, "http_remote_hosted", True)
monkeypatch.setattr(config, "transport_mode", "http")
await _setup_two_user_registry()
from services.resources.unity_instances import unity_instances
from tests.integration.test_helpers import DummyContext
ctx = DummyContext()
ctx.set_state("user_id", "userA")
result = await unity_instances(ctx)
assert result["success"] is True
assert result["instance_count"] == 2
instance_names = {i["name"] for i in result["instances"]}
assert instance_names == {"ProjectAlpha", "ProjectGamma"}
assert "ProjectBeta" not in instance_names
class TestSetActiveInstanceIsolation:
@pytest.mark.asyncio
async def test_set_active_instance_only_sees_own_sessions(self, monkeypatch):
"""set_active_instance should only offer sessions belonging to the current user."""
monkeypatch.setattr(config, "http_remote_hosted", True)
monkeypatch.setattr(config, "transport_mode", "http")
await _setup_two_user_registry()
from services.tools.set_active_instance import set_active_instance
from transport.unity_instance_middleware import UnityInstanceMiddleware
from tests.integration.test_helpers import DummyContext
middleware = UnityInstanceMiddleware()
monkeypatch.setattr(
"services.tools.set_active_instance.get_unity_instance_middleware",
lambda: middleware,
)
ctx = DummyContext()
ctx.set_state("user_id", "userA")
result = await set_active_instance(ctx, "ProjectAlpha@hashA1")
assert result["success"] is True
assert middleware.get_active_instance(ctx) == "ProjectAlpha@hashA1"
@pytest.mark.asyncio
async def test_set_active_instance_rejects_other_users_instance(self, monkeypatch):
"""set_active_instance should not find another user's instance."""
monkeypatch.setattr(config, "http_remote_hosted", True)
monkeypatch.setattr(config, "transport_mode", "http")
await _setup_two_user_registry()
from services.tools.set_active_instance import set_active_instance
from transport.unity_instance_middleware import UnityInstanceMiddleware
from tests.integration.test_helpers import DummyContext
middleware = UnityInstanceMiddleware()
monkeypatch.setattr(
"services.tools.set_active_instance.get_unity_instance_middleware",
lambda: middleware,
)
ctx = DummyContext()
ctx.set_state("user_id", "userA")
# UserA tries to select UserB's instance -> should fail
result = await set_active_instance(ctx, "ProjectBeta@hashB1")
assert result["success"] is False