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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
## Quick Start

README에 아래 코드를 추가하고 `사용자명`을 본인의 Dreamhack 닉네임으로 변경하세요.
닉네임에 `&`, `#`, `+`, 공백 같은 특수문자가 있다면 [메인 페이지](https://dreamhack-readme-stats.vercel.app)에서 코드를 생성하세요. URL에 맞게 자동으로 인코딩됩니다.

### Wargame Stats

Expand All @@ -34,9 +35,9 @@ README에 아래 코드를 추가하고 `사용자명`을 본인의 Dreamhack
![Dreamhack Categories](https://dreamhack-readme-stats.vercel.app/api/most-solved?username=사용자명)
```

> 💡 클릭 시 Dreamhack 프로필로 이동하게 하려면 HTML 사용:
> 💡 클릭 시 Dreamhack 프로필로 이동하게 하려면 HTML 사용 (`사용자ID`는 프로필 주소 `https://dreamhack.io/users/12345`의 숫자이며, 메인 페이지에서 미리보기하면 자동으로 채워집니다):
> ```html
> <a href="https://dreamhack.io/users/사용자명">
> <a href="https://dreamhack.io/users/사용자ID">
> <img src="https://dreamhack-readme-stats.vercel.app/api/stats?username=사용자명" />
> </a>
> ```
Expand Down
52 changes: 25 additions & 27 deletions src/__tests__/pages/api/most-solved.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,81 +133,79 @@ describe('most-solved API 엔드포인트 테스트', () => {
expect(generateSvgArg.categories).toHaveLength(0);
});

it('사용자 이름이 없을 때 400 에러를 반환해야 함', async () => {
// HTTP 요청 모킹 (사용자 이름 없음)
it('사용자 이름이 없을 때 안내 카드를 반환해야 함', async () => {
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' });
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()).toContain('Most Solved Categories');
expect(res._getData()).toContain('Missing username');
});

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

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

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

// 응답 검증
expect(res._getStatusCode()).toBe(400);
expect(JSON.parse(res._getData())).toEqual({ error: 'User not found' });
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()).toContain('User not found');
expect(res._getData()).toContain('nonexistentuser');
// 다크 테마 배경색 적용
expect(res._getData()).toContain('#0d1117');
});

it('사용자 데이터를 가져올 수 없을 때 400 에러를 반환해야 함', async () => {
// 모킹된 함수 구현
it('사용자 데이터를 가져올 수 없을 때 오류 카드를 반환해야 함', 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(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()).toContain('Failed to load stats');
expect(dreamhackUtils.getUserData).toHaveBeenCalledWith(123, username);
});

it('예외 발생 시 500 에러를 반환해야 함', async () => {
// 모킹된 함수 구현 (예외 발생)
it('예외 발생 시 오류 카드를 반환해야 함', 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' });
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()).toContain('Failed to load stats');
});
});
78 changes: 44 additions & 34 deletions src/__tests__/pages/api/stats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,103 +123,113 @@ describe('stats API 엔드포인트 테스트', () => {
);
});

it('사용자 이름이 없을 때 400 에러를 반환해야 함', async () => {
// HTTP 요청 모킹 (사용자 이름 없음)
it('사용자 이름이 없을 때 안내 카드를 반환해야 함', async () => {
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' });
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()).toContain('Missing username');
});

it('사용자를 찾을 수 없을 때 400 에러를 반환해야 함', async () => {
// 모킹된 함수 구현 (사용자 ID가 null)
it('사용자를 찾을 수 없을 때 안내 카드를 반환해야 함', async () => {
(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' });
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()).toContain('User not found');
expect(res._getData()).toContain('nonexistentuser');
// 사용자가 없으면 마지막 랭크는 조회하지 않음
expect(dreamhackUtils.getLastRank).not.toHaveBeenCalled();
});

it('마지막 랭크를 가져올 수 없을 때 500 에러를 반환해야 함', async () => {
// 모킹된 함수 구현
it('안내 카드에 표시하는 닉네임을 이스케이프해야 함', async () => {
(dreamhackUtils.getUserId as jest.Mock).mockResolvedValueOnce(null);

const { req, res } = createMocks({
method: 'GET',
query: {
username: '<script>alert(1)</script>',
},
});

await handler(req, res);

expect(res._getData()).not.toContain('<script>');
expect(res._getData()).toContain('&lt;script&gt;');
});

it('마지막 랭크를 가져올 수 없을 때 오류 카드를 반환해야 함', async () => {
(dreamhackUtils.getUserId as jest.Mock).mockResolvedValueOnce(123);
(dreamhackUtils.getLastRank as jest.Mock).mockResolvedValueOnce(null);

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

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

// 응답 검증
expect(res._getStatusCode()).toBe(500);
expect(JSON.parse(res._getData())).toEqual({ error: 'Failed to fetch last rank' });
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()).toContain('Failed to load stats');
});

it('사용자 데이터를 가져올 수 없을 때 400 에러를 반환해야 함', async () => {
// 모킹된 함수 구현
it('사용자 데이터를 가져올 수 없을 때 오류 카드를 반환해야 함', async () => {
const username = 'testuser';
(dreamhackUtils.getUserId as jest.Mock).mockResolvedValueOnce(123);
(dreamhackUtils.getLastRank as jest.Mock).mockResolvedValueOnce(1000);
(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(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()).toContain('Failed to load stats');
expect(dreamhackUtils.getUserData).toHaveBeenCalledWith(123, username);
});

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

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

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

// 응답 검증
expect(res._getStatusCode()).toBe(500);
expect(JSON.parse(res._getData())).toEqual({ error: 'Internal Server Error' });
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()).toContain('Failed to load stats');
});
});
52 changes: 52 additions & 0 deletions src/__tests__/pages/api/user-id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { createMocks } from 'node-mocks-http';
import handler from '../../../pages/api/user-id';
import * as dreamhackUtils from '../../../utils/dreamhack';

jest.mock('../../../utils/dreamhack', () => ({
getUserId: jest.fn(),
}));

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

it('사용자 ID와 프로필 주소를 반환해야 함', async () => {
(dreamhackUtils.getUserId as jest.Mock).mockResolvedValueOnce(20691);

const { req, res } = createMocks({ method: 'GET', query: { username: 'weakness' } });
await handler(req, res);

expect(res._getStatusCode()).toBe(200);
expect(JSON.parse(res._getData())).toEqual({ id: 20691, profileUrl: 'https://dreamhack.io/users/20691' });
expect(dreamhackUtils.getUserId).toHaveBeenCalledWith('weakness');
});

it('사용자 이름이 없으면 400을 반환해야 함', async () => {
const { req, res } = createMocks({ method: 'GET', query: {} });
await handler(req, res);

expect(res._getStatusCode()).toBe(400);
expect(dreamhackUtils.getUserId).not.toHaveBeenCalled();
});

it('사용자를 찾을 수 없으면 404를 반환해야 함', async () => {
(dreamhackUtils.getUserId as jest.Mock).mockResolvedValueOnce(null);

const { req, res } = createMocks({ method: 'GET', query: { username: 'miroo' } });
await handler(req, res);

expect(res._getStatusCode()).toBe(404);
expect(JSON.parse(res._getData())).toEqual({ error: 'User not found' });
});

it('예외 발생 시 500을 반환해야 함', async () => {
(dreamhackUtils.getUserId as jest.Mock).mockRejectedValueOnce(new Error('테스트 에러'));

const { req, res } = createMocks({ method: 'GET', query: { username: 'weakness' } });
await handler(req, res);

expect(res._getStatusCode()).toBe(500);
});
});
Loading
Loading