feat: auth-aware UI and role-based access control (#67)
All checks were successful
CI / backend-tests (push) Successful in 32s
CI / frontend-tests (push) Successful in 29s

## 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:
2026-03-21 11:44:05 +01:00
committed by TheFurya
parent f7731b0497
commit e8ded9184b
27 changed files with 826 additions and 347 deletions

View File

@@ -7,7 +7,7 @@ from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
import app.models # noqa: F401 — ensures all models register with Base.metadata
from app.core.auth import AuthUser, get_current_user
from app.core.auth import AuthUser, get_current_user, require_admin
from app.core.database import Base, get_session
from app.main import app
@@ -24,6 +24,7 @@ async def engine():
"""Create the test engine and schema once for the entire session."""
eng = create_async_engine(TEST_DATABASE_URL, echo=False)
async with eng.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
yield eng
async with eng.begin() as conn:
@@ -69,7 +70,11 @@ async def client(db_session):
@pytest.fixture
def mock_auth_user():
"""Return a mock authenticated user for tests."""
return AuthUser(id="test-user-123", email="test@example.com", role="authenticated")
return AuthUser(
id="00000000-0000-4000-a000-000000000001",
email="test@example.com",
role="authenticated",
)
@pytest.fixture
@@ -93,11 +98,34 @@ async def auth_client(db_session, auth_override):
yield ac
@pytest.fixture
def admin_override(mock_auth_user):
"""Override require_admin and get_current_user to return a mock user."""
def _override():
return mock_auth_user
app.dependency_overrides[require_admin] = _override
app.dependency_overrides[get_current_user] = _override
yield
app.dependency_overrides.pop(require_admin, None)
app.dependency_overrides.pop(get_current_user, None)
@pytest.fixture
async def admin_client(db_session, admin_override):
"""Async HTTP client with mocked admin authentication."""
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as ac:
yield ac
@pytest.fixture
def valid_token():
"""Generate a valid JWT token for testing."""
payload = {
"sub": "test-user-123",
"sub": "00000000-0000-4000-a000-000000000001",
"email": "test@example.com",
"role": "authenticated",
"aud": "authenticated",

View File

@@ -1,12 +1,14 @@
import time
from uuid import UUID
import jwt
import pytest
from httpx import ASGITransport, AsyncClient
from app.core.auth import AuthUser, get_current_user, require_auth
from app.core.auth import AuthUser, get_current_user, require_admin, require_auth
from app.core.config import settings
from app.main import app
from app.models.user import User
@pytest.fixture
@@ -177,3 +179,140 @@ async def test_read_endpoint_without_token(db_session):
) as ac:
response = await ac.get("/runs")
assert response.status_code == 200
async def test_require_admin_valid_admin_user(db_session):
"""Test require_admin passes through for admin user."""
user_id = "11111111-1111-1111-1111-111111111111"
admin_user = User(
id=UUID(user_id),
email="admin@example.com",
is_admin=True,
)
db_session.add(admin_user)
await db_session.commit()
auth_user = AuthUser(id=user_id, email="admin@example.com")
result = await require_admin(user=auth_user, session=db_session)
assert result is auth_user
async def test_require_admin_non_admin_user(db_session):
"""Test require_admin raises 403 for non-admin user."""
from fastapi import HTTPException
user_id = "22222222-2222-2222-2222-222222222222"
regular_user = User(
id=UUID(user_id),
email="user@example.com",
is_admin=False,
)
db_session.add(regular_user)
await db_session.commit()
auth_user = AuthUser(id=user_id, email="user@example.com")
with pytest.raises(HTTPException) as exc_info:
await require_admin(user=auth_user, session=db_session)
assert exc_info.value.status_code == 403
assert exc_info.value.detail == "Admin access required"
async def test_require_admin_user_not_in_db(db_session):
"""Test require_admin raises 403 for user not in database."""
from fastapi import HTTPException
auth_user = AuthUser(
id="33333333-3333-3333-3333-333333333333", email="ghost@example.com"
)
with pytest.raises(HTTPException) as exc_info:
await require_admin(user=auth_user, session=db_session)
assert exc_info.value.status_code == 403
assert exc_info.value.detail == "Admin access required"
async def test_admin_endpoint_returns_403_for_non_admin(
db_session, jwt_secret, monkeypatch
):
"""Test that admin endpoint returns 403 for authenticated non-admin user."""
user_id = "44444444-4444-4444-4444-444444444444"
regular_user = User(
id=UUID(user_id),
email="nonadmin@example.com",
is_admin=False,
)
db_session.add(regular_user)
await db_session.commit()
monkeypatch.setattr(settings, "supabase_jwt_secret", jwt_secret)
token = jwt.encode(
{
"sub": user_id,
"email": "nonadmin@example.com",
"role": "authenticated",
"aud": "authenticated",
"exp": int(time.time()) + 3600,
},
jwt_secret,
algorithm="HS256",
)
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
headers={"Authorization": f"Bearer {token}"},
) as ac:
response = await ac.post(
"/games",
json={
"name": "Test Game",
"slug": "test-game",
"generation": 1,
"region": "Kanto",
"category": "core",
},
)
assert response.status_code == 403
assert response.json()["detail"] == "Admin access required"
async def test_admin_endpoint_succeeds_for_admin(db_session, jwt_secret, monkeypatch):
"""Test that admin endpoint succeeds for authenticated admin user."""
user_id = "55555555-5555-5555-5555-555555555555"
admin_user = User(
id=UUID(user_id),
email="admin@example.com",
is_admin=True,
)
db_session.add(admin_user)
await db_session.commit()
monkeypatch.setattr(settings, "supabase_jwt_secret", jwt_secret)
token = jwt.encode(
{
"sub": user_id,
"email": "admin@example.com",
"role": "authenticated",
"aud": "authenticated",
"exp": int(time.time()) + 3600,
},
jwt_secret,
algorithm="HS256",
)
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
headers={"Authorization": f"Bearer {token}"},
) as ac:
response = await ac.post(
"/games",
json={
"name": "Test Game",
"slug": "test-game",
"generation": 1,
"region": "Kanto",
"category": "core",
},
)
assert response.status_code == 201
assert response.json()["name"] == "Test Game"

