Skip to content

Commit

Permalink
feat(game): game info에서 토너먼트 다음 게임 유저 정보 전송 (#252)
Browse files Browse the repository at this point in the history
* refactor(game): match 서비스로 이전
* feat(test): 기본 테스트 생성
* feat(game): 토너먼트 next_game 유저 정보 전송
* fix(consumer): consumers 중복 로그아웃 버그 수정
* docs(game): game info 필드 추
  • Loading branch information
middlefitting authored Feb 22, 2024
1 parent 72a39e4 commit 39b0765
Show file tree
Hide file tree
Showing 14 changed files with 174 additions and 29 deletions.
2 changes: 1 addition & 1 deletion backend/backend/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application

from chat.middleware import JWTAuthMiddlewareStack
from backend.middleware import JWTAuthMiddlewareStack
from backend.routing import websocket_urlpatterns

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
Expand Down
2 changes: 1 addition & 1 deletion backend/backend/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async def set_user_info(self):

async def handle_duplicate_login(self):
await self.channel_layer.group_send(
f"{self.scope['user'].id}_global",
f"{self.scope['user'].id}{self.CONSUMER_GROUP}",
{
"type": "user_logout",
"user_id": self.scope['user'].id,
Expand Down
4 changes: 3 additions & 1 deletion backend/chat/middleware.py → backend/backend/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ def get_user_from_jwt(token):
try:
decoded_data = AccessToken(token)
user_id = decoded_data['user_id']
mfa_require = decoded_data['mfa_require']
if mfa_require:
return AnonymousUser()
return get_user_model().objects.get(id=user_id)
except (InvalidToken, TokenError, get_user_model().DoesNotExist):
return AnonymousUser()
Expand All @@ -28,7 +31,6 @@ def __init__(self, inner):
self.inner = inner

async def __call__(self, scope, receive, send):

cookies = get_cookie_header(scope).split(";")
cookies = list(map(lambda cookie: cookie.strip(), cookies))
access_tokens = [cookie.split('=', 1)[1] for cookie in cookies if cookie.startswith('access_token=')]
Expand Down
1 change: 1 addition & 0 deletions backend/chat/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class ChatConsumer(DefaultConsumer):
SINGLE_MESSAGE = "single_message"
LOGIN_MESSAGE = "login_message"
LOGIN_GROUP = "chat_login_group"
CONSUMER_GROUP = "_chat"

async def connect(self):
await super(ChatConsumer, self).connect()
Expand Down
19 changes: 19 additions & 0 deletions backend/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import asyncio
import pytest

from backend.redis import RedisConnection


@pytest.fixture(scope="session")
def event_loop():
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
yield loop
loop.close()


async def clear_redis():
redis_conn = await RedisConnection.get_instance()
await redis_conn.redis.flushdb()
41 changes: 22 additions & 19 deletions backend/game/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,23 @@

from backend.consumers import DefaultConsumer
from game.message_type import GameMessageType
from game.service import GameService
from game.service import MatchService


class GameConsumer(DefaultConsumer):
game_service = None
match_service = None
LOGIN_GROUP = "game_login_group"
CONSUMER_GROUP = ""

async def connect(self):
await super(GameConsumer, self).connect()
if not isinstance(self.scope['user'], AnonymousUser):
await self.channel_layer.group_add(str(self.scope['user'].id), self.channel_name)
self.game_service = await GameService.get_instance()
self.match_service = await MatchService.get_instance()
user_id = self.scope['user'].id
if await self.game_service.is_user_in_game(user_id):
game_session = await self.game_service.get_user_game_session(user_id)
await self.game_service.handle_info_message(user_id, game_session)
if await self.match_service.is_user_in_game(user_id):
game_session = await self.match_service.get_user_game_session(user_id)
await self.match_service.handle_info_message(user_id, game_session)

async def disconnect(self, close_code):
await super(GameConsumer, self).disconnect(close_code)
Expand All @@ -33,27 +34,27 @@ async def receive(self, text_data):
text_data_json = json.loads(text_data)
message_type = text_data_json.get('type')
if message_type == GameMessageType.SINGLE_GAME_CREATE:
await self.game_service.start_single_pingpong_game(self.scope['user'].id)
await self.match_service.start_single_pingpong_game(self.scope['user'].id)
elif message_type == GameMessageType.MOVE_BAR:
await self.game_service.move_bar(self.scope['user'].id, text_data_json.get("command"))
await self.match_service.move_bar(self.scope['user'].id, text_data_json.get("command"))
elif message_type == GameMessageType.JOIN_MULTI_GAME_QUEUE:
await self.game_service.join_multi_queue(self.scope['user'].id)
await self.match_service.join_multi_queue(self.scope['user'].id)
elif message_type == GameMessageType.JOIN_TOURNAMENT_GAME_QUEUE:
await self.game_service.join_tournament_queue(self.scope['user'].id)
await self.match_service.join_tournament_queue(self.scope['user'].id)
elif message_type == GameMessageType.RESPONSE_ACCEPT_QUEUE:
await self.game_service.accept_queue_response(self.scope['user'].id, text_data_json)
await self.match_service.accept_queue_response(self.scope['user'].id, text_data_json)
elif message_type == GameMessageType.INVITE_USER:
await self.game_service.invite_user(self.scope['user'].id, text_data_json)
await self.match_service.invite_user(self.scope['user'].id, text_data_json)
elif message_type == GameMessageType.RESPONSE_INVITE:
await self.game_service.accept_invite(self.scope['user'].id, text_data_json)
await self.match_service.accept_invite(self.scope['user'].id, text_data_json)
elif message_type == GameMessageType.DELETE_MULTI_GAME_QUEUE:
await self.game_service.delete_from_queue(self.scope['user'].id,
GameService.MULTIPLAYER_QUEUE_KEY,
GameService.MULTIPLAYER_QUEUE_SET_KEY)
await self.match_service.delete_from_queue(self.scope['user'].id,
MatchService.MULTIPLAYER_QUEUE_KEY,
MatchService.MULTIPLAYER_QUEUE_SET_KEY)
elif message_type == GameMessageType.DELETE_TOURNAMENT_GAME_QUEUE:
await self.game_service.delete_from_queue(self.scope['user'].id,
GameService.TOURNAMENT_QUEUE_KEY,
GameService.TOURNAMENT_QUEUE_SET_KEY)
await self.match_service.delete_from_queue(self.scope['user'].id,
MatchService.TOURNAMENT_QUEUE_KEY,
MatchService.TOURNAMENT_QUEUE_SET_KEY)

async def update_game(self, event):
"""
Expand Down Expand Up @@ -87,6 +88,8 @@ async def info_game(self, event):
"circle_radius": event["circle_radius"],
"screen_height": event["screen_height"],
"screen_width": event["screen_width"],
"next_left_player": event["next_left_player"],
"next_right_player": event["next_right_player"],
}))

async def wait_game(self, event):
Expand Down
19 changes: 12 additions & 7 deletions backend/game/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from game.models import Game


class GameService:
class MatchService:
_instance = None
_redis = None
_channel_layer = None
Expand Down Expand Up @@ -242,14 +242,14 @@ async def tournament_game(self, users_id: list):
await asyncio.sleep(3)
await self.handle_next_game_message(users_id[2])
await self.handle_next_game_message(users_id[3])
game_session = await self.new_game_session_logic(users_id, users_id[0], users_id[1], self.TOURNAMENT_GAME)
game_session = await self.new_game_session_logic(users_id, users_id[0], users_id[1], self.TOURNAMENT_GAME, users_id[2], users_id[3])
game_info = await self.get_game_info(game_session)
winner1 = game_info.get("winner")
await self.save_game_result(game_info)
await self.delete_game_session_logic(users_id, game_session)

await self.handle_next_game_message(winner1)
game_session = await self.new_game_session_logic(users_id, users_id[2], users_id[3], self.TOURNAMENT_GAME)
game_session = await self.new_game_session_logic(users_id, users_id[2], users_id[3], self.TOURNAMENT_GAME, winner1)
game_info = await self.get_game_info(game_session)
winner2 = game_info.get("winner")
await self.save_game_result(game_info)
Expand Down Expand Up @@ -437,12 +437,13 @@ def save_game_result(self, game_info):


# GAME_LOGIC
async def new_game_session_logic(self, users_id, left_player, right_player, game_type):
async def new_game_session_logic(self, users_id, left_player, right_player, game_type,
next_left_player=None, next_right_player=None):
"""
새로운 게임 세션을 생성
"""
game_session = str(uuid.uuid4())
await self.set_game_info(left_player, right_player, game_type, game_session)
await self.set_game_info(left_player, right_player, game_type, game_session, next_left_player, next_right_player)
for user_id in users_id:
await self.set_user_game_session(user_id, game_session)
await self.handle_info_message(user_id, game_session)
Expand Down Expand Up @@ -664,6 +665,8 @@ async def handle_info_message(self, user_id, game_session):
"bar_width": self.BAR_WIDTH,
"bar_height": self.BAR_HEIGHT,
"circle_radius": self.CIRCLE_RADIUS,
"next_left_player": game_info.get("next_left_player"),
"next_right_player": game_info.get("next_right_player"),
})

async def handle_wait_message(self, user_id, time):
Expand Down Expand Up @@ -716,7 +719,7 @@ async def delete_game_info(self, game_session):
"""
await self._redis.delete(f"game_info:{game_session}")

async def set_game_info(self, left_user_id, right_user_id, game_type, game_session):
async def set_game_info(self, left_user_id, right_user_id, game_type, game_session, next_left_player, next_right_player):
"""
게임 세션 정보를 생성
"""
Expand All @@ -729,5 +732,7 @@ async def set_game_info(self, left_user_id, right_user_id, game_type, game_sessi
"left_score": "0",
"right_score": "0",
"status": self.BEFORE,
"winner": "NONE"
"winner": "NONE",
"next_left_player": str(next_left_player) if next_left_player is not None else "NONE",
"next_right_player": str(next_right_player) if next_right_player is not None else "NONE",
})
3 changes: 3 additions & 0 deletions backend/game/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ class InfoGame(APIView):
'circle_radius': openapi.Schema(type=openapi.TYPE_INTEGER, description="공 반지름"),
'screen_height': openapi.Schema(type=openapi.TYPE_INTEGER, description="맵 높이"),
'screen_width': openapi.Schema(type=openapi.TYPE_INTEGER, description="맵 너비"),
'next_left_player': openapi.Schema(type=openapi.TYPE_INTEGER, description="다음 게임 왼쪽 유저 pk"),
'next_right_player': openapi.Schema(type=openapi.TYPE_INTEGER, description="다음 게임 오른쪽 유저 pk"),
}
)),
})
Expand Down
2 changes: 2 additions & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[tool.pytest.ini_options]
asyncio_mode = "auto"
2 changes: 2 additions & 0 deletions backend/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
DJANGO_SETTINGS_MODULE = backend.settings
4 changes: 4 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ zope.interface==6.1

