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).
70 lines
2.8 KiB
Python
70 lines
2.8 KiB
Python
"""Tests for the whoami endpoint and the static SPA host."""
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from tavolo.app import app
|
|
from tavolo.config import settings
|
|
from tests.helpers import async_test, oidc_user
|
|
|
|
|
|
class MeRouteTest(unittest.TestCase):
|
|
@async_test
|
|
async def test_me_authenticated(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/me")
|
|
self.assertEqual(200, response.status_code)
|
|
self.assertEqual({"sub": "alice", "name": "alice"}, response.json())
|
|
|
|
@async_test
|
|
async def test_me_unauthenticated(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/me")
|
|
self.assertEqual(401, response.status_code)
|
|
|
|
|
|
class StaticRouteTest(unittest.TestCase):
|
|
"""The app only serves the SPA shell; asset files under /static are
|
|
served by Granian and are not reachable through the ASGI transport."""
|
|
|
|
@async_test
|
|
async def test_serves_shell_and_spa_fallback(self) -> None:
|
|
with tempfile.TemporaryDirectory() as dist:
|
|
(Path(dist) / "index.html").write_text("<html>spa</html>")
|
|
|
|
patched = dataclasses.replace(settings, static_dir=dist)
|
|
with mock.patch("tavolo.static.settings", patched):
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
index = await client.get("/")
|
|
self.assertEqual(200, index.status_code)
|
|
self.assertEqual("text/html; charset=utf-8", index.headers["content-type"])
|
|
self.assertIn(b"spa", index.content)
|
|
|
|
# Unknown client-side route falls back to the app shell.
|
|
fallback = await client.get("/game/some-id")
|
|
self.assertEqual(200, fallback.status_code)
|
|
self.assertIn(b"spa", fallback.content)
|
|
|
|
@async_test
|
|
async def test_missing_dist_returns_404(self) -> None:
|
|
patched = dataclasses.replace(settings, static_dir="/nonexistent-dist")
|
|
with mock.patch("tavolo.static.settings", patched):
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client:
|
|
response = await client.get("/")
|
|
self.assertEqual(404, response.status_code)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|