feat: auth-aware UI and role-based access control (#67)
## Summary - Add `is_admin` column to users table with Alembic migration and a `require_admin` FastAPI dependency that protects all admin-facing write endpoints (games, pokemon, evolutions, bosses, routes CRUD) - Expose admin status to frontend via user API and update AuthContext to fetch/store `isAdmin` after login - Make navigation menu auth-aware (different links for logged-out, logged-in, and admin users) and protect frontend routes with `ProtectedRoute` and `AdminRoute` components, preserving deep-linking through redirects - Fix test reliability: `drop_all` before `create_all` to clear stale PostgreSQL enums from interrupted test runs - Fix test auth: add `admin_client` fixture and use valid UUID for mock user so tests pass with new admin-protected endpoints ## Test plan - [x] All 252 backend tests pass - [ ] Verify non-admin users cannot access admin write endpoints (games, pokemon, evolutions, bosses CRUD) - [ ] Verify admin users can access admin endpoints normally - [ ] Verify navigation shows correct links for logged-out, logged-in, and admin states - [ ] Verify `/admin/*` routes redirect non-admin users with a toast - [ ] Verify `/runs/new` and `/genlockes/new` redirect unauthenticated users to login, then back after auth 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #67 Co-authored-by: Julian Tabel <juliantabel.jt@gmail.com> Co-committed-by: Julian Tabel <juliantabel.jt@gmail.com>
This commit was merged in pull request #67.
This commit is contained in:
@@ -55,7 +55,7 @@ async def games_ctx(db_session: AsyncSession) -> dict:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def ctx(db_session: AsyncSession, client: AsyncClient, games_ctx: dict) -> dict:
|
||||
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)
|
||||
@@ -67,7 +67,7 @@ async def ctx(db_session: AsyncSession, client: AsyncClient, games_ctx: dict) ->
|
||||
db_session.add(pikachu)
|
||||
await db_session.commit()
|
||||
|
||||
r = await client.post(
|
||||
r = await admin_client.post(
|
||||
GENLOCKES_BASE,
|
||||
json={
|
||||
"name": "Test Genlocke",
|
||||
@@ -80,7 +80,7 @@ async def ctx(db_session: AsyncSession, client: AsyncClient, games_ctx: dict) ->
|
||||
leg1 = next(leg for leg in genlocke["legs"] if leg["legOrder"] == 1)
|
||||
run_id = leg1["runId"]
|
||||
|
||||
enc_r = await client.post(
|
||||
enc_r = await admin_client.post(
|
||||
f"{RUNS_BASE}/{run_id}/encounters",
|
||||
json={"routeId": route1.id, "pokemonId": pikachu.id, "status": "caught"},
|
||||
)
|
||||
@@ -104,13 +104,13 @@ async def ctx(db_session: AsyncSession, client: AsyncClient, games_ctx: dict) ->
|
||||
|
||||
|
||||
class TestListGenlockes:
|
||||
async def test_empty_returns_empty_list(self, client: AsyncClient):
|
||||
response = await client.get(GENLOCKES_BASE)
|
||||
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, client: AsyncClient, ctx: dict):
|
||||
response = await client.get(GENLOCKES_BASE)
|
||||
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
|
||||
@@ -123,9 +123,9 @@ class TestListGenlockes:
|
||||
|
||||
class TestCreateGenlocke:
|
||||
async def test_creates_with_legs_and_first_run(
|
||||
self, client: AsyncClient, games_ctx: dict
|
||||
self, admin_client: AsyncClient, games_ctx: dict
|
||||
):
|
||||
response = await client.post(
|
||||
response = await admin_client.post(
|
||||
GENLOCKES_BASE,
|
||||
json={
|
||||
"name": "My Genlocke",
|
||||
@@ -144,14 +144,14 @@ class TestCreateGenlocke:
|
||||
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, client: AsyncClient):
|
||||
response = await client.post(
|
||||
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, client: AsyncClient):
|
||||
response = await client.post(
|
||||
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
|
||||
@@ -164,9 +164,9 @@ class TestCreateGenlocke:
|
||||
|
||||
class TestGetGenlocke:
|
||||
async def test_returns_genlocke_with_legs_and_stats(
|
||||
self, client: AsyncClient, ctx: dict
|
||||
self, admin_client: AsyncClient, ctx: dict
|
||||
):
|
||||
response = await client.get(f"{GENLOCKES_BASE}/{ctx['genlocke_id']}")
|
||||
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"]
|
||||
@@ -174,8 +174,8 @@ class TestGetGenlocke:
|
||||
assert "stats" in data
|
||||
assert data["stats"]["totalLegs"] == 2
|
||||
|
||||
async def test_not_found_returns_404(self, client: AsyncClient):
|
||||
assert (await client.get(f"{GENLOCKES_BASE}/9999")).status_code == 404
|
||||
async def test_not_found_returns_404(self, admin_client: AsyncClient):
|
||||
assert (await admin_client.get(f"{GENLOCKES_BASE}/9999")).status_code == 404
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -184,30 +184,30 @@ class TestGetGenlocke:
|
||||
|
||||
|
||||
class TestUpdateGenlocke:
|
||||
async def test_updates_name(self, client: AsyncClient, ctx: dict):
|
||||
response = await client.patch(
|
||||
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, client: AsyncClient):
|
||||
async def test_not_found_returns_404(self, admin_client: AsyncClient):
|
||||
assert (
|
||||
await client.patch(f"{GENLOCKES_BASE}/9999", json={"name": "x"})
|
||||
await admin_client.patch(f"{GENLOCKES_BASE}/9999", json={"name": "x"})
|
||||
).status_code == 404
|
||||
|
||||
|
||||
class TestDeleteGenlocke:
|
||||
async def test_deletes_genlocke(self, client: AsyncClient, ctx: dict):
|
||||
async def test_deletes_genlocke(self, admin_client: AsyncClient, ctx: dict):
|
||||
assert (
|
||||
await client.delete(f"{GENLOCKES_BASE}/{ctx['genlocke_id']}")
|
||||
await admin_client.delete(f"{GENLOCKES_BASE}/{ctx['genlocke_id']}")
|
||||
).status_code == 204
|
||||
assert (
|
||||
await client.get(f"{GENLOCKES_BASE}/{ctx['genlocke_id']}")
|
||||
await admin_client.get(f"{GENLOCKES_BASE}/{ctx['genlocke_id']}")
|
||||
).status_code == 404
|
||||
|
||||
async def test_not_found_returns_404(self, client: AsyncClient):
|
||||
assert (await client.delete(f"{GENLOCKES_BASE}/9999")).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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -216,8 +216,8 @@ class TestDeleteGenlocke:
|
||||
|
||||
|
||||
class TestGenlockeLegs:
|
||||
async def test_adds_leg(self, client: AsyncClient, ctx: dict):
|
||||
response = await client.post(
|
||||
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"]},
|
||||
)
|
||||
@@ -225,28 +225,28 @@ class TestGenlockeLegs:
|
||||
legs = response.json()["legs"]
|
||||
assert len(legs) == 3 # was 2, now 3
|
||||
|
||||
async def test_remove_leg_without_run(self, client: AsyncClient, ctx: dict):
|
||||
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 client.delete(
|
||||
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, client: AsyncClient, ctx: dict
|
||||
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 client.delete(
|
||||
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, client: AsyncClient, ctx: dict
|
||||
self, admin_client: AsyncClient, ctx: dict
|
||||
):
|
||||
response = await client.post(
|
||||
response = await admin_client.post(
|
||||
f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/legs",
|
||||
json={"gameId": 9999},
|
||||
)
|
||||
@@ -259,33 +259,33 @@ class TestGenlockeLegs:
|
||||
|
||||
|
||||
class TestAdvanceLeg:
|
||||
async def test_uncompleted_run_returns_400(self, client: AsyncClient, ctx: dict):
|
||||
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 client.post(
|
||||
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, client: AsyncClient, games_ctx: dict):
|
||||
async def test_no_next_leg_returns_400(self, admin_client: AsyncClient, games_ctx: dict):
|
||||
"""A single-leg genlocke cannot be advanced."""
|
||||
r = await client.post(
|
||||
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 client.patch(f"{RUNS_BASE}/{run_id}", json={"status": "completed"})
|
||||
await admin_client.patch(f"{RUNS_BASE}/{run_id}", json={"status": "completed"})
|
||||
|
||||
response = await client.post(
|
||||
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, client: AsyncClient, ctx: dict):
|
||||
async def test_advances_to_next_leg(self, admin_client: AsyncClient, ctx: dict):
|
||||
"""Completing the current run allows advancing to the next leg."""
|
||||
await client.patch(f"{RUNS_BASE}/{ctx['run_id']}", json={"status": "completed"})
|
||||
await admin_client.patch(f"{RUNS_BASE}/{ctx['run_id']}", json={"status": "completed"})
|
||||
|
||||
response = await client.post(
|
||||
response = await admin_client.post(
|
||||
f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/legs/1/advance"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -293,11 +293,11 @@ class TestAdvanceLeg:
|
||||
leg2 = next(leg for leg in legs if leg["legOrder"] == 2)
|
||||
assert leg2["runId"] is not None
|
||||
|
||||
async def test_advances_with_transfers(self, client: AsyncClient, ctx: dict):
|
||||
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 client.patch(f"{RUNS_BASE}/{ctx['run_id']}", json={"status": "completed"})
|
||||
await admin_client.patch(f"{RUNS_BASE}/{ctx['run_id']}", json={"status": "completed"})
|
||||
|
||||
response = await client.post(
|
||||
response = await admin_client.post(
|
||||
f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/legs/1/advance",
|
||||
json={"transferEncounterIds": [ctx["encounter_id"]]},
|
||||
)
|
||||
@@ -308,7 +308,7 @@ class TestAdvanceLeg:
|
||||
assert new_run_id is not None
|
||||
|
||||
# The new run should contain the transferred (egg) encounter
|
||||
run_detail = (await client.get(f"{RUNS_BASE}/{new_run_id}")).json()
|
||||
run_detail = (await admin_client.get(f"{RUNS_BASE}/{new_run_id}")).json()
|
||||
assert len(run_detail["encounters"]) == 1
|
||||
|
||||
|
||||
@@ -318,56 +318,56 @@ class TestAdvanceLeg:
|
||||
|
||||
|
||||
class TestGenlockeGraveyard:
|
||||
async def test_returns_empty_graveyard(self, client: AsyncClient, ctx: dict):
|
||||
response = await client.get(f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/graveyard")
|
||||
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, client: AsyncClient):
|
||||
assert (await client.get(f"{GENLOCKES_BASE}/9999/graveyard")).status_code == 404
|
||||
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, client: AsyncClient, ctx: dict):
|
||||
response = await client.get(f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/lineages")
|
||||
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, client: AsyncClient):
|
||||
assert (await client.get(f"{GENLOCKES_BASE}/9999/lineages")).status_code == 404
|
||||
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, client: AsyncClient, ctx: dict):
|
||||
response = await client.get(
|
||||
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, client: AsyncClient):
|
||||
async def test_not_found_returns_404(self, admin_client: AsyncClient):
|
||||
assert (
|
||||
await client.get(f"{GENLOCKES_BASE}/9999/retired-families")
|
||||
await admin_client.get(f"{GENLOCKES_BASE}/9999/retired-families")
|
||||
).status_code == 404
|
||||
|
||||
|
||||
class TestLegSurvivors:
|
||||
async def test_returns_survivors(self, client: AsyncClient, ctx: dict):
|
||||
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 client.get(
|
||||
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, client: AsyncClient, ctx: dict):
|
||||
async def test_leg_not_found_returns_404(self, admin_client: AsyncClient, ctx: dict):
|
||||
assert (
|
||||
await client.get(f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/legs/99/survivors")
|
||||
await admin_client.get(f"{GENLOCKES_BASE}/{ctx['genlocke_id']}/legs/99/survivors")
|
||||
).status_code == 404
|
||||
|
||||
|
||||
@@ -385,13 +385,13 @@ BOSS_PAYLOAD = {
|
||||
|
||||
|
||||
class TestBossCRUD:
|
||||
async def test_empty_list(self, client: AsyncClient, games_ctx: dict):
|
||||
response = await client.get(f"{GAMES_BASE}/{games_ctx['game1_id']}/bosses")
|
||||
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, client: AsyncClient, games_ctx: dict):
|
||||
response = await client.post(
|
||||
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
|
||||
@@ -400,50 +400,50 @@ class TestBossCRUD:
|
||||
assert data["levelCap"] == 14
|
||||
assert data["pokemon"] == []
|
||||
|
||||
async def test_updates_boss(self, client: AsyncClient, games_ctx: dict):
|
||||
async def test_updates_boss(self, admin_client: AsyncClient, games_ctx: dict):
|
||||
boss = (
|
||||
await client.post(
|
||||
await admin_client.post(
|
||||
f"{GAMES_BASE}/{games_ctx['game1_id']}/bosses", json=BOSS_PAYLOAD
|
||||
)
|
||||
).json()
|
||||
response = await client.put(
|
||||
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, client: AsyncClient, games_ctx: dict):
|
||||
async def test_deletes_boss(self, admin_client: AsyncClient, games_ctx: dict):
|
||||
boss = (
|
||||
await client.post(
|
||||
await admin_client.post(
|
||||
f"{GAMES_BASE}/{games_ctx['game1_id']}/bosses", json=BOSS_PAYLOAD
|
||||
)
|
||||
).json()
|
||||
assert (
|
||||
await client.delete(
|
||||
await admin_client.delete(
|
||||
f"{GAMES_BASE}/{games_ctx['game1_id']}/bosses/{boss['id']}"
|
||||
)
|
||||
).status_code == 204
|
||||
assert (
|
||||
await client.get(f"{GAMES_BASE}/{games_ctx['game1_id']}/bosses")
|
||||
await admin_client.get(f"{GAMES_BASE}/{games_ctx['game1_id']}/bosses")
|
||||
).json() == []
|
||||
|
||||
async def test_boss_not_found_returns_404(
|
||||
self, client: AsyncClient, games_ctx: dict
|
||||
self, admin_client: AsyncClient, games_ctx: dict
|
||||
):
|
||||
assert (
|
||||
await client.put(
|
||||
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, client: AsyncClient):
|
||||
assert (await client.get(f"{GAMES_BASE}/9999/bosses")).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, client: AsyncClient):
|
||||
async def test_game_without_version_group_returns_400(self, admin_client: AsyncClient):
|
||||
game = (
|
||||
await client.post(
|
||||
await admin_client.post(
|
||||
GAMES_BASE,
|
||||
json={
|
||||
"name": "No VG",
|
||||
@@ -454,7 +454,7 @@ class TestBossCRUD:
|
||||
)
|
||||
).json()
|
||||
assert (
|
||||
await client.get(f"{GAMES_BASE}/{game['id']}/bosses")
|
||||
await admin_client.get(f"{GAMES_BASE}/{game['id']}/bosses")
|
||||
).status_code == 400
|
||||
|
||||
|
||||
@@ -465,27 +465,27 @@ class TestBossCRUD:
|
||||
|
||||
class TestBossResults:
|
||||
@pytest.fixture
|
||||
async def boss_ctx(self, client: AsyncClient, games_ctx: dict) -> dict:
|
||||
async def boss_ctx(self, admin_client: AsyncClient, games_ctx: dict) -> dict:
|
||||
"""A boss battle and a run for boss-result tests."""
|
||||
boss = (
|
||||
await client.post(
|
||||
await admin_client.post(
|
||||
f"{GAMES_BASE}/{games_ctx['game1_id']}/bosses", json=BOSS_PAYLOAD
|
||||
)
|
||||
).json()
|
||||
run = (
|
||||
await client.post(
|
||||
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, client: AsyncClient, boss_ctx: dict):
|
||||
response = await client.get(f"{RUNS_BASE}/{boss_ctx['run_id']}/boss-results")
|
||||
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, client: AsyncClient, boss_ctx: dict):
|
||||
response = await client.post(
|
||||
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},
|
||||
)
|
||||
@@ -495,13 +495,13 @@ class TestBossResults:
|
||||
assert data["attempts"] == 1
|
||||
assert data["completedAt"] is not None
|
||||
|
||||
async def test_upserts_existing_result(self, client: AsyncClient, boss_ctx: dict):
|
||||
async def test_upserts_existing_result(self, admin_client: AsyncClient, boss_ctx: dict):
|
||||
"""POSTing the same boss twice updates the result (upsert)."""
|
||||
await client.post(
|
||||
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 client.post(
|
||||
response = await admin_client.post(
|
||||
f"{RUNS_BASE}/{boss_ctx['run_id']}/boss-results",
|
||||
json={"bossBattleId": boss_ctx["boss_id"], "result": "lost", "attempts": 3},
|
||||
)
|
||||
@@ -510,31 +510,31 @@ class TestBossResults:
|
||||
assert response.json()["attempts"] == 3
|
||||
# Still only one record
|
||||
all_results = (
|
||||
await client.get(f"{RUNS_BASE}/{boss_ctx['run_id']}/boss-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, client: AsyncClient, boss_ctx: dict):
|
||||
async def test_deletes_boss_result(self, admin_client: AsyncClient, boss_ctx: dict):
|
||||
result = (
|
||||
await client.post(
|
||||
await admin_client.post(
|
||||
f"{RUNS_BASE}/{boss_ctx['run_id']}/boss-results",
|
||||
json={"bossBattleId": boss_ctx["boss_id"], "result": "won"},
|
||||
)
|
||||
).json()
|
||||
assert (
|
||||
await client.delete(
|
||||
await admin_client.delete(
|
||||
f"{RUNS_BASE}/{boss_ctx['run_id']}/boss-results/{result['id']}"
|
||||
)
|
||||
).status_code == 204
|
||||
assert (
|
||||
await client.get(f"{RUNS_BASE}/{boss_ctx['run_id']}/boss-results")
|
||||
await admin_client.get(f"{RUNS_BASE}/{boss_ctx['run_id']}/boss-results")
|
||||
).json() == []
|
||||
|
||||
async def test_invalid_run_returns_404(self, client: AsyncClient, boss_ctx: dict):
|
||||
assert (await client.get(f"{RUNS_BASE}/9999/boss-results")).status_code == 404
|
||||
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, client: AsyncClient, boss_ctx: dict):
|
||||
response = await client.post(
|
||||
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"},
|
||||
)
|
||||
@@ -547,8 +547,8 @@ class TestBossResults:
|
||||
|
||||
|
||||
class TestStats:
|
||||
async def test_returns_stats_structure(self, client: AsyncClient):
|
||||
response = await client.get(STATS_BASE)
|
||||
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
|
||||
@@ -556,9 +556,9 @@ class TestStats:
|
||||
assert data["topCaughtPokemon"] == []
|
||||
assert data["typeDistribution"] == []
|
||||
|
||||
async def test_reflects_created_data(self, client: AsyncClient, ctx: dict):
|
||||
async def test_reflects_created_data(self, admin_client: AsyncClient, ctx: dict):
|
||||
"""Stats should reflect the run and encounter created in ctx."""
|
||||
response = await client.get(STATS_BASE)
|
||||
response = await admin_client.get(STATS_BASE)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["totalRuns"] >= 1
|
||||
@@ -572,23 +572,23 @@ class TestStats:
|
||||
|
||||
|
||||
class TestExport:
|
||||
async def test_export_games_returns_list(self, client: AsyncClient):
|
||||
response = await client.get(f"{EXPORT_BASE}/games")
|
||||
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, client: AsyncClient):
|
||||
response = await client.get(f"{EXPORT_BASE}/pokemon")
|
||||
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, client: AsyncClient):
|
||||
response = await client.get(f"{EXPORT_BASE}/evolutions")
|
||||
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, client: AsyncClient):
|
||||
assert (await client.get(f"{EXPORT_BASE}/games/9999/routes")).status_code == 404
|
||||
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, client: AsyncClient):
|
||||
assert (await client.get(f"{EXPORT_BASE}/games/9999/bosses")).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
|
||||
|
||||
Reference in New Issue
Block a user