1+ import { Request , Response } from 'express' ;
2+ import { Socket } from 'net' ;
3+ import {
4+ createLogContext ,
5+ logError ,
6+ logAccess ,
7+ getClientIp ,
8+ getLogLevel ,
9+ } from '@/utils/logging.util' ;
10+ import { CustomError } from '@/exception' ;
11+ import { User } from '@/types' ;
12+ import logger from '@/configs/logger.config' ;
13+
14+ // logger 모킹
15+ jest . mock ( '@/configs/logger.config' , ( ) => ( {
16+ error : jest . fn ( ) ,
17+ warn : jest . fn ( ) ,
18+ info : jest . fn ( ) ,
19+ } ) ) ;
20+
21+ describe ( 'Logging Utilities' , ( ) => {
22+ let mockRequest : Partial < Request > ;
23+ let mockResponse : Partial < Response > ;
24+
25+ beforeEach ( ( ) => {
26+ mockRequest = {
27+ headers : { 'x-forwarded-for' : '127.0.0.1' } ,
28+ method : 'GET' ,
29+ originalUrl : '/api/test' ,
30+ /* eslint-disable @typescript-eslint/consistent-type-assertions */
31+ user : { id : 123 , velog_uuid : 'user123' } as User ,
32+ requestId : 'test-request-id' ,
33+ startTime : Date . now ( ) - 100 ,
34+ } ;
35+
36+ mockResponse = {
37+ statusCode : 200 ,
38+ get : jest . fn ( ) ,
39+ } ;
40+ } ) ;
41+
42+ afterEach ( ( ) => {
43+ jest . clearAllMocks ( ) ;
44+ } ) ;
45+
46+ describe ( 'getClientIp' , ( ) => {
47+ it ( 'x-forwarded-for 헤더에서 IP를 추출해야 한다' , ( ) => {
48+ mockRequest . headers = { 'x-forwarded-for' : '192.168.1.1, 10.0.0.1' } ;
49+ expect ( getClientIp ( mockRequest as Request ) ) . toBe ( '192.168.1.1' ) ;
50+ } ) ;
51+
52+ it ( 'x-real-ip 헤더에서 IP를 추출해야 한다' , ( ) => {
53+ mockRequest . headers = { 'x-real-ip' : '203.0.113.1' } ;
54+ expect ( getClientIp ( mockRequest as Request ) ) . toBe ( '203.0.113.1' ) ;
55+ } ) ;
56+
57+ it ( '헤더가 없으면 unknown을 반환해야 한다' , ( ) => {
58+ mockRequest . headers = { } ;
59+ /* eslint-disable @typescript-eslint/consistent-type-assertions */
60+ mockRequest . socket = { remoteAddress : undefined } as Socket ;
61+ expect ( getClientIp ( mockRequest as Request ) ) . toBe ( 'unknown' ) ;
62+ } ) ;
63+ } ) ;
64+
65+ describe ( 'getLogLevel' , ( ) => {
66+ it ( '200은 info 레벨을 반환해야 한다' , ( ) => {
67+ expect ( getLogLevel ( 200 ) ) . toBe ( 'info' ) ;
68+ } ) ;
69+
70+ it ( '404는 warn 레벨을 반환해야 한다' , ( ) => {
71+ expect ( getLogLevel ( 404 ) ) . toBe ( 'warn' ) ;
72+ } ) ;
73+
74+ it ( '500은 error 레벨을 반환해야 한다' , ( ) => {
75+ expect ( getLogLevel ( 500 ) ) . toBe ( 'error' ) ;
76+ } ) ;
77+ } ) ;
78+
79+ describe ( 'createLogContext' , ( ) => {
80+ it ( '요청에서 올바른 로그 컨텍스트를 생성해야 한다' , ( ) => {
81+ const context = createLogContext ( mockRequest as Request ) ;
82+
83+ expect ( context . requestId ) . toBe ( 'test-request-id' ) ;
84+ expect ( context . userId ) . toBe ( 123 ) ;
85+ expect ( context . method ) . toBe ( 'GET' ) ;
86+ expect ( context . url ) . toBe ( '/api/test' ) ;
87+ expect ( context . userAgent ) . toBeUndefined ( ) ;
88+ expect ( context . ip ) . toBe ( '127.0.0.1' ) ;
89+ } ) ;
90+ } ) ;
91+
92+ describe ( 'logError' , ( ) => {
93+ it ( '일반 에러를 올바르게 로깅해야 한다' , ( ) => {
94+ const error = new Error ( 'Test error' ) ;
95+ mockResponse . statusCode = 500 ; // error 레벨을 위해 500으로 설정
96+
97+ logError ( mockRequest as Request , mockResponse as Response , error ) ;
98+
99+ expect ( logger . error ) . toHaveBeenCalledWith (
100+ expect . objectContaining ( {
101+ message : expect . objectContaining ( {
102+ logger : 'error' ,
103+ message : 'Test error' ,
104+ statusCode : 500 ,
105+ requestId : 'test-request-id' ,
106+ userId : 123 ,
107+ method : 'GET' ,
108+ url : '/api/test' ,
109+ ip : '127.0.0.1' ,
110+ } )
111+ } )
112+ ) ;
113+ } ) ;
114+
115+ it ( 'CustomError의 경우 에러 코드를 포함해야 한다' , ( ) => {
116+ const customError = new CustomError ( 'Custom error' , 'CUSTOM_ERROR' , 400 ) ;
117+ mockResponse . statusCode = 400 ;
118+
119+ logError ( mockRequest as Request , mockResponse as Response , customError ) ;
120+
121+ expect ( logger . error ) . toHaveBeenCalledWith (
122+ expect . objectContaining ( {
123+ message : expect . objectContaining ( {
124+ errorCode : 'CUSTOM_ERROR' ,
125+ } )
126+ } )
127+ ) ;
128+ } ) ;
129+
130+ it ( '500이거나 예상하지 못한 에러는 스택 트레이스를 포함해야 한다' , ( ) => {
131+ const error = new Error ( 'Test error' ) ;
132+ mockResponse . statusCode = 500 ;
133+
134+ logError ( mockRequest as Request , mockResponse as Response , error ) ;
135+
136+ expect ( logger . error ) . toHaveBeenCalledWith (
137+ expect . objectContaining ( {
138+ message : expect . objectContaining ( {
139+ stack : expect . any ( String ) ,
140+ } )
141+ } )
142+ ) ;
143+ } ) ;
144+ } ) ;
145+
146+ describe ( 'logAccess' , ( ) => {
147+ it ( '액세스 로그를 올바르게 생성해야 한다' , ( ) => {
148+ ( mockResponse . get as jest . Mock ) . mockReturnValue ( '1024' ) ;
149+
150+ logAccess ( mockRequest as Request , mockResponse as Response ) ;
151+
152+ expect ( logger . info ) . toHaveBeenCalledWith (
153+ expect . objectContaining ( {
154+ logger : 'access' ,
155+ statusCode : 200 ,
156+ responseTime : expect . any ( Number ) ,
157+ responseSize : 1024 ,
158+ } )
159+ ) ;
160+ } ) ;
161+ } ) ;
162+ } ) ;
0 commit comments