1+ import { Pool } from 'pg' ;
2+ import { TotalStatsRepository } from '@/repositories/totalStats.repository' ;
3+ import { DBError } from '@/exception' ;
4+ import { getKSTDateStringWithOffset } from '@/utils/date.util' ;
5+ import { createMockQueryResult } from './fixture' ;
6+
7+ // Mock dependencies
8+ jest . mock ( '@/configs/logger.config' , ( ) => ( {
9+ error : jest . fn ( ) ,
10+ } ) ) ;
11+
12+ jest . mock ( '@/utils/date.util' , ( ) => ( {
13+ getKSTDateStringWithOffset : jest . fn ( ) ,
14+ } ) ) ;
15+
16+ describe ( 'TotalStatsRepository' , ( ) => {
17+ let repository : TotalStatsRepository ;
18+ let mockPool : jest . Mocked < Pool > ;
19+ let mockGetKSTDateStringWithOffset : jest . MockedFunction < typeof getKSTDateStringWithOffset > ;
20+
21+ beforeEach ( ( ) => {
22+ mockPool = {
23+ query : jest . fn ( ) ,
24+ } as unknown as jest . Mocked < Pool > ;
25+
26+ mockGetKSTDateStringWithOffset = getKSTDateStringWithOffset as jest . MockedFunction < typeof getKSTDateStringWithOffset > ;
27+
28+ repository = new TotalStatsRepository ( mockPool ) ;
29+ jest . clearAllMocks ( ) ;
30+ } ) ;
31+
32+ describe ( 'getTotalStats' , ( ) => {
33+ const userId = 1 ;
34+ const period = 7 ;
35+ const mockStartDate = '2025-05-27' ;
36+
37+ beforeEach ( ( ) => {
38+ mockGetKSTDateStringWithOffset . mockReturnValue ( mockStartDate ) ;
39+ } ) ;
40+
41+ describe ( 'view 타입 통계 조회' , ( ) => {
42+ it ( '조회수 통계를 성공적으로 조회해야 한다' , async ( ) => {
43+ // Given
44+ const mockViewStats = [
45+ { date : '2025-05-27' , total_value : '100' } ,
46+ { date : '2025-05-28' , total_value : '150' } ,
47+ { date : '2025-05-29' , total_value : '200' } ,
48+ ] ;
49+
50+ mockPool . query . mockResolvedValue ( createMockQueryResult ( mockViewStats ) ) ;
51+
52+ // When
53+ const result = await repository . getTotalStats ( userId , period , 'view' ) ;
54+
55+ // Then
56+ expect ( result ) . toEqual ( mockViewStats ) ;
57+ expect ( mockGetKSTDateStringWithOffset ) . toHaveBeenCalledWith ( - period * 24 * 60 ) ;
58+ expect ( mockPool . query ) . toHaveBeenCalledWith (
59+ expect . stringContaining ( 'SUM(pds.daily_view_count)' ) ,
60+ [ userId , mockStartDate ]
61+ ) ;
62+ } ) ;
63+
64+ it ( '조회수 통계 조회 시 DB 에러가 발생하면 DBError를 던져야 한다' , async ( ) => {
65+ // Given
66+ mockPool . query . mockRejectedValue ( new Error ( 'Database connection failed' ) ) ;
67+
68+ // When & Then
69+ await expect ( repository . getTotalStats ( userId , period , 'view' ) ) . rejects . toThrow (
70+ new DBError ( '조회수 통계 조회 중 문제가 발생했습니다.' )
71+ ) ;
72+ } ) ;
73+ } ) ;
74+
75+ describe ( 'like 타입 통계 조회' , ( ) => {
76+ it ( '좋아요 통계를 성공적으로 조회해야 한다' , async ( ) => {
77+ // Given
78+ const mockLikeStats = [
79+ { date : '2025-05-27' , total_value : '50' } ,
80+ { date : '2025-05-28' , total_value : '75' } ,
81+ { date : '2025-05-29' , total_value : '100' } ,
82+ ] ;
83+
84+ mockPool . query . mockResolvedValue ( createMockQueryResult ( mockLikeStats ) ) ;
85+
86+ // When
87+ const result = await repository . getTotalStats ( userId , period , 'like' ) ;
88+
89+ // Then
90+ expect ( result ) . toEqual ( mockLikeStats ) ;
91+ expect ( mockGetKSTDateStringWithOffset ) . toHaveBeenCalledWith ( - period * 24 * 60 ) ;
92+ expect ( mockPool . query ) . toHaveBeenCalledWith (
93+ expect . stringContaining ( 'SUM(pds.daily_like_count)' ) ,
94+ [ userId , mockStartDate ]
95+ ) ;
96+ } ) ;
97+
98+ it ( '좋아요 통계 조회 시 DB 에러가 발생하면 DBError를 던져야 한다' , async ( ) => {
99+ // Given
100+ mockPool . query . mockRejectedValue ( new Error ( 'Database connection failed' ) ) ;
101+
102+ // When & Then
103+ await expect ( repository . getTotalStats ( userId , period , 'like' ) ) . rejects . toThrow (
104+ new DBError ( '좋아요 통계 조회 중 문제가 발생했습니다.' )
105+ ) ;
106+ } ) ;
107+ } ) ;
108+
109+ describe ( 'post 타입 통계 조회' , ( ) => {
110+ it ( '게시글 통계를 성공적으로 조회해야 한다' , async ( ) => {
111+ // Given
112+ const mockPostStats = [
113+ { date : '2025-05-27' , total_value : 5 } ,
114+ { date : '2025-05-28' , total_value : 7 } ,
115+ { date : '2025-05-29' , total_value : 10 } ,
116+ ] ;
117+
118+ mockPool . query . mockResolvedValue ( createMockQueryResult ( mockPostStats ) ) ;
119+
120+ // When
121+ const result = await repository . getTotalStats ( userId , period , 'post' ) ;
122+
123+ // Then
124+ expect ( result ) . toEqual ( mockPostStats ) ;
125+ expect ( mockGetKSTDateStringWithOffset ) . toHaveBeenCalledWith ( - period * 24 * 60 ) ;
126+ expect ( mockPool . query ) . toHaveBeenCalledWith (
127+ expect . stringContaining ( 'WITH date_series AS' ) ,
128+ [ userId , mockStartDate ]
129+ ) ;
130+ expect ( mockPool . query ) . toHaveBeenCalledWith (
131+ expect . stringContaining ( 'SUM(dp.post_count) OVER' ) ,
132+ [ userId , mockStartDate ]
133+ ) ;
134+ } ) ;
135+
136+ it ( '게시글 통계 조회 시 DB 에러가 발생하면 DBError를 던져야 한다' , async ( ) => {
137+ // Given
138+ mockPool . query . mockRejectedValue ( new Error ( 'Database connection failed' ) ) ;
139+
140+ // When & Then
141+ await expect ( repository . getTotalStats ( userId , period , 'post' ) ) . rejects . toThrow (
142+ new DBError ( '게시글 통계 조회 중 문제가 발생했습니다.' )
143+ ) ;
144+ } ) ;
145+ } ) ;
146+
147+ describe ( '잘못된 타입 처리' , ( ) => {
148+ it ( '지원되지 않는 통계 타입이 전달되면 DBError를 던져야 한다' , async ( ) => {
149+ // When & Then
150+ await expect (
151+ repository . getTotalStats ( userId , period , 'invalid' as any )
152+ ) . rejects . toThrow ( new DBError ( '지원되지 않는 통계 타입입니다.' ) ) ;
153+
154+ expect ( mockPool . query ) . not . toHaveBeenCalled ( ) ;
155+ } ) ;
156+ } ) ;
157+
158+ describe ( '다양한 기간 테스트' , ( ) => {
159+ it ( '30일 기간으로 통계를 조회할 수 있어야 한다' , async ( ) => {
160+ // Given
161+ const period30 = 30 ;
162+ const mockStats = [ { date : '2025-04-27' , total_value : '1000' } ] ;
163+
164+ mockPool . query . mockResolvedValue ( createMockQueryResult ( mockStats ) ) ;
165+
166+ // When
167+ await repository . getTotalStats ( userId , period30 , 'view' ) ;
168+
169+ // Then
170+ expect ( mockGetKSTDateStringWithOffset ) . toHaveBeenCalledWith ( - period30 * 24 * 60 ) ;
171+ } ) ;
172+ } ) ;
173+
174+ describe ( '빈 결과 처리' , ( ) => {
175+ it ( '데이터가 없을 때 빈 배열을 반환해야 한다' , async ( ) => {
176+ // Given
177+ mockPool . query . mockResolvedValue ( createMockQueryResult ( [ ] ) ) ;
178+
179+ // When
180+ const result = await repository . getTotalStats ( userId , period , 'view' ) ;
181+
182+ // Then
183+ expect ( result ) . toEqual ( [ ] ) ;
184+ expect ( result ) . toHaveLength ( 0 ) ;
185+ } ) ;
186+ } ) ;
187+
188+ describe ( 'SQL 쿼리 검증' , ( ) => {
189+ beforeEach ( ( ) => {
190+ mockPool . query . mockResolvedValue ( createMockQueryResult ( [ ] ) ) ;
191+ } ) ;
192+
193+ it ( 'view 통계 쿼리가 올바른 테이블과 조건을 포함해야 한다' , async ( ) => {
194+ // When
195+ await repository . getTotalStats ( userId , period , 'view' ) ;
196+
197+ // Then
198+ const calledQuery = mockPool . query . mock . calls [ 0 ] [ 0 ] as string ;
199+ expect ( calledQuery ) . toContain ( 'posts_postdailystatistics pds' ) ;
200+ expect ( calledQuery ) . toContain ( 'JOIN posts_post p ON p.id = pds.post_id' ) ;
201+ expect ( calledQuery ) . toContain ( 'p.user_id = $1' ) ;
202+ expect ( calledQuery ) . toContain ( 'p.is_active = true' ) ;
203+ expect ( calledQuery ) . toContain ( 'pds.date >= $2' ) ;
204+ expect ( calledQuery ) . toContain ( 'SUM(pds.daily_view_count)' ) ;
205+ } ) ;
206+
207+ it ( 'like 통계 쿼리가 올바른 컬럼을 조회해야 한다' , async ( ) => {
208+ // When
209+ await repository . getTotalStats ( userId , period , 'like' ) ;
210+
211+ // Then
212+ const calledQuery = mockPool . query . mock . calls [ 0 ] [ 0 ] as string ;
213+ expect ( calledQuery ) . toContain ( 'SUM(pds.daily_like_count)' ) ;
214+ } ) ;
215+
216+ it ( 'post 통계 쿼리가 CTE와 윈도우 함수를 사용해야 한다' , async ( ) => {
217+ // When
218+ await repository . getTotalStats ( userId , period , 'post' ) ;
219+
220+ // Then
221+ const calledQuery = mockPool . query . mock . calls [ 0 ] [ 0 ] as string ;
222+ expect ( calledQuery ) . toContain ( 'WITH date_series AS' ) ;
223+ expect ( calledQuery ) . toContain ( 'generate_series' ) ;
224+ expect ( calledQuery ) . toContain ( 'SUM(dp.post_count) OVER' ) ;
225+ expect ( calledQuery ) . toContain ( 'DATE(p.released_at)' ) ;
226+ } ) ;
227+ } ) ;
228+ } ) ;
229+ } ) ;
0 commit comments