CI / Build and push docker image (push) Successful in 2m6s
Partnership games go to 21: scopone scientifico (the tournament target) and scopa a quattro, while two-player scopa keeps its usual 11. The scopa package resolves the default from the player count and exposes a per variant options_schema default, so the lobby creation form shows the right value without frontend changes.
413 lines
18 KiB
Python
413 lines
18 KiB
Python
"""Game lobby route tests via kaya's ASGI transport (all registered games)."""
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from tavolo.app import app
|
|
from tests.helpers import async_test, oidc_user
|
|
|
|
|
|
class GamesRouteTest(unittest.TestCase):
|
|
@async_test
|
|
async def test_create_requires_auth(self) -> None:
|
|
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:
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user("alice"):
|
|
created = await client.post(
|
|
"/api/games", json={"options": {"target_score": 16}}
|
|
)
|
|
self.assertEqual(201, created.status_code)
|
|
body = created.json()
|
|
self.assertEqual("lobby", body["phase"])
|
|
self.assertEqual(3, body["seats_open"])
|
|
self.assertEqual(16, body["target_score"])
|
|
self.assertEqual(6, len(body["join_code"]))
|
|
game_id = body["id"]
|
|
|
|
with oidc_user("alice"):
|
|
snapshot = await client.get(f"/api/games/{game_id}")
|
|
self.assertEqual(200, snapshot.status_code)
|
|
self.assertEqual("alice", snapshot.json()["players"][0]["sub"])
|
|
|
|
# A logged-in non-player is a read-only spectator: no join
|
|
# code, and the view is marked as a spectator view.
|
|
with oidc_user("mallory"):
|
|
watched = await client.get(f"/api/games/{game_id}")
|
|
self.assertEqual(200, watched.status_code)
|
|
self.assertTrue(watched.json()["spectator"])
|
|
self.assertNotIn("join_code", watched.json())
|
|
|
|
# Anonymous users may watch too.
|
|
anonymous = await client.get(f"/api/games/{game_id}")
|
|
self.assertEqual(200, anonymous.status_code)
|
|
self.assertTrue(anonymous.json()["spectator"])
|
|
self.assertNotIn("join_code", anonymous.json())
|
|
|
|
@async_test
|
|
async def test_join_fills_seats_and_starts_game(self) -> None:
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user("alice"):
|
|
created = await client.post("/api/games", json={})
|
|
code = created.json()["join_code"]
|
|
|
|
for player in ("bob", "carol"):
|
|
with oidc_user(player):
|
|
joined = await client.post("/api/games/join", json={"code": code})
|
|
self.assertEqual(200, joined.status_code)
|
|
self.assertEqual("lobby", joined.json()["phase"])
|
|
|
|
with oidc_user("dave"):
|
|
started = await client.post("/api/games/join", json={"code": code})
|
|
self.assertEqual(200, started.status_code)
|
|
state = started.json()
|
|
game_id = state["id"]
|
|
self.assertEqual("playing", state["phase"])
|
|
self.assertEqual(4, len(state["players"]))
|
|
self.assertEqual([], state["table"])
|
|
for participant in state["players"]:
|
|
self.assertEqual(10, participant["cards_left"])
|
|
# The view is personalized to Dave: he sees his own hand in
|
|
# seat 3 but not Alice's in seat 0.
|
|
self.assertIn("hand", state["players"][3])
|
|
self.assertNotIn("hand", state["players"][0])
|
|
self.assertEqual(1, state["turn"])
|
|
|
|
# A spectator sees the table, the score and each player's
|
|
# captured count and scope — but no hand at all.
|
|
watched = await client.get(f"/api/games/{game_id}")
|
|
self.assertEqual(200, watched.status_code)
|
|
spect = watched.json()
|
|
self.assertTrue(spect["spectator"])
|
|
self.assertNotIn("join_code", spect)
|
|
self.assertEqual([], spect["table"])
|
|
self.assertEqual({"A": 0, "B": 0}, spect["scores"])
|
|
for participant in spect["players"]:
|
|
self.assertNotIn("hand", participant)
|
|
self.assertEqual(10, participant["cards_left"])
|
|
self.assertEqual(0, participant["captured_count"])
|
|
self.assertEqual(0, participant["scope"])
|
|
|
|
@async_test
|
|
async def test_create_napola_option(self) -> None:
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user("alice"):
|
|
default = await client.post("/api/games", json={})
|
|
self.assertEqual(201, default.status_code)
|
|
self.assertTrue(default.json()["napola"])
|
|
|
|
with oidc_user("alice"):
|
|
disabled = await client.post(
|
|
"/api/games", json={"options": {"napola": False}}
|
|
)
|
|
self.assertEqual(201, disabled.status_code)
|
|
self.assertFalse(disabled.json()["napola"])
|
|
|
|
with oidc_user("alice"):
|
|
invalid = await client.post(
|
|
"/api/games", json={"options": {"napola": "yes"}}
|
|
)
|
|
self.assertEqual(400, invalid.status_code)
|
|
|
|
@async_test
|
|
async def test_create_asso_pigliatutto_option(self) -> None:
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user("alice"):
|
|
default = await client.post("/api/games", json={})
|
|
self.assertEqual(201, default.status_code)
|
|
self.assertFalse(default.json()["asso_pigliatutto"])
|
|
|
|
with oidc_user("alice"):
|
|
enabled = await client.post(
|
|
"/api/games", json={"options": {"asso_pigliatutto": True}}
|
|
)
|
|
self.assertEqual(201, enabled.status_code)
|
|
self.assertTrue(enabled.json()["asso_pigliatutto"])
|
|
|
|
with oidc_user("alice"):
|
|
invalid = await client.post(
|
|
"/api/games", json={"options": {"asso_pigliatutto": "yes"}}
|
|
)
|
|
self.assertEqual(400, invalid.status_code)
|
|
|
|
@async_test
|
|
async def test_preview_join_code(self) -> None:
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user("alice"):
|
|
created = await client.post(
|
|
"/api/games",
|
|
json={
|
|
"options": {
|
|
"target_score": 16,
|
|
"napola": False,
|
|
"asso_pigliatutto": True,
|
|
}
|
|
},
|
|
)
|
|
code = created.json()["join_code"]
|
|
|
|
response = await client.get(f"/api/games/preview?code={code}")
|
|
self.assertEqual(401, response.status_code)
|
|
|
|
with oidc_user("bob"):
|
|
missing = await client.get("/api/games/preview")
|
|
unknown = await client.get("/api/games/preview?code=ZZZZZZ")
|
|
self.assertEqual(400, missing.status_code)
|
|
self.assertEqual(404, unknown.status_code)
|
|
|
|
with oidc_user("bob"):
|
|
preview = await client.get(f"/api/games/preview?code={code}")
|
|
self.assertEqual(200, preview.status_code)
|
|
body = preview.json()
|
|
self.assertEqual("scopone_scientifico", body["game_type"])
|
|
self.assertEqual("Scopone scientifico", body["game_name"])
|
|
self.assertTrue(body["game_description"])
|
|
self.assertEqual("lobby", body["phase"])
|
|
self.assertEqual(16, body["target_score"])
|
|
self.assertFalse(body["napola"])
|
|
self.assertTrue(body["asso_pigliatutto"])
|
|
self.assertEqual(3, body["seats_open"])
|
|
self.assertEqual(["alice"], [p["sub"] for p in body["players"]])
|
|
|
|
for player in ("bob", "carol", "dave"):
|
|
with oidc_user(player):
|
|
await client.post("/api/games/join", json={"code": code})
|
|
with oidc_user("bob"):
|
|
started = await client.get(f"/api/games/preview?code={code}")
|
|
self.assertEqual(200, started.status_code)
|
|
self.assertEqual("playing", started.json()["phase"])
|
|
self.assertEqual(0, started.json()["seats_open"])
|
|
|
|
@async_test
|
|
async def test_join_errors(self) -> None:
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user("alice"):
|
|
created = await client.post("/api/games", json={})
|
|
code = created.json()["join_code"]
|
|
|
|
with oidc_user("bob"):
|
|
unknown = await client.post("/api/games/join", json={"code": "ZZZZZZ"})
|
|
self.assertEqual(404, unknown.status_code)
|
|
|
|
with oidc_user("alice"):
|
|
duplicate = await client.post("/api/games/join", json={"code": code})
|
|
self.assertEqual(409, duplicate.status_code)
|
|
|
|
with oidc_user("bob"):
|
|
missing = await client.post("/api/games/join", json={})
|
|
self.assertEqual(400, missing.status_code)
|
|
|
|
for player in ("bob", "carol", "dave"):
|
|
with oidc_user(player):
|
|
await client.post("/api/games/join", json={"code": code})
|
|
with oidc_user("erin"):
|
|
late = await client.post("/api/games/join", json={"code": code})
|
|
self.assertEqual(409, late.status_code)
|
|
|
|
@async_test
|
|
async def test_create_rejects_bad_target_score(self) -> None:
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user("alice"):
|
|
zero = await client.post(
|
|
"/api/games", json={"options": {"target_score": 0}}
|
|
)
|
|
text = await client.post(
|
|
"/api/games", json={"options": {"target_score": "eleven"}}
|
|
)
|
|
huge = await client.post(
|
|
"/api/games", json={"options": {"target_score": 1000}}
|
|
)
|
|
self.assertEqual(400, zero.status_code)
|
|
self.assertEqual(400, text.status_code)
|
|
self.assertEqual(400, huge.status_code)
|
|
|
|
@async_test
|
|
async def test_my_games_lists_my_ongoing_games(self) -> None:
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user("alice"):
|
|
created = await client.post(
|
|
"/api/games", json={"game_type": "briscola"}
|
|
)
|
|
game_id = created.json()["id"]
|
|
|
|
with oidc_user("alice"):
|
|
listed = await client.get("/api/me/games")
|
|
self.assertEqual(200, listed.status_code)
|
|
# The app suite shares one in-memory store, so other tests'
|
|
# games may be listed too: pick ours by id.
|
|
by_id = {g["id"]: g for g in listed.json()["results"]}
|
|
self.assertIn(game_id, by_id)
|
|
entry = by_id[game_id]
|
|
self.assertEqual("briscola", entry["game_type"])
|
|
self.assertEqual("Briscola", entry["game_name"])
|
|
self.assertTrue(entry["game_description"])
|
|
self.assertEqual("lobby", entry["phase"])
|
|
self.assertEqual(1, entry["seats_open"])
|
|
self.assertEqual(0, entry["your_seat"])
|
|
self.assertNotIn("join_code", entry)
|
|
|
|
# A logged-in non-player does not see the game.
|
|
with oidc_user("mallory"):
|
|
others = await client.get("/api/me/games")
|
|
self.assertNotIn(
|
|
game_id, [g["id"] for g in others.json()["results"]]
|
|
)
|
|
|
|
# Once the match starts it stays listed, for both players.
|
|
code = created.json()["join_code"]
|
|
with oidc_user("bob"):
|
|
await client.post("/api/games/join", json={"code": code})
|
|
with oidc_user("bob"):
|
|
started = await client.get("/api/me/games")
|
|
bob_entry = next(
|
|
g for g in started.json()["results"] if g["id"] == game_id
|
|
)
|
|
self.assertEqual("playing", bob_entry["phase"])
|
|
self.assertEqual(1, bob_entry["your_seat"])
|
|
self.assertEqual(0, bob_entry["seats_open"])
|
|
|
|
@async_test
|
|
async def test_get_unknown_game(self) -> None:
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user("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:
|
|
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(
|
|
[
|
|
"scopone_scientifico",
|
|
"scopa",
|
|
"scopa_a_quattro",
|
|
"briscola",
|
|
"briscola_a_quattro",
|
|
"briscola_chiamata",
|
|
],
|
|
[g["id"] for g in results],
|
|
)
|
|
scopone = results[0]
|
|
self.assertEqual("Scopone scientifico", scopone["name"])
|
|
self.assertTrue(scopone["description"])
|
|
self.assertEqual(4, scopone["min_players"])
|
|
self.assertEqual(4, scopone["max_players"])
|
|
self.assertIn("target_score", scopone["options_schema"]["properties"])
|
|
self.assertEqual(
|
|
21,
|
|
scopone["options_schema"]["properties"]["target_score"]["default"],
|
|
)
|
|
self.assertIn("napola", scopone["options_schema"]["properties"])
|
|
self.assertIn(
|
|
"asso_pigliatutto", scopone["options_schema"]["properties"]
|
|
)
|
|
scopa = results[1]
|
|
self.assertEqual("Scopa", scopa["name"])
|
|
self.assertTrue(scopa["description"])
|
|
self.assertEqual(2, scopa["min_players"])
|
|
self.assertEqual(2, scopa["max_players"])
|
|
self.assertIn("target_score", scopa["options_schema"]["properties"])
|
|
self.assertEqual(
|
|
11, scopa["options_schema"]["properties"]["target_score"]["default"]
|
|
)
|
|
self.assertIn(
|
|
"asso_pigliatutto", scopa["options_schema"]["properties"]
|
|
)
|
|
scopa_quattro = results[2]
|
|
self.assertEqual("Scopa a quattro", scopa_quattro["name"])
|
|
self.assertTrue(scopa_quattro["description"])
|
|
self.assertEqual(4, scopa_quattro["min_players"])
|
|
self.assertEqual(4, scopa_quattro["max_players"])
|
|
self.assertIn(
|
|
"target_score", scopa_quattro["options_schema"]["properties"]
|
|
)
|
|
self.assertEqual(
|
|
21,
|
|
scopa_quattro["options_schema"]["properties"]["target_score"][
|
|
"default"
|
|
],
|
|
)
|
|
self.assertIn(
|
|
"asso_pigliatutto", scopa_quattro["options_schema"]["properties"]
|
|
)
|
|
briscola = results[3]
|
|
self.assertEqual("Briscola", briscola["name"])
|
|
self.assertTrue(briscola["description"])
|
|
self.assertEqual(2, briscola["min_players"])
|
|
self.assertEqual(2, briscola["max_players"])
|
|
self.assertIn("target_score", briscola["options_schema"]["properties"])
|
|
quattro = results[4]
|
|
self.assertEqual("Briscola a quattro", quattro["name"])
|
|
self.assertTrue(quattro["description"])
|
|
self.assertEqual(4, quattro["min_players"])
|
|
self.assertEqual(4, quattro["max_players"])
|
|
self.assertIn("target_score", quattro["options_schema"]["properties"])
|
|
chiamata = results[5]
|
|
self.assertEqual("Briscola chiamata", chiamata["name"])
|
|
self.assertTrue(chiamata["description"])
|
|
self.assertEqual(5, chiamata["min_players"])
|
|
self.assertEqual(5, chiamata["max_players"])
|
|
self.assertEqual({}, chiamata["options_schema"])
|
|
|
|
@async_test
|
|
async def test_create_defaults_game_type(self) -> None:
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user("alice"):
|
|
created = await client.post("/api/games", json={})
|
|
self.assertEqual(201, created.status_code)
|
|
self.assertEqual("scopone_scientifico", created.json()["game_type"])
|
|
|
|
@async_test
|
|
async def test_create_with_explicit_game_type(self) -> None:
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user("alice"):
|
|
created = await client.post(
|
|
"/api/games", json={"game_type": "scopone_scientifico"}
|
|
)
|
|
self.assertEqual(201, created.status_code)
|
|
body = created.json()
|
|
self.assertEqual("scopone_scientifico", body["game_type"])
|
|
|
|
with oidc_user("alice"):
|
|
snapshot = await client.get(f"/api/games/{body['id']}")
|
|
self.assertEqual("scopone_scientifico", snapshot.json()["game_type"])
|
|
|
|
@async_test
|
|
async def test_create_rejects_unknown_game_type(self) -> None:
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
with oidc_user("alice"):
|
|
unknown = await client.post("/api/games", json={"game_type": "tresette"})
|
|
non_string = await client.post("/api/games", json={"game_type": 42})
|
|
self.assertEqual(400, unknown.status_code)
|
|
self.assertEqual(400, non_string.status_code)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|