mirror of
https://github.com/evroon/bracket.git
synced 2026-07-31 18:26:23 -04:00
- 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
267 lines
9.6 KiB
Python
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}"
|
|
)
|