Skip to content

Commit 8650263

Browse files
committed
feature: 미들웨어 자체 테스트 코드 추가, 그에 따른 velog api 유닛 테스트 추가
1 parent 89b29ca commit 8650263

File tree

3 files changed

+400
-0
lines changed

3 files changed

+400
-0
lines changed
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import { Request, Response } from 'express';
2+
import { authMiddleware } from '@/middlewares/auth.middleware';
3+
import pool from '@/configs/db.config';
4+
5+
// pool.query 모킹
6+
jest.mock('@/configs/db.config', () => ({
7+
query: jest.fn(),
8+
}));
9+
10+
// logger 모킹
11+
jest.mock('@/configs/logger.config', () => ({
12+
error: jest.fn(),
13+
info: jest.fn(),
14+
}));
15+
16+
describe('인증 미들웨어', () => {
17+
let mockRequest: Partial<Request>;
18+
let mockResponse: Partial<Response>;
19+
let nextFunction: jest.Mock;
20+
21+
beforeEach(() => {
22+
// 테스트마다 request, response, next 함수 초기화
23+
mockRequest = {
24+
body: {},
25+
headers: {},
26+
cookies: {},
27+
};
28+
mockResponse = {
29+
json: jest.fn(),
30+
status: jest.fn().mockReturnThis(),
31+
};
32+
nextFunction = jest.fn();
33+
});
34+
35+
afterEach(() => {
36+
jest.clearAllMocks();
37+
});
38+
39+
describe('verify', () => {
40+
const validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYzc1MDcyNDAtMDkzYi0xMWVhLTlhYWUtYTU4YTg2YmIwNTIwIiwiaWF0IjoxNjAzOTM0NTI5LCJleHAiOjE2MDM5MzgxMjksImlzcyI6InZlbG9nLmlvIiwic3ViIjoiYWNjZXNzX3Rva2VuIn0.Q_I4PMBeeZSU-HbPZt7z9OW-tQjE0NI0I0DLF2qpZjY';
41+
42+
it('유효한 토큰으로 사용자 정보를 Request에 추가해야 한다', async () => {
43+
// 유효한 토큰 준비
44+
mockRequest.cookies = {
45+
'access_token': validToken,
46+
'refresh_token': 'refresh-token'
47+
};
48+
49+
// 사용자 정보 mock
50+
const mockUser = {
51+
id: 1,
52+
username: 'testuser',
53+
54+
velog_uuid: 'c7507240-093b-11ea-9aae-a58a86bb0520'
55+
};
56+
57+
// DB 쿼리 결과 모킹
58+
(pool.query as jest.Mock).mockResolvedValueOnce({
59+
rows: [mockUser]
60+
});
61+
62+
// 미들웨어 실행
63+
await authMiddleware.verify(
64+
mockRequest as Request,
65+
mockResponse as Response,
66+
nextFunction
67+
);
68+
69+
// 검증
70+
expect(nextFunction).toHaveBeenCalledTimes(1);
71+
expect(nextFunction).not.toHaveBeenCalledWith(expect.any(Error));
72+
expect(mockRequest.user).toEqual(mockUser);
73+
expect(mockRequest.tokens).toEqual({
74+
accessToken: validToken,
75+
refreshToken: 'refresh-token'
76+
});
77+
expect(pool.query).toHaveBeenCalledWith(
78+
'SELECT * FROM "users_user" WHERE velog_uuid = $1',
79+
['c7507240-093b-11ea-9aae-a58a86bb0520']
80+
);
81+
});
82+
83+
it('토큰이 없으면 InvalidTokenError를 전달해야 한다', async () => {
84+
// 토큰 없음
85+
mockRequest.cookies = {};
86+
87+
// 미들웨어 실행
88+
await authMiddleware.verify(
89+
mockRequest as Request,
90+
mockResponse as Response,
91+
nextFunction
92+
);
93+
94+
// 검증
95+
expect(nextFunction).toHaveBeenCalledTimes(1);
96+
expect(nextFunction).toHaveBeenCalledWith(
97+
expect.objectContaining({
98+
name: 'InvalidTokenError',
99+
message: 'accessToken과 refreshToken의 입력이 올바르지 않습니다'
100+
})
101+
);
102+
});
103+
104+
it('유효하지 않은 토큰으로 InvalidTokenError를 전달해야 한다', async () => {
105+
// 유효하지 않은 토큰 (JWT 형식은 맞지만 내용이 잘못됨)
106+
const invalidToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnZhbGlkIjoidG9rZW4ifQ.invalidSignature';
107+
mockRequest.cookies = {
108+
'access_token': invalidToken,
109+
'refresh_token': 'refresh-token'
110+
};
111+
112+
// 미들웨어 실행
113+
await authMiddleware.verify(
114+
mockRequest as Request,
115+
mockResponse as Response,
116+
nextFunction
117+
);
118+
119+
// 검증
120+
expect(nextFunction).toHaveBeenCalledTimes(1);
121+
expect(nextFunction).toHaveBeenCalledWith(expect.any(Error));
122+
});
123+
124+
it('UUID가 없는 페이로드로 InvalidTokenError를 전달해야 한다', async () => {
125+
// UUID가 없는 토큰 (페이로드를 임의로 조작)
126+
const tokenWithoutUUID = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MDM5MzQ1MjksImV4cCI6MTYwMzkzODEyOSwiaXNzIjoidmVsb2cuaW8iLCJzdWIiOiJhY2Nlc3NfdG9rZW4ifQ.2fLHQ3yKs9UmBQUa2oat9UOLiXzXvrhv_XHU2qwLBs8';
127+
128+
mockRequest.cookies = {
129+
'access_token': tokenWithoutUUID,
130+
'refresh_token': 'refresh-token'
131+
};
132+
133+
// 미들웨어 실행
134+
await authMiddleware.verify(
135+
mockRequest as Request,
136+
mockResponse as Response,
137+
nextFunction
138+
);
139+
140+
// 검증
141+
expect(nextFunction).toHaveBeenCalledTimes(1);
142+
expect(nextFunction).toHaveBeenCalledWith(
143+
expect.objectContaining({
144+
name: 'InvalidTokenError',
145+
message: '유효하지 않은 토큰 페이로드 입니다.'
146+
})
147+
);
148+
});
149+
150+
it('사용자를 찾을 수 없으면 next를 호출해야 한다', async () => {
151+
// 유효한 토큰 준비
152+
mockRequest.cookies = {
153+
'access_token': validToken,
154+
'refresh_token': 'refresh-token'
155+
};
156+
157+
// 사용자가 없음 모킹
158+
(pool.query as jest.Mock).mockResolvedValueOnce({
159+
rows: []
160+
});
161+
162+
// 미들웨어 실행
163+
await authMiddleware.verify(
164+
mockRequest as Request,
165+
mockResponse as Response,
166+
nextFunction
167+
);
168+
169+
// 검증
170+
expect(nextFunction).toHaveBeenCalledTimes(1);
171+
expect(mockRequest.user).toBeUndefined();
172+
});
173+
174+
it('쿠키 대신 요청 본문에서 토큰을 추출해야 한다', async () => {
175+
// 요청 본문에 토큰 설정
176+
mockRequest.body = {
177+
accessToken: validToken,
178+
refreshToken: 'refresh-token'
179+
};
180+
181+
// 사용자 정보 mock
182+
const mockUser = {
183+
id: 1,
184+
username: 'testuser',
185+
186+
velog_uuid: 'c7507240-093b-11ea-9aae-a58a86bb0520'
187+
};
188+
189+
// DB 쿼리 결과 모킹
190+
(pool.query as jest.Mock).mockResolvedValueOnce({
191+
rows: [mockUser]
192+
});
193+
194+
// 미들웨어 실행
195+
await authMiddleware.verify(
196+
mockRequest as Request,
197+
mockResponse as Response,
198+
nextFunction
199+
);
200+
201+
// 검증
202+
expect(nextFunction).toHaveBeenCalledTimes(1);
203+
expect(nextFunction).not.toHaveBeenCalledWith(expect.any(Error));
204+
expect(mockRequest.user).toEqual(mockUser);
205+
});
206+
207+
it('쿠키와 요청 본문 대신 헤더에서 토큰을 추출해야 한다', async () => {
208+
// 헤더에 토큰 설정
209+
mockRequest.headers = {
210+
'access_token': validToken,
211+
'refresh_token': 'refresh-token'
212+
};
213+
214+
// 사용자 정보 mock
215+
const mockUser = {
216+
id: 1,
217+
username: 'testuser',
218+
219+
velog_uuid: 'c7507240-093b-11ea-9aae-a58a86bb0520'
220+
};
221+
222+
// DB 쿼리 결과 모킹
223+
(pool.query as jest.Mock).mockResolvedValueOnce({
224+
rows: [mockUser]
225+
});
226+
227+
// 미들웨어 실행
228+
await authMiddleware.verify(
229+
mockRequest as Request,
230+
mockResponse as Response,
231+
nextFunction
232+
);
233+
234+
// 검증
235+
expect(nextFunction).toHaveBeenCalledTimes(1);
236+
expect(nextFunction).not.toHaveBeenCalledWith(expect.any(Error));
237+
expect(mockRequest.user).toEqual(mockUser);
238+
});
239+
});
240+
});
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import axios from 'axios';
2+
import { fetchVelogApi } from '@/modules/velog/velog.api';
3+
import { VELOG_API_URL, VELOG_QUERIES } from '@/modules/velog/velog.constans';
4+
5+
// axios 모킹
6+
jest.mock('axios');
7+
const mockedAxios = axios as jest.Mocked<typeof axios>;
8+
9+
// logger 모킹 (콘솔 출력 방지)
10+
jest.mock('@/configs/logger.config', () => ({
11+
error: jest.fn(),
12+
info: jest.fn(),
13+
}));
14+
15+
describe('Velog API', () => {
16+
const mockAccessToken = 'test-access-token';
17+
const mockRefreshToken = 'test-refresh-token';
18+
19+
beforeEach(() => {
20+
jest.clearAllMocks();
21+
});
22+
23+
describe('fetchVelogApi', () => {
24+
it('유효한 토큰으로 사용자 정보를 성공적으로 가져와야 한다', async () => {
25+
// API 응답 모킹
26+
const mockResponse = {
27+
data: {
28+
data: {
29+
currentUser: {
30+
id: 'user-uuid',
31+
username: 'testuser',
32+
33+
profile: {
34+
thumbnail: 'https://example.com/avatar.png'
35+
}
36+
}
37+
}
38+
}
39+
};
40+
41+
mockedAxios.post.mockResolvedValueOnce(mockResponse);
42+
43+
const result = await fetchVelogApi(mockAccessToken, mockRefreshToken);
44+
45+
// 결과 검증
46+
expect(result).toEqual({
47+
id: 'user-uuid',
48+
username: 'testuser',
49+
50+
profile: {
51+
thumbnail: 'https://example.com/avatar.png'
52+
}
53+
});
54+
55+
// axios 호출 검증
56+
expect(mockedAxios.post).toHaveBeenCalledTimes(1);
57+
expect(mockedAxios.post).toHaveBeenCalledWith(
58+
VELOG_API_URL,
59+
{ VELOG_QUERIES, variables: {} },
60+
{
61+
headers: {
62+
authority: 'v3.velog.io',
63+
origin: 'https://velog.io',
64+
'content-type': 'application/json',
65+
cookie: `access_token=${mockAccessToken}; refresh_token=${mockRefreshToken}`,
66+
},
67+
}
68+
);
69+
});
70+
71+
it('이메일이 없는 사용자 정보도 성공적으로 처리해야 한다', async () => {
72+
// 이메일이 없는 API 응답 모킹
73+
const mockResponse = {
74+
data: {
75+
data: {
76+
currentUser: {
77+
id: 'user-uuid',
78+
username: 'testuser',
79+
// email 필드 없음
80+
profile: {
81+
thumbnail: 'https://example.com/avatar.png'
82+
}
83+
}
84+
}
85+
}
86+
};
87+
88+
mockedAxios.post.mockResolvedValueOnce(mockResponse);
89+
90+
const result = await fetchVelogApi(mockAccessToken, mockRefreshToken);
91+
92+
// 결과 검증 - email이 null로 설정되었는지 확인
93+
expect(result).toEqual({
94+
id: 'user-uuid',
95+
username: 'testuser',
96+
email: null,
97+
profile: {
98+
thumbnail: 'https://example.com/avatar.png'
99+
}
100+
});
101+
});
102+
103+
it('API 응답에 오류가 있으면 InvalidTokenError를 던져야 한다', async () => {
104+
// 오류가 포함된 API 응답 모킹
105+
const mockResponse = {
106+
data: {
107+
errors: [{ message: '인증 실패' }],
108+
data: { currentUser: null }
109+
}
110+
};
111+
112+
mockedAxios.post.mockResolvedValueOnce(mockResponse);
113+
114+
// 함수 호출 시 예외 발생 검증
115+
await expect(fetchVelogApi(mockAccessToken, mockRefreshToken))
116+
.rejects.toThrow(expect.objectContaining({
117+
name: 'InvalidTokenError',
118+
message: 'Velog API 인증에 실패했습니다.'
119+
}));
120+
});
121+
122+
it('currentUser가 null이면 InvalidTokenError를 던져야 한다', async () => {
123+
// currentUser가 null인 API 응답 모킹
124+
const mockResponse = {
125+
data: {
126+
data: {
127+
currentUser: null
128+
}
129+
}
130+
};
131+
132+
mockedAxios.post.mockResolvedValueOnce(mockResponse);
133+
134+
// 함수 호출 시 예외 발생 검증
135+
await expect(fetchVelogApi(mockAccessToken, mockRefreshToken))
136+
.rejects.toThrow(expect.objectContaining({
137+
name: 'InvalidTokenError',
138+
message: 'Velog 사용자 정보를 가져오지 못했습니다.'
139+
}));
140+
141+
});
142+
143+
it('API 호출 자체가 실패하면 InvalidTokenError를 던져야 한다', async () => {
144+
// axios 호출 실패 모킹
145+
mockedAxios.post.mockRejectedValueOnce(new Error('네트워크 오류'));
146+
147+
// 함수 호출 시 예외 발생 검증
148+
await expect(fetchVelogApi(mockAccessToken, mockRefreshToken))
149+
.rejects.toThrow(expect.objectContaining({
150+
name: 'InvalidTokenError',
151+
message: 'Velog API 인증에 실패했습니다.'
152+
}));
153+
});
154+
});
155+
});

0 commit comments

Comments
 (0)