Skip to content

Commit

Permalink
Merge pull request #13 from boostcampwm-2024/dev-be
Browse files Browse the repository at this point in the history
Dev be
  • Loading branch information
uuuo3o authored Jan 15, 2025
2 parents 9e23dbb + 40d080f commit dd4c6ff
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 4 deletions.
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions server/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
services:
redis:
image: redis:latest
container_name: redis_test
ports:
- "6379:6379"
2 changes: 2 additions & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/socket.io": "^3.0.2",
"@types/socket.io-client": "^3.0.0",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
Expand All @@ -53,6 +54,7 @@
"ioredis-mock": "^8.9.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"socket.io-client": "^4.8.1",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.1.0",
Expand Down
192 changes: 192 additions & 0 deletions server/src/drawing/drawing.gateway.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { INestApplication } from '@nestjs/common';
import { io, Socket } from 'socket.io-client';
import { RedisService } from '../redis/redis.service';
import { DrawingGateway } from './drawing.gateway';
import { DrawingService } from './drawing.service';
import { DrawingRepository } from './drawing.repository';

describe('DrawingGateway e2e 테스트', () => {
let app: INestApplication;
let redisService: RedisService;
let clientA: Socket;

const URL = 'http://localhost:3001/socket.io/drawing';

const mockConfigService = {
provide: ConfigService,
useValue: {
get: jest.fn().mockImplementation((key: string) => {
if (key === 'REDIS_HOST') return 'localhost';
if (key === 'REDIS_PORT') return '6379';
return null;
}),
},
};

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DrawingGateway, DrawingService, DrawingRepository, RedisService, mockConfigService],
}).compile();

app = module.createNestApplication();
redisService = module.get<RedisService>(RedisService);

// 테스트용 서버 실행
await app.listen(3001);
});

beforeEach(async () => {
await redisService.hset('room:room1', { roomId: 'room1' });
await redisService.hset('room:room1:player:player1', { playerId: 'player1' });

clientA = io(URL, {
auth: {
roomId: 'room1',
playerId: 'player1',
},
});

await new Promise<void>((resolve) => {
clientA.on('connect', resolve);
});
});

afterEach(async () => {
clientA.close();
await redisService.flushAll();
});

afterAll(async () => {
await app.close();
redisService.quit();
});

describe('handleConnection', () => {
it('roomId 또는 playerId의 값이 존재하지 않는 경우 "Room ID and Player ID are required" 에러가 발생한다.', async () => {
const authInfo = [
{ roomId: '', playerId: '' },
{ roomId: 'room1', playerId: '' },
{ roomId: '', playerId: 'player1' },
];

for (const auth of authInfo) {
const socket = io(URL, {
auth,
});

await new Promise<void>((resolve) => {
socket.on('error', (e) => {
expect(e.code).toBe(4000);
expect(e.message).toBe('Room ID and Player ID are required');
resolve();
});
});

socket.close();
}
});

it('room이 redis 내에 존재하지 않는 경우 "Room not found" 에러가 발생한다.', async () => {
const invalidRoomClient = io(URL, {
auth: {
roomId: 'failed-room',
playerId: 'player1',
},
});

await new Promise<void>((resolve) => {
invalidRoomClient.on('error', (e) => {
expect(e.code).toBe(6005);
expect(e.message).toBe('Room not found');
resolve();
});
});

invalidRoomClient.close();
});

it('player가 redis 내에 존재하지 않는 경우 "Player not found in room" 에러가 발생한다.', async () => {
const invalidPlayerClient = io(URL, {
auth: {
roomId: 'room1',
playerId: 'failed-player',
},
});

await new Promise<void>((resolve) => {
invalidPlayerClient.on('error', (e) => {
expect(e.code).toBe(6006);
expect(e.message).toBe('Player not found in room');
resolve();
});
});

invalidPlayerClient.close();
});

it('room과 player가 정상적으로 존재하는 경우 정상적으로 연결된다.', async () => {
expect(clientA.connected).toBe(true);
});
});

