CI/CD: Gitea Actions workflow med tests
CI/CD / test (push) Failing after 3m12s
CI/CD / build (push) Has been skipped

This commit is contained in:
2026-07-11 02:10:27 +02:00
parent 995921b25b
commit 07a6781c5e
7 changed files with 346 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Tests for pokedexter API."""
+101
View File
@@ -0,0 +1,101 @@
"""Test fixtures — async database and HTTP client."""
import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from httpx import ASGITransport, AsyncClient
from app.database import Base, get_db
from app.main import app
from app.auth import hash_password
from app.models import User
import os
DATABASE_URL = os.getenv(
"DATABASE_URL",
"postgresql+asyncpg://pokedexter:pokedexter_secret@localhost/pokedexter",
)
@pytest_asyncio.fixture(scope="session")
async def engine():
"""Create a fresh database for testing."""
e = create_async_engine(DATABASE_URL, echo=False)
async with e.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
yield e
async with e.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await e.dispose()
@pytest_asyncio.fixture
async def session(engine):
"""Per-test database session."""
maker = async_sessionmaker(engine, expire_on_commit=False)
async with maker() as s:
yield s
@pytest_asyncio.fixture
async def client(session):
"""HTTP client with injected test DB session."""
async def override_get_db():
yield session
app.dependency_overrides[get_db] = override_get_db
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
app.dependency_overrides.clear()
@pytest_asyncio.fixture
async def admin_user(session):
"""Insert admin user into test DB."""
u = User(
username="admin",
password_hash=hash_password("admin123"),
role="admin",
)
session.add(u)
await session.commit()
return u
@pytest_asyncio.fixture
async def normal_user(session):
"""Insert normal user into test DB."""
u = User(
username="testuser",
password_hash=hash_password("test123"),
role="user",
)
session.add(u)
await session.commit()
return u
@pytest_asyncio.fixture
async def admin_token(client, admin_user):
"""Log in as admin and return the JWT token."""
resp = await client.post("/api/login", json={
"username": "admin",
"password": "admin123",
})
assert resp.status_code == 200
return resp.cookies.get("token")
@pytest_asyncio.fixture
async def user_token(client, normal_user):
"""Log in as normal user and return the JWT token."""
resp = await client.post("/api/login", json={
"username": "testuser",
"password": "test123",
})
assert resp.status_code == 200
return resp.cookies.get("token")
+39
View File
@@ -0,0 +1,39 @@
"""Tests for admin endpoints."""
import pytest
@pytest.mark.asyncio
class TestAdmin:
"""Admin routes require admin role."""
async def test_dashboard_requires_admin(self, client, user_token):
client.cookies.set("token", user_token)
resp = await client.get("/admin/", follow_redirects=False)
assert resp.status_code == 401
async def test_dashboard_as_admin(self, client, admin_token):
client.cookies.set("token", admin_token)
resp = await client.get("/admin/")
assert resp.status_code == 200
assert "text/html" in resp.headers["content-type"]
async def test_series_admin_requires_admin(self, client, user_token):
client.cookies.set("token", user_token)
resp = await client.get("/admin/series", follow_redirects=False)
assert resp.status_code == 401
async def test_series_admin_as_admin(self, client, admin_token):
client.cookies.set("token", admin_token)
resp = await client.get("/admin/series")
assert resp.status_code == 200
async def test_sets_admin_as_admin(self, client, admin_token):
client.cookies.set("token", admin_token)
resp = await client.get("/admin/sets")
assert resp.status_code == 200
async def test_cards_admin_as_admin(self, client, admin_token):
client.cookies.set("token", admin_token)
resp = await client.get("/admin/cards")
assert resp.status_code == 200
+52
View File
@@ -0,0 +1,52 @@
"""Tests for authentication endpoints."""
import pytest
@pytest.mark.asyncio
class TestAuth:
async def test_login_page(self, client):
resp = await client.get("/login")
assert resp.status_code == 200
assert "text/html" in resp.headers["content-type"]
async def test_login_success(self, client, admin_user):
resp = await client.post("/api/login", json={
"username": "admin",
"password": "admin123",
})
assert resp.status_code == 200
data = resp.json()
assert data["ok"] is True
assert "token" in resp.cookies
async def test_login_wrong_password(self, client, admin_user):
resp = await client.post("/api/login", json={
"username": "admin",
"password": "forkert",
})
assert resp.status_code == 401
async def test_login_unknown_user(self, client):
resp = await client.post("/api/login", json={
"username": "findes_ikke",
"password": "test123",
})
assert resp.status_code == 401
async def test_logout(self, client, admin_token):
client.cookies.set("token", admin_token)
resp = await client.post("/api/logout")
assert resp.status_code == 200
# Cookie should be deleted
set_cookie = resp.headers.get("set-cookie", "")
assert "token=;" in set_cookie or "Max-Age=0" in set_cookie
async def test_index_redirect_when_not_logged_in(self, client):
resp = await client.get("/", follow_redirects=False)
assert resp.status_code == 303 # Redirect to /login
async def test_index_when_logged_in(self, client, admin_token):
client.cookies.set("token", admin_token)
resp = await client.get("/")
assert resp.status_code == 200
+74
View File
@@ -0,0 +1,74 @@
"""Tests for card/series/set endpoints."""
import pytest
@pytest.mark.asyncio
class TestCardsAPI:
"""Tests for card browsning (no auth required for browse endpoints)."""
async def test_series_list(self, client, admin_token):
client.cookies.set("token", admin_token)
resp = await client.get("/series")
assert resp.status_code == 200
assert "text/html" in resp.headers["content-type"]
async def test_sets_list(self, client, admin_token):
client.cookies.set("token", admin_token)
resp = await client.get("/sets")
assert resp.status_code == 200
async def test_search_page(self, client, admin_token):
client.cookies.set("token", admin_token)
resp = await client.get("/search")
assert resp.status_code == 200
# ── JSON API ──────────────────────────────────────────────
async def test_api_series(self, client):
resp = await client.get("/api/series")
assert resp.status_code == 200
assert isinstance(resp.json(), list)
async def test_api_sets(self, client):
resp = await client.get("/api/sets")
assert resp.status_code == 200
assert isinstance(resp.json(), list)
async def test_api_series_sets_not_found(self, client):
resp = await client.get("/api/series/FinnesIkke/sets")
assert resp.status_code == 200
data = resp.json()
assert "error" in data
async def test_api_sets_cards_not_found(self, client):
resp = await client.get("/api/sets/XXX/cards")
assert resp.status_code == 200
assert resp.json() == []
async def test_search_empty(self, client):
resp = await client.get("/api/search")
assert resp.status_code == 200
assert resp.json() == []
async def test_search_no_match(self, client):
resp = await client.get("/api/search?q=zzz_nonexistent_xyz")
assert resp.status_code == 200
assert resp.json() == []
# ── 404 tests ─────────────────────────────────────────────
async def test_serie_detail_404(self, client, admin_token):
client.cookies.set("token", admin_token)
resp = await client.get("/series/FinnesIkke")
assert resp.status_code == 404
async def test_set_detail_404(self, client, admin_token):
client.cookies.set("token", admin_token)
resp = await client.get("/sets/ZZZ")
assert resp.status_code == 404
async def test_card_detail_404(self, client, admin_token):
client.cookies.set("token", admin_token)
resp = await client.get("/card/ZZZ/999")
assert resp.status_code == 404