Skip to content
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable jest/no-disabled-tests */
/**
* 주의: 이 통합 테스트는 현재 시간에 의존적입니다.
* getCurrentKSTDateString과 getKSTDateStringWithOffset 함수는 실제 시간을 기준으로
Expand All @@ -10,22 +9,21 @@ import dotenv from 'dotenv';
import pg, { Pool } from 'pg';
import { LeaderboardRepository } from '@/repositories/leaderboard.repository';
import { PostLeaderboardSortType, UserLeaderboardSortType } from '@/types';
import { getKSTDateStringWithOffset } from '@/utils/date.util';

dotenv.config();

jest.setTimeout(60000); // 각 케이스당 60초 타임아웃 설정
jest.setTimeout(30000); // 각 케이스당 30초 타임아웃 설정

/**
* LeaderboardRepository 통합 테스트
*
* 이 테스트 파일은 실제 데이터베이스와 연결하여 LeaderboardRepository의 모든 메서드를
* 실제 환경과 동일한 조건에서 테스트합니다.
*/
describe.skip('LeaderboardRepository 통합 테스트', () => {
describe('LeaderboardRepository 통합 테스트', () => {
let testPool: Pool;
let repo: LeaderboardRepository;

// eslint-disable-next-line @typescript-eslint/naming-convention
const DEFAULT_PARAMS = {
USER_SORT: 'viewCount' as UserLeaderboardSortType,
POST_SORT: 'viewCount' as PostLeaderboardSortType,
Expand All @@ -45,7 +43,7 @@ describe.skip('LeaderboardRepository 통합 테스트', () => {
idleTimeoutMillis: 30000, // 연결 유휴 시간 (30초)
connectionTimeoutMillis: 5000, // 연결 시간 초과 (5초)
allowExitOnIdle: false, // 유휴 상태에서 종료 허용
statement_timeout: 60000, // 쿼리 타임아웃 증가 (60초)
statement_timeout: 30000, // 쿼리 타임아웃 증가 (30초)
};

// localhost 가 아니면 ssl 필수
Expand Down Expand Up @@ -80,10 +78,17 @@ describe.skip('LeaderboardRepository 통합 테스트', () => {

afterAll(async () => {
try {
jest.clearAllMocks();

// 풀 완전 종료
await testPool.end();
// 모든 쿼리 완료 대기
await new Promise(resolve => setTimeout(resolve, 1000));

// 풀 완전 종료
if (testPool) {
// 강제 종료: 모든 활성 쿼리와 연결 중지
await testPool.end();
}

// 추가 정리 시간
await new Promise(resolve => setTimeout(resolve, 1000));

logger.info('LeaderboardRepository 통합 테스트 DB 연결 종료');
} catch (error) {
Expand Down Expand Up @@ -225,6 +230,16 @@ describe.skip('LeaderboardRepository 통합 테스트', () => {
expect(user.username).not.toBeNull();
});
});

it('데이터 수집이 비정상적인 유저는 리더보드에 포함되지 않아야 한다', async () => {
const result = await repo.getUserLeaderboard(DEFAULT_PARAMS.USER_SORT, DEFAULT_PARAMS.DATE_RANGE, 30);

if (!isEnoughData(result, 1, '사용자 리더보드 비정상 유저 필터링')) return;

result.forEach((user) => {
expect(Number(user.total_views)).not.toBe(Number(user.view_diff));
});
});
});

describe('getPostLeaderboard', () => {
Expand Down Expand Up @@ -342,6 +357,20 @@ describe.skip('LeaderboardRepository 통합 테스트', () => {
expect(areDifferent).toBe(true);
}
});

it('데이터 수집이 비정상적인 게시물은 리더보드에 포함되지 않아야 한다', async () => {
const result = await repo.getPostLeaderboard(DEFAULT_PARAMS.POST_SORT, DEFAULT_PARAMS.DATE_RANGE, 30);
const pastDateKST = getKSTDateStringWithOffset(-DEFAULT_PARAMS.DATE_RANGE * 24 * 60);

if (!isEnoughData(result, 1, '게시물 리더보드 비정상 게시물 필터링')) return;

result.forEach((post) => {
if (post.released_at < pastDateKST) {
// eslint-disable-next-line jest/no-conditional-expect
expect(Number(post.total_views)).not.toBe(Number(post.view_diff));
}
});
});
});
});

Expand Down
22 changes: 20 additions & 2 deletions src/repositories/__test__/leaderboard.repo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,20 @@ describe('LeaderboardRepository', () => {
await repo.getUserLeaderboard('viewCount', mockDateRange, 10);

expect(mockPool.query).toHaveBeenCalledWith(
expect.stringContaining('WHERE date >='), // pastDateKST를 사용하는 부분 확인
expect.stringContaining('WHERE date ='), // pastDateKST를 사용하는 부분 확인
[expect.any(Number)], // limit
);
});

it('데이터 수집이 비정상적인 유저는 리더보드에 포함되지 않아야 한다', async () => {
await repo.getUserLeaderboard('viewCount', 30, 10);

expect(mockPool.query).toHaveBeenCalledWith(
expect.stringContaining('HAVING SUM(COALESCE(ts.today_view, 0)) != SUM(COALESCE(ts.today_view, 0) - COALESCE(ss.start_view, 0))'),
expect.anything(),
);
});

it('에러 발생 시 DBError를 던져야 한다', async () => {
mockPool.query.mockRejectedValue(new Error('DB connection failed'));
await expect(repo.getUserLeaderboard('viewCount', 30, 10)).rejects.toThrow(DBError);
Expand Down Expand Up @@ -156,11 +165,20 @@ describe('LeaderboardRepository', () => {
await repo.getPostLeaderboard('viewCount', mockDateRange, 10);

expect(mockPool.query).toHaveBeenCalledWith(
expect.stringContaining('WHERE date >='), // pastDateKST를 사용하는 부분 확인
expect.stringContaining('WHERE date ='), // pastDateKST를 사용하는 부분 확인
[expect.any(Number)], // limit
);
});

it('데이터 수집이 비정상적인 게시물은 리더보드에 포함되지 않아야 한다', async () => {
await repo.getPostLeaderboard('viewCount', 30, 10);

expect(mockPool.query).toHaveBeenCalledWith(
expect.stringContaining('COALESCE(ts.today_view, 0) != COALESCE(ts.today_view, 0) - COALESCE(ss.start_view, 0)'),
expect.anything()
);
});

it('에러 발생 시 DBError를 던져야 한다', async () => {
mockPool.query.mockRejectedValue(new Error('DB connection failed'));
await expect(repo.getPostLeaderboard('viewCount', 30, 10)).rejects.toThrow(DBError);
Expand Down
32 changes: 19 additions & 13 deletions src/repositories/leaderboard.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class LeaderboardRepository {
async getUserLeaderboard(sort: UserLeaderboardSortType, dateRange: number, limit: number) {
try {
const pastDateKST = getKSTDateStringWithOffset(-dateRange * 24 * 60);
const cteQuery = this.buildLeaderboardCteQuery(dateRange);
const cteQuery = this.buildLeaderboardCteQuery(dateRange, pastDateKST);

const query = `
${cteQuery}
Expand All @@ -21,15 +21,16 @@ export class LeaderboardRepository {
COALESCE(SUM(ts.today_view), 0) AS total_views,
COALESCE(SUM(ts.today_like), 0) AS total_likes,
COUNT(DISTINCT CASE WHEN p.is_active = true THEN p.id END) AS total_posts,
SUM(COALESCE(ts.today_view, 0) - COALESCE(ss.start_view, COALESCE(ts.today_view, 0))) AS view_diff,
SUM(COALESCE(ts.today_like, 0) - COALESCE(ss.start_like, COALESCE(ts.today_like, 0))) AS like_diff,
SUM(COALESCE(ts.today_view, 0) - COALESCE(ss.start_view, 0)) AS view_diff,
SUM(COALESCE(ts.today_like, 0) - COALESCE(ss.start_like, 0)) AS like_diff,
COUNT(DISTINCT CASE WHEN p.released_at >= '${pastDateKST}' AND p.is_active = true THEN p.id END) AS post_diff
FROM users_user u
LEFT JOIN posts_post p ON p.user_id = u.id
LEFT JOIN today_stats ts ON ts.post_id = p.id
LEFT JOIN start_stats ss ON ss.post_id = p.id
WHERE u.username IS NOT NULL
GROUP BY u.id, u.email, u.username
HAVING SUM(COALESCE(ts.today_view, 0)) != SUM(COALESCE(ts.today_view, 0) - COALESCE(ss.start_view, 0))
ORDER BY ${this.SORT_COL_MAPPING[sort]} DESC, u.id
LIMIT $1;
`;
Expand All @@ -44,7 +45,8 @@ export class LeaderboardRepository {

async getPostLeaderboard(sort: PostLeaderboardSortType, dateRange: number, limit: number) {
try {
const cteQuery = this.buildLeaderboardCteQuery(dateRange);
const pastDateKST = getKSTDateStringWithOffset(-dateRange * 24 * 60);
const cteQuery = this.buildLeaderboardCteQuery(dateRange, pastDateKST);

const query = `
${cteQuery}
Expand All @@ -56,13 +58,18 @@ export class LeaderboardRepository {
u.username AS username,
COALESCE(ts.today_view, 0) AS total_views,
COALESCE(ts.today_like, 0) AS total_likes,
COALESCE(ts.today_view, 0) - COALESCE(ss.start_view, COALESCE(ts.today_view, 0)) AS view_diff,
COALESCE(ts.today_like, 0) - COALESCE(ss.start_like, COALESCE(ts.today_like, 0)) AS like_diff
COALESCE(ts.today_view, 0) - COALESCE(ss.start_view, 0) AS view_diff,
COALESCE(ts.today_like, 0) - COALESCE(ss.start_like, 0) AS like_diff
FROM posts_post p
LEFT JOIN users_user u ON u.id = p.user_id
LEFT JOIN today_stats ts ON ts.post_id = p.id
LEFT JOIN start_stats ss ON ss.post_id = p.id
WHERE p.is_active = true
AND (
p.released_at >= '${pastDateKST}'
OR
COALESCE(ts.today_view, 0) != COALESCE(ts.today_view, 0) - COALESCE(ss.start_view, 0)
)
ORDER BY ${this.SORT_COL_MAPPING[sort]} DESC, p.id
LIMIT $1;
`;
Expand All @@ -76,10 +83,11 @@ export class LeaderboardRepository {
}

// 오늘 날짜와 기준 날짜의 통계를 가져오는 CTE(임시 결과 집합) 쿼리 빌드
private buildLeaderboardCteQuery(dateRange: number) {
private buildLeaderboardCteQuery(dateRange: number, pastDateKST?: string) {
const nowDateKST = getCurrentKSTDateString();
// 과거 날짜 계산 (dateRange일 전)
const pastDateKST = getKSTDateStringWithOffset(-dateRange * 24 * 60);
if (!pastDateKST) {
pastDateKST = getKSTDateStringWithOffset(-dateRange * 24 * 60);
}

return `
WITH
Expand All @@ -89,17 +97,15 @@ export class LeaderboardRepository {
daily_view_count AS today_view,
daily_like_count AS today_like
FROM posts_postdailystatistics
WHERE date <= '${nowDateKST}'
ORDER BY post_id, date DESC
WHERE date = '${nowDateKST}'
),
start_stats AS (
SELECT DISTINCT ON (post_id)
post_id,
daily_view_count AS start_view,
daily_like_count AS start_like
FROM posts_postdailystatistics
WHERE date >= '${pastDateKST}'
ORDER BY post_id, date ASC
WHERE date = '${pastDateKST}'
)
`;
}
Expand Down
Loading