Skip to content

Commit 8303b53

Browse files
authored
Merge pull request #201 from TEAM-ISAID/test/#191-challenge-test
Test: Challenge 리펙토링 및 테스트 작성 (#191)
2 parents 9629e98 + 66279d2 commit 8303b53

12 files changed

Lines changed: 1237 additions & 250 deletions

File tree

__mocks__/prisma-factory.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ type MockFn<T> = {
22
[P in keyof T]: jest.Mock;
33
};
44

5+
/**
6+
* 기본 Prisma Mock 생성
7+
*/
58
export const createPrismaMock = (overrides = {}) => {
69
const baseMock = {
710
user: {
@@ -16,6 +19,9 @@ export const createPrismaMock = (overrides = {}) => {
1619
};
1720
};
1821

22+
/**
23+
* ETF 테스트용 Prisma Mock 생성
24+
*/
1925
export const createEtfTestPrismaMock = (overrides = {}) => {
2026
const baseMock = {
2127
user: {
@@ -52,3 +58,91 @@ export const createEtfTestPrismaMock = (overrides = {}) => {
5258
...overrides,
5359
};
5460
};
61+
62+
/**
63+
* 챌린지 테스트용 Prisma Mock 생성
64+
*/
65+
export const createChallengePrismaMock = (overrides = {}) => {
66+
const baseMock = {
67+
user: {
68+
findUnique: jest.fn(),
69+
findFirst: jest.fn(),
70+
create: jest.fn(),
71+
update: jest.fn(),
72+
delete: jest.fn(),
73+
},
74+
challenge: {
75+
findUnique: jest.fn(),
76+
findMany: jest.fn(),
77+
create: jest.fn(),
78+
update: jest.fn(),
79+
delete: jest.fn(),
80+
},
81+
userChallengeClaim: {
82+
findFirst: jest.fn(),
83+
findMany: jest.fn(),
84+
create: jest.fn(),
85+
update: jest.fn(),
86+
delete: jest.fn(),
87+
deleteMany: jest.fn(),
88+
},
89+
userChallengeProgress: {
90+
findFirst: jest.fn(),
91+
findMany: jest.fn(),
92+
create: jest.fn(),
93+
update: jest.fn(),
94+
updateMany: jest.fn(),
95+
delete: jest.fn(),
96+
deleteMany: jest.fn(),
97+
upsert: jest.fn(),
98+
},
99+
etf: {
100+
findUnique: jest.fn(),
101+
findMany: jest.fn(),
102+
create: jest.fn(),
103+
update: jest.fn(),
104+
},
105+
etfDailyTrading: {
106+
findFirst: jest.fn(),
107+
findMany: jest.fn(),
108+
create: jest.fn(),
109+
update: jest.fn(),
110+
},
111+
eTFTransaction: {
112+
findUnique: jest.fn(),
113+
findMany: jest.fn(),
114+
create: jest.fn(),
115+
update: jest.fn(),
116+
delete: jest.fn(),
117+
},
118+
eTFHolding: {
119+
findUnique: jest.fn(),
120+
findMany: jest.fn(),
121+
create: jest.fn(),
122+
update: jest.fn(),
123+
upsert: jest.fn(),
124+
delete: jest.fn(),
125+
},
126+
isaAccount: {
127+
findUnique: jest.fn(),
128+
findMany: jest.fn(),
129+
create: jest.fn(),
130+
update: jest.fn(),
131+
},
132+
$transaction: jest.fn(),
133+
};
134+
135+
return {
136+
...baseMock,
137+
...overrides,
138+
};
139+
};
140+
141+
// 타입 정의
142+
export type BasePrismaMock = ReturnType<typeof createPrismaMock>;
143+
export type EtfTestPrismaMock = ReturnType<typeof createEtfTestPrismaMock>;
144+
export type ChallengePrismaMock = ReturnType<typeof createChallengePrismaMock>;
145+
export type PrismaMockInstance =
146+
| BasePrismaMock
147+
| EtfTestPrismaMock
148+
| ChallengePrismaMock;

__mocks__/prisma.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { createEtfTestPrismaMock, createPrismaMock } from './prisma-factory';
1+
import {
2+
createChallengePrismaMock,
3+
createEtfTestPrismaMock,
4+
createPrismaMock,
5+
} from './prisma-factory';
26

37
// Jest에서 사용할 수 있도록 전역 mock 인스턴스 생성
48
let mockPrismaInstance = createPrismaMock();
@@ -17,6 +21,11 @@ export const resetWithEtfTestPrismaMock = () => {
1721
return mockPrismaInstance;
1822
};
1923

24+
export const resetWithChallengePrismaMock = () => {
25+
mockPrismaInstance = createChallengePrismaMock();
26+
return mockPrismaInstance;
27+
};
28+
2029
// 커스텀 overrides로 재설정
2130
export const resetWithCustomMock = (overrides = {}) => {
2231
mockPrismaInstance = createPrismaMock(overrides);
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
import { getServerSession } from 'next-auth';
5+
import { resetWithChallengePrismaMock } from '@/__mocks__/prisma';
6+
import { POST } from '@/app/api/challenge/claim/route';
7+
import { claimChallengeReward } from '@/services/challenge/challenge-claim';
8+
import { canClaimChallenge } from '@/services/challenge/challenge-status';
9+
10+
// Mock dependencies
11+
jest.mock('next-auth');
12+
jest.mock('@/services/challenge/challenge-status');
13+
jest.mock('@/services/challenge/challenge-claim');
14+
jest.mock('@/lib/prisma', () => {
15+
const { getPrismaMock } = require('@/__mocks__/prisma');
16+
return {
17+
get prisma() {
18+
return getPrismaMock();
19+
},
20+
};
21+
});
22+
23+
const mockGetServerSession = getServerSession as jest.MockedFunction<
24+
typeof getServerSession
25+
>;
26+
const mockCanClaimChallenge = canClaimChallenge as jest.MockedFunction<
27+
typeof canClaimChallenge
28+
>;
29+
const mockClaimChallengeReward = claimChallengeReward as jest.MockedFunction<
30+
typeof claimChallengeReward
31+
>;
32+
33+
describe('/api/challenge/claim', () => {
34+
let mockPrisma: any;
35+
36+
beforeEach(() => {
37+
jest.clearAllMocks();
38+
mockPrisma = resetWithChallengePrismaMock();
39+
});
40+
41+
describe('POST', () => {
42+
it('should return 401 when not authenticated', async () => {
43+
mockGetServerSession.mockResolvedValue(null);
44+
45+
const request = new Request('http://localhost/api/challenge/claim', {
46+
method: 'POST',
47+
body: JSON.stringify({ challengeId: '1' }),
48+
});
49+
50+
const response = await POST(request);
51+
const data = await response.json();
52+
53+
expect(response.status).toBe(401);
54+
expect(data.message).toBe('Unauthorized');
55+
});
56+
57+
it('should return 400 when challengeId is missing', async () => {
58+
mockGetServerSession.mockResolvedValue({
59+
user: { id: '1' },
60+
});
61+
62+
const request = new Request('http://localhost/api/challenge/claim', {
63+
method: 'POST',
64+
body: JSON.stringify({}),
65+
});
66+
67+
const response = await POST(request);
68+
const data = await response.json();
69+
70+
expect(response.status).toBe(400);
71+
expect(data.message).toBe('Challenge ID is required');
72+
});
73+
74+
it('should successfully claim challenge reward', async () => {
75+
mockGetServerSession.mockResolvedValue({
76+
user: { id: '1' },
77+
});
78+
79+
mockPrisma.$transaction.mockImplementation(async (callback: any) => {
80+
const mockTx = mockPrisma;
81+
return await callback(mockTx);
82+
});
83+
84+
mockCanClaimChallenge.mockResolvedValue({ canClaim: true });
85+
mockClaimChallengeReward.mockResolvedValue({
86+
success: true,
87+
message: 'Reward claimed successfully',
88+
transactionId: BigInt(123),
89+
});
90+
91+
const request = new Request('http://localhost/api/challenge/claim', {
92+
method: 'POST',
93+
body: JSON.stringify({ challengeId: '1' }),
94+
});
95+
96+
const response = await POST(request);
97+
const data = await response.json();
98+
99+
expect(response.status).toBe(200);
100+
expect(data.message).toBe('Reward claimed successfully');
101+
expect(data.transactionId).toBe('123');
102+
103+
expect(mockCanClaimChallenge).toHaveBeenCalledWith(
104+
BigInt(1),
105+
BigInt(1),
106+
mockPrisma
107+
);
108+
expect(mockClaimChallengeReward).toHaveBeenCalledWith(
109+
{ challengeId: BigInt(1), userId: BigInt(1) },
110+
mockPrisma
111+
);
112+
});
113+
114+
it('should return 500 when cannot claim challenge', async () => {
115+
mockGetServerSession.mockResolvedValue({
116+
user: { id: '1' },
117+
});
118+
119+
mockPrisma.$transaction.mockImplementation(async (callback: any) => {
120+
const mockTx = mockPrisma;
121+
return await callback(mockTx);
122+
});
123+
124+
mockCanClaimChallenge.mockResolvedValue({
125+
canClaim: false,
126+
reason: 'Already claimed',
127+
});
128+
129+
const request = new Request('http://localhost/api/challenge/claim', {
130+
method: 'POST',
131+
body: JSON.stringify({ challengeId: '1' }),
132+
});
133+
134+
const response = await POST(request);
135+
const data = await response.json();
136+
137+
expect(response.status).toBe(500);
138+
expect(data.message).toBe('Already claimed');
139+
});
140+
141+
it('should return 400 when claim service returns failure', async () => {
142+
mockGetServerSession.mockResolvedValue({
143+
user: { id: '1' },
144+
});
145+
146+
mockPrisma.$transaction.mockImplementation(async (callback: any) => {
147+
const mockTx = mockPrisma;
148+
return await callback(mockTx);
149+
});
150+
151+
mockCanClaimChallenge.mockResolvedValue({ canClaim: true });
152+
mockClaimChallengeReward.mockResolvedValue({
153+
success: false,
154+
message: 'ISA account not found',
155+
});
156+
157+
const request = new Request('http://localhost/api/challenge/claim', {
158+
method: 'POST',
159+
body: JSON.stringify({ challengeId: '1' }),
160+
});
161+
162+
const response = await POST(request);
163+
const data = await response.json();
164+
165+
expect(response.status).toBe(400);
166+
expect(data.message).toBe('ISA account not found');
167+
});
168+
169+
it('should handle unexpected errors', async () => {
170+
mockGetServerSession.mockResolvedValue({
171+
user: { id: '1' },
172+
});
173+
174+
mockPrisma.$transaction.mockRejectedValue(new Error('Database error'));
175+
176+
const request = new Request('http://localhost/api/challenge/claim', {
177+
method: 'POST',
178+
body: JSON.stringify({ challengeId: '1' }),
179+
});
180+
181+
const response = await POST(request);
182+
const data = await response.json();
183+
184+
expect(response.status).toBe(500);
185+
expect(data.message).toBe('Database error');
186+
});
187+
});
188+
});

__tests__/api/etf-portfolio-api.test.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

0 commit comments

Comments
 (0)