Files
nuzlocke-tracker/backend/tests/test_genlocke_boss.py
Julian Tabel a4fa5bf1e4 feat: infer genlocke visibility from first leg's run
Genlockes now inherit visibility from their first leg's run:
- Private runs make genlockes hidden from public listings
- All genlocke read endpoints now accept optional auth
- Returns 404 for private genlockes to non-owners

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 13:47:05 +01:00

816 lines
30 KiB
Python

"""Integration tests for the Genlockes & Bosses API."""
import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.auth import AuthUser, get_current_user
from app.main import app
from app.models.game import Game
from app.models.nuzlocke_run import NuzlockeRun, RunVisibility
from app.models.pokemon import Pokemon
from app.models.route import Route
from app.models.version_group import VersionGroup
GENLOCKES_BASE = "/api/v1/genlockes"
RUNS_BASE = "/api/v1/runs"
GAMES_BASE = "/api/v1/games"
STATS_BASE = "/api/v1/stats"
EXPORT_BASE = "/api/v1/export"
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
async def games_ctx(db_session: AsyncSession) -> dict:
"""Two games with version groups."""
vg1 = VersionGroup(name="GT VG1", slug="gt-vg1")
vg2 = VersionGroup(name="GT VG2", slug="gt-vg2")
db_session.add_all([vg1, vg2])
await db_session.flush()
game1 = Game(
name="GT Game 1",
slug="gt-game-1",
generation=1,
region="kanto",
version_group_id=vg1.id,
)
game2 = Game(
name="GT Game 2",
slug="gt-game-2",
generation=2,
region="johto",
version_group_id=vg2.id,
)
db_session.add_all([game1, game2])
await db_session.commit()
return {
"game1_id": game1.id,
"game2_id": game2.id,
"vg1_id": vg1.id,
"vg2_id": vg2.id,
}
@pytest.fixture
async def ctx(
db_session: AsyncSession, admin_client: AsyncClient, games_ctx: dict
) -> dict:
"""Full context: routes + pokemon + genlocke + encounter for advance/transfer tests."""
route1 = Route(name="GT Route 1", version_group_id=games_ctx["vg1_id"], order=1)
route2 = Route(name="GT Route 2", version_group_id=games_ctx["vg2_id"], order=1)
db_session.add_all([route1, route2])
pikachu = Pokemon(
pokeapi_id=25, national_dex=25, name="pikachu", types=["electric"]
)
db_session.add(pikachu)
await db_session.commit()
r = await admin_client.post(
GENLOCKES_BASE,
json={
"name": "Test Genlocke",
"gameIds": [games_ctx["game1_id"], games_ctx["game2_id"]],
},
)
assert r.status_code == 201
genlocke = r.json()
leg1 = next(leg for leg in genlocke["legs"] if leg["legOrder"] == 1)
run_id = leg1["runId"]
enc_r = await admin_client.post(
f"{RUNS_BASE}/{run_id}/encounters",
json={"routeId": route1.id, "pokemonId": pikachu.id, "status": "caught"},
)
assert enc_r.status_code == 201
return {
**games_ctx,
"route1_id": route1.id,
"route2_id": route2.id,
"pikachu_id": pikachu.id,
"genlocke_id": genlocke["id"],
"run_id": run_id,
"encounter_id": enc_r.json()["id"],
"genlocke": genlocke,
}
# ---------------------------------------------------------------------------
# Genlockes — list
# ---------------------------------------------------------------------------
class TestListGenlockes:
async def test_empty_returns_empty_list(self, admin_client: AsyncClient):
response = await admin_client.get(GENLOCKES_BASE)
assert response.status_code == 200
assert response.json() == []
async def test_returns_created_genlocke(self, admin_client: AsyncClient, ctx: dict):
response = await admin_client.get(GENLOCKES_BASE)
assert response.status_code == 200
names = [g["name"] for g in response.json()]
assert "Test Genlocke" in names
# ---------------------------------------------------------------------------
# Genlockes — visibility (inferred from first leg's run)
# ---------------------------------------------------------------------------
class TestGenlockeVisibility:
"""Test that genlocke visibility is inferred from the first leg's run."""
@pytest.fixture
async def private_genlocke_ctx(
self, db_session: AsyncSession, admin_client: AsyncClient, games_ctx: dict
) -> dict:
"""Create a genlocke and make its first leg's run private."""
r = await admin_client.post(
GENLOCKES_BASE,
json={
"name": "Private Genlocke",
"gameIds": [games_ctx["game1_id"], games_ctx["game2_id"]],
},
)
assert r.status_code == 201
genlocke = r.json()
leg1 = next(leg for leg in genlocke["legs"] if leg["legOrder"] == 1)
run_id = leg1["runId"]
# Make the run private
run = await db_session.get(NuzlockeRun, run_id)
assert run is not None
run.visibility = RunVisibility.PRIVATE
await db_session.commit()
return {
**games_ctx,
"genlocke_id": genlocke["id"],
"run_id": run_id,
"owner_id": str(run.owner_id),
}
async def test_private_genlocke_hidden_from_unauthenticated_list(
self, db_session: AsyncSession, private_genlocke_ctx: dict
):
"""Unauthenticated users should not see private genlockes in the list."""
# Temporarily remove auth override to simulate unauthenticated request
app.dependency_overrides.pop(get_current_user, None)
try:
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as unauth_client:
response = await unauth_client.get(GENLOCKES_BASE)
assert response.status_code == 200
names = [g["name"] for g in response.json()]
assert "Private Genlocke" not in names
finally:
pass
async def test_private_genlocke_visible_to_owner_in_list(
self, admin_client: AsyncClient, private_genlocke_ctx: dict
):
"""Owner should still see their private genlocke in the list."""
response = await admin_client.get(GENLOCKES_BASE)
assert response.status_code == 200
names = [g["name"] for g in response.json()]
assert "Private Genlocke" in names
async def test_private_genlocke_404_for_unauthenticated_get(
self, db_session: AsyncSession, private_genlocke_ctx: dict
):
"""Unauthenticated users should get 404 for private genlocke details."""
app.dependency_overrides.pop(get_current_user, None)
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as unauth_client:
response = await unauth_client.get(
f"{GENLOCKES_BASE}/{private_genlocke_ctx['genlocke_id']}"
)
assert response.status_code == 404
async def test_private_genlocke_accessible_to_owner(
self, admin_client: AsyncClient, private_genlocke_ctx: dict
):
"""Owner should still be able to access their private genlocke."""
response = await admin_client.get(
f"{GENLOCKES_BASE}/{private_genlocke_ctx['genlocke_id']}"
)
assert response.status_code == 200
assert response.json()["name"] == "Private Genlocke"
async def test_private_genlocke_graveyard_404_for_unauthenticated(
self, db_session: AsyncSession, private_genlocke_ctx: dict
):
"""Unauthenticated users should get 404 for private genlocke graveyard."""
app.dependency_overrides.pop(get_current_user, None)
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as unauth_client:
response = await unauth_client.get(
f"{GENLOCKES_BASE}/{private_genlocke_ctx['genlocke_id']}/graveyard"
)
assert response.status_code == 404
async def test_private_genlocke_lineages_404_for_unauthenticated(
self, db_session: AsyncSession, private_genlocke_ctx: dict
):
"""Unauthenticated users should get 404 for private genlocke lineages."""
app.dependency_overrides.pop(get_current_user, None)
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as unauth_client:
response = await unauth_client.get(
f"{GENLOCKES_BASE}/{private_genlocke_ctx['genlocke_id']}/lineages"
)
assert response.status_code == 404
async def test_private_genlocke_survivors_404_for_unauthenticated(
self, db_session: AsyncSession, private_genlocke_ctx: dict
):
"""Unauthenticated users should get 404 for private genlocke survivors."""
app.dependency_overrides.pop(get_current_user, None)
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as unauth_client:
response = await unauth_client.get(
f"{GENLOCKES_BASE}/{private_genlocke_ctx['genlocke_id']}/legs/1/survivors"
)
assert response.status_code == 404
async def test_private_genlocke_retired_families_404_for_unauthenticated(
self, db_session: AsyncSession, private_genlocke_ctx: dict
):
"""Unauthenticated users should get 404 for private retired-families."""
app.dependency_overrides.pop(get_current_user, None)
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as unauth_client:
response = await unauth_client.get(
f"{GENLOCKES_BASE}/{private_genlocke_ctx['genlocke_id']}/retired-families"
)
assert response.status_code == 404
async def test_private_genlocke_404_for_different_user(
self, db_session: AsyncSession, private_genlocke_ctx: dict
):
"""A different authenticated user should get 404 for private genlockes."""
# Create a different user's auth
different_user = AuthUser(
id="00000000-0000-4000-a000-000000000099",
email="other@example.com",
role="authenticated",
)
def _override():
return different_user
app.dependency_overrides[get_current_user] = _override
try:
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as other_client:
response = await other_client.get(
f"{GENLOCKES_BASE}/{private_genlocke_ctx['genlocke_id']}"
)
assert response.status_code == 404
# Also check list
list_response = await other_client.get(GENLOCKES_BASE)
assert list_response.status_code == 200
names = [g["name"] for g in list_response.json()]
assert "Private Genlocke" not in names
finally:
app.dependency_overrides.pop(get_current_user, None)
# ---------------------------------------------------------------------------
# Genlockes — create
# ---------------------------------------------------------------------------
class TestCreateGenlocke:
async def test_creates_with_legs_and_first_run(
self, admin_client: AsyncClient, games_ctx: dict
):
response = await admin_client.post(
GENLOCKES_BASE,
json={
"name": "My Genlocke",
"gameIds": [games_ctx["game1_id"], games_ctx["game2_id"]],
},
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "My Genlocke"
assert data["status"] == "active"
assert len(data["legs"]) == 2
# Leg 1 should already have a run linked
leg1 = next(leg for leg in data["legs"] if leg["legOrder"] == 1)
assert leg1["runId"] is not None
# Leg 2 should not yet have a run
leg2 = next(leg for leg in data["legs"] if leg["legOrder"] == 2)
assert leg2["runId"] is None
async def test_empty_game_ids_returns_400(self, admin_client: AsyncClient):
response = await admin_client.post(
GENLOCKES_BASE, json={"name": "Bad", "gameIds": []}
)
assert response.status_code == 400
async def test_invalid_game_id_returns_404(self, admin_client: AsyncClient):
response = await admin_client.post(
GENLOCKES_BASE, json={"name": "Bad", "gameIds": [9999]}
)
assert response.status_code == 404
# ---------------------------------------------------------------------------
# Genlockes — get
# ---------------------------------------------------------------------------
class TestGetGenlocke:
async def test_returns_genlocke_with_legs_and_stats(
self, admin_client: AsyncClient, ctx: dict
):
response = await admin_client.get(f"{GENLOCKES_BASE}/{ctx['genlocke_id']}")
assert response.status_code == 200
data = response.json()
assert data["id"] == ctx["genlocke_id"]
assert len(data["legs"]) == 2
assert "stats" in data
assert data["stats"]["totalLegs"] == 2
async def test_not_found_returns_404(self, admin_client: AsyncClient):
assert (await admin_client.get(f"{GENLOCKES_BASE}/9999")).status_code == 404
# ---------------------------------------------------------------------------
# Genlockes — update / delete
# ---------------------------------------------------------------------------
class TestUpdateGenlocke:
async def test_updates_name(self, admin_client: AsyncClient, ctx: dict):
response = await admin_client.patch(
f"{GENLOCKES_BASE}/{ctx['genlocke_id']}", json={"name": "Renamed"}
)
assert response.status_code == 200
assert response.json()["name"] == "Renamed"
async def test_not_found_returns_404(self, admin_client: AsyncClient):
assert (
await admin_client.patch(f"{GENLOCKES_BASE}/9999", json={"name": "x"})
).status_code == 404
class TestDeleteGenlocke:
async def test_deletes_genlocke(self, admin_client: AsyncClient, ctx: dict):
assert (
await admin_client.delete(f"{GENLOCKES_BASE}/{ctx['genlocke_id']}")
).status_code == 204
assert (
await admin_client.get(f"{GENLOCKES_BASE}/{ctx['genlocke_id']}")
).status_code == 404
async def test_not_found_returns_404(self, admin_client: AsyncClient):
assert (await admin_client.delete(f"{GENLOCKES_BASE}/9999")).status_code == 404
# ---------------------------------------------------------------------------
# Genlockes — legs (add / remove)
# ---------------------------------------------------------------------------
class TestGenlockeLegs:
async def test_adds_leg(self, admin_client: AsyncClient, ctx: dict):
response = await admin_client.post(
f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/legs",
json={"gameId": ctx["game1_id"]},
)
assert response.status_code == 201
legs = response.json()["legs"]
assert len(legs) == 3 # was 2, now 3
async def test_remove_leg_without_run(self, admin_client: AsyncClient, ctx: dict):
# Leg 2 has no run yet — can be removed
leg2 = next(leg for leg in ctx["genlocke"]["legs"] if leg["legOrder"] == 2)
response = await admin_client.delete(
f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/legs/{leg2['id']}"
)
assert response.status_code == 204
async def test_remove_leg_with_run_returns_400(
self, admin_client: AsyncClient, ctx: dict
):
# Leg 1 has a run — cannot remove
leg1 = next(leg for leg in ctx["genlocke"]["legs"] if leg["legOrder"] == 1)
response = await admin_client.delete(
f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/legs/{leg1['id']}"
)
assert response.status_code == 400
async def test_add_leg_invalid_game_returns_404(
self, admin_client: AsyncClient, ctx: dict
):
response = await admin_client.post(
f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/legs",
json={"gameId": 9999},
)
assert response.status_code == 404
# ---------------------------------------------------------------------------
# Genlockes — advance leg
# ---------------------------------------------------------------------------
class TestAdvanceLeg:
async def test_uncompleted_run_returns_400(
self, admin_client: AsyncClient, ctx: dict
):
"""Cannot advance when leg 1's run is still active."""
response = await admin_client.post(
f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/legs/1/advance"
)
assert response.status_code == 400
async def test_no_next_leg_returns_400(
self, admin_client: AsyncClient, games_ctx: dict
):
"""A single-leg genlocke cannot be advanced."""
r = await admin_client.post(
GENLOCKES_BASE,
json={"name": "Single Leg", "gameIds": [games_ctx["game1_id"]]},
)
genlocke = r.json()
run_id = genlocke["legs"][0]["runId"]
await admin_client.patch(f"{RUNS_BASE}/{run_id}", json={"status": "completed"})
response = await admin_client.post(
f"{GENLOCKES_BASE}/{genlocke['id']}/legs/1/advance"
)
assert response.status_code == 400
async def test_advances_to_next_leg(self, admin_client: AsyncClient, ctx: dict):
"""Completing the current run allows advancing to the next leg."""
await admin_client.patch(
f"{RUNS_BASE}/{ctx['run_id']}", json={"status": "completed"}
)
response = await admin_client.post(
f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/legs/1/advance"
)
assert response.status_code == 200
legs = response.json()["legs"]
leg2 = next(leg for leg in legs if leg["legOrder"] == 2)
assert leg2["runId"] is not None
async def test_advances_with_transfers(self, admin_client: AsyncClient, ctx: dict):
"""Advancing with transfer_encounter_ids creates egg encounters in the next leg."""
await admin_client.patch(
f"{RUNS_BASE}/{ctx['run_id']}", json={"status": "completed"}
)
response = await admin_client.post(
f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/legs/1/advance",
json={"transferEncounterIds": [ctx["encounter_id"]]},
)
assert response.status_code == 200
legs = response.json()["legs"]
leg2 = next(leg for leg in legs if leg["legOrder"] == 2)
new_run_id = leg2["runId"]
assert new_run_id is not None
# The new run should contain the transferred (egg) encounter
run_detail = (await admin_client.get(f"{RUNS_BASE}/{new_run_id}")).json()
assert len(run_detail["encounters"]) == 1
# ---------------------------------------------------------------------------
# Genlockes — read-only detail endpoints
# ---------------------------------------------------------------------------
class TestGenlockeGraveyard:
async def test_returns_empty_graveyard(self, admin_client: AsyncClient, ctx: dict):
response = await admin_client.get(
f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/graveyard"
)
assert response.status_code == 200
data = response.json()
assert data["entries"] == []
assert data["totalDeaths"] == 0
async def test_not_found_returns_404(self, admin_client: AsyncClient):
assert (
await admin_client.get(f"{GENLOCKES_BASE}/9999/graveyard")
).status_code == 404
class TestGenlockeLineages:
async def test_returns_empty_lineages(self, admin_client: AsyncClient, ctx: dict):
response = await admin_client.get(
f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/lineages"
)
assert response.status_code == 200
data = response.json()
assert data["lineages"] == []
assert data["totalLineages"] == 0
async def test_not_found_returns_404(self, admin_client: AsyncClient):
assert (
await admin_client.get(f"{GENLOCKES_BASE}/9999/lineages")
).status_code == 404
class TestGenlockeRetiredFamilies:
async def test_returns_empty_retired_families(
self, admin_client: AsyncClient, ctx: dict
):
response = await admin_client.get(
f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/retired-families"
)
assert response.status_code == 200
data = response.json()
assert data["retired_pokemon_ids"] == []
async def test_not_found_returns_404(self, admin_client: AsyncClient):
assert (
await admin_client.get(f"{GENLOCKES_BASE}/9999/retired-families")
).status_code == 404
class TestLegSurvivors:
async def test_returns_survivors(self, admin_client: AsyncClient, ctx: dict):
"""The one caught encounter in leg 1 shows up as a survivor."""
response = await admin_client.get(
f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/legs/1/survivors"
)
assert response.status_code == 200
assert len(response.json()) == 1
async def test_leg_not_found_returns_404(
self, admin_client: AsyncClient, ctx: dict
):
assert (
await admin_client.get(
f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/legs/99/survivors"
)
).status_code == 404
# ---------------------------------------------------------------------------
# Boss battles — CRUD (game-scoped)
# ---------------------------------------------------------------------------
BOSS_PAYLOAD = {
"name": "Brock",
"bossType": "gym",
"levelCap": 14,
"order": 1,
"location": "Pewter City",
}
class TestBossCRUD:
async def test_empty_list(self, admin_client: AsyncClient, games_ctx: dict):
response = await admin_client.get(
f"{GAMES_BASE}/{games_ctx['game1_id']}/bosses"
)
assert response.status_code == 200
assert response.json() == []
async def test_creates_boss(self, admin_client: AsyncClient, games_ctx: dict):
response = await admin_client.post(
f"{GAMES_BASE}/{games_ctx['game1_id']}/bosses", json=BOSS_PAYLOAD
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Brock"
assert data["levelCap"] == 14
assert data["pokemon"] == []
async def test_updates_boss(self, admin_client: AsyncClient, games_ctx: dict):
boss = (
await admin_client.post(
f"{GAMES_BASE}/{games_ctx['game1_id']}/bosses", json=BOSS_PAYLOAD
)
).json()
response = await admin_client.put(
f"{GAMES_BASE}/{games_ctx['game1_id']}/bosses/{boss['id']}",
json={"levelCap": 20},
)
assert response.status_code == 200
assert response.json()["levelCap"] == 20
async def test_deletes_boss(self, admin_client: AsyncClient, games_ctx: dict):
boss = (
await admin_client.post(
f"{GAMES_BASE}/{games_ctx['game1_id']}/bosses", json=BOSS_PAYLOAD
)
).json()
assert (
await admin_client.delete(
f"{GAMES_BASE}/{games_ctx['game1_id']}/bosses/{boss['id']}"
)
).status_code == 204
assert (
await admin_client.get(f"{GAMES_BASE}/{games_ctx['game1_id']}/bosses")
).json() == []
async def test_boss_not_found_returns_404(
self, admin_client: AsyncClient, games_ctx: dict
):
assert (
await admin_client.put(
f"{GAMES_BASE}/{games_ctx['game1_id']}/bosses/9999",
json={"levelCap": 10},
)
).status_code == 404
async def test_invalid_game_returns_404(self, admin_client: AsyncClient):
assert (await admin_client.get(f"{GAMES_BASE}/9999/bosses")).status_code == 404
async def test_game_without_version_group_returns_400(
self, admin_client: AsyncClient
):
game = (
await admin_client.post(
GAMES_BASE,
json={
"name": "No VG",
"slug": "no-vg",
"generation": 1,
"region": "kanto",
},
)
).json()
assert (
await admin_client.get(f"{GAMES_BASE}/{game['id']}/bosses")
).status_code == 400
# ---------------------------------------------------------------------------
# Boss results — CRUD (run-scoped)
# ---------------------------------------------------------------------------
class TestBossResults:
@pytest.fixture
async def boss_ctx(self, admin_client: AsyncClient, games_ctx: dict) -> dict:
"""A boss battle and a run for boss-result tests."""
boss = (
await admin_client.post(
f"{GAMES_BASE}/{games_ctx['game1_id']}/bosses", json=BOSS_PAYLOAD
)
).json()
run = (
await admin_client.post(
RUNS_BASE, json={"gameId": games_ctx["game1_id"], "name": "Boss Run"}
)
).json()
return {"boss_id": boss["id"], "run_id": run["id"]}
async def test_empty_list(self, admin_client: AsyncClient, boss_ctx: dict):
response = await admin_client.get(
f"{RUNS_BASE}/{boss_ctx['run_id']}/boss-results"
)
assert response.status_code == 200
assert response.json() == []
async def test_creates_boss_result(self, admin_client: AsyncClient, boss_ctx: dict):
response = await admin_client.post(
f"{RUNS_BASE}/{boss_ctx['run_id']}/boss-results",
json={"bossBattleId": boss_ctx["boss_id"], "result": "won", "attempts": 1},
)
assert response.status_code == 201
data = response.json()
assert data["result"] == "won"
assert data["attempts"] == 1
assert data["completedAt"] is not None
async def test_upserts_existing_result(
self, admin_client: AsyncClient, boss_ctx: dict
):
"""POSTing the same boss twice updates the result (upsert)."""
await admin_client.post(
f"{RUNS_BASE}/{boss_ctx['run_id']}/boss-results",
json={"bossBattleId": boss_ctx["boss_id"], "result": "won", "attempts": 1},
)
response = await admin_client.post(
f"{RUNS_BASE}/{boss_ctx['run_id']}/boss-results",
json={"bossBattleId": boss_ctx["boss_id"], "result": "lost", "attempts": 3},
)
assert response.status_code == 201
assert response.json()["result"] == "lost"
assert response.json()["attempts"] == 3
# Still only one record
all_results = (
await admin_client.get(f"{RUNS_BASE}/{boss_ctx['run_id']}/boss-results")
).json()
assert len(all_results) == 1
async def test_deletes_boss_result(self, admin_client: AsyncClient, boss_ctx: dict):
result = (
await admin_client.post(
f"{RUNS_BASE}/{boss_ctx['run_id']}/boss-results",
json={"bossBattleId": boss_ctx["boss_id"], "result": "won"},
)
).json()
assert (
await admin_client.delete(
f"{RUNS_BASE}/{boss_ctx['run_id']}/boss-results/{result['id']}"
)
).status_code == 204
assert (
await admin_client.get(f"{RUNS_BASE}/{boss_ctx['run_id']}/boss-results")
).json() == []
async def test_invalid_run_returns_404(
self, admin_client: AsyncClient, boss_ctx: dict
):
assert (
await admin_client.get(f"{RUNS_BASE}/9999/boss-results")
).status_code == 404
async def test_invalid_boss_returns_404(
self, admin_client: AsyncClient, boss_ctx: dict
):
response = await admin_client.post(
f"{RUNS_BASE}/{boss_ctx['run_id']}/boss-results",
json={"bossBattleId": 9999, "result": "won"},
)
assert response.status_code == 404
# ---------------------------------------------------------------------------
# Stats
# ---------------------------------------------------------------------------
class TestStats:
async def test_returns_stats_structure(self, admin_client: AsyncClient):
response = await admin_client.get(STATS_BASE)
assert response.status_code == 200
data = response.json()
assert data["totalRuns"] == 0
assert data["totalEncounters"] == 0
assert data["topCaughtPokemon"] == []
assert data["typeDistribution"] == []
async def test_reflects_created_data(self, admin_client: AsyncClient, ctx: dict):
"""Stats should reflect the run and encounter created in ctx."""
response = await admin_client.get(STATS_BASE)
assert response.status_code == 200
data = response.json()
assert data["totalRuns"] >= 1
assert data["totalEncounters"] >= 1
assert data["caughtCount"] >= 1
# ---------------------------------------------------------------------------
# Export
# ---------------------------------------------------------------------------
class TestExport:
async def test_export_games_returns_list(self, admin_client: AsyncClient):
response = await admin_client.get(f"{EXPORT_BASE}/games")
assert response.status_code == 200
assert isinstance(response.json(), list)
async def test_export_pokemon_returns_list(self, admin_client: AsyncClient):
response = await admin_client.get(f"{EXPORT_BASE}/pokemon")
assert response.status_code == 200
assert isinstance(response.json(), list)
async def test_export_evolutions_returns_list(self, admin_client: AsyncClient):
response = await admin_client.get(f"{EXPORT_BASE}/evolutions")
assert response.status_code == 200
assert isinstance(response.json(), list)
async def test_export_game_routes_not_found_returns_404(
self, admin_client: AsyncClient
):
assert (
await admin_client.get(f"{EXPORT_BASE}/games/9999/routes")
).status_code == 404
async def test_export_game_bosses_not_found_returns_404(
self, admin_client: AsyncClient
):
assert (
await admin_client.get(f"{EXPORT_BASE}/games/9999/bosses")
).status_code == 404