Add more integration tests (#40)

This commit is contained in:
Erik Vroon
2022-12-27 15:39:04 +01:00
committed by GitHub
parent 01a1670f5c
commit cca3aedb7f
15 changed files with 262 additions and 68 deletions

View File

@@ -1,9 +1,9 @@
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
from bracket.config import config
from bracket.config import Environment, config, environment
from bracket.database import database
from bracket.routes import auth, matches, players, rounds, teams, tournaments
from bracket.routes import auth, clubs, matches, players, rounds, teams, tournaments
app = FastAPI(
title="Bracket API",
@@ -30,7 +30,10 @@ async def startup() -> None:
@app.on_event("shutdown")
async def shutdown() -> None:
await database.disconnect()
# On CI, we need first to clean up db data in conftest.py before we can disconnect the
# db connections.
if environment != Environment.CI:
await database.disconnect()
@app.get('/ping', summary="Healthcheck ping")
@@ -39,6 +42,7 @@ async def ping() -> str:
app.include_router(auth.router, tags=['auth'])
app.include_router(clubs.router, tags=['clubs'])
app.include_router(tournaments.router, tags=['tournaments'])
app.include_router(players.router, tags=['players'])
app.include_router(rounds.router, tags=['rounds'])

View File

@@ -2,18 +2,12 @@ from fastapi import APIRouter, Depends
from heliclockter import datetime_utc
from bracket.database import database
from bracket.models.db.player import Player
from bracket.models.db.team import Team, TeamBody, TeamToInsert, TeamWithPlayers
from bracket.models.db.user import UserPublic
from bracket.routes.auth import get_current_user
from bracket.routes.models import (
SingleTeamResponse,
SuccessResponse,
TeamsResponse,
TeamsWithPlayersResponse,
)
from bracket.routes.models import SingleTeamResponse, SuccessResponse, TeamsWithPlayersResponse
from bracket.schema import players, teams
from bracket.utils.db import fetch_all_parsed, fetch_one_parsed
from bracket.utils.db import fetch_one_parsed
router = APIRouter()

View File

@@ -1,3 +1,5 @@
from zoneinfo import ZoneInfo
from heliclockter import datetime_utc
from passlib.context import CryptContext
@@ -11,27 +13,29 @@ from bracket.models.db.user import User
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
DUMMY_MOCK_TIME = datetime_utc(2022, 1, 11, 4, 32, 11, tzinfo=ZoneInfo('UTC'))
DUMMY_CLUB = Club(
name='Some Cool Club',
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
)
DUMMY_TOURNAMENT = Tournament(
club_id=1,
name='Some Cool Tournament',
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
)
DUMMY_ROUND1 = Round(
tournament_id=1,
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
is_draft=False,
name='Round 1',
)
DUMMY_ROUND2 = Round(
tournament_id=1,
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
is_active=True,
is_draft=False,
name='Round 2',
@@ -39,13 +43,13 @@ DUMMY_ROUND2 = Round(
DUMMY_ROUND3 = Round(
tournament_id=1,
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
is_draft=True,
name='Round 3',
)
DUMMY_MATCH1 = Match(
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
round_id=1,
team1_id=1,
team2_id=2,
@@ -54,7 +58,7 @@ DUMMY_MATCH1 = Match(
)
DUMMY_MATCH2 = Match(
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
round_id=1,
team1_id=3,
team2_id=4,
@@ -63,7 +67,7 @@ DUMMY_MATCH2 = Match(
)
DUMMY_MATCH3 = Match(
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
round_id=2,
team1_id=1,
team2_id=4,
@@ -72,7 +76,7 @@ DUMMY_MATCH3 = Match(
)
DUMMY_MATCH4 = Match(
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
round_id=2,
team1_id=2,
team2_id=3,
@@ -84,32 +88,32 @@ DUMMY_USER = User(
email='admin@example.com',
name='Admin',
password_hash=pwd_context.hash('adminadmin'),
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
)
DUMMY_TEAM1 = Team(
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
name='Team 1',
tournament_id=1,
active=True,
)
DUMMY_TEAM2 = Team(
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
name='Team 2',
tournament_id=1,
active=True,
)
DUMMY_TEAM3 = Team(
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
name='Team 3',
tournament_id=1,
active=True,
)
DUMMY_TEAM4 = Team(
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
name='Team 4',
tournament_id=1,
active=True,
@@ -118,7 +122,7 @@ DUMMY_TEAM4 = Team(
DUMMY_PLAYER1 = Player(
name='Luke',
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
team_id=1,
tournament_id=1,
elo_score=0,
@@ -126,7 +130,7 @@ DUMMY_PLAYER1 = Player(
DUMMY_PLAYER2 = Player(
name='Anakin',
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
team_id=1,
tournament_id=1,
elo_score=0,
@@ -134,7 +138,7 @@ DUMMY_PLAYER2 = Player(
DUMMY_PLAYER3 = Player(
name='Leia',
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
team_id=2,
tournament_id=1,
elo_score=0,
@@ -142,7 +146,7 @@ DUMMY_PLAYER3 = Player(
DUMMY_PLAYER4 = Player(
name='Yoda',
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
team_id=2,
tournament_id=1,
elo_score=0,
@@ -150,7 +154,7 @@ DUMMY_PLAYER4 = Player(
DUMMY_PLAYER5 = Player(
name='Boba',
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
team_id=3,
tournament_id=1,
elo_score=0,
@@ -158,7 +162,7 @@ DUMMY_PLAYER5 = Player(
DUMMY_PLAYER6 = Player(
name='General',
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
team_id=3,
tournament_id=1,
elo_score=0,
@@ -166,7 +170,7 @@ DUMMY_PLAYER6 = Player(
DUMMY_PLAYER7 = Player(
name='Han',
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
team_id=4,
tournament_id=1,
elo_score=0,
@@ -174,7 +178,7 @@ DUMMY_PLAYER7 = Player(
DUMMY_PLAYER8 = Player(
name='Emperor',
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
team_id=4,
tournament_id=1,
elo_score=0,
@@ -182,7 +186,7 @@ DUMMY_PLAYER8 = Player(
DUMMY_PLAYER9 = Player(
name='R2D2',
created=datetime_utc.now(),
created=DUMMY_MOCK_TIME,
team_id=None,
tournament_id=1,
elo_score=0,

View File

@@ -3,6 +3,6 @@ set -evo pipefail
black .
dmypy run -- --follow-imports=normal --junit-xml= .
ENVIRONMENT=CI pytest --cov .
ENVIRONMENT=CI pytest --cov --cov-report=xml .
pylint alembic bracket tests
isort .

View File

@@ -61,6 +61,7 @@ disable = [
'unused-argument', # Gives false positives.
'invalid-name',
'dangerous-default-value',
'duplicate-code',
]
[tool.bandit]

View File

@@ -8,7 +8,7 @@ from bracket.config import config
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.mocks import MOCK_NOW, MOCK_USER
from tests.integration_tests.mocks import MOCK_NOW, MOCK_USER, get_mock_token
from tests.integration_tests.sql import inserted_user
@@ -47,12 +47,7 @@ async def test_get_token_invalid_credentials(startup_and_shutdown_uvicorn_server
async def test_auth_on_protected_endpoint(startup_and_shutdown_uvicorn_server: None) -> None:
token = (
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.'
+ 'eyJ1c2VyIjoiZG9uYWxkX2R1Y2siLCJleHAiOjcyNTgxMjAyMDB9.'
+ 'CRk4n5gmgto5K-qWtI4hbcqo92BxLkggwwK1yTgWGLM'
)
headers = {'Authorization': f'Bearer {token}'}
headers = {'Authorization': f'Bearer {get_mock_token()}'}
async with inserted_user(MOCK_USER) as user_inserted:
response = JsonDict(await send_request(HTTPMethod.GET, 'users/me', {}, headers))

View File

@@ -0,0 +1,18 @@
from bracket.utils.dummy_records import DUMMY_MOCK_TIME
from bracket.utils.http import HTTPMethod
from tests.integration_tests.api.shared import send_auth_request
from tests.integration_tests.models import AuthContext
async def test_clubs_endpoint(
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
) -> None:
assert await send_auth_request(HTTPMethod.GET, 'clubs', auth_context, {}) == {
'data': [
{
'created': DUMMY_MOCK_TIME.isoformat(),
'id': 1,
'name': 'Some Cool Club',
}
],
}

View File

@@ -1,17 +1,17 @@
# pylint: disable=redefined-outer-name
import asyncio
import os
from asyncio import AbstractEventLoop
from functools import partial
from typing import AsyncIterator, Iterator
from typing import AsyncIterator
import aioresponses
import pytest
from aiohttp import ClientResponse
from databases import Database
from bracket.database import database, engine
from bracket.schema import metadata
from tests.integration_tests.api.shared import UvicornTestServer
from tests.integration_tests.models import AuthContext
from tests.integration_tests.sql import inserted_auth_context
os.environ['ENVIRONMENT'] = 'CI'
@@ -29,13 +29,6 @@ async def startup_and_shutdown_uvicorn_server() -> AsyncIterator[None]:
await server.down()
@pytest.fixture
def mock_http() -> Iterator[aioresponses.aioresponses]:
with aioresponses.aioresponses() as m:
m.add = partial(m.add, response_class=ClientResponse) # type: ignore[assignment]
yield m
@pytest.fixture(scope="session")
def event_loop() -> AsyncIterator[AbstractEventLoop]: # type: ignore[misc]
try:
@@ -48,10 +41,18 @@ def event_loop() -> AsyncIterator[AbstractEventLoop]: # type: ignore[misc]
@pytest.fixture(scope="session", autouse=True)
async def reinit_database(
event_loop: AbstractEventLoop, # pylint: disable=redefined-outer-name
) -> AsyncIterator[Database]:
async def reinit_database(event_loop: AbstractEventLoop) -> AsyncIterator[Database]:
await database.connect()
metadata.drop_all(engine)
metadata.create_all(engine)
yield database
try:
yield database
finally:
await database.disconnect()
@pytest.fixture(scope="session")
async def auth_context(reinit_database: Database) -> AsyncIterator[AuthContext]:
async with reinit_database:
async with inserted_auth_context() as auth_context:
yield auth_context

View File

@@ -0,0 +1,24 @@
from bracket.utils.dummy_records import DUMMY_MOCK_TIME, DUMMY_PLAYER1, DUMMY_TEAM1
from bracket.utils.http import HTTPMethod
from tests.integration_tests.api.shared import send_tournament_request
from tests.integration_tests.models import AuthContext
from tests.integration_tests.sql import inserted_player, inserted_team
async def test_players_endpoint(
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
) -> None:
async with inserted_team(DUMMY_TEAM1):
async with inserted_player(DUMMY_PLAYER1) as player_inserted:
assert await send_tournament_request(HTTPMethod.GET, 'players', auth_context, {}) == {
'data': [
{
'created': DUMMY_MOCK_TIME.isoformat(),
'id': player_inserted.id,
'elo_score': 0.0,
'name': 'Luke',
'team_id': 1,
'tournament_id': 1,
}
],
}

View File

@@ -0,0 +1,25 @@
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 send_tournament_request
from tests.integration_tests.models import AuthContext
from tests.integration_tests.sql import inserted_round, inserted_team
async def test_rounds_endpoint(
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
) -> 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, {}) == {
'data': [
{
'created': DUMMY_MOCK_TIME.isoformat(),
'id': round_inserted.id,
'is_active': False,
'is_draft': False,
'matches': [],
'name': 'Round 1',
'tournament_id': 1,
}
],
}

View File

@@ -10,6 +10,7 @@ from fastapi import FastAPI
from bracket.app import app
from bracket.utils.http import HTTPMethod
from bracket.utils.types import JsonDict, JsonObject
from tests.integration_tests.models import AuthContext
def find_free_port() -> int:
@@ -74,3 +75,17 @@ async def send_request(
) as resp:
response: JsonObject = await resp.json()
return response
async def send_auth_request(
method: HTTPMethod, endpoint: str, auth_context: AuthContext, body: JsonDict = {}
) -> JsonObject:
return await send_request(method, endpoint, body, auth_context.headers)
async def send_tournament_request(
method: HTTPMethod, endpoint: str, auth_context: AuthContext, body: JsonDict = {}
) -> JsonObject:
return await send_request(
method, f'tournaments/{auth_context.tournament.id}/{endpoint}', body, auth_context.headers
)

View File

@@ -0,0 +1,23 @@
from bracket.utils.dummy_records import DUMMY_MOCK_TIME, DUMMY_TEAM1
from bracket.utils.http import HTTPMethod
from tests.integration_tests.api.shared import send_tournament_request
from tests.integration_tests.models import AuthContext
from tests.integration_tests.sql import inserted_team
async def test_teams_endpoint(
startup_and_shutdown_uvicorn_server: None, auth_context: AuthContext
) -> None:
async with inserted_team(DUMMY_TEAM1) as team_inserted:
assert await send_tournament_request(HTTPMethod.GET, 'teams', auth_context, {}) == {
'data': [
{
'active': True,
'created': DUMMY_MOCK_TIME.isoformat(),
'id': team_inserted.id,
'name': 'Team 1',
'players': [],
'tournament_id': 1,
}
],
}

View File

@@ -9,6 +9,14 @@ MOCK_NOW = datetime_utc(
)
def get_mock_token() -> str:
return (
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.'
+ 'eyJ1c2VyIjoiZG9uYWxkX2R1Y2siLCJleHAiOjcyNTgxMjAyMDB9.'
+ 'CRk4n5gmgto5K-qWtI4hbcqo92BxLkggwwK1yTgWGLM'
)
MOCK_USER = User(
email='donald_duck',
name='Donald Duck',

View File

@@ -0,0 +1,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
class AuthContext(BaseModel):
club: Club
tournament: Tournament
user: User
headers: dict[str, str]

View File

@@ -1,20 +1,90 @@
from contextlib import asynccontextmanager
from typing import AsyncIterator
from typing import AsyncIterator, Type, cast
from sqlalchemy import Table
from bracket.database import database
from bracket.models.db.club import Club
from bracket.models.db.match import Match
from bracket.models.db.player import Player
from bracket.models.db.round import Round
from bracket.models.db.team import Team
from bracket.models.db.tournament import Tournament
from bracket.models.db.user import User, UserInDB
from bracket.schema import users
from bracket.schema import clubs, matches, players, rounds, teams, tournaments, users
from bracket.utils.db import fetch_one_parsed
from bracket.utils.dummy_records import DUMMY_CLUB, DUMMY_TOURNAMENT
from bracket.utils.types import BaseModelT
from tests.integration_tests.mocks import MOCK_USER, get_mock_token
from tests.integration_tests.models import AuthContext
@asynccontextmanager
async def inserted_generic(
data_model: BaseModelT, table: Table, return_type: Type[BaseModelT]
) -> AsyncIterator[BaseModelT]:
last_record_id = await database.execute(query=table.insert(), values=data_model.dict())
row_inserted = await fetch_one_parsed(
database, return_type, table.select().where(table.c.id == last_record_id)
)
assert isinstance(row_inserted, return_type)
try:
yield row_inserted
finally:
await database.execute(query=table.delete().where(table.c.id == last_record_id))
@asynccontextmanager
async def inserted_user(user: User) -> AsyncIterator[UserInDB]:
last_record_id = await database.execute(query=users.insert(), values=user.dict())
user_inserted = await fetch_one_parsed(
database, UserInDB, users.select().where(users.c.id == last_record_id)
)
assert user_inserted is not None
try:
yield user_inserted
finally:
await database.execute(query=users.delete().where(users.c.id == last_record_id))
async with inserted_generic(user, users, UserInDB) as row_inserted:
yield cast(UserInDB, row_inserted)
@asynccontextmanager
async def inserted_club(club: Club) -> AsyncIterator[Club]:
async with inserted_generic(club, clubs, Club) as row_inserted:
yield row_inserted
@asynccontextmanager
async def inserted_tournament(tournament: Tournament) -> AsyncIterator[Tournament]:
async with inserted_generic(tournament, tournaments, Tournament) as row_inserted:
yield row_inserted
@asynccontextmanager
async def inserted_team(team: Team) -> AsyncIterator[Team]:
async with inserted_generic(team, teams, Team) as row_inserted:
yield row_inserted
@asynccontextmanager
async def inserted_player(player: Player) -> AsyncIterator[Player]:
async with inserted_generic(player, players, Player) as row_inserted:
yield row_inserted
@asynccontextmanager
async def inserted_round(round_: Round) -> AsyncIterator[Round]:
async with inserted_generic(round_, rounds, Round) as row_inserted:
yield row_inserted
@asynccontextmanager
async def inserted_match(match: Match) -> AsyncIterator[Match]:
async with inserted_generic(match, matches, Match) as row_inserted:
yield row_inserted
@asynccontextmanager
async def inserted_auth_context() -> AsyncIterator[AuthContext]:
headers = {'Authorization': f'Bearer {get_mock_token()}'}
async with inserted_user(MOCK_USER) as user_inserted:
async with inserted_club(DUMMY_CLUB) as club_inserted:
async with inserted_tournament(DUMMY_TOURNAMENT) as tournament_inserted:
yield AuthContext(
headers=headers,
user=user_inserted,
club=club_inserted,
tournament=tournament_inserted,
)