Skip to content
Prev Previous commit
Next Next commit
feature: 미들웨어 자체 테스트 코드 추가, 그에 따른 velog api 유닛 테스트 추가
  • Loading branch information
Nuung committed May 4, 2025
commit 865026330889adb4d57f6dd9d0921e8790da0d06
240 changes: 240 additions & 0 deletions src/middlewares/__test__/auth.middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import { Request, Response } from 'express';
import { authMiddleware } from '@/middlewares/auth.middleware';
import pool from '@/configs/db.config';

// pool.query 모킹
jest.mock('@/configs/db.config', () => ({
query: jest.fn(),
}));

// logger 모킹
jest.mock('@/configs/logger.config', () => ({
error: jest.fn(),
info: jest.fn(),
}));

describe('인증 미들웨어', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let nextFunction: jest.Mock;

beforeEach(() => {
// 테스트마다 request, response, next 함수 초기화
mockRequest = {
body: {},
headers: {},
cookies: {},
};
mockResponse = {
json: jest.fn(),
status: jest.fn().mockReturnThis(),
};
nextFunction = jest.fn();
});

afterEach(() => {
jest.clearAllMocks();
});

describe('verify', () => {
const validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYzc1MDcyNDAtMDkzYi0xMWVhLTlhYWUtYTU4YTg2YmIwNTIwIiwiaWF0IjoxNjAzOTM0NTI5LCJleHAiOjE2MDM5MzgxMjksImlzcyI6InZlbG9nLmlvIiwic3ViIjoiYWNjZXNzX3Rva2VuIn0.Q_I4PMBeeZSU-HbPZt7z9OW-tQjE0NI0I0DLF2qpZjY';

it('유효한 토큰으로 사용자 정보를 Request에 추가해야 한다', async () => {
// 유효한 토큰 준비
mockRequest.cookies = {
'access_token': validToken,
'refresh_token': 'refresh-token'
};

// 사용자 정보 mock
const mockUser = {
id: 1,
username: 'testuser',
email: 'test@example.com',
velog_uuid: 'c7507240-093b-11ea-9aae-a58a86bb0520'
};

// DB 쿼리 결과 모킹
(pool.query as jest.Mock).mockResolvedValueOnce({
rows: [mockUser]
});

// 미들웨어 실행
await authMiddleware.verify(
mockRequest as Request,
mockResponse as Response,
nextFunction
);

// 검증
expect(nextFunction).toHaveBeenCalledTimes(1);
expect(nextFunction).not.toHaveBeenCalledWith(expect.any(Error));
expect(mockRequest.user).toEqual(mockUser);
expect(mockRequest.tokens).toEqual({
accessToken: validToken,
refreshToken: 'refresh-token'
});
expect(pool.query).toHaveBeenCalledWith(
'SELECT * FROM "users_user" WHERE velog_uuid = $1',
['c7507240-093b-11ea-9aae-a58a86bb0520']
);
});

it('토큰이 없으면 InvalidTokenError를 전달해야 한다', async () => {
// 토큰 없음
mockRequest.cookies = {};

// 미들웨어 실행
await authMiddleware.verify(
mockRequest as Request,
mockResponse as Response,
nextFunction
);

// 검증
expect(nextFunction).toHaveBeenCalledTimes(1);
expect(nextFunction).toHaveBeenCalledWith(
expect.objectContaining({
name: 'InvalidTokenError',
message: 'accessToken과 refreshToken의 입력이 올바르지 않습니다'
})
);
});

it('유효하지 않은 토큰으로 InvalidTokenError를 전달해야 한다', async () => {
// 유효하지 않은 토큰 (JWT 형식은 맞지만 내용이 잘못됨)
const invalidToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnZhbGlkIjoidG9rZW4ifQ.invalidSignature';
mockRequest.cookies = {
'access_token': invalidToken,
'refresh_token': 'refresh-token'
};

// 미들웨어 실행
await authMiddleware.verify(
mockRequest as Request,
mockResponse as Response,
nextFunction
);

// 검증
expect(nextFunction).toHaveBeenCalledTimes(1);
expect(nextFunction).toHaveBeenCalledWith(expect.any(Error));
});

it('UUID가 없는 페이로드로 InvalidTokenError를 전달해야 한다', async () => {
// UUID가 없는 토큰 (페이로드를 임의로 조작)
const tokenWithoutUUID = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MDM5MzQ1MjksImV4cCI6MTYwMzkzODEyOSwiaXNzIjoidmVsb2cuaW8iLCJzdWIiOiJhY2Nlc3NfdG9rZW4ifQ.2fLHQ3yKs9UmBQUa2oat9UOLiXzXvrhv_XHU2qwLBs8';

mockRequest.cookies = {
'access_token': tokenWithoutUUID,
'refresh_token': 'refresh-token'
};

// 미들웨어 실행
await authMiddleware.verify(
mockRequest as Request,
mockResponse as Response,
nextFunction
);

// 검증
expect(nextFunction).toHaveBeenCalledTimes(1);
expect(nextFunction).toHaveBeenCalledWith(
expect.objectContaining({
name: 'InvalidTokenError',
message: '유효하지 않은 토큰 페이로드 입니다.'
})
);
});

it('사용자를 찾을 수 없으면 next를 호출해야 한다', async () => {
// 유효한 토큰 준비
mockRequest.cookies = {
'access_token': validToken,
'refresh_token': 'refresh-token'
};

// 사용자가 없음 모킹
(pool.query as jest.Mock).mockResolvedValueOnce({
rows: []
});

// 미들웨어 실행
await authMiddleware.verify(
mockRequest as Request,
mockResponse as Response,
nextFunction
);

// 검증
expect(nextFunction).toHaveBeenCalledTimes(1);
expect(mockRequest.user).toBeUndefined();
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

미들웨어 로직상 DB에 사용자가 없으면 DB Error를 던지고 next를 호출하는데, 그 부분이 포함되면 더 좋을 것 같네요~!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 포인트 지적 너무 감사해요! 테스트 구성을 아예 바꿀게요! 생각해보니까 에러 여부 자체 테스팅 보다

  1. next callback 호출하는지 2) 동시에 callback 인자에 error instance 가 인자로 주어지는지 로 모두 맞출게요!

it('쿠키 대신 요청 본문에서 토큰을 추출해야 한다', async () => {
// 요청 본문에 토큰 설정
mockRequest.body = {
accessToken: validToken,
refreshToken: 'refresh-token'
};

// 사용자 정보 mock
const mockUser = {
id: 1,
username: 'testuser',
email: 'test@example.com',
velog_uuid: 'c7507240-093b-11ea-9aae-a58a86bb0520'
};

// DB 쿼리 결과 모킹
(pool.query as jest.Mock).mockResolvedValueOnce({
rows: [mockUser]
});

// 미들웨어 실행
await authMiddleware.verify(
mockRequest as Request,
mockResponse as Response,
nextFunction
);

// 검증
expect(nextFunction).toHaveBeenCalledTimes(1);
expect(nextFunction).not.toHaveBeenCalledWith(expect.any(Error));
expect(mockRequest.user).toEqual(mockUser);
});

it('쿠키와 요청 본문 대신 헤더에서 토큰을 추출해야 한다', async () => {
// 헤더에 토큰 설정
mockRequest.headers = {
'access_token': validToken,
'refresh_token': 'refresh-token'
};

// 사용자 정보 mock
const mockUser = {
id: 1,
username: 'testuser',
email: 'test@example.com',
velog_uuid: 'c7507240-093b-11ea-9aae-a58a86bb0520'
};

// DB 쿼리 결과 모킹
(pool.query as jest.Mock).mockResolvedValueOnce({
rows: [mockUser]
});

// 미들웨어 실행
await authMiddleware.verify(
mockRequest as Request,
mockResponse as Response,
nextFunction
);

// 검증
expect(nextFunction).toHaveBeenCalledTimes(1);
expect(nextFunction).not.toHaveBeenCalledWith(expect.any(Error));
expect(mockRequest.user).toEqual(mockUser);
});
});
});
155 changes: 155 additions & 0 deletions src/modules/__test__/velog.api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import axios from 'axios';
import { fetchVelogApi } from '@/modules/velog/velog.api';
import { VELOG_API_URL, VELOG_QUERIES } from '@/modules/velog/velog.constans';

