diff --git a/muxplex/main.py b/muxplex/main.py index 478b529..b7628f0 100644 --- a/muxplex/main.py +++ b/muxplex/main.py @@ -962,6 +962,26 @@ async def federation_sessions(request: Request) -> list[dict]: return all_sessions +@app.post("/api/federation/generate-key") +async def federation_generate_key() -> dict: + """Generate a new federation key, write it to FEDERATION_KEY_PATH, and return it. + + Creates the parent directory (mode 0700) if it doesn't exist. + Writes the key with a trailing newline and sets file mode to 0600. + + Returns {key: str, path: str}. + """ + import secrets as _secrets + from muxplex.settings import FEDERATION_KEY_PATH + + key = _secrets.token_urlsafe(32) + path = FEDERATION_KEY_PATH + path.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + path.write_text(key + "\n") + path.chmod(0o600) + return {"key": key, "path": str(path)} + + @app.post("/api/federation/{remote_id}/connect/{session_name}") async def federation_connect( remote_id: str, session_name: str, request: Request diff --git a/muxplex/tests/test_api.py b/muxplex/tests/test_api.py index 7a3aabd..a8186b1 100644 --- a/muxplex/tests/test_api.py +++ b/muxplex/tests/test_api.py @@ -1785,6 +1785,49 @@ def test_federation_connect_returns_503_when_remote_unreachable( assert response.status_code == 503 +# --------------------------------------------------------------------------- +# POST /api/federation/generate-key (task-13) +# --------------------------------------------------------------------------- + + +def test_federation_generate_key_creates_file(client, tmp_path, monkeypatch): + """POST /api/federation/generate-key creates a key file and returns the key in the response. + + - Endpoint returns 200 with {"key": , "path": } + - Returned key length > 20 + - The key file is created at the redirected FEDERATION_KEY_PATH + - The file contents match the returned key (with trailing newline stripped) + """ + import muxplex.settings as settings_mod + + key_path = tmp_path / "federation_key" + monkeypatch.setattr(settings_mod, "FEDERATION_KEY_PATH", key_path) + + response = client.post("/api/federation/generate-key") + assert response.status_code == 200, ( + f"Expected 200, got {response.status_code}: {response.text}" + ) + + data = response.json() + assert "key" in data, f"Response must include 'key' field, got: {data}" + assert "path" in data, f"Response must include 'path' field, got: {data}" + + returned_key = data["key"] + assert len(returned_key) > 20, ( + f"Key must be longer than 20 chars, got length {len(returned_key)}" + ) + + # Verify file was created + assert key_path.exists(), f"Key file must be created at {key_path}" + + # Verify file contents match returned key + file_contents = key_path.read_text().strip() + assert file_contents == returned_key, ( + f"File contents must match returned key. " + f"File: {file_contents!r}, key: {returned_key!r}" + ) + + def test_federation_connect_returns_502_when_remote_returns_error_status( client, monkeypatch, tmp_path ):