|
| 1 | +import logger from '@/configs/logger.config'; |
| 2 | +import dotenv from 'dotenv'; |
| 3 | +import pg from 'pg'; |
| 4 | +import { Pool } from 'pg'; |
| 5 | +import { LeaderboardRepository } from '@/repositories/leaderboard.repository'; |
| 6 | +import { PostLeaderboardSortType, UserLeaderboardSortType } from '@/types'; |
| 7 | + |
| 8 | +dotenv.config(); |
| 9 | + |
| 10 | +jest.setTimeout(20000); // 각 케이스당 20초 타임아웃 설정 |
| 11 | + |
| 12 | +/** |
| 13 | + * LeaderboardRepository 통합 테스트 |
| 14 | + * |
| 15 | + * 이 테스트 파일은 실제 데이터베이스와 연결하여 LeaderboardRepository의 모든 메서드를 |
| 16 | + * 실제 환경과 동일한 조건에서 테스트합니다. |
| 17 | + */ |
| 18 | +describe('LeaderboardRepository 통합 테스트', () => { |
| 19 | + let testPool: Pool; |
| 20 | + let repo: LeaderboardRepository; |
| 21 | + |
| 22 | + // eslint-disable-next-line @typescript-eslint/naming-convention |
| 23 | + const DEFAULT_PARAMS = { |
| 24 | + USER_SORT: 'viewCount' as UserLeaderboardSortType, |
| 25 | + POST_SORT: 'viewCount' as PostLeaderboardSortType, |
| 26 | + DATE_RANGE: 30, |
| 27 | + LIMIT: 10, |
| 28 | + }; |
| 29 | + |
| 30 | + beforeAll(async () => { |
| 31 | + try { |
| 32 | + const testPoolConfig: pg.PoolConfig = { |
| 33 | + database: process.env.DATABASE_NAME, |
| 34 | + user: process.env.POSTGRES_USER, |
| 35 | + host: process.env.POSTGRES_HOST, |
| 36 | + password: process.env.POSTGRES_PASSWORD, |
| 37 | + port: Number(process.env.POSTGRES_PORT), |
| 38 | + max: 1, // 최대 연결 수 |
| 39 | + idleTimeoutMillis: 30000, // 연결 유휴 시간 (30초) |
| 40 | + connectionTimeoutMillis: 5000, // 연결 시간 초과 (5초) |
| 41 | + allowExitOnIdle: false, // 유휴 상태에서 종료 허용 |
| 42 | + statement_timeout: 30000, |
| 43 | + }; |
| 44 | + |
| 45 | + // localhost 가 아니면 ssl 필수 |
| 46 | + if (process.env.POSTGRES_HOST != 'localhost') { |
| 47 | + testPoolConfig.ssl = { |
| 48 | + rejectUnauthorized: false, |
| 49 | + }; |
| 50 | + } |
| 51 | + |
| 52 | + testPool = new Pool(testPoolConfig); |
| 53 | + |
| 54 | + // 연결 확인 |
| 55 | + await testPool.query('SELECT 1'); |
| 56 | + logger.info('LeaderboardRepository 통합 테스트 DB 연결 성공'); |
| 57 | + |
| 58 | + // 리포지토리 인스턴스 생성 |
| 59 | + repo = new LeaderboardRepository(testPool); |
| 60 | + |
| 61 | + // 충분한 데이터가 있는지 확인 (limit 기본값인 10을 기준으로 함) |
| 62 | + const userCheck = await testPool.query('SELECT COUNT(*) >= 10 AS is_enough FROM users_user'); |
| 63 | + const postCheck = await testPool.query('SELECT COUNT(*) >= 10 AS is_enough FROM posts_post'); |
| 64 | + const statsCheck = await testPool.query('SELECT COUNT(*) > 0 AS is_enough FROM posts_postdailystatistics'); |
| 65 | + |
| 66 | + if (!userCheck.rows[0].is_enough || !postCheck.rows[0].is_enough || !statsCheck.rows[0].is_enough) { |
| 67 | + logger.warn('주의: LeaderboardRepository 통합 테스트를 위한 충분한 데이터가 없습니다.'); |
| 68 | + } |
| 69 | + } catch (error) { |
| 70 | + logger.error('LeaderboardRepository 통합 테스트 설정 중 오류 발생:', error); |
| 71 | + throw error; |
| 72 | + } |
| 73 | + }); |
| 74 | + |
| 75 | + afterAll(async () => { |
| 76 | + try { |
| 77 | + jest.clearAllMocks(); |
| 78 | + |
| 79 | + // 풀 완전 종료 |
| 80 | + await testPool.end(); |
| 81 | + |
| 82 | + logger.info('LeaderboardRepository 통합 테스트 DB 연결 종료'); |
| 83 | + } catch (error) { |
| 84 | + logger.error('LeaderboardRepository 통합 테스트 종료 중 오류:', error); |
| 85 | + } |
| 86 | + }); |
| 87 | + |
| 88 | + describe('getUserLeaderboard', () => { |
| 89 | + it('사용자 통계 배열로 이루어진 리더보드를 반환해야 한다', async () => { |
| 90 | + const result = await repo.getUserLeaderboard( |
| 91 | + DEFAULT_PARAMS.USER_SORT, |
| 92 | + DEFAULT_PARAMS.DATE_RANGE, |
| 93 | + DEFAULT_PARAMS.LIMIT, |
| 94 | + ); |
| 95 | + |
| 96 | + expect(Array.isArray(result)).toBe(true); |
| 97 | + |
| 98 | + if (!isEnoughData(result, 1, '사용자 리더보드 반환값')) return; |
| 99 | + |
| 100 | + result.forEach((leaderboardUser) => { |
| 101 | + expect(leaderboardUser).toHaveProperty('id'); |
| 102 | + expect(leaderboardUser).toHaveProperty('email'); |
| 103 | + expect(leaderboardUser).toHaveProperty('total_views'); |
| 104 | + expect(leaderboardUser).toHaveProperty('total_likes'); |
| 105 | + expect(leaderboardUser).toHaveProperty('total_posts'); |
| 106 | + expect(leaderboardUser).toHaveProperty('view_diff'); |
| 107 | + expect(leaderboardUser).toHaveProperty('like_diff'); |
| 108 | + expect(leaderboardUser).toHaveProperty('post_diff'); |
| 109 | + }); |
| 110 | + }); |
| 111 | + |
| 112 | + it('통계와 관련된 필드는 음수가 아니어야 한다', async () => { |
| 113 | + const result = await repo.getUserLeaderboard( |
| 114 | + DEFAULT_PARAMS.USER_SORT, |
| 115 | + DEFAULT_PARAMS.DATE_RANGE, |
| 116 | + DEFAULT_PARAMS.LIMIT, |
| 117 | + ); |
| 118 | + |
| 119 | + if (!isEnoughData(result, 1, '사용자 리더보드 반환값')) return; |
| 120 | + |
| 121 | + result.forEach((leaderboardUser) => { |
| 122 | + expect(Number(leaderboardUser.total_views)).toBeGreaterThanOrEqual(0); |
| 123 | + expect(Number(leaderboardUser.total_likes)).toBeGreaterThanOrEqual(0); |
| 124 | + expect(Number(leaderboardUser.total_posts)).toBeGreaterThanOrEqual(0); |
| 125 | + expect(Number(leaderboardUser.view_diff)).toBeGreaterThanOrEqual(0); |
| 126 | + expect(Number(leaderboardUser.like_diff)).toBeGreaterThanOrEqual(0); |
| 127 | + expect(Number(leaderboardUser.post_diff)).toBeGreaterThanOrEqual(0); |
| 128 | + }); |
| 129 | + }); |
| 130 | + |
| 131 | + describe.each([ |
| 132 | + { sort: 'viewCount', field: 'view_diff' }, |
| 133 | + { sort: 'likeCount', field: 'like_diff' }, |
| 134 | + { sort: 'postCount', field: 'post_diff' }, |
| 135 | + ])('sort 파라미터에 따라 내림차순 정렬되어야 한다', ({ sort, field }) => { |
| 136 | + it(`sort가 ${sort}인 경우 ${field} 필드를 기준으로 정렬해야 한다`, async () => { |
| 137 | + const result = await repo.getUserLeaderboard( |
| 138 | + sort as UserLeaderboardSortType, |
| 139 | + DEFAULT_PARAMS.DATE_RANGE, |
| 140 | + DEFAULT_PARAMS.LIMIT, |
| 141 | + ); |
| 142 | + |
| 143 | + if (!isEnoughData(result, 2, `사용자 리더보드 정렬 (${sort})`)) return; |
| 144 | + |
| 145 | + const isSorted = result.every((leaderboardUser, idx) => { |
| 146 | + if (idx === 0) return true; |
| 147 | + return Number(leaderboardUser[field]) <= Number(result[idx - 1][field]); |
| 148 | + }); |
| 149 | + |
| 150 | + expect(isSorted).toBe(true); |
| 151 | + }); |
| 152 | + }); |
| 153 | + |
| 154 | + it('다양한 정렬 기준으로 결과를 반환해야 한다', async () => { |
| 155 | + const resultByViewDiff = await repo.getUserLeaderboard( |
| 156 | + 'viewCount', |
| 157 | + DEFAULT_PARAMS.DATE_RANGE, |
| 158 | + DEFAULT_PARAMS.LIMIT, |
| 159 | + ); |
| 160 | + const resultByLikeDiff = await repo.getUserLeaderboard( |
| 161 | + 'likeCount', |
| 162 | + DEFAULT_PARAMS.DATE_RANGE, |
| 163 | + DEFAULT_PARAMS.LIMIT, |
| 164 | + ); |
| 165 | + const resultByPostDiff = await repo.getUserLeaderboard( |
| 166 | + 'postCount', |
| 167 | + DEFAULT_PARAMS.DATE_RANGE, |
| 168 | + DEFAULT_PARAMS.LIMIT, |
| 169 | + ); |
| 170 | + |
| 171 | + if (!isEnoughData(resultByViewDiff, 2, '사용자 리더보드 정렬')) return; |
| 172 | + |
| 173 | + // 정렬 기준에 따라 결과가 달라야 하나, 순위가 같을 수 있어 하나라도 다르면 통과 |
| 174 | + const areDifferent = resultByViewDiff.some( |
| 175 | + (userByViewDiff, idx) => |
| 176 | + userByViewDiff.id !== resultByLikeDiff[idx].id || userByViewDiff.id !== resultByPostDiff[idx].id, |
| 177 | + ); |
| 178 | + |
| 179 | + // 데이터 상태에 따라 결과가 같을 수도 있어 조건부 검증 |
| 180 | + if (areDifferent) { |
| 181 | + // eslint-disable-next-line jest/no-conditional-expect |
| 182 | + expect(areDifferent).toBe(true); |
| 183 | + } |
| 184 | + }); |
| 185 | + |
| 186 | + it('limit 파라미터가 결과 개수를 제한해야 한다', async () => { |
| 187 | + const limit5Result = await repo.getUserLeaderboard(DEFAULT_PARAMS.USER_SORT, DEFAULT_PARAMS.DATE_RANGE, 5); |
| 188 | + const limit10Result = await repo.getUserLeaderboard(DEFAULT_PARAMS.USER_SORT, DEFAULT_PARAMS.DATE_RANGE, 10); |
| 189 | + |
| 190 | + if (!isEnoughData(limit10Result, 10, '사용자 리더보드 limit 파라미터')) return; |
| 191 | + |
| 192 | + expect(limit5Result.length).toBe(5); |
| 193 | + expect(limit10Result.length).toBe(10); |
| 194 | + }); |
| 195 | + |
| 196 | + it('dateRange 파라미터를 통한 날짜 범위가 적용되어야 한다', async () => { |
| 197 | + const range3Result = await repo.getUserLeaderboard(DEFAULT_PARAMS.USER_SORT, 3, DEFAULT_PARAMS.LIMIT); |
| 198 | + const range30Result = await repo.getUserLeaderboard(DEFAULT_PARAMS.USER_SORT, 30, DEFAULT_PARAMS.LIMIT); |
| 199 | + |
| 200 | + if (!isEnoughData(range3Result, 2, '사용자 리더보드 dateRange 파라미터')) return; |
| 201 | + |
| 202 | + // 3일 범위 결과와 30일 범위 결과가 달라야 하나, 순위가 같을 수 있어 하나라도 다르면 통과 |
| 203 | + const areDifferent = range3Result.some((userBy3Days, idx) => userBy3Days.id !== range30Result[idx].id); |
| 204 | + |
| 205 | + // 데이터 상태에 따라 결과가 같을 수도 있어 조건부 검증 |
| 206 | + if (areDifferent) { |
| 207 | + // eslint-disable-next-line jest/no-conditional-expect |
| 208 | + expect(areDifferent).toBe(true); |
| 209 | + } |
| 210 | + }); |
| 211 | + |
| 212 | + it('email이 null인 사용자는 제외되어야 한다', async () => { |
| 213 | + const result = await repo.getUserLeaderboard(DEFAULT_PARAMS.USER_SORT, DEFAULT_PARAMS.DATE_RANGE, 30); |
| 214 | + |
| 215 | + if (!isEnoughData(result, 1, '사용자 리더보드 email null 제외')) return; |
| 216 | + |
| 217 | + result.forEach((user) => { |
| 218 | + expect(user.email).not.toBeNull(); |
| 219 | + }); |
| 220 | + }); |
| 221 | + }); |
| 222 | + |
| 223 | + describe('getPostLeaderboard', () => { |
| 224 | + it('게시물 통계 배열로 이루어진 리더보드를 반환해야 한다', async () => { |
| 225 | + const result = await repo.getPostLeaderboard( |
| 226 | + DEFAULT_PARAMS.POST_SORT, |
| 227 | + DEFAULT_PARAMS.DATE_RANGE, |
| 228 | + DEFAULT_PARAMS.LIMIT, |
| 229 | + ); |
| 230 | + |
| 231 | + expect(Array.isArray(result)).toBe(true); |
| 232 | + |
| 233 | + if (!isEnoughData(result, 1, '게시물 리더보드 반환값')) return; |
| 234 | + |
| 235 | + result.forEach((leaderboardPost) => { |
| 236 | + expect(leaderboardPost).toHaveProperty('id'); |
| 237 | + expect(leaderboardPost).toHaveProperty('title'); |
| 238 | + expect(leaderboardPost).toHaveProperty('slug'); |
| 239 | + expect(leaderboardPost).toHaveProperty('total_views'); |
| 240 | + expect(leaderboardPost).toHaveProperty('total_likes'); |
| 241 | + expect(leaderboardPost).toHaveProperty('view_diff'); |
| 242 | + expect(leaderboardPost).toHaveProperty('like_diff'); |
| 243 | + expect(leaderboardPost).toHaveProperty('released_at'); |
| 244 | + }); |
| 245 | + }); |
| 246 | + |
| 247 | + it('통계와 관련된 필드는 음수가 아니어야 한다', async () => { |
| 248 | + const result = await repo.getPostLeaderboard( |
| 249 | + DEFAULT_PARAMS.POST_SORT, |
| 250 | + DEFAULT_PARAMS.DATE_RANGE, |
| 251 | + DEFAULT_PARAMS.LIMIT, |
| 252 | + ); |
| 253 | + |
| 254 | + if (!isEnoughData(result, 1, '게시물 리더보드 반환값')) return; |
| 255 | + |
| 256 | + result.forEach((leaderboardPost) => { |
| 257 | + expect(Number(leaderboardPost.total_views)).toBeGreaterThanOrEqual(0); |
| 258 | + expect(Number(leaderboardPost.total_likes)).toBeGreaterThanOrEqual(0); |
| 259 | + expect(Number(leaderboardPost.view_diff)).toBeGreaterThanOrEqual(0); |
| 260 | + expect(Number(leaderboardPost.like_diff)).toBeGreaterThanOrEqual(0); |
| 261 | + }); |
| 262 | + }); |
| 263 | + |
| 264 | + describe.each([ |
| 265 | + { sort: 'viewCount', field: 'view_diff' }, |
| 266 | + { sort: 'likeCount', field: 'like_diff' }, |
| 267 | + ])('sort 파라미터에 따라 내림차순 정렬되어야 한다', ({ sort, field }) => { |
| 268 | + it(`sort가 ${sort}인 경우 ${field} 필드를 기준으로 정렬해야 한다`, async () => { |
| 269 | + const result = await repo.getPostLeaderboard( |
| 270 | + sort as PostLeaderboardSortType, |
| 271 | + DEFAULT_PARAMS.DATE_RANGE, |
| 272 | + DEFAULT_PARAMS.LIMIT, |
| 273 | + ); |
| 274 | + |
| 275 | + if (!isEnoughData(result, 2, `게시물 리더보드 정렬 (${sort})`)) return; |
| 276 | + |
| 277 | + const isSorted = result.every((leaderboardPost, idx) => { |
| 278 | + if (idx === 0) return true; |
| 279 | + return Number(leaderboardPost[field]) <= Number(result[idx - 1][field]); |
| 280 | + }); |
| 281 | + |
| 282 | + expect(isSorted).toBe(true); |
| 283 | + }); |
| 284 | + }); |
| 285 | + |
| 286 | + it('다양한 정렬 기준으로 결과를 반환해야 한다', async () => { |
| 287 | + const resultByViewDiff = await repo.getPostLeaderboard( |
| 288 | + 'viewCount', |
| 289 | + DEFAULT_PARAMS.DATE_RANGE, |
| 290 | + DEFAULT_PARAMS.LIMIT, |
| 291 | + ); |
| 292 | + const resultByLikeDiff = await repo.getPostLeaderboard( |
| 293 | + 'likeCount', |
| 294 | + DEFAULT_PARAMS.DATE_RANGE, |
| 295 | + DEFAULT_PARAMS.LIMIT, |
| 296 | + ); |
| 297 | + |
| 298 | + if (!isEnoughData(resultByViewDiff, 2, '게시물 리더보드 정렬')) return; |
| 299 | + |
| 300 | + // 정렬 기준에 따라 결과가 달라야 하나, 순위가 같을 수 있어 하나라도 다르면 통과 |
| 301 | + const areDifferent = resultByViewDiff.some( |
| 302 | + (postByViewDiff, idx) => postByViewDiff.id !== resultByLikeDiff[idx].id, |
| 303 | + ); |
| 304 | + |
| 305 | + // 데이터 상태에 따라 결과가 같을 수도 있어 조건부 검증 |
| 306 | + if (areDifferent) { |
| 307 | + // eslint-disable-next-line jest/no-conditional-expect |
| 308 | + expect(areDifferent).toBe(true); |
| 309 | + } |
| 310 | + }); |
| 311 | + |
| 312 | + it('limit 파라미터가 결과 개수를 제한해야 한다', async () => { |
| 313 | + const limit5Result = await repo.getPostLeaderboard(DEFAULT_PARAMS.POST_SORT, DEFAULT_PARAMS.DATE_RANGE, 5); |
| 314 | + const limit10Result = await repo.getPostLeaderboard(DEFAULT_PARAMS.POST_SORT, DEFAULT_PARAMS.DATE_RANGE, 10); |
| 315 | + |
| 316 | + if (!isEnoughData(limit10Result, 10, '게시물 리더보드 limit 파라미터')) return; |
| 317 | + |
| 318 | + expect(limit5Result.length).toBe(5); |
| 319 | + expect(limit10Result.length).toBe(10); |
| 320 | + }); |
| 321 | + |
| 322 | + it('dateRange 파라미터를 통한 날짜 범위가 적용되어야 한다', async () => { |
| 323 | + const range3Result = await repo.getPostLeaderboard(DEFAULT_PARAMS.POST_SORT, 3, DEFAULT_PARAMS.LIMIT); |
| 324 | + const range30Result = await repo.getPostLeaderboard(DEFAULT_PARAMS.POST_SORT, 30, DEFAULT_PARAMS.LIMIT); |
| 325 | + |
| 326 | + if (!isEnoughData(range3Result, 2, '게시물 리더보드 dateRange 파라미터')) return; |
| 327 | + |
| 328 | + // 3일 범위 결과와 30일 범위 결과가 달라야 하나, 순위가 같을 수 있어 하나라도 다르면 통과 |
| 329 | + const areDifferent = range3Result.some((postBy3Days, idx) => postBy3Days.id !== range30Result[idx].id); |
| 330 | + |
| 331 | + // 데이터 상태에 따라 결과가 같을 수도 있어 조건부 검증 |
| 332 | + if (areDifferent) { |
| 333 | + // eslint-disable-next-line jest/no-conditional-expect |
| 334 | + expect(areDifferent).toBe(true); |
| 335 | + } |
| 336 | + }); |
| 337 | + }); |
| 338 | +}); |
| 339 | + |
| 340 | +function isEnoughData(result: unknown[], limit: number, testName: string): boolean { |
| 341 | + if (result.length < limit) { |
| 342 | + logger.info(`충분한 데이터가 없어 ${testName} 테스트를 건너뜁니다.`); |
| 343 | + return false; |
| 344 | + } |
| 345 | + return true; |
| 346 | +} |
0 commit comments