"""OpenAPI document tests: components, response schemas and Swagger UI.""" from __future__ import annotations import unittest from typing import Any, Dict, Tuple from httpx import ASGITransport, AsyncClient from kaya.openapi import schema_ref from tavolo.app import app from tavolo.platform.openapi import SCHEMAS from tests.helpers import async_test #: Methods documented for the application's own routes. HTTP_METHODS = ("get", "post", "put", "patch", "delete", "options", "head") class OpenApiDocumentTest(unittest.TestCase): async def _get(self, path: str) -> Any: transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client: response = await client.get(path) self.assertEqual(200, response.status_code) return response async def _spec(self) -> Dict[str, Any]: response = await self._get("/api/openapi.json") body: Dict[str, Any] = response.json() return body async def _schema( self, spec: Dict[str, Any], path: str, method: str, status: str ) -> Dict[str, Any]: schema: Dict[str, Any] = spec["paths"][path][method]["responses"][status][ "content" ]["application/json"]["schema"] return schema @async_test async def test_components_registered(self) -> None: spec = await self._spec() self.assertEqual(SCHEMAS, spec["components"]["schemas"]) @async_test async def test_every_api_response_documents_a_schema(self) -> None: spec = await self._spec() checked = 0 for path, item in spec["paths"].items(): if not path.startswith("/api/"): continue for method, operation in item.items(): if method not in HTTP_METHODS: continue for status, response in operation["responses"].items(): with self.subTest(path=path, method=method, status=status): if status == "204": # No content, so no schema either. self.assertNotIn("content", response) continue self.assertIn("application/json", response["content"]) self.assertTrue( response["content"]["application/json"]["schema"] ) checked += 1 self.assertGreater(checked, 0) @async_test async def test_response_schemas_reference_components(self) -> None: spec = await self._spec() cases: Tuple[Tuple[str, str, str, Dict[str, Any]], ...] = ( ("/api/health", "get", "200", schema_ref("Health")), ("/api/me", "get", "200", schema_ref("Me")), ("/api/me", "get", "401", schema_ref("Error")), ("/api/auth/providers", "get", "200", schema_ref("AuthProvidersPage")), ("/api/game-types", "get", "200", schema_ref("GameTypesPage")), ("/api/games", "post", "201", schema_ref("Lobby")), ("/api/games/preview", "get", "200", schema_ref("GamePreview")), ("/api/games/{game_id}", "get", "200", schema_ref("GameView")), ("/api/games/{game_id}", "get", "404", schema_ref("Error")), ("/api/me/games", "get", "200", schema_ref("OngoingGamesPage")), ("/api/me/matches", "get", "200", schema_ref("MatchesPage")), ("/api/leaderboard", "get", "200", schema_ref("LeaderboardPage")), ("/api/me/ratings", "get", "200", schema_ref("RatingsPage")), ) for path, method, status, expected in cases: with self.subTest(path=path, method=method, status=status): self.assertEqual( expected, await self._schema(spec, path, method, status) ) @async_test async def test_join_response_is_lobby_or_game_view(self) -> None: spec = await self._spec() schema = await self._schema(spec, "/api/games/join", "post", "200") self.assertEqual( {"anyOf": [schema_ref("Lobby"), schema_ref("GameView")]}, schema ) @async_test async def test_component_nesting_and_nullability(self) -> None: spec = await self._spec() schemas = spec["components"]["schemas"] matches = schemas["MatchesPage"]["properties"] self.assertEqual(schema_ref("Match"), matches["results"]["items"]) self.assertEqual(["string", "null"], matches["next_cursor"]["type"]) match = schemas["Match"]["properties"] self.assertEqual(schema_ref("MatchPlayer"), match["players"]["items"]) self.assertEqual(["integer", "null"], match["your_elo_delta"]["type"]) lobby = schemas["Lobby"] self.assertEqual(schema_ref("LobbyPlayer"), lobby["properties"]["players"]["items"]) self.assertIs(True, lobby["additionalProperties"]) self.assertEqual( ["string", "null"], schemas["LobbyPlayer"]["properties"]["team"]["type"], ) game_view = schemas["GameView"] self.assertEqual("boolean", game_view["properties"]["spectator"]["type"]) self.assertIs(True, game_view["additionalProperties"]) # The join code is optional (players only), so it is not required. self.assertNotIn("join_code", game_view["required"]) board = schemas["LeaderboardPage"] self.assertEqual(["integer", "null"], board["properties"]["your_rank"]["type"]) self.assertEqual( ["integer", "null"], board["properties"]["your_page"]["type"] ) self.assertEqual(schema_ref("LeaderboardEntry"), board["properties"]["results"]["items"]) @async_test async def test_docs_serve_swagger_ui(self) -> None: response = await self._get("/api/docs") self.assertIn("text/html", response.headers["content-type"]) self.assertIn("swagger-ui", response.text)