Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8afb294
♻️ refactor: roomExists 메서드 임시 구현으로 변경 (#67)
son-hyejun Jan 7, 2026
74f5ab9
🧪 test: RoomService 테스트 파일 생성 #67
son-hyejun Jan 7, 2026
cef6fa4
🧪 test: RoomService createQuickRoom 테스트 추가 #67
son-hyejun Jan 7, 2026
5887d73
✨ feat: Quick Room 생성 기능 구현 #67
son-hyejun Jan 7, 2026
9ebe49d
🧪 test: 룸 코드 중복 재시도 로직 테스트 추가 #67
son-hyejun Jan 7, 2026
666ffc6
⚙️ setting: Jest ESM 모듈 변환 설정 추가 #67
son-hyejun Jan 7, 2026
b8c784e
♻️ refactor: 룸 코드 생성 로직 개선 및 검증 강화 #67
son-hyejun Jan 7, 2026
ac6a48a
🧪 test: 룸 코드 생성 실패 시 예외 처리 테스트 추가 #67
son-hyejun Jan 7, 2026
431a693
♻️ refactor: 룸 코드 생성 로직 분리 및 책임 명확화 #67
son-hyejun Jan 7, 2026
ad87cdc
🧪 test: 트랜잭션 기반 Room 생성 테스트로 전환 #67
son-hyejun Jan 7, 2026
5f4eac0
♻️ refactor: Room 생성 로직에 트랜잭션 적용 #67
son-hyejun Jan 7, 2026
2fe0027
🧪 test: Room과 방장 Pt 동시 생성 검증 테스트 추가 #67
son-hyejun Jan 7, 2026
e581a7d
✨ feat: Quick Room 생성 시 방장 Pt 자동 생성 및 반환값 추가 #67
son-hyejun Jan 7, 2026
76355f4
♻️ refactor: RoomService 테스트 코드 가독성 개선 #67
son-hyejun Jan 7, 2026
c885ead
✨ feat: QuickRoom 응답 DTO 추가 및 반환 타입 명시 #67
son-hyejun Jan 7, 2026
b006271
♻️ refactor: QuickRoomResponseDto를 CreateRoomResponseDto로 이름 변경 #67
son-hyejun Jan 7, 2026
726e847
✨ feat: RoomCreationOptions 인터페이스 추가 #67
son-hyejun Jan 7, 2026
521449f
♻️ refactor: 방 생성 로직을 createRoom private 메서드로 추출 #67
son-hyejun Jan 7, 2026
9e6b98e
✨ feat: Quick Room 생성 API 엔드포인트 추가 #67
son-hyejun Jan 7, 2026
d3893dc
🧪 test: RoomController Quick Room 생성 테스트 추가 #67
son-hyejun Jan 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
"testEnvironment": "node",
"transformIgnorePatterns": [
"node_modules/(?!.pnpm/(nanoid|uuid)|nanoid|uuid)"
]
}
}
4 changes: 4 additions & 0 deletions apps/server/src/modules/room/dto/create-room-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export class CreateRoomResponseDto {
roomCode: string;
myPtId: string;
}
48 changes: 48 additions & 0 deletions apps/server/src/modules/room/room.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RoomController } from './room.controller';
import { RoomService } from './room.service';
import { CreateRoomResponseDto } from './dto/create-room-response.dto';

describe('RoomController', () => {
let controller: RoomController;
let service: RoomService;

const mockRoomService = {
createQuickRoom: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [RoomController],
providers: [
{
provide: RoomService,
useValue: mockRoomService,
},
],
}).compile();

controller = module.get<RoomController>(RoomController);
service = module.get<RoomService>(RoomService);
});

it('Controller가 정의되어야 한다', () => {
expect(controller).toBeDefined();
});

describe('createQuickRoom (POST /api/room/quick)', () => {
it('Service를 호출하고 생성된 방 정보를 반환해야 한다', async () => {
const mockResponse: CreateRoomResponseDto = {
roomCode: 'ABCDEF',
myPtId: 'test-uuid-1234',
};

mockRoomService.createQuickRoom.mockResolvedValue(mockResponse);

const result = await controller.createQuickRoom();

expect(service.createQuickRoom).toHaveBeenCalledTimes(1);
expect(result).toEqual(mockResponse);
});
});
});
14 changes: 13 additions & 1 deletion apps/server/src/modules/room/room.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
import {
Controller,
Get,
Param,
NotFoundException,
Post,
} from '@nestjs/common';
import { RoomService } from './room.service';
import { CreateRoomResponseDto } from './dto/create-room-response.dto';

@Controller('api/room')
export class RoomController {
Expand All @@ -13,4 +20,9 @@ export class RoomController {
}
return { exists: true };
}

@Post('quick')
async createQuickRoom(): Promise<CreateRoomResponseDto> {
return await this.roomService.createQuickRoom();
}
}
8 changes: 8 additions & 0 deletions apps/server/src/modules/room/room.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { DefaultRolePolicy, HostTransferPolicy } from './room.entity';

export interface RoomCreationOptions {
hostTransferPolicy: HostTransferPolicy;
defaultRolePolicy: DefaultRolePolicy;
roomPassword?: string;
hostPassword?: string;
}
185 changes: 185 additions & 0 deletions apps/server/src/modules/room/room.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RoomService } from './room.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DefaultRolePolicy, HostTransferPolicy, Room } from './room.entity';
import { Repository, DataSource, QueryRunner } from 'typeorm';
import { InternalServerErrorException } from '@nestjs/common';
import { PtRole } from '../pt/pt.entity';

// 테스트 상수
const MOCK_ROOM_CODE = 'UNI001';
const MOCK_ROOM_ID = 100;
const MOCK_PT_ID = 'uuid-host-1';

