diff --git a/love_letter/config.py b/love_letter/config.py index 68548ae..498e5c1 100644 --- a/love_letter/config.py +++ b/love_letter/config.py @@ -14,6 +14,7 @@ class Configuration: "LOBBY_ISSUER", "https://dev-1l0ixjw8yohsluoi.us.auth0.com/" ) LOBBY_AUDIENCE = os.environ.get("LOBBY_ISSUER", "https://api.gaas.waterballsa.tw") + LOBBY_USERS_ME_API = "https://api.gaas.waterballsa.tw/users/me" config = Configuration() diff --git a/love_letter/usecase/lobby_game_status.py b/love_letter/usecase/lobby_game_status.py new file mode 100644 index 0000000..d72ca8e --- /dev/null +++ b/love_letter/usecase/lobby_game_status.py @@ -0,0 +1,21 @@ +from love_letter.models import Game +from love_letter.models.event import GetStatusEvent +from love_letter.usecase.common import Presenter, game_repository + + +class LobbyGameStatusInput: + game_id: str + player_id: str + + +class LobbyGameStatus: + def execute(self, input: LobbyGameStatusInput, presenter: Presenter): + game: Game = game_repository.get(input.game_id) + presenter.present(events=[GetStatusEvent(game)]) + + @classmethod + def input(cls, game_id: str, player_id: str) -> LobbyGameStatusInput: + input = LobbyGameStatusInput() + input.game_id = game_id + input.player_id = player_id + return input diff --git a/love_letter/web/app.py b/love_letter/web/app.py index e82040b..2a62e82 100644 --- a/love_letter/web/app.py +++ b/love_letter/web/app.py @@ -1,13 +1,14 @@ from typing import Union import uvicorn -from fastapi import Depends, FastAPI +from fastapi import Depends, FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from love_letter.models import GuessCard, ToSomeoneCard from love_letter.usecase.create_game import CreateGame from love_letter.usecase.get_status import GetStatus from love_letter.usecase.join_game import JoinGame +from love_letter.usecase.lobby_game_status import LobbyGameStatus from love_letter.usecase.lobby_start_game import LobbyStartGame from love_letter.usecase.play_card import PlayCard from love_letter.usecase.start_game import StartGame @@ -23,6 +24,7 @@ PlayCardPresenter, GetStatusPresenter, LobbyStartGamePresenter, + LobbyGameStatusPresenter, ) # isort: on @@ -99,6 +101,16 @@ async def lobby_start_game(players: LobbyPlayers): return presenter.as_view_model() +@app.get("/games/{game_id}/status", response_model=GameStatus) +async def lobby_game_status(request: Request, game_id: str): + jwt_token = request.headers.get("Authorization") + player_id = JWTBearer.get_player_id(jwt_token) + presenter = LobbyGameStatusPresenter.presenter() + LobbyGameStatus().execute(LobbyGameStatus.input(game_id, player_id), presenter) + game = presenter.as_view_model() + return build_player_view(game, player_id) + + @app.get("/health") async def health(): return {"success": True} diff --git a/love_letter/web/auth.py b/love_letter/web/auth.py index ba7f5f8..496312a 100644 --- a/love_letter/web/auth.py +++ b/love_letter/web/auth.py @@ -1,6 +1,8 @@ import datetime +from functools import lru_cache import jwt +import requests from fastapi import HTTPException from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from starlette.requests import Request @@ -63,3 +65,18 @@ def create_jwt(): key="", ) return f"Bearer {token}" + + @staticmethod + @lru_cache + def get_player_id(jwt_token: str) -> str: + response = "" + try: + response = requests.get( + config.LOBBY_USERS_ME_API, headers={"Authorization": jwt_token} + ) + except Exception as e: + print(e) + + if response and response.status_code == 200: + return response.json()["id"] + raise ValueError("Not found player id") diff --git a/love_letter/web/presenter.py b/love_letter/web/presenter.py index f6a9265..2707c4f 100644 --- a/love_letter/web/presenter.py +++ b/love_letter/web/presenter.py @@ -158,3 +158,15 @@ def as_view_model(self): @classmethod def presenter(cls) -> "LobbyStartGamePresenter": return LobbyStartGamePresenter() + + +class LobbyGameStatusPresenter(Presenter): + def as_view_model(self): + for event in self.events: + if isinstance(event, GetStatusEvent): + return event.game + raise BaseException("Game is unavailable.") + + @classmethod + def presenter(cls) -> "LobbyGameStatusPresenter": + return LobbyGameStatusPresenter() diff --git a/tests/test_lobby_api.py b/tests/test_lobby_api.py index afcfa17..154f2c9 100644 --- a/tests/test_lobby_api.py +++ b/tests/test_lobby_api.py @@ -1,6 +1,9 @@ -from starlette.testclient import TestClient +from unittest.mock import patch + +from fastapi.testclient import TestClient from love_letter.config import config +from love_letter.models import Round from love_letter.web.app import app from love_letter.web.auth import JWTBearer from love_letter.web.dto import GameStatus @@ -12,8 +15,18 @@ class LobbyTestCase(LoveLetterRepositoryAwareTestCase): def setUp(self) -> None: self.t: TestClient = TestClient(app) self.jwt_token: str = JWTBearer.create_jwt() + self.game_id: str = self.__class__.game_id + # disable random-picker for the first round + # it always returns the first player + self.origin_choose_one_randomly = Round.choose_one_randomly + Round.choose_one_randomly = lambda players: players[0] + + @classmethod + def setUpClass(cls): + cls.game_id: str = "" def tearDown(self) -> None: + Round.choose_one_randomly = self.origin_choose_one_randomly self.t.close() def test_health_api(self): @@ -22,7 +35,7 @@ def test_health_api(self): self.assertEqual(response.status_code, 200) self.assertTrue(data["success"]) - def test_start_game(self): + def test_1_start_game(self): players = [ {"id": "6497f6f226b40d440b9a90cc", "nickname": "板橋金城武"}, {"id": "6498112b26b40d440b9a90ce", "nickname": "三重彭于晏"}, @@ -39,14 +52,81 @@ def test_start_game(self): data = response.json() self.assertTrue(data["url"].startswith(config.FRONTEND_HOST)) - game_id = data["url"].split("/")[-1] + self.game_id = data["url"].split("/")[-1] + self.__class__.game_id = self.game_id status_of_player: GameStatus = GameStatus.parse_obj( - get_status(game_id, players[0]["nickname"]) + get_status(self.game_id, players[0]["nickname"]) ) status_players = status_of_player.players - self.assertEqual(status_of_player.game_id, game_id) + self.assertEqual(status_of_player.game_id, self.game_id) self.assertEqual( [p.name for p in status_players], [p["nickname"] for p in players] ) self.assertEqual([p.id for p in status_players], [p["id"] for p in players]) + + @patch("love_letter.web.app.JWTBearer") + def test_2_game_status(self, mock_jwt_bearer): + # 把呼叫大平台 /users/me 的api給mock掉,因為我們自己創建的jwt token無法取得對應的player_id + mock_jwt_bearer.get_player_id.return_value = "6497f6f226b40d440b9a90cc" + response = self.t.get( + f"/games/{self.game_id}/status", headers={"Authorization": self.jwt_token} + ) + data = response.json() + + # 檢查是否mock了mock_jwt_bearer.get_player_id方法 + mock_jwt_bearer.get_player_id.assert_called_once_with(self.jwt_token) + self.assertEqual(response.status_code, 200) + self.assertEqual(data["game_id"], self.game_id) + self.assertEqual( + { + "game_id": self.game_id, + "events": [{"type": "round_started", "winner": None}], + "players": [ + {"id": "6497f6f226b40d440b9a90cc", "name": "板橋金城武", "score": 0}, + {"id": "6498112b26b40d440b9a90ce", "name": "三重彭于晏", "score": 0}, + {"id": "6499df157fed0c21a4fd0425", "name": "蘆洲劉德華", "score": 0}, + {"id": "649836ed7fed0c21a4fd0423", "name": "永和周杰倫", "score": 0}, + ], + "rounds": [ + { + "players": [ + { + "seen_cards": [], + "cards": [], + "name": "板橋金城武", + "out": False, + }, + { + "cards": [], + "seen_cards": [], + "name": "三重彭于晏", + "out": False, + }, + { + "cards": [], + "seen_cards": [], + "name": "蘆洲劉德華", + "out": False, + }, + { + "cards": [], + "seen_cards": [], + "name": "永和周杰倫", + "out": False, + }, + ], + "turn_player": { + "seen_cards": [], + "cards": [], + "name": "板橋金城武", + "out": False, + }, + "winner": None, + "start_player": "板橋金城武", + } + ], + "final_winner": None, + }, + data, + )