This commit is contained in:
2026-02-11 23:54:42 +01:00 Unverified
commit ace6f5a022
47 changed files with 5315 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
# backend\tests\__init__.py
+97
View File
@@ -0,0 +1,97 @@
# backend/tests/conftest.py
from typing import AsyncGenerator, Generator
import pytest
from app.core.auth import get_current_user, get_optional_user
from app.database import Base, get_db
# Import your app and models
from app.main import app
from fastapi import Request
from httpx import ASGITransport, AsyncClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
# --- DATABASE SETUP ---
# Use in-memory SQLite.
# StaticPool is CRITICAL for in-memory SQLite with async tests to share connection.
SQLALCHEMY_DATABASE_URL = "sqlite:///"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@pytest.fixture(scope="session")
def prepare_db():
Base.metadata.create_all(bind=engine)
yield
Base.metadata.drop_all(bind=engine)
@pytest.fixture(scope="function")
def db(prepare_db) -> Generator[Session, None, None]:
connection = engine.connect()
transaction = connection.begin()
session = TestingSessionLocal(bind=connection)
yield session
session.close()
transaction.rollback()
connection.close()
@pytest.fixture(scope="function")
async def client(db: Session) -> AsyncGenerator[AsyncClient, None]:
def override_get_db():
try:
yield db
finally:
pass
# Strict Auth: Always requires a token (simulated by header presence)
def override_get_current_user(request: Request):
if "Authorization" not in request.headers:
# Let FastAPI raise the 401 naturally if header is missing
raise pytest.skip("Auth header missing in strict auth test")
return "test_admin"
# Optional Auth: Returns Admin IF header exists, else None
def override_get_optional_user(request: Request):
if "Authorization" in request.headers:
return "test_admin"
return None
app.dependency_overrides[get_db] = override_get_db
app.dependency_overrides[get_current_user] = override_get_current_user
app.dependency_overrides[get_optional_user] = override_get_optional_user
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as c:
yield c
app.dependency_overrides.clear()
# --- HELPER FIXTURES ---
@pytest.fixture
def auth_headers(client):
return {"Authorization": "Bearer test_token"}
@pytest.fixture
def valid_tournament_payload():
return {
"name": "Test Tournament",
"code": "1234",
"type": "Double",
"timestamp": "2024-01-01T10:00:00",
"duration": 15,
"teams": ["Team A", "Team B", "Team C", "Team D"],
"courts": ["Court 1", "Court 2"],
}
+94
View File
@@ -0,0 +1,94 @@
# backend/tests/test_scoring.py
import pytest
from httpx import AsyncClient
pytestmark = pytest.mark.anyio
async def test_scoring_flow(
client: AsyncClient, auth_headers, valid_tournament_payload
):
# 1. Create Tournament
res = await client.post(
"/tournaments", json=valid_tournament_payload, headers=auth_headers
)
t_id = res.json()["id"]
t_code = valid_tournament_payload["code"]
# 2. Get Matches to find a Round 1 match
matches_res = await client.get(f"/tournaments/{t_id}/matches")
matches = matches_res.json()
# Find a match that has real players (not BYE)
# In double elim, Round 1 matches usually have seeds.
target_match = next(m for m in matches if m["p1"] and m["p2"])
match_id = target_match["id"]
next_match_id = target_match["next_win"] # Note: using alias from schema
# 3. Report Score WITHOUT Auth Header (Public user with Code)
score_payload = {
"id": match_id,
"code": t_code,
"sets": [{"p1": 21, "p2": 19}, {"p1": 21, "p2": 15}], # P1 Wins
}
report_res = await client.post(
f"/tournaments/{t_id}/matches/{match_id}/score", json=score_payload
)
assert report_res.status_code == 200
# 4. Verify Winner Advanced
# Fetch the *Next* match
next_match_res = await client.get(f"/tournaments/{t_id}/matches/{next_match_id}")
next_match = next_match_res.json()
# Assert P1 from previous match is now in the next match
# Note: We check if the name matches the winner
winner_name = target_match["p1"]
assert (next_match["p1"] == winner_name) or (next_match["p2"] == winner_name)
# 5. Test Invalid Code
bad_payload = score_payload.copy()
bad_payload["code"] = "WRONG"
bad_res = await client.post(
f"/tournaments/{t_id}/matches/{match_id}/score", json=bad_payload
)
assert bad_res.status_code == 403
async def test_clear_score(client: AsyncClient, auth_headers, valid_tournament_payload):
# Setup: Create & Score
res = await client.post(
"/tournaments", json=valid_tournament_payload, headers=auth_headers
)
t_id = res.json()["id"]
matches = (await client.get(f"/tournaments/{t_id}/matches")).json()
target = next(m for m in matches if m["p1"] and m["p2"])
score_payload = {"id": target["id"], "code": "1234", "sets": [{"p1": 25, "p2": 0}]}
await client.post(
f"/tournaments/{t_id}/matches/{target['id']}/score", json=score_payload
)
# Verify Finished
check_res = await client.get(f"/tournaments/{t_id}/matches/{target['id']}")
assert check_res.json()["status"] == "Finished"
# Action: Clear Score
clear_res = await client.delete(
f"/tournaments/{t_id}/matches/{target['id']}/score", headers=auth_headers
)
assert clear_res.status_code == 200
# Verify Reset
final_res = await client.get(f"/tournaments/{t_id}/matches/{target['id']}")
data = final_res.json()
# 1. We already fixed this to expect 'Scheduled'
assert data["status"] == "Scheduled"
# 2. FIX: Check 'winner_side' instead of 'winner'
# Use the Enum value "none"
assert data["winner_side"] == "none"
assert len(data["sets"]) == 0
+66
View File
@@ -0,0 +1,66 @@
# backend/tests/test_structure.py
import pytest
from httpx import AsyncClient
pytestmark = pytest.mark.anyio
async def test_manage_teams(
client: AsyncClient, auth_headers, valid_tournament_payload
):
# Setup
res = await client.post(
"/tournaments", json=valid_tournament_payload, headers=auth_headers
)
t_id = res.json()["id"]
# 1. Add a Team via POST
new_team = {"name": "Team E"}
post_res = await client.post(
f"/tournaments/{t_id}/teams", json=new_team, headers=auth_headers
)
assert post_res.status_code == 200
assert post_res.json()["name"] == "Team E"
# 2. Verify Bracket Regenerated (Match count should likely change or re-seed)
matches_res = await client.get(f"/tournaments/{t_id}/matches")
# With 4 teams -> ~6 matches. With 5 teams -> ~8-10 matches in Double Elim.
assert len(matches_res.json()) > 0
# 3. Bulk Update via PATCH (Replace all teams)
new_team_list = ["Team X", "Team Y"]
patch_res = await client.patch(
f"/tournaments/{t_id}/teams", json=new_team_list, headers=auth_headers
)
assert patch_res.status_code == 200
data = patch_res.json()
assert len(data) == 2
assert data[0]["name"] in ["Team X", "Team Y"]
async def test_manage_courts(
client: AsyncClient, auth_headers, valid_tournament_payload
):
res = await client.post(
"/tournaments", json=valid_tournament_payload, headers=auth_headers
)
t_id = res.json()["id"]
# Get initial courts
courts_res = await client.get(f"/tournaments/{t_id}/courts")
initial_courts = courts_res.json()
assert len(initial_courts) == 2
# Delete a court
court_id = initial_courts[0]["id"]
del_res = await client.delete(
f"/tournaments/{t_id}/courts/{court_id}", headers=auth_headers
)
assert del_res.status_code == 200
# Create a court
create_res = await client.post(
f"/tournaments/{t_id}/courts", json={"name": "New Court"}, headers=auth_headers
)
assert create_res.status_code == 200
assert create_res.json()["name"] == "New Court"
+87
View File
@@ -0,0 +1,87 @@
# backend/tests/test_tournaments.py
import pytest
from httpx import AsyncClient
# Mark all tests in this file as async
pytestmark = pytest.mark.anyio
async def test_create_tournament(
client: AsyncClient, auth_headers, valid_tournament_payload
):
response = await client.post(
"/tournaments", json=valid_tournament_payload, headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["name"] == "Test Tournament"
assert data["team_count"] == 4
assert data["court_count"] == 2
assert "id" in data
async def test_list_tournaments(
client: AsyncClient, auth_headers, valid_tournament_payload
):
# Create one first
await client.post(
"/tournaments", json=valid_tournament_payload, headers=auth_headers
)
response = await client.get("/tournaments")
assert response.status_code == 200
data = response.json()
assert len(data) >= 1
assert data[0]["name"] == "Test Tournament"
async def test_get_tournament_detail(
client: AsyncClient, auth_headers, valid_tournament_payload
):
create_res = await client.post(
"/tournaments", json=valid_tournament_payload, headers=auth_headers
)
t_id = create_res.json()["id"]
response = await client.get(f"/tournaments/{t_id}")
assert response.status_code == 200
data = response.json()
# Check deeply nested fields
assert len(data["matches"]) > 0 # Logic should have generated matches
assert len(data["teams"]) == 4
async def test_update_settings(
client: AsyncClient, auth_headers, valid_tournament_payload
):
create_res = await client.post(
"/tournaments", json=valid_tournament_payload, headers=auth_headers
)
t_id = create_res.json()["id"]
update_payload = {"name": "Updated Name", "code": "9999"}
response = await client.patch(
f"/tournaments/{t_id}", json=update_payload, headers=auth_headers
)
# This will now succeed because we updated the response_model!
assert response.status_code == 200
data = response.json()
assert data["name"] == "Updated Name"
assert data["code"] == "9999"
async def test_delete_tournament(
client: AsyncClient, auth_headers, valid_tournament_payload
):
create_res = await client.post(
"/tournaments", json=valid_tournament_payload, headers=auth_headers
)
t_id = create_res.json()["id"]
del_res = await client.delete(f"/tournaments/{t_id}", headers=auth_headers)
assert del_res.status_code == 200
# Verify it's gone
get_res = await client.get(f"/tournaments/{t_id}")
assert get_res.status_code == 404