const createMockQueryRunner = () => ({
connect: jest.fn(),
startTransaction: jest.fn(),
commitTransaction: jest.fn(),
rollbackTransaction: jest.fn(),
release: jest.fn(),
manager: {
create: jest.fn(),
save: jest.fn(),
},
});

describe('RoomService', () => {
let service: RoomService;
let repository: jest.Mocked<Repository<Room>>;
let dataSource: jest.Mocked<DataSource>;
let queryRunner: jest.Mocked<QueryRunner>;

beforeEach(async () => {
// 각 테스트마다 새로운 QueryRunner Mock 생성
const mockQueryRunnerInstance = createMockQueryRunner();

const module: TestingModule = await Test.createTestingModule({
providers: [
RoomService,
{
provide: getRepositoryToken(Room),
useValue: {
findOne: jest.fn(),
},
},
{
provide: DataSource,
useValue: {
createQueryRunner: jest
.fn()
.mockReturnValue(mockQueryRunnerInstance),
},
},
],
}).compile();

service = module.get<RoomService>(RoomService);
repository = module.get(getRepositoryToken(Room));
dataSource = module.get(DataSource);
queryRunner = dataSource.createQueryRunner() as jest.Mocked<QueryRunner>;

// 셋업 과정에서 생긴 호출 기록 초기화
(dataSource.createQueryRunner as jest.Mock).mockClear();

// 공통 Mock 동작
(queryRunner.manager.create as jest.Mock).mockImplementation(
(entity, data) => data,
);
});

it('Service가 정의되어야 한다', () => {
expect(service).toBeDefined();
});

describe('createQuickRoom', () => {
it('성공 시: 트랜잭션 내에서 Room과 방장(Pt)을 저장하고 결과를 반환한다', async () => {
// Arrange
jest
.spyOn(service as any, 'generateRoomCode')
.mockReturnValue(MOCK_ROOM_CODE);
repository.findOne.mockResolvedValue(null); // 중복 없음

const savedRoom = { roomId: MOCK_ROOM_ID, roomCode: MOCK_ROOM_CODE };
const savedPt = {
ptId: MOCK_PT_ID,
roomId: MOCK_ROOM_ID,
role: PtRole.HOST,
};

(queryRunner.manager.save as jest.Mock)
.mockResolvedValueOnce(savedRoom) // Room 저장 성공
.mockResolvedValueOnce(savedPt); // Pt 저장 성공

// Act
const result = await service.createQuickRoom();

// Assert
// 1. 흐름 검증
expect(dataSource.createQueryRunner).toHaveBeenCalled();
expect(queryRunner.connect).toHaveBeenCalled();
expect(queryRunner.startTransaction).toHaveBeenCalled();
expect(queryRunner.commitTransaction).toHaveBeenCalled();
expect(queryRunner.release).toHaveBeenCalled();
expect(queryRunner.rollbackTransaction).not.toHaveBeenCalled();

// 2. 데이터 저장 검증
expect(queryRunner.manager.save).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
roomCode: MOCK_ROOM_CODE,
hostTransferPolicy: HostTransferPolicy.AUTO_TRANSFER,
defaultRolePolicy: DefaultRolePolicy.VIEWER,
}),
);

expect(queryRunner.manager.save).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
role: PtRole.HOST,
code: '0000',
}),
);

// 3. 반환값 검증
expect(result).toEqual({
roomCode: MOCK_ROOM_CODE,
myPtId: MOCK_PT_ID,
});
});

it('룸 코드가 중복되면 최대 3번까지 재시도하고, 성공하면 저장한다', async () => {
// Arrange
jest
.spyOn(service as any, 'generateRoomCode')
.mockReturnValueOnce('DUP001') // 1차 시도 (중복)
.mockReturnValueOnce('UNI002'); // 2차 시도 (성공)

repository.findOne
.mockResolvedValueOnce({ roomId: 1 } as Room) // 1차 결과: 존재함
.mockResolvedValueOnce(null); // 2차 결과: 없음

const mockRoom = { roomCode: 'UNI002' } as Room;
(queryRunner.manager.save as jest.Mock).mockResolvedValue(mockRoom);

// Act
await service.createQuickRoom();

// Assert
expect(repository.findOne).toHaveBeenCalledTimes(2); // 재시도 확인
expect(queryRunner.manager.create).toHaveBeenCalledWith(
Room,
expect.objectContaining({ roomCode: 'UNI002' }),
);
expect(queryRunner.commitTransaction).toHaveBeenCalled();
});

it('3번 모두 중복되면 예외를 던지고 트랜잭션은 시작되지 않는다', async () => {
// Arrange
jest.spyOn(service as any, 'generateRoomCode').mockReturnValue('DUP999');
repository.findOne.mockResolvedValue({ roomId: 999 } as Room); // 계속 중복

// Act & Assert
await expect(service.createQuickRoom()).rejects.toThrow(
InternalServerErrorException,
);

expect(repository.findOne).toHaveBeenCalledTimes(3);
expect(dataSource.createQueryRunner).not.toHaveBeenCalled(); // 트랜잭션 시작 X
});

it('저장 중 DB 에러가 발생하면 롤백해야 한다', async () => {
// Arrange
repository.findOne.mockResolvedValue(null);
(queryRunner.manager.save as jest.Mock).mockRejectedValue(
new Error('DB Error'),
);

// Act & Assert
await expect(service.createQuickRoom()).rejects.toThrow('DB Error');

expect(queryRunner.startTransaction).toHaveBeenCalled();
expect(queryRunner.rollbackTransaction).toHaveBeenCalled(); // 롤백 확인
expect(queryRunner.release).toHaveBeenCalled(); // 리소스 해제 확인
});
});
});
Loading