From 2f54f65e6291e98f010c940e44eb6ef96e7441f8 Mon Sep 17 00:00:00 2001 From: Sergey Natalenko Date: Sat, 2 Mar 2024 12:06:18 +0300 Subject: [PATCH] Add release workflow --- .github/workflows/release.yml | 2 +- .../games/{__init__.py => game_create.py} | 0 .../{read_by_id_game.py => game_details.py} | 4 +- .../games/{list_game.py => game_list.py} | 0 .../games/{update_game.py => game_update.py} | 0 .../handlers/games/lobby/list_lobby.py | 7 +- industry_game/handlers/ping.py | 3 +- .../handlers/players/login_player.py | 2 +- industry_game/services/rest.py | 8 +- industry_game/utils/lobby/models.py | 3 +- industry_game/utils/lobby/storage.py | 11 ++- industry_game/utils/timer.py | 7 +- industry_game/utils/users/base.py | 4 +- industry_game/utils/users/storage.py | 1 - poetry.lock | 56 ++++++------- pyproject.toml | 2 +- tests/plugins/factories/games.py | 29 ++++--- tests/plugins/factories/users.py | 30 ++++--- tests/plugins/jwt_auth.py | 17 ++++ tests/plugins/rest.py | 1 - tests/plugins/users.py | 2 + .../test_api/test_games/test_game_details.py | 76 ++++++++++++++++++ .../{test_games_list.py => test_game_list.py} | 4 +- .../test_players/test_login_player.py | 80 +++++++++++++++++++ 24 files changed, 268 insertions(+), 81 deletions(-) rename industry_game/handlers/games/{__init__.py => game_create.py} (100%) rename industry_game/handlers/games/{read_by_id_game.py => game_details.py} (81%) rename industry_game/handlers/games/{list_game.py => game_list.py} (100%) rename industry_game/handlers/games/{update_game.py => game_update.py} (100%) create mode 100644 tests/test_api/test_games/test_game_details.py rename tests/test_api/test_games/{test_games_list.py => test_game_list.py} (97%) create mode 100644 tests/test_api/test_players/test_login_player.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 055881d..e9ae9e5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,7 +46,7 @@ jobs: tags: | andytakker/industry-game-rest:latest andytakker/vk-parser:${{ steps.vars.outputs.sha_short }} - + - name: Build and push frontend image uses: docker/build-push-action@v5 with: diff --git a/industry_game/handlers/games/__init__.py b/industry_game/handlers/games/game_create.py similarity index 100% rename from industry_game/handlers/games/__init__.py rename to industry_game/handlers/games/game_create.py diff --git a/industry_game/handlers/games/read_by_id_game.py b/industry_game/handlers/games/game_details.py similarity index 81% rename from industry_game/handlers/games/read_by_id_game.py rename to industry_game/handlers/games/game_details.py index 62cd404..aeb0376 100644 --- a/industry_game/handlers/games/read_by_id_game.py +++ b/industry_game/handlers/games/game_details.py @@ -2,13 +2,13 @@ from aiohttp.web_exceptions import HTTPNotFound from aiohttp.web_response import Response -from industry_game.utils.http.auth.base import require_authorization +from industry_game.utils.http.auth.base import AuthMixin, require_authorization from industry_game.utils.http.deps import DependenciesMixin from industry_game.utils.http.params import parse_path_param from industry_game.utils.http.response import msgspec_json_response -class ReadByIdGameHandler(View, DependenciesMixin): +class GameDetailsHandler(View, DependenciesMixin, AuthMixin): @require_authorization async def get(self) -> Response: game_id = parse_path_param(self.request, "game_id", int) diff --git a/industry_game/handlers/games/list_game.py b/industry_game/handlers/games/game_list.py similarity index 100% rename from industry_game/handlers/games/list_game.py rename to industry_game/handlers/games/game_list.py diff --git a/industry_game/handlers/games/update_game.py b/industry_game/handlers/games/game_update.py similarity index 100% rename from industry_game/handlers/games/update_game.py rename to industry_game/handlers/games/game_update.py diff --git a/industry_game/handlers/games/lobby/list_lobby.py b/industry_game/handlers/games/lobby/list_lobby.py index 298ed48..6e63d46 100644 --- a/industry_game/handlers/games/lobby/list_lobby.py +++ b/industry_game/handlers/games/lobby/list_lobby.py @@ -1,6 +1,9 @@ from aiohttp.web import HTTPNotFound, Response, View -from industry_game.utils.http.auth.base import AuthMixin, require_authorization +from industry_game.utils.http.auth.base import ( + AuthMixin, + require_admin_authorization, +) from industry_game.utils.http.deps import DependenciesMixin from industry_game.utils.http.params import ( PaginationParamsModel, @@ -11,7 +14,7 @@ class ListGameLobbyHandler(View, DependenciesMixin, AuthMixin): - @require_authorization + @require_admin_authorization async def get(self) -> Response: game_id = parse_path_param(self.request, "game_id", int) game = await self.game_storage.read_by_id(game_id=game_id) diff --git a/industry_game/handlers/ping.py b/industry_game/handlers/ping.py index ac74585..f87cdbc 100644 --- a/industry_game/handlers/ping.py +++ b/industry_game/handlers/ping.py @@ -1,4 +1,3 @@ -import asyncio import logging from http import HTTPStatus @@ -17,7 +16,7 @@ class PingHandler(View, DependenciesMixin): async def get(self) -> Response: try: db = await self._ping() - except asyncio.TimeoutError: + except TimeoutError: db = False deps = { "db": db, diff --git a/industry_game/handlers/players/login_player.py b/industry_game/handlers/players/login_player.py index 5f7d92e..7c488f2 100644 --- a/industry_game/handlers/players/login_player.py +++ b/industry_game/handlers/players/login_player.py @@ -37,4 +37,4 @@ async def parse_player_model(self) -> AuthUserModel: try: return AuthUserModel.model_validate_json(body) except ValidationError: - raise HTTPBadRequest + raise HTTPBadRequest(reason="Incorrect user auth data") diff --git a/industry_game/services/rest.py b/industry_game/services/rest.py index 6e1b435..dee003b 100644 --- a/industry_game/services/rest.py +++ b/industry_game/services/rest.py @@ -11,7 +11,9 @@ from yarl import URL from industry_game.handlers.games.create_game import CreateGameHandler -from industry_game.handlers.games.list_game import ListGameHandler +from industry_game.handlers.games.game_details import GameDetailsHandler +from industry_game.handlers.games.game_list import ListGameHandler +from industry_game.handlers.games.game_update import UpdateGameHandler from industry_game.handlers.games.lobby.add_user_to_lobby import ( AddUserToGameLobbyHandler, ) @@ -22,8 +24,6 @@ from industry_game.handlers.games.lobby.read_lobby import ( ReadGameUserLobbyHandler, ) -from industry_game.handlers.games.read_by_id_game import ReadByIdGameHandler -from industry_game.handlers.games.update_game import UpdateGameHandler from industry_game.handlers.ping import PingHandler from industry_game.handlers.players.list_player import ListPlayerHandler from industry_game.handlers.players.login_player import LoginPlayerHandler @@ -83,7 +83,7 @@ class REST(AIOHTTPService): # game handlers (hdrs.METH_GET, "/api/v1/games/", ListGameHandler), (hdrs.METH_POST, "/api/v1/games/", CreateGameHandler), - (hdrs.METH_GET, "/api/v1/games/{game_id}/", ReadByIdGameHandler), + (hdrs.METH_GET, "/api/v1/games/{game_id}/", GameDetailsHandler), (hdrs.METH_POST, "/api/v1/games/{game_id}/", UpdateGameHandler), # lobby handlers (hdrs.METH_GET, "/api/v1/games/{game_id}/lobby/", ListGameLobbyHandler), diff --git a/industry_game/utils/lobby/models.py b/industry_game/utils/lobby/models.py index c2b681c..892647c 100644 --- a/industry_game/utils/lobby/models.py +++ b/industry_game/utils/lobby/models.py @@ -2,6 +2,7 @@ from industry_game.db.models import UserGameLobby as UserGameLobbyDb from industry_game.utils.pagination import MetaPagination +from industry_game.utils.users.models import ShortUser class Lobby(msgspec.Struct, frozen=True): @@ -18,4 +19,4 @@ def from_model(cls, obj: UserGameLobbyDb) -> "Lobby": class LobbyPagination(msgspec.Struct, frozen=True): meta: MetaPagination - items: list[Lobby] + items: list[ShortUser] diff --git a/industry_game/utils/lobby/storage.py b/industry_game/utils/lobby/storage.py index 5a24593..d0d2069 100644 --- a/industry_game/utils/lobby/storage.py +++ b/industry_game/utils/lobby/storage.py @@ -3,10 +3,12 @@ from sqlalchemy import delete, func, insert, select from sqlalchemy.ext.asyncio import AsyncSession +from industry_game.db.models import User as UserDb from industry_game.db.models import UserGameLobby as UserGameLobbyDb from industry_game.utils.db import AbstractStorage, inject_session from industry_game.utils.lobby.models import Lobby, LobbyPagination from industry_game.utils.pagination import MetaPagination +from industry_game.utils.users.models import ShortUser class LobbyStorage(AbstractStorage): @@ -80,16 +82,17 @@ async def count(self, session: AsyncSession) -> int: @inject_session async def get_items( self, session: AsyncSession, game_id: int, page: int, page_size: int - ) -> list[Lobby]: + ) -> list[ShortUser]: query = ( - select(UserGameLobbyDb) + select(UserDb) + .join(UserGameLobbyDb, UserDb.id == UserGameLobbyDb.user_id) .where(UserGameLobbyDb.game_id == game_id) .limit(page_size) .offset((page - 1) * page_size) ) games = await session.scalars(query) - items: list[Lobby] = [] + items: list[ShortUser] = [] for game in games: - items.append(Lobby.from_model(game)) + items.append(ShortUser.from_model(game)) return items diff --git a/industry_game/utils/timer.py b/industry_game/utils/timer.py index 47683de..cdcffdd 100644 --- a/industry_game/utils/timer.py +++ b/industry_game/utils/timer.py @@ -13,7 +13,12 @@ class Timer: _end_time: float | None _task: asyncio.Task | None - def __init__(self, coroutine: Awaitable, seconds: float, speed: float = 1) -> None: + def __init__( + self, + coroutine: Awaitable, + seconds: float, + speed: float = 1, + ) -> None: self._coroutine = coroutine self._seconds = seconds self._re_seconds = seconds diff --git a/industry_game/utils/users/base.py b/industry_game/utils/users/base.py index 82985aa..8071cf3 100644 --- a/industry_game/utils/users/base.py +++ b/industry_game/utils/users/base.py @@ -1,6 +1,6 @@ from enum import StrEnum -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from industry_game.utils.msgspec import CustomStruct @@ -16,6 +16,8 @@ class RegisterPlayerModel(BaseModel): class AuthUserModel(BaseModel): + model_config = ConfigDict(str_min_length=8) + username: str password: str diff --git a/industry_game/utils/users/storage.py b/industry_game/utils/users/storage.py index 650759c..a028449 100644 --- a/industry_game/utils/users/storage.py +++ b/industry_game/utils/users/storage.py @@ -65,7 +65,6 @@ async def get_by_username_and_password_hash( password_hash: str, ) -> FullUser | None: stmt = select(UserDb).where( - UserDb.type == UserType.PLAYER, UserDb.username == username, UserDb.password_hash == password_hash, ) diff --git a/poetry.lock b/poetry.lock index 54b9ac5..3db21ef 100644 --- a/poetry.lock +++ b/poetry.lock @@ -708,15 +708,33 @@ files = [ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] +[[package]] +name = "factory-boy" +version = "3.3.0" +description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." +optional = false +python-versions = ">=3.7" +files = [ + {file = "factory_boy-3.3.0-py2.py3-none-any.whl", hash = "sha256:a2cdbdb63228177aa4f1c52f4b6d83fab2b8623bf602c7dedd7eb83c0f69c04c"}, + {file = "factory_boy-3.3.0.tar.gz", hash = "sha256:bc76d97d1a65bbd9842a6d722882098eb549ec8ee1081f9fb2e8ff29f0c300f1"}, +] + +[package.dependencies] +Faker = ">=0.7.0" + +[package.extras] +dev = ["Django", "Pillow", "SQLAlchemy", "coverage", "flake8", "isort", "mongoengine", "sqlalchemy-utils", "tox", "wheel (>=0.32.0)", "zest.releaser[recommended]"] +doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] + [[package]] name = "faker" -version = "23.2.1" +version = "23.3.0" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.8" files = [ - {file = "Faker-23.2.1-py3-none-any.whl", hash = "sha256:0520a6b97e07c658b2798d7140971c1d5bc4bcd5013e7937fe075fd054aa320c"}, - {file = "Faker-23.2.1.tar.gz", hash = "sha256:f07b64d27f67b62c7f0536a72f47813015b3b51cd4664918454011094321e464"}, + {file = "Faker-23.3.0-py3-none-any.whl", hash = "sha256:117ce1a2805c1bc5ca753b3dc6f9d567732893b2294b827d3164261ee8f20267"}, + {file = "Faker-23.3.0.tar.gz", hash = "sha256:458d93580de34403a8dec1e8d5e6be2fee96c4deca63b95d71df7a6a80a690de"}, ] [package.dependencies] @@ -1474,30 +1492,6 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "polyfactory" -version = "2.14.1" -description = "Mock data generation factories" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "polyfactory-2.14.1-py3-none-any.whl", hash = "sha256:8aff3be75e046501ec5c411c78c23db284322c760fef50d560ee6ed683f217c8"}, - {file = "polyfactory-2.14.1.tar.gz", hash = "sha256:8c1d5f15dad1ebfd0845d65d4a55f9791cddfa6b3096ad9f9e2fd02a4804631b"}, -] - -[package.dependencies] -faker = "*" -typing-extensions = ">=4.6.0" - -[package.extras] -attrs = ["attrs (>=22.2.0)"] -beanie = ["beanie", "pydantic[email]"] -full = ["attrs", "beanie", "msgspec", "odmantic", "pydantic", "sqlalchemy"] -msgspec = ["msgspec"] -odmantic = ["odmantic (<1.0.0)", "pydantic[email]"] -pydantic = ["pydantic[email]"] -sqlalchemy = ["sqlalchemy (>=1.4.29)"] - [[package]] name = "pre-commit" version = "3.6.0" @@ -1760,13 +1754,13 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] @@ -2328,4 +2322,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "91a1636d2a583b638fe7b8fceb4bd392c97b389418efd298eb4538af0da8a884" +content-hash = "d5520e2070c3ef1234bbe8f7dc217e1384e5bd95f91947092479ae5efb5fa3e1" diff --git a/pyproject.toml b/pyproject.toml index b6104be..8d873d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ pre-commit = "^3.6.0" bandit = "^1.7.7" ruff = "^0.2.0" aiomisc-pytest = "^1.1.1" -polyfactory = "^2.14.1" +factory-boy = "^3.3.0" [build-system] requires = ["poetry-core"] diff --git a/tests/plugins/factories/games.py b/tests/plugins/factories/games.py index 37eb0d4..5d54562 100644 --- a/tests/plugins/factories/games.py +++ b/tests/plugins/factories/games.py @@ -1,32 +1,31 @@ from collections.abc import Callable -from datetime import UTC, datetime +import factory import pytest -from polyfactory.factories.sqlalchemy_factory import SQLAlchemyFactory from sqlalchemy.ext.asyncio import AsyncSession -from industry_game.db.models import Game, User +from industry_game.db.models import Game +from industry_game.utils.users.base import UserType +from tests.plugins.factories.users import UserFactory -def utc_now() -> datetime: - return datetime.now(tz=UTC) +class GameFactory(factory.Factory): + class Meta: + model = Game - -class GameFactory(SQLAlchemyFactory[Game]): - __set_primary_key__ = False - __set_foreign_keys__ = False - __use_defaults__ = True - - created_at = utc_now - updated_at = utc_now + id = factory.Sequence(lambda n: n + 1) + name = "New game" + description = "New game description" finished_at = None started_at = None + created_by = factory.SubFactory(UserFactory, type=UserType.ADMIN) + @pytest.fixture def create_game(session: AsyncSession) -> Callable: - async def factory(created_by: User, **kwargs) -> Game: - game = GameFactory.build(created_by=created_by, **kwargs) + async def factory(**kwargs) -> Game: + game = GameFactory(**kwargs) session.add(game) await session.commit() await session.flush(game) diff --git a/tests/plugins/factories/users.py b/tests/plugins/factories/users.py index b470f5e..b2b704d 100644 --- a/tests/plugins/factories/users.py +++ b/tests/plugins/factories/users.py @@ -1,33 +1,41 @@ from collections.abc import Callable -from typing import Any +import factory import pytest -from polyfactory.factories.sqlalchemy_factory import SQLAlchemyFactory from sqlalchemy.ext.asyncio import AsyncSession from industry_game.db.models import User from industry_game.utils.security import Passgen +from industry_game.utils.users.base import UserType -def empty_properties() -> dict[str, Any]: - return dict() +class UserPropertiesFactory(factory.Factory): + class Meta: + model = dict + name = "First Last Name" + telegram = "@tg_username" -class UserFactory(SQLAlchemyFactory[User]): - __set_primary_key__ = False - properties = empty_properties +class UserFactory(factory.Factory): + class Meta: + model = User + + id = factory.Sequence(lambda n: n + 1) + username = "username" + type = UserType.PLAYER + password_hash = "" + properties = factory.SubFactory(UserPropertiesFactory) @pytest.fixture def create_user(session: AsyncSession, passgen: Passgen) -> Callable: async def factory(**kwargs) -> User: - password = "secret" + password = kwargs.get("password", "secret00") if "password" in kwargs: - password = kwargs["password"] del kwargs["password"] - password_hash = passgen.hashpw(password) - user = UserFactory.build(**kwargs, password_hash=password_hash) + kwargs["password_hash"] = passgen.hashpw(password) + user = UserFactory(**kwargs) session.add(user) await session.commit() await session.flush(user) diff --git a/tests/plugins/jwt_auth.py b/tests/plugins/jwt_auth.py index 79701a8..cbb6bb0 100644 --- a/tests/plugins/jwt_auth.py +++ b/tests/plugins/jwt_auth.py @@ -1,6 +1,9 @@ +from collections.abc import Callable + import pytest from cryptography.hazmat.primitives.asymmetric import rsa +from industry_game.db.models import User from industry_game.utils.http.auth.jwt import ( JwtAuthrorizationProvider, JwtProcessor, @@ -52,3 +55,17 @@ def admin_token(admin: AuthUser, jwt_processor: JwtProcessor) -> str: @pytest.fixture def player_token(player: AuthUser, jwt_processor: JwtProcessor) -> str: return jwt_processor.encode(player.to_dict()) + + +@pytest.fixture +def token_from_user(jwt_processor: JwtProcessor) -> Callable: + def _factory(user: User) -> str: + return jwt_processor.encode( + { + "id": user.id, + "username": user.username, + "type": user.type, + } + ) + + return _factory diff --git a/tests/plugins/rest.py b/tests/plugins/rest.py index cdaf66e..72c0817 100644 --- a/tests/plugins/rest.py +++ b/tests/plugins/rest.py @@ -1,4 +1,3 @@ -from argparse import Namespace from collections.abc import AsyncIterator, Mapping, Sequence import pytest diff --git a/tests/plugins/users.py b/tests/plugins/users.py index 9d2418c..ad0cc7c 100644 --- a/tests/plugins/users.py +++ b/tests/plugins/users.py @@ -15,6 +15,7 @@ def player_storage( ) -> PlayerStorage: return PlayerStorage(session_factory=session_factory) + @pytest.fixture def player_processor( player_storage: PlayerStorage, @@ -27,6 +28,7 @@ def player_processor( authorization_provider=authorization_provider, ) + @pytest.fixture def admin() -> AuthUser: return AuthUser( diff --git a/tests/test_api/test_games/test_game_details.py b/tests/test_api/test_games/test_game_details.py new file mode 100644 index 0000000..6e89903 --- /dev/null +++ b/tests/test_api/test_games/test_game_details.py @@ -0,0 +1,76 @@ +from collections.abc import Mapping +from http import HTTPStatus + +from aiohttp.test_utils import TestClient +from yarl import URL + +from tests.utils.datetime import format_tz + +API_URL = URL("/api/v1/games/1/") + + +async def test_game_details_unauthorized(api_client: TestClient): + response = await api_client.get(API_URL) + assert response.status == HTTPStatus.UNAUTHORIZED + + +async def test_game_details_player_status_ok( + api_client: TestClient, + player_headers: Mapping[str, str], + create_game, +): + await create_game(id=1) + response = await api_client.get(API_URL, headers=player_headers) + assert response.status == HTTPStatus.OK + + +async def test_game_details_player_format( + api_client: TestClient, + player_headers: Mapping[str, str], + create_game, +): + game = await create_game(id=1) + response = await api_client.get(API_URL, headers=player_headers) + result = await response.json() + assert result == { + "id": game.id, + "name": game.name, + "description": game.description, + "status": game.status.value, + "created_by_id": game.created_by_id, + "finished_at": format_tz(game.finished_at), + "started_at": format_tz(game.started_at), + "created_at": format_tz(game.created_at), + "updated_at": format_tz(game.updated_at), + } + + +async def test_game_details_admin_status_ok( + api_client: TestClient, + admin_headers: Mapping[str, str], + create_game, +): + await create_game(id=1) + response = await api_client.get(API_URL, headers=admin_headers) + assert response.status == HTTPStatus.OK + + +async def test_game_details_admin_format( + api_client: TestClient, + admin_headers: Mapping[str, str], + create_game, +): + game = await create_game(id=1) + response = await api_client.get(API_URL, headers=admin_headers) + result = await response.json() + assert result == { + "id": game.id, + "name": game.name, + "description": game.description, + "status": game.status.value, + "created_by_id": game.created_by_id, + "finished_at": format_tz(game.finished_at), + "started_at": format_tz(game.started_at), + "created_at": format_tz(game.created_at), + "updated_at": format_tz(game.updated_at), + } diff --git a/tests/test_api/test_games/test_games_list.py b/tests/test_api/test_games/test_game_list.py similarity index 97% rename from tests/test_api/test_games/test_games_list.py rename to tests/test_api/test_games/test_game_list.py index 0080da4..b9facf6 100644 --- a/tests/test_api/test_games/test_games_list.py +++ b/tests/test_api/test_games/test_game_list.py @@ -11,7 +11,7 @@ API_URL = URL("/api/v1/games/") -async def test_games_list_unauthorized(api_client: TestClient) -> None: +async def test_games_list_unauthorized(api_client: TestClient): response = await api_client.get(API_URL) assert response.status == HTTPStatus.UNAUTHORIZED @@ -19,7 +19,7 @@ async def test_games_list_unauthorized(api_client: TestClient) -> None: async def test_games_list_players_status_ok( api_client: TestClient, player_headers: Mapping[str, str], -) -> None: +): response = await api_client.get(API_URL, headers=player_headers) assert response.status == HTTPStatus.OK diff --git a/tests/test_api/test_players/test_login_player.py b/tests/test_api/test_players/test_login_player.py new file mode 100644 index 0000000..fc31658 --- /dev/null +++ b/tests/test_api/test_players/test_login_player.py @@ -0,0 +1,80 @@ +from http import HTTPStatus + +import pytest +from aiohttp.test_utils import TestClient +from yarl import URL + +from industry_game.utils.http.auth.jwt import AUTH_COOKIE +from industry_game.utils.users.base import UserType + +API_URL = URL("/api/v1/players/login/") + + +async def test_login_empty_json_error(api_client: TestClient): + response = await api_client.post(API_URL) + assert response.status == HTTPStatus.BAD_REQUEST + + +async def test_login_user_not_found_error(api_client: TestClient): + response = await api_client.post( + API_URL, + json={ + "username": "new_user", + "password": "password", + }, + ) + assert response.status == HTTPStatus.NOT_FOUND + + +@pytest.mark.parametrize("user_type", (UserType.PLAYER, UserType.ADMIN)) +async def test_login_player_successful_status_ok( + api_client: TestClient, create_user, user_type +): + player = await create_user(type=user_type) + response = await api_client.post( + API_URL, + json={ + "username": player.username, + "password": "secret00", + }, + ) + assert response.status == HTTPStatus.OK + + +@pytest.mark.parametrize("user_type", (UserType.PLAYER, UserType.ADMIN)) +async def test_login_player_successful_format( + api_client: TestClient, + token_from_user, + create_user, + user_type, +): + player = await create_user(type=user_type) + response = await api_client.post( + API_URL, + json={ + "username": player.username, + "password": "secret00", + }, + ) + + result = await response.json() + assert result == {"token": token_from_user(player)} + + +@pytest.mark.parametrize("user_type", (UserType.PLAYER, UserType.ADMIN)) +async def test_login_player_successful_set_cookie( + api_client: TestClient, + token_from_user, + create_user, + user_type, +): + user = await create_user(type=user_type) + response = await api_client.post( + API_URL, + json={ + "username": user.username, + "password": "secret00", + }, + ) + + assert response.cookies[AUTH_COOKIE].value == token_from_user(user)