"""Integration tests for the Games & Routes API.""" import pytest from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from app.models.game import Game from app.models.version_group import VersionGroup BASE = "/api/v1/games" GAME_PAYLOAD = { "name": "Pokemon Red", "slug": "red", "generation": 1, "region": "kanto", } @pytest.fixture async def game(client: AsyncClient) -> dict: """A game created via the API (no version_group_id).""" response = await client.post(BASE, json=GAME_PAYLOAD) assert response.status_code == 201 return response.json() @pytest.fixture async def game_with_vg(db_session: AsyncSession) -> tuple[int, int]: """A game with a VersionGroup, required for route operations.""" vg = VersionGroup(name="Red/Blue", slug="red-blue") db_session.add(vg) await db_session.flush() g = Game( name="Pokemon Red", slug="red-vg", generation=1, region="kanto", version_group_id=vg.id, ) db_session.add(g) await db_session.commit() await db_session.refresh(g) return g.id, vg.id # --------------------------------------------------------------------------- # Games — list # --------------------------------------------------------------------------- class TestListGames: async def test_empty_returns_empty_list(self, client: AsyncClient): response = await client.get(BASE) assert response.status_code == 200 assert response.json() == [] async def test_returns_created_game(self, client: AsyncClient, game: dict): response = await client.get(BASE) assert response.status_code == 200 slugs = [g["slug"] for g in response.json()] assert "red" in slugs # --------------------------------------------------------------------------- # Games — create # --------------------------------------------------------------------------- class TestCreateGame: async def test_creates_and_returns_game(self, client: AsyncClient): response = await client.post(BASE, json=GAME_PAYLOAD) assert response.status_code == 201 data = response.json() assert data["name"] == "Pokemon Red" assert data["slug"] == "red" assert isinstance(data["id"], int) async def test_duplicate_slug_returns_409(self, client: AsyncClient, game: dict): response = await client.post( BASE, json={**GAME_PAYLOAD, "name": "Pokemon Red v2"} ) assert response.status_code == 409 async def test_missing_required_field_returns_422(self, client: AsyncClient): response = await client.post(BASE, json={"name": "Pokemon Red"}) assert response.status_code == 422 # --------------------------------------------------------------------------- # Games — get # --------------------------------------------------------------------------- class TestGetGame: async def test_returns_game_with_empty_routes( self, client: AsyncClient, game: dict ): response = await client.get(f"{BASE}/{game['id']}") assert response.status_code == 200 data = response.json() assert data["id"] == game["id"] assert data["slug"] == "red" assert data["routes"] == [] async def test_not_found_returns_404(self, client: AsyncClient): assert (await client.get(f"{BASE}/9999")).status_code == 404 # --------------------------------------------------------------------------- # Games — update # --------------------------------------------------------------------------- class TestUpdateGame: async def test_updates_name(self, client: AsyncClient, game: dict): response = await client.put( f"{BASE}/{game['id']}", json={"name": "Pokemon Blue"} ) assert response.status_code == 200 assert response.json()["name"] == "Pokemon Blue" async def test_slug_unchanged_on_partial_update( self, client: AsyncClient, game: dict ): response = await client.put(f"{BASE}/{game['id']}", json={"name": "New Name"}) assert response.json()["slug"] == "red" async def test_not_found_returns_404(self, client: AsyncClient): assert (await client.put(f"{BASE}/9999", json={"name": "x"})).status_code == 404 async def test_duplicate_slug_returns_409(self, client: AsyncClient): await client.post(BASE, json={**GAME_PAYLOAD, "slug": "blue", "name": "Blue"}) r1 = await client.post( BASE, json={**GAME_PAYLOAD, "slug": "red", "name": "Red"} ) game_id = r1.json()["id"] response = await client.put(f"{BASE}/{game_id}", json={"slug": "blue"}) assert response.status_code == 409 # --------------------------------------------------------------------------- # Games — delete # --------------------------------------------------------------------------- class TestDeleteGame: async def test_deletes_game(self, client: AsyncClient, game: dict): response = await client.delete(f"{BASE}/{game['id']}") assert response.status_code == 204 assert (await client.get(f"{BASE}/{game['id']}")).status_code == 404 async def test_not_found_returns_404(self, client: AsyncClient): assert (await client.delete(f"{BASE}/9999")).status_code == 404 # --------------------------------------------------------------------------- # Games — by-region # --------------------------------------------------------------------------- class TestListByRegion: async def test_returns_list(self, client: AsyncClient): response = await client.get(f"{BASE}/by-region") assert response.status_code == 200 assert isinstance(response.json(), list) async def test_region_structure(self, client: AsyncClient): response = await client.get(f"{BASE}/by-region") regions = response.json() assert len(regions) > 0 first = regions[0] assert "name" in first assert "generation" in first assert "games" in first assert isinstance(first["games"], list) async def test_game_appears_in_region(self, client: AsyncClient, game: dict): response = await client.get(f"{BASE}/by-region") all_games = [g for region in response.json() for g in region["games"]] assert any(g["slug"] == "red" for g in all_games) # --------------------------------------------------------------------------- # Routes — create / get # --------------------------------------------------------------------------- class TestCreateRoute: async def test_creates_route(self, client: AsyncClient, game_with_vg: tuple): game_id, _ = game_with_vg response = await client.post( f"{BASE}/{game_id}/routes", json={"name": "Pallet Town", "order": 1}, ) assert response.status_code == 201 data = response.json() assert data["name"] == "Pallet Town" assert data["order"] == 1 assert isinstance(data["id"], int) async def test_game_detail_includes_route( self, client: AsyncClient, game_with_vg: tuple ): game_id, _ = game_with_vg await client.post( f"{BASE}/{game_id}/routes", json={"name": "Route 1", "order": 1} ) response = await client.get(f"{BASE}/{game_id}") routes = response.json()["routes"] assert len(routes) == 1 assert routes[0]["name"] == "Route 1" async def test_game_without_version_group_returns_400( self, client: AsyncClient, game: dict ): response = await client.post( f"{BASE}/{game['id']}/routes", json={"name": "Route 1", "order": 1}, ) assert response.status_code == 400 async def test_list_routes_excludes_routes_without_encounters( self, client: AsyncClient, game_with_vg: tuple ): """list_game_routes only returns routes that have Pokemon encounters.""" game_id, _ = game_with_vg await client.post( f"{BASE}/{game_id}/routes", json={"name": "Route 1", "order": 1} ) response = await client.get(f"{BASE}/{game_id}/routes?flat=true") assert response.status_code == 200 assert response.json() == [] # --------------------------------------------------------------------------- # Routes — update # --------------------------------------------------------------------------- class TestUpdateRoute: async def test_updates_route_name(self, client: AsyncClient, game_with_vg: tuple): game_id, _ = game_with_vg r = ( await client.post( f"{BASE}/{game_id}/routes", json={"name": "Old Name", "order": 1} ) ).json() response = await client.put( f"{BASE}/{game_id}/routes/{r['id']}", json={"name": "New Name"}, ) assert response.status_code == 200 assert response.json()["name"] == "New Name" async def test_route_not_found_returns_404( self, client: AsyncClient, game_with_vg: tuple ): game_id, _ = game_with_vg assert ( await client.put(f"{BASE}/{game_id}/routes/9999", json={"name": "x"}) ).status_code == 404 # --------------------------------------------------------------------------- # Routes — delete # --------------------------------------------------------------------------- class TestDeleteRoute: async def test_deletes_route(self, client: AsyncClient, game_with_vg: tuple): game_id, _ = game_with_vg r = ( await client.post( f"{BASE}/{game_id}/routes", json={"name": "Route 1", "order": 1} ) ).json() assert ( await client.delete(f"{BASE}/{game_id}/routes/{r['id']}") ).status_code == 204 # No longer in game detail detail = (await client.get(f"{BASE}/{game_id}")).json() assert all(route["id"] != r["id"] for route in detail["routes"]) async def test_route_not_found_returns_404( self, client: AsyncClient, game_with_vg: tuple ): game_id, _ = game_with_vg assert (await client.delete(f"{BASE}/{game_id}/routes/9999")).status_code == 404 # --------------------------------------------------------------------------- # Routes — reorder # --------------------------------------------------------------------------- class TestReorderRoutes: async def test_reorders_routes(self, client: AsyncClient, game_with_vg: tuple): game_id, _ = game_with_vg r1 = ( await client.post( f"{BASE}/{game_id}/routes", json={"name": "A", "order": 1} ) ).json() r2 = ( await client.post( f"{BASE}/{game_id}/routes", json={"name": "B", "order": 2} ) ).json() response = await client.put( f"{BASE}/{game_id}/routes/reorder", json={ "routes": [{"id": r1["id"], "order": 2}, {"id": r2["id"], "order": 1}] }, ) assert response.status_code == 200 by_id = {r["id"]: r["order"] for r in response.json()} assert by_id[r1["id"]] == 2 assert by_id[r2["id"]] == 1