125 lines
4.9 KiB
Python
125 lines
4.9 KiB
Python
"""Tests for the viewer reverse-proxy app."""
|
|
|
|
import httpx
|
|
import pytest
|
|
from starlette.testclient import TestClient
|
|
|
|
from mcp_proxy.viewer import _DISPLAY_NAMES, _render_shell, create_viewer_app
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shell rendering
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRenderShell:
|
|
def test_empty_registry(self):
|
|
html = _render_shell({})
|
|
assert "No services available" in html
|
|
|
|
def test_single_service(self):
|
|
html = _render_shell({"jira": 9000})
|
|
assert "Services" in html
|
|
assert "1 services" in html
|
|
assert 'data-service="jira"' in html
|
|
assert 'src="/__viewer__/set?app=jira"' in html
|
|
|
|
def test_multiple_services(self):
|
|
html = _render_shell({"jira": 9000, "slack": 9001, "google_mail": 9002})
|
|
assert "3 services" in html
|
|
for name in ("jira", "slack", "google_mail"):
|
|
assert f'data-service="{name}"' in html
|
|
|
|
def test_display_names_used(self):
|
|
html = _render_shell({"jira": 9000})
|
|
assert _DISPLAY_NAMES["jira"] in html # "Issues"
|
|
|
|
def test_unknown_service_gets_title_case(self):
|
|
html = _render_shell({"my_custom_service": 9000})
|
|
assert "My Custom Service" in html
|
|
|
|
def test_services_sorted(self):
|
|
html = _render_shell({"slack": 9001, "jira": 9000})
|
|
jira_pos = html.index('data-service="jira"')
|
|
slack_pos = html.index('data-service="slack"')
|
|
assert jira_pos < slack_pos
|
|
|
|
def test_first_service_is_default_iframe(self):
|
|
html = _render_shell({"slack": 9001, "jira": 9000})
|
|
# sorted → jira comes first
|
|
assert 'src="/__viewer__/set?app=jira"' in html
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Viewer app routes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestViewerApp:
|
|
@pytest.fixture
|
|
def app(self):
|
|
return create_viewer_app(
|
|
service_registry={"test_svc": 9999, "other": 8888},
|
|
proxy_token="secret123",
|
|
)
|
|
|
|
@pytest.fixture
|
|
def client(self, app):
|
|
return TestClient(app, raise_server_exceptions=False)
|
|
|
|
def test_index_returns_html(self, client):
|
|
resp = client.get("/")
|
|
assert resp.status_code == 200
|
|
assert "Services" in resp.text
|
|
|
|
def test_reverse_proxy_unknown_service_returns_404(self, client):
|
|
resp = client.get("/__viewer__/set?app=nonexistent")
|
|
assert resp.status_code == 404
|
|
assert "Unknown service" in resp.text
|
|
|
|
def test_iframe_fallback_uses_server_state_when_cookie_blocked(self, app, monkeypatch):
|
|
"""When embedded in an iframe, browsers may block the cookie. The
|
|
viewer should fall back to server-side state when any referer is
|
|
present (indicating a redirect, not a direct navigation)."""
|
|
|
|
async def mock_request(self, *args, **kwargs):
|
|
return httpx.Response(200, text="service content")
|
|
|
|
monkeypatch.setattr(httpx.AsyncClient, "request", mock_request)
|
|
client = TestClient(app, raise_server_exceptions=False)
|
|
|
|
# First, select a service via /__viewer__/set to populate server state.
|
|
resp = client.get("/__viewer__/set?app=test_svc", follow_redirects=False)
|
|
assert resp.status_code == 307
|
|
|
|
# Now simulate an iframe request with NO cookie but a referer present
|
|
# (browser stripped the path but kept the origin).
|
|
resp = client.get(
|
|
"/",
|
|
headers={"referer": "http://some-outer-host/"},
|
|
cookies={}, # no cookie — blocked by browser
|
|
)
|
|
# Should proxy to the service, not re-render the shell
|
|
assert resp.status_code == 200
|
|
assert "service content" in resp.text
|
|
assert "Services" not in resp.text # not the shell HTML
|
|
|
|
def test_iframe_fallback_not_triggered_without_referer(self, client):
|
|
"""Without a referer, the server state fallback should NOT activate —
|
|
direct navigation should always show the shell."""
|
|
resp = client.get("/", cookies={})
|
|
assert resp.status_code == 200
|
|
assert "Services" in resp.text
|
|
|
|
def test_reverse_proxy_connection_error_returns_502(self, app, monkeypatch):
|
|
# Mock httpx to simulate a connection error regardless of port state
|
|
async def mock_request(self, *args, **kwargs):
|
|
raise httpx.ConnectError("Connection refused")
|
|
|
|
monkeypatch.setattr(httpx.AsyncClient, "request", mock_request)
|
|
client = TestClient(app, raise_server_exceptions=False)
|
|
# Set the service cookie first, then make a proxied request
|
|
client.cookies.set("__viewer_service", "test_svc")
|
|
resp = client.get("/some-page", headers={"referer": "http://testserver/"})
|
|
assert resp.status_code == 502
|
|
assert "Service unavailable" in resp.text
|