Files
bracket/backend/tests/integration_tests/api/tournaments_test.py
copilot-swe-agent[bot] 4835aa7311 Replace databases dependency with asyncpg, remove SQLAlchemy except schema.py
- Rewrite database.py with asyncpg pool-based DatabasePool class
- Support named params (:param) → positional ($N) conversion
- Transaction context manager using contextvars for connection reuse
- Replace all SQLAlchemy query building with raw SQL in routes and sql modules
- Update utils/db.py to use raw SQL strings instead of SQLAlchemy Select
- Update utils/db_init.py to use local engine creation for DDL only
- Update all test files to use string table names and raw SQL
- Remove databases and sqlalchemy-stubs from dependencies
- Keep sqlalchemy only for schema.py definitions and alembic migrations
2026-07-28 19:00:52 +00:00

267 lines
9.6 KiB
Python

import aiofiles
import aiofiles.os
import aiohttp
import pytest
from bracket.database import database
from bracket.logic.tournaments import sql_delete_tournament_completely
from bracket.models.db.tournament import Tournament, TournamentStatus
from bracket.sql.tournaments import sql_delete_tournament, sql_get_tournament_by_endpoint_name
from bracket.utils.db import fetch_one_parsed_certain
from bracket.utils.dummy_records import DUMMY_MOCK_TIME, DUMMY_TOURNAMENT
from bracket.utils.http import HTTPMethod
from bracket.utils.types import assert_some
from tests.integration_tests.api.shared import (
SUCCESS_RESPONSE,
send_auth_request,
send_request,
send_tournament_request,
)
from tests.integration_tests.models import AuthContext
from tests.integration_tests.sql import inserted_tournament
@pytest.mark.asyncio(loop_scope="session")
async def test_tournaments_endpoint(
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
) -> None:
assert await send_auth_request(HTTPMethod.GET, "tournaments", auth_context, {}) == {
"data": [
{
"id": auth_context.tournament.id,
"club_id": auth_context.club.id,
"created": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"),
"start_time": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"),
"name": "Some Cool Tournament",
"logo_path": None,
"dashboard_public": True,
"dashboard_endpoint": "endpoint-test",
"players_can_be_in_multiple_teams": True,
"auto_assign_courts": True,
"duration_minutes": 10,
"margin_minutes": 5,
"status": "OPEN",
}
],
}
@pytest.mark.asyncio(loop_scope="session")
async def test_tournament_endpoint(
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
) -> None:
assert await send_auth_request(
HTTPMethod.GET, f"tournaments/{auth_context.tournament.id}", auth_context, {}
) == {
"data": {
"id": auth_context.tournament.id,
"club_id": auth_context.club.id,
"created": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"),
"start_time": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"),
"logo_path": None,
"name": "Some Cool Tournament",
"dashboard_public": True,
"dashboard_endpoint": "endpoint-test",
"players_can_be_in_multiple_teams": True,
"auto_assign_courts": True,
"duration_minutes": 10,
"margin_minutes": 5,
"status": "OPEN",
},
}
@pytest.mark.asyncio(loop_scope="session")
async def test_create_tournament(
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
) -> None:
dashboard_endpoint = "some-new-endpoint"
body = {
"name": "Some new name",
"start_time": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"),
"club_id": auth_context.club.id,
"dashboard_public": True,
"dashboard_endpoint": dashboard_endpoint,
"players_can_be_in_multiple_teams": True,
"auto_assign_courts": True,
"duration_minutes": 12,
"margin_minutes": 3,
}
assert (
await send_auth_request(HTTPMethod.POST, "tournaments", auth_context, json=body)
== SUCCESS_RESPONSE
)
# Cleanup
tournament = assert_some(await sql_get_tournament_by_endpoint_name(dashboard_endpoint))
await sql_delete_tournament_completely(tournament.id)
@pytest.mark.asyncio(loop_scope="session")
async def test_create_tournament_duplicate_dashboard_endpoint(
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
) -> None:
body = {
"name": "Some new name",
"start_time": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"),
"club_id": auth_context.club.id,
"dashboard_public": True,
"dashboard_endpoint": "endpoint-test",
"players_can_be_in_multiple_teams": True,
"auto_assign_courts": True,
"duration_minutes": 12,
"margin_minutes": 3,
}
assert await send_auth_request(HTTPMethod.POST, "tournaments", auth_context, json=body) == {
"detail": "This dashboard link is already taken"
}
@pytest.mark.asyncio(loop_scope="session")
async def test_update_tournament(
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
) -> None:
body = {
"name": "Some new name",
"start_time": DUMMY_MOCK_TIME.isoformat().replace("+00:00", "Z"),
"dashboard_public": False,
"players_can_be_in_multiple_teams": True,
"auto_assign_courts": True,
"duration_minutes": 12,
"margin_minutes": 3,
}
assert (
await send_tournament_request(HTTPMethod.PUT, "", auth_context, json=body)
== SUCCESS_RESPONSE
)
updated_tournament = await fetch_one_parsed_certain(
database,
Tournament,
"SELECT * FROM tournaments WHERE id = :id",
{"id": auth_context.tournament.id},
)
assert updated_tournament.name == body["name"]
assert updated_tournament.dashboard_public == body["dashboard_public"]
@pytest.mark.asyncio(loop_scope="session")
async def test_archive_and_unarchive_tournament(
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
) -> None:
query = "SELECT * FROM tournaments WHERE id = :id"
values = {"id": auth_context.tournament.id}
body = {"status": "ARCHIVED"}
assert (
await send_tournament_request(HTTPMethod.POST, "change-status", auth_context, json=body)
== SUCCESS_RESPONSE
)
updated_tournament = await fetch_one_parsed_certain(database, Tournament, query, values)
assert updated_tournament.status is TournamentStatus.ARCHIVED
assert updated_tournament.dashboard_public is False
# Archiving twice is not allowed
assert await send_tournament_request(
HTTPMethod.POST, "change-status", auth_context, json=body
) == {"detail": "Tournament already has the requested status"}
# Unarchive the tournament
body = {"status": "OPEN"}
assert (
await send_tournament_request(HTTPMethod.POST, "change-status", auth_context, json=body)
== SUCCESS_RESPONSE
)
updated_tournament = await fetch_one_parsed_certain(database, Tournament, query, values)
assert updated_tournament.status is TournamentStatus.OPEN
assert updated_tournament.dashboard_public is False
@pytest.mark.asyncio(loop_scope="session")
async def test_delete_tournament(
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
) -> None:
async with inserted_tournament(
DUMMY_TOURNAMENT.model_copy(
update={"club_id": auth_context.club.id, "dashboard_endpoint": None}
)
) as tournament_inserted:
assert (
await send_tournament_request(
HTTPMethod.DELETE,
"",
auth_context.model_copy(update={"tournament": tournament_inserted}),
)
== SUCCESS_RESPONSE
)
await sql_delete_tournament(tournament_inserted.id)
@pytest.mark.asyncio(loop_scope="session")
async def test_tournament_upload_and_remove_logo(
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
) -> None:
test_file_path = "tests/integration_tests/assets/test_logo.png"
data = aiohttp.FormData()
data.add_field(
"file",
open(test_file_path, "rb"), # pylint: disable=consider-using-with
filename="test_logo.png",
content_type="image/png",
)
response = await send_tournament_request(
method=HTTPMethod.POST,
endpoint="logo",
auth_context=auth_context,
body=data,
)
assert response.get("data", {}).get("logo_path"), f"Response: {response}"
assert await aiofiles.os.path.exists(f"static/tournament-logos/{response['data']['logo_path']}")
response = await send_tournament_request(
method=HTTPMethod.POST, endpoint="logo", auth_context=auth_context, body=aiohttp.FormData()
)
assert response["data"]["logo_path"] is None, f"Response: {response}"
assert not await aiofiles.os.path.exists(
f"static/tournament-logos/{response['data']['logo_path']}"
)
UNAUTHORIZED_RESPONSE = {
"detail": "Could not validate credentials or page is not publicly available"
}
@pytest.mark.asyncio(loop_scope="session")
async def test_non_public_tournament_endpoints_blocked_for_unauthenticated_users(
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
) -> None:
"""
Unauthenticated requests to a tournament with dashboard_public=False must be rejected.
This tests the fix for GHSA-9mjc-6fp2-hm9v.
"""
async with inserted_tournament(
DUMMY_TOURNAMENT.model_copy(
update={
"club_id": auth_context.club.id,
"dashboard_public": False,
"dashboard_endpoint": "non-public-endpoint",
}
)
) as private_tournament:
tournament_id = private_tournament.id
for endpoint in (
f"tournaments/{tournament_id}",
f"tournaments/{tournament_id}/courts",
f"tournaments/{tournament_id}/teams",
f"tournaments/{tournament_id}/rankings",
f"tournaments/{tournament_id}/stages?no_draft_rounds=true",
):
response = await send_request(HTTPMethod.GET, endpoint)
assert response == UNAUTHORIZED_RESPONSE, (
f"Expected 401 for unauthenticated access to non-public endpoint {endpoint!r}, "
f"got: {response}"
)