channels_redis
aioredis

pytest
pytest-django
pytest-asyncio
blinker
Empty file added backend/tests/__init__.py
Empty file.
37 changes: 37 additions & 0 deletions backend/tests/test_game_consumer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import json

import pytest
from channels.testing import WebsocketCommunicator

from backend import settings
from backend.asgi import application
from conftest import clear_redis
from game.message_type import GameMessageType
from tests.test_socket_aute_middleware import create_test_user_and_token


@pytest.mark.django_db(transaction=True)
@pytest.mark.asyncio
async def test_single_game():
await clear_redis()
user, refresh = await create_test_user_and_token()
refresh['mfa_require'] = False
access_token_bytes = bytes("access_token=" + str(refresh.access_token), 'utf-8')
url = bytes(settings.BASE_URL, 'utf-8')
headers = [(b'origin', url), (b'cookie', access_token_bytes)]
communicator = WebsocketCommunicator(application, 'ws/game/', headers)
connected, _ = await communicator.connect()
assert connected, "Failed to connect to websocket"
await communicator.send_to(json.dumps({"type": GameMessageType.SINGLE_GAME_CREATE}))
try:
while True:
response = await communicator.receive_from(timeout=10)
print(response)
data = json.loads(response)
if data["type"] == GameMessageType.INFO_GAME and data["status"] == "end":
assert int(data["left_score"]) == 11 or int(
data["right_score"]) == 11, "Game did not end with a score of 11 for one player"
break
finally:
await communicator.disconnect()
await clear_redis()
67 changes: 67 additions & 0 deletions backend/tests/test_socket_aute_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import asyncio

