Skip to content

Commit

Permalink
added interactor for game files
Browse files Browse the repository at this point in the history
  • Loading branch information
ychebyshev committed Mar 14, 2024
1 parent 0d6e039 commit e264b6f
Show file tree
Hide file tree
Showing 13 changed files with 185 additions and 38 deletions.
9 changes: 3 additions & 6 deletions shvatka/api/dependencies/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def create_user_token(self, user: dto.User) -> Token:

async def get_current_user(
self,
token: Token | None,
token: Token,
dao: HolderDao,
) -> dto.User:
logger.debug("try to check token %s", token)
Expand All @@ -76,9 +76,6 @@ async def get_current_user(
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
if token is None:
logger.warning("no token are present")
raise credentials_exception
try:
payload = jwt.decode(
token.access_token,
Expand Down Expand Up @@ -118,13 +115,13 @@ def get_cookie_auth(self) -> OAuth2PasswordBearerWithCookie:
@provide(scope=Scope.REQUEST)
async def get_token(
self, request: Request, cookie_auth: OAuth2PasswordBearerWithCookie
) -> Token | None:
) -> Token:
return cookie_auth.get_token(request)

@provide(scope=Scope.REQUEST)
async def get_current_user(
self,
token: Token | None,
token: Token,
auth_properties: AuthProperties,
dao: HolderDao,
) -> dto.User:
Expand Down
38 changes: 20 additions & 18 deletions shvatka/api/routes/game.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,52 @@
from typing import Annotated

from dishka.integrations.fastapi import FromDishka as Depends
from dishka.integrations.fastapi import FromDishka as DiDepends, inject
from dishka.integrations.fastapi import FromDishka
from dishka.integrations.fastapi import inject
from fastapi import APIRouter
from fastapi.params import Path
from starlette.responses import StreamingResponse
from starlette.status import HTTP_404_NOT_FOUND, HTTP_403_FORBIDDEN

from shvatka.api.models import responses
from shvatka.core.interfaces.clients.file_storage import FileGateway
from shvatka.api.utils.error_converter import to_http_error
from shvatka.core.games.interactors import FileReader
from shvatka.core.models import dto
from shvatka.core.services.game import (
get_authors_games,
get_completed_games,
get_full_game,
get_game,
)
from shvatka.core.services.scenario.files import get_file_content
from shvatka.core.utils import exceptions
from shvatka.infrastructure.db.dao.holder import HolderDao


@inject
async def get_my_games_list(
player: Annotated[dto.Player, Depends()],
dao: Annotated[HolderDao, Depends()],
player: Annotated[dto.Player, FromDishka()],
dao: Annotated[HolderDao, FromDishka()],
):
return responses.Page(await get_authors_games(player, dao.game))


@inject
async def get_active_game(
game: Annotated[dto.Game | None, Depends()],
game: Annotated[dto.Game | None, FromDishka()],
) -> responses.Game | None:
return responses.Game.from_core(game)


@inject
async def get_all_games(
dao: Annotated[HolderDao, Depends()],
dao: Annotated[HolderDao, FromDishka()],
) -> responses.Page[responses.Game]:
games = await get_completed_games(dao.game)
return responses.Page([responses.Game.from_core(game) for game in games])


@inject
async def get_game_card(
dao: Annotated[HolderDao, Depends()],
player: Annotated[dto.Player, Depends()],
dao: Annotated[HolderDao, FromDishka()],
player: Annotated[dto.Player, FromDishka()],
id_: int = Path(alias="id"), # type: ignore[assignment]
):
game = await get_full_game(id_, player, dao.game)
Expand All @@ -54,16 +55,17 @@ async def get_game_card(

@inject
async def get_game_file(
dao: Annotated[HolderDao, Depends()],
player: Annotated[dto.Player, Depends()],
file_gateway: Annotated[FileGateway, DiDepends()],
user: Annotated[dto.User, FromDishka()],
file_reader: Annotated[FileReader, FromDishka()],
id_: int = Path(alias="id"), # type: ignore[assignment]
guid: str = Path(alias="guid"), # type: ignore[assignment]
) -> StreamingResponse:
game = await get_game(id_, dao=dao.game)
return StreamingResponse(
b for b in await get_file_content(guid, file_gateway, player, game, dao.file_info)
)
try:
return StreamingResponse(b for b in await file_reader(guid=guid, user=user, game_id=id_))
except exceptions.FileNotFound as e:
raise to_http_error(e, HTTP_404_NOT_FOUND) from e
except exceptions.NotAuthorizedForEdit as e:
raise to_http_error(e, HTTP_403_FORBIDDEN) from e


def setup() -> APIRouter:
Expand Down
18 changes: 6 additions & 12 deletions shvatka/api/utils/cookie_auth.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from typing import Dict
from typing import Optional

from fastapi import HTTPException
Expand All @@ -18,26 +17,21 @@ def __init__(
self,
token_url: str,
scheme_name: Optional[str] = None,
scopes: Optional[Dict[str, str]] = None,
auto_error: bool = True,
):
if not scopes:
scopes = {}
flows = OAuthFlowsModel(password=OAuthFlowPassword(tokenUrl=token_url))
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)

def get_token(self, request: Request) -> Optional[Token]:
def get_token(self, request: Request) -> Token:
authorization = request.cookies.get("Authorization", "")

scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer":
if self.auto_error:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
return None
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
return Token(access_token=param, token_type="bearer")


Expand Down
13 changes: 13 additions & 0 deletions shvatka/api/utils/error_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from fastapi import HTTPException
from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR

from shvatka.core.utils import exceptions


def to_http_error(
error: exceptions.SHError,
code: int = HTTP_500_INTERNAL_SERVER_ERROR,
) -> HTTPException:
return HTTPException(
status_code=code, detail={"text": error.notify_user, "description": error.text}
)
Empty file added shvatka/core/games/__init__.py
Empty file.
30 changes: 30 additions & 0 deletions shvatka/core/games/interactors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from typing import BinaryIO

from shvatka.core.interfaces.clients.file_storage import FileGateway
from shvatka.core.interfaces.dal.complex import GameFileLoader
from shvatka.core.models import dto
from shvatka.core.services.scenario.files import check_file_meta_can_read
from shvatka.core.utils import exceptions


class FileReader:
def __init__(self, dao: GameFileLoader, file_gateway: FileGateway):
self.file_gateway = file_gateway
self.dao = dao

async def __call__(self, guid: str, game_id: int, user: dto.User) -> BinaryIO:
author = await self.dao.get_by_user(user)
game = await self.dao.get_full(game_id)
if guid not in game.get_guids():
raise exceptions.FileNotFound(
text=f"There is no file with uuid {guid} associated with game id {game_id}",
game_id=game_id,
game=game,
user_id=user.db_id,
user=user,
player_id=author.id,
player=author,
)
meta = await self.dao.get_by_guid(guid)
check_file_meta_can_read(author, meta, game)
return await self.file_gateway.get(meta)
7 changes: 6 additions & 1 deletion shvatka/core/interfaces/dal/complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
MaxGameNumberGetter,
GameNumberUpdater,
GameStatusCompleter,
GameByIdGetter,
)
from shvatka.core.interfaces.dal.key_log import TeamKeysMerger, GameKeyGetter
from shvatka.core.interfaces.dal.level_times import TeamLevelsMerger, LevelTimesGetter
from shvatka.core.interfaces.dal.organizer import OrgByPlayerGetter
from shvatka.core.interfaces.dal.player import TeamPlayersMerger
from shvatka.core.interfaces.dal.player import TeamPlayersMerger, PlayerByUserGetter
from shvatka.core.interfaces.dal.team import ForumTeamMerger, TeamRemover
from shvatka.core.interfaces.dal.waiver import WaiverMerger, GameWaiversGetter
from shvatka.core.models import dto
Expand Down Expand Up @@ -47,3 +48,7 @@ class GameCompleter(
class GamePackager(GameKeyGetter, LevelTimesGetter, GameWaiversGetter, FileInfoGetter, Protocol):
async def get_full(self, id_: int) -> dto.FullGame:
raise NotImplementedError


class GameFileLoader(FileInfoGetter, GameByIdGetter, PlayerByUserGetter, Protocol):
pass
5 changes: 5 additions & 0 deletions shvatka/core/interfaces/dal/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ async def get_by_user_id(self, user_id: int) -> dto.Player:
raise NotImplementedError


class PlayerByUserGetter(Protocol):
async def get_by_user(self, user: dto.User) -> dto.Player:
raise NotImplementedError


class TeamPlayerGetter(Protocol):
async def get_team_player(self, player: dto.Player) -> dto.TeamPlayer:
raise NotImplementedError
Expand Down
19 changes: 18 additions & 1 deletion shvatka/infrastructure/db/dao/complex/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dataclasses import dataclass
from typing import Iterable

from shvatka.core.interfaces.dal.complex import GamePackager
from shvatka.core.interfaces.dal.complex import GamePackager, GameFileLoader
from shvatka.core.interfaces.dal.game import GameUpserter, GameCreator
from shvatka.core.models import dto
from shvatka.core.models.dto import scn
Expand Down Expand Up @@ -99,3 +99,20 @@ async def get_full(self, id_: int) -> dto.FullGame:

async def get_by_guid(self, guid: str) -> scn.VerifiableFileMeta:
return await self.dao.file_info.get_by_guid(guid)


class GameFilesGetterImpl(GameFileLoader):
def __init__(self, dao: "HolderDao"):
self.dao = dao

async def get_by_guid(self, guid: str) -> scn.VerifiableFileMeta:
return await self.dao.file_info.get_by_guid(guid)

async def get_by_id(self, id_: int, author: dto.Player | None = None) -> dto.Game:
return await self.dao.game.get_by_id(id_, author)

async def get_full(self, id_: int) -> dto.FullGame:
return await self.dao.game.get_full(id_)

async def get_by_user(self, user: dto.User) -> dto.Player:
return await self.dao.player.get_by_user(user)
3 changes: 3 additions & 0 deletions shvatka/infrastructure/di/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from shvatka.infrastructure.di.db import DbProvider, RedisProvider
from shvatka.infrastructure.di.files import FileClientProvider
from shvatka.infrastructure.di.game import GameProvider
from shvatka.infrastructure.di.interactors import DAOProvider, InteractorProvider


def get_providers(paths_env):
Expand All @@ -14,4 +15,6 @@ def get_providers(paths_env):
GameProvider(),
FileClientProvider(),
BotProvider(),
DAOProvider(),
InteractorProvider(),
]
19 changes: 19 additions & 0 deletions shvatka/infrastructure/di/interactors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from dishka import Provider, Scope, provide

