"""Tests for UnityInstanceMiddleware auth enforcement in remote-hosted mode.""" import asyncio import sys from unittest.mock import AsyncMock, Mock, patch import pytest from core.config import config from tests.integration.test_helpers import DummyContext class TestMiddlewareAuthEnforcement: @pytest.mark.asyncio async def test_remote_hosted_requires_user_id(self, monkeypatch): """_inject_unity_instance should raise RuntimeError when remote-hosted and no user_id.""" monkeypatch.setattr(config, "http_remote_hosted", True) from transport.unity_instance_middleware import UnityInstanceMiddleware middleware = UnityInstanceMiddleware() # Mock _resolve_user_id to return None (no API key / failed validation) monkeypatch.setattr(middleware, "_resolve_user_id", AsyncMock(return_value=None)) ctx = DummyContext() middleware_ctx = Mock() middleware_ctx.fastmcp_context = ctx with pytest.raises(RuntimeError, match="API key authentication required"): await middleware._inject_unity_instance(middleware_ctx) @pytest.mark.asyncio async def test_sets_user_id_in_context_state(self, monkeypatch): """_inject_unity_instance should set user_id in ctx state when resolved.""" monkeypatch.setattr(config, "http_remote_hosted", True) from transport.unity_instance_middleware import UnityInstanceMiddleware middleware = UnityInstanceMiddleware() monkeypatch.setattr(middleware, "_resolve_user_id", AsyncMock(return_value="user-55")) # We need PluginHub to be configured for the session resolution path # But we don't need it to actually find a session for this test from transport.plugin_hub import PluginHub from transport.plugin_registry import PluginRegistry registry = PluginRegistry() loop = asyncio.get_running_loop() PluginHub.configure(registry, loop) ctx = DummyContext() ctx.client_id = "client-1" middleware_ctx = Mock() middleware_ctx.fastmcp_context = ctx # Set an active instance so the middleware doesn't try to auto-select middleware.set_active_instance(ctx, "Proj@hash1") # Register a matching session so resolution doesn't fail await registry.register("s1", "Proj", "hash1", "2022", user_id="user-55") await middleware._inject_unity_instance(middleware_ctx) assert ctx.get_state("user_id") == "user-55" class TestMiddlewareSessionKey: def test_get_session_key_uses_user_id_fallback(self): """When no client_id, middleware should use user:$user_id as session key.""" from transport.unity_instance_middleware import UnityInstanceMiddleware middleware = UnityInstanceMiddleware() ctx = DummyContext() # Simulate no client_id attribute if hasattr(ctx, "client_id"): delattr(ctx, "client_id") ctx.set_state("user_id", "user-77") key = middleware.get_session_key(ctx) assert key == "user:user-77" def test_get_session_key_prefers_client_id(self): """client_id should take precedence over user_id.""" from transport.unity_instance_middleware import UnityInstanceMiddleware middleware = UnityInstanceMiddleware() ctx = DummyContext() ctx.client_id = "client-abc" ctx.set_state("user_id", "user-77") key = middleware.get_session_key(ctx) assert key == "client-abc" class TestAutoSelectDisabledRemoteHosted: @pytest.mark.asyncio async def test_auto_select_returns_none_in_remote_hosted(self, monkeypatch): """_maybe_autoselect_instance should return None in remote-hosted mode even with one session.""" monkeypatch.setattr(config, "http_remote_hosted", True) monkeypatch.setattr(config, "transport_mode", "http") # Re-import middleware to pick up the stubbed transport module monkeypatch.delitem( sys.modules, "transport.unity_instance_middleware", raising=False) from transport.unity_instance_middleware import UnityInstanceMiddleware, PluginHub as HubRef # Configure PluginHub with one session so auto-select has something to find from transport.plugin_registry import PluginRegistry registry = PluginRegistry() await registry.register("s1", "Proj", "h1", "2022", user_id="userA") loop = asyncio.get_running_loop() HubRef.configure(registry, loop) middleware = UnityInstanceMiddleware() ctx = DummyContext() ctx.client_id = "client-1" result = await middleware._maybe_autoselect_instance(ctx) # Remote-hosted mode should NOT auto-select (early return at the transport check) assert result is None class TestHttpAuthBehavior: @pytest.mark.asyncio async def test_http_local_does_not_require_user_id(self, monkeypatch): """HTTP local mode should allow requests without user_id.""" monkeypatch.setattr(config, "http_remote_hosted", False) monkeypatch.setattr(config, "transport_mode", "http") from transport import unity_transport async def fake_send_command_for_instance(*_args, **_kwargs): return {"success": True, "data": {"ok": True}} monkeypatch.setattr( unity_transport.PluginHub, "send_command_for_instance", fake_send_command_for_instance, ) async def _unused_send_fn(*_args, **_kwargs): raise AssertionError("send_fn should not be used in HTTP mode") result = await unity_transport.send_with_unity_instance( _unused_send_fn, None, "ping", {} ) assert result["success"] is True assert result["data"] == {"ok": True} @pytest.mark.asyncio async def test_http_remote_requires_user_id(self, monkeypatch): """HTTP remote-hosted mode should reject requests without user_id.""" monkeypatch.setattr(config, "http_remote_hosted", True) monkeypatch.setattr(config, "transport_mode", "http") from transport import unity_transport async def _unused_send_fn(*_args, **_kwargs): raise AssertionError("send_fn should not be used in HTTP mode") result = await unity_transport.send_with_unity_instance( _unused_send_fn, None, "ping", {} ) assert result["success"] is False assert result["error"] == "auth_required"