import pytest
from channels.db import database_sync_to_async
from channels.testing import WebsocketCommunicator
from rest_framework_simplejwt.tokens import RefreshToken

from backend import settings
from backend.asgi import application
from conftest import clear_redis
from user.models import User


@database_sync_to_async
def create_test_user_and_token():
user = User.objects.create_user(username="testuser", password="testpassword", email="[email protected]",
nickname="middle")
refresh = RefreshToken.for_user(user)
return user, refresh


@pytest.mark.django_db(transaction=True)
@pytest.mark.asyncio
async def test_success_with_valid_token():
clear_redis()
try:
user, refresh = await create_test_user_and_token()
refresh['mfa_require'] = False
access_token_bytes = bytes("access_token=" + str(refresh.access_token), 'utf-8')
url = bytes(settings.BASE_URL, 'utf-8')
headers = [(b'origin', url), (b'cookie', access_token_bytes)]
communicator = WebsocketCommunicator(application, 'ws/game/', headers)
connected, subprotocol = await communicator.connect()
assert connected
finally:
await communicator.disconnect()
clear_redis()



@pytest.mark.django_db(transaction=True)
@pytest.mark.asyncio
async def test_fail_with_2fa_required_token():
clear_redis()
user, refresh = await create_test_user_and_token()
refresh['mfa_require'] = True
access_token_bytes = bytes("access_token=" + str(refresh.access_token), 'utf-8')
url = bytes(settings.BASE_URL, 'utf-8')
headers = [(b'origin', url), (b'cookie', access_token_bytes)]
communicator = WebsocketCommunicator(application, 'ws/game/', headers)
connected, subprotocol = await communicator.connect()
assert not connected
await communicator.disconnect()
clear_redis()


@pytest.mark.django_db(transaction=True)
@pytest.mark.asyncio
async def test_fail_with_anonymous_user():
clear_redis()
url = bytes(settings.BASE_URL, 'utf-8')
headers = [(b'origin', url)]
communicator = WebsocketCommunicator(application, 'ws/game/', headers)
connected, subprotocol = await communicator.connect()
assert not connected
await communicator.disconnect()
clear_redis()

0 comments on commit 39b0765

Please sign in to comment.