From bb3faa2c916f58da272b1dc64f5ee3da25666306 Mon Sep 17 00:00:00 2001 From: NewCodes7 Date: Tue, 26 Nov 2024 01:42:09 +0900 Subject: [PATCH 1/6] =?UTF-8?q?refactor:=20=EA=B2=8C=EC=9E=84=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/common/constants/game-mode.ts | 4 ++++ BE/src/game/dto/create-game.dto.ts | 3 ++- BE/src/game/dto/update-room-option.dto.ts | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 BE/src/common/constants/game-mode.ts diff --git a/BE/src/common/constants/game-mode.ts b/BE/src/common/constants/game-mode.ts new file mode 100644 index 0000000..f3dbac3 --- /dev/null +++ b/BE/src/common/constants/game-mode.ts @@ -0,0 +1,4 @@ +export enum GameMode { + RANKING = 'RANKING', + SURVIVAL = 'SURVIVAL', +} diff --git a/BE/src/game/dto/create-game.dto.ts b/BE/src/game/dto/create-game.dto.ts index 32edb41..2dd8f98 100644 --- a/BE/src/game/dto/create-game.dto.ts +++ b/BE/src/game/dto/create-game.dto.ts @@ -1,6 +1,7 @@ import { IsIn, IsInt, IsString, Max, MaxLength, Min, MinLength } from 'class-validator'; import { Transform, Type } from 'class-transformer'; import { WsException } from '@nestjs/websockets'; +import { GameMode } from '../../common/constants/game-mode'; export class CreateGameDto { @IsString() @@ -9,7 +10,7 @@ export class CreateGameDto { title: string; @IsString() - @IsIn(['RANKING', 'SURVIVAL']) + @IsIn([GameMode.RANKING, GameMode.SURVIVAL]) gameMode: string; @Type(() => Number) diff --git a/BE/src/game/dto/update-room-option.dto.ts b/BE/src/game/dto/update-room-option.dto.ts index 4c144b1..3f414b7 100644 --- a/BE/src/game/dto/update-room-option.dto.ts +++ b/BE/src/game/dto/update-room-option.dto.ts @@ -1,6 +1,7 @@ import { IsIn, IsInt, IsString, Length, Max, MaxLength, Min, MinLength } from 'class-validator'; import { Transform, Type } from 'class-transformer'; import { WsException } from '@nestjs/websockets'; +import { GameMode } from '../../common/constants/game-mode'; export class UpdateRoomOptionDto { @IsString() @@ -8,7 +9,7 @@ export class UpdateRoomOptionDto { gameId: string; @IsString() - @IsIn(['RANKING', 'SURVIVAL']) + @IsIn([GameMode.RANKING, GameMode.SURVIVAL]) gameMode: string; @IsString() From 7eb23c65bfabdb7a0c24981d7eec844d7e48583f Mon Sep 17 00:00:00 2001 From: NewCodes7 Date: Tue, 26 Nov 2024 01:44:21 +0900 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20hmset=EC=9D=80=20redis=EC=97=90?= =?UTF-8?q?=EC=84=9C=20deprecated=EB=90=9C=20=EB=AC=B8=EB=B2=95=EC=9D=B4?= =?UTF-8?q?=EB=9D=BC=20hset=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/game/service/game.room.service.ts | 13 +++++++------ BE/src/game/service/game.service.ts | 14 +++++++------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/BE/src/game/service/game.room.service.ts b/BE/src/game/service/game.room.service.ts index f868659..3efb053 100644 --- a/BE/src/game/service/game.room.service.ts +++ b/BE/src/game/service/game.room.service.ts @@ -27,7 +27,7 @@ export class GameRoomService { const currentRoomPins = await this.redis.smembers(REDIS_KEY.ACTIVE_ROOMS); const roomId = generateUniquePin(currentRoomPins); - await this.redis.hmset(REDIS_KEY.ROOM(roomId), { + await this.redis.hset(REDIS_KEY.ROOM(roomId), { host: clientId, status: 'waiting', title: gameConfig.title, @@ -68,12 +68,13 @@ export class GameRoomService { const positionY = Math.random(); await this.redis.set(`${playerKey}:Changes`, 'Join'); - await this.redis.hmset(playerKey, { + await this.redis.hset(playerKey, { playerName: dto.playerName, positionX: positionX.toString(), positionY: positionY.toString(), disconnected: '0', - gameId: dto.gameId + gameId: dto.gameId, + isAlive: '1' }); await this.redis.zadd(REDIS_KEY.ROOM_LEADERBOARD(dto.gameId), 0, clientId); @@ -112,7 +113,7 @@ export class GameRoomService { this.gameValidator.validatePlayerIsHost(SocketEvents.UPDATE_ROOM_OPTION, room, clientId); await this.redis.set(`${roomKey}:Changes`, 'Option'); - await this.redis.hmset(roomKey, { + await this.redis.hset(roomKey, { title: title, gameMode: gameMode, maxPlayerCount: maxPlayerCount.toString(), @@ -131,7 +132,7 @@ export class GameRoomService { this.gameValidator.validatePlayerIsHost(SocketEvents.UPDATE_ROOM_QUIZSET, room, clientId); await this.redis.set(`${roomKey}:Changes`, 'Quizset'); - await this.redis.hmset(roomKey, { + await this.redis.hset(roomKey, { quizSetId: quizSetId.toString(), quizCount: quizCount.toString() }); @@ -152,7 +153,7 @@ export class GameRoomService { pipeline.srem(REDIS_KEY.ROOM_PLAYERS(roomId), clientId); // pipeline.del(REDIS_KEY.PLAYER(clientId)); // 1. 플레이어 상태를 'disconnected'로 변경하고 TTL 설정 - pipeline.hmset(REDIS_KEY.PLAYER(clientId), { + pipeline.hset(REDIS_KEY.PLAYER(clientId), { disconnected: '1', disconnectedAt: Date.now().toString() }); diff --git a/BE/src/game/service/game.service.ts b/BE/src/game/service/game.service.ts index 3a2f3fa..89a2786 100644 --- a/BE/src/game/service/game.service.ts +++ b/BE/src/game/service/game.service.ts @@ -31,7 +31,7 @@ export class GameService { this.gameValidator.validatePlayerInRoom(SocketEvents.UPDATE_POSITION, gameId, player); await this.redis.set(`${playerKey}:Changes`, 'Position'); - await this.redis.hmset(playerKey, { + await this.redis.hset(playerKey, { positionX: newPosition[0].toString(), positionY: newPosition[1].toString() }); @@ -87,13 +87,13 @@ export class GameService { ...selectedQuizList.map((quiz) => quiz.id) ); for (const quiz of selectedQuizList) { - await this.redis.hmset(REDIS_KEY.ROOM_QUIZ(gameId, quiz.id), { + await this.redis.hset(REDIS_KEY.ROOM_QUIZ(gameId, quiz.id), { quiz: quiz.quiz, answer: quiz.choiceList.find((choice) => choice.isAnswer).order, limitTime: quiz.limitTime.toString(), choiceCount: quiz.choiceList.length.toString() }); - await this.redis.hmset( + await this.redis.hset( REDIS_KEY.ROOM_QUIZ_CHOICES(gameId, quiz.id), quiz.choiceList.reduce( (acc, choice) => { @@ -113,7 +113,7 @@ export class GameService { // 게임이 시작되었음을 알림 await this.redis.set(`${roomKey}:Changes`, 'Start'); - await this.redis.hmset(roomKey, { + await this.redis.hset(roomKey, { status: 'playing' }); @@ -121,7 +121,7 @@ export class GameService { await this.redis.set(REDIS_KEY.ROOM_CURRENT_QUIZ(gameId), '-1:end'); // 0:start, 0:end, 1:start, 1:end await this.redis.set(REDIS_KEY.ROOM_TIMER(gameId), 'timer', 'EX', 3); - this.logger.verbose(`게임 시작: ${gameId}`); + this.logger.verbose(`게임 시작 (gameId: ${gameId}) (gameMode: ${room.gameMode})`); } async subscribeRedisEvent(server: Server) { @@ -143,12 +143,12 @@ export class GameService { const players = await this.redis.smembers(roomPlayersKey); if (host === clientId && players.length > 0) { const newHost = await this.redis.srandmember(REDIS_KEY.ROOM_PLAYERS(playerData.gameId)); - await this.redis.hmset(roomKey, { + await this.redis.hset(roomKey, { host: newHost }); } await this.redis.set(`${playerKey}:Changes`, 'Disconnect', 'EX', 600); // 해당플레이어의 변화정보 10분 후에 삭제 - await this.redis.hmset(playerKey, { + await this.redis.hset(playerKey, { disconnected: '1' }); From 90d25835dc46c67e7cccae84b76754daa89dca88 Mon Sep 17 00:00:00 2001 From: NewCodes7 Date: Tue, 26 Nov 2024 01:48:37 +0900 Subject: [PATCH 3/6] =?UTF-8?q?test:=20=EA=B8=B0=EC=A1=B4=20=EA=B2=8C?= =?UTF-8?q?=EC=9E=84=20=EB=A1=9C=EC=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/test/integration/game.integration.spec.ts | 778 ------------------ .../game/game-chat.integratoin.spec.ts | 61 ++ .../game/game-room.integration.spec.ts | 152 ++++ .../game/game-survival.integration.spec.ts | 166 ++++ .../integration/game/game.integration.spec.ts | 345 ++++++++ .../{ => quiz-set}/quiz.integration.spec.ts | 26 +- BE/test/integration/setup/game.setup.ts | 32 + BE/test/integration/setup/util.ts | 25 + 8 files changed, 794 insertions(+), 791 deletions(-) delete mode 100644 BE/test/integration/game.integration.spec.ts create mode 100644 BE/test/integration/game/game-chat.integratoin.spec.ts create mode 100644 BE/test/integration/game/game-room.integration.spec.ts create mode 100644 BE/test/integration/game/game-survival.integration.spec.ts create mode 100644 BE/test/integration/game/game.integration.spec.ts rename BE/test/integration/{ => quiz-set}/quiz.integration.spec.ts (93%) create mode 100644 BE/test/integration/setup/game.setup.ts create mode 100644 BE/test/integration/setup/util.ts diff --git a/BE/test/integration/game.integration.spec.ts b/BE/test/integration/game.integration.spec.ts deleted file mode 100644 index 70164ad..0000000 --- a/BE/test/integration/game.integration.spec.ts +++ /dev/null @@ -1,778 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import { IoAdapter } from '@nestjs/platform-socket.io'; -import { io, Socket } from 'socket.io-client'; -import { GameGateway } from '../../src/game/game.gateway'; -import { GameService } from '../../src/game/service/game.service'; -import socketEvents from '../../src/common/constants/socket-events'; -import { Redis } from 'ioredis'; -import { GameValidator } from '../../src/game/validations/game.validator'; -import { GameChatService } from '../../src/game/service/game.chat.service'; -import { GameRoomService } from '../../src/game/service/game.room.service'; -import { HttpService } from '@nestjs/axios'; -import { mockQuizData } from '../mocks/quiz-data.mock'; -import RedisMock from 'ioredis-mock'; -import { REDIS_KEY } from '../../src/common/constants/redis-key.constant'; -import { QuizCacheService } from '../../src/game/service/quiz.cache.service'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { UserModel } from '../../src/user/entities/user.entity'; -import { UserQuizArchiveModel } from '../../src/user/entities/user-quiz-archive.entity'; -import { ConfigModule } from '@nestjs/config'; -import { QuizSetModel } from '../../src/quiz-set/entities/quiz-set.entity'; -import { QuizModel } from '../../src/quiz-set/entities/quiz.entity'; -import { QuizChoiceModel } from '../../src/quiz-set/entities/quiz-choice.entity'; -import { QuizSetService } from '../../src/quiz-set/service/quiz-set.service'; -import { QuizSetCreateService } from '../../src/quiz-set/service/quiz-set-create.service'; -import { QuizSetReadService } from '../../src/quiz-set/service/quiz-set-read.service'; -import { QuizSetUpdateService } from '../../src/quiz-set/service/quiz-set-update.service'; -import { QuizSetDeleteService } from '../../src/quiz-set/service/quiz-set-delete.service'; -import { ExceptionMessage } from '../../src/common/constants/exception-message'; -import { RoomCleanupSubscriber } from '../../src/game/redis/subscribers/room.cleanup.subscriber'; -import { JwtService } from '@nestjs/jwt'; -import { AuthModule } from '../../src/auth/auth.module'; -import { WsJwtAuthGuard } from '../../src/auth/guard/ws-jwt-auth.guard'; -import { JwtStrategy } from '../../src/auth/guard/jwt.strategy'; -import { AuthService } from '../../src/auth/auth.service'; -import { UserService } from '../../src/user/user.service'; -import { ScoringSubscriber } from '../../src/game/redis/subscribers/scoring.subscriber'; -import { RedisSubscriberService } from '../../src/game/redis/redis-subscriber.service'; -import { TimerSubscriber } from '../../src/game/redis/subscribers/timer.subscriber'; -import { RoomSubscriber } from '../../src/game/redis/subscribers/room.subscriber'; -import { PlayerSubscriber } from '../../src/game/redis/subscribers/player.subscriber'; - -/*disable eslint*/ -const mockHttpService = { - axiosRef: jest.fn().mockImplementation(() => { - return Promise.resolve({ - data: mockQuizData, - status: 200 - }); - }) -}; - -describe('GameGateway (e2e)', () => { - let app: INestApplication; - let client1: Socket; - let client2: Socket; - let client3: Socket; - let redisMock: Redis; - let moduleRef: TestingModule; // 추가 - // let quizCacheService: QuizCacheService; // 추가 - - const TEST_PORT = 3001; - - beforeAll(async () => { - /* ioredis-mock을 사용하여 테스트용 인메모리 Redis 생성 */ - redisMock = new RedisMock(); - jest.spyOn(redisMock, 'config').mockImplementation(() => Promise.resolve('OK')); - - // hset 메소드 오버라이드 - const originalHset = redisMock.hmset.bind(redisMock); - redisMock.hmset = async function (key: string, ...args: any[]) { - const result = await originalHset(key, ...args); - - await this.publish(`__keyspace@0__:${key}`, 'hset'); - - return result; - }; - - moduleRef = await Test.createTestingModule({ - imports: [ - ConfigModule.forRoot({ - envFilePath: '../.env', - isGlobal: true - }), - TypeOrmModule.forRoot({ - type: 'mysql', - host: process.env.DB_HOST_TEST || process.env.DB_HOST || '127.0.0.1', - port: +process.env.DB_PORT_TEST || +process.env.DB_PORT || 3306, - username: process.env.DB_USER_TEST || process.env.DB_USER || 'root', - password: process.env.DB_PASSWD_TEST || process.env.DB_PASSWD || 'test', - database: process.env.DB_NAME_TEST || process.env.DB_NAME || 'test_db', - entities: [QuizSetModel, QuizModel, QuizChoiceModel, UserModel, UserQuizArchiveModel], - synchronize: true // test모드에서는 항상 활성화 - }), - TypeOrmModule.forFeature([QuizSetModel, QuizModel, QuizChoiceModel, UserModel]), - AuthModule - ], - providers: [ - WsJwtAuthGuard, - AuthService, - UserService, - JwtStrategy, - JwtService, - GameGateway, - GameService, - GameChatService, - GameRoomService, - GameValidator, - QuizCacheService, - QuizSetService, - QuizSetCreateService, - QuizSetReadService, - QuizSetUpdateService, - QuizSetDeleteService, - RedisSubscriberService, - ScoringSubscriber, - TimerSubscriber, - RoomSubscriber, - PlayerSubscriber, - RoomCleanupSubscriber, - { - provide: 'default_IORedisModuleConnectionToken', - useValue: redisMock - }, - { - provide: HttpService, - useValue: mockHttpService - } - ] - }).compile(); - - app = moduleRef.createNestApplication(); - app.useWebSocketAdapter(new IoAdapter(app)); - await app.listen(TEST_PORT); - }); - - beforeEach(async () => { - await redisMock.flushall(); - - return new Promise((resolve) => { - let connectedClients = 0; - const onConnect = () => { - connectedClients++; - if (connectedClients === 3) { - resolve(); - } - }; - - client1 = io(`http://localhost:${TEST_PORT}/game`, { - transports: ['websocket'], - forceNew: true - }); - client2 = io(`http://localhost:${TEST_PORT}/game`, { - transports: ['websocket'], - forceNew: true - }); - client3 = io(`http://localhost:${TEST_PORT}/game`, { - transports: ['websocket'], - forceNew: true - }); - - client1.on('connect', onConnect); - client2.on('connect', onConnect); - client3.on('connect', onConnect); - }); - }); - - afterEach(async () => { - if (client1 && client1.connected) { - client1.disconnect(); - } - if (client2 && client2.connected) { - client2.disconnect(); - } - if (client3 && client3.connected) { - client3.disconnect(); - } - await redisMock.flushall(); - jest.clearAllMocks(); - }); - - afterAll(async () => { - if (app) { - await app.close(); - } - }); - - describe('createRoom 이벤트 테스트', () => { - it('유효한 설정으로 게임방 생성 성공', async () => { - const gameConfig = { - title: 'hello world!', - gameMode: 'RANKING', - maxPlayerCount: 2, - isPublic: true - }; - - const response = await new Promise<{ gameId: string }>((resolve) => { - client1.once(socketEvents.CREATE_ROOM, resolve); - client1.emit(socketEvents.CREATE_ROOM, gameConfig); - }); - - expect(response.gameId).toBeDefined(); - expect(typeof response.gameId).toBe('string'); - - // 실제 Redis 저장 확인 - const roomData = await redisMock.hgetall(`Room:${response.gameId}`); - expect(roomData).toBeDefined(); - expect(roomData.title).toBe(gameConfig.title); - expect(roomData.gameMode).toBe(gameConfig.gameMode); - expect(roomData.maxPlayerCount).toBe(gameConfig.maxPlayerCount.toString()); - }); - - const invalidConfigs = [ - { - case: '빈 title', - config: { title: '', gameMode: 'RANKING', maxPlayerCount: 2, isPublic: true } - }, - { - case: '빈 gameMode', - config: { title: 'hello', gameMode: '', maxPlayerCount: 2, isPublic: true } - }, - { - case: '잘못된 gameMode', - config: { title: 'hello', gameMode: 'invalid', maxPlayerCount: 2, isPublic: true } - }, - { - case: '최소 인원 미달', - config: { title: 'hello', gameMode: 'RANKING', maxPlayerCount: 0, isPublic: true } - } - ]; - - invalidConfigs.forEach(({ case: testCase, config }) => { - it(`${testCase}인 경우 에러 발생`, (done) => { - client1.once('exception', (error) => { - expect(error).toBeDefined(); - expect(error.eventName).toBe(socketEvents.CREATE_ROOM); - done(); - }); - - client1.emit(socketEvents.CREATE_ROOM, config); - }); - }); - }); - - describe('joinRoom 이벤트 테스트', () => { - it('존재하는 방 참여 성공', async () => { - // 방 생성 - const createResponse = await new Promise<{ gameId: string }>((resolve) => { - client1.once(socketEvents.CREATE_ROOM, resolve); - client1.emit(socketEvents.CREATE_ROOM, { - title: 'Test Room', - gameMode: 'RANKING', - maxPlayerCount: 5, - isPublic: true - }); - }); - - // 방 참여 - const joinResponse = await new Promise((resolve) => { - client2.once(socketEvents.JOIN_ROOM, resolve); - client2.emit(socketEvents.JOIN_ROOM, { - gameId: createResponse.gameId, - playerName: 'TestPlayer' - }); - }); - - expect(joinResponse.players).toBeDefined(); - - // Redis에서 플레이어 정보 확인 - const playerData = await redisMock.hgetall(`Player:${client2.id}`); - expect(playerData).toBeDefined(); - expect(playerData.playerName).toBe('TestPlayer'); - }); - - it('존재하지 않는 방 참여 실패', (done) => { - client1.once('exception', (error) => { - expect(error.eventName).toBe('joinRoom'); - expect(error.message).toBe('존재하지 않는 게임 방입니다.'); - done(); - }); - - client1.emit(socketEvents.JOIN_ROOM, { - gameId: '999999', - playerName: 'TestPlayer' - }); - }); - - it('게임 진행 중인 방 참여 실패', (done) => { - let gameId: string; - - // exception 이벤트 리스너 먼저 등록 - client2.once('exception', (error) => { - try { - expect(error.eventName).toBe('joinRoom'); - expect(error.message).toBe(ExceptionMessage.GAME_ALREADY_STARTED); - done(); - } catch (err) { - done(err); - } - }); - - // 방 생성 - client1.emit(socketEvents.CREATE_ROOM, { - title: 'Test Room', - gameMode: 'RANKING', - maxPlayerCount: 5, - isPublic: true - }); - - // 순차적으로 이벤트 처리 - client1.once(socketEvents.CREATE_ROOM, (response) => { - gameId = response.gameId; - - // 첫 번째 플레이어 입장 - client1.emit(socketEvents.JOIN_ROOM, { - gameId: gameId, - playerName: 'Player1' - }); - }); - - client1.once(socketEvents.JOIN_ROOM, () => { - // 게임 시작 - client1.emit(socketEvents.START_GAME, { gameId }); - }); - - client1.once(socketEvents.START_GAME, () => { - // 두 번째 플레이어 참여 시도 - client2.emit(socketEvents.JOIN_ROOM, { - gameId: gameId, - playerName: 'TestPlayer' - }); - }); - }); - }); - - describe('chatMessage 이벤트 테스트', () => { - it('같은 방의 모든 플레이어에게 메시지 전송', async () => { - // 방 생성 및 참여 설정 - const createResponse = await new Promise<{ gameId: string }>((resolve) => { - client1.once(socketEvents.CREATE_ROOM, resolve); - client1.emit(socketEvents.CREATE_ROOM, { - title: 'Chat Test Room', - gameMode: 'RANKING', - maxPlayerCount: 5, - isPublic: true - }); - }); - - // 플레이어들 입장 - await Promise.all([ - new Promise((resolve) => { - client1.once(socketEvents.JOIN_ROOM, () => resolve()); - client1.emit(socketEvents.JOIN_ROOM, { - gameId: createResponse.gameId, - playerName: 'Player1' - }); - }), - new Promise((resolve) => { - client2.once(socketEvents.JOIN_ROOM, () => resolve()); - client2.emit(socketEvents.JOIN_ROOM, { - gameId: createResponse.gameId, - playerName: 'Player2' - }); - }) - ]); - - // 채팅 메시지 테스트 - const testMessage = 'Hello, everyone!'; - const messagePromises = [ - new Promise((resolve) => client1.once(socketEvents.CHAT_MESSAGE, resolve)), - new Promise((resolve) => client2.once(socketEvents.CHAT_MESSAGE, resolve)) - ]; - - client1.emit(socketEvents.CHAT_MESSAGE, { - gameId: createResponse.gameId, - message: testMessage - }); - - const receivedMessages = await Promise.all(messagePromises); - receivedMessages.forEach((msg) => { - expect(msg.message).toBe(testMessage); - expect(msg.playerName).toBe('Player1'); - }); - }); - }); - - describe('updatePosition 이벤트 테스트', () => { - it('위치 업데이트 성공', async () => { - // 방 생성 및 참여 설정 - const createResponse = await new Promise<{ gameId: string }>((resolve) => { - client1.once(socketEvents.CREATE_ROOM, resolve); - client1.emit(socketEvents.CREATE_ROOM, { - title: 'Chat Test Room', - gameMode: 'RANKING', - maxPlayerCount: 5, - isPublic: true - }); - }); - - // 플레이어들 입장 - await Promise.all([ - new Promise((resolve) => { - client1.once(socketEvents.JOIN_ROOM, () => resolve()); - client1.emit(socketEvents.JOIN_ROOM, { - gameId: createResponse.gameId, - playerName: 'Player1' - }); - }), - new Promise((resolve) => { - client2.once(socketEvents.JOIN_ROOM, () => resolve()); - client2.emit(socketEvents.JOIN_ROOM, { - gameId: createResponse.gameId, - playerName: 'Player2' - }); - }) - ]); - const newPosition = [0.2, 0.5]; - - const updateResponse = await new Promise((resolve) => { - client1.once(socketEvents.UPDATE_POSITION, resolve); - client1.emit(socketEvents.UPDATE_POSITION, { - gameId: createResponse.gameId, - newPosition - }); - }); - - expect(updateResponse.playerPosition).toEqual(newPosition); - - // Redis에서 위치 정보 확인 - const playerData = await redisMock.hgetall(`Player:${client1.id}`); - expect(parseFloat(playerData.positionX)).toBe(newPosition[0]); - expect(parseFloat(playerData.positionY)).toBe(newPosition[1]); - }); - }); - - describe('startGame 이벤트 테스트', () => { - it('게임 시작할 때 초기 설정 성공', async () => { - // 방 생성 및 참여 설정 - const createResponse = await new Promise<{ gameId: string }>((resolve) => { - client1.once(socketEvents.CREATE_ROOM, resolve); - client1.emit(socketEvents.CREATE_ROOM, { - title: 'Chat Test Room', - gameMode: 'RANKING', - maxPlayerCount: 5, - isPublic: true - }); - }); - - // 플레이어들 입장 - await Promise.all([ - new Promise((resolve) => { - client1.once(socketEvents.JOIN_ROOM, () => resolve()); - client1.emit(socketEvents.JOIN_ROOM, { - gameId: createResponse.gameId, - playerName: 'Player1' - }); - }), - new Promise((resolve) => { - client2.once(socketEvents.JOIN_ROOM, () => resolve()); - client2.emit(socketEvents.JOIN_ROOM, { - gameId: createResponse.gameId, - playerName: 'Player2' - }); - }) - ]); - const gameId = createResponse.gameId; - - client1.emit(socketEvents.START_GAME, { - gameId - }); - - await new Promise((resolve) => setTimeout(resolve, 1500)); // 1.5초 대기 - - const quizSetIds = await redisMock.smembers(REDIS_KEY.ROOM_QUIZ_SET(gameId)); - - // 내림차순 조회 (높은 점수부터) - const leaderboard = await redisMock.zrevrange(REDIS_KEY.ROOM_LEADERBOARD(gameId), 0, -1); - - expect(gameId).toBe(createResponse.gameId); - expect(quizSetIds.length).toBeGreaterThan(0); // FIX: 추후 더 fit하게 바꾸기 - expect(leaderboard).toBeDefined(); - }); - - it('게임 시작시 quizSetTitle이 올바르게 설정되어야 한다', async () => { - // 1. 방 생성 - const createResponse = await new Promise<{ gameId: string }>((resolve) => { - client1.once(socketEvents.CREATE_ROOM, resolve); - client1.emit(socketEvents.CREATE_ROOM, { - title: 'Test Room', - gameMode: 'RANKING', - maxPlayerCount: 5, - isPublic: true - }); - }); - const gameId = createResponse.gameId; - - // 2. 플레이어 입장 - await Promise.all([ - new Promise((resolve) => { - client1.once(socketEvents.JOIN_ROOM, () => resolve()); - client1.emit(socketEvents.JOIN_ROOM, { - gameId: gameId, - playerName: 'Player1' - }); - }) - ]); - - // 3. 게임 시작 전 상태 확인 - const beforeRoom = await redisMock.hgetall(`Room:${gameId}`); - expect(beforeRoom.quizSetTitle).toBe(mockQuizData.title); - - // 4. 게임 시작 - await new Promise((resolve) => { - client1.once(socketEvents.START_GAME, () => resolve()); - client1.emit(socketEvents.START_GAME, { gameId }); - }); - - // 잠시 대기 (Redis 업데이트 대기) - await new Promise((resolve) => setTimeout(resolve, 100)); - - // 5. 게임 시작 후 상태 확인 - const afterRoom = await redisMock.hgetall(`Room:${gameId}`); - expect(afterRoom.quizSetTitle).toBe(mockQuizData.title); - }); - - it('캐시에 없는 퀴즈셋의 경우 DB에서 조회하고 캐시에 저장해야 한다.', async () => { - // 1. 방 생성 - const createResponse = await new Promise<{ gameId: string }>((resolve) => { - client1.once(socketEvents.CREATE_ROOM, resolve); - client1.emit(socketEvents.CREATE_ROOM, { - title: 'Cache Test Room', - gameMode: 'RANKING', - maxPlayerCount: 5, - isPublic: true - }); - }); - const gameId = createResponse.gameId; - - // 퀴즈셋 ID 설정 - const testQuizSetId = 1; - await redisMock.hset(`Room:${gameId}`, 'quizSetId', testQuizSetId.toString()); - - // 캐시 키 설정 - const cacheKey = REDIS_KEY.QUIZSET_ID(testQuizSetId); - - // 초기 상태 확인 - 캐시에 데이터 없어야 함 - const initialCache = await redisMock.get(cacheKey); - expect(initialCache).toBeNull(); - - // 플레이어 입장 (호스트 권한을 위해) - await new Promise((resolve) => { - client1.once(socketEvents.JOIN_ROOM, () => resolve()); - client1.emit(socketEvents.JOIN_ROOM, { - gameId: gameId, - playerName: 'Player1' - }); - }); - - //updateRoomQuizSet - await new Promise((resolve) => { - client1.once(socketEvents.UPDATE_ROOM_QUIZSET, () => resolve()); - client1.emit(socketEvents.UPDATE_ROOM_QUIZSET, { - gameId: gameId, - quizSetId: testQuizSetId, - quizCount: 1 - }); - }); - - // 5. QuizSetService mock 설정 - const mockQuizSet = { - id: String(testQuizSetId), - title: 'Test Quiz Set', - category: 'Test Category', - quizList: [ - { - id: '1', - quiz: 'Test Question 1', - choiceList: [ - { order: 1, content: 'Choice 1', isAnswer: true }, - { order: 2, content: 'Choice 2', isAnswer: false } - ], - limitTime: 30 - } - ] - }; - const quizServiceSpy = jest - .spyOn(moduleRef.get(QuizSetService), 'findOne') - .mockResolvedValue(mockQuizSet); - - // 6. 게임 시작 - await new Promise((resolve) => { - client1.once(socketEvents.START_GAME, () => resolve()); - client1.emit(socketEvents.START_GAME, { gameId }); - }); - - // Redis 업데이트 대기 - await new Promise((resolve) => setTimeout(resolve, 100)); - - // 7. 캐시 저장 확인 - const cachedData = await redisMock.get(cacheKey); - expect(cachedData).not.toBeNull(); - expect(JSON.parse(cachedData!)).toEqual(mockQuizSet); - - // QuizSetService.findOne이 호출되었는지 - expect(quizServiceSpy).toHaveBeenCalled(); - }); - - it('캐시에 있는 퀴즈셋의 경우 DB 조회 없이 캐시에서 가져와야 한다', async () => { - // 1. 방 생성 - const createResponse = await new Promise<{ gameId: string }>((resolve) => { - client1.once(socketEvents.CREATE_ROOM, resolve); - client1.emit(socketEvents.CREATE_ROOM, { - title: 'Cache Hit Test Room', - gameMode: 'RANKING', - maxPlayerCount: 5, - isPublic: true - }); - }); - const gameId = createResponse.gameId; - - // 2. 퀴즈셋 ID 설정 - const testQuizSetId = 2; - await redisMock.hset(`Room:${gameId}`, 'quizSetId', testQuizSetId.toString()); - - // 플레이어 입장 (호스트 권한을 위해) - await new Promise((resolve) => { - client1.once(socketEvents.JOIN_ROOM, () => resolve()); - client1.emit(socketEvents.JOIN_ROOM, { - gameId: gameId, - playerName: 'Player1' - }); - }); - - //updateRoomQuizSet - await new Promise((resolve) => { - client1.once(socketEvents.UPDATE_ROOM_QUIZSET, () => resolve()); - client1.emit(socketEvents.UPDATE_ROOM_QUIZSET, { - gameId: gameId, - quizSetId: testQuizSetId, - quizCount: 1 - }); - }); - - // 3. 미리 캐시에 데이터 저장 - const cachedQuizSet = { - id: testQuizSetId, - title: 'Cached Quiz Set', - quizList: [ - { - id: 1, - quiz: 'Cached Question', - choiceList: [ - { order: 1, content: 'Cached Choice 1', isAnswer: true }, - { order: 2, content: 'Cached Choice 2', isAnswer: false } - ], - limitTime: 30 - } - ] - }; - await redisMock.set( - REDIS_KEY.QUIZSET_ID(testQuizSetId), - JSON.stringify(cachedQuizSet), - 'EX', - 1800 - ); - - // 4. QuizSetService mock 설정 (호출되지 않아야 함) - const quizServiceSpy = jest.spyOn(moduleRef.get(QuizSetService), 'findOne'); - - // 5. 게임 시작 - await new Promise((resolve) => { - client1.once(socketEvents.START_GAME, () => resolve()); - client1.emit(socketEvents.START_GAME, { gameId }); - }); - - // Redis 업데이트 대기 - await new Promise((resolve) => setTimeout(resolve, 100)); - - // 6. 검증 - // QuizSetService.findOne이 호출되지 않았는지 확인 - expect(quizServiceSpy).not.toHaveBeenCalled(); - - // Room에 설정된 title이 캐시된 데이터의 title과 일치하는지 확인 - const roomData = await redisMock.hgetall(`Room:${gameId}`); - expect(roomData.quizSetTitle).toBe(cachedQuizSet.title); - }); - - it('캐시가 만료되면 DB에서 다시 조회해야 한다', async () => { - // 1. 방 생성 - const createResponse = await new Promise<{ gameId: string }>((resolve) => { - client1.once(socketEvents.CREATE_ROOM, resolve); - client1.emit(socketEvents.CREATE_ROOM, { - title: 'Expiry Test Room', - gameMode: 'RANKING', - maxPlayerCount: 5, - isPublic: true - }); - }); - const gameId = createResponse.gameId; - - // 2. 퀴즈셋 ID 설정 - const testQuizSetId = 3; - await redisMock.hset(`Room:${gameId}`, 'quizSetId', testQuizSetId.toString()); - - // 플레이어 입장 (호스트 권한을 위해) - await new Promise((resolve) => { - client1.once(socketEvents.JOIN_ROOM, () => resolve()); - client1.emit(socketEvents.JOIN_ROOM, { - gameId: gameId, - playerName: 'Player1' - }); - }); - - //updateRoomQuizSet - await new Promise((resolve) => { - client1.once(socketEvents.UPDATE_ROOM_QUIZSET, () => resolve()); - client1.emit(socketEvents.UPDATE_ROOM_QUIZSET, { - gameId: gameId, - quizSetId: testQuizSetId, - quizCount: 1 - }); - }); - - // 3. 캐시에 데이터 저장 (1초 후 만료) - const cachedQuizSet = { - id: testQuizSetId, - title: 'Soon to Expire Quiz Set', - quizList: [ - { - id: 1, - quiz: 'Question', - choiceList: [ - { order: 1, content: 'Choice 1', isAnswer: true }, - { order: 2, content: 'Choice 2', isAnswer: false } - ], - limitTime: 30 - } - ] - }; - await redisMock.set(`quizset:${testQuizSetId}`, JSON.stringify(cachedQuizSet), 'EX', 1); - - // 4. DB에서 가져올 새로운 데이터 설정 - const newQuizSet = { - id: '3', - title: 'New Quiz Set', - category: 'Test Category', - quizList: [ - { - id: '1', - quiz: 'Test Question 1', - choiceList: [ - { order: 1, content: 'Choice 1', isAnswer: true }, - { order: 2, content: 'Choice 2', isAnswer: false } - ], - limitTime: 30 - } - ] - }; - jest.spyOn(moduleRef.get(QuizSetService), 'findOne').mockResolvedValue(newQuizSet); - - // 5. 캐시 만료 대기 - await new Promise((resolve) => setTimeout(resolve, 1100)); - - // 6. 게임 시작 - await new Promise((resolve) => { - client1.once(socketEvents.START_GAME, () => resolve()); - client1.emit(socketEvents.START_GAME, { gameId }); - }); - - // Redis 업데이트 대기 - await new Promise((resolve) => setTimeout(resolve, 100)); - - // 7. 검증 - const roomData = await redisMock.hgetall(`Room:${gameId}`); - expect(roomData.quizSetTitle).toBe(newQuizSet.title); - }); - }); -}); diff --git a/BE/test/integration/game/game-chat.integratoin.spec.ts b/BE/test/integration/game/game-chat.integratoin.spec.ts new file mode 100644 index 0000000..4c78c4d --- /dev/null +++ b/BE/test/integration/game/game-chat.integratoin.spec.ts @@ -0,0 +1,61 @@ +import { SocketTestHelper } from '../setup/socket.helper'; +import { setupTestingModule } from '../setup/game.setup'; +import socketEvents from '../../../src/common/constants/socket-events'; +import { createRoom, joinRoom } from '../setup/util'; + +describe('Game Chat 통합테스트', () => { + let app; + let redisMock; + let socketHelper: SocketTestHelper; + let client1, client2, client3; + const TEST_PORT = 3001; + + beforeAll(async () => { + const setup = await setupTestingModule(); + app = setup.app; + redisMock = setup.redisMock; + socketHelper = new SocketTestHelper(); + }); + + beforeEach(async () => { + await redisMock.flushall(); + + [client1, client2, client3] = await socketHelper.connectClients(TEST_PORT, 3); + }); + + afterEach(async () => { + await socketHelper.disconnectAll(); + await redisMock.flushall(); + }); + + afterAll(async () => { + if (app) { + await app.close(); + } + }); + + describe('chatMessage 이벤트 테스트', () => { + it('같은 방의 모든 플레이어에게 메시지 전송', async () => { + const createResponse = await createRoom(client1); + const joinResponse = await joinRoom(client2, createResponse.gameId); + const joinResponse2 = await joinRoom(client3, createResponse.gameId); + + const testMessage = 'Hello, everyone!'; + const messagePromises = [ + new Promise((resolve) => client1.once(socketEvents.CHAT_MESSAGE, resolve)), + new Promise((resolve) => client2.once(socketEvents.CHAT_MESSAGE, resolve)) + ]; + + client1.emit(socketEvents.CHAT_MESSAGE, { + gameId: createResponse.gameId, + message: testMessage + }); + + const receivedMessages = await Promise.all(messagePromises); + receivedMessages.forEach((msg) => { + expect(msg.message).toBe(testMessage); + expect(msg.playerName).toBe('Player1'); + }); + }); + }); +}); \ No newline at end of file diff --git a/BE/test/integration/game/game-room.integration.spec.ts b/BE/test/integration/game/game-room.integration.spec.ts new file mode 100644 index 0000000..84a45a9 --- /dev/null +++ b/BE/test/integration/game/game-room.integration.spec.ts @@ -0,0 +1,152 @@ +import { SocketTestHelper } from '../setup/socket.helper'; +import { setupTestingModule } from '../setup/game.setup'; +import { createRoom, joinRoom } from '../setup/util'; +import socketEvents from '../../../src/common/constants/socket-events'; + +describe('Game Room 통합테스트', () => { + let app; + let redisMock; + let socketHelper: SocketTestHelper; + let client1, client2, client3; + const TEST_PORT = 3001; + + beforeAll(async () => { + const setup = await setupTestingModule(); + app = setup.app; + redisMock = setup.redisMock; + socketHelper = new SocketTestHelper(); + }); + + beforeEach(async () => { + await redisMock.flushall(); + + [client1, client2, client3] = await socketHelper.connectClients(TEST_PORT, 3); + }); + + afterEach(async () => { + await socketHelper.disconnectAll(); + await redisMock.flushall(); + }); + + afterAll(async () => { + if (app) { + await app.close(); + } + }); + + describe('createRoom 이벤트 테스트', () => { + it('유효한 설정으로 게임방 생성 성공', async () => { + const createResponse = await createRoom(client1); + + expect(createResponse.gameId).toBeDefined(); + expect(typeof createResponse.gameId).toBe('string'); + + // 실제 Redis 저장 확인 + const roomData = await redisMock.hgetall(`Room:${createResponse.gameId}`); + expect(roomData).toBeDefined(); + expect(roomData.title).toBeDefined(); + expect(roomData.gameMode).toBeDefined(); + expect(roomData.maxPlayerCount).toBeDefined(); + }); + }); + + describe('createRoom 이벤트 실패 케이스', () => { + test.each([ + [ + '빈 title', + { title: '', gameMode: 'RANKING', maxPlayerCount: 2, isPublic: true } + ], + [ + '빈 gameMode', + { title: 'hello', gameMode: '', maxPlayerCount: 2, isPublic: true } + ], + [ + '잘못된 gameMode', + { title: 'hello', gameMode: 'invalid', maxPlayerCount: 2, isPublic: true } + ], + [ + '최소 인원 미달', + { title: 'hello', gameMode: 'RANKING', maxPlayerCount: 0, isPublic: true } + ] + ])('%s인 경우 에러 발생', async (testCase, config) => { + const errorPromise = new Promise(resolve => { + client1.once('exception', resolve); + }); + + client1.emit(socketEvents.CREATE_ROOM, config); + + const error = await errorPromise as any; + expect(error).toBeDefined(); + expect(error.eventName).toBe(socketEvents.CREATE_ROOM); + }); + }); + + describe('joinRoom 이벤트 테스트', () => { + it('존재하는 방 참여 성공', async () => { + const createResponse = await createRoom(client1); + const joinResponse = await joinRoom(client2, createResponse.gameId); + + expect(joinResponse.players).toBeDefined(); + + // Redis에서 플레이어 정보 확인 + const playerData = await redisMock.hgetall(`Player:${client2.id}`); + expect(playerData).toBeDefined(); + expect(playerData.playerName).toBe('TestPlayer'); + }); + + it('존재하지 않는 방 참여 실패', (done) => { + client1.once('exception', (error) => { + expect(error.eventName).toBe('joinRoom'); + expect(error.message).toBe('존재하지 않는 게임 방입니다.'); + done(); + }); + + client1.emit(socketEvents.JOIN_ROOM, { + gameId: '999999', + playerName: 'TestPlayer' + }); + }); + + // TODO: JOIN_ROOM 바꾼 후 테스트 코드 변경 필요 + // it('게임 진행 중인 방 참여 실패', async (done) => { + // let gameId: string; + // + // // exception 이벤트 리스너 먼저 등록 + // client2.once('exception', (error) => { + // try { + // expect(error.eventName).toBe('joinRoom'); + // expect(error.message).toBe(ExceptionMessage.GAME_ALREADY_STARTED); + // done(); + // } catch (err) { + // done(err); + // } + // }); + // + // // 방 생성 + // const createResponse = await new Promise<{ gameId: string }>((resolve) => { + // client1.once(socketEvents.CREATE_ROOM, resolve); + // client1.emit(socketEvents.CREATE_ROOM, { + // title: 'Test Room', + // gameMode: 'RANKING', + // maxPlayerCount: 5, + // isPublic: true + // }); + // }); + // + // client1.once(socketEvents.JOIN_ROOM, () => { + // // 게임 시작 + // client1.emit(socketEvents.START_GAME, { gameId }); + // }); + // + // // 방 참여 + // const joinResponse = await new Promise((resolve) => { + // client2.once(socketEvents.JOIN_ROOM, resolve); + // client2.emit(socketEvents.JOIN_ROOM, { + // gameId: createResponse.gameId, + // playerName: 'TestPlayer' + // }); + // }); + // + // }); + }); +}); \ No newline at end of file diff --git a/BE/test/integration/game/game-survival.integration.spec.ts b/BE/test/integration/game/game-survival.integration.spec.ts new file mode 100644 index 0000000..d802dec --- /dev/null +++ b/BE/test/integration/game/game-survival.integration.spec.ts @@ -0,0 +1,166 @@ +import { SocketTestHelper } from '../setup/socket.helper'; +import { setupTestingModule } from '../setup/game.setup'; +import socketEvents from '../../../src/common/constants/socket-events'; +import { createRoom, joinRoom } from '../setup/util'; + +describe('Game Survival 통합테스트', () => { + let app; + let redisMock; + let socketHelper: SocketTestHelper; + let client1, client2, client3; + const TEST_PORT = 3001; + + beforeAll(async () => { + const setup = await setupTestingModule(); + app = setup.app; + redisMock = setup.redisMock; + socketHelper = new SocketTestHelper(); + }); + + beforeEach(async () => { + await redisMock.flushall(); + + [client1, client2, client3] = await socketHelper.connectClients(TEST_PORT, 3); + }); + + afterEach(async () => { + await socketHelper.disconnectAll(); + await redisMock.flushall(); + }); + + afterAll(async () => { + if (app) { + await app.close(); + } + }); + + // describe('관전자끼리 플레이 테스트', () => { + // it ('관전자의 메시지가 생존자에게 보이지 않아야 한다.', async () => { + // const createResponse = await createRoom(client1); + // const joinResponse = await joinRoom(client1, createResponse.gameId); + // const joinResponse2 = await joinRoom(client2, createResponse.gameId); + // const joinResponse3 = await joinRoom(client3, createResponse.gameId); + // + // // mock 퀴즈셋으로 변경 + // jest.mock('../../mocks/quiz-data.mock.ts', () => ({ + // mockQuizData: { + // id: '1', + // title: '기본 퀴즈셋', + // category: 'common', + // quizList: [ + // { + // id: '1', + // quiz: '호눅스님과 jk 님은 동갑인가요?', + // limitTime: 0.1, + // choiceList: [ + // { + // content: 'O', + // order: 1, + // isAnswer: false + // }, + // { + // content: 'X', + // order: 2, + // isAnswer: false + // }, + // { + // content: '모르겠다.', + // order: 3, + // isAnswer: false + // }, + // { + // content: '크롱', + // order: 4, + // isAnswer: true + // } + // ] + // }, + // { + // id: '2', + // quiz: '1 + 1 = ?', + // limitTime: 0.1, + // choiceList: [ + // { + // content: '1', + // order: 1, + // isAnswer: false + // }, + // { + // content: '2', + // order: 2, + // isAnswer: false + // }, + // { + // content: '3', + // order: 3, + // isAnswer: false + // }, + // { + // content: '4', + // order: 4, + // isAnswer: true + // } + // ] + // } + // ] + // } + // })); + // + // // 위치 업데이트 두 플레이어를 오답으로 처리 + // const incorrectPosition = [0.1, 0.1]; + // const correctPosition = [0.9, 0.9]; + // client1.emit(socketEvents.UPDATE_POSITION, { + // gameId: createResponse.gameId, + // newPosition: incorrectPosition + // }); + // client2.emit(socketEvents.UPDATE_POSITION, { + // gameId: createResponse.gameId, + // newPosition: incorrectPosition + // }); + // client3.emit(socketEvents.UPDATE_POSITION, { + // gameId: createResponse.gameId, + // newPosition: correctPosition + // }); + // + // // 게임 시작 + // client1.emit(socketEvents.START_GAME, { + // gameId: createResponse.gameId + // }); + // + // console.log('1 =', 1); + // + // // FIX: 왜 endQuizTime이 오지 않는 걸까? + // // 한 퀴즈 끝날 때까지 대기 + // await new Promise((resolve) => client1.once(socketEvents.END_QUIZ_TIME, resolve)); + // + // console.log('2 =', 2); + // + // // 두 플레이어 중 한 플레이어가 메시지 전송 + // const testMessage = 'Hello, everyone!'; + // const messagePromises = [ + // new Promise((resolve) => client1.once(socketEvents.CHAT_MESSAGE, resolve)), + // new Promise((resolve) => client2.once(socketEvents.CHAT_MESSAGE, resolve)), + // ]; + // const messagePromises2 = [ + // new Promise((resolve) => client3.once(socketEvents.CHAT_MESSAGE, resolve)), + // ]; + // client1.emit(socketEvents.CHAT_MESSAGE, { + // gameId: createResponse.gameId, + // message: testMessage + // }); + // + // // 검증 (생존자에게 가진 않았는지) + // const receivedMessages = await Promise.all(messagePromises); + // receivedMessages.forEach((msg) => { + // expect(msg.message).toBe(testMessage); + // expect(msg.playerName).toBe('Player1'); + // }); + // + // // 관전자에게 보이지 않아야 한다. + // const receivedMessages2 = await Promise.all(messagePromises2); + // receivedMessages2.forEach((msg) => { + // expect(msg).toBeUndefined(); + // }); + // }); + // }) +}); \ No newline at end of file diff --git a/BE/test/integration/game/game.integration.spec.ts b/BE/test/integration/game/game.integration.spec.ts new file mode 100644 index 0000000..6a2f81f --- /dev/null +++ b/BE/test/integration/game/game.integration.spec.ts @@ -0,0 +1,345 @@ +import socketEvents from '../../../src/common/constants/socket-events'; +import { createRoom, joinRoom } from '../setup/util'; +import { REDIS_KEY } from '../../../src/common/constants/redis-key.constant'; +import { SocketTestHelper } from '../setup/socket.helper'; +import { setupTestingModule } from '../setup/game.setup'; + +describe('Game 통합테스트', () => { + let app; + let redisMock; + let socketHelper: SocketTestHelper; + let client1, client2, client3; + const TEST_PORT = 3001; + + beforeAll(async () => { + const setup = await setupTestingModule(); + app = setup.app; + redisMock = setup.redisMock; + socketHelper = new SocketTestHelper(); + }); + + beforeEach(async () => { + await redisMock.flushall(); + + [client1, client2, client3] = await socketHelper.connectClients(TEST_PORT, 3); + }); + + afterEach(async () => { + await socketHelper.disconnectAll(); + await redisMock.flushall(); + }); + + afterAll(async () => { + if (app) { + await app.close(); + } + }); + + describe('updatePosition 이벤트 테스트', () => { + it('위치 업데이트 성공', async () => { + const createResponse = await createRoom(client1); + const joinResponse = await joinRoom(client1, createResponse.gameId); + const joinResponse2 = await joinRoom(client2, createResponse.gameId); + const joinResponse3 = await joinRoom(client3, createResponse.gameId); + + const newPosition = [0.2, 0.5]; + + const updateResponse = await new Promise((resolve) => { + client1.once(socketEvents.UPDATE_POSITION, resolve); + client1.emit(socketEvents.UPDATE_POSITION, { + gameId: createResponse.gameId, + newPosition + }); + }); + + expect(updateResponse.playerPosition).toEqual(newPosition); + + // Redis에서 위치 정보 확인 + const playerData = await redisMock.hgetall(`Player:${client1.id}`); + expect(parseFloat(playerData.positionX)).toBe(newPosition[0]); + expect(parseFloat(playerData.positionY)).toBe(newPosition[1]); + }); + }); + + describe('startGame 이벤트 테스트', () => { + it('게임 시작할 때 초기 설정 성공', async () => { + const createResponse = await createRoom(client1); + const joinResponse = await joinRoom(client1, createResponse.gameId); + const joinResponse2 = await joinRoom(client2, createResponse.gameId); + const joinResponse3 = await joinRoom(client3, createResponse.gameId); + + client1.emit(socketEvents.START_GAME, { + gameId: createResponse.gameId + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Redis 검증 + const quizSetIds = await redisMock.smembers(REDIS_KEY.ROOM_QUIZ_SET(createResponse.gameId)); + const leaderboard = await redisMock.zrevrange( + REDIS_KEY.ROOM_LEADERBOARD(createResponse.gameId), + 0, + -1 + ); + + const room = await redisMock.hgetall(`Room:${createResponse.gameId}`); + + expect(quizSetIds.length).toBeGreaterThan(0); + expect(leaderboard).toBeDefined(); + expect(room.quizSetTitle).toBeDefined(); + }); + + // TODO: 향후 개선 필요 (우선순위가 높진 않아 현재 PASS) + // it('캐시에 없는 퀴즈셋의 경우 DB에서 조회하고 캐시에 저장해야 한다.', async () => { + // // 1. 방 생성 + // const createResponse = await new Promise<{ gameId: string }>((resolve) => { + // client1.once(socketEvents.CREATE_ROOM, resolve); + // client1.emit(socketEvents.CREATE_ROOM, { + // title: 'Cache Test Room', + // gameMode: 'RANKING', + // maxPlayerCount: 5, + // isPublic: true + // }); + // }); + // const gameId = createResponse.gameId; + // + // // 퀴즈셋 ID 설정 + // const testQuizSetId = 1; + // await redisMock.hset(`Room:${gameId}`, 'quizSetId', testQuizSetId.toString()); + // + // // 캐시 키 설정 + // const cacheKey = REDIS_KEY.QUIZSET_ID(testQuizSetId); + // + // // 초기 상태 확인 - 캐시에 데이터 없어야 함 + // const initialCache = await redisMock.get(cacheKey); + // expect(initialCache).toBeNull(); + // + // // 플레이어 입장 (호스트 권한을 위해) + // await new Promise((resolve) => { + // client1.once(socketEvents.JOIN_ROOM, () => resolve()); + // client1.emit(socketEvents.JOIN_ROOM, { + // gameId: gameId, + // playerName: 'Player1' + // }); + // }); + // + // //updateRoomQuizSet + // await new Promise((resolve) => { + // client1.once(socketEvents.UPDATE_ROOM_QUIZSET, () => resolve()); + // client1.emit(socketEvents.UPDATE_ROOM_QUIZSET, { + // gameId: gameId, + // quizSetId: testQuizSetId, + // quizCount: 1 + // }); + // }); + // + // // 5. QuizSetService mock 설정 + // const mockQuizSet = { + // id: String(testQuizSetId), + // title: 'Test Quiz Set', + // category: 'Test Category', + // quizList: [ + // { + // id: '1', + // quiz: 'Test Question 1', + // choiceList: [ + // { order: 1, content: 'Choice 1', isAnswer: true }, + // { order: 2, content: 'Choice 2', isAnswer: false } + // ], + // limitTime: 30 + // } + // ] + // }; + // const quizServiceSpy = jest + // .spyOn(moduleRef.get(QuizSetService), 'findOne') + // .mockResolvedValue(mockQuizSet); + // + // // 6. 게임 시작 + // await new Promise((resolve) => { + // client1.once(socketEvents.START_GAME, () => resolve()); + // client1.emit(socketEvents.START_GAME, { gameId }); + // }); + // + // // Redis 업데이트 대기 + // await new Promise((resolve) => setTimeout(resolve, 100)); + // + // // 7. 캐시 저장 확인 + // const cachedData = await redisMock.get(cacheKey); + // expect(cachedData).not.toBeNull(); + // expect(JSON.parse(cachedData!)).toEqual(mockQuizSet); + // + // // QuizSetService.findOne이 호출되었는지 + // expect(quizServiceSpy).toHaveBeenCalled(); + // }); + // + // it('캐시에 있는 퀴즈셋의 경우 DB 조회 없이 캐시에서 가져와야 한다', async () => { + // // 1. 방 생성 + // const createResponse = await new Promise<{ gameId: string }>((resolve) => { + // client1.once(socketEvents.CREATE_ROOM, resolve); + // client1.emit(socketEvents.CREATE_ROOM, { + // title: 'Cache Hit Test Room', + // gameMode: 'RANKING', + // maxPlayerCount: 5, + // isPublic: true + // }); + // }); + // const gameId = createResponse.gameId; + // + // // 2. 퀴즈셋 ID 설정 + // const testQuizSetId = 2; + // await redisMock.hset(`Room:${gameId}`, 'quizSetId', testQuizSetId.toString()); + // + // // 플레이어 입장 (호스트 권한을 위해) + // await new Promise((resolve) => { + // client1.once(socketEvents.JOIN_ROOM, () => resolve()); + // client1.emit(socketEvents.JOIN_ROOM, { + // gameId: gameId, + // playerName: 'Player1' + // }); + // }); + // + // //updateRoomQuizSet + // await new Promise((resolve) => { + // client1.once(socketEvents.UPDATE_ROOM_QUIZSET, () => resolve()); + // client1.emit(socketEvents.UPDATE_ROOM_QUIZSET, { + // gameId: gameId, + // quizSetId: testQuizSetId, + // quizCount: 1 + // }); + // }); + // + // // 3. 미리 캐시에 데이터 저장 + // const cachedQuizSet = { + // id: testQuizSetId, + // title: 'Cached Quiz Set', + // quizList: [ + // { + // id: 1, + // quiz: 'Cached Question', + // choiceList: [ + // { order: 1, content: 'Cached Choice 1', isAnswer: true }, + // { order: 2, content: 'Cached Choice 2', isAnswer: false } + // ], + // limitTime: 30 + // } + // ] + // }; + // await redisMock.set( + // REDIS_KEY.QUIZSET_ID(testQuizSetId), + // JSON.stringify(cachedQuizSet), + // 'EX', + // 1800 + // ); + // + // // 4. QuizSetService mock 설정 (호출되지 않아야 함) + // const quizServiceSpy = jest.spyOn(moduleRef.get(QuizSetService), 'findOne'); + // + // // 5. 게임 시작 + // await new Promise((resolve) => { + // client1.once(socketEvents.START_GAME, () => resolve()); + // client1.emit(socketEvents.START_GAME, { gameId }); + // }); + // + // // Redis 업데이트 대기 + // await new Promise((resolve) => setTimeout(resolve, 100)); + // + // // 6. 검증 + // // QuizSetService.findOne이 호출되지 않았는지 확인 + // expect(quizServiceSpy).not.toHaveBeenCalled(); + // + // // Room에 설정된 title이 캐시된 데이터의 title과 일치하는지 확인 + // const roomData = await redisMock.hgetall(`Room:${gameId}`); + // expect(roomData.quizSetTitle).toBe(cachedQuizSet.title); + // }); + // + // it('캐시가 만료되면 DB에서 다시 조회해야 한다', async () => { + // // 1. 방 생성 + // const createResponse = await new Promise<{ gameId: string }>((resolve) => { + // client1.once(socketEvents.CREATE_ROOM, resolve); + // client1.emit(socketEvents.CREATE_ROOM, { + // title: 'Expiry Test Room', + // gameMode: 'RANKING', + // maxPlayerCount: 5, + // isPublic: true + // }); + // }); + // const gameId = createResponse.gameId; + // + // // 2. 퀴즈셋 ID 설정 + // const testQuizSetId = 3; + // await redisMock.hset(`Room:${gameId}`, 'quizSetId', testQuizSetId.toString()); + // + // // 플레이어 입장 (호스트 권한을 위해) + // await new Promise((resolve) => { + // client1.once(socketEvents.JOIN_ROOM, () => resolve()); + // client1.emit(socketEvents.JOIN_ROOM, { + // gameId: gameId, + // playerName: 'Player1' + // }); + // }); + // + // //updateRoomQuizSet + // await new Promise((resolve) => { + // client1.once(socketEvents.UPDATE_ROOM_QUIZSET, () => resolve()); + // client1.emit(socketEvents.UPDATE_ROOM_QUIZSET, { + // gameId: gameId, + // quizSetId: testQuizSetId, + // quizCount: 1 + // }); + // }); + // + // // 3. 캐시에 데이터 저장 (1초 후 만료) + // const cachedQuizSet = { + // id: testQuizSetId, + // title: 'Soon to Expire Quiz Set', + // quizList: [ + // { + // id: 1, + // quiz: 'Question', + // choiceList: [ + // { order: 1, content: 'Choice 1', isAnswer: true }, + // { order: 2, content: 'Choice 2', isAnswer: false } + // ], + // limitTime: 30 + // } + // ] + // }; + // await redisMock.set(`quizset:${testQuizSetId}`, JSON.stringify(cachedQuizSet), 'EX', 1); + // + // // 4. DB에서 가져올 새로운 데이터 설정 + // const newQuizSet = { + // id: '3', + // title: 'New Quiz Set', + // category: 'Test Category', + // quizList: [ + // { + // id: '1', + // quiz: 'Test Question 1', + // choiceList: [ + // { order: 1, content: 'Choice 1', isAnswer: true }, + // { order: 2, content: 'Choice 2', isAnswer: false } + // ], + // limitTime: 30 + // } + // ] + // }; + // jest.spyOn(moduleRef.get(QuizSetService), 'findOne').mockResolvedValue(newQuizSet); + // + // // 5. 캐시 만료 대기 + // await new Promise((resolve) => setTimeout(resolve, 1100)); + // + // // 6. 게임 시작 + // await new Promise((resolve) => { + // client1.once(socketEvents.START_GAME, () => resolve()); + // client1.emit(socketEvents.START_GAME, { gameId }); + // }); + // + // // Redis 업데이트 대기 + // await new Promise((resolve) => setTimeout(resolve, 100)); + // + // // 7. 검증 + // const roomData = await redisMock.hgetall(`Room:${gameId}`); + // expect(roomData.quizSetTitle).toBe(newQuizSet.title); + // }); + }); +}); diff --git a/BE/test/integration/quiz.integration.spec.ts b/BE/test/integration/quiz-set/quiz.integration.spec.ts similarity index 93% rename from BE/test/integration/quiz.integration.spec.ts rename to BE/test/integration/quiz-set/quiz.integration.spec.ts index 33f3f89..52e4087 100644 --- a/BE/test/integration/quiz.integration.spec.ts +++ b/BE/test/integration/quiz-set/quiz.integration.spec.ts @@ -3,19 +3,19 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { DataSource, QueryRunner } from 'typeorm'; import { BadRequestException } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { QuizSetService } from '../../src/quiz-set/service/quiz-set.service'; -import { QuizSetModel } from '../../src/quiz-set/entities/quiz-set.entity'; -import { QuizModel } from '../../src/quiz-set/entities/quiz.entity'; -import { QuizChoiceModel } from '../../src/quiz-set/entities/quiz-choice.entity'; -import { UserModel } from '../../src/user/entities/user.entity'; -import { UserQuizArchiveModel } from '../../src/user/entities/user-quiz-archive.entity'; -import { CreateQuizSetDto } from '../../src/quiz-set/dto/create-quiz.dto'; -import { QuizSetCreateService } from '../../src/quiz-set/service/quiz-set-create.service'; -import { QuizSetReadService } from '../../src/quiz-set/service/quiz-set-read.service'; -import { QuizSetUpdateService } from '../../src/quiz-set/service/quiz-set-update.service'; -import { QuizSetDeleteService } from '../../src/quiz-set/service/quiz-set-delete.service'; -import { UserService } from '../../src/user/user.service'; -import { AuthModule } from '../../src/auth/auth.module'; +import { QuizSetService } from '../../../src/quiz-set/service/quiz-set.service'; +import { QuizSetModel } from '../../../src/quiz-set/entities/quiz-set.entity'; +import { QuizModel } from '../../../src/quiz-set/entities/quiz.entity'; +import { QuizChoiceModel } from '../../../src/quiz-set/entities/quiz-choice.entity'; +import { UserModel } from '../../../src/user/entities/user.entity'; +import { UserQuizArchiveModel } from '../../../src/user/entities/user-quiz-archive.entity'; +import { CreateQuizSetDto } from '../../../src/quiz-set/dto/create-quiz.dto'; +import { QuizSetCreateService } from '../../../src/quiz-set/service/quiz-set-create.service'; +import { QuizSetReadService } from '../../../src/quiz-set/service/quiz-set-read.service'; +import { QuizSetUpdateService } from '../../../src/quiz-set/service/quiz-set-update.service'; +import { QuizSetDeleteService } from '../../../src/quiz-set/service/quiz-set-delete.service'; +import { UserService } from '../../../src/user/user.service'; +import { AuthModule } from '../../../src/auth/auth.module'; describe('QuizService', () => { let quizService: QuizSetService; diff --git a/BE/test/integration/setup/game.setup.ts b/BE/test/integration/setup/game.setup.ts new file mode 100644 index 0000000..a11b39d --- /dev/null +++ b/BE/test/integration/setup/game.setup.ts @@ -0,0 +1,32 @@ +import { Test } from '@nestjs/testing'; +import { IoAdapter } from '@nestjs/platform-socket.io'; +import RedisMock from 'ioredis-mock'; +import { AppModule } from '../../../src/app.module'; + +export const TEST_PORT = 3001; + +export async function setupTestingModule() { + const redisMock = new RedisMock(); + jest.spyOn(redisMock, 'config').mockImplementation(() => Promise.resolve('OK')); + + const originalHset = redisMock.hset.bind(redisMock); + redisMock.hset = async function (key: string, ...args: any[]) { + const result = await originalHset(key, ...args); + await this.publish(`__keyspace@0__:${key}`, 'hset'); + return result; + }; + + const moduleRef = await Test.createTestingModule({ + imports: [AppModule], + }) + .overrideProvider('default_IORedisModuleConnectionToken') + .useValue(redisMock) + .compile(); + + const app = moduleRef.createNestApplication(); + app.useWebSocketAdapter(new IoAdapter(app)); + await app.init(); + await app.listen(TEST_PORT); + + return { app, moduleRef, redisMock }; +} \ No newline at end of file diff --git a/BE/test/integration/setup/util.ts b/BE/test/integration/setup/util.ts new file mode 100644 index 0000000..320aba4 --- /dev/null +++ b/BE/test/integration/setup/util.ts @@ -0,0 +1,25 @@ +import socketEvents from '../../../src/common/constants/socket-events'; + +export async function createRoom(client) { + const createResponse = await new Promise<{ gameId: string }>((resolve) => { + client.once(socketEvents.CREATE_ROOM, resolve); + client.emit(socketEvents.CREATE_ROOM, { + title: 'Test Room', + gameMode: 'RANKING', + maxPlayerCount: 5, + isPublic: true + }); + }); + return createResponse; +} + +export async function joinRoom(client, gameId) { + const joinResponse = await new Promise((resolve) => { + client.once(socketEvents.JOIN_ROOM, resolve); + client.emit(socketEvents.JOIN_ROOM, { + gameId: gameId, + playerName: 'TestPlayer' + }); + }); + return joinResponse; +} \ No newline at end of file From 519f61472a2791291522980c4129a237fec3e7f5 Mon Sep 17 00:00:00 2001 From: NewCodes7 Date: Tue, 26 Nov 2024 01:49:16 +0900 Subject: [PATCH 4/6] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=ED=8C=8C=EC=9D=BC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/test/integration/setup/socket.helper.ts | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 BE/test/integration/setup/socket.helper.ts diff --git a/BE/test/integration/setup/socket.helper.ts b/BE/test/integration/setup/socket.helper.ts new file mode 100644 index 0000000..374d905 --- /dev/null +++ b/BE/test/integration/setup/socket.helper.ts @@ -0,0 +1,44 @@ +import { io, Socket } from 'socket.io-client'; + +export class SocketTestHelper { + private clients: Socket[] = []; + + async connectClients(port: number, count: number): Promise { + await this.disconnectAll(); // 기존 연결이 있다면 정리 + + return new Promise((resolve) => { + let connectedClients = 0; + const onConnect = () => { + connectedClients++; + if (connectedClients === count) { + resolve(this.clients); + } + }; + + for (let i = 0; i < count; i++) { + const client = io(`http://localhost:${port}/game`, { + transports: ['websocket'], + forceNew: true + }); + client.on('connect', onConnect); + this.clients.push(client); + } + }); + } + + async disconnectAll() { + for (const client of this.clients) { + if (client && client.connected) { + client.disconnect(); + } + } + this.clients = []; + } + + getClient(index: number): Socket { + if (index < 0 || index >= this.clients.length) { + throw new Error(`Invalid client index: ${index}`); + } + return this.clients[index]; + } +} \ No newline at end of file From 2293758331338b305d97edce2b4be486c6a46a21 Mon Sep 17 00:00:00 2001 From: NewCodes7 Date: Tue, 26 Nov 2024 01:49:37 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=EC=84=9C=EB=B0=94=EC=9D=B4?= =?UTF-8?q?=EB=B2=8C=20=EB=AA=A8=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/common/filters/ws-exception.filter.ts | 2 +- .../redis/subscribers/player.subscriber.ts | 32 ++++++++++-- .../redis/subscribers/scoring.subscriber.ts | 4 +- .../redis/subscribers/timer.subscriber.ts | 51 +++++++++++++++---- BE/src/game/service/game.chat.service.ts | 29 +++++++++-- .../user/entities/user-quiz-archive.entity.ts | 6 +-- 6 files changed, 95 insertions(+), 29 deletions(-) diff --git a/BE/src/common/filters/ws-exception.filter.ts b/BE/src/common/filters/ws-exception.filter.ts index 22134c0..43806bd 100644 --- a/BE/src/common/filters/ws-exception.filter.ts +++ b/BE/src/common/filters/ws-exception.filter.ts @@ -11,7 +11,7 @@ export class WsExceptionFilter extends BaseWsExceptionFilter { // ValidationPipe에서 발생한 에러 처리 if (exception instanceof GameWsException) { - this.logger.error(`Validation Error: ${JSON.stringify(exception.message)}`); + this.logger.error(`Validation Error: ${JSON.stringify(exception.message)}`, exception.stack); client.emit('exception', { eventName: exception.eventName, diff --git a/BE/src/game/redis/subscribers/player.subscriber.ts b/BE/src/game/redis/subscribers/player.subscriber.ts index 6d2b61e..23864e6 100644 --- a/BE/src/game/redis/subscribers/player.subscriber.ts +++ b/BE/src/game/redis/subscribers/player.subscriber.ts @@ -67,11 +67,33 @@ export class PlayerSubscriber extends RedisSubscriber { } private async handlePlayerPosition(playerId: string, playerData: any, server: Server) { - server.to(playerData.gameId).emit(SocketEvents.UPDATE_POSITION, { - playerId, - playerPosition: [parseFloat(playerData.positionX), parseFloat(playerData.positionY)] - }); - this.logger.verbose(`Player position updated: ${playerId}`); + const { gameId, positionX, positionY } = playerData; + const playerPosition = [parseFloat(positionX), parseFloat(positionY)]; + const updateData = { playerId, playerPosition }; + + const isAlivePlayer = await this.redis.hget(REDIS_KEY.PLAYER(playerId), 'isAlive'); + + if (isAlivePlayer === '1') { + server.to(gameId).emit(SocketEvents.UPDATE_POSITION, updateData); + } else if (isAlivePlayer === '0') { + const players = await this.redis.smembers(REDIS_KEY.ROOM_PLAYERS(gameId)); + const deadPlayers = await Promise.all( + players.map(async (id) => { + const isAlive = await this.redis.hget(REDIS_KEY.PLAYER(id), 'isAlive'); + return { id, isAlive }; + }) + ); + + deadPlayers + .filter(player => player.isAlive === '0') + .forEach(player => { + server.to(player.id).emit(SocketEvents.UPDATE_POSITION, updateData); + }); + } + + this.logger.verbose( + `[updatePosition] RoomId: ${gameId} | playerId: ${playerId} | isAlive: ${isAlivePlayer === '1' ? '생존자' : '관전자'} | position: [${positionX}, ${positionY}]` + ); } private async handlePlayerDisconnect(playerId: string, playerData: any, server: Server) { diff --git a/BE/src/game/redis/subscribers/scoring.subscriber.ts b/BE/src/game/redis/subscribers/scoring.subscriber.ts index a0b840d..376070d 100644 --- a/BE/src/game/redis/subscribers/scoring.subscriber.ts +++ b/BE/src/game/redis/subscribers/scoring.subscriber.ts @@ -48,11 +48,11 @@ export class ScoringSubscriber extends RedisSubscriber { }); await this.updateQuizState(gameId, quiz.quizNum); - this.logger.verbose(`endQuizTime: ${gameId} - ${quiz.quizNum}`); + this.logger.verbose(`[endQuizTime] RoomId: ${gameId} | quizNum: ${quiz.quizNum}`); } private async updateQuizState(gameId: string, quizNum: number) { - await this.redis.set(REDIS_KEY.ROOM_CURRENT_QUIZ(gameId), `${quizNum}:end`); + await this.redis.set(REDIS_KEY.ROOM_CURRENT_QUIZ(gameId), `${quizNum}:end`); // timer.subscriber.ts 구독 핸들러 실행 await this.redis.set(REDIS_KEY.ROOM_TIMER(gameId), 'timer', 'EX', '10', 'NX'); } } diff --git a/BE/src/game/redis/subscribers/timer.subscriber.ts b/BE/src/game/redis/subscribers/timer.subscriber.ts index cb2dc4b..b29eb7b 100644 --- a/BE/src/game/redis/subscribers/timer.subscriber.ts +++ b/BE/src/game/redis/subscribers/timer.subscriber.ts @@ -5,6 +5,7 @@ import Redis from 'ioredis'; import { Server } from 'socket.io'; import { REDIS_KEY } from '../../../common/constants/redis-key.constant'; import SocketEvents from '../../../common/constants/socket-events'; +import { GameMode } from '../../../common/constants/game-mode'; @Injectable() export class TimerSubscriber extends RedisSubscriber { @@ -46,40 +47,68 @@ export class TimerSubscriber extends RedisSubscriber { const sockets = await server.in(gameId).fetchSockets(); const clients = sockets.map((socket) => socket.id); + const correctPlayers = []; + const inCorrectPlayers = []; // 플레이어 답안 처리 for (const clientId of clients) { const player = await this.redis.hgetall(REDIS_KEY.PLAYER(clientId)); + + if (player.isAlive === '0') { + continue; + } + const selectAnswer = this.calculateAnswer(player.positionX, player.positionY); await this.redis.set(`${REDIS_KEY.PLAYER(clientId)}:Changes`, 'AnswerCorrect'); if (selectAnswer.toString() === quiz.answer) { correctPlayers.push(clientId); - await this.redis.hmset(REDIS_KEY.PLAYER(clientId), { isAnswerCorrect: '1' }); + await this.redis.hset(REDIS_KEY.PLAYER(clientId), { isAnswerCorrect: '1' }); } else { - await this.redis.hmset(REDIS_KEY.PLAYER(clientId), { isAnswerCorrect: '0' }); + inCorrectPlayers.push(clientId); + await this.redis.hset(REDIS_KEY.PLAYER(clientId), { isAnswerCorrect: '0' }); } } // 점수 업데이트 - for (const clientId of correctPlayers) { - await this.redis.zincrby( - REDIS_KEY.ROOM_LEADERBOARD(gameId), - 1000 / correctPlayers.length, - clientId - ); + const gameMode = await this.redis.hget(REDIS_KEY.ROOM(gameId), 'gameMode'); + const leaderboardKey = REDIS_KEY.ROOM_LEADERBOARD(gameId); + + if (gameMode === GameMode.RANKING) { + const score = 1000 / correctPlayers.length; + correctPlayers.forEach(clientId => { + this.redis.zincrby(leaderboardKey, score, clientId); + }); + } else if (gameMode === GameMode.SURVIVAL) { + correctPlayers.forEach(clientId => { + this.redis.zadd(leaderboardKey, 1, clientId); + }); + inCorrectPlayers.forEach(clientId => { + this.redis.zadd(leaderboardKey, 0, clientId); + this.redis.hset(REDIS_KEY.PLAYER(clientId), { isAlive: '0' }); + }); } await this.redis.publish(`scoring:${gameId}`, clients.length.toString()); - this.logger.verbose(`채점: ${gameId} - ${clients.length}`); + + this.logger.verbose( + `[Quiz] Room: ${gameId} | gameMode: ${gameMode === GameMode.SURVIVAL ? '서바이벌' : '랭킹'} | totalPlayers: ${clients.length} | ${gameMode === GameMode.SURVIVAL ? `생존자: ${correctPlayers.length}명` : `정답자: ${correctPlayers.length}명`}` + ); } private async handleNextQuiz(gameId: string, currentQuizNum: number, server: Server) { const newQuizNum = currentQuizNum + 1; const quizList = await this.redis.smembers(REDIS_KEY.ROOM_QUIZ_SET(gameId)); - if (quizList.length <= newQuizNum) { + // 생존 모드에서 모두 탈락하진 않았는지 체크 + const players = await this.redis.smembers(REDIS_KEY.ROOM_PLAYERS(gameId)); + const alivePlayers = players.filter(async (id) => { + const isAlive = await this.redis.hget(REDIS_KEY.PLAYER(id), 'isAlive'); + return isAlive === '1'; + }); + + if (quizList.length <= newQuizNum || alivePlayers.length === 0) { const leaderboard = await this.redis.zrange( REDIS_KEY.ROOM_LEADERBOARD(gameId), 0, @@ -90,7 +119,7 @@ export class TimerSubscriber extends RedisSubscriber { server.to(gameId).emit(SocketEvents.END_GAME, { host: leaderboard[0] }); - this.logger.verbose(`endGame: ${leaderboard[0]}`); + this.logger.verbose(`[endGame]: ${gameId}`); return; } diff --git a/BE/src/game/service/game.chat.service.ts b/BE/src/game/service/game.chat.service.ts index 4a25904..740ac41 100644 --- a/BE/src/game/service/game.chat.service.ts +++ b/BE/src/game/service/game.chat.service.ts @@ -37,7 +37,10 @@ export class GameChatService { timestamp: new Date() }) ); - this.logger.verbose(`채팅 전송: ${gameId} - ${clientId} (${player.playerName}) = ${message}`); + + this.logger.verbose( + `[chatMessage] Room: ${gameId} | playerId: ${clientId} | playerName: ${player.playerName} | isAlive: ${player.isAlive ? '생존자' : '관전자'} | Message: ${message}` + ); } async subscribeChatEvent(server: Server) { @@ -45,11 +48,27 @@ export class GameChatService { chatSubscriber.psubscribe('chat:*'); chatSubscriber.on('pmessage', async (_pattern, channel, message) => { - console.log(`channel: ${channel}`); // channel: chat:317172 - console.log(`message: ${message}`); // message: {"playerId":"8CT28Iw5FgjgPHNyAAAs","playerName":"Player1","message":"Hello, everyone!","timestamp":"2024-11-14T08:32:38.617Z"} - const gameId = channel.split(':')[1]; + const gameId = channel.split(':')[1]; // ex. channel: chat:317172 const chatMessage = JSON.parse(message); - server.to(gameId).emit(SocketEvents.CHAT_MESSAGE, chatMessage); + + const playerKey = REDIS_KEY.PLAYER(chatMessage.playerId); + const isAlivePlayer = await this.redis.hget(playerKey, 'isAlive'); + + if (isAlivePlayer === '1') { + server.to(gameId).emit(SocketEvents.CHAT_MESSAGE, chatMessage); + return; + } + + // 죽은 사람의 채팅은 죽은 사람끼리만 볼 수 있도록 처리 + const players = await this.redis.smembers(REDIS_KEY.ROOM_PLAYERS(gameId)); + await Promise.all(players.map(async (playerId) => { + const playerKey = REDIS_KEY.PLAYER(playerId); + const isAlive = await this.redis.hget(playerKey, 'isAlive'); + + if (isAlive === '0') { + server.to(playerId).emit(SocketEvents.CHAT_MESSAGE, chatMessage); + } + })); }); } } diff --git a/BE/src/user/entities/user-quiz-archive.entity.ts b/BE/src/user/entities/user-quiz-archive.entity.ts index 7e2757d..4b15c36 100644 --- a/BE/src/user/entities/user-quiz-archive.entity.ts +++ b/BE/src/user/entities/user-quiz-archive.entity.ts @@ -2,11 +2,7 @@ import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; import { BaseModel } from '../../common/entity/base.entity'; import { QuizSetModel } from '../../quiz-set/entities/quiz-set.entity'; import { UserModel } from './user.entity'; - -export enum GameMode { - SURVIVAL = 'SURVIVAL', - RANKING = 'RANKING' -} +import { GameMode } from '../../common/constants/game-mode'; @Entity('user_quiz_archive') export class UserQuizArchiveModel extends BaseModel { From a2ea180713d7c646d756e1efbb79762c640391c0 Mon Sep 17 00:00:00 2001 From: NewCodes7 Date: Tue, 26 Nov 2024 02:04:44 +0900 Subject: [PATCH 6/6] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=8F=99=EC=A0=81=20=ED=8F=AC=ED=8A=B8=20?= =?UTF-8?q?=ED=95=A0=EB=8B=B9=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../game/game-chat.integratoin.spec.ts | 5 ++-- .../game/game-room.integration.spec.ts | 5 ++-- .../game/game-survival.integration.spec.ts | 11 +++++--- .../integration/game/game.integration.spec.ts | 5 ++-- BE/test/integration/setup/game.setup.ts | 8 +++--- BE/test/integration/setup/util.ts | 25 +++++++++++++++++++ 6 files changed, 45 insertions(+), 14 deletions(-) diff --git a/BE/test/integration/game/game-chat.integratoin.spec.ts b/BE/test/integration/game/game-chat.integratoin.spec.ts index 4c78c4d..1c032fa 100644 --- a/BE/test/integration/game/game-chat.integratoin.spec.ts +++ b/BE/test/integration/game/game-chat.integratoin.spec.ts @@ -8,19 +8,20 @@ describe('Game Chat 통합테스트', () => { let redisMock; let socketHelper: SocketTestHelper; let client1, client2, client3; - const TEST_PORT = 3001; + let port; beforeAll(async () => { const setup = await setupTestingModule(); app = setup.app; redisMock = setup.redisMock; + port = setup.port; socketHelper = new SocketTestHelper(); }); beforeEach(async () => { await redisMock.flushall(); - [client1, client2, client3] = await socketHelper.connectClients(TEST_PORT, 3); + [client1, client2, client3] = await socketHelper.connectClients(port, 3); }); afterEach(async () => { diff --git a/BE/test/integration/game/game-room.integration.spec.ts b/BE/test/integration/game/game-room.integration.spec.ts index 84a45a9..a9dd7d9 100644 --- a/BE/test/integration/game/game-room.integration.spec.ts +++ b/BE/test/integration/game/game-room.integration.spec.ts @@ -8,19 +8,20 @@ describe('Game Room 통합테스트', () => { let redisMock; let socketHelper: SocketTestHelper; let client1, client2, client3; - const TEST_PORT = 3001; + let port; beforeAll(async () => { const setup = await setupTestingModule(); app = setup.app; redisMock = setup.redisMock; + port = setup.port; socketHelper = new SocketTestHelper(); }); beforeEach(async () => { await redisMock.flushall(); - [client1, client2, client3] = await socketHelper.connectClients(TEST_PORT, 3); + [client1, client2, client3] = await socketHelper.connectClients(port, 3); }); afterEach(async () => { diff --git a/BE/test/integration/game/game-survival.integration.spec.ts b/BE/test/integration/game/game-survival.integration.spec.ts index d802dec..2c7f07e 100644 --- a/BE/test/integration/game/game-survival.integration.spec.ts +++ b/BE/test/integration/game/game-survival.integration.spec.ts @@ -1,26 +1,25 @@ import { SocketTestHelper } from '../setup/socket.helper'; import { setupTestingModule } from '../setup/game.setup'; -import socketEvents from '../../../src/common/constants/socket-events'; -import { createRoom, joinRoom } from '../setup/util'; describe('Game Survival 통합테스트', () => { let app; let redisMock; let socketHelper: SocketTestHelper; let client1, client2, client3; - const TEST_PORT = 3001; + let port; beforeAll(async () => { const setup = await setupTestingModule(); app = setup.app; redisMock = setup.redisMock; + port = setup.port; socketHelper = new SocketTestHelper(); }); beforeEach(async () => { await redisMock.flushall(); - [client1, client2, client3] = await socketHelper.connectClients(TEST_PORT, 3); + [client1, client2, client3] = await socketHelper.connectClients(port, 3); }); afterEach(async () => { @@ -34,6 +33,10 @@ describe('Game Survival 통합테스트', () => { } }); + it ('a', () => { + expect(1).toBe(1); + }) + // describe('관전자끼리 플레이 테스트', () => { // it ('관전자의 메시지가 생존자에게 보이지 않아야 한다.', async () => { // const createResponse = await createRoom(client1); diff --git a/BE/test/integration/game/game.integration.spec.ts b/BE/test/integration/game/game.integration.spec.ts index 6a2f81f..9976ced 100644 --- a/BE/test/integration/game/game.integration.spec.ts +++ b/BE/test/integration/game/game.integration.spec.ts @@ -9,19 +9,20 @@ describe('Game 통합테스트', () => { let redisMock; let socketHelper: SocketTestHelper; let client1, client2, client3; - const TEST_PORT = 3001; + let port; beforeAll(async () => { const setup = await setupTestingModule(); app = setup.app; redisMock = setup.redisMock; + port = setup.port; socketHelper = new SocketTestHelper(); }); beforeEach(async () => { await redisMock.flushall(); - [client1, client2, client3] = await socketHelper.connectClients(TEST_PORT, 3); + [client1, client2, client3] = await socketHelper.connectClients(port, 3); }); afterEach(async () => { diff --git a/BE/test/integration/setup/game.setup.ts b/BE/test/integration/setup/game.setup.ts index a11b39d..25f9748 100644 --- a/BE/test/integration/setup/game.setup.ts +++ b/BE/test/integration/setup/game.setup.ts @@ -2,8 +2,7 @@ import { Test } from '@nestjs/testing'; import { IoAdapter } from '@nestjs/platform-socket.io'; import RedisMock from 'ioredis-mock'; import { AppModule } from '../../../src/app.module'; - -export const TEST_PORT = 3001; +import { getAvailablePort } from './util'; export async function setupTestingModule() { const redisMock = new RedisMock(); @@ -26,7 +25,8 @@ export async function setupTestingModule() { const app = moduleRef.createNestApplication(); app.useWebSocketAdapter(new IoAdapter(app)); await app.init(); - await app.listen(TEST_PORT); + const port = await getAvailablePort(); + await app.listen(port); - return { app, moduleRef, redisMock }; + return { app, moduleRef, redisMock, port }; } \ No newline at end of file diff --git a/BE/test/integration/setup/util.ts b/BE/test/integration/setup/util.ts index 320aba4..30e7500 100644 --- a/BE/test/integration/setup/util.ts +++ b/BE/test/integration/setup/util.ts @@ -1,4 +1,5 @@ import socketEvents from '../../../src/common/constants/socket-events'; +import * as net from 'node:net'; export async function createRoom(client) { const createResponse = await new Promise<{ gameId: string }>((resolve) => { @@ -22,4 +23,28 @@ export async function joinRoom(client, gameId) { }); }); return joinResponse; +} + +export async function getAvailablePort(startPort = 3000): Promise { + const port = startPort; + try { + const server = net.createServer(); + return new Promise((resolve, reject) => { + server.listen(port, () => { + server.once('close', () => { + resolve(port); + }); + server.close(); + }); + server.on('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE') { + resolve(getAvailablePort(port + 1)); + } else { + reject(err); + } + }); + }); + } catch (err) { + return getAvailablePort(port + 1); + } } \ No newline at end of file