Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@ HTML 코드:
</a>
```

## 카테고리 차트

Dreamhack의 워게임 카테고리별 점수를 원형 차트로 표시합니다. 각 카테고리별 점수와 랭킹을 확인할 수 있습니다.

### 사용 방법

#### Markdown
```markdown
![Dreamhack Category Chart](https://dreamhack-readme-stats.vercel.app/api/most-solved?username=사용자명)
```

#### HTML
```html
<a href="https://dreamhack.io/users/사용자명" target="_blank" rel="noopener noreferrer">
<img src="https://dreamhack-readme-stats.vercel.app/api/most-solved?username=사용자명" alt="Dreamhack Category Chart" />
</a>
```

### 예시

![Dreamhack Category Chart](https://dreamhack-readme-stats.vercel.app/api/most-solved?username=weakness)

## 기술 스택

- Next.js
Expand Down
213 changes: 213 additions & 0 deletions src/__tests__/pages/api/most-solved.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { createMocks } from 'node-mocks-http';
import handler from '../../../pages/api/most-solved';
import * as dreamhackUtils from '../../../utils/dreamhack';
import * as generateCategorySvgUtils from '../../../utils/generateCategorySvg';
import { TuserData } from '../../../types';

// dreamhack 유틸리티 함수 모킹
jest.mock('../../../utils/dreamhack', () => ({
getUserId: jest.fn(),
getUserData: jest.fn(),
}));

// generateCategorySvg 유틸리티 함수 모킹
jest.mock('../../../utils/generateCategorySvg', () => ({
generateCategorySvg: jest.fn(),
}));

describe('most-solved API 엔드포인트 테스트', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(console, 'time').mockImplementation();
jest.spyOn(console, 'timeEnd').mockImplementation();
jest.spyOn(console, 'error').mockImplementation();
});

it('유효한 사용자 이름으로 카테고리 SVG를 반환해야 함', async () => {
// 모킹된 데이터 설정
const mockUserId = 20691;
const mockUserData: TuserData = {
nickname: 'weakness',
contributions: { level: 1, rank: 100 },
exp: 1000,
total_wargame: 50,
wargame: {
solved: 50,
rank: 200,
score: 5000,
category: {
web: { score: 2000, rank: 100 },
pwnable: { score: 1500, rank: 150 },
reversing: { score: 1000, rank: 200 },
crypto: { score: 500, rank: 250 }
}
},
ctf: { rank: 300, tier: 'Gold', rating: 2000 },
profile_image: 'image.jpg'
};
const mockSvg = '<svg>Mock Category SVG</svg>';

// 모킹된 함수 구현
(dreamhackUtils.getUserId as jest.Mock).mockResolvedValueOnce(mockUserId);
(dreamhackUtils.getUserData as jest.Mock).mockResolvedValueOnce(mockUserData);
(generateCategorySvgUtils.generateCategorySvg as jest.Mock).mockReturnValueOnce(mockSvg);

// HTTP 요청 모킹
const { req, res } = createMocks({
method: 'GET',
query: {
username: 'weakness',
},
});

// API 핸들러 호출
await handler(req, res);

// 응답 검증
expect(res._getStatusCode()).toBe(200);
expect(res._getHeaders()).toHaveProperty('content-type', 'image/svg+xml');
expect(res._getHeaders()).toHaveProperty('cache-control', 'no-cache, no-store, must-revalidate');
expect(res._getData()).toBe(mockSvg);

// 모킹된 함수 호출 검증
expect(dreamhackUtils.getUserId).toHaveBeenCalledWith('weakness');
expect(dreamhackUtils.getUserData).toHaveBeenCalledWith(mockUserId, 'weakness');

// generateCategorySvg 호출 인자 검증
const generateSvgArg = (generateCategorySvgUtils.generateCategorySvg as jest.Mock).mock.calls[0][0];
expect(generateSvgArg).toHaveProperty('nickname', 'weakness');
expect(generateSvgArg).toHaveProperty('total_score', 5000);
expect(generateSvgArg.categories).toHaveLength(4);

// 카테고리가 점수 순으로 정렬되었는지 확인
expect(generateSvgArg.categories[0].name).toBe('web');
expect(generateSvgArg.categories[0].score).toBe(2000);
expect(generateSvgArg.categories[1].name).toBe('pwnable');
expect(generateSvgArg.categories[2].name).toBe('reversing');
expect(generateSvgArg.categories[3].name).toBe('crypto');
});

it('카테고리가 없는 사용자에 대해 빈 카테고리 SVG를 반환해야 함', async () => {
// 모킹된 데이터 설정 (category 없음)
const mockUserId = 20691;
const mockUserData: TuserData = {
nickname: 'weakness',
contributions: { level: 1, rank: 500 },
exp: 500,
total_wargame: 10,
wargame: {
solved: 10,
rank: 500,
score: 1000
// category 필드 없음
},
ctf: { rank: 600, tier: 'Silver', rating: 1000 },
profile_image: 'image.jpg'
};
const mockSvg = '<svg>Empty Category SVG</svg>';

// 모킹된 함수 구현
(dreamhackUtils.getUserId as jest.Mock).mockResolvedValueOnce(mockUserId);
(dreamhackUtils.getUserData as jest.Mock).mockResolvedValueOnce(mockUserData);
(generateCategorySvgUtils.generateCategorySvg as jest.Mock).mockReturnValueOnce(mockSvg);

// HTTP 요청 모킹
const { req, res } = createMocks({
method: 'GET',
query: {
username: 'weakness',
},
});

// API 핸들러 호출
await handler(req, res);

// 응답 검증
expect(res._getStatusCode()).toBe(200);
expect(res._getData()).toBe(mockSvg);

// generateCategorySvg 호출 인자 검증
const generateSvgArg = (generateCategorySvgUtils.generateCategorySvg as jest.Mock).mock.calls[0][0];
expect(generateSvgArg).toHaveProperty('nickname', 'weakness');
expect(generateSvgArg).toHaveProperty('total_score', 1000);
expect(generateSvgArg.categories).toHaveLength(0);
});

it('사용자 이름이 없을 때 400 에러를 반환해야 함', async () => {
// HTTP 요청 모킹 (사용자 이름 없음)
const { req, res } = createMocks({
method: 'GET',
query: {},
});

// API 핸들러 호출
await handler(req, res);

// 응답 검증
expect(res._getStatusCode()).toBe(400);
expect(JSON.parse(res._getData())).toEqual({ error: 'Username is required' });
});

it('사용자를 찾을 수 없을 때 400 에러를 반환해야 함', async () => {
// 모킹된 함수 구현 (사용자 ID가 null)
(dreamhackUtils.getUserId as jest.Mock).mockResolvedValueOnce(null);

// HTTP 요청 모킹
const { req, res } = createMocks({
method: 'GET',
query: {
username: 'nonexistentuser',
},
});

// API 핸들러 호출
await handler(req, res);

// 응답 검증
expect(res._getStatusCode()).toBe(400);
expect(JSON.parse(res._getData())).toEqual({ error: 'User not found' });
});

it('사용자 데이터를 가져올 수 없을 때 400 에러를 반환해야 함', async () => {
// 모킹된 함수 구현
const username = 'weakness';
(dreamhackUtils.getUserId as jest.Mock).mockResolvedValueOnce(123);
(dreamhackUtils.getUserData as jest.Mock).mockResolvedValueOnce(null);

// HTTP 요청 모킹
const { req, res } = createMocks({
method: 'GET',
query: {
username,
},
});

// API 핸들러 호출
await handler(req, res);

// 응답 검증
expect(res._getStatusCode()).toBe(400);
expect(JSON.parse(res._getData())).toEqual({ error: 'User information cannot be read.' });
expect(dreamhackUtils.getUserData).toHaveBeenCalledWith(123, username);
});

it('예외 발생 시 500 에러를 반환해야 함', async () => {
// 모킹된 함수 구현 (예외 발생)
(dreamhackUtils.getUserId as jest.Mock).mockRejectedValueOnce(new Error('테스트 에러'));

// HTTP 요청 모킹
const { req, res } = createMocks({
method: 'GET',
query: {
username: 'weakness',
},
});

// API 핸들러 호출
await handler(req, res);

// 응답 검증
expect(res._getStatusCode()).toBe(500);
expect(JSON.parse(res._getData())).toEqual({ error: 'Internal Server Error' });
});
});
12 changes: 6 additions & 6 deletions src/__tests__/pages/api/stats.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createMocks } from 'node-mocks-http';
import handler from '../../../pages/api/stats';
import * as dreamhackUtils from '../../../utils/dreamhack';
import * as generateSvgUtils from '../../../utils/generateSvg';
import * as generateSvgUtils from '../../../utils/generateStatsSvg';
import { TuserData } from '../../../types';

// dreamhack 유틸리티 함수 모킹
Expand All @@ -12,9 +12,9 @@ jest.mock('../../../utils/dreamhack', () => ({
calculateTopPercentage: jest.fn(),
}));

// generateSvg 유틸리티 함수 모킹
jest.mock('../../../utils/generateSvg', () => ({
generateSvg: jest.fn(),
// generateStatsSvg 유틸리티 함수 모킹
jest.mock('../../../utils/generateStatsSvg', () => ({
generateStatsSvg: jest.fn(),
}));

describe('stats API 엔드포인트 테스트', () => {
Expand Down Expand Up @@ -45,7 +45,7 @@ describe('stats API 엔드포인트 테스트', () => {
(dreamhackUtils.getLastRank as jest.Mock).mockResolvedValueOnce(mockLastRank);
(dreamhackUtils.getUserData as jest.Mock).mockResolvedValueOnce(mockUserData);
(dreamhackUtils.calculateTopPercentage as jest.Mock).mockReturnValueOnce('20.00');
(generateSvgUtils.generateSvg as jest.Mock).mockReturnValueOnce(mockSvg);
(generateSvgUtils.generateStatsSvg as jest.Mock).mockReturnValueOnce(mockSvg);

// HTTP 요청 모킹
const { req, res } = createMocks({
Expand All @@ -69,7 +69,7 @@ describe('stats API 엔드포인트 테스트', () => {
expect(dreamhackUtils.getLastRank).toHaveBeenCalled();
expect(dreamhackUtils.getUserData).toHaveBeenCalledWith(mockUserId, 'weakness');
expect(dreamhackUtils.calculateTopPercentage).toHaveBeenCalledWith(mockUserData.wargame.rank, mockLastRank);
expect(generateSvgUtils.generateSvg).toHaveBeenCalledWith({
expect(generateSvgUtils.generateStatsSvg).toHaveBeenCalledWith({
nickname: mockUserData.nickname,
wargame_solved: mockUserData.total_wargame,
wargame_rank: `${mockUserData.wargame.rank}/${mockLastRank}`,
Expand Down
16 changes: 9 additions & 7 deletions src/__tests__/utils/dreamhack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,13 @@ describe('dreamhack 유틸리티 함수 테스트', () => {
});

// getUserId 함수 호출 결과 모킹
const mockNewUserId = 12345;
const invalidUserId = 9999999; // 존재하지 않는 ID
const validUserId = 20691; // 실제 존재하는 ID

(redisUtils.getUserIdFromCache as jest.Mock).mockResolvedValueOnce(null);
const mockResponse: TUserRankingResponse = {
results: [
{ id: mockNewUserId, nickname: 'weakness' }
{ id: validUserId, nickname: 'weakness' }
]
};
(fetch as jest.Mock).mockResolvedValueOnce({
Expand All @@ -171,11 +173,11 @@ describe('dreamhack 유틸리티 함수 테스트', () => {
json: jest.fn().mockResolvedValueOnce(mockUserData),
});

const result = await getUserData(20691, 'weakness');
const result = await getUserData(invalidUserId, 'weakness');

// 첫 번째 API 호출 확인
// 첫 번째 API 호출 확인 (존재하지 않는 ID)
expect(fetch).toHaveBeenNthCalledWith(1,
'https://dreamhack.io/api/v1/user/profile/20691/'
`https://dreamhack.io/api/v1/user/profile/${invalidUserId}/`
);

// 새로운 사용자 ID 조회 확인
Expand All @@ -184,9 +186,9 @@ describe('dreamhack 유틸리티 함수 테스트', () => {
'https://dreamhack.io/api/v1/ranking/wargame/?filter=global&limit=100&offset=0&search=weakness&scope=all&name=&category='
);

// 새로운 사용자 ID로 다시 API 호출 확인
// 새로운 사용자 ID로 다시 API 호출 확인 (유효한 ID)
expect(fetch).toHaveBeenNthCalledWith(3,
'https://dreamhack.io/api/v1/user/profile/12345/'
`https://dreamhack.io/api/v1/user/profile/${validUserId}/`
);

expect(result).toEqual(mockUserData);
Expand Down
Loading