Files

144 lines
5.0 KiB
Python

"""Cancellation wakes the actual callback worker without crossing profile ownership."""
import asyncio
import threading
import pytest
from tools.mcp_dashboard_oauth import DashboardOAuthFlow
from tui_gateway import mcp_oauth_sessions as sessions
@pytest.mark.parametrize("client_redirect", [False, True])
def test_cancel_is_scoped_idempotent_and_releases_worker(
tmp_path, monkeypatch, client_redirect
):
monkeypatch.setattr(sessions, "_sessions", {})
finished = threading.Event()
def worker(session_id, *_args):
flow = sessions._sessions[session_id]["flow"]
try:
asyncio.run(
flow.publish_authorization_url(
"https://idp.example/authorize?state=test"
)
)
asyncio.run(flow.wait_for_callback(timeout=10))
flow.mark_approved()
except RuntimeError as exc:
flow.mark_error(str(exc))
finally:
flow.mark_worker_done()
finished.set()
monkeypatch.setattr(sessions, "_worker", worker)
home = str(tmp_path / "origin")
monkeypatch.setenv("HERMES_HOME", home)
result = sessions.start_flow(
home,
"reports",
{"url": "https://mcp.example"},
client_redirect_uri="http://127.0.0.1:49152/callback"
if client_redirect
else None,
)
sid = result["session_id"]
rec = sessions._sessions[sid]
try:
assert (
sessions.cancel_flow(sid, "reports", str(tmp_path / "other"))["ok"] is False
)
assert sessions.cancel_flow(sid, "other", home)["ok"] is False
assert rec["flow"].snapshot()["status"] == "authorization_required"
assert sessions.cancel_flow(sid, "reports", home)["ok"] is True
assert finished.wait(5), (
"cancel must wake the worker, not leave a 5-minute occupied slot"
)
assert sessions.poll_flow(sid, "reports")["status"] == "error"
assert rec["httpd"] is None
assert sessions.cancel_flow(sid, "reports", home)["ok"] is True
assert (
sessions.deliver_callback_flow(sid, "reports", code="late", state="test")[
"ok"
]
is False
)
# A new start can take the per-server slot as soon as the old worker exits.
finished.clear()
retry = sessions.start_flow(
home,
"reports",
{"url": "https://mcp.example"},
client_redirect_uri="http://127.0.0.1:49152/callback",
)
assert sessions.cancel_flow(retry["session_id"], "reports", home)["ok"] is True
assert finished.wait(5)
finally:
rec["flow"].mark_error("test cleanup")
sessions._shutdown_listener(rec)
finished.wait(5)
@pytest.mark.parametrize("operation", ["poll", "callback", "cancel"])
def test_session_operations_require_resolved_owner(tmp_path, monkeypatch, operation):
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
home = str(tmp_path / "owner")
flow = DashboardOAuthFlow(
"owned", "reports", None, home, "http://127.0.0.1:49152/callback"
)
asyncio.run(flow.publish_authorization_url("https://idp.example/authorize?state=test"))
monkeypatch.setattr(sessions, "_sessions", {
"owned": {"flow": flow, "server_name": "reports", "hermes_home": home, "httpd": None}
})
def invoke():
if operation == "poll":
return sessions.poll_flow("owned", "reports")
if operation == "callback":
return sessions.deliver_callback_flow("owned", "reports", code="valid", state="test")
from hermes_constants import get_hermes_home
return sessions.cancel_flow("owned", "reports", str(get_hermes_home()))
token = set_hermes_home_override(tmp_path / "other")
try:
rejected = invoke()
finally:
reset_hermes_home_override(token)
assert "profile mismatch" in (rejected.get("error_message") or "")
assert "auth_url" not in rejected
assert flow.snapshot()["status"] == "authorization_required"
token = set_hermes_home_override(home)
try:
accepted = invoke()
finally:
reset_hermes_home_override(token)
assert accepted.get("ok", accepted.get("status") == "pending") is True
def test_cancel_does_not_revoke_an_approved_flow(tmp_path, monkeypatch):
home = str(tmp_path)
flow = DashboardOAuthFlow(
"approved", "reports", None, home, "http://127.0.0.1:49152/callback"
)
flow.mark_approved()
flow.mark_worker_done()
monkeypatch.setattr(
sessions,
"_sessions",
{
"approved": {
"flow": flow,
"server_name": "reports",
"hermes_home": home,
"httpd": None,
}
},
)
assert sessions.cancel_flow("approved", "reports", home) == {
"ok": True,
"status": "approved",
}
assert sessions.cancel_flow("missing", "reports", home)["ok"] is False