"""Hosted-dashboard bridge for MCP OAuth browser callbacks.""" import asyncio import threading import pytest def test_dashboard_flow_exposes_authorization_url_and_accepts_callback(): from tools.mcp_dashboard_oauth import DashboardOAuthFlow flow = DashboardOAuthFlow( flow_id="flow-1", server_name="reports", profile=None, hermes_home="/tmp/hermes-test", redirect_uri="https://agent.example/mcp/oauth/callback/flow-1", ) asyncio.run(flow.publish_authorization_url("https://idp.example/authorize?state=s1")) assert flow.snapshot() == { "flow_id": "flow-1", "server_name": "reports", "status": "authorization_required", "authorization_url": "https://idp.example/authorize?state=s1", "error": None, } flow.deliver_callback(code="code-1", state="s1", error=None) assert asyncio.run(flow.wait_for_callback()) == ("code-1", "s1") def test_dashboard_flow_rejects_wrong_state_without_consuming_callback(): from tools.mcp_dashboard_oauth import DashboardOAuthFlow flow = DashboardOAuthFlow( flow_id="flow-state", server_name="reports", profile=None, hermes_home="/tmp/hermes-test", redirect_uri="https://agent.example/mcp/oauth/callback/flow-state", ) asyncio.run( flow.publish_authorization_url( "https://idp.example/authorize?state=expected-state" ) ) with pytest.raises(ValueError, match="state mismatch"): flow.deliver_callback(code="attacker", state="wrong-state", error=None) flow.deliver_callback(code="legitimate", state="expected-state", error=None) assert asyncio.run(flow.wait_for_callback()) == ( "legitimate", "expected-state", ) def test_dashboard_flow_rejects_second_callback(): from tools.mcp_dashboard_oauth import DashboardOAuthFlow flow = DashboardOAuthFlow( flow_id="flow-2", server_name="reports", profile=None, hermes_home="/tmp/hermes-test", redirect_uri="https://agent.example/mcp/oauth/callback/flow-2", ) asyncio.run( flow.publish_authorization_url( "https://idp.example/authorize?state=state" ) ) flow.deliver_callback(code="first", state="state", error=None) with pytest.raises(ValueError, match="already received"): flow.deliver_callback(code="second", state="state", error=None) def test_dashboard_flow_accepts_only_one_concurrent_callback(): from tools.mcp_dashboard_oauth import DashboardOAuthFlow flow = DashboardOAuthFlow( flow_id="flow-race", server_name="reports", profile=None, hermes_home="/tmp/hermes-test", redirect_uri="https://agent.example/mcp/oauth/callback/flow-race", ) asyncio.run(flow.publish_authorization_url("https://idp.example/authorize?state=state")) start = threading.Barrier(3) outcomes: list[str] = [] def deliver(code: str) -> None: start.wait() try: flow.deliver_callback(code=code, state="state", error=None) outcomes.append("accepted") except ValueError: outcomes.append("rejected") workers = [threading.Thread(target=deliver, args=(code,)) for code in ("one", "two")] for worker in workers: worker.start() start.wait() for worker in workers: worker.join() assert sorted(outcomes) == ["accepted", "rejected"] def test_dashboard_flow_cannot_resurrect_after_terminal_error(): from tools.mcp_dashboard_oauth import DashboardOAuthFlow flow = DashboardOAuthFlow( flow_id="flow-terminal", server_name="reports", profile=None, hermes_home="/tmp/hermes-test", redirect_uri="https://agent.example/mcp/oauth/callback/flow-terminal", ) flow.mark_error("start timed out") with pytest.raises(RuntimeError, match="already ended"): asyncio.run( flow.publish_authorization_url( "https://idp.example/authorize?state=too-late" ) ) assert flow.status == "error" assert flow.authorization_url is None def test_dashboard_context_overrides_redirect_and_handlers(): from tools.mcp_dashboard_oauth import ( DashboardOAuthFlow, dashboard_oauth_flow, get_dashboard_oauth_flow, ) flow = DashboardOAuthFlow( flow_id="flow-3", server_name="reports", profile=None, hermes_home="/tmp/hermes-test", redirect_uri="https://agent.example/mcp/oauth/callback/flow-3", ) assert get_dashboard_oauth_flow() is None with dashboard_oauth_flow(flow): assert get_dashboard_oauth_flow() is flow assert get_dashboard_oauth_flow() is None def test_mcp_oauth_helpers_use_dashboard_flow_without_loopback_port(): from tools.mcp_dashboard_oauth import DashboardOAuthFlow, dashboard_oauth_flow from tools.mcp_oauth import ( HermesTokenStorage, _build_client_metadata, _configure_callback_port, _make_callback_waiter, _make_redirect_handler, ) flow = DashboardOAuthFlow( flow_id="flow-4", server_name="reports", profile=None, hermes_home="/tmp/hermes-test", redirect_uri="https://agent.example/mcp/oauth/callback/flow-4", ) cfg = {} with dashboard_oauth_flow(flow): assert _configure_callback_port(cfg, HermesTokenStorage("reports")) == 0 metadata = _build_client_metadata(cfg) assert str(metadata.redirect_uris[0]) == flow.redirect_uri asyncio.run( _make_redirect_handler(0)( "https://idp.example/authorize?state=state-4" ) ) flow.deliver_callback(code="code-4", state="state-4", error=None) assert asyncio.run(_make_callback_waiter(0)()) == ("code-4", "state-4") assert flow.authorization_url == "https://idp.example/authorize?state=state-4" def test_manager_build_allows_dashboard_flow_without_tty(tmp_path, monkeypatch): from tools.mcp_dashboard_oauth import DashboardOAuthFlow, dashboard_oauth_flow from tools.mcp_oauth_manager import MCPOAuthManager monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.setattr("tools.mcp_oauth.sys.stdin.isatty", lambda: False) flow = DashboardOAuthFlow( flow_id="flow-5", server_name="reports", profile=None, hermes_home="/tmp/hermes-test", redirect_uri="https://agent.example/api/mcp/oauth/callback/flow-5", ) with dashboard_oauth_flow(flow): provider = MCPOAuthManager().get_or_build_provider( "reports", "https://mcp.example/mcp", {} ) assert provider is not None assert str(provider.context.client_metadata.redirect_uris[0]) == flow.redirect_uri def test_manager_evict_preserves_persisted_oauth_state(tmp_path, monkeypatch): from tools.mcp_oauth import HermesTokenStorage from tools.mcp_oauth_manager import MCPOAuthManager, _ProviderEntry monkeypatch.setenv("HERMES_HOME", str(tmp_path)) storage = HermesTokenStorage("reports") storage._tokens_path().parent.mkdir(parents=True) storage._tokens_path().write_text( '{"access_token":"a","token_type":"Bearer"}' ) manager = MCPOAuthManager() manager._entries[manager._key("reports")] = _ProviderEntry( server_url="https://mcp.example/mcp", oauth_config={} ) manager.evict("reports") assert manager._key("reports") not in manager._entries assert storage._tokens_path().exists() def test_reconnect_mcp_server_signals_live_task(monkeypatch): from tools import mcp_tool class Event: called = False def set(self): self.called = True class Server: _reconnect_event = Event() server = Server() monkeypatch.setitem(mcp_tool._servers, "reports", server) monkeypatch.setattr(mcp_tool, "_mcp_loop", None) assert mcp_tool.reconnect_mcp_server("reports") is True assert server._reconnect_event.called is True def test_reconnect_mcp_server_keeps_manager_entry_until_live_task_rebuilds( tmp_path, monkeypatch ): from tools import mcp_tool from tools.mcp_oauth_manager import MCPOAuthManager, _ProviderEntry class Event: called = False def set(self): self.called = True class Server: _reconnect_event = Event() server = Server() manager = MCPOAuthManager() manager._entries[manager._key("reports", tmp_path)] = _ProviderEntry( server_url="https://mcp.example/mcp", oauth_config={} ) monkeypatch.setitem(mcp_tool._servers, "reports", server) monkeypatch.setattr(mcp_tool, "_mcp_loop", None) assert mcp_tool.reconnect_mcp_server("reports") is True assert manager._key("reports", tmp_path) in manager._entries def test_failed_reauth_rollback_preserves_newer_oauth_state(tmp_path, monkeypatch): from tools.mcp_oauth import HermesTokenStorage monkeypatch.setenv("HERMES_HOME", str(tmp_path)) storage = HermesTokenStorage("reports") storage._tokens_path().parent.mkdir(parents=True) storage._tokens_path().write_text("OLD") backup = storage.snapshot() storage.remove() storage._tokens_path().write_text("FRESH") storage.restore(backup, only_if_absent=True) assert storage._tokens_path().read_text() == "FRESH"