// axios 모킹
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

// logger 모킹 (콘솔 출력 방지)
jest.mock('@/configs/logger.config', () => ({
error: jest.fn(),
info: jest.fn(),
}));

describe('Velog API', () => {
const mockAccessToken = 'test-access-token';
const mockRefreshToken = 'test-refresh-token';

beforeEach(() => {
jest.clearAllMocks();
});

describe('fetchVelogApi', () => {
it('유효한 토큰으로 사용자 정보를 성공적으로 가져와야 한다', async () => {
// API 응답 모킹
const mockResponse = {
data: {
data: {
currentUser: {
id: 'user-uuid',
username: 'testuser',
email: 'test@example.com',
profile: {
thumbnail: 'https://example.com/avatar.png'
}
}
}
}
};

mockedAxios.post.mockResolvedValueOnce(mockResponse);

const result = await fetchVelogApi(mockAccessToken, mockRefreshToken);

// 결과 검증
expect(result).toEqual({
id: 'user-uuid',
username: 'testuser',
email: 'test@example.com',
profile: {
thumbnail: 'https://example.com/avatar.png'
}
});

// axios 호출 검증
expect(mockedAxios.post).toHaveBeenCalledTimes(1);
expect(mockedAxios.post).toHaveBeenCalledWith(
VELOG_API_URL,
{ VELOG_QUERIES, variables: {} },
{
headers: {
authority: 'v3.velog.io',
origin: 'https://velog.io',
'content-type': 'application/json',
cookie: `access_token=${mockAccessToken}; refresh_token=${mockRefreshToken}`,
},
}
);
});

it('이메일이 없는 사용자 정보도 성공적으로 처리해야 한다', async () => {
// 이메일이 없는 API 응답 모킹
const mockResponse = {
data: {
data: {
currentUser: {
id: 'user-uuid',
username: 'testuser',
// email 필드 없음
profile: {
thumbnail: 'https://example.com/avatar.png'
}
}
}
}
};

mockedAxios.post.mockResolvedValueOnce(mockResponse);

const result = await fetchVelogApi(mockAccessToken, mockRefreshToken);

// 결과 검증 - email이 null로 설정되었는지 확인
expect(result).toEqual({
id: 'user-uuid',
username: 'testuser',
email: null,
profile: {
thumbnail: 'https://example.com/avatar.png'
}
});
});

it('API 응답에 오류가 있으면 InvalidTokenError를 던져야 한다', async () => {
// 오류가 포함된 API 응답 모킹
const mockResponse = {
data: {
errors: [{ message: '인증 실패' }],
data: { currentUser: null }
}
};

mockedAxios.post.mockResolvedValueOnce(mockResponse);

// 함수 호출 시 예외 발생 검증
await expect(fetchVelogApi(mockAccessToken, mockRefreshToken))
.rejects.toThrow(expect.objectContaining({
name: 'InvalidTokenError',
message: 'Velog API 인증에 실패했습니다.'
}));
});

it('currentUser가 null이면 InvalidTokenError를 던져야 한다', async () => {
// currentUser가 null인 API 응답 모킹
const mockResponse = {
data: {
data: {
currentUser: null
}
}
};

mockedAxios.post.mockResolvedValueOnce(mockResponse);

// 함수 호출 시 예외 발생 검증
await expect(fetchVelogApi(mockAccessToken, mockRefreshToken))
.rejects.toThrow(expect.objectContaining({
name: 'InvalidTokenError',
message: 'Velog 사용자 정보를 가져오지 못했습니다.'
}));

});

it('API 호출 자체가 실패하면 InvalidTokenError를 던져야 한다', async () => {
// axios 호출 실패 모킹
mockedAxios.post.mockRejectedValueOnce(new Error('네트워크 오류'));

// 함수 호출 시 예외 발생 검증
await expect(fetchVelogApi(mockAccessToken, mockRefreshToken))
.rejects.toThrow(expect.objectContaining({
name: 'InvalidTokenError',
message: 'Velog API 인증에 실패했습니다.'
}));
});
});
});
Loading