View File

@@ -17,9 +17,9 @@ GAME_PAYLOAD = {
@pytest.fixture
async def game(auth_client: AsyncClient) -> dict:
async def game(admin_client: AsyncClient) -> dict:
"""A game created via the API (no version_group_id)."""
response = await auth_client.post(BASE, json=GAME_PAYLOAD)
response = await admin_client.post(BASE, json=GAME_PAYLOAD)
assert response.status_code == 201
return response.json()
@@ -68,8 +68,8 @@ class TestListGames:
class TestCreateGame:
async def test_creates_and_returns_game(self, auth_client: AsyncClient):
response = await auth_client.post(BASE, json=GAME_PAYLOAD)
async def test_creates_and_returns_game(self, admin_client: AsyncClient):
response = await admin_client.post(BASE, json=GAME_PAYLOAD)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Pokemon Red"
@@ -77,15 +77,15 @@ class TestCreateGame:
assert isinstance(data["id"], int)
async def test_duplicate_slug_returns_409(
self, auth_client: AsyncClient, game: dict
self, admin_client: AsyncClient, game: dict
):
response = await auth_client.post(
response = await admin_client.post(
BASE, json={**GAME_PAYLOAD, "name": "Pokemon Red v2"}
)
assert response.status_code == 409
async def test_missing_required_field_returns_422(self, auth_client: AsyncClient):
response = await auth_client.post(BASE, json={"name": "Pokemon Red"})
async def test_missing_required_field_returns_422(self, admin_client: AsyncClient):
response = await admin_client.post(BASE, json={"name": "Pokemon Red"})
assert response.status_code == 422
@@ -115,35 +115,35 @@ class TestGetGame:
class TestUpdateGame:
async def test_updates_name(self, auth_client: AsyncClient, game: dict):
response = await auth_client.put(
async def test_updates_name(self, admin_client: AsyncClient, game: dict):
response = await admin_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, auth_client: AsyncClient, game: dict
self, admin_client: AsyncClient, game: dict
):
response = await auth_client.put(
response = await admin_client.put(
f"{BASE}/{game['id']}", json={"name": "New Name"}
)
assert response.json()["slug"] == "red"
async def test_not_found_returns_404(self, auth_client: AsyncClient):
async def test_not_found_returns_404(self, admin_client: AsyncClient):
assert (
await auth_client.put(f"{BASE}/9999", json={"name": "x"})
await admin_client.put(f"{BASE}/9999", json={"name": "x"})
).status_code == 404
async def test_duplicate_slug_returns_409(self, auth_client: AsyncClient):
await auth_client.post(
async def test_duplicate_slug_returns_409(self, admin_client: AsyncClient):
await admin_client.post(
BASE, json={**GAME_PAYLOAD, "slug": "blue", "name": "Blue"}
)
r1 = await auth_client.post(
r1 = await admin_client.post(
BASE, json={**GAME_PAYLOAD, "slug": "red", "name": "Red"}
)
game_id = r1.json()["id"]
response = await auth_client.put(f"{BASE}/{game_id}", json={"slug": "blue"})
response = await admin_client.put(f"{BASE}/{game_id}", json={"slug": "blue"})
assert response.status_code == 409
@@ -153,13 +153,13 @@ class TestUpdateGame:
class TestDeleteGame:
async def test_deletes_game(self, auth_client: AsyncClient, game: dict):
response = await auth_client.delete(f"{BASE}/{game['id']}")
async def test_deletes_game(self, admin_client: AsyncClient, game: dict):
response = await admin_client.delete(f"{BASE}/{game['id']}")
assert response.status_code == 204
assert (await auth_client.get(f"{BASE}/{game['id']}")).status_code == 404
assert (await admin_client.get(f"{BASE}/{game['id']}")).status_code == 404
async def test_not_found_returns_404(self, auth_client: AsyncClient):
assert (await auth_client.delete(f"{BASE}/9999")).status_code == 404
async def test_not_found_returns_404(self, admin_client: AsyncClient):
assert (await admin_client.delete(f"{BASE}/9999")).status_code == 404
# ---------------------------------------------------------------------------
@@ -195,9 +195,9 @@ class TestListByRegion:
class TestCreateRoute:
async def test_creates_route(self, auth_client: AsyncClient, game_with_vg: tuple):
async def test_creates_route(self, admin_client: AsyncClient, game_with_vg: tuple):
game_id, _ = game_with_vg
response = await auth_client.post(
response = await admin_client.post(
f"{BASE}/{game_id}/routes",
json={"name": "Pallet Town", "order": 1},
)
@@ -208,35 +208,35 @@ class TestCreateRoute:
assert isinstance(data["id"], int)
async def test_game_detail_includes_route(
self, auth_client: AsyncClient, game_with_vg: tuple
self, admin_client: AsyncClient, game_with_vg: tuple
):
game_id, _ = game_with_vg
await auth_client.post(
await admin_client.post(
f"{BASE}/{game_id}/routes", json={"name": "Route 1", "order": 1}
)
response = await auth_client.get(f"{BASE}/{game_id}")
response = await admin_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, auth_client: AsyncClient, game: dict
self, admin_client: AsyncClient, game: dict
):
response = await auth_client.post(
response = await admin_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, auth_client: AsyncClient, game_with_vg: tuple
self, admin_client: AsyncClient, game_with_vg: tuple
):
"""list_game_routes only returns routes that have Pokemon encounters."""
game_id, _ = game_with_vg
await auth_client.post(
await admin_client.post(
f"{BASE}/{game_id}/routes", json={"name": "Route 1", "order": 1}
)
response = await auth_client.get(f"{BASE}/{game_id}/routes?flat=true")
response = await admin_client.get(f"{BASE}/{game_id}/routes?flat=true")
assert response.status_code == 200
assert response.json() == []
@@ -248,15 +248,15 @@ class TestCreateRoute:
class TestUpdateRoute:
async def test_updates_route_name(
self, auth_client: AsyncClient, game_with_vg: tuple
self, admin_client: AsyncClient, game_with_vg: tuple
):
game_id, _ = game_with_vg
r = (
await auth_client.post(
await admin_client.post(
f"{BASE}/{game_id}/routes", json={"name": "Old Name", "order": 1}
)
).json()
response = await auth_client.put(
response = await admin_client.put(
f"{BASE}/{game_id}/routes/{r['id']}",
json={"name": "New Name"},
)
@@ -264,11 +264,11 @@ class TestUpdateRoute:
assert response.json()["name"] == "New Name"
async def test_route_not_found_returns_404(
self, auth_client: AsyncClient, game_with_vg: tuple
self, admin_client: AsyncClient, game_with_vg: tuple
):
game_id, _ = game_with_vg
assert (
await auth_client.put(f"{BASE}/{game_id}/routes/9999", json={"name": "x"})
await admin_client.put(f"{BASE}/{game_id}/routes/9999", json={"name": "x"})
).status_code == 404
@@ -278,26 +278,26 @@ class TestUpdateRoute:
class TestDeleteRoute:
async def test_deletes_route(self, auth_client: AsyncClient, game_with_vg: tuple):
async def test_deletes_route(self, admin_client: AsyncClient, game_with_vg: tuple):
game_id, _ = game_with_vg
r = (
await auth_client.post(
await admin_client.post(
f"{BASE}/{game_id}/routes", json={"name": "Route 1", "order": 1}
)
).json()
assert (
await auth_client.delete(f"{BASE}/{game_id}/routes/{r['id']}")
await admin_client.delete(f"{BASE}/{game_id}/routes/{r['id']}")
).status_code == 204
# No longer in game detail
detail = (await auth_client.get(f"{BASE}/{game_id}")).json()
detail = (await admin_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, auth_client: AsyncClient, game_with_vg: tuple
self, admin_client: AsyncClient, game_with_vg: tuple
):
game_id, _ = game_with_vg
assert (
await auth_client.delete(f"{BASE}/{game_id}/routes/9999")
await admin_client.delete(f"{BASE}/{game_id}/routes/9999")
).status_code == 404
@@ -307,20 +307,20 @@ class TestDeleteRoute:
class TestReorderRoutes:
async def test_reorders_routes(self, auth_client: AsyncClient, game_with_vg: tuple):
async def test_reorders_routes(self, admin_client: AsyncClient, game_with_vg: tuple):
game_id, _ = game_with_vg
r1 = (
await auth_client.post(
await admin_client.post(
f"{BASE}/{game_id}/routes", json={"name": "A", "order": 1}
)
).json()
r2 = (
await auth_client.post(
await admin_client.post(
f"{BASE}/{game_id}/routes", json={"name": "B", "order": 2}
)
).json()
response = await auth_client.put(
response = await admin_client.put(
f"{BASE}/{game_id}/routes/reorder",
json={
"routes": [{"id": r1["id"], "order": 2}, {"id": r2["id"], "order": 1}]

View File

@@ -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

View File

@@ -29,21 +29,21 @@ CHARMANDER_DATA = {
@pytest.fixture
async def pikachu(client: AsyncClient) -> dict:
response = await client.post(POKEMON_BASE, json=PIKACHU_DATA)
async def pikachu(admin_client: AsyncClient) -> dict:
response = await admin_client.post(POKEMON_BASE, json=PIKACHU_DATA)
assert response.status_code == 201
return response.json()
@pytest.fixture
async def charmander(client: AsyncClient) -> dict:
response = await client.post(POKEMON_BASE, json=CHARMANDER_DATA)
async def charmander(admin_client: AsyncClient) -> dict:
response = await admin_client.post(POKEMON_BASE, json=CHARMANDER_DATA)
assert response.status_code == 201
return response.json()
@pytest.fixture
async def ctx(db_session: AsyncSession, client: AsyncClient) -> dict:
async def ctx(db_session: AsyncSession, admin_client: AsyncClient) -> dict:
"""Full context: game + route + two pokemon + nuzlocke encounter on pikachu."""
vg = VersionGroup(name="Poke Test VG", slug="poke-test-vg")
db_session.add(vg)
@@ -63,11 +63,11 @@ async def ctx(db_session: AsyncSession, client: AsyncClient) -> dict:
db_session.add(route)
await db_session.flush()
r1 = await client.post(POKEMON_BASE, json=PIKACHU_DATA)
r1 = await admin_client.post(POKEMON_BASE, json=PIKACHU_DATA)
assert r1.status_code == 201
pikachu = r1.json()
r2 = await client.post(POKEMON_BASE, json=CHARMANDER_DATA)
r2 = await admin_client.post(POKEMON_BASE, json=CHARMANDER_DATA)
assert r2.status_code == 201
charmander = r2.json()
@@ -146,8 +146,8 @@ class TestListPokemon:
class TestCreatePokemon:
async def test_creates_pokemon(self, client: AsyncClient):
response = await client.post(POKEMON_BASE, json=PIKACHU_DATA)
async def test_creates_pokemon(self, admin_client: AsyncClient):
response = await admin_client.post(POKEMON_BASE, json=PIKACHU_DATA)
assert response.status_code == 201
data = response.json()
assert data["name"] == "pikachu"
@@ -156,16 +156,16 @@ class TestCreatePokemon:
assert isinstance(data["id"], int)
async def test_duplicate_pokeapi_id_returns_409(
self, client: AsyncClient, pikachu: dict
self, admin_client: AsyncClient, pikachu: dict
):
response = await client.post(
response = await admin_client.post(
POKEMON_BASE,
json={**PIKACHU_DATA, "name": "pikachu-copy"},
)
assert response.status_code == 409
async def test_missing_required_returns_422(self, client: AsyncClient):
response = await client.post(POKEMON_BASE, json={"name": "pikachu"})
async def test_missing_required_returns_422(self, admin_client: AsyncClient):
response = await admin_client.post(POKEMON_BASE, json={"name": "pikachu"})
assert response.status_code == 422
@@ -190,25 +190,25 @@ class TestGetPokemon:
class TestUpdatePokemon:
async def test_updates_name(self, client: AsyncClient, pikachu: dict):
response = await client.put(
async def test_updates_name(self, admin_client: AsyncClient, pikachu: dict):
response = await admin_client.put(
f"{POKEMON_BASE}/{pikachu['id']}", json={"name": "Pikachu"}
)
assert response.status_code == 200
assert response.json()["name"] == "Pikachu"
async def test_duplicate_pokeapi_id_returns_409(
self, client: AsyncClient, pikachu: dict, charmander: dict
self, admin_client: AsyncClient, pikachu: dict, charmander: dict
):
response = await client.put(
response = await admin_client.put(
f"{POKEMON_BASE}/{pikachu['id']}",
json={"pokeapiId": charmander["pokeapiId"]},
)
assert response.status_code == 409
async def test_not_found_returns_404(self, client: AsyncClient):
async def test_not_found_returns_404(self, admin_client: AsyncClient):
assert (
await client.put(f"{POKEMON_BASE}/9999", json={"name": "x"})
await admin_client.put(f"{POKEMON_BASE}/9999", json={"name": "x"})
).status_code == 404
@@ -218,22 +218,22 @@ class TestUpdatePokemon:
class TestDeletePokemon:
async def test_deletes_pokemon(self, client: AsyncClient, charmander: dict):
async def test_deletes_pokemon(self, admin_client: AsyncClient, charmander: dict):
assert (
await client.delete(f"{POKEMON_BASE}/{charmander['id']}")
await admin_client.delete(f"{POKEMON_BASE}/{charmander['id']}")
).status_code == 204
assert (
await client.get(f"{POKEMON_BASE}/{charmander['id']}")
await admin_client.get(f"{POKEMON_BASE}/{charmander['id']}")
).status_code == 404
async def test_not_found_returns_404(self, client: AsyncClient):
assert (await client.delete(f"{POKEMON_BASE}/9999")).status_code == 404
async def test_not_found_returns_404(self, admin_client: AsyncClient):
assert (await admin_client.delete(f"{POKEMON_BASE}/9999")).status_code == 404
async def test_pokemon_with_encounters_returns_409(
self, client: AsyncClient, ctx: dict
self, admin_client: AsyncClient, ctx: dict
):
"""Pokemon referenced by a nuzlocke encounter cannot be deleted."""
response = await client.delete(f"{POKEMON_BASE}/{ctx['pikachu_id']}")
response = await admin_client.delete(f"{POKEMON_BASE}/{ctx['pikachu_id']}")
assert response.status_code == 409
@@ -249,9 +249,9 @@ class TestPokemonFamilies:
assert response.json()["families"] == []
async def test_returns_family_grouping(
self, client: AsyncClient, pikachu: dict, charmander: dict
self, admin_client: AsyncClient, pikachu: dict, charmander: dict
):
await client.post(
await admin_client.post(
EVO_BASE,
json={
"fromPokemonId": pikachu["id"],
@@ -259,7 +259,7 @@ class TestPokemonFamilies:
"trigger": "level-up",
},
)
response = await client.get(f"{POKEMON_BASE}/families")
response = await admin_client.get(f"{POKEMON_BASE}/families")
assert response.status_code == 200
families = response.json()["families"]
assert len(families) == 1
@@ -280,9 +280,9 @@ class TestPokemonEvolutionChain:
assert response.json() == []
async def test_returns_chain_for_multi_stage(
self, client: AsyncClient, pikachu: dict, charmander: dict
self, admin_client: AsyncClient, pikachu: dict, charmander: dict
):
await client.post(
await admin_client.post(
EVO_BASE,
json={
"fromPokemonId": pikachu["id"],
@@ -290,7 +290,7 @@ class TestPokemonEvolutionChain:
"trigger": "level-up",
},
)
response = await client.get(f"{POKEMON_BASE}/{pikachu['id']}/evolution-chain")
response = await admin_client.get(f"{POKEMON_BASE}/{pikachu['id']}/evolution-chain")
assert response.status_code == 200
chain = response.json()
assert len(chain) == 1
@@ -317,9 +317,9 @@ class TestListEvolutions:
assert data["total"] == 0
async def test_returns_created_evolution(
self, client: AsyncClient, pikachu: dict, charmander: dict
self, admin_client: AsyncClient, pikachu: dict, charmander: dict
):
await client.post(
await admin_client.post(
EVO_BASE,
json={
"fromPokemonId": pikachu["id"],
@@ -327,14 +327,14 @@ class TestListEvolutions:
"trigger": "level-up",
},
)
response = await client.get(EVO_BASE)
response = await admin_client.get(EVO_BASE)
assert response.status_code == 200
assert response.json()["total"] == 1
async def test_filter_by_trigger(
self, client: AsyncClient, pikachu: dict, charmander: dict
self, admin_client: AsyncClient, pikachu: dict, charmander: dict
):
await client.post(
await admin_client.post(
EVO_BASE,
json={
"fromPokemonId": pikachu["id"],
@@ -342,9 +342,9 @@ class TestListEvolutions:
"trigger": "use-item",
},
)
hit = await client.get(EVO_BASE, params={"trigger": "use-item"})
hit = await admin_client.get(EVO_BASE, params={"trigger": "use-item"})
assert hit.json()["total"] == 1
miss = await client.get(EVO_BASE, params={"trigger": "level-up"})
miss = await admin_client.get(EVO_BASE, params={"trigger": "level-up"})
assert miss.json()["total"] == 0
@@ -355,9 +355,9 @@ class TestListEvolutions:
class TestCreateEvolution:
async def test_creates_evolution(
self, client: AsyncClient, pikachu: dict, charmander: dict
self, admin_client: AsyncClient, pikachu: dict, charmander: dict
):
response = await client.post(
response = await admin_client.post(
EVO_BASE,
json={
"fromPokemonId": pikachu["id"],
@@ -374,9 +374,9 @@ class TestCreateEvolution:
assert data["toPokemon"]["name"] == "charmander"
async def test_invalid_from_pokemon_returns_404(
self, client: AsyncClient, charmander: dict
self, admin_client: AsyncClient, charmander: dict
):
response = await client.post(
response = await admin_client.post(
EVO_BASE,
json={
"fromPokemonId": 9999,
@@ -387,9 +387,9 @@ class TestCreateEvolution:
assert response.status_code == 404
async def test_invalid_to_pokemon_returns_404(
self, client: AsyncClient, pikachu: dict
self, admin_client: AsyncClient, pikachu: dict
):
response = await client.post(
response = await admin_client.post(
EVO_BASE,
json={
"fromPokemonId": pikachu["id"],
@@ -408,9 +408,9 @@ class TestCreateEvolution:
class TestUpdateEvolution:
@pytest.fixture
async def evolution(
self, client: AsyncClient, pikachu: dict, charmander: dict
self, admin_client: AsyncClient, pikachu: dict, charmander: dict
) -> dict:
response = await client.post(
response = await admin_client.post(
EVO_BASE,
json={
"fromPokemonId": pikachu["id"],
@@ -420,16 +420,16 @@ class TestUpdateEvolution:
)
return response.json()
async def test_updates_trigger(self, client: AsyncClient, evolution: dict):
response = await client.put(
async def test_updates_trigger(self, admin_client: AsyncClient, evolution: dict):
response = await admin_client.put(
f"{EVO_BASE}/{evolution['id']}", json={"trigger": "use-item"}
)
assert response.status_code == 200
assert response.json()["trigger"] == "use-item"
async def test_not_found_returns_404(self, client: AsyncClient):
async def test_not_found_returns_404(self, admin_client: AsyncClient):
assert (
await client.put(f"{EVO_BASE}/9999", json={"trigger": "level-up"})
await admin_client.put(f"{EVO_BASE}/9999", json={"trigger": "level-up"})
).status_code == 404
@@ -441,9 +441,9 @@ class TestUpdateEvolution:
class TestDeleteEvolution:
@pytest.fixture
async def evolution(
self, client: AsyncClient, pikachu: dict, charmander: dict
self, admin_client: AsyncClient, pikachu: dict, charmander: dict
) -> dict:
response = await client.post(
response = await admin_client.post(
EVO_BASE,
json={
"fromPokemonId": pikachu["id"],
@@ -453,12 +453,12 @@ class TestDeleteEvolution:
)
return response.json()
async def test_deletes_evolution(self, client: AsyncClient, evolution: dict):
assert (await client.delete(f"{EVO_BASE}/{evolution['id']}")).status_code == 204
assert (await client.get(EVO_BASE)).json()["total"] == 0
async def test_deletes_evolution(self, admin_client: AsyncClient, evolution: dict):
assert (await admin_client.delete(f"{EVO_BASE}/{evolution['id']}")).status_code == 204
assert (await admin_client.get(EVO_BASE)).json()["total"] == 0
async def test_not_found_returns_404(self, client: AsyncClient):
assert (await client.delete(f"{EVO_BASE}/9999")).status_code == 404
async def test_not_found_returns_404(self, admin_client: AsyncClient):
assert (await admin_client.delete(f"{EVO_BASE}/9999")).status_code == 404
# ---------------------------------------------------------------------------
@@ -467,13 +467,13 @@ class TestDeleteEvolution:
class TestRouteEncounters:
async def test_empty_list_for_route(self, client: AsyncClient, ctx: dict):
response = await client.get(f"{ROUTE_BASE}/{ctx['route_id']}/pokemon")
async def test_empty_list_for_route(self, admin_client: AsyncClient, ctx: dict):
response = await admin_client.get(f"{ROUTE_BASE}/{ctx['route_id']}/pokemon")
assert response.status_code == 200
assert response.json() == []
async def test_creates_route_encounter(self, client: AsyncClient, ctx: dict):
response = await client.post(
async def test_creates_route_encounter(self, admin_client: AsyncClient, ctx: dict):
response = await admin_client.post(
f"{ROUTE_BASE}/{ctx['route_id']}/pokemon",
json={
"pokemonId": ctx["charmander_id"],
@@ -490,8 +490,8 @@ class TestRouteEncounters:
assert data["encounterRate"] == 10
assert data["pokemon"]["name"] == "charmander"
async def test_invalid_route_returns_404(self, client: AsyncClient, ctx: dict):
response = await client.post(
async def test_invalid_route_returns_404(self, admin_client: AsyncClient, ctx: dict):
response = await admin_client.post(
f"{ROUTE_BASE}/9999/pokemon",
json={
"pokemonId": ctx["charmander_id"],
@@ -504,8 +504,8 @@ class TestRouteEncounters:
)
assert response.status_code == 404
async def test_invalid_pokemon_returns_404(self, client: AsyncClient, ctx: dict):
response = await client.post(
async def test_invalid_pokemon_returns_404(self, admin_client: AsyncClient, ctx: dict):
response = await admin_client.post(
f"{ROUTE_BASE}/{ctx['route_id']}/pokemon",
json={
"pokemonId": 9999,
@@ -518,8 +518,8 @@ class TestRouteEncounters:
)
assert response.status_code == 404
async def test_updates_route_encounter(self, client: AsyncClient, ctx: dict):
r = await client.post(
async def test_updates_route_encounter(self, admin_client: AsyncClient, ctx: dict):
r = await admin_client.post(
f"{ROUTE_BASE}/{ctx['route_id']}/pokemon",
json={
"pokemonId": ctx["charmander_id"],
@@ -531,23 +531,23 @@ class TestRouteEncounters:
},
)
enc = r.json()
response = await client.put(
response = await admin_client.put(
f"{ROUTE_BASE}/{ctx['route_id']}/pokemon/{enc['id']}",
json={"encounterRate": 25},
)
assert response.status_code == 200
assert response.json()["encounterRate"] == 25
async def test_update_not_found_returns_404(self, client: AsyncClient, ctx: dict):
async def test_update_not_found_returns_404(self, admin_client: AsyncClient, ctx: dict):
assert (
await client.put(
await admin_client.put(
f"{ROUTE_BASE}/{ctx['route_id']}/pokemon/9999",
json={"encounterRate": 5},
)
).status_code == 404
async def test_deletes_route_encounter(self, client: AsyncClient, ctx: dict):
r = await client.post(
async def test_deletes_route_encounter(self, admin_client: AsyncClient, ctx: dict):
r = await admin_client.post(
f"{ROUTE_BASE}/{ctx['route_id']}/pokemon",
json={
"pokemonId": ctx["charmander_id"],
@@ -560,13 +560,13 @@ class TestRouteEncounters:
)
enc = r.json()
assert (
await client.delete(f"{ROUTE_BASE}/{ctx['route_id']}/pokemon/{enc['id']}")
await admin_client.delete(f"{ROUTE_BASE}/{ctx['route_id']}/pokemon/{enc['id']}")
).status_code == 204
assert (
await client.get(f"{ROUTE_BASE}/{ctx['route_id']}/pokemon")
await admin_client.get(f"{ROUTE_BASE}/{ctx['route_id']}/pokemon")
).json() == []
async def test_delete_not_found_returns_404(self, client: AsyncClient, ctx: dict):
async def test_delete_not_found_returns_404(self, admin_client: AsyncClient, ctx: dict):
assert (
await client.delete(f"{ROUTE_BASE}/{ctx['route_id']}/pokemon/9999")
await admin_client.delete(f"{ROUTE_BASE}/{ctx['route_id']}/pokemon/9999")
).status_code == 404