From 22ac2b0e19c04291da51087574913d242d7da646 Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Mon, 8 Apr 2024 16:20:44 +0900 Subject: [PATCH 01/20] =?UTF-8?q?refactor:=20[AsyncSQLModel=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85]=20quiz=20answer=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/game/model.py | 6 ++++- backend/src/quiz/model.py | 13 ++++++----- backend/src/quiz/quiz_answer_service.py | 11 +++++---- backend/src/quiz/repository.py | 30 ++++--------------------- 4 files changed, 24 insertions(+), 36 deletions(-) diff --git a/backend/src/game/model.py b/backend/src/game/model.py index 6575036..8d58e46 100644 --- a/backend/src/game/model.py +++ b/backend/src/game/model.py @@ -1,5 +1,7 @@ from datetime import datetime +from typing import Awaitable +from async_sqlmodel import AsyncSQLModel, AwaitableField from sqlalchemy import BigInteger, Column, UniqueConstraint from sqlmodel import Field, Relationship, SQLModel @@ -55,7 +57,7 @@ class GameAlias(CreatedAtMixin, UpdatedAtMixin, SQLModel, table=True): # Screenshot -class GameScreenshot(CreatedAtMixin, UpdatedAtMixin, SQLModel, table=True): +class GameScreenshot(CreatedAtMixin, UpdatedAtMixin, AsyncSQLModel, table=True): __tablename__: str = "game_screenshot" id: int | None = Field(default=None, primary_key=True) @@ -64,3 +66,5 @@ class GameScreenshot(CreatedAtMixin, UpdatedAtMixin, SQLModel, table=True): game_id: int = Field(foreign_key="game.id") game: Game = Relationship() + awt_game: Awaitable[Game] = AwaitableField(field="game") + diff --git a/backend/src/quiz/model.py b/backend/src/quiz/model.py index cf418a5..d18aba3 100644 --- a/backend/src/quiz/model.py +++ b/backend/src/quiz/model.py @@ -1,5 +1,7 @@ from datetime import date +from typing import Awaitable +from async_sqlmodel import AsyncSQLModel, AwaitableField from sqlmodel import Field, Relationship, SQLModel from ..game.model import Game, GameScreenshot @@ -15,16 +17,16 @@ class QuizScreenshotLink(SQLModel, table=True): screenshot_id: int = Field(foreign_key="game_screenshot.id") -class Quiz(CreatedAtMixin, UpdatedAtMixin, SQLModel, table=True): +class Quiz(CreatedAtMixin, UpdatedAtMixin, AsyncSQLModel, table=True): __tablename__: str = "quiz" id: int | None = Field(default=None, primary_key=True) screenshots: list[GameScreenshot] = Relationship(link_model=QuizScreenshotLink) + awt_screenshots: Awaitable[list[GameScreenshot]] = AwaitableField(field="screenshots") - @property - def game(self) -> Game: - return self.screenshots[0].game + async def get_game(self) -> Game: + return await (await self.awt_screenshots)[0].awt_game class QuizAnswer(CreatedAtMixin, UpdatedAtMixin, SQLModel, table=True): @@ -40,7 +42,7 @@ class QuizAnswer(CreatedAtMixin, UpdatedAtMixin, SQLModel, table=True): quiz: Quiz = Relationship() -class DailyQuiz(CreatedAtMixin, UpdatedAtMixin, SQLModel, table=True): +class DailyQuiz(CreatedAtMixin, UpdatedAtMixin, AsyncSQLModel, table=True): __tablename__: str = "daily_quiz" id: int | None = Field(default=None, primary_key=True) @@ -50,3 +52,4 @@ class DailyQuiz(CreatedAtMixin, UpdatedAtMixin, SQLModel, table=True): quiz_id: int = Field(foreign_key="quiz.id") feature: str = Field(max_length=64) quiz: Quiz = Relationship() + awt_quiz: Awaitable[Quiz] = AwaitableField(field="quiz") diff --git a/backend/src/quiz/quiz_answer_service.py b/backend/src/quiz/quiz_answer_service.py index 9d303ab..dd1132b 100644 --- a/backend/src/quiz/quiz_answer_service.py +++ b/backend/src/quiz/quiz_answer_service.py @@ -1,5 +1,7 @@ from collections.abc import Sequence +from quiz.model import Quiz + from .model import QuizAnswer from .quiz_validator import QuizValidator from .repository import QuizAnswerRepository, QuizRepository @@ -33,10 +35,11 @@ async def get_quiz_answer(self, *, quiz_id: int, user_id: int) -> Sequence[QuizA await self._validate_quiz(quiz_id=quiz_id) return await self._quiz_answer_repo.get_by_quiz_id_and_user_id(quiz_id=quiz_id, user_id=user_id) - async def _validate_quiz(self, *, quiz_id: int): + async def _validate_quiz(self, *, quiz_id: int) -> Quiz: quiz = await self._quiz_repo.get(id=quiz_id) - self._quiz_validator.validate_quiz_existed(quiz=quiz) + return self._quiz_validator.validate_quiz_existed(quiz=quiz) async def _get_correct_answer(self, *, quiz_id: int) -> str: - quiz = await self._quiz_repo.get_with_game(id=quiz_id) - return self._quiz_validator.validate_quiz_existed(quiz=quiz).game.name + quiz = await self._validate_quiz(quiz_id=quiz_id) + game = await quiz.get_game() + return game.name diff --git a/backend/src/quiz/repository.py b/backend/src/quiz/repository.py index 7fb1a82..0e9191b 100644 --- a/backend/src/quiz/repository.py +++ b/backend/src/quiz/repository.py @@ -1,12 +1,9 @@ from collections.abc import Sequence from datetime import date, datetime -from typing import Optional -from sqlalchemy.orm import selectinload from sqlmodel import select from sqlmodel.ext.asyncio.session import AsyncSession -from ..game.model import GameScreenshot from ..repository import CRUDMixin, IRepository from .model import DailyQuiz, Quiz, QuizAnswer @@ -17,23 +14,8 @@ class QuizRepository(IRepository[Quiz], CRUDMixin): def __init__(self, session: AsyncSession) -> None: self._session = session - async def get_with_game(self, *, id: int) -> Optional[Quiz]: - stmt = ( - select(Quiz) - .where(Quiz.id == id) - .options(selectinload(Quiz.screenshots).selectinload(GameScreenshot.game)) # type: ignore - ) - rs = await self._session.exec(stmt) - return rs.first() - - async def get_by_created_at_interval_with_screenshots( - self, *, start_at: datetime, end_at: datetime - ) -> Sequence[Quiz]: - stmts = ( - select(Quiz) - .where(Quiz.created_at >= start_at, Quiz.created_at <= end_at) - .options(selectinload(Quiz.screenshots)) # type: ignore - ) + async def get_by_created_at_interval(self, *, start_at: datetime, end_at: datetime) -> Sequence[Quiz]: + stmts = select(Quiz).where(Quiz.created_at >= start_at, Quiz.created_at <= end_at) rs = await self._session.exec(stmts) return rs.all() @@ -56,11 +38,7 @@ class DailyQuizRepository(IRepository[DailyQuiz], CRUDMixin): def __init__(self, session: AsyncSession) -> None: self._session = session - async def get_by_target_date_with_quiz_and_screenshots(self, *, target_date: date) -> Sequence[DailyQuiz]: - stmt = ( - select(DailyQuiz) - .where(DailyQuiz.target_date == target_date) - .options(selectinload(DailyQuiz.quiz).selectinload(Quiz.screenshots)) # type: ignore - ) + async def get_by_target_date(self, *, target_date: date) -> Sequence[DailyQuiz]: + stmt = select(DailyQuiz).where(DailyQuiz.target_date == target_date) rs = await self._session.exec(stmt) return rs.all() From 0f74ba65cd2d29c3787a8ed35445759adc68c6ad Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Mon, 8 Apr 2024 16:26:09 +0900 Subject: [PATCH 02/20] =?UTF-8?q?refactor:=20[AsyncSQLModel=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85]=20quiz,=20daily=5Fquiz=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/quiz/daily_quiz_loader.py | 13 ++++++------- backend/src/quiz/quiz_service.py | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/backend/src/quiz/daily_quiz_loader.py b/backend/src/quiz/daily_quiz_loader.py index b5c0a55..03f95cf 100644 --- a/backend/src/quiz/daily_quiz_loader.py +++ b/backend/src/quiz/daily_quiz_loader.py @@ -12,12 +12,13 @@ class DailyQuizLoader: def __init__(self, *, daily_quiz_repository: DailyQuizRepository) -> None: self._daily_quiz_repo = daily_quiz_repository - def _daily_quizzes(self, daily_quizzes: Iterable[DailyQuiz]) -> list[schema.DailyQuiz]: + async def _daily_quizzes(self, daily_quizzes: Iterable[DailyQuiz]) -> list[schema.DailyQuiz]: quizzes: list[schema.DailyQuiz] = [] for daily_quiz in daily_quizzes: - assert daily_quiz.quiz.id is not None + quiz = await daily_quiz.awt_quiz + assert quiz.id is not None - screenshots = [Url(s.url) for s in daily_quiz.quiz.screenshots] + screenshots = [Url(s.url) for s in await quiz.awt_screenshots] quizzes.append( schema.DailyQuiz(quiz_id=daily_quiz.quiz_id, screenshots=screenshots, feature=daily_quiz.feature) ) @@ -27,7 +28,5 @@ def _daily_quizzes(self, daily_quizzes: Iterable[DailyQuiz]) -> list[schema.Dail async def get_daily_quizzes(self) -> Sequence[schema.DailyQuiz]: utc_now_date = datetime.utcnow().date() - daily_quizzes = await self._daily_quiz_repo.get_by_target_date_with_quiz_and_screenshots( - target_date=utc_now_date - ) - return self._daily_quizzes(daily_quizzes) + daily_quizzes = await self._daily_quiz_repo.get_by_target_date(target_date=utc_now_date) + return await self._daily_quizzes(daily_quizzes) diff --git a/backend/src/quiz/quiz_service.py b/backend/src/quiz/quiz_service.py index 26cd1f2..9006c8a 100644 --- a/backend/src/quiz/quiz_service.py +++ b/backend/src/quiz/quiz_service.py @@ -23,5 +23,5 @@ async def get_correct_answer(self, *, quiz_id: int, user_id: int) -> str: return quiz.game.name async def _get_quiz(self, *, quiz_id: int) -> Quiz: - quiz = await self._quiz_repo.get_with_game(id=quiz_id) + quiz = await self._quiz_repo.get(id=quiz_id) return self._quiz_validator.validate_quiz_existed(quiz=quiz) From d8ec53eee4f5f2fc861cc4dd9a36a9a184f9f216 Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Mon, 8 Apr 2024 16:45:00 +0900 Subject: [PATCH 03/20] typo: Alreay -> Already --- backend/src/quiz/exception.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/quiz/exception.py b/backend/src/quiz/exception.py index 0885d84..278e398 100644 --- a/backend/src/quiz/exception.py +++ b/backend/src/quiz/exception.py @@ -6,7 +6,7 @@ class QuizNotFoundError(NotFoundError): class QuizAlreadyCompletedError(BadRequestError): - DETAIL = "Quiz Alreay Completed" + DETAIL = "Quiz Already Completed" class QuizNotCompletedError(BadRequestError): From dfc2419bfb883e61dd8fd7e4e37bca6172a32700 Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Tue, 9 Apr 2024 17:30:46 +0900 Subject: [PATCH 04/20] =?UTF-8?q?refactor:=20[AsyncSQLModel=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85]=20guest=20quiz=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/quiz/guest/guest_quiz_service.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/src/quiz/guest/guest_quiz_service.py b/backend/src/quiz/guest/guest_quiz_service.py index 9739899..cafcd72 100644 --- a/backend/src/quiz/guest/guest_quiz_service.py +++ b/backend/src/quiz/guest/guest_quiz_service.py @@ -1,6 +1,7 @@ from datetime import datetime from ...guest.schema import Guest +from ..model import Quiz from ..quiz_validator import QuizValidator from ..repository import QuizRepository from ..schema import QuizAnswer @@ -17,7 +18,8 @@ def __init__( self._quiz_validator = quiz_validator async def submit_answer(self, *, guest: Guest, quiz_id: int, answer: str) -> tuple[bool, Guest]: - correct_answer = await self._get_correct_answer(quiz_id=quiz_id) + quiz = await self._validate_quiz(quiz_id=quiz_id) + correct_answer = await self._get_correct_answer(quiz=quiz) self._quiz_validator.validate_quiz_not_completed(answers=guest.quiz_answers[quiz_id]) correct = correct_answer == answer @@ -30,14 +32,13 @@ async def get_quiz_answer(self, *, guest: Guest, quiz_id: int) -> list[QuizAnswe return guest.quiz_answers[quiz_id] async def get_correct_answer(self, *, guest: Guest, quiz_id: int) -> str: - await self._validate_quiz(quiz_id=quiz_id) + quiz = await self._validate_quiz(quiz_id=quiz_id) self._quiz_validator.validate_quiz_completed(answers=guest.quiz_answers[quiz_id]) - return await self._get_correct_answer(quiz_id=quiz_id) + return await self._get_correct_answer(quiz=quiz) - async def _get_correct_answer(self, *, quiz_id: int) -> str: - quiz = await self._quiz_repo.get_with_game(id=quiz_id) - return self._quiz_validator.validate_quiz_existed(quiz=quiz).game.name + async def _get_correct_answer(self, *, quiz: Quiz) -> str: + return (await quiz.get_game()).name async def _validate_quiz(self, *, quiz_id: int): quiz = await self._quiz_repo.get(id=quiz_id) - self._quiz_validator.validate_quiz_existed(quiz=quiz) + return self._quiz_validator.validate_quiz_existed(quiz=quiz) From 1bd6da56bf941eebad1446cf788aa3f5f840ddac Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Tue, 9 Apr 2024 17:30:59 +0900 Subject: [PATCH 05/20] =?UTF-8?q?refactor:=20[AsyncSQLModel=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85]=20quiz=5Fservice=20=EB=88=84=EB=9D=BD=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/quiz/quiz_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/quiz/quiz_service.py b/backend/src/quiz/quiz_service.py index 9006c8a..0fbd138 100644 --- a/backend/src/quiz/quiz_service.py +++ b/backend/src/quiz/quiz_service.py @@ -20,7 +20,7 @@ async def get_correct_answer(self, *, quiz_id: int, user_id: int) -> str: quiz_answers = await self._quiz_answer_repo.get_by_quiz_id_and_user_id(quiz_id=quiz_id, user_id=user_id) self._quiz_validator.validate_quiz_completed(answers=quiz_answers) - return quiz.game.name + return (await quiz.get_game()).name async def _get_quiz(self, *, quiz_id: int) -> Quiz: quiz = await self._quiz_repo.get(id=quiz_id) From 2b84d955772a39cb3445b619488538007a6d7a22 Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Tue, 9 Apr 2024 17:31:20 +0900 Subject: [PATCH 06/20] =?UTF-8?q?refactor:=20[AsyncSQLModel=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B9=84=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/tests/config.py | 9 ---- backend/tests/database.py | 6 +-- backend/tests/integration/conftest.py | 54 ++++++++++--------- .../game/test_auto_complete_name.py | 51 +++++++++--------- .../tests/integration/quiz/test_get_answer.py | 22 ++++---- .../quiz/test_get_correct_answer.py | 44 ++++++++------- .../quiz/test_get_daily_quizzes.py | 14 ++--- .../quiz/test_guest_correct_answer.py | 36 +++++++------ .../integration/quiz/test_guest_get_answer.py | 14 ++--- .../quiz/test_guest_submit_answer.py | 40 +++++++------- .../integration/quiz/test_submit_answer.py | 50 +++++++++-------- backend/tests/utils/auth.py | 9 ++-- backend/tests/utils/database.py | 9 ++-- backend/tests/utils/es.py | 10 ++-- backend/tests/utils/game.py | 19 ++++--- backend/tests/utils/quiz.py | 38 +++++++------ backend/tests/utils/screenshot.py | 13 +++-- 17 files changed, 226 insertions(+), 212 deletions(-) delete mode 100644 backend/tests/config.py diff --git a/backend/tests/config.py b/backend/tests/config.py deleted file mode 100644 index f444a6b..0000000 --- a/backend/tests/config.py +++ /dev/null @@ -1,9 +0,0 @@ -from pydantic import MySQLDsn -from pydantic_settings import BaseSettings - - -class TestConfig(BaseSettings): - TEST_DATABASE_URL: MySQLDsn | str - - -test_settings = TestConfig() # type: ignore diff --git a/backend/tests/database.py b/backend/tests/database.py index c58be46..e7630f2 100644 --- a/backend/tests/database.py +++ b/backend/tests/database.py @@ -1,5 +1,5 @@ -from sqlalchemy import create_engine +from sqlalchemy.ext.asyncio import create_async_engine -from .config import test_settings +from src.config import settings -engine = create_engine(test_settings.TEST_DATABASE_URL, echo=True) # type: ignore +engine = create_async_engine(settings.DATABASE_URL, echo=True) # type: ignore diff --git a/backend/tests/integration/conftest.py b/backend/tests/integration/conftest.py index a7c04c2..41ce267 100644 --- a/backend/tests/integration/conftest.py +++ b/backend/tests/integration/conftest.py @@ -1,10 +1,10 @@ -from typing import Any, Generator +import asyncio +from typing import AsyncGenerator import pytest -from elasticsearch import Elasticsearch -from fastapi.testclient import TestClient -from sqlalchemy import Engine -from sqlmodel import Session +from elasticsearch import AsyncElasticsearch +from httpx import AsyncClient +from sqlmodel.ext.asyncio.session import AsyncSession from src.auth.dependency import current_active_user from src.auth.model import User @@ -15,30 +15,33 @@ from tests.utils.database import create_all_table, drop_tables from tests.utils.es import create_all_indexes, delete_all_indexes - -@pytest.fixture() -def client() -> Generator[TestClient, Any, None]: - with TestClient(app) as client: - yield client +lock = asyncio.Lock() @pytest.fixture(autouse=True) -def database(): - drop_tables() - create_all_table() - yield - drop_tables() +async def database(): + async with lock: + await drop_tables() + await create_all_table() + yield + await drop_tables() + + +@pytest.fixture() +async def client() -> AsyncGenerator[AsyncClient, None]: + async with AsyncClient(app=app, base_url="http://testserver") as client: + yield client @pytest.fixture() -def session(database: Engine) -> Generator[Session, Any, None]: - with Session(engine) as session: +async def session(database) -> AsyncGenerator[AsyncSession, None]: + async with AsyncSession(engine, expire_on_commit=False) as session: yield session @pytest.fixture() -def current_user(session: Session) -> Generator[User, Any, None]: - user = create_random_user(session, email="email@example.com") +async def current_user(session: AsyncSession) -> AsyncGenerator[User, None]: + user = await create_random_user(session, email="email@example.com") def override_current_active_user() -> User: return user @@ -49,10 +52,11 @@ def override_current_active_user() -> User: @pytest.fixture() -def es_client() -> Generator[Elasticsearch, Any, None]: - es_client = Elasticsearch(settings.ELASTIC_SEARCH_URL) - delete_all_indexes(es_client) - create_all_indexes(es_client) +async def es_client() -> AsyncGenerator[AsyncElasticsearch, None]: + es_client = AsyncElasticsearch(settings.ELASTIC_SEARCH_URL) + + await delete_all_indexes(es_client) + await create_all_indexes(es_client) yield es_client - delete_all_indexes(es_client) - es_client.close() + await delete_all_indexes(es_client) + await es_client.close() diff --git a/backend/tests/integration/game/test_auto_complete_name.py b/backend/tests/integration/game/test_auto_complete_name.py index 7f1bd91..3334e1e 100644 --- a/backend/tests/integration/game/test_auto_complete_name.py +++ b/backend/tests/integration/game/test_auto_complete_name.py @@ -1,11 +1,10 @@ -from collections.abc import Generator -from typing import Any +from typing import AsyncGenerator import pytest -from elasticsearch import Elasticsearch +from elasticsearch import AsyncElasticsearch from fastapi import status -from fastapi.testclient import TestClient -from sqlmodel import Session +from httpx import AsyncClient +from sqlmodel.ext.asyncio.session import AsyncSession from src.game.model import Game from src.main import app @@ -13,14 +12,16 @@ @pytest.fixture(scope="module") -def client() -> Generator[TestClient, Any, None]: - with TestClient(app) as client: +async def client() -> AsyncGenerator[AsyncClient, None]: + async with AsyncClient(app=app) as client: yield client -def create_indexed_game(session: Session, es_client: Elasticsearch, name: str, aliases: list[str] = []) -> Game: - game = create_random_game(session, name=name, aliases=aliases) - index_game(es_client, game) +async def create_indexed_game( + session: AsyncSession, es_client: AsyncElasticsearch, name: str, aliases: list[str] = [] +) -> Game: + game = await create_random_game(session, name=name, aliases=aliases) + await index_game(es_client, game) return game @@ -37,12 +38,12 @@ def create_indexed_game(session: Session, es_client: Elasticsearch, name: str, a ("Nier:Automata", "nier:auto"), ), ) -def test_auto_complete_game_name( - client: TestClient, session: Session, es_client: Elasticsearch, game_name: str, query: str +async def test_auto_complete_game_name( + client: AsyncClient, session: AsyncSession, es_client: AsyncElasticsearch, game_name: str, query: str ): - saved_game = create_indexed_game(session, es_client, game_name) + saved_game = await create_indexed_game(session, es_client, game_name) - res = client.get(f"/game/auto_complete_name?query={query}") + res = await client.get(f"/game/auto_complete_name?query={query}") assert res.status_code == status.HTTP_200_OK res_json = res.json() @@ -57,26 +58,28 @@ def test_auto_complete_game_name( ("NieR:Automata", "오토마타", "오토"), ), ) -def test_auto_complete_game_name_by_alias( - client: TestClient, session: Session, es_client: Elasticsearch, game_name: str, alias: str, query: str +async def test_auto_complete_game_name_by_alias( + client: AsyncClient, session: AsyncSession, es_client: AsyncElasticsearch, game_name: str, alias: str, query: str ): - saved_game = create_indexed_game(session, es_client, game_name, [alias]) + saved_game = await create_indexed_game(session, es_client, game_name, [alias]) - res = client.get(f"/game/auto_complete_name?query={query}") + res = await client.get(f"/game/auto_complete_name?query={query}") assert res.status_code == status.HTTP_200_OK res_json = res.json() assert res_json["games"] == [{"name": saved_game.name, "match": alias}] -def test_auto_complete_for_multiple_games(client: TestClient, session: Session, es_client: Elasticsearch): - create_indexed_game(session, es_client, name="game1") - create_indexed_game(session, es_client, name="game2") - create_indexed_game(session, es_client, name="game3") - create_indexed_game(session, es_client, name="game4") +async def test_auto_complete_for_multiple_games( + client: AsyncClient, session: AsyncSession, es_client: AsyncElasticsearch +): + await create_indexed_game(session, es_client, name="game1") + await create_indexed_game(session, es_client, name="game2") + await create_indexed_game(session, es_client, name="game3") + await create_indexed_game(session, es_client, name="game4") query = "game" - res = client.get(f"/game/auto_complete_name?query={query}") + res = await client.get(f"/game/auto_complete_name?query={query}") assert res.status_code == status.HTTP_200_OK res_json = res.json() diff --git a/backend/tests/integration/quiz/test_get_answer.py b/backend/tests/integration/quiz/test_get_answer.py index 80498f6..37ed465 100644 --- a/backend/tests/integration/quiz/test_get_answer.py +++ b/backend/tests/integration/quiz/test_get_answer.py @@ -1,18 +1,18 @@ from fastapi import status -from fastapi.testclient import TestClient -from sqlmodel import Session +from httpx import AsyncClient +from sqlmodel.ext.asyncio.session import AsyncSession from src.auth.model import User from tests.utils.quiz import create_random_quiz, create_random_quiz_answer from tests.utils.utils import jsontime2datetime -def test_get_answer(session: Session, client: TestClient, current_user: User): - quiz = create_random_quiz(session) - quiz_answer1 = create_random_quiz_answer(session, quiz_id=quiz.id, user_id=current_user.id) - quiz_answer2 = create_random_quiz_answer(session, quiz_id=quiz.id, user_id=current_user.id) +async def test_get_answer(session: AsyncSession, client: AsyncClient, current_user: User): + quiz = await create_random_quiz(session) + quiz_answer1 = await create_random_quiz_answer(session, quiz_id=quiz.id, user_id=current_user.id) + quiz_answer2 = await create_random_quiz_answer(session, quiz_id=quiz.id, user_id=current_user.id) - res = client.get(f"/quiz/answer?quiz_id={quiz.id}") + res = await client.get(f"/quiz/answer?quiz_id={quiz.id}") assert res.status_code == status.HTTP_200_OK res_json = res.json() @@ -23,15 +23,15 @@ def test_get_answer(session: Session, client: TestClient, current_user: User): assert jsontime2datetime(quiz_ans_res["created_at"]) == quiz_ans.created_at -def test_get_answer_with_not_existed_quiz_id(client: TestClient, current_user: User): +async def test_get_answer_with_not_existed_quiz_id(client: AsyncClient, current_user: User): quiz_id = 1 - res = client.get(f"/quiz/answer?quiz_id={quiz_id}") + res = await client.get(f"/quiz/answer?quiz_id={quiz_id}") assert res.status_code == status.HTTP_404_NOT_FOUND -def test_get_answer_with_unauthorized_request(client: TestClient): +async def test_get_answer_with_unauthorized_request(client: AsyncClient): quiz_id = 1 - res = client.get(f"/quiz/answer?quiz_id={quiz_id}") + res = await client.get(f"/quiz/answer?quiz_id={quiz_id}") assert res.status_code == status.HTTP_401_UNAUTHORIZED diff --git a/backend/tests/integration/quiz/test_get_correct_answer.py b/backend/tests/integration/quiz/test_get_correct_answer.py index 30c8c00..2710507 100644 --- a/backend/tests/integration/quiz/test_get_correct_answer.py +++ b/backend/tests/integration/quiz/test_get_correct_answer.py @@ -1,53 +1,59 @@ from fastapi import status -from fastapi.testclient import TestClient -from sqlmodel import Session +from httpx import AsyncClient +from sqlmodel.ext.asyncio.session import AsyncSession from src.auth.model import User from src.config import settings from tests.utils.quiz import create_random_quiz, create_random_quiz_answer -def test_get_correct_answer_with_correct_submission(client: TestClient, session: Session, current_user: User): - quiz = create_random_quiz(session) - create_random_quiz_answer(session, quiz_id=quiz.id, user_id=current_user.id, correct=True) +async def test_get_correct_answer_with_correct_submission( + client: AsyncClient, session: AsyncSession, current_user: User +): + quiz = await create_random_quiz(session) + await create_random_quiz_answer(session, quiz_id=quiz.id, user_id=current_user.id, correct=True) - res = client.get(f"/quiz/correct_answer?quiz_id={quiz.id}") + res = await client.get(f"/quiz/correct_answer?quiz_id={quiz.id}") assert res.status_code == status.HTTP_200_OK res_json = res.json() - assert res_json == {"correct_answer": quiz.game.name} + assert res_json == {"correct_answer": (await quiz.get_game()).name} -def test_get_correct_answer_with_exceed_submission_limit(client: TestClient, session: Session, current_user: User): - quiz = create_random_quiz(session) +async def test_get_correct_answer_with_exceed_submission_limit( + client: AsyncClient, session: AsyncSession, current_user: User +): + quiz = await create_random_quiz(session) [ - create_random_quiz_answer(session, quiz_id=quiz.id, user_id=current_user.id, correct=False) + await create_random_quiz_answer(session, quiz_id=quiz.id, user_id=current_user.id, correct=False) for _ in range(settings.QUIZ_ANSWER_SUBMISSION_LIMIT) ] - res = client.get(f"/quiz/correct_answer?quiz_id={quiz.id}") + res = await client.get(f"/quiz/correct_answer?quiz_id={quiz.id}") assert res.status_code == status.HTTP_200_OK res_json = res.json() - assert res_json == {"correct_answer": quiz.game.name} + assert res_json == {"correct_answer": (await quiz.get_game()).name} -def test_get_correct_answer_with_unauthorized_request(client: TestClient): +async def test_get_correct_answer_with_unauthorized_request(client: AsyncClient): quiz_id = 1 - res = client.get(f"/quiz/correct_answer?quiz_id={quiz_id}") + res = await client.get(f"/quiz/correct_answer?quiz_id={quiz_id}") assert res.status_code == status.HTTP_401_UNAUTHORIZED -def test_get_correct_answer_with_not_existed_quiz_id(client: TestClient, current_user: User): +async def test_get_correct_answer_with_not_existed_quiz_id(client: AsyncClient, current_user: User): quiz_id = 1 - res = client.get(f"/quiz/correct_answer?quiz_id={quiz_id}") + res = await client.get(f"/quiz/correct_answer?quiz_id={quiz_id}") assert res.status_code == status.HTTP_404_NOT_FOUND -def test_get_correct_answer_with_not_completed_quiz(client: TestClient, session: Session, current_user: User): - quiz = create_random_quiz(session) +async def test_get_correct_answer_with_not_completed_quiz( + client: AsyncClient, session: AsyncSession, current_user: User +): + quiz = await create_random_quiz(session) - res = client.get(f"/quiz/correct_answer?quiz_id={quiz.id}") + res = await client.get(f"/quiz/correct_answer?quiz_id={quiz.id}") assert res.status_code == status.HTTP_400_BAD_REQUEST diff --git a/backend/tests/integration/quiz/test_get_daily_quizzes.py b/backend/tests/integration/quiz/test_get_daily_quizzes.py index f95c193..bb15fb3 100644 --- a/backend/tests/integration/quiz/test_get_daily_quizzes.py +++ b/backend/tests/integration/quiz/test_get_daily_quizzes.py @@ -1,23 +1,25 @@ from datetime import datetime from fastapi import status -from fastapi.testclient import TestClient -from sqlmodel import Session +from httpx import AsyncClient +from sqlmodel.ext.asyncio.session import AsyncSession from tests.utils.quiz import create_random_daily_quiz -def test_get_daily_quizzes(client: TestClient, session: Session): +async def test_get_daily_quizzes(client: AsyncClient, session: AsyncSession): today = datetime.utcnow().date() - saved_daily_quizzes = [create_random_daily_quiz(session, target_date=today) for _ in range(5)] + saved_daily_quizzes = [await create_random_daily_quiz(session, target_date=today) for _ in range(5)] - res = client.get("/quiz/daily_quizes") + res = await client.get("/quiz/daily_quizes") assert res.status_code == status.HTTP_200_OK res_json = res.json() assert len(res_json["daily_quizes"]) == 5 for daily_quiz_json, saved_daily_quiz in zip(res_json["daily_quizes"], saved_daily_quizzes): assert len(daily_quiz_json["screenshots"]) == 5 - assert daily_quiz_json["screenshots"] == [s.url for s in saved_daily_quiz.quiz.screenshots] + assert daily_quiz_json["screenshots"] == [ + s.url for s in (await (await saved_daily_quiz.awt_quiz).awt_screenshots) + ] assert daily_quiz_json["quiz_id"] == saved_daily_quiz.quiz_id assert daily_quiz_json["feature"] == saved_daily_quiz.feature diff --git a/backend/tests/integration/quiz/test_guest_correct_answer.py b/backend/tests/integration/quiz/test_guest_correct_answer.py index 1be72e3..6fc921a 100644 --- a/backend/tests/integration/quiz/test_guest_correct_answer.py +++ b/backend/tests/integration/quiz/test_guest_correct_answer.py @@ -3,36 +3,38 @@ from uuid import uuid4 from fastapi import status -from fastapi.testclient import TestClient -from sqlmodel import Session +from httpx import AsyncClient +from sqlmodel.ext.asyncio.session import AsyncSession from src.config import settings from tests.utils.quiz import create_random_quiz -def test_get_guest_correct_answer_with_correct_submission(client: TestClient, session: Session): - quiz = create_random_quiz(session) +async def test_get_guest_correct_answer_with_correct_submission(client: AsyncClient, session: AsyncSession): + quiz = await create_random_quiz(session) guest = { "id": str(uuid4()), "quiz_answers": { - quiz.id: [{"answer": quiz.game.name, "correct": True, "created_at": "2024-02-20T16:02:01.816Z"}] + quiz.id: [ + {"answer": (await quiz.get_game()).name, "correct": True, "created_at": "2024-02-20T16:02:01.816Z"} + ] }, } - res = client.get( + res = await client.get( f"/quiz/guest/correct_answer?quiz_id={quiz.id}", cookies={"guest": base64.b64encode(json.dumps(guest).encode()).decode()}, ) assert res.status_code == status.HTTP_200_OK res_json = res.json() - assert res_json == {"correct_answer": quiz.game.name} + assert res_json == {"correct_answer": (await quiz.get_game()).name} -def test_get_guest_correct_answer_with_exceed_submission_limit(client: TestClient, session: Session): - quiz = create_random_quiz(session) - wrong_answer = quiz.game.name + "haha" +async def test_get_guest_correct_answer_with_exceed_submission_limit(client: AsyncClient, session: AsyncSession): + quiz = await create_random_quiz(session) + wrong_answer = (await quiz.get_game()).name + "haha" guest = { "id": str(uuid4()), @@ -44,25 +46,25 @@ def test_get_guest_correct_answer_with_exceed_submission_limit(client: TestClien }, } - res = client.get( + res = await client.get( f"/quiz/guest/correct_answer?quiz_id={quiz.id}", cookies={"guest": base64.b64encode(json.dumps(guest).encode()).decode()}, ) assert res.status_code == status.HTTP_200_OK res_json = res.json() - assert res_json == {"correct_answer": quiz.game.name} + assert res_json == {"correct_answer": (await quiz.get_game()).name} -def test_get_guest_correct_answer_with_not_existed_quiz_id(client: TestClient): +async def test_get_guest_correct_answer_with_not_existed_quiz_id(client: AsyncClient): quiz_id = 1 - res = client.get(f"/quiz/guest/correct_answer?quiz_id={quiz_id}") + res = await client.get(f"/quiz/guest/correct_answer?quiz_id={quiz_id}") assert res.status_code == status.HTTP_404_NOT_FOUND -def test_get_guest_correct_answer_with_not_completed_quiz(client: TestClient, session: Session): - quiz = create_random_quiz(session) +async def test_get_guest_correct_answer_with_not_completed_quiz(client: AsyncClient, session: AsyncSession): + quiz = await create_random_quiz(session) - res = client.get(f"/quiz/guest/correct_answer?quiz_id={quiz.id}") + res = await client.get(f"/quiz/guest/correct_answer?quiz_id={quiz.id}") assert res.status_code == status.HTTP_400_BAD_REQUEST diff --git a/backend/tests/integration/quiz/test_guest_get_answer.py b/backend/tests/integration/quiz/test_guest_get_answer.py index e196346..2647e33 100644 --- a/backend/tests/integration/quiz/test_guest_get_answer.py +++ b/backend/tests/integration/quiz/test_guest_get_answer.py @@ -3,15 +3,15 @@ from uuid import uuid4 from fastapi import status -from fastapi.testclient import TestClient -from sqlmodel import Session +from httpx import AsyncClient +from sqlmodel.ext.asyncio.session import AsyncSession from tests.utils.quiz import create_random_quiz from tests.utils.utils import jsontime2datetime -def test_get_guest_answer(session: Session, client: TestClient): - quiz = create_random_quiz(session) +async def test_get_guest_answer(session: AsyncSession, client: AsyncClient): + quiz = await create_random_quiz(session) wrong_answers = ["빙빙바리바리구1", "빙빙바리바리구2", "빙빙바리바리구3"] guest = { @@ -24,7 +24,7 @@ def test_get_guest_answer(session: Session, client: TestClient): }, } - res = client.get( + res = await client.get( f"/quiz/guest/answer?quiz_id={quiz.id}", cookies={"guest": base64.b64encode(json.dumps(guest).encode()).decode()}, ) @@ -38,8 +38,8 @@ def test_get_guest_answer(session: Session, client: TestClient): assert jsontime2datetime(quiz_ans_res["created_at"]) == jsontime2datetime(quiz_ans["created_at"]) -def test_get_guest_answer_with_not_existed_quiz_id(client: TestClient): +async def test_get_guest_answer_with_not_existed_quiz_id(client: AsyncClient): quiz_id = 1 - res = client.get(f"/quiz/guest/answer?quiz_id={quiz_id}") + res = await client.get(f"/quiz/guest/answer?quiz_id={quiz_id}") assert res.status_code == status.HTTP_404_NOT_FOUND diff --git a/backend/tests/integration/quiz/test_guest_submit_answer.py b/backend/tests/integration/quiz/test_guest_submit_answer.py index 9e63072..3c519c4 100644 --- a/backend/tests/integration/quiz/test_guest_submit_answer.py +++ b/backend/tests/integration/quiz/test_guest_submit_answer.py @@ -3,17 +3,19 @@ from uuid import uuid4 from fastapi import status -from fastapi.testclient import TestClient -from sqlmodel import Session +from httpx import AsyncClient +from sqlmodel.ext.asyncio.session import AsyncSession from src.config import settings from tests.utils.quiz import create_random_quiz -def test_post_guest_submit_true_answer(client: TestClient, session: Session): - quiz = create_random_quiz(session) +async def test_post_guest_submit_true_answer(client: AsyncClient, session: AsyncSession): + quiz = await create_random_quiz(session) - res = client.post("/quiz/guest/submit_answer", json={"quiz_id": quiz.id, "answer": quiz.game.name}) + res = await client.post( + "/quiz/guest/submit_answer", json={"quiz_id": quiz.id, "answer": (await quiz.get_game()).name} + ) assert res.status_code == status.HTTP_200_OK res_json = res.json() @@ -22,12 +24,12 @@ def test_post_guest_submit_true_answer(client: TestClient, session: Session): guest = json.loads(base64.b64decode(res.cookies["guest"])) quiz_answer = guest["quiz_answers"][str(quiz.id)][0] - assert quiz_answer["answer"] == quiz.game.name + assert quiz_answer["answer"] == (await quiz.get_game()).name assert quiz_answer["correct"] is True -def test_post_guest_submit_false_answer(client: TestClient, session: Session): - quiz = create_random_quiz(session) +async def test_post_guest_submit_false_answer(client: AsyncClient, session: AsyncSession): + quiz = await create_random_quiz(session) wrong_answers = ["빙빙바리바리구1", "빙빙바리바리구2", "빙빙바리바리구3"] guest = { @@ -36,7 +38,7 @@ def test_post_guest_submit_false_answer(client: TestClient, session: Session): } for wrong_answer in wrong_answers: - res = client.post( + res = await client.post( "/quiz/guest/submit_answer", json={"quiz_id": quiz.id, "answer": wrong_answer}, cookies={"guest": base64.b64encode(json.dumps(guest).encode()).decode()}, @@ -53,13 +55,13 @@ def test_post_guest_submit_false_answer(client: TestClient, session: Session): assert quiz_answer["correct"] is False -def test_post_guest_submit_answer_with_not_existed_quiz_id(client: TestClient): - res = client.post("/quiz/guest/submit_answer", json={"quiz_id": -1, "answer": "아무거나 빙빙바리바리구"}) +async def test_post_guest_submit_answer_with_not_existed_quiz_id(client: AsyncClient): + res = await client.post("/quiz/guest/submit_answer", json={"quiz_id": -1, "answer": "아무거나 빙빙바리바리구"}) assert res.status_code == status.HTTP_404_NOT_FOUND -def test_post_guest_submit_answer_with_exceed_submission_limit(client: TestClient, session: Session): - quiz = create_random_quiz(session) +async def test_post_guest_submit_answer_with_exceed_submission_limit(client: AsyncClient, session: AsyncSession): + quiz = await create_random_quiz(session) wrong_answer = "빙빙바리바리구" guest = { @@ -72,7 +74,7 @@ def test_post_guest_submit_answer_with_exceed_submission_limit(client: TestClien }, } - res = client.post( + res = await client.post( "/quiz/guest/submit_answer", json={"quiz_id": quiz.id, "answer": "아무거나 방방빙방"}, cookies={"guest": base64.b64encode(json.dumps(guest).encode()).decode()}, @@ -80,17 +82,19 @@ def test_post_guest_submit_answer_with_exceed_submission_limit(client: TestClien assert res.status_code == status.HTTP_400_BAD_REQUEST -def test_post_guest_submit_answer_with_prior_correct_answer(client: TestClient, session: Session): - quiz = create_random_quiz(session) +async def test_post_guest_submit_answer_with_prior_correct_answer(client: AsyncClient, session: AsyncSession): + quiz = await create_random_quiz(session) guest = { "id": str(uuid4()), "quiz_answers": { - str(quiz.id): [{"answer": quiz.game.name, "correct": True, "created_at": "2024-02-20T16:02:01.816Z"}] + str(quiz.id): [ + {"answer": (await quiz.get_game()).name, "correct": True, "created_at": "2024-02-20T16:02:01.816Z"} + ] }, } - res = client.post( + res = await client.post( "/quiz/guest/submit_answer", json={"quiz_id": quiz.id, "answer": "아무거나 방방빙방"}, cookies={"guest": base64.b64encode(json.dumps(guest).encode()).decode()}, diff --git a/backend/tests/integration/quiz/test_submit_answer.py b/backend/tests/integration/quiz/test_submit_answer.py index 49bdc0b..b248511 100644 --- a/backend/tests/integration/quiz/test_submit_answer.py +++ b/backend/tests/integration/quiz/test_submit_answer.py @@ -1,68 +1,74 @@ from fastapi import status -from fastapi.testclient import TestClient -from sqlmodel import Session +from httpx import AsyncClient, Response +from sqlmodel.ext.asyncio.session import AsyncSession from src.auth.model import User from src.config import settings from tests.utils.quiz import create_random_quiz, create_random_quiz_answer, get_quiz_answer -def test_post_submit_true_answer(client: TestClient, session: Session, current_user: User): - saved_quiz = create_random_quiz(session) +async def test_post_submit_true_answer(client: AsyncClient, session: AsyncSession, current_user: User): + saved_quiz = await create_random_quiz(session) - res = client.post("/quiz/submit_answer", json={"quiz_id": saved_quiz.id, "answer": saved_quiz.game.name}) + res = await client.post( + "/quiz/submit_answer", json={"quiz_id": saved_quiz.id, "answer": (await saved_quiz.get_game()).name} + ) assert res.status_code == status.HTTP_200_OK res_json = res.json() assert res_json["correct"] is True # check db - submit = get_quiz_answer(session, quiz_id=saved_quiz.id) + submit = await get_quiz_answer(session, quiz_id=saved_quiz.id) assert submit is not None assert submit.correct is True assert submit.user_id == current_user.id -def test_post_submit_false_answer(client: TestClient, session: Session, current_user: User): - saved_quiz = create_random_quiz(session) +async def test_post_submit_false_answer(client: AsyncClient, session: AsyncSession, current_user: User): + saved_quiz = await create_random_quiz(session) - res = client.post("/quiz/submit_answer", json={"quiz_id": saved_quiz.id, "answer": "빙빙바리바리구"}) + res = await client.post("/quiz/submit_answer", json={"quiz_id": saved_quiz.id, "answer": "빙빙바리바리구"}) assert res.status_code == status.HTTP_200_OK res_json = res.json() assert res_json["correct"] is False # check db - submit = get_quiz_answer(session, quiz_id=saved_quiz.id) + submit = await get_quiz_answer(session, quiz_id=saved_quiz.id) assert submit is not None assert submit.correct is False assert submit.user_id == current_user.id -def test_post_submit_answer_with_not_existed_quiz_id(client: TestClient, current_user: User): - res = client.post("/quiz/submit_answer", json={"quiz_id": -1, "answer": "아무거나 빙빙바리바리구"}) +async def test_post_submit_answer_with_not_existed_quiz_id(client: AsyncClient, current_user: User): + res = await client.post("/quiz/submit_answer", json={"quiz_id": -1, "answer": "아무거나 빙빙바리바리구"}) assert res.status_code == status.HTTP_404_NOT_FOUND -def test_post_submit_answer_with_unauthorized_request(client: TestClient): - res = client.post("/quiz/submit_answer", json={"quiz_id": -1, "answer": "아무거나 빙빙바리바리구"}) +async def test_post_submit_answer_with_unauthorized_request(client: AsyncClient): + res = await client.post("/quiz/submit_answer", json={"quiz_id": -1, "answer": "아무거나 빙빙바리바리구"}) assert res.status_code == status.HTTP_401_UNAUTHORIZED -def test_post_submit_answer_with_exceed_submission_limit(client: TestClient, session: Session, current_user: User): - quiz = create_random_quiz(session) +async def test_post_submit_answer_with_exceed_submission_limit( + client: AsyncClient, session: AsyncSession, current_user: User +): + quiz = await create_random_quiz(session) _ = [ - create_random_quiz_answer(session, quiz_id=quiz.id, user_id=current_user.id, correct=False) + await create_random_quiz_answer(session, quiz_id=quiz.id, user_id=current_user.id, correct=False) for _ in range(settings.QUIZ_ANSWER_SUBMISSION_LIMIT) ] - res = client.post("/quiz/submit_answer", json={"quiz_id": quiz.id, "answer": "아무거나 방방빙방"}) + res: Response = await client.post("/quiz/submit_answer", json={"quiz_id": quiz.id, "answer": "아무거나 방방빙방"}) assert res.status_code == status.HTTP_400_BAD_REQUEST -def test_post_submit_answer_with_prior_correct_answer(client: TestClient, session: Session, current_user: User): - quiz = create_random_quiz(session) - create_random_quiz_answer(session, quiz_id=quiz.id, user_id=current_user.id, correct=True) +async def test_post_submit_answer_with_prior_correct_answer( + client: AsyncClient, session: AsyncSession, current_user: User +): + quiz = await create_random_quiz(session) + await create_random_quiz_answer(session, quiz_id=quiz.id, user_id=current_user.id, correct=True) - res = client.post("/quiz/submit_answer", json={"quiz_id": quiz.id, "answer": "아무거나 방방빙방"}) + res = await client.post("/quiz/submit_answer", json={"quiz_id": quiz.id, "answer": "아무거나 방방빙방"}) assert res.status_code == status.HTTP_400_BAD_REQUEST diff --git a/backend/tests/utils/auth.py b/backend/tests/utils/auth.py index 18cd4d4..effbaf7 100644 --- a/backend/tests/utils/auth.py +++ b/backend/tests/utils/auth.py @@ -1,12 +1,12 @@ -from sqlmodel import Session +from sqlmodel.ext.asyncio.session import AsyncSession from src.auth.model import User from .utils import random_email -def create_random_user( - session: Session, +async def create_random_user( + session: AsyncSession, *, email: str | None = None, is_active: bool = True, @@ -25,7 +25,6 @@ def create_random_user( ) session.add(user) - session.commit() - session.refresh(user) + await session.commit() return user diff --git a/backend/tests/utils/database.py b/backend/tests/utils/database.py index b88ee45..ebd19d1 100644 --- a/backend/tests/utils/database.py +++ b/backend/tests/utils/database.py @@ -1,11 +1,12 @@ +from sqlalchemy.util.concurrency import greenlet_spawn from sqlmodel import SQLModel from tests.database import engine -def drop_tables(): - SQLModel.metadata.drop_all(engine) +async def drop_tables(): + await greenlet_spawn(SQLModel.metadata.drop_all, engine.sync_engine) -def create_all_table(): - SQLModel.metadata.create_all(engine) +async def create_all_table(): + await greenlet_spawn(SQLModel.metadata.create_all, engine.sync_engine) diff --git a/backend/tests/utils/es.py b/backend/tests/utils/es.py index dae7479..52ba78b 100644 --- a/backend/tests/utils/es.py +++ b/backend/tests/utils/es.py @@ -1,19 +1,19 @@ -from elasticsearch import Elasticsearch +from elasticsearch import AsyncElasticsearch from src.es import INDEXES -def delete_all_indexes(es_client: Elasticsearch): +async def delete_all_indexes(es_client: AsyncElasticsearch): for index in INDEXES: try: - es_client.indices.delete(index=index) + await es_client.indices.delete(index=index) except Exception: pass -def create_all_indexes(es_client: Elasticsearch): +async def create_all_indexes(es_client: AsyncElasticsearch): for index in INDEXES: try: - es_client.indices.create(index=index) + await es_client.indices.create(index=index) except Exception: pass diff --git a/backend/tests/utils/game.py b/backend/tests/utils/game.py index 5571e71..9f2e999 100644 --- a/backend/tests/utils/game.py +++ b/backend/tests/utils/game.py @@ -1,8 +1,8 @@ +from asyncio import Lock from datetime import datetime -from threading import Lock -from elasticsearch import Elasticsearch -from sqlmodel import Session +from elasticsearch import AsyncElasticsearch +from sqlmodel.ext.asyncio.session import AsyncSession from src.es import GAME_INDEX from src.game.model import Game, GameAlias @@ -13,8 +13,8 @@ steam_id_lock = Lock() -def create_random_game( - session: Session, *, name: str | None = None, released_at: datetime | None = None, aliases: list[str] = [] +async def create_random_game( + session: AsyncSession, *, name: str | None = None, released_at: datetime | None = None, aliases: list[str] = [] ) -> Game: global steam_id_counter @@ -23,7 +23,7 @@ def create_random_game( if released_at is None: released_at = random_datetime() - with steam_id_lock: + async with steam_id_lock: steam_id_counter += 1 game = Game( steam_id=steam_id_counter, @@ -33,15 +33,14 @@ def create_random_game( ) session.add(game) - session.commit() - session.refresh(game) + await session.commit() return game -def index_game(es_client: Elasticsearch, game: Game): +async def index_game(es_client: AsyncElasticsearch, game: Game): q_name = "".join([c if c.isalnum() else " " for c in game.name]) - es_client.index( + await es_client.index( index=GAME_INDEX, body={"q_name": q_name, "name": game.name, "aliases": [alias.name for alias in game.aliases], "id": game.id}, refresh=True, diff --git a/backend/tests/utils/quiz.py b/backend/tests/utils/quiz.py index 7a66b9f..49f7d07 100644 --- a/backend/tests/utils/quiz.py +++ b/backend/tests/utils/quiz.py @@ -2,7 +2,8 @@ from datetime import date from typing import Optional -from sqlmodel import Session, select +from sqlmodel import select +from sqlmodel.ext.asyncio.session import AsyncSession from src.game.model import GameScreenshot from src.quiz.model import DailyQuiz, Quiz, QuizAnswer @@ -15,34 +16,33 @@ QUIZ_SCREENSHOT_COUNT = 5 -def create_random_quiz(session: Session, *, screenshots: list[GameScreenshot] | None = None) -> Quiz: +async def create_random_quiz(session: AsyncSession, *, screenshots: list[GameScreenshot] | None = None) -> Quiz: if screenshots: assert len(screenshots) == QUIZ_SCREENSHOT_COUNT # 하나의 게임에만 속해 있어야 한다. assert len(set(s.game_id for s in screenshots)) == 1 else: - game = create_random_game(session) - screenshots = [create_random_game_screenshot(session, game_id=game.id) for _ in range(5)] + game = await create_random_game(session) + screenshots = [await create_random_game_screenshot(session, game_id=game.id) for _ in range(5)] quiz = Quiz(screenshots=screenshots) session.add(quiz) - session.commit() - session.refresh(quiz) + await session.commit() return quiz -def create_random_quiz_answer( - session: Session, *, quiz_id: int | None = None, user_id: int | None = None, correct: bool | None = None +async def create_random_quiz_answer( + session: AsyncSession, *, quiz_id: int | None = None, user_id: int | None = None, correct: bool | None = None ) -> QuizAnswer: if quiz_id is None: - quiz = create_random_quiz(session) + quiz = await create_random_quiz(session) assert quiz.id is not None quiz_id = quiz.id if user_id is None: - user = create_random_user(session) + user = await create_random_user(session) assert user.id is not None user_id = user.id @@ -54,14 +54,13 @@ def create_random_quiz_answer( quiz_answer = QuizAnswer(quiz_id=quiz_id, user_id=user_id, answer=answer, correct=correct) session.add(quiz_answer) - session.commit() - session.refresh(quiz_answer) + await session.commit() return quiz_answer -def get_quiz_answer( - session: Session, *, quiz_id: int | None = None, answer: str | None = None +async def get_quiz_answer( + session: AsyncSession, *, quiz_id: int | None = None, answer: str | None = None ) -> Optional[QuizAnswer]: stmt = select(QuizAnswer) if quiz_id: @@ -69,14 +68,14 @@ def get_quiz_answer( if answer: stmt = stmt.where(QuizAnswer.answer == answer) - return session.exec(stmt).first() + return (await session.exec(stmt)).first() -def create_random_daily_quiz( - session: Session, *, target_date: date, quiz_id: int | None = None, feature: str | None = None +async def create_random_daily_quiz( + session: AsyncSession, *, target_date: date, quiz_id: int | None = None, feature: str | None = None ) -> DailyQuiz: if quiz_id is None: - quiz = create_random_quiz(session) + quiz = await create_random_quiz(session) assert quiz.id is not None quiz_id = quiz.id @@ -86,8 +85,7 @@ def create_random_daily_quiz( daily_quiz = DailyQuiz(target_date=target_date, quiz_id=quiz_id, feature=feature) session.add(daily_quiz) - session.commit() - session.refresh(daily_quiz) + await session.commit() return daily_quiz diff --git a/backend/tests/utils/screenshot.py b/backend/tests/utils/screenshot.py index 9d800f8..df89350 100644 --- a/backend/tests/utils/screenshot.py +++ b/backend/tests/utils/screenshot.py @@ -1,6 +1,6 @@ -from threading import Lock +from asyncio import Lock -from sqlmodel import Session +from sqlmodel.ext.asyncio.session import AsyncSession from src.game.model import GameScreenshot @@ -11,20 +11,19 @@ steam_file_id_lock = Lock() -def create_random_game_screenshot(session: Session, *, game_id: int | None = None) -> GameScreenshot: +async def create_random_game_screenshot(session: AsyncSession, *, game_id: int | None = None) -> GameScreenshot: global steam_file_id_counter if game_id is None: - game = create_random_game(session) + game = await create_random_game(session) assert game.id is not None game_id = game.id - with steam_file_id_lock: + async with steam_file_id_lock: steam_file_id_counter += 1 screenshot = GameScreenshot(steam_file_id=steam_file_id_counter, url=random_image_url(), game_id=game_id) session.add(screenshot) - session.commit() - session.refresh(screenshot) + await session.commit() return screenshot From ef28a4b318cab20fffaa6baf2ae2df17575af4d8 Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Tue, 9 Apr 2024 17:32:11 +0900 Subject: [PATCH 07/20] =?UTF-8?q?refactor:=20[AsyncSQLModel=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85]=20=ED=99=98=EA=B2=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.test.env | 3 +- backend/poetry.lock | 150 +++++++++++++++++++++++++---------------- backend/pyproject.toml | 4 +- backend/pytest.ini | 3 +- 4 files changed, 98 insertions(+), 62 deletions(-) diff --git a/backend/.test.env b/backend/.test.env index dd707cf..1462e4e 100644 --- a/backend/.test.env +++ b/backend/.test.env @@ -8,5 +8,4 @@ GOOGLE_OAUTH2_CLIENT_SECRET=ee FACEBOOK_OAUTH2_CLIENT_ID=ee FACEBOOK_OAUTH2_CLIENT_SECRET=ee -ENVIRONMENT=TEST -TEST_DATABASE_URL=sqlite:///test.db \ No newline at end of file +ENVIRONMENT=TEST \ No newline at end of file diff --git a/backend/poetry.lock b/backend/poetry.lock index dcb1198..dfebfe8 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -194,6 +194,22 @@ doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd- test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (<0.22)"] +[[package]] +name = "async-sqlmodel" +version = "0.1.3" +description = "" +optional = false +python-versions = "<4.0,>=3.7" +files = [ + {file = "async_sqlmodel-0.1.3-py3-none-any.whl", hash = "sha256:6ddca1acc9401fbd3a0f410dfa7ebe4810ab75dbe78bb4eb01fa1e2042b5b7d2"}, + {file = "async_sqlmodel-0.1.3.tar.gz", hash = "sha256:04cb0a43303a9634645f049a6cbc58cc345810d9ce7185a98967450330c41382"}, +] + +[package.dependencies] +greenlet = ">=3.0.3,<4.0.0" +sqlalchemy = ">=2.0.29,<3.0.0" +sqlmodel = ">=0.0.16,<0.0.17" + [[package]] name = "async-timeout" version = "4.0.3" @@ -1341,6 +1357,24 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.23.6" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"}, + {file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + [[package]] name = "pytest-dotenv" version = "0.5.2" @@ -1440,70 +1474,70 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.23" +version = "2.0.29" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-win32.whl", hash = "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-win_amd64.whl", hash = "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-win32.whl", hash = "sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-win_amd64.whl", hash = "sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-win32.whl", hash = "sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-win_amd64.whl", hash = "sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:14aebfe28b99f24f8a4c1346c48bc3d63705b1f919a24c27471136d2f219f02d"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e983fa42164577d073778d06d2cc5d020322425a509a08119bdcee70ad856bf"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e0dc9031baa46ad0dd5a269cb7a92a73284d1309228be1d5935dac8fb3cae24"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5f94aeb99f43729960638e7468d4688f6efccb837a858b34574e01143cf11f89"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:63bfc3acc970776036f6d1d0e65faa7473be9f3135d37a463c5eba5efcdb24c8"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-win32.whl", hash = "sha256:f48ed89dd11c3c586f45e9eec1e437b355b3b6f6884ea4a4c3111a3358fd0c18"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-win_amd64.whl", hash = "sha256:1e018aba8363adb0599e745af245306cb8c46b9ad0a6fc0a86745b6ff7d940fc"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-win32.whl", hash = "sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-win_amd64.whl", hash = "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-win32.whl", hash = "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-win_amd64.whl", hash = "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b"}, - {file = "SQLAlchemy-2.0.23-py3-none-any.whl", hash = "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d"}, - {file = "SQLAlchemy-2.0.23.tar.gz", hash = "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c142852ae192e9fe5aad5c350ea6befe9db14370b34047e1f0f7cf99e63c63b"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:99a1e69d4e26f71e750e9ad6fdc8614fbddb67cfe2173a3628a2566034e223c7"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef3fbccb4058355053c51b82fd3501a6e13dd808c8d8cd2561e610c5456013c"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d6753305936eddc8ed190e006b7bb33a8f50b9854823485eed3a886857ab8d1"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0f3ca96af060a5250a8ad5a63699180bc780c2edf8abf96c58af175921df847a"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c4520047006b1d3f0d89e0532978c0688219857eb2fee7c48052560ae76aca1e"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-win32.whl", hash = "sha256:b2a0e3cf0caac2085ff172c3faacd1e00c376e6884b5bc4dd5b6b84623e29e4f"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-win_amd64.whl", hash = "sha256:01d10638a37460616708062a40c7b55f73e4d35eaa146781c683e0fa7f6c43fb"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:308ef9cb41d099099fffc9d35781638986870b29f744382904bf9c7dadd08513"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:296195df68326a48385e7a96e877bc19aa210e485fa381c5246bc0234c36c78e"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a13b917b4ffe5a0a31b83d051d60477819ddf18276852ea68037a144a506efb9"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f6d971255d9ddbd3189e2e79d743ff4845c07f0633adfd1de3f63d930dbe673"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:61405ea2d563407d316c63a7b5271ae5d274a2a9fbcd01b0aa5503635699fa1e"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de7202ffe4d4a8c1e3cde1c03e01c1a3772c92858837e8f3879b497158e4cb44"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-win32.whl", hash = "sha256:b5d7ed79df55a731749ce65ec20d666d82b185fa4898430b17cb90c892741520"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-win_amd64.whl", hash = "sha256:205f5a2b39d7c380cbc3b5dcc8f2762fb5bcb716838e2d26ccbc54330775b003"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d96710d834a6fb31e21381c6d7b76ec729bd08c75a25a5184b1089141356171f"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:52de4736404e53c5c6a91ef2698c01e52333988ebdc218f14c833237a0804f1b"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c7b02525ede2a164c5fa5014915ba3591730f2cc831f5be9ff3b7fd3e30958e"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dfefdb3e54cd15f5d56fd5ae32f1da2d95d78319c1f6dfb9bcd0eb15d603d5d"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a88913000da9205b13f6f195f0813b6ffd8a0c0c2bd58d499e00a30eb508870c"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fecd5089c4be1bcc37c35e9aa678938d2888845a134dd016de457b942cf5a758"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-win32.whl", hash = "sha256:8197d6f7a3d2b468861ebb4c9f998b9df9e358d6e1cf9c2a01061cb9b6cf4e41"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-win_amd64.whl", hash = "sha256:9b19836ccca0d321e237560e475fd99c3d8655d03da80c845c4da20dda31b6e1"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:87a1d53a5382cdbbf4b7619f107cc862c1b0a4feb29000922db72e5a66a5ffc0"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0732dffe32333211801b28339d2a0babc1971bc90a983e3035e7b0d6f06b93"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90453597a753322d6aa770c5935887ab1fc49cc4c4fdd436901308383d698b4b"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ea311d4ee9a8fa67f139c088ae9f905fcf0277d6cd75c310a21a88bf85e130f5"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5f20cb0a63a3e0ec4e169aa8890e32b949c8145983afa13a708bc4b0a1f30e03"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-win32.whl", hash = "sha256:e5bbe55e8552019c6463709b39634a5fc55e080d0827e2a3a11e18eb73f5cdbd"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-win_amd64.whl", hash = "sha256:c2f9c762a2735600654c654bf48dad388b888f8ce387b095806480e6e4ff6907"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e614d7a25a43a9f54fcce4675c12761b248547f3d41b195e8010ca7297c369c"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:471fcb39c6adf37f820350c28aac4a7df9d3940c6548b624a642852e727ea586"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:988569c8732f54ad3234cf9c561364221a9e943b78dc7a4aaf35ccc2265f1930"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dddaae9b81c88083e6437de95c41e86823d150f4ee94bf24e158a4526cbead01"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:334184d1ab8f4c87f9652b048af3f7abea1c809dfe526fb0435348a6fef3d380"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:38b624e5cf02a69b113c8047cf7f66b5dfe4a2ca07ff8b8716da4f1b3ae81567"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-win32.whl", hash = "sha256:bab41acf151cd68bc2b466deae5deeb9e8ae9c50ad113444151ad965d5bf685b"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-win_amd64.whl", hash = "sha256:52c8011088305476691b8750c60e03b87910a123cfd9ad48576d6414b6ec2a1d"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3071ad498896907a5ef756206b9dc750f8e57352113c19272bdfdc429c7bd7de"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dba622396a3170974f81bad49aacebd243455ec3cc70615aeaef9e9613b5bca5"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b184e3de58009cc0bf32e20f137f1ec75a32470f5fede06c58f6c355ed42a72"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c37f1050feb91f3d6c32f864d8e114ff5545a4a7afe56778d76a9aec62638ba"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bda7ce59b06d0f09afe22c56714c65c957b1068dee3d5e74d743edec7daba552"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:25664e18bef6dc45015b08f99c63952a53a0a61f61f2e48a9e70cec27e55f699"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-win32.whl", hash = "sha256:77d29cb6c34b14af8a484e831ab530c0f7188f8efed1c6a833a2c674bf3c26ec"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-win_amd64.whl", hash = "sha256:04c487305ab035a9548f573763915189fc0fe0824d9ba28433196f8436f1449c"}, + {file = "SQLAlchemy-2.0.29-py3-none-any.whl", hash = "sha256:dc4ee2d4ee43251905f88637d5281a8d52e916a021384ec10758826f5cbae305"}, + {file = "SQLAlchemy-2.0.29.tar.gz", hash = "sha256:bd9566b8e58cabd700bc367b60e90d9349cd16f0984973f98a9a09f9c64e86f0"}, ] [package.dependencies] greenlet = {version = "!=0.4.17", optional = true, markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\" or extra == \"asyncio\""} -typing-extensions = ">=4.2.0" +typing-extensions = ">=4.6.0" [package.extras] aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] @@ -1513,7 +1547,7 @@ mssql-pyodbc = ["pyodbc"] mypy = ["mypy (>=0.910)"] mysql = ["mysqlclient (>=1.4.0)"] mysql-connector = ["mysql-connector-python"] -oracle = ["cx-oracle (>=8)"] +oracle = ["cx_oracle (>=8)"] oracle-oracledb = ["oracledb (>=1.0.1)"] postgresql = ["psycopg2 (>=2.7)"] postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] @@ -1523,17 +1557,17 @@ postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2cffi = ["psycopg2cffi"] postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3-binary"] +sqlcipher = ["sqlcipher3_binary"] [[package]] name = "sqlmodel" -version = "0.0.14" +version = "0.0.16" description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness." optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "sqlmodel-0.0.14-py3-none-any.whl", hash = "sha256:accea3ff5d878e41ac439b11e78613ed61ce300cfcb860e87a2d73d4884cbee4"}, - {file = "sqlmodel-0.0.14.tar.gz", hash = "sha256:0bff8fc94af86b44925aa813f56cf6aabdd7f156b73259f2f60692c6a64ac90e"}, + {file = "sqlmodel-0.0.16-py3-none-any.whl", hash = "sha256:b972f5d319580d6c37ecc417881f6ec4d1ad3ed3583d0ac0ed43234a28bf605a"}, + {file = "sqlmodel-0.0.16.tar.gz", hash = "sha256:966656f18a8e9a2d159eb215b07fb0cf5222acfae3362707ca611848a8a06bd1"}, ] [package.dependencies] @@ -1735,4 +1769,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "92e0aed8871ac1ecdf09ae2679b37a38715e486d9a242a7d2ad7b65e88e2a4c4" +content-hash = "155e12e480c4f12697cde15361f29c1accfe320d1abf59c029c89aa757c74951" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 0852220..598d218 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -11,7 +11,7 @@ fastapi = "^0.105.0" pydantic-settings = "^2.1.0" sqlalchemy = "^2.0.23" redis = "^5.0.1" -sqlmodel = "^0.0.14" +sqlmodel = "0.0.16" fastapi-restful = "^0.5.0" typing-inspect = "^0.9.0" elasticsearch = "7.13.4" @@ -22,6 +22,7 @@ uvicorn = "^0.25.0" fastapi-users = {extras = ["oauth", "sqlalchemy"], version = "^12.1.2"} fastapi-users-db-sqlmodel = "^0.3.0" aiohttp = "^3.9.3" +async-sqlmodel = "^0.1.3" [tool.poetry.group.dev.dependencies] @@ -30,6 +31,7 @@ faker = "^21.0.0" aiosqlite = "^0.19.0" pytest = "^7.4.4" alembic = "^1.13.1" +pytest-asyncio = "^0.23.6" [build-system] requires = ["poetry-core"] diff --git a/backend/pytest.ini b/backend/pytest.ini index 87a53d5..22949cb 100644 --- a/backend/pytest.ini +++ b/backend/pytest.ini @@ -1,4 +1,5 @@ [pytest] env_override_existing_values = 1 env_files = - .test.env \ No newline at end of file + .test.env +asyncio_mode=auto \ No newline at end of file From 756fbff8aabc21d860a6dc4abd7b37090a57d7d8 Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Tue, 9 Apr 2024 17:36:48 +0900 Subject: [PATCH 08/20] =?UTF-8?q?feat:=20=EA=B2=8C=EC=9E=84=20=ED=92=80?= =?UTF-8?q?=EC=9D=B4=20=EA=B8=B0=EB=A1=9D=20=EB=AA=A8=EB=8D=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/game/dependency.py | 14 +++++++++++--- backend/src/game/exception.py | 5 +++++ backend/src/game/model.py | 12 ++++++++++++ backend/src/game/repository.py | 22 ++++++++++++++++++++++ backend/src/game/service.py | 17 ++++++++++++++++- backend/src/quiz/dependency.py | 7 ++++++- backend/src/quiz/quiz_answer_service.py | 22 ++++++++++++++++------ backend/src/quiz/router.py | 1 + 8 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 backend/src/game/exception.py create mode 100644 backend/src/game/repository.py diff --git a/backend/src/game/dependency.py b/backend/src/game/dependency.py index 8255377..abe94aa 100644 --- a/backend/src/game/dependency.py +++ b/backend/src/game/dependency.py @@ -2,12 +2,20 @@ from fastapi import Depends -from ..dependency import ElasticSearchClientDep +from ..dependency import ElasticSearchClientDep, SessionDep +from .repository import SolvedGameRepository from .service import GameService -async def get_game_service(es_client: ElasticSearchClientDep) -> GameService: - return GameService(es_client) +async def get_solved_game_repository(session: SessionDep): + return SolvedGameRepository(session) + + +async def get_game_service( + es_client: ElasticSearchClientDep, solved_game_repository: "SolvedGameRepositoryDep" +) -> GameService: + return GameService(es_client=es_client, solved_game_repository=solved_game_repository) GameServiceDep = Annotated[GameService, Depends(get_game_service)] +SolvedGameRepositoryDep = Annotated[SolvedGameRepository, Depends(get_solved_game_repository)] diff --git a/backend/src/game/exception.py b/backend/src/game/exception.py new file mode 100644 index 0000000..e129318 --- /dev/null +++ b/backend/src/game/exception.py @@ -0,0 +1,5 @@ +from ..exception import BadRequestError + + +class GameAlreadySolvedError(BadRequestError): + DETAIL = "Game Already Solved" diff --git a/backend/src/game/model.py b/backend/src/game/model.py index 8d58e46..3f678b9 100644 --- a/backend/src/game/model.py +++ b/backend/src/game/model.py @@ -68,3 +68,15 @@ class GameScreenshot(CreatedAtMixin, UpdatedAtMixin, AsyncSQLModel, table=True): game: Game = Relationship() awt_game: Awaitable[Game] = AwaitableField(field="game") + +# Solved + + +class SolvedGame(CreatedAtMixin, UpdatedAtMixin, SQLModel, table=True): + __tablename__: str = "solved_game" + __table_args__ = (UniqueConstraint("user_id", "game_id"),) + + id: int | None = Field(default=None, primary_key=True) + + game_id: int = Field(foreign_key="game.id") + user_id: int = Field(foreign_key="user.id") diff --git a/backend/src/game/repository.py b/backend/src/game/repository.py new file mode 100644 index 0000000..e454792 --- /dev/null +++ b/backend/src/game/repository.py @@ -0,0 +1,22 @@ +from sqlmodel import select +from sqlmodel.ext.asyncio.session import AsyncSession + +from ..repository import CRUDMixin, IRepository +from .model import SolvedGame + + +class SolvedGameRepository(IRepository[SolvedGame], CRUDMixin): + model = SolvedGame + + def __init__(self, session: AsyncSession) -> None: + self._session = session + + async def get_by_user_and_game(self, *, user_id: int, game_id: int) -> SolvedGame: + stmt = select(SolvedGame).where(SolvedGame.user_id == user_id, SolvedGame.game_id == game_id) + rs = await self._session.exec(stmt) + return rs.one() + + async def exists_by_user_and_game(self, *, user_id: int, game_id: int) -> bool: + stmt = select(SolvedGame).where(SolvedGame.user_id == user_id, SolvedGame.game_id == game_id) + rs = await self._session.exec(stmt) + return rs.one_or_none() is not None diff --git a/backend/src/game/service.py b/backend/src/game/service.py index 2cf71b8..00f8c8b 100644 --- a/backend/src/game/service.py +++ b/backend/src/game/service.py @@ -1,12 +1,16 @@ from elasticsearch import AsyncElasticsearch from ..es import GAME_INDEX +from .exception import GameAlreadySolvedError +from .model import SolvedGame +from .repository import SolvedGameRepository from .schema import AutoCompleteName class GameService: - def __init__(self, es_client: AsyncElasticsearch) -> None: + def __init__(self, *, es_client: AsyncElasticsearch, solved_game_repository: SolvedGameRepository) -> None: self._es_client = es_client + self._solved_game_repo = solved_game_repository async def auto_complete_name(self, query: str) -> list[AutoCompleteName]: auto_complete_names: list[AutoCompleteName] = [] @@ -42,3 +46,14 @@ async def auto_complete_name(self, query: str) -> list[AutoCompleteName]: auto_complete_names.append(AutoCompleteName(name=name, match=match)) return auto_complete_names + + async def solve_game(self, *, user_id: int, game_id: int): + await self._validate_not_solved(user_id=user_id, game_id=game_id) + + solved_game = SolvedGame(user_id=user_id, game_id=game_id) + await self._solved_game_repo.create(model=solved_game) + + async def _validate_not_solved(self, *, user_id: int, game_id: int): + solved = await self._solved_game_repo.exists_by_user_and_game(user_id=user_id, game_id=game_id) + if solved: + raise GameAlreadySolvedError diff --git a/backend/src/quiz/dependency.py b/backend/src/quiz/dependency.py index df9b395..cc3712e 100644 --- a/backend/src/quiz/dependency.py +++ b/backend/src/quiz/dependency.py @@ -3,6 +3,7 @@ from fastapi import Depends from ..dependency import SessionDep +from ..game.dependency import GameServiceDep from .daily_quiz_loader import DailyQuizLoader from .quiz_answer_service import QuizAnswerService from .quiz_service import QuizService @@ -48,9 +49,13 @@ async def get_quiz_answer_service( quiz_repository: QuizRepositoryDep, quiz_answer_repository: QuizAnswerRepositoryDep, quiz_validator: QuizValidatorDep, + game_service: GameServiceDep, ) -> QuizAnswerService: return QuizAnswerService( - quiz_repository=quiz_repository, quiz_answer_repository=quiz_answer_repository, quiz_validator=quiz_validator + quiz_repository=quiz_repository, + quiz_answer_repository=quiz_answer_repository, + quiz_validator=quiz_validator, + game_service=game_service, ) diff --git a/backend/src/quiz/quiz_answer_service.py b/backend/src/quiz/quiz_answer_service.py index dd1132b..53ecb57 100644 --- a/backend/src/quiz/quiz_answer_service.py +++ b/backend/src/quiz/quiz_answer_service.py @@ -1,8 +1,7 @@ from collections.abc import Sequence -from quiz.model import Quiz - -from .model import QuizAnswer +from ..game.service import GameService +from .model import Quiz, QuizAnswer from .quiz_validator import QuizValidator from .repository import QuizAnswerRepository, QuizRepository @@ -14,14 +13,18 @@ def __init__( quiz_repository: QuizRepository, quiz_answer_repository: QuizAnswerRepository, quiz_validator: QuizValidator, + game_service: GameService ) -> None: self._quiz_repo = quiz_repository self._quiz_answer_repo = quiz_answer_repository self._quiz_validator = quiz_validator + self._game_service = game_service async def submit_answer(self, *, quiz_id: int, user_id: int, answer: str) -> bool: """퀴즈에 대한 정답 여부를 반환하는 함수""" - correct_answer = await self._get_correct_answer(quiz_id=quiz_id) + quiz = await self._validate_quiz(quiz_id=quiz_id) + + correct_answer = await self._get_correct_answer(quiz=quiz) quiz_answers = await self._quiz_answer_repo.get_by_quiz_id_and_user_id(quiz_id=quiz_id, user_id=user_id) self._quiz_validator.validate_quiz_not_completed(answers=quiz_answers) @@ -29,17 +32,24 @@ async def submit_answer(self, *, quiz_id: int, user_id: int, answer: str) -> boo quiz_submit = QuizAnswer(answer=answer, correct=correct, quiz_id=quiz_id, user_id=user_id) await self._quiz_answer_repo.create(model=quiz_submit) + if correct: + await self._on_correct_answer(quiz=quiz, user_id=user_id) + return correct async def get_quiz_answer(self, *, quiz_id: int, user_id: int) -> Sequence[QuizAnswer]: await self._validate_quiz(quiz_id=quiz_id) return await self._quiz_answer_repo.get_by_quiz_id_and_user_id(quiz_id=quiz_id, user_id=user_id) + async def _on_correct_answer(self, *, quiz: Quiz, user_id: int): + game = await quiz.get_game() + assert game.id is not None + await self._game_service.solve_game(user_id=user_id, game_id=game.id) + async def _validate_quiz(self, *, quiz_id: int) -> Quiz: quiz = await self._quiz_repo.get(id=quiz_id) return self._quiz_validator.validate_quiz_existed(quiz=quiz) - async def _get_correct_answer(self, *, quiz_id: int) -> str: - quiz = await self._validate_quiz(quiz_id=quiz_id) + async def _get_correct_answer(self, *, quiz: Quiz) -> str: game = await quiz.get_game() return game.name diff --git a/backend/src/quiz/router.py b/backend/src/quiz/router.py index d036fa2..067eb00 100644 --- a/backend/src/quiz/router.py +++ b/backend/src/quiz/router.py @@ -38,6 +38,7 @@ async def submit_answer( correct = await self.quiz_answer_service.submit_answer( quiz_id=quiz_submit_req.quiz_id, user_id=current_user.id, answer=quiz_submit_req.answer ) + return SubmitAnswerResponse(correct=correct) @router.get("/quiz/answer") From b6bccefa4c76db39c468f3acbfe98d6ba010d338 Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Tue, 9 Apr 2024 18:34:49 +0900 Subject: [PATCH 09/20] =?UTF-8?q?feat:=20game=5Fsolved=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=B6=94=EA=B0=80=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...db66457957a9_0009_add_game_solved_table.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 backend/migration/versions/db66457957a9_0009_add_game_solved_table.py diff --git a/backend/migration/versions/db66457957a9_0009_add_game_solved_table.py b/backend/migration/versions/db66457957a9_0009_add_game_solved_table.py new file mode 100644 index 0000000..6cc864d --- /dev/null +++ b/backend/migration/versions/db66457957a9_0009_add_game_solved_table.py @@ -0,0 +1,41 @@ +"""0009-ADD-GAME-SOLVED-TABLE + +Revision ID: db66457957a9 +Revises: 07a7ec136a04 +Create Date: 2024-04-09 17:43:04.298908 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +# revision identifiers, used by Alembic. +revision: str = 'db66457957a9' +down_revision: Union[str, None] = '07a7ec136a04' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('solved_game', + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('game_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['game_id'], ['game.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id', 'game_id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('solved_game') + # ### end Alembic commands ### From faff4a797e9167d97220d70a4983663d2b7b5c6d Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Tue, 9 Apr 2024 22:21:47 +0900 Subject: [PATCH 10/20] =?UTF-8?q?test:=20solved=5Fgame=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/quiz/test_submit_answer.py | 29 +++++++++++++++++++ backend/tests/utils/game.py | 9 +++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/backend/tests/integration/quiz/test_submit_answer.py b/backend/tests/integration/quiz/test_submit_answer.py index b248511..8ca03ba 100644 --- a/backend/tests/integration/quiz/test_submit_answer.py +++ b/backend/tests/integration/quiz/test_submit_answer.py @@ -4,6 +4,7 @@ from src.auth.model import User from src.config import settings +from tests.utils.game import get_solved_game from tests.utils.quiz import create_random_quiz, create_random_quiz_answer, get_quiz_answer @@ -24,6 +25,12 @@ async def test_post_submit_true_answer(client: AsyncClient, session: AsyncSessio assert submit.correct is True assert submit.user_id == current_user.id + game = await saved_quiz.get_game() + assert current_user.id is not None + assert game.id is not None + solved_game = await get_solved_game(session, user_id=current_user.id, game_id=game.id) + assert solved_game is not None + async def test_post_submit_false_answer(client: AsyncClient, session: AsyncSession, current_user: User): saved_quiz = await create_random_quiz(session) @@ -40,6 +47,12 @@ async def test_post_submit_false_answer(client: AsyncClient, session: AsyncSessi assert submit.correct is False assert submit.user_id == current_user.id + game = await saved_quiz.get_game() + assert current_user.id is not None + assert game.id is not None + solved_game = await get_solved_game(session, user_id=current_user.id, game_id=game.id) + assert solved_game is None + async def test_post_submit_answer_with_not_existed_quiz_id(client: AsyncClient, current_user: User): res = await client.post("/quiz/submit_answer", json={"quiz_id": -1, "answer": "아무거나 빙빙바리바리구"}) @@ -72,3 +85,19 @@ async def test_post_submit_answer_with_prior_correct_answer( res = await client.post("/quiz/submit_answer", json={"quiz_id": quiz.id, "answer": "아무거나 방방빙방"}) assert res.status_code == status.HTTP_400_BAD_REQUEST + + +async def test_post_submit_multiple_quizzes_for_same_game( + client: AsyncClient, session: AsyncSession, current_user: User +): + quiz1 = await create_random_quiz(session) + quiz2 = await create_random_quiz(session, screenshots=await quiz1.awt_screenshots) + game = await quiz1.get_game() + + res1 = await client.post("/quiz/submit_answer", json={"quiz_id": quiz1.id, "answer": game.name}) + assert res1.status_code == status.HTTP_200_OK + assert res1.json()["correct"] is True + + res2 = await client.post("/quiz/submit_answer", json={"quiz_id": quiz2.id, "answer": game.name}) + assert res2.status_code == status.HTTP_200_OK + assert res1.json()["correct"] is True diff --git a/backend/tests/utils/game.py b/backend/tests/utils/game.py index 9f2e999..71e6b33 100644 --- a/backend/tests/utils/game.py +++ b/backend/tests/utils/game.py @@ -2,10 +2,11 @@ from datetime import datetime from elasticsearch import AsyncElasticsearch +from sqlmodel import select from sqlmodel.ext.asyncio.session import AsyncSession from src.es import GAME_INDEX -from src.game.model import Game, GameAlias +from src.game.model import Game, GameAlias, SolvedGame from .utils import random_datetime, random_name @@ -38,6 +39,12 @@ async def create_random_game( return game +async def get_solved_game(session: AsyncSession, *, user_id: int, game_id: int) -> SolvedGame | None: + stmt = select(SolvedGame).where(SolvedGame.user_id == user_id, SolvedGame.game_id == game_id) + rs = await session.exec(stmt) + return rs.one_or_none() + + async def index_game(es_client: AsyncElasticsearch, game: Game): q_name = "".join([c if c.isalnum() else " " for c in game.name]) await es_client.index( From 359f8ad1f4ee55cedcf979fa054da3c05b59046f Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Tue, 9 Apr 2024 22:26:21 +0900 Subject: [PATCH 11/20] =?UTF-8?q?fix:=20=EA=B0=99=EC=9D=80=20=EA=B2=8C?= =?UTF-8?q?=EC=9E=84=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=97=AC=EB=9F=AC?= =?UTF-8?q?=EA=B0=9C=EC=9D=98=20=ED=80=B4=EC=A6=88=EB=A5=BC=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=9C=EC=8B=9C=20=EC=A0=95=EB=8B=B5=20=EA=B8=B0=EB=A1=9D?= =?UTF-8?q?=EC=9D=80=20=ED=95=9C=EB=B2=88=EB=A7=8C(=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=EC=8B=9C=20OR=20=EC=97=B0=EC=82=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/game/service.py | 15 +++++++++------ backend/src/quiz/quiz_answer_service.py | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/backend/src/game/service.py b/backend/src/game/service.py index 00f8c8b..887c8d4 100644 --- a/backend/src/game/service.py +++ b/backend/src/game/service.py @@ -47,13 +47,16 @@ async def auto_complete_name(self, query: str) -> list[AutoCompleteName]: return auto_complete_names - async def solve_game(self, *, user_id: int, game_id: int): - await self._validate_not_solved(user_id=user_id, game_id=game_id) + async def on_correct_answer(self, *, game_id: int, user_id: int): + exists = await self._solved_game_repo.exists_by_user_and_game(user_id=user_id, game_id=game_id) + if not exists: + await self._solve_game(user_id=user_id, game_id=game_id) + async def _solve_game(self, *, user_id: int, game_id: int): solved_game = SolvedGame(user_id=user_id, game_id=game_id) await self._solved_game_repo.create(model=solved_game) - async def _validate_not_solved(self, *, user_id: int, game_id: int): - solved = await self._solved_game_repo.exists_by_user_and_game(user_id=user_id, game_id=game_id) - if solved: - raise GameAlreadySolvedError + # async def _validate_not_solved(self, *, user_id: int, game_id: int): + # solved = await self._solved_game_repo.exists_by_user_and_game(user_id=user_id, game_id=game_id) + # if solved: + # raise GameAlreadySolvedError diff --git a/backend/src/quiz/quiz_answer_service.py b/backend/src/quiz/quiz_answer_service.py index 53ecb57..20c2942 100644 --- a/backend/src/quiz/quiz_answer_service.py +++ b/backend/src/quiz/quiz_answer_service.py @@ -44,7 +44,7 @@ async def get_quiz_answer(self, *, quiz_id: int, user_id: int) -> Sequence[QuizA async def _on_correct_answer(self, *, quiz: Quiz, user_id: int): game = await quiz.get_game() assert game.id is not None - await self._game_service.solve_game(user_id=user_id, game_id=game.id) + await self._game_service.on_correct_answer(user_id=user_id, game_id=game.id) async def _validate_quiz(self, *, quiz_id: int) -> Quiz: quiz = await self._quiz_repo.get(id=quiz_id) From 959883fb00db3a040cc1d9d3124e09e3767b17c2 Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Wed, 10 Apr 2024 18:54:35 +0900 Subject: [PATCH 12/20] =?UTF-8?q?refactor:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20import=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/game/service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/game/service.py b/backend/src/game/service.py index 887c8d4..0fe907b 100644 --- a/backend/src/game/service.py +++ b/backend/src/game/service.py @@ -1,7 +1,6 @@ from elasticsearch import AsyncElasticsearch from ..es import GAME_INDEX -from .exception import GameAlreadySolvedError from .model import SolvedGame from .repository import SolvedGameRepository from .schema import AutoCompleteName From b32c8bb00cb859f3392fea5185a5e950e812bba5 Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Thu, 11 Apr 2024 16:41:43 +0900 Subject: [PATCH 13/20] =?UTF-8?q?refactor:=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD=20(=ED=98=B8=EC=B6=9C?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EC=83=81=ED=99=A9=EB=B3=B4=EB=8B=A4=20?= =?UTF-8?q?=ED=96=89=EB=8F=99=EC=97=90=20=EB=8D=94=20=EC=A7=91=EC=A4=91?= =?UTF-8?q?=ED=95=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/game/service.py | 14 +++----------- backend/src/quiz/quiz_answer_service.py | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/backend/src/game/service.py b/backend/src/game/service.py index 0fe907b..b16c8c9 100644 --- a/backend/src/game/service.py +++ b/backend/src/game/service.py @@ -46,16 +46,8 @@ async def auto_complete_name(self, query: str) -> list[AutoCompleteName]: return auto_complete_names - async def on_correct_answer(self, *, game_id: int, user_id: int): + async def solve_game(self, *, game_id: int, user_id: int): exists = await self._solved_game_repo.exists_by_user_and_game(user_id=user_id, game_id=game_id) if not exists: - await self._solve_game(user_id=user_id, game_id=game_id) - - async def _solve_game(self, *, user_id: int, game_id: int): - solved_game = SolvedGame(user_id=user_id, game_id=game_id) - await self._solved_game_repo.create(model=solved_game) - - # async def _validate_not_solved(self, *, user_id: int, game_id: int): - # solved = await self._solved_game_repo.exists_by_user_and_game(user_id=user_id, game_id=game_id) - # if solved: - # raise GameAlreadySolvedError + solved_game = SolvedGame(user_id=user_id, game_id=game_id) + await self._solved_game_repo.create(model=solved_game) diff --git a/backend/src/quiz/quiz_answer_service.py b/backend/src/quiz/quiz_answer_service.py index 20c2942..53ecb57 100644 --- a/backend/src/quiz/quiz_answer_service.py +++ b/backend/src/quiz/quiz_answer_service.py @@ -44,7 +44,7 @@ async def get_quiz_answer(self, *, quiz_id: int, user_id: int) -> Sequence[QuizA async def _on_correct_answer(self, *, quiz: Quiz, user_id: int): game = await quiz.get_game() assert game.id is not None - await self._game_service.on_correct_answer(user_id=user_id, game_id=game.id) + await self._game_service.solve_game(user_id=user_id, game_id=game.id) async def _validate_quiz(self, *, quiz_id: int) -> Quiz: quiz = await self._quiz_repo.get(id=quiz_id) From bb38dfeae755cd927aee7690dc1acdbf75063bd0 Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Thu, 11 Apr 2024 18:20:24 +0900 Subject: [PATCH 14/20] =?UTF-8?q?feat:=20=EC=A0=90=EC=88=98=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/auth/dependency.py | 12 ++++++- backend/src/auth/repository.py | 11 +++++++ backend/src/game/dependency.py | 8 ++++- backend/src/game/manager.py | 13 ++++++++ backend/src/game/service.py | 8 ----- backend/src/quiz/dependency.py | 43 ++++++++++++++---------- backend/src/quiz/quiz_answer_service.py | 44 +++++++++++++++++-------- backend/src/quiz/quiz_manager.py | 22 +++++++++++++ backend/src/quiz/quiz_validator.py | 24 ++++---------- backend/src/quiz/router.py | 8 ++--- backend/src/rank_score/dependency.py | 12 +++++++ backend/src/rank_score/manager.py | 29 ++++++++++++++++ backend/src/repository.py | 1 + 13 files changed, 173 insertions(+), 62 deletions(-) create mode 100644 backend/src/auth/repository.py create mode 100644 backend/src/game/manager.py create mode 100644 backend/src/quiz/quiz_manager.py create mode 100644 backend/src/rank_score/dependency.py create mode 100644 backend/src/rank_score/manager.py diff --git a/backend/src/auth/dependency.py b/backend/src/auth/dependency.py index 054c3b6..65c8b97 100644 --- a/backend/src/auth/dependency.py +++ b/backend/src/auth/dependency.py @@ -2,9 +2,19 @@ from fastapi import Depends +from ..dependency import SessionDep +from .model import User +from .repository import UserRepository from .schema import UserRead from .user import fastapi_users current_active_user = fastapi_users.current_user(active=True) -CURRENT_USER_DEP = Annotated[UserRead, Depends(current_active_user)] + +async def get_user_repository(session: SessionDep) -> UserRepository: + return UserRepository(session=session) + + +UserRepositoryDep = Annotated[UserRepository, Depends(get_user_repository)] +CURRENT_USER_DEP = Annotated[User, Depends(current_active_user)] +CURRENT_READ_USER_DEP = Annotated[UserRead, Depends(current_active_user)] diff --git a/backend/src/auth/repository.py b/backend/src/auth/repository.py new file mode 100644 index 0000000..cba1e44 --- /dev/null +++ b/backend/src/auth/repository.py @@ -0,0 +1,11 @@ +from sqlmodel.ext.asyncio.session import AsyncSession + +from ..repository import CRUDMixin, IRepository +from .model import User + + +class UserRepository(IRepository[User], CRUDMixin): + model = User + + def __init__(self, session: AsyncSession) -> None: + self._session = session diff --git a/backend/src/game/dependency.py b/backend/src/game/dependency.py index abe94aa..36c7615 100644 --- a/backend/src/game/dependency.py +++ b/backend/src/game/dependency.py @@ -3,6 +3,7 @@ from fastapi import Depends from ..dependency import ElasticSearchClientDep, SessionDep +from .manager import GameManager from .repository import SolvedGameRepository from .service import GameService @@ -11,11 +12,16 @@ async def get_solved_game_repository(session: SessionDep): return SolvedGameRepository(session) +async def get_game_manager(solved_game_repository: "SolvedGameRepositoryDep") -> GameManager: + return GameManager(solved_game_repository=solved_game_repository) + + async def get_game_service( es_client: ElasticSearchClientDep, solved_game_repository: "SolvedGameRepositoryDep" ) -> GameService: return GameService(es_client=es_client, solved_game_repository=solved_game_repository) -GameServiceDep = Annotated[GameService, Depends(get_game_service)] SolvedGameRepositoryDep = Annotated[SolvedGameRepository, Depends(get_solved_game_repository)] +GameManagerDep = Annotated[GameManager, Depends(get_game_manager)] +GameServiceDep = Annotated[GameService, Depends(get_game_service)] diff --git a/backend/src/game/manager.py b/backend/src/game/manager.py new file mode 100644 index 0000000..5455cef --- /dev/null +++ b/backend/src/game/manager.py @@ -0,0 +1,13 @@ +from .model import SolvedGame +from .repository import SolvedGameRepository + + +class GameManager: + def __init__(self, *, solved_game_repository: SolvedGameRepository) -> None: + self._solved_game_repo = solved_game_repository + + async def solve_game(self, *, game_id: int, user_id: int): + exists = await self._solved_game_repo.exists_by_user_and_game(user_id=user_id, game_id=game_id) + if not exists: + solved_game = SolvedGame(user_id=user_id, game_id=game_id) + await self._solved_game_repo.create(model=solved_game) diff --git a/backend/src/game/service.py b/backend/src/game/service.py index b16c8c9..ee98ec8 100644 --- a/backend/src/game/service.py +++ b/backend/src/game/service.py @@ -1,7 +1,6 @@ from elasticsearch import AsyncElasticsearch from ..es import GAME_INDEX -from .model import SolvedGame from .repository import SolvedGameRepository from .schema import AutoCompleteName @@ -9,7 +8,6 @@ class GameService: def __init__(self, *, es_client: AsyncElasticsearch, solved_game_repository: SolvedGameRepository) -> None: self._es_client = es_client - self._solved_game_repo = solved_game_repository async def auto_complete_name(self, query: str) -> list[AutoCompleteName]: auto_complete_names: list[AutoCompleteName] = [] @@ -45,9 +43,3 @@ async def auto_complete_name(self, query: str) -> list[AutoCompleteName]: auto_complete_names.append(AutoCompleteName(name=name, match=match)) return auto_complete_names - - async def solve_game(self, *, game_id: int, user_id: int): - exists = await self._solved_game_repo.exists_by_user_and_game(user_id=user_id, game_id=game_id) - if not exists: - solved_game = SolvedGame(user_id=user_id, game_id=game_id) - await self._solved_game_repo.create(model=solved_game) diff --git a/backend/src/quiz/dependency.py b/backend/src/quiz/dependency.py index cc3712e..8c4e048 100644 --- a/backend/src/quiz/dependency.py +++ b/backend/src/quiz/dependency.py @@ -3,9 +3,11 @@ from fastapi import Depends from ..dependency import SessionDep -from ..game.dependency import GameServiceDep +from ..game.dependency import GameManagerDep +from ..rank_score.dependency import RankScoreManagerDep from .daily_quiz_loader import DailyQuizLoader from .quiz_answer_service import QuizAnswerService +from .quiz_manager import QuizManager from .quiz_service import QuizService from .quiz_validator import QuizValidator from .repository import DailyQuizRepository, QuizAnswerRepository, QuizRepository @@ -23,22 +25,18 @@ async def get_daily_quiz_repository(session: SessionDep) -> DailyQuizRepository: return DailyQuizRepository(session) -QuizRepositoryDep = Annotated[QuizRepository, Depends(get_quiz_repository)] -QuizAnswerRepositoryDep = Annotated[QuizAnswerRepository, Depends(get_quiz_answer_repository)] -DailyQuizRepositoryDep = Annotated[DailyQuizRepository, Depends(get_daily_quiz_repository)] - - -async def get_quiz_validator() -> QuizValidator: - return QuizValidator() +async def get_quiz_manager() -> QuizManager: + return QuizManager() -QuizValidatorDep = Annotated[QuizValidator, Depends(get_quiz_validator)] +async def get_quiz_validator(quiz_manager: "QuizManagerDep") -> QuizValidator: + return QuizValidator(quiz_manager=quiz_manager) async def get_quiz_service( - quiz_repository: QuizRepositoryDep, - quiz_answer_repository: QuizAnswerRepositoryDep, - quiz_validator: QuizValidatorDep, + quiz_repository: "QuizRepositoryDep", + quiz_answer_repository: "QuizAnswerRepositoryDep", + quiz_validator: "QuizValidatorDep", ) -> QuizService: return QuizService( quiz_repository=quiz_repository, quiz_answer_repository=quiz_answer_repository, quiz_validator=quiz_validator @@ -46,27 +44,36 @@ async def get_quiz_service( async def get_quiz_answer_service( - quiz_repository: QuizRepositoryDep, - quiz_answer_repository: QuizAnswerRepositoryDep, - quiz_validator: QuizValidatorDep, - game_service: GameServiceDep, + quiz_repository: "QuizRepositoryDep", + quiz_answer_repository: "QuizAnswerRepositoryDep", + quiz_validator: "QuizValidatorDep", + game_manager: GameManagerDep, + quiz_manager: "QuizManagerDep", + rank_score_manager: RankScoreManagerDep, ) -> QuizAnswerService: return QuizAnswerService( quiz_repository=quiz_repository, quiz_answer_repository=quiz_answer_repository, quiz_validator=quiz_validator, - game_service=game_service, + game_manager=game_manager, + quiz_manager=quiz_manager, + rank_score_manager=rank_score_manager, ) async def get_daily_quiz_loader( - daily_quiz_repository: DailyQuizRepositoryDep, + daily_quiz_repository: "DailyQuizRepositoryDep", ): return DailyQuizLoader( daily_quiz_repository=daily_quiz_repository, ) +QuizRepositoryDep = Annotated[QuizRepository, Depends(get_quiz_repository)] +QuizAnswerRepositoryDep = Annotated[QuizAnswerRepository, Depends(get_quiz_answer_repository)] +DailyQuizRepositoryDep = Annotated[DailyQuizRepository, Depends(get_daily_quiz_repository)] +QuizManagerDep = Annotated[QuizManager, Depends(get_quiz_manager)] +QuizValidatorDep = Annotated[QuizValidator, Depends(get_quiz_validator)] QuizServiceDep = Annotated[QuizService, Depends(get_quiz_service)] QuizAnswerServiceDep = Annotated[QuizAnswerService, Depends(get_quiz_answer_service)] DailyQuizLoaderDep = Annotated[DailyQuizLoader, Depends(get_daily_quiz_loader)] diff --git a/backend/src/quiz/quiz_answer_service.py b/backend/src/quiz/quiz_answer_service.py index 53ecb57..b26afdc 100644 --- a/backend/src/quiz/quiz_answer_service.py +++ b/backend/src/quiz/quiz_answer_service.py @@ -1,7 +1,10 @@ from collections.abc import Sequence -from ..game.service import GameService +from ..auth.model import User +from ..game.manager import GameManager +from ..rank_score.manager import RankScoreManager from .model import Quiz, QuizAnswer +from .quiz_manager import QuizManager from .quiz_validator import QuizValidator from .repository import QuizAnswerRepository, QuizRepository @@ -13,39 +16,54 @@ def __init__( quiz_repository: QuizRepository, quiz_answer_repository: QuizAnswerRepository, quiz_validator: QuizValidator, - game_service: GameService + game_manager: GameManager, + quiz_manager: QuizManager, + rank_score_manager: RankScoreManager, ) -> None: self._quiz_repo = quiz_repository self._quiz_answer_repo = quiz_answer_repository self._quiz_validator = quiz_validator - self._game_service = game_service + self._game_manager = game_manager + self._quiz_manager = quiz_manager + self._rank_score_manager = rank_score_manager - async def submit_answer(self, *, quiz_id: int, user_id: int, answer: str) -> bool: + async def submit_answer(self, *, quiz_id: int, user: User, answer: str) -> bool: """퀴즈에 대한 정답 여부를 반환하는 함수""" + assert user.id is not None + quiz = await self._validate_quiz(quiz_id=quiz_id) correct_answer = await self._get_correct_answer(quiz=quiz) - quiz_answers = await self._quiz_answer_repo.get_by_quiz_id_and_user_id(quiz_id=quiz_id, user_id=user_id) + quiz_answers = await self._quiz_answer_repo.get_by_quiz_id_and_user_id(quiz_id=quiz_id, user_id=user.id) self._quiz_validator.validate_quiz_not_completed(answers=quiz_answers) correct = correct_answer == answer - quiz_submit = QuizAnswer(answer=answer, correct=correct, quiz_id=quiz_id, user_id=user_id) + quiz_submit = QuizAnswer(answer=answer, correct=correct, quiz_id=quiz_id, user_id=user.id) await self._quiz_answer_repo.create(model=quiz_submit) - if correct: - await self._on_correct_answer(quiz=quiz, user_id=user_id) + answers = [*quiz_answers, quiz_submit] + if self._quiz_manager.is_quiz_completed(answers=answers): + await self._on_quiz_completed(quiz=quiz, user=user, answers=answers) return correct + async def _on_quiz_completed(self, *, quiz: Quiz, user: User, answers: Sequence[QuizAnswer]): + has_solved_game = False + success = self._quiz_manager.is_quiz_success(answers=answers) + + if success: + game = await quiz.get_game() + assert game.id is not None + assert user.id is not None + await self._game_manager.solve_game(user_id=user.id, game_id=game.id) + has_solved_game = True + + self._rank_score_manager.update_score(user=user, has_solved_game=has_solved_game, is_quiz_success=success) + async def get_quiz_answer(self, *, quiz_id: int, user_id: int) -> Sequence[QuizAnswer]: await self._validate_quiz(quiz_id=quiz_id) return await self._quiz_answer_repo.get_by_quiz_id_and_user_id(quiz_id=quiz_id, user_id=user_id) - async def _on_correct_answer(self, *, quiz: Quiz, user_id: int): - game = await quiz.get_game() - assert game.id is not None - await self._game_service.solve_game(user_id=user_id, game_id=game.id) - async def _validate_quiz(self, *, quiz_id: int) -> Quiz: quiz = await self._quiz_repo.get(id=quiz_id) return self._quiz_validator.validate_quiz_existed(quiz=quiz) diff --git a/backend/src/quiz/quiz_manager.py b/backend/src/quiz/quiz_manager.py new file mode 100644 index 0000000..b3d2366 --- /dev/null +++ b/backend/src/quiz/quiz_manager.py @@ -0,0 +1,22 @@ +from collections.abc import Sequence +from typing import Protocol + +from ..config import settings + + +class IQuizAnswer(Protocol): + correct: bool + + +class QuizManager: + def is_quiz_completed(self, *, answers: Sequence[IQuizAnswer]) -> bool: + return self._has_correct_answer(answers=answers) or self._is_submission_limit_reached(answers=answers) + + def is_quiz_success(self, *, answers: Sequence[IQuizAnswer]): + return self._has_correct_answer(answers=answers) + + def _has_correct_answer(self, *, answers: Sequence[IQuizAnswer]) -> bool: + return any(answer.correct for answer in answers) + + def _is_submission_limit_reached(self, *, answers: Sequence[IQuizAnswer]) -> bool: + return len(answers) >= settings.QUIZ_ANSWER_SUBMISSION_LIMIT diff --git a/backend/src/quiz/quiz_validator.py b/backend/src/quiz/quiz_validator.py index 24c793c..06fc164 100644 --- a/backend/src/quiz/quiz_validator.py +++ b/backend/src/quiz/quiz_validator.py @@ -1,23 +1,22 @@ from collections.abc import Sequence -from typing import Optional, Protocol +from typing import Optional -from ..config import settings from .exception import QuizAlreadyCompletedError, QuizNotCompletedError, QuizNotFoundError from .model import Quiz - - -class IQuizAnswer(Protocol): - correct: bool +from .quiz_manager import IQuizAnswer, QuizManager class QuizValidator: + def __init__(self, *, quiz_manager: QuizManager) -> None: + self._quiz_manager = quiz_manager + def validate_quiz_not_completed(self, *, answers: Sequence[IQuizAnswer]): - if self._is_quiz_completed(answers=answers): + if self._quiz_manager.is_quiz_completed(answers=answers): raise QuizAlreadyCompletedError def validate_quiz_completed(self, *, answers: Sequence[IQuizAnswer]): - if not self._is_quiz_completed(answers=answers): + if not self._quiz_manager.is_quiz_completed(answers=answers): raise QuizNotCompletedError def validate_quiz_existed(self, *, quiz: Optional[Quiz]) -> Quiz: @@ -25,12 +24,3 @@ def validate_quiz_existed(self, *, quiz: Optional[Quiz]) -> Quiz: raise QuizNotFoundError return quiz - - def _is_quiz_completed(self, *, answers: Sequence[IQuizAnswer]) -> bool: - return self._has_correct_answer(answers=answers) or self._is_submission_limit_reached(answers=answers) - - def _has_correct_answer(self, *, answers: Sequence[IQuizAnswer]) -> bool: - return any(answer.correct for answer in answers) - - def _is_submission_limit_reached(self, *, answers: Sequence[IQuizAnswer]) -> bool: - return len(answers) >= settings.QUIZ_ANSWER_SUBMISSION_LIMIT diff --git a/backend/src/quiz/router.py b/backend/src/quiz/router.py index 067eb00..b6df7ec 100644 --- a/backend/src/quiz/router.py +++ b/backend/src/quiz/router.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Depends from fastapi_restful.cbv import cbv -from ..auth.dependency import CURRENT_USER_DEP +from ..auth.dependency import CURRENT_READ_USER_DEP, CURRENT_USER_DEP from .daily_quiz_loader import DailyQuizLoader from .dependency import get_daily_quiz_loader, get_quiz_answer_service, get_quiz_service from .guest.router import router as guest_router @@ -36,13 +36,13 @@ async def submit_answer( self, quiz_submit_req: SubmitAnswerRequest, current_user: CURRENT_USER_DEP ) -> SubmitAnswerResponse: correct = await self.quiz_answer_service.submit_answer( - quiz_id=quiz_submit_req.quiz_id, user_id=current_user.id, answer=quiz_submit_req.answer + quiz_id=quiz_submit_req.quiz_id, user=current_user, answer=quiz_submit_req.answer ) return SubmitAnswerResponse(correct=correct) @router.get("/quiz/answer") - async def get_quiz_answer(self, quiz_id: int, current_user: CURRENT_USER_DEP) -> QuizAnswerResponse: + async def get_quiz_answer(self, quiz_id: int, current_user: CURRENT_READ_USER_DEP) -> QuizAnswerResponse: quiz_answers = await self.quiz_answer_service.get_quiz_answer(quiz_id=quiz_id, user_id=current_user.id) return QuizAnswerResponse( @@ -52,7 +52,7 @@ async def get_quiz_answer(self, quiz_id: int, current_user: CURRENT_USER_DEP) -> ) @router.get("/quiz/correct_answer") - async def get_correct_answer(self, quiz_id: int, current_user: CURRENT_USER_DEP): + async def get_correct_answer(self, quiz_id: int, current_user: CURRENT_READ_USER_DEP): correct_answer = await self.quiz_service.get_correct_answer(quiz_id=quiz_id, user_id=current_user.id) return CorrectAnswerResponse(correct_answer=correct_answer) diff --git a/backend/src/rank_score/dependency.py b/backend/src/rank_score/dependency.py new file mode 100644 index 0000000..71a77e5 --- /dev/null +++ b/backend/src/rank_score/dependency.py @@ -0,0 +1,12 @@ +from typing import Annotated + +from fastapi import Depends + +from .manager import RankScoreManager + + +async def get_rank_score_manager() -> RankScoreManager: + return RankScoreManager() + + +RankScoreManagerDep = Annotated[RankScoreManager, Depends(get_rank_score_manager)] diff --git a/backend/src/rank_score/manager.py b/backend/src/rank_score/manager.py new file mode 100644 index 0000000..883a1f3 --- /dev/null +++ b/backend/src/rank_score/manager.py @@ -0,0 +1,29 @@ +from ..auth.model import User + + +class RankScoreManager: + def update_score(self, *, user: User, has_solved_game: bool, is_quiz_success: bool): + if not has_solved_game and is_quiz_success: + self._on_correct_first(user=user) + elif has_solved_game and is_quiz_success: + self._on_correct_repeat(user=user) + elif not has_solved_game and not is_quiz_success: + self._on_failed(user=user) + elif has_solved_game and not is_quiz_success: + self._on_failed_after_previous_solved(user=user) + + def _on_correct_first(self, *, user: User): + """처음 게임을 맞출때 (해당 게임을 맞춘 적 있는 경우 X)""" + user.rank_score += 10 + + def _on_correct_repeat(self, *, user: User): + """반복해서 게임을 맞출때 (해당 게임을 맞춘 적 있는 경우 O)""" + user.rank_score += 2 + + def _on_failed(self, *, user: User): + """대한 퀴즈 오답시 (해당 게임을 맞춘 적 있는 경우 X)""" + user.rank_score -= 2 + + def _on_failed_after_previous_solved(self, *, user: User): + """이미 맞춘 게임에 대한 퀴즈 오답시 (해당 게임을 맞춘 적 있는 경우 O)""" + user.rank_score -= 5 diff --git a/backend/src/repository.py b/backend/src/repository.py index 7a84918..5c73a98 100644 --- a/backend/src/repository.py +++ b/backend/src/repository.py @@ -25,6 +25,7 @@ async def exists(self: IRepository[ModelTypeS], *, id: int) -> bool: return rs.one() async def create(self: IRepository[ModelTypeS], *, model: ModelTypeS) -> ModelTypeS: + # TODO: try except가 필요한지 확인 try: self._session.add(model) await self._session.commit() From bf0da2d876e0300efaefd7d6b7f1fc12aee1261a Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Thu, 11 Apr 2024 18:24:00 +0900 Subject: [PATCH 15/20] =?UTF-8?q?feat:=20=EC=84=A4=EC=A0=95=EC=97=90=20?= =?UTF-8?q?=EA=B2=8C=EC=9E=84=20=EB=B3=80=EB=8F=99=EC=A0=90=EC=88=98=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/config.py | 6 ++++++ backend/src/rank_score/manager.py | 9 +++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/backend/src/config.py b/backend/src/config.py index e7660d3..81ea81b 100644 --- a/backend/src/config.py +++ b/backend/src/config.py @@ -23,6 +23,12 @@ class Config(BaseSettings): FACEBOOK_OAUTH2_CLIENT_SECRET: str FACEBOOK_OAUTH2_REDIRECT_URL: str | None = None + # score + SCORE_DIFF_ON_CORRECT_FIRST: int = 10 + SCORE_DIFF_ON_CORRECT_REPEAT: int = 2 + SCORE_DIFF_ON_FAILED: int = -2 + SCORE_DIFF_ON_FAILED_AFTER_PREV_SOLVED: int = -5 + ENVIRONMENT: Envrionment = Envrionment.PRODUCTION model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") diff --git a/backend/src/rank_score/manager.py b/backend/src/rank_score/manager.py index 883a1f3..7998638 100644 --- a/backend/src/rank_score/manager.py +++ b/backend/src/rank_score/manager.py @@ -1,4 +1,5 @@ from ..auth.model import User +from ..config import settings class RankScoreManager: @@ -14,16 +15,16 @@ def update_score(self, *, user: User, has_solved_game: bool, is_quiz_success: bo def _on_correct_first(self, *, user: User): """처음 게임을 맞출때 (해당 게임을 맞춘 적 있는 경우 X)""" - user.rank_score += 10 + user.rank_score = max(0, user.rank_score + settings.SCORE_DIFF_ON_CORRECT_FIRST) def _on_correct_repeat(self, *, user: User): """반복해서 게임을 맞출때 (해당 게임을 맞춘 적 있는 경우 O)""" - user.rank_score += 2 + user.rank_score = max(0, user.rank_score + settings.SCORE_DIFF_ON_CORRECT_REPEAT) def _on_failed(self, *, user: User): """대한 퀴즈 오답시 (해당 게임을 맞춘 적 있는 경우 X)""" - user.rank_score -= 2 + user.rank_score = max(0, user.rank_score + settings.SCORE_DIFF_ON_FAILED) def _on_failed_after_previous_solved(self, *, user: User): """이미 맞춘 게임에 대한 퀴즈 오답시 (해당 게임을 맞춘 적 있는 경우 O)""" - user.rank_score -= 5 + user.rank_score = max(0, user.rank_score + settings.SCORE_DIFF_ON_FAILED_AFTER_PREV_SOLVED) From d911d1db42787bb6edb3b4747326e337ec9e6fa8 Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Thu, 11 Apr 2024 18:44:43 +0900 Subject: [PATCH 16/20] =?UTF-8?q?test:=20=EC=A0=90=EC=88=98=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/score/test_rank_score.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 backend/tests/integration/score/test_rank_score.py diff --git a/backend/tests/integration/score/test_rank_score.py b/backend/tests/integration/score/test_rank_score.py new file mode 100644 index 0000000..7f731a1 --- /dev/null +++ b/backend/tests/integration/score/test_rank_score.py @@ -0,0 +1,129 @@ +import pytest +from fastapi import status +from httpx import AsyncClient +from sqlmodel.ext.asyncio.session import AsyncSession + +from src.auth.model import User +from src.config import settings +from tests.utils.quiz import create_random_quiz + + +@pytest.mark.parametrize("score_diff_on_correct_first", (0, 1, settings.SCORE_DIFF_ON_CORRECT_FIRST, 100)) +async def test_complete_quiz_on_correct_first( + client: AsyncClient, session: AsyncSession, current_user: User, score_diff_on_correct_first: int +): + settings.SCORE_DIFF_ON_CORRECT_FIRST = score_diff_on_correct_first + before_score = current_user.rank_score + quiz = await create_random_quiz(session) + game = await quiz.get_game() + + res = await client.post("/quiz/submit_answer", json={"quiz_id": quiz.id, "answer": game.name}) + assert res.status_code == status.HTTP_200_OK + assert res.json()["correct"] is True + + assert current_user.rank_score - before_score == settings.SCORE_DIFF_ON_CORRECT_FIRST + + +@pytest.mark.parametrize("score_diff_on_correct_repeat", (0, 1, settings.SCORE_DIFF_ON_CORRECT_REPEAT, 100)) +async def test_complete_quiz_on_correct_repeat( + client: AsyncClient, session: AsyncSession, current_user: User, score_diff_on_correct_repeat: int +): + settings.SCORE_DIFF_ON_CORRECT_REPEAT = score_diff_on_correct_repeat + quiz1 = await create_random_quiz(session) + quiz2 = await create_random_quiz(session, screenshots=await quiz1.awt_screenshots) + game = await quiz1.get_game() + + res1 = await client.post("/quiz/submit_answer", json={"quiz_id": quiz1.id, "answer": game.name}) + assert res1.status_code == status.HTTP_200_OK + assert res1.json()["correct"] is True + before_score = current_user.rank_score + + res2 = await client.post("/quiz/submit_answer", json={"quiz_id": quiz2.id, "answer": game.name}) + assert res2.status_code == status.HTTP_200_OK + assert res2.json()["correct"] is True + + assert current_user.rank_score - before_score == settings.SCORE_DIFF_ON_CORRECT_REPEAT + + +@pytest.mark.parametrize("score_diff_on_failed", (0, -1, settings.SCORE_DIFF_ON_FAILED, -100)) +async def test_complete_quiz_on_failed( + client: AsyncClient, session: AsyncSession, current_user: User, score_diff_on_failed: int +): + settings.SCORE_DIFF_ON_FAILED = score_diff_on_failed + current_user.rank_score = 10000 + before_score = current_user.rank_score + quiz = await create_random_quiz(session) + + for _ in range(settings.QUIZ_ANSWER_SUBMISSION_LIMIT): + res = await client.post("/quiz/submit_answer", json={"quiz_id": quiz.id, "answer": "빙빙바리바리구"}) + assert res.status_code == status.HTTP_200_OK + assert res.json()["correct"] is False + + assert current_user.rank_score - before_score == settings.SCORE_DIFF_ON_FAILED + + +@pytest.mark.parametrize("score_diff_on_failed", (-1, settings.SCORE_DIFF_ON_FAILED, -100)) +async def test_complete_quiz_on_failed_with_underflow( + client: AsyncClient, session: AsyncSession, current_user: User, score_diff_on_failed: int +): + settings.SCORE_DIFF_ON_FAILED = score_diff_on_failed + current_user.rank_score = 0 + quiz = await create_random_quiz(session) + + for _ in range(settings.QUIZ_ANSWER_SUBMISSION_LIMIT): + res = await client.post("/quiz/submit_answer", json={"quiz_id": quiz.id, "answer": "빙빙바리바리구"}) + assert res.status_code == status.HTTP_200_OK + assert res.json()["correct"] is False + + assert current_user.rank_score == 0 + + +@pytest.mark.parametrize( + "score_diff_on_failed_after_prev_solved", (0, -1, settings.SCORE_DIFF_ON_FAILED_AFTER_PREV_SOLVED, -100) +) +async def test_complete_quiz_on_failed_after_prev_solved( + client: AsyncClient, session: AsyncSession, current_user: User, score_diff_on_failed_after_prev_solved: int +): + settings.SCORE_DIFF_ON_FAILED_AFTER_PREV_SOLVED = score_diff_on_failed_after_prev_solved + quiz1 = await create_random_quiz(session) + quiz2 = await create_random_quiz(session, screenshots=await quiz1.awt_screenshots) + game = await quiz1.get_game() + + res1 = await client.post("/quiz/submit_answer", json={"quiz_id": quiz1.id, "answer": game.name}) + assert res1.status_code == status.HTTP_200_OK + assert res1.json()["correct"] is True + + current_user.rank_score = 10000 + before_score = current_user.rank_score + + for _ in range(settings.QUIZ_ANSWER_SUBMISSION_LIMIT): + res = await client.post("/quiz/submit_answer", json={"quiz_id": quiz2.id, "answer": "빙빙바리바리구"}) + assert res.status_code == status.HTTP_200_OK + assert res.json()["correct"] is False + + assert current_user.rank_score - before_score == settings.SCORE_DIFF_ON_FAILED_AFTER_PREV_SOLVED + + +@pytest.mark.parametrize( + "score_diff_on_failed_after_prev_solved", (-1, settings.SCORE_DIFF_ON_FAILED_AFTER_PREV_SOLVED, -100) +) +async def test_complete_quiz_on_failed_after_prev_solved_with_underflow( + client: AsyncClient, session: AsyncSession, current_user: User, score_diff_on_failed_after_prev_solved: int +): + settings.SCORE_DIFF_ON_FAILED_AFTER_PREV_SOLVED = score_diff_on_failed_after_prev_solved + quiz1 = await create_random_quiz(session) + quiz2 = await create_random_quiz(session, screenshots=await quiz1.awt_screenshots) + game = await quiz1.get_game() + + res1 = await client.post("/quiz/submit_answer", json={"quiz_id": quiz1.id, "answer": game.name}) + assert res1.status_code == status.HTTP_200_OK + assert res1.json()["correct"] is True + + current_user.rank_score = 0 + + for _ in range(settings.QUIZ_ANSWER_SUBMISSION_LIMIT): + res = await client.post("/quiz/submit_answer", json={"quiz_id": quiz2.id, "answer": "빙빙바리바리구"}) + assert res.status_code == status.HTTP_200_OK + assert res.json()["correct"] is False + + assert current_user.rank_score == 0 From 71854cbd18f5cc23c73cc24611cc3c232c6f1d0d Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Thu, 11 Apr 2024 18:48:23 +0900 Subject: [PATCH 17/20] =?UTF-8?q?fix:=20=EC=A0=90=EC=88=98=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/game/manager.py | 5 ++++- backend/src/quiz/quiz_answer_service.py | 13 +++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/src/game/manager.py b/backend/src/game/manager.py index 5455cef..c46bf9e 100644 --- a/backend/src/game/manager.py +++ b/backend/src/game/manager.py @@ -7,7 +7,10 @@ def __init__(self, *, solved_game_repository: SolvedGameRepository) -> None: self._solved_game_repo = solved_game_repository async def solve_game(self, *, game_id: int, user_id: int): - exists = await self._solved_game_repo.exists_by_user_and_game(user_id=user_id, game_id=game_id) + exists = await self.has_solved_game(game_id=game_id, user_id=user_id) if not exists: solved_game = SolvedGame(user_id=user_id, game_id=game_id) await self._solved_game_repo.create(model=solved_game) + + async def has_solved_game(self, *, game_id: int, user_id: int): + return await self._solved_game_repo.exists_by_user_and_game(user_id=user_id, game_id=game_id) diff --git a/backend/src/quiz/quiz_answer_service.py b/backend/src/quiz/quiz_answer_service.py index b26afdc..570b87a 100644 --- a/backend/src/quiz/quiz_answer_service.py +++ b/backend/src/quiz/quiz_answer_service.py @@ -51,15 +51,16 @@ async def _on_quiz_completed(self, *, quiz: Quiz, user: User, answers: Sequence[ has_solved_game = False success = self._quiz_manager.is_quiz_success(answers=answers) - if success: - game = await quiz.get_game() - assert game.id is not None - assert user.id is not None - await self._game_manager.solve_game(user_id=user.id, game_id=game.id) - has_solved_game = True + game = await quiz.get_game() + assert game.id is not None + assert user.id is not None + has_solved_game = await self._game_manager.has_solved_game(game_id=game.id, user_id=user.id) self._rank_score_manager.update_score(user=user, has_solved_game=has_solved_game, is_quiz_success=success) + if success: + await self._game_manager.solve_game(user_id=user.id, game_id=game.id) + async def get_quiz_answer(self, *, quiz_id: int, user_id: int) -> Sequence[QuizAnswer]: await self._validate_quiz(quiz_id=quiz_id) return await self._quiz_answer_repo.get_by_quiz_id_and_user_id(quiz_id=quiz_id, user_id=user_id) From 7203e427a36e65f73807f7d656870161d1c58d55 Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Thu, 11 Apr 2024 18:52:07 +0900 Subject: [PATCH 18/20] =?UTF-8?q?test:=20/me=EC=97=90=20=EB=8C=80=ED=95=9C?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/tests/integration/auth/test_me.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 backend/tests/integration/auth/test_me.py diff --git a/backend/tests/integration/auth/test_me.py b/backend/tests/integration/auth/test_me.py new file mode 100644 index 0000000..3db3104 --- /dev/null +++ b/backend/tests/integration/auth/test_me.py @@ -0,0 +1,15 @@ +from fastapi import status +from httpx import AsyncClient + +from src.auth.model import User + + +async def test_get_me(client: AsyncClient, current_user: User): + current_user.rank_score = 100 + + res = await client.get("/me") + assert res.status_code == status.HTTP_200_OK + res_json = res.json() + + assert res_json["email"] == current_user.email + assert res_json["rank_score"] == current_user.rank_score From ed63c52d5dfff28e25011442ad0caf3d46fc2d16 Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Fri, 12 Apr 2024 16:08:12 +0900 Subject: [PATCH 19/20] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20es=20client=EC=97=90=20=EB=8C=80=ED=95=9C?= =?UTF-8?q?=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=A3=A8=ED=94=84=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/tests/integration/conftest.py | 6 ++++++ .../tests/integration/game/test_auto_complete_name.py | 9 --------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/backend/tests/integration/conftest.py b/backend/tests/integration/conftest.py index 41ce267..2f7e104 100644 --- a/backend/tests/integration/conftest.py +++ b/backend/tests/integration/conftest.py @@ -9,6 +9,7 @@ from src.auth.dependency import current_active_user from src.auth.model import User from src.config import settings +from src.dependency import es_client as es_client_ from src.main import app from tests.database import engine from tests.utils.auth import create_random_user @@ -55,8 +56,13 @@ def override_current_active_user() -> User: async def es_client() -> AsyncGenerator[AsyncElasticsearch, None]: es_client = AsyncElasticsearch(settings.ELASTIC_SEARCH_URL) + def override_es_client() -> AsyncElasticsearch: + return es_client + await delete_all_indexes(es_client) await create_all_indexes(es_client) + + app.dependency_overrides[es_client_] = override_es_client yield es_client await delete_all_indexes(es_client) await es_client.close() diff --git a/backend/tests/integration/game/test_auto_complete_name.py b/backend/tests/integration/game/test_auto_complete_name.py index 3334e1e..eeb9061 100644 --- a/backend/tests/integration/game/test_auto_complete_name.py +++ b/backend/tests/integration/game/test_auto_complete_name.py @@ -1,5 +1,3 @@ -from typing import AsyncGenerator - import pytest from elasticsearch import AsyncElasticsearch from fastapi import status @@ -7,16 +5,9 @@ from sqlmodel.ext.asyncio.session import AsyncSession from src.game.model import Game -from src.main import app from tests.utils.game import create_random_game, index_game -@pytest.fixture(scope="module") -async def client() -> AsyncGenerator[AsyncClient, None]: - async with AsyncClient(app=app) as client: - yield client - - async def create_indexed_game( session: AsyncSession, es_client: AsyncElasticsearch, name: str, aliases: list[str] = [] ) -> Game: From abf200da04436b47ce7e96556d571501846ba97a Mon Sep 17 00:00:00 2001 From: 2jun0 Date: Fri, 12 Apr 2024 16:25:24 +0900 Subject: [PATCH 20/20] =?UTF-8?q?feat:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=EC=9D=98=20=EB=B2=94=EC=9C=84=EB=A5=BC=20=EB=84=93?= =?UTF-8?q?=ED=9E=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/dependency.py | 1 + backend/src/repository.py | 11 +---------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/backend/src/dependency.py b/backend/src/dependency.py index af71e0b..8b91dac 100644 --- a/backend/src/dependency.py +++ b/backend/src/dependency.py @@ -12,6 +12,7 @@ async def get_session() -> AsyncGenerator[AsyncSession, Any]: async with AsyncSession(engine, expire_on_commit=False) as session: yield session + await session.commit() async def es_client() -> AsyncGenerator[AsyncElasticsearch, Any]: diff --git a/backend/src/repository.py b/backend/src/repository.py index 5c73a98..011e479 100644 --- a/backend/src/repository.py +++ b/backend/src/repository.py @@ -1,6 +1,5 @@ from typing import Generic, Protocol, Type, TypeVar -from sqlalchemy import exc from sqlmodel import SQLModel, exists, select from sqlmodel.ext.asyncio.session import AsyncSession @@ -25,13 +24,5 @@ async def exists(self: IRepository[ModelTypeS], *, id: int) -> bool: return rs.one() async def create(self: IRepository[ModelTypeS], *, model: ModelTypeS) -> ModelTypeS: - # TODO: try except가 필요한지 확인 - try: - self._session.add(model) - await self._session.commit() - except exc.IntegrityError: - await self._session.rollback() - raise - - await self._session.refresh(model) + self._session.add(model) return model