260b7a38da
- 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
55 lines
2.1 KiB
Python
55 lines
2.1 KiB
Python
"""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"]
|