describe('handleDraw', () => {
it('roomId 값이 존재하지 않는 경우 "Room ID is required" 에러가 발생한다.', async () => {
const invalidRoomClient = io(URL, {
auth: {
roomId: 'failed-room',
playerId: 'player1',
},
});

invalidRoomClient.on('connect_error', (e) => {
expect(e.message).toBe('Room ID is required');
invalidRoomClient.close();
});
});

it('정상적으로 그림이 그려지는 경우', async () => {
const drawingData = {
pos: 56,
fillColor: { R: 0, G: 0, B: 0, A: 0 },
};

/**
* client.to(roomId).emit('drawUpdated') 이므로
* 그림 그린 사람을 제외하고 다른 사람이 존재해야 이벤트를 제대로 수신받는지 확인이 가능함
* 이를 위해 clientB를 생성
*/
await redisService.hset('room:room1:player:player2', { playerId: 'player2' });

const clientB = io(URL, {
auth: {
roomId: 'room1',
playerId: 'player2',
},
});

// clientB가 연결이 완료될 때까지 기다림
await new Promise<void>((resolve) => {
clientB.on('connect', resolve);
});

// drawUpdated 이벤트 수신을 위한 Promise 생성
const drawUpdatePromise = new Promise<void>((resolve) => {
clientB.on('drawUpdated', (data) => {
expect(data).toEqual({
playerId: 'player1',
drawingData: drawingData,
});
resolve();
});
});

// clientA가 실제로 이벤트를 발생시킴
clientA.emit('draw', { drawingData });

await drawUpdatePromise;
clientB.close();
});
});
});
130 changes: 130 additions & 0 deletions server/src/drawing/drawing.gateway.integration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { Socket } from 'socket.io';
import { RedisService } from '../redis/redis.service';
import { DrawingGateway } from './drawing.gateway';
import { DrawingService } from './drawing.service';
import { DrawingRepository } from './drawing.repository';
import { BadRequestException, PlayerNotFoundException, RoomNotFoundException } from '../exceptions/game.exception';

describe('DrawingGateway 통합 테스트', () => {
let gateway: DrawingGateway;
let service: DrawingService;
let redisService: RedisService;
let client: Socket;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
DrawingGateway,
DrawingService,
DrawingRepository,
RedisService,
{
provide: ConfigService,
useValue: {
get: jest.fn().mockImplementation((key: string) => {
if (key === 'REDIS_HOST') return 'localhost';
if (key === 'REDIS_PORT') return '6379';
return null;
}),
},
},
],
}).compile();

gateway = module.get<DrawingGateway>(DrawingGateway);
service = module.get<DrawingService>(DrawingService);
redisService = module.get<RedisService>(RedisService);
});

beforeEach(async () => {
client = {
handshake: {
auth: { roomId: '', playerId: '' },
},
data: {},
join: jest.fn(),
to: jest.fn().mockReturnValue({
emit: jest.fn(),
}),
} as any;

await redisService.hset('room:room1', { roomId: 'room1' });
await redisService.hset('room:room1:player:player1', { playerId: 'player1' });
});

// 테스트가 수행될 때마다 DB를 비워줌
afterEach(async () => {
await redisService.flushAll();
});

// 테스트가 종료되면 Redis를 종료
afterAll(() => {
redisService.quit();
});

describe('handleConnection', () => {
it('roomId 또는 playerId의 값이 존재하지 않는 경우', async () => {
const authInfo = [
{ roomId: '', playerId: '' },
{ roomId: 'room1', playerId: '' },
{ roomId: '', playerId: 'player1' },
];

for (const auth of authInfo) {
client.handshake.auth = auth;
await expect(gateway.handleConnection(client)).rejects.toThrowError(
new BadRequestException('Room ID and Player ID are required'),
);
}
});

it('room이 redis 내에 존재하지 않는 경우', async () => {
client.handshake.auth = { roomId: 'failed-room', playerId: 'player1' };

await expect(gateway.handleConnection(client)).rejects.toThrowError(new RoomNotFoundException('Room not found'));
});

it('player가 redis 내에 존재하지 않는 경우', async () => {
client.handshake.auth = { roomId: 'room1', playerId: 'failed-player' };

await expect(gateway.handleConnection(client)).rejects.toThrowError(
new PlayerNotFoundException('Player not found in room'),
);
});

it('room과 player가 정상적으로 존재하는 경우', async () => {
client.handshake.auth = { roomId: 'room1', playerId: 'player1' };
await gateway.handleConnection(client);

expect(client.join).toHaveBeenCalledWith('room1');
expect(client.data.roomId).toBe('room1');
expect(client.data.playerId).toBe('player1');
});
});

describe('handleDraw', () => {
it('roomId 값이 존재하지 않는 경우', async () => {
client.data = {};
const data = { drawingData: {} };

await expect(gateway.handleDraw(client, data)).rejects.toThrowError(
new BadRequestException('Room ID is required'),
);
});

it('정상적으로 그림이 그려지는 경우', async () => {
client.data = { roomId: 'room1', playerId: 'player1' };
const data = { drawingData: { pos: 56, fillColor: { R: 0, G: 0, B: 0, A: 0 } } };

await gateway.handleDraw(client, data);

expect(client.to).toHaveBeenCalledWith('room1');
expect(client.to('room1').emit).toHaveBeenCalledWith('drawUpdated', {
playerId: 'player1',
drawingData: data.drawingData,
});
});
});
});
Loading

0 comments on commit dd4c6ff

Please sign in to comment.