feat: add POST /api/federation/{remote_id}/connect proxy
This commit is contained in:
@@ -962,6 +962,41 @@ async def federation_sessions(request: Request) -> list[dict]:
|
||||
return all_sessions
|
||||
|
||||
|
||||
@app.post("/api/federation/{remote_id}/connect/{session_name}")
|
||||
async def federation_connect(
|
||||
remote_id: str, session_name: str, request: Request
|
||||
) -> dict:
|
||||
"""Proxy a connect POST to a remote instance to spawn its ttyd.
|
||||
|
||||
Looks up the remote by integer index into ``remote_instances`` in settings,
|
||||
sends ``POST {remote_url}/api/sessions/{session_name}/connect`` with a
|
||||
Bearer auth header, and returns the remote's JSON response.
|
||||
|
||||
Raises HTTP 404 if ``remote_id`` is not a valid integer index.
|
||||
"""
|
||||
settings = load_settings()
|
||||
remotes = settings.get("remote_instances", [])
|
||||
try:
|
||||
idx = int(remote_id)
|
||||
remote = remotes[idx]
|
||||
except (ValueError, IndexError):
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Remote instance '{remote_id}' not found",
|
||||
)
|
||||
|
||||
remote_url: str = remote.get("url", "").rstrip("/")
|
||||
remote_key: str = remote.get("key", "")
|
||||
url = f"{remote_url}/api/sessions/{session_name}/connect"
|
||||
|
||||
http_client: httpx.AsyncClient = request.app.state.federation_client
|
||||
resp = await http_client.post(
|
||||
url,
|
||||
headers={"Authorization": f"Bearer {remote_key}"},
|
||||
)
|
||||
return resp.json()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Static file serving — MUST come after all API routes (first-match-wins)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1633,3 +1633,103 @@ def test_federation_sessions_includes_remote_failure_status(
|
||||
entry = failure_entries[0]
|
||||
assert entry["status"] == "unreachable"
|
||||
assert entry.get("remoteId") == "remote-1"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /api/federation/{remote_id}/connect/{session_name} (task-12)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_federation_connect_proxies_to_remote(client, monkeypatch, tmp_path):
|
||||
"""POST /api/federation/{remote_id}/connect/{session_name} proxies POST to remote's connect endpoint.
|
||||
|
||||
Looks up remote by integer index, sends POST {remote_url}/api/sessions/{session_name}/connect
|
||||
with Bearer auth header, and returns the remote's JSON response.
|
||||
"""
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import muxplex.settings as settings_mod
|
||||
|
||||
settings_path = tmp_path / "settings.json"
|
||||
monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_path)
|
||||
|
||||
settings_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"remote_instances": [
|
||||
{
|
||||
"url": "http://remote-host:8088",
|
||||
"key": "secret-key-123",
|
||||
"name": "remote-host",
|
||||
"id": "remote-0",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Track what POST was called with
|
||||
post_calls = []
|
||||
|
||||
async def mock_post(url, **kwargs):
|
||||
post_calls.append({"url": url, "kwargs": kwargs})
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.json.return_value = {"active_session": "my-session", "ttyd_port": 7682}
|
||||
return mock_resp
|
||||
|
||||
mock_fed_client = MagicMock()
|
||||
mock_fed_client.post = mock_post
|
||||
monkeypatch.setattr(client.app.state, "federation_client", mock_fed_client)
|
||||
|
||||
response = client.post("/api/federation/0/connect/my-session")
|
||||
assert response.status_code == 200
|
||||
|
||||
# Verify the POST was made to the correct URL with session name
|
||||
assert len(post_calls) == 1, f"Expected exactly 1 POST call, got {len(post_calls)}"
|
||||
call = post_calls[0]
|
||||
assert call["url"] == "http://remote-host:8088/api/sessions/my-session/connect", (
|
||||
f"Expected POST to remote connect URL, got: {call['url']}"
|
||||
)
|
||||
|
||||
# Verify Bearer auth was included
|
||||
headers = call["kwargs"].get("headers", {})
|
||||
assert headers.get("Authorization") == "Bearer secret-key-123", (
|
||||
f"Expected Bearer auth header, got: {headers}"
|
||||
)
|
||||
|
||||
# Verify the response is the remote's JSON
|
||||
data = response.json()
|
||||
assert data["active_session"] == "my-session"
|
||||
assert data["ttyd_port"] == 7682
|
||||
|
||||
|
||||
def test_federation_connect_returns_404_for_invalid_remote_id(client, monkeypatch, tmp_path):
|
||||
"""POST /api/federation/{remote_id}/connect/{session_name} returns 404 when remote_id is out of range."""
|
||||
import json
|
||||
|
||||
import muxplex.settings as settings_mod
|
||||
|
||||
settings_path = tmp_path / "settings.json"
|
||||
monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_path)
|
||||
|
||||
# No remote instances configured
|
||||
settings_path.write_text(json.dumps({"remote_instances": []}))
|
||||
|
||||
response = client.post("/api/federation/0/connect/my-session")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_federation_connect_returns_404_for_non_integer_remote_id(client, monkeypatch, tmp_path):
|
||||
"""POST /api/federation/{remote_id}/connect/{session_name} returns 404 when remote_id is not an integer."""
|
||||
import json
|
||||
|
||||
import muxplex.settings as settings_mod
|
||||
|
||||
settings_path = tmp_path / "settings.json"
|
||||
monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_path)
|
||||
settings_path.write_text(json.dumps({"remote_instances": []}))
|
||||
|
||||
response = client.post("/api/federation/not-an-int/connect/my-session")
|
||||
assert response.status_code == 404
|
||||
|
||||
Reference in New Issue
Block a user