from shvatka.core.games.interactors import FileReader
from shvatka.core.interfaces.dal.complex import GameFileLoader
from shvatka.infrastructure.db.dao.complex.game import GameFilesGetterImpl
from shvatka.infrastructure.db.dao.holder import HolderDao


class DAOProvider(Provider):
scope = Scope.REQUEST

@provide
def get_game_files(self, dao: HolderDao) -> GameFileLoader:
return GameFilesGetterImpl(dao)


class InteractorProvider(Provider):
scope = Scope.REQUEST
file_reader = provide(FileReader)
58 changes: 58 additions & 0 deletions tests/integration/api_full/test_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from shvatka.api.models import responses
from shvatka.core.models import dto
from shvatka.core.models.enums import GameStatus
from shvatka.core.services.player import upsert_player
from shvatka.infrastructure.db.dao.holder import HolderDao
from shvatka.api.dependencies.auth import AuthProperties
from tests.fixtures.scn_fixtures import GUID, GUID_2


@pytest.mark.asyncio
Expand Down Expand Up @@ -67,3 +69,59 @@ async def test_game_card(
assert [lvl.scenario for lvl in actual.levels] == [
lvl.scenario for lvl in finished_game.levels
]


@pytest.mark.asyncio
async def test_game_file(
finished_game: dto.FullGame,
dao: HolderDao,
client: AsyncClient,
auth: AuthProperties,
user: dto.User,
):
token = auth.create_user_token(user)
await dao.game.set_completed(finished_game)
await dao.game.set_number(finished_game, 1)
await dao.commit()
resp = await client.get(
f"/games/{finished_game.id}/files/{GUID}",
cookies={"Authorization": "Bearer " + token.access_token},
)
assert resp.is_success
assert resp.read() == b"123"


@pytest.mark.asyncio
async def test_game_file_not_accessible(
finished_game: dto.FullGame,
dao: HolderDao,
client: AsyncClient,
auth: AuthProperties,
user: dto.User,
):
token = auth.create_user_token(user)
await dao.game.set_completed(finished_game)
await dao.game.set_number(finished_game, 1)
await dao.commit()
resp = await client.get(
f"/games/{finished_game.id}/files/{GUID_2}",
cookies={"Authorization": "Bearer " + token.access_token},
)
assert resp.status_code == 404


@pytest.mark.asyncio
async def test_game_file_game_not_completed(
game: dto.FullGame,
dao: HolderDao,
client: AsyncClient,
auth: AuthProperties,
user: dto.User,
):
await upsert_player(user, dao.player)
token = auth.create_user_token(user)
resp = await client.get(
f"/games/{game.id}/files/{GUID}",
cookies={"Authorization": "Bearer " + token.access_token},
)
assert resp.status_code == 403
4 changes: 4 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
RedisProvider,
GameProvider,
FileClientProvider,
InteractorProvider,
DAOProvider,
)
from shvatka.tgbot.main_factory import DpProvider, LockProvider
from shvatka.tgbot.username_resolver.user_getter import UserGetter
Expand Down Expand Up @@ -71,6 +73,8 @@ async def dishka():
LockProvider(),
DCFProvider(),
TelegraphProvider(),
InteractorProvider(),
DAOProvider(),
mock_provider,
)
yield container
Expand Down

0 comments on commit e264b6f

Please sign in to comment.