Skip to content

Commit 764ec4b

Browse files
committed
test: SVG Badge API 단위 테스트 추가
1 parent ffdc0f0 commit 764ec4b

File tree

2 files changed

+253
-1
lines changed

2 files changed

+253
-1
lines changed

src/repositories/__test__/leaderboard.repo.test.ts

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Pool } from 'pg';
2-
import { DBError } from '@/exception';
2+
import { DBError, NotFoundError } from '@/exception';
33
import { UserLeaderboardSortType, PostLeaderboardSortType } from '@/types';
44
import { LeaderboardRepository } from '@/repositories/leaderboard.repository';
55
import { mockPool, createMockQueryResult } from '@/utils/fixtures';
@@ -184,4 +184,118 @@ describe('LeaderboardRepository', () => {
184184
await expect(repo.getPostLeaderboard('viewCount', 30, 10)).rejects.toThrow(DBError);
185185
});
186186
});
187+
188+
describe('getUserStats', () => {
189+
const mockUserStats = {
190+
username: 'test-user',
191+
total_views: '1000',
192+
total_likes: '50',
193+
total_posts: '10',
194+
view_diff: '100',
195+
like_diff: '5',
196+
post_diff: '2',
197+
};
198+
199+
it('특정 사용자의 통계를 반환해야 한다', async () => {
200+
mockPool.query.mockResolvedValue(createMockQueryResult([mockUserStats]));
201+
202+
const result = await repo.getUserStats('test-user', 30);
203+
204+
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('WHERE u.username = $1'), ['test-user']);
205+
expect(result).toEqual(mockUserStats);
206+
});
207+
208+
it('username 파라미터가 쿼리에 올바르게 적용되어야 한다', async () => {
209+
mockPool.query.mockResolvedValue(createMockQueryResult([mockUserStats]));
210+
211+
await repo.getUserStats('test-user', 30);
212+
213+
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('GROUP BY u.username'), ['test-user']);
214+
});
215+
216+
it('사용자가 존재하지 않으면 NotFoundError를 던져야 한다', async () => {
217+
mockPool.query.mockResolvedValue(createMockQueryResult([]));
218+
219+
await expect(repo.getUserStats('non-existent', 30)).rejects.toThrow(NotFoundError);
220+
await expect(repo.getUserStats('non-existent', 30)).rejects.toThrow('사용자를 찾을 수 없습니다: non-existent');
221+
});
222+
223+
it('CTE 쿼리가 포함되어야 한다', async () => {
224+
mockPool.query.mockResolvedValue(createMockQueryResult([mockUserStats]));
225+
226+
await repo.getUserStats('test-user', 30);
227+
228+
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('WITH'), expect.anything());
229+
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('today_stats'), expect.anything());
230+
});
231+
232+
it('쿼리 에러 발생 시 DBError를 던져야 한다', async () => {
233+
mockPool.query.mockRejectedValue(new Error('DB connection failed'));
234+
235+
await expect(repo.getUserStats('test-user', 30)).rejects.toThrow(DBError);
236+
await expect(repo.getUserStats('test-user', 30)).rejects.toThrow('사용자 통계 조회 중 문제가 발생했습니다.');
237+
});
238+
});
239+
240+
describe('getRecentPosts', () => {
241+
const mockRecentPosts = [
242+
{
243+
title: 'Test Post 1',
244+
released_at: '2025-01-01',
245+
today_view: '100',
246+
today_like: '10',
247+
view_diff: '20',
248+
},
249+
{
250+
title: 'Test Post 2',
251+
released_at: '2025-01-02',
252+
today_view: '200',
253+
today_like: '20',
254+
view_diff: '30',
255+
},
256+
];
257+
258+
it('특정 사용자의 최근 게시글을 반환해야 한다', async () => {
259+
mockPool.query.mockResolvedValue(createMockQueryResult(mockRecentPosts));
260+
261+
const result = await repo.getRecentPosts('test-user', 30, 3);
262+
263+
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('WHERE u.username = $1'), ['test-user', 3]);
264+
expect(result).toEqual(mockRecentPosts);
265+
});
266+
267+
it('limit 파라미터가 쿼리에 올바르게 적용되어야 한다', async () => {
268+
mockPool.query.mockResolvedValue(createMockQueryResult(mockRecentPosts));
269+
270+
await repo.getRecentPosts('test-user', 30, 5);
271+
272+
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('LIMIT $2'), ['test-user', 5]);
273+
});
274+
275+
it('released_at 기준 내림차순 정렬이 포함되어야 한다', async () => {
276+
mockPool.query.mockResolvedValue(createMockQueryResult(mockRecentPosts));
277+
278+
await repo.getRecentPosts('test-user', 30, 3);
279+
280+
expect(mockPool.query).toHaveBeenCalledWith(
281+
expect.stringContaining('ORDER BY p.released_at DESC'),
282+
expect.anything(),
283+
);
284+
});
285+
286+
it('CTE 쿼리가 포함되어야 한다', async () => {
287+
mockPool.query.mockResolvedValue(createMockQueryResult(mockRecentPosts));
288+
289+
await repo.getRecentPosts('test-user', 30, 3);
290+
291+
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('WITH'), expect.anything());
292+
});
293+
294+
it('쿼리 에러 발생 시 DBError를 던져야 한다', async () => {
295+
mockPool.query.mockRejectedValue(new Error('DB connection failed'));
296+
297+
await expect(repo.getRecentPosts('test-user', 30, 3)).rejects.toThrow(DBError);
298+
await expect(repo.getRecentPosts('test-user', 30, 3)).rejects.toThrow('최근 게시글 조회 중 문제가 발생했습니다.');
299+
});
300+
});
187301
});
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { Pool } from 'pg';
2+
import { DBError, NotFoundError } from '@/exception';
3+
import { LeaderboardRepository } from '@/repositories/leaderboard.repository';
4+
5+
jest.mock('@/types', () => ({
6+
BadgeDataResponseDto: jest.fn().mockImplementation((user, recentPosts) => ({
7+
user,
8+
recentPosts,
9+
})),
10+
}));
11+
jest.mock('@/repositories/leaderboard.repository');
12+
13+
import { SvgService } from '@/services/svg.service';
14+
15+
describe('SvgService', () => {
16+
let service: SvgService;
17+
let mockRepo: jest.Mocked<LeaderboardRepository>;
18+
let mockPool: jest.Mocked<Pool>;
19+
20+
beforeEach(() => {
21+
const mockPoolObj = {};
22+
mockPool = mockPoolObj as jest.Mocked<Pool>;
23+
24+
const repoInstance = new LeaderboardRepository(mockPool);
25+
mockRepo = repoInstance as jest.Mocked<LeaderboardRepository>;
26+
27+
service = new SvgService(mockRepo);
28+
});
29+
30+
afterEach(() => {
31+
jest.clearAllMocks();
32+
});
33+
34+
const mockUserStats = {
35+
username: 'test-user',
36+
total_views: '1000',
37+
total_likes: '50',
38+
total_posts: '10',
39+
view_diff: '100',
40+
like_diff: '5',
41+
post_diff: '2',
42+
};
43+
44+
const mockRecentPosts = [
45+
{
46+
title: 'Test Post 1',
47+
released_at: '2025-01-01',
48+
today_view: '100',
49+
today_like: '10',
50+
view_diff: '20',
51+
},
52+
{
53+
title: 'Test Post 2',
54+
released_at: '2025-01-02',
55+
today_view: '200',
56+
today_like: '20',
57+
view_diff: '30',
58+
},
59+
];
60+
61+
describe('getBadgeData', () => {
62+
it('type이 default일 때 사용자 통계와 최근 게시글을 반환해야 한다', async () => {
63+
mockRepo.getUserStats.mockResolvedValue(mockUserStats);
64+
mockRepo.getRecentPosts.mockResolvedValue(mockRecentPosts);
65+
66+
const result = await service.getBadgeData('test-user', 'default');
67+
68+
expect(mockRepo.getUserStats).toHaveBeenCalledWith('test-user', 30);
69+
expect(mockRepo.getRecentPosts).toHaveBeenCalledWith('test-user', 30, 3);
70+
expect(result).toHaveProperty('user');
71+
expect(result).toHaveProperty('recentPosts');
72+
expect(result.user.username).toBe('test-user');
73+
expect(result.user.totalViews).toBe(1000);
74+
expect(result.recentPosts).toHaveLength(2);
75+
});
76+
77+
it('type이 simple일 때 최근 게시글을 조회하지 않아야 한다', async () => {
78+
mockRepo.getUserStats.mockResolvedValue(mockUserStats);
79+
80+
const result = await service.getBadgeData('test-user', 'simple');
81+
82+
expect(mockRepo.getUserStats).toHaveBeenCalledWith('test-user', 30);
83+
expect(mockRepo.getRecentPosts).not.toHaveBeenCalled();
84+
expect(result.recentPosts).toHaveLength(0);
85+
});
86+
87+
it('문자열 통계 값을 숫자로 변환해야 한다', async () => {
88+
mockRepo.getUserStats.mockResolvedValue(mockUserStats);
89+
mockRepo.getRecentPosts.mockResolvedValue(mockRecentPosts);
90+
91+
const result = await service.getBadgeData('test-user', 'default');
92+
93+
expect(typeof result.user.totalViews).toBe('number');
94+
expect(typeof result.user.totalLikes).toBe('number');
95+
expect(typeof result.user.totalPosts).toBe('number');
96+
expect(typeof result.user.viewDiff).toBe('number');
97+
expect(typeof result.user.likeDiff).toBe('number');
98+
expect(typeof result.user.postDiff).toBe('number');
99+
});
100+
101+
it('최근 게시글 데이터를 올바르게 변환해야 한다', async () => {
102+
mockRepo.getUserStats.mockResolvedValue(mockUserStats);
103+
mockRepo.getRecentPosts.mockResolvedValue(mockRecentPosts);
104+
105+
const result = await service.getBadgeData('test-user', 'default');
106+
107+
expect(result.recentPosts[0]).toEqual({
108+
title: 'Test Post 1',
109+
releasedAt: '2025-01-01',
110+
viewCount: 100,
111+
likeCount: 10,
112+
viewDiff: 20,
113+
});
114+
});
115+
116+
it('dateRange 파라미터가 Repository에 전달되어야 한다', async () => {
117+
mockRepo.getUserStats.mockResolvedValue(mockUserStats);
118+
mockRepo.getRecentPosts.mockResolvedValue([]);
119+
120+
await service.getBadgeData('test-user', 'default', 7);
121+
122+
expect(mockRepo.getUserStats).toHaveBeenCalledWith('test-user', 7);
123+
expect(mockRepo.getRecentPosts).toHaveBeenCalledWith('test-user', 7, 3);
124+
});
125+
126+
it('Repository에서 NotFoundError 발생 시 그대로 전파해야 한다', async () => {
127+
mockRepo.getUserStats.mockRejectedValue(new NotFoundError('사용자를 찾을 수 없습니다'));
128+
129+
await expect(service.getBadgeData('non-existent', 'default')).rejects.toThrow(NotFoundError);
130+
});
131+
132+
it('Repository에서 DBError 발생 시 그대로 전파해야 한다', async () => {
133+
mockRepo.getUserStats.mockRejectedValue(new DBError('DB 오류'));
134+
135+
await expect(service.getBadgeData('test-user', 'default')).rejects.toThrow(DBError);
136+
});
137+
});
138+
});

0 commit comments

Comments
 (0)