PC3 Phase 2.2: game-server orchestration + POST /api/worlds/{id}/join
- backend/app/services/game_servers.py: an in-process supervisor. One
headless Godot dedicated-server process per world, keyed by world_id,
spawned on demand from the join endpoint, port-allocated from a
configured range, with a background reaper that kills a world's server
once its status file reports 0 players for the idle timeout. Off by
default (GAME_SERVERS_ENABLED); the checkpoint/playtest launcher enables it.
- POST /api/worlds/{id}/join -> {world_id, host, port}; 404 for
unknown/hidden worlds, 503 when orchestration is disabled. Auth required.
- main.py lifespan starts/stops the supervisor.
- config.py: GODOT_BIN, GAME_PROJECT_DIR, GAME_SERVER_HOST, port range,
idle timeout.
- tests/test_world_join.py (auth / 404 / visibility / 503) -- suite 72 passed.
- checkpoint runner also runs the 2p multiplayer shell test.
Verified end to end: enabled orchestration, POST /join spawned the Godot
server, a real client connected to the returned address and moved; backend
shutdown reaped the process.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
"""Tests for POST /api/worlds/{id}/join.
|
||||
|
||||
The happy path spawns a real Godot process, so it's covered by a manual /
|
||||
checkpoint integration test, not here. These cover the envelope: auth,
|
||||
visibility, and the 503 when orchestration is disabled (the default).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
|
||||
def _auth(client: TestClient, name: str) -> tuple[dict, int]:
|
||||
r = client.post("/api/auth/register", json={
|
||||
"username": name, "email": f"{name}@e.com", "password": "secret123", "over_18": True,
|
||||
})
|
||||
assert r.status_code == 201, r.text
|
||||
headers = {"Authorization": f"Bearer {r.json()['access_token']}"}
|
||||
me = client.get("/api/users/me", headers=headers)
|
||||
return headers, me.json()["id"]
|
||||
|
||||
|
||||
def _make_world(client: TestClient, headers: dict, visibility: str = "public") -> int:
|
||||
r = client.post("/api/worlds/", headers=headers, json={"name": "W", "visibility": visibility})
|
||||
assert r.status_code == 201, r.text
|
||||
return r.json()["id"]
|
||||
|
||||
|
||||
class TestJoinWorld:
|
||||
def test_requires_auth(self):
|
||||
client = TestClient(app)
|
||||
assert client.post("/api/worlds/1/join").status_code == 401
|
||||
|
||||
def test_unknown_world_is_404(self):
|
||||
client = TestClient(app)
|
||||
headers, _ = _auth(client, "join_404")
|
||||
assert client.post("/api/worlds/999999/join", headers=headers).status_code == 404
|
||||
|
||||
def test_private_world_of_another_user_is_404(self):
|
||||
client = TestClient(app)
|
||||
owner_headers, _ = _auth(client, "join_owner")
|
||||
wid = _make_world(client, owner_headers, "private")
|
||||
other_headers, _ = _auth(client, "join_other")
|
||||
assert client.post(f"/api/worlds/{wid}/join", headers=other_headers).status_code == 404
|
||||
|
||||
def test_disabled_orchestration_returns_503(self):
|
||||
# GAME_SERVERS_ENABLED defaults to False in tests.
|
||||
client = TestClient(app)
|
||||
headers, _ = _auth(client, "join_503")
|
||||
wid = _make_world(client, headers, "public")
|
||||
r = client.post(f"/api/worlds/{wid}/join", headers=headers)
|
||||
assert r.status_code == 503
|
||||
assert "not enabled" in r.json()["detail"]
|
||||
Reference in New Issue
Block a user