Move the game-independent machinery (lobby, live-game store, websocket, deadline scheduler, match history, leaderboards) into a new tavolo-platform distribution behind a GameEngine contract, the scopone scientifico rules plus a platform adapter into tavolo-scopone, and keep only the composition root in tavolo-app. The three distributions share the tavolo namespace (PEP 420, kaya-style monorepo). Match history becomes fully generic: Match carries the engine's result JSON and MatchPlayer points/details instead of scopone-shaped team columns (migration 3 backfills existing rows). Lobby creation takes an opaque per-game options object and websocket actions dispatch to the session's engine. Tests: platform suite runs against a DummyEngine toy game, scopone keeps the rules tests plus new adapter tests, server/tests covers the wired stack end to end (194 tests, was 143).
205 lines
9.1 KiB
Python
205 lines
9.1 KiB
Python
"""Game lobby route tests via kaya's ASGI transport, on the DummyEngine."""
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from helpers import async_test, make_platform, oidc_user
|
|
|
|
|
|
class GamesRouteTest(unittest.TestCase):
|
|
@async_test
|
|
async def test_create_requires_auth(self) -> None:
|
|
app, _, _ = make_platform()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
response = await client.post("/api/games", json={})
|
|
self.assertEqual(401, response.status_code)
|
|
self.assertEqual({"error": "unauthenticated"}, response.json())
|
|
|
|
@async_test
|
|
async def test_create_and_read_lobby(self) -> None:
|
|
app, platform, _ = make_platform()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user(platform.oidc, "alice"):
|
|
created = await client.post(
|
|
"/api/games", json={"options": {"target": 5}}
|
|
)
|
|
self.assertEqual(201, created.status_code)
|
|
body = created.json()
|
|
self.assertEqual("lobby", body["phase"])
|
|
self.assertEqual(1, body["seats_open"])
|
|
self.assertEqual(5, body["target"])
|
|
self.assertEqual("dummy", body["game_type"])
|
|
self.assertEqual("A", body["players"][0]["team"])
|
|
self.assertEqual(6, len(body["join_code"]))
|
|
game_id = body["id"]
|
|
|
|
with oidc_user(platform.oidc, "alice"):
|
|
snapshot = await client.get(f"/api/games/{game_id}")
|
|
self.assertEqual(200, snapshot.status_code)
|
|
snap = snapshot.json()
|
|
self.assertEqual(game_id, snap["id"])
|
|
self.assertEqual("dummy", snap["game_type"])
|
|
self.assertTrue(snap["viewer_seated"])
|
|
|
|
with oidc_user(platform.oidc, "mallory"):
|
|
forbidden = await client.get(f"/api/games/{game_id}")
|
|
self.assertEqual(403, forbidden.status_code)
|
|
|
|
@async_test
|
|
async def test_join_starts_game(self) -> None:
|
|
app, platform, _ = make_platform()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user(platform.oidc, "alice"):
|
|
created = await client.post("/api/games", json={})
|
|
code = created.json()["join_code"]
|
|
|
|
with oidc_user(platform.oidc, "bob"):
|
|
started = await client.post("/api/games/join", json={"code": code})
|
|
self.assertEqual(200, started.status_code)
|
|
state = started.json()
|
|
# The second join started the match, so the response is the
|
|
# personalized view rather than the lobby payload.
|
|
self.assertTrue(state["started"])
|
|
self.assertFalse(state["finished"])
|
|
self.assertEqual(0, state["plays"])
|
|
self.assertTrue(state["viewer_seated"])
|
|
|
|
@async_test
|
|
async def test_create_rejects_bad_options(self) -> None:
|
|
app, platform, _ = make_platform()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user(platform.oidc, "alice"):
|
|
zero = await client.post(
|
|
"/api/games", json={"options": {"target": 0}}
|
|
)
|
|
text = await client.post(
|
|
"/api/games", json={"options": {"target": "three"}}
|
|
)
|
|
non_object = await client.post(
|
|
"/api/games", json={"options": [1, 2]}
|
|
)
|
|
self.assertEqual(400, zero.status_code)
|
|
self.assertEqual(400, text.status_code)
|
|
self.assertEqual(400, non_object.status_code)
|
|
|
|
@async_test
|
|
async def test_join_errors(self) -> None:
|
|
app, platform, _ = make_platform()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user(platform.oidc, "alice"):
|
|
created = await client.post("/api/games", json={})
|
|
code = created.json()["join_code"]
|
|
|
|
with oidc_user(platform.oidc, "bob"):
|
|
unknown = await client.post("/api/games/join", json={"code": "ZZZZZZ"})
|
|
self.assertEqual(404, unknown.status_code)
|
|
|
|
with oidc_user(platform.oidc, "alice"):
|
|
duplicate = await client.post("/api/games/join", json={"code": code})
|
|
self.assertEqual(409, duplicate.status_code)
|
|
|
|
with oidc_user(platform.oidc, "bob"):
|
|
missing = await client.post("/api/games/join", json={})
|
|
self.assertEqual(400, missing.status_code)
|
|
|
|
with oidc_user(platform.oidc, "bob"):
|
|
await client.post("/api/games/join", json={"code": code})
|
|
with oidc_user(platform.oidc, "erin"):
|
|
late = await client.post("/api/games/join", json={"code": code})
|
|
self.assertEqual(409, late.status_code)
|
|
|
|
@async_test
|
|
async def test_get_unknown_game(self) -> None:
|
|
app, platform, _ = make_platform()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user(platform.oidc, "alice"):
|
|
response = await client.get("/api/games/does-not-exist")
|
|
self.assertEqual(404, response.status_code)
|
|
|
|
|
|
class GameTypesRouteTest(unittest.TestCase):
|
|
@async_test
|
|
async def test_lists_available_game_types(self) -> None:
|
|
app, _, _ = make_platform()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
response = await client.get("/api/game-types")
|
|
self.assertEqual(200, response.status_code)
|
|
results = response.json()["results"]
|
|
self.assertEqual(["dummy"], [g["id"] for g in results])
|
|
self.assertEqual("Dummy game", results[0]["name"])
|
|
self.assertTrue(results[0]["description"])
|
|
self.assertEqual(2, results[0]["min_players"])
|
|
self.assertEqual(2, results[0]["max_players"])
|
|
self.assertIn("target", results[0]["options_schema"]["properties"])
|
|
|
|
@async_test
|
|
async def test_create_defaults_game_type(self) -> None:
|
|
app, platform, _ = make_platform()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user(platform.oidc, "alice"):
|
|
created = await client.post("/api/games", json={})
|
|
self.assertEqual(201, created.status_code)
|
|
self.assertEqual("dummy", created.json()["game_type"])
|
|
|
|
@async_test
|
|
async def test_create_with_explicit_game_type(self) -> None:
|
|
app, platform, _ = make_platform()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user(platform.oidc, "alice"):
|
|
created = await client.post(
|
|
"/api/games", json={"game_type": "dummy"}
|
|
)
|
|
self.assertEqual(201, created.status_code)
|
|
body = created.json()
|
|
self.assertEqual("dummy", body["game_type"])
|
|
|
|
with oidc_user(platform.oidc, "alice"):
|
|
snapshot = await client.get(f"/api/games/{body['id']}")
|
|
self.assertEqual("dummy", snapshot.json()["game_type"])
|
|
|
|
@async_test
|
|
async def test_create_rejects_unknown_game_type(self) -> None:
|
|
app, platform, _ = make_platform()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user(platform.oidc, "alice"):
|
|
unknown = await client.post("/api/games", json={"game_type": "briscola"})
|
|
non_string = await client.post("/api/games", json={"game_type": 42})
|
|
self.assertEqual(400, unknown.status_code)
|
|
self.assertEqual(400, non_string.status_code)
|
|
|
|
|
|
class MeRouteTest(unittest.TestCase):
|
|
@async_test
|
|
async def test_me_authenticated(self) -> None:
|
|
app, platform, _ = make_platform()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user(platform.oidc, "alice"):
|
|
response = await client.get("/api/me")
|
|
self.assertEqual(200, response.status_code)
|
|
self.assertEqual({"sub": "alice", "name": "alice"}, response.json())
|
|
|
|
@async_test
|
|
async def test_me_unauthenticated(self) -> None:
|
|
app, _, _ = make_platform()
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
response = await client.get("/api/me")
|
|
self.assertEqual(401, response.status_code)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|