mirror of
https://github.com/evroon/bracket.git
synced 2026-04-27 10:47:05 -04:00
Add test coverage (#61)
This commit is contained in:
@@ -23,6 +23,7 @@ types-passlib = "*"
|
||||
pyjwt = "2.6.0"
|
||||
click = "8.1.3"
|
||||
python-multipart = "*"
|
||||
parameterized = "*"
|
||||
|
||||
[dev-packages]
|
||||
mypy = "0.991"
|
||||
|
||||
@@ -6,6 +6,7 @@ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from heliclockter import datetime_utc, timedelta
|
||||
from jwt import DecodeError, ExpiredSignatureError
|
||||
from pydantic import BaseModel
|
||||
from starlette.requests import Request
|
||||
|
||||
from bracket.config import config
|
||||
from bracket.database import database
|
||||
@@ -38,10 +39,6 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
async def get_user(email: str) -> UserInDB | None:
|
||||
return await fetch_one_parsed(database, UserInDB, users.select().where(users.c.email == email))
|
||||
|
||||
@@ -55,13 +52,9 @@ async def authenticate_user(email: str, password: str) -> UserInDB | None:
|
||||
return user
|
||||
|
||||
|
||||
def create_access_token(data: dict[str, Any], expires_delta: timedelta | None = None) -> str:
|
||||
def create_access_token(data: dict[str, Any], expires_delta: timedelta) -> str:
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime_utc.now() + expires_delta
|
||||
else:
|
||||
expire = datetime_utc.now() + timedelta(minutes=15)
|
||||
|
||||
expire = datetime_utc.now() + expires_delta
|
||||
to_encode.update({"exp": expire})
|
||||
return jwt.encode(to_encode, config.jwt_secret, algorithm=ALGORITHM)
|
||||
|
||||
@@ -111,13 +104,17 @@ async def user_authenticated_for_tournament(
|
||||
|
||||
|
||||
async def user_authenticated_or_public_dashboard(
|
||||
tournament_id: int, token: str = Depends(oauth2_scheme)
|
||||
tournament_id: int, request: Request
|
||||
) -> UserPublic | None:
|
||||
user = await check_jwt_and_get_user(token)
|
||||
if user is not None and await get_user_access_to_tournament(
|
||||
tournament_id, assert_some(user.id)
|
||||
):
|
||||
return user
|
||||
try:
|
||||
token: str = assert_some(await oauth2_scheme(request))
|
||||
user = await check_jwt_and_get_user(token)
|
||||
if user is not None and await get_user_access_to_tournament(
|
||||
tournament_id, assert_some(user.id)
|
||||
):
|
||||
return user
|
||||
except HTTPException:
|
||||
pass
|
||||
|
||||
tournaments_fetched = await fetch_all_parsed(
|
||||
database, Tournament, tournaments.select().where(tournaments.c.id == tournament_id)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from heliclockter import datetime_utc
|
||||
from starlette import status
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.models.db.tournament import (
|
||||
@@ -13,6 +14,8 @@ from bracket.routes.auth import user_authenticated, user_authenticated_for_tourn
|
||||
from bracket.routes.models import SuccessResponse, TournamentsResponse
|
||||
from bracket.schema import tournaments
|
||||
from bracket.utils.db import fetch_all_parsed
|
||||
from bracket.utils.sql import get_user_access_to_club
|
||||
from bracket.utils.types import assert_some
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -51,8 +54,18 @@ async def delete_tournament(
|
||||
|
||||
@router.post("/tournaments", response_model=SuccessResponse)
|
||||
async def create_tournament(
|
||||
tournament_to_insert: TournamentBody, _: UserPublic = Depends(user_authenticated)
|
||||
tournament_to_insert: TournamentBody, user: UserPublic = Depends(user_authenticated)
|
||||
) -> SuccessResponse:
|
||||
has_access_to_club = await get_user_access_to_club(
|
||||
tournament_to_insert.club_id, assert_some(user.id)
|
||||
)
|
||||
if not has_access_to_club:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Club ID is invalid",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
await database.execute(
|
||||
query=tournaments.insert(),
|
||||
values=TournamentToInsert(
|
||||
|
||||
@@ -11,7 +11,7 @@ async def get_rounds_with_matches(tournament_id: int) -> RoundsWithMatchesRespon
|
||||
WITH teams_with_players AS (
|
||||
SELECT DISTINCT ON (teams.id)
|
||||
teams.*,
|
||||
to_json(array_agg(p)) as players
|
||||
to_json(array_remove(array_agg(p), NULL)) as players
|
||||
FROM teams
|
||||
LEFT JOIN players p on p.team_id = teams.id
|
||||
WHERE teams.tournament_id = :tournament_id
|
||||
@@ -33,6 +33,7 @@ async def get_rounds_with_matches(tournament_id: int) -> RoundsWithMatchesRespon
|
||||
GROUP BY rounds.id
|
||||
'''
|
||||
result = await database.fetch_all(query=query, values={'tournament_id': tournament_id})
|
||||
print([x._mapping for x in result])
|
||||
return RoundsWithMatchesResponse.parse_obj(
|
||||
{'data': [RoundWithMatches.parse_obj(x._mapping) for x in result]}
|
||||
)
|
||||
@@ -75,4 +76,14 @@ async def get_user_access_to_tournament(tournament_id: int, user_id: int) -> boo
|
||||
WHERE user_id = :user_id
|
||||
'''
|
||||
result = await database.fetch_all(query=query, values={'user_id': user_id})
|
||||
return tournament_id in [tournament.id for tournament in result] # type: ignore[attr-defined]
|
||||
return tournament_id in {tournament.id for tournament in result} # type: ignore[attr-defined]
|
||||
|
||||
|
||||
async def get_user_access_to_club(club_id: int, user_id: int) -> bool:
|
||||
query = f'''
|
||||
SELECT club_id
|
||||
FROM users_x_clubs
|
||||
WHERE user_id = :user_id
|
||||
'''
|
||||
result = await database.fetch_all(query=query, values={'user_id': user_id})
|
||||
return club_id in {club.club_id for club in result} # type: ignore[attr-defined]
|
||||
|
||||
@@ -5,11 +5,13 @@ from unittest.mock import Mock, patch
|
||||
import jwt
|
||||
|
||||
from bracket.config import config
|
||||
from bracket.utils.dummy_records import DUMMY_CLUB, DUMMY_TOURNAMENT
|
||||
from bracket.utils.http import HTTPMethod
|
||||
from bracket.utils.types import JsonDict
|
||||
from tests.integration_tests.api.shared import send_request
|
||||
from tests.integration_tests.api.shared import send_auth_request, send_request
|
||||
from tests.integration_tests.mocks import MOCK_NOW, MOCK_USER, get_mock_token
|
||||
from tests.integration_tests.sql import inserted_user
|
||||
from tests.integration_tests.models import AuthContext
|
||||
from tests.integration_tests.sql import inserted_club, inserted_tournament, inserted_user
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -65,3 +67,18 @@ async def test_invalid_token(startup_and_shutdown_uvicorn_server: None) -> None:
|
||||
|
||||
response = JsonDict(await send_request(HTTPMethod.GET, 'users/me', {}, None, headers))
|
||||
assert response == {'detail': 'Could not validate credentials'}
|
||||
|
||||
|
||||
async def test_not_authenticated_for_tournament(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
async with inserted_club(DUMMY_CLUB) as club_inserted:
|
||||
async with inserted_tournament(
|
||||
DUMMY_TOURNAMENT.copy(update={'club_id': club_inserted.id})
|
||||
) as tournament_inserted:
|
||||
response = JsonDict(
|
||||
await send_auth_request(
|
||||
HTTPMethod.GET, f'tournaments/{tournament_inserted.id}/teams', auth_context
|
||||
)
|
||||
)
|
||||
assert response == {'detail': 'Could not validate credentials'}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
from bracket.database import database
|
||||
from bracket.models.db.match import Match
|
||||
from bracket.schema import matches
|
||||
from bracket.utils.db import fetch_one_parsed_certain
|
||||
from bracket.utils.dummy_records import (
|
||||
DUMMY_MATCH1,
|
||||
DUMMY_PLAYER1,
|
||||
DUMMY_PLAYER2,
|
||||
DUMMY_ROUND1,
|
||||
@@ -6,9 +11,96 @@ from bracket.utils.dummy_records import (
|
||||
DUMMY_TEAM2,
|
||||
)
|
||||
from bracket.utils.http import HTTPMethod
|
||||
from tests.integration_tests.api.shared import send_tournament_request
|
||||
from tests.integration_tests.api.shared import SUCCESS_RESPONSE, send_tournament_request
|
||||
from tests.integration_tests.models import AuthContext
|
||||
from tests.integration_tests.sql import inserted_player, inserted_round, inserted_team
|
||||
from tests.integration_tests.sql import (
|
||||
assert_row_count_and_clear,
|
||||
inserted_match,
|
||||
inserted_player,
|
||||
inserted_round,
|
||||
inserted_team,
|
||||
)
|
||||
|
||||
|
||||
async def test_create_match(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
async with inserted_round(DUMMY_ROUND1) as round_inserted:
|
||||
async with inserted_team(DUMMY_TEAM1) as team1_inserted:
|
||||
async with inserted_team(DUMMY_TEAM2) as team2_inserted:
|
||||
body = {
|
||||
'team1_id': team1_inserted.id,
|
||||
'team2_id': team2_inserted.id,
|
||||
'round_id': round_inserted.id,
|
||||
}
|
||||
assert (
|
||||
await send_tournament_request(
|
||||
HTTPMethod.POST, 'matches', auth_context, json=body
|
||||
)
|
||||
== SUCCESS_RESPONSE
|
||||
)
|
||||
await assert_row_count_and_clear(matches, 1)
|
||||
|
||||
|
||||
async def test_delete_match(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
async with inserted_round(DUMMY_ROUND1) as round_inserted:
|
||||
async with inserted_team(DUMMY_TEAM1) as team1_inserted:
|
||||
async with inserted_team(DUMMY_TEAM2) as team2_inserted:
|
||||
async with inserted_match(
|
||||
DUMMY_MATCH1.copy(
|
||||
update={
|
||||
'round_id': round_inserted.id,
|
||||
'team1_id': team1_inserted.id,
|
||||
'team2_id': team2_inserted.id,
|
||||
}
|
||||
)
|
||||
) as match_inserted:
|
||||
assert (
|
||||
await send_tournament_request(
|
||||
HTTPMethod.DELETE, f'matches/{match_inserted.id}', auth_context, {}
|
||||
)
|
||||
== SUCCESS_RESPONSE
|
||||
)
|
||||
await assert_row_count_and_clear(matches, 0)
|
||||
|
||||
|
||||
async def test_update_match(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
async with inserted_team(DUMMY_TEAM1) as team1_inserted:
|
||||
async with inserted_team(DUMMY_TEAM2) as team2_inserted:
|
||||
async with inserted_round(DUMMY_ROUND1) as round_inserted:
|
||||
async with inserted_match(
|
||||
DUMMY_MATCH1.copy(
|
||||
update={
|
||||
'round_id': round_inserted.id,
|
||||
'team1_id': team1_inserted.id,
|
||||
'team2_id': team2_inserted.id,
|
||||
}
|
||||
)
|
||||
) as match_inserted:
|
||||
body = {'team1_score': 42, 'team2_score': 24, 'round_id': round_inserted.id}
|
||||
assert (
|
||||
await send_tournament_request(
|
||||
HTTPMethod.PATCH,
|
||||
f'matches/{match_inserted.id}',
|
||||
auth_context,
|
||||
None,
|
||||
body,
|
||||
)
|
||||
== SUCCESS_RESPONSE
|
||||
)
|
||||
patched_match = await fetch_one_parsed_certain(
|
||||
database,
|
||||
Match,
|
||||
query=matches.select().where(matches.c.id == round_inserted.id),
|
||||
)
|
||||
assert patched_match.team1_score == body['team1_score']
|
||||
assert patched_match.team2_score == body['team2_score']
|
||||
|
||||
await assert_row_count_and_clear(matches, 1)
|
||||
|
||||
|
||||
async def test_upcoming_matches_endpoint(
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
import pytest
|
||||
|
||||
from bracket.database import database
|
||||
from bracket.models.db.round import Round
|
||||
from bracket.schema import rounds
|
||||
from bracket.utils.db import fetch_one_parsed_certain
|
||||
from bracket.utils.dummy_records import DUMMY_MOCK_TIME, DUMMY_ROUND1, DUMMY_TEAM1
|
||||
from bracket.utils.http import HTTPMethod
|
||||
from tests.integration_tests.api.shared import SUCCESS_RESPONSE, send_tournament_request
|
||||
from tests.integration_tests.api.shared import (
|
||||
SUCCESS_RESPONSE,
|
||||
send_request,
|
||||
send_tournament_request,
|
||||
)
|
||||
from tests.integration_tests.models import AuthContext
|
||||
from tests.integration_tests.sql import assert_row_count_and_clear, inserted_round, inserted_team
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("with_auth",), [(True,), (False,)])
|
||||
async def test_rounds_endpoint(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext, with_auth: bool
|
||||
) -> None:
|
||||
async with inserted_team(DUMMY_TEAM1):
|
||||
async with inserted_round(DUMMY_ROUND1) as round_inserted:
|
||||
assert await send_tournament_request(HTTPMethod.GET, 'rounds', auth_context, {}) == {
|
||||
if with_auth:
|
||||
response = await send_tournament_request(HTTPMethod.GET, 'rounds', auth_context, {})
|
||||
else:
|
||||
response = await send_request(
|
||||
HTTPMethod.GET, f'tournaments/{auth_context.tournament.id}/rounds'
|
||||
)
|
||||
|
||||
assert response == {
|
||||
'data': [
|
||||
{
|
||||
'created': DUMMY_MOCK_TIME.isoformat(),
|
||||
|
||||
@@ -2,7 +2,7 @@ from bracket.database import database
|
||||
from bracket.models.db.tournament import Tournament
|
||||
from bracket.schema import tournaments
|
||||
from bracket.utils.db import fetch_one_parsed_certain
|
||||
from bracket.utils.dummy_records import DUMMY_MOCK_TIME
|
||||
from bracket.utils.dummy_records import DUMMY_MOCK_TIME, DUMMY_TOURNAMENT
|
||||
from bracket.utils.http import HTTPMethod
|
||||
from tests.integration_tests.api.shared import (
|
||||
SUCCESS_RESPONSE,
|
||||
@@ -10,7 +10,7 @@ from tests.integration_tests.api.shared import (
|
||||
send_tournament_request,
|
||||
)
|
||||
from tests.integration_tests.models import AuthContext
|
||||
from tests.integration_tests.sql import assert_row_count_and_clear
|
||||
from tests.integration_tests.sql import assert_row_count_and_clear, inserted_tournament
|
||||
|
||||
|
||||
async def test_tournaments_endpoint(
|
||||
@@ -29,12 +29,23 @@ async def test_tournaments_endpoint(
|
||||
}
|
||||
|
||||
|
||||
async def test_create_tournament(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
body = {'name': 'Some new name', 'club_id': auth_context.club.id, 'dashboard_public': False}
|
||||
assert (
|
||||
await send_auth_request(HTTPMethod.POST, 'tournaments', auth_context, json=body)
|
||||
== SUCCESS_RESPONSE
|
||||
)
|
||||
await database.execute(tournaments.delete().where(tournaments.c.name == body['name']))
|
||||
|
||||
|
||||
async def test_update_tournament(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
body = {'name': 'Some new name', 'dashboard_public': False}
|
||||
assert (
|
||||
await send_tournament_request(HTTPMethod.PATCH, '', auth_context, None, body)
|
||||
await send_tournament_request(HTTPMethod.PATCH, '', auth_context, json=body)
|
||||
== SUCCESS_RESPONSE
|
||||
)
|
||||
patched_tournament = await fetch_one_parsed_certain(
|
||||
@@ -46,3 +57,17 @@ async def test_update_tournament(
|
||||
assert patched_tournament.dashboard_public == body['dashboard_public']
|
||||
|
||||
await assert_row_count_and_clear(tournaments, 1)
|
||||
|
||||
|
||||
async def test_delete_tournament(
|
||||
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
|
||||
) -> None:
|
||||
async with inserted_tournament(DUMMY_TOURNAMENT) as tournament_inserted:
|
||||
assert (
|
||||
await send_tournament_request(
|
||||
HTTPMethod.DELETE, '', auth_context.copy(update={'tournament': tournament_inserted})
|
||||
)
|
||||
== SUCCESS_RESPONSE
|
||||
)
|
||||
|
||||
await assert_row_count_and_clear(tournaments, 0)
|
||||
|
||||
@@ -3,10 +3,12 @@ from pydantic import BaseModel
|
||||
from bracket.models.db.club import Club
|
||||
from bracket.models.db.tournament import Tournament
|
||||
from bracket.models.db.user import User
|
||||
from bracket.models.db.user_x_club import UserXClub
|
||||
|
||||
|
||||
class AuthContext(BaseModel):
|
||||
club: Club
|
||||
tournament: Tournament
|
||||
user: User
|
||||
user_x_club: UserXClub
|
||||
headers: dict[str, str]
|
||||
|
||||
@@ -96,10 +96,11 @@ async def inserted_auth_context() -> AsyncIterator[AuthContext]:
|
||||
async with inserted_tournament(DUMMY_TOURNAMENT) as tournament_inserted:
|
||||
async with inserted_user_x_club(
|
||||
UserXClub(user_id=user_inserted.id, club_id=club_inserted.id)
|
||||
):
|
||||
) as user_x_club_inserted:
|
||||
yield AuthContext(
|
||||
headers=headers,
|
||||
user=user_inserted,
|
||||
club=club_inserted,
|
||||
tournament=tournament_inserted,
|
||||
user_x_club=user_x_club_inserted,
|
||||
)
|
||||
|
||||
46
backend/tests/unit_tests/elo_test.py
Normal file
46
backend/tests/unit_tests/elo_test.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from bracket.logic.elo import PlayerStatistics, calculate_elo_per_player
|
||||
from bracket.models.db.match import MatchWithTeamDetails
|
||||
from bracket.models.db.round import RoundWithMatches
|
||||
from bracket.models.db.team import TeamWithPlayers
|
||||
from bracket.utils.dummy_records import DUMMY_MOCK_TIME, DUMMY_PLAYER1, DUMMY_PLAYER2
|
||||
|
||||
|
||||
def test_elo_calculation() -> None:
|
||||
round_ = RoundWithMatches(
|
||||
tournament_id=1,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
is_draft=True,
|
||||
is_active=False,
|
||||
name='Some round',
|
||||
matches=[
|
||||
MatchWithTeamDetails(
|
||||
created=DUMMY_MOCK_TIME,
|
||||
team1_id=1,
|
||||
team2_id=1,
|
||||
team1_score=3,
|
||||
team2_score=4,
|
||||
round_id=1,
|
||||
team1=TeamWithPlayers(
|
||||
name='Dummy team 1',
|
||||
tournament_id=1,
|
||||
active=True,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
players=[DUMMY_PLAYER1.copy(update={'id': 1})],
|
||||
),
|
||||
team2=TeamWithPlayers(
|
||||
name='Dummy team 2',
|
||||
tournament_id=1,
|
||||
active=True,
|
||||
created=DUMMY_MOCK_TIME,
|
||||
players=[DUMMY_PLAYER2.copy(update={'id': 2})],
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
calculation = calculate_elo_per_player([round_])
|
||||
assert calculation == {
|
||||
1: PlayerStatistics(losses=1, elo_score=3, swiss_score=Decimal('0.00')),
|
||||
2: PlayerStatistics(wins=1, elo_score=4, swiss_score=Decimal('1.00')),
|
||||
}
|
||||
Reference in New Issue
Block a user