Skip to content

Commit 2973419

Browse files
committed
Refactoring. Closes #88
1 parent cb75132 commit 2973419

File tree

4 files changed

+464
-450
lines changed

4 files changed

+464
-450
lines changed
Lines changed: 152 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,86 @@
11
import { describe, it, expect, beforeEach, vi, beforeAll } from 'vitest'
22
import { NextRequest } from 'next/server'
33

4-
const mockExecuteTakeFirst: any = vi.fn()
5-
const mockExecute: any = vi.fn()
6-
const mockUpdateTable: any = vi.fn(() => ({
7-
set: mockSet,
4+
// Mocking guide-utils functions
5+
const mockGetCourseIdByPrefix = vi.fn()
6+
const mockGetGuideIdBySuffix = vi.fn()
7+
const mockGetActividadpfId = vi.fn()
8+
9+
vi.mock('@/lib/guide-utils', () => ({
10+
getCourseIdByPrefix: mockGetCourseIdByPrefix,
11+
getGuideIdBySuffix: mockGetGuideIdBySuffix,
12+
getActividadpfId: mockGetActividadpfId,
13+
}))
14+
15+
// Mock other dependencies
16+
const mockUpdateUserAndCoursePoints = vi.fn()
17+
const mockRecordEvent = vi.fn()
18+
19+
vi.mock('@/lib/scores', () => ({
20+
updateUserAndCoursePoints: mockUpdateUserAndCoursePoints,
21+
}))
22+
23+
vi.mock('@/lib/metrics-server', () => ({
24+
recordEvent: mockRecordEvent,
25+
}))
26+
27+
// Simplified Kysely mock
28+
const mockExecuteTakeFirst = vi.fn()
29+
const mockExecute = vi.fn()
30+
const mockUpdateTable = vi.fn(() => ({
31+
set: vi.fn().mockReturnThis(),
832
where: vi.fn().mockReturnThis(),
933
execute: vi.fn(),
1034
}))
11-
const mockSet: any = vi.fn().mockReturnThis()
12-
13-
const mockFn: any = {
14-
countAll: vi.fn(() => ({
15-
as: vi.fn(() => mockFn),
16-
})),
17-
sum: vi.fn(() => ({
18-
as: vi.fn(() => mockFn),
19-
})),
20-
}
35+
const mockInsertInto = vi.fn(() => ({
36+
values: vi.fn().mockReturnThis(),
37+
execute: vi.fn(),
38+
}))
2139

22-
class MockKysely {
23-
selectFrom() { return this }
24-
where() { return this }
25-
selectAll() { return this }
26-
executeTakeFirst() { return mockExecuteTakeFirst() }
27-
orderBy() { return this }
28-
limit() { return this }
29-
select() { return this }
30-
insertInto() { return this }
31-
values() { return this }
32-
returningAll() { return this }
33-
execute() { return mockExecute() }
34-
executeTakeFirstOrThrow() { return mockExecuteTakeFirst() }
35-
updateTable() { return mockUpdateTable() }
36-
fn = mockFn
37-
}
3840

39-
const mockSql = {
40-
execute: vi.fn(),
41+
class MockKysely {
42+
selectFrom = vi.fn().mockReturnThis()
43+
where = vi.fn().mockReturnThis()
44+
selectAll = vi.fn().mockReturnThis()
45+
executeTakeFirst = mockExecuteTakeFirst
46+
select = vi.fn().mockReturnThis()
47+
execute = mockExecute
48+
updateTable = mockUpdateTable
49+
insertInto = mockInsertInto
4150
}
4251

4352
vi.mock('kysely', () => ({
4453
Kysely: MockKysely,
45-
PostgresDialect: vi.fn(),
46-
sql: vi.fn(() => mockSql),
47-
}))
48-
49-
vi.mock('pg', () => ({
50-
Pool: vi.fn(),
54+
sql: vi.fn(),
5155
}))
5256

57+
vi.mock('pg', () => ({}))
5358
vi.mock('@/.config/kysely.config.ts', () => ({
5459
newKyselyPostgresql: () => new MockKysely(),
5560
}))
5661

62+
// Mock viem and crypto functions
5763
const mockGetContract = vi.fn()
58-
const mockSendTransaction = vi.fn().mockResolvedValue('0xmocktxhash')
59-
const mockWaitForTransactionReceipt = vi.fn()
60-
const mockGetStudentGuideStatus = vi.fn()
64+
const mockCallWriteFun = vi.fn()
6165

6266
vi.mock('viem', async () => {
6367
const actual = await vi.importActual('viem')
6468
return {
6569
...actual,
66-
createPublicClient: vi.fn(() => ({
67-
waitForTransactionReceipt: mockWaitForTransactionReceipt,
68-
})),
69-
createWalletClient: vi.fn(() => ({
70-
sendTransaction: mockSendTransaction,
71-
})),
7270
getContract: mockGetContract,
73-
encodeFunctionData: vi.fn(() => '0xmockEncodedData'),
71+
createPublicClient: vi.fn(),
72+
createWalletClient: vi.fn(),
7473
http: vi.fn(),
74+
privateKeyToAccount: vi.fn(() => ({})),
7575
}
7676
})
7777

78-
let POST: any, GET: any
78+
vi.mock('@/lib/crypto', () => ({
79+
callWriteFun: mockCallWriteFun,
80+
}))
81+
82+
let POST: (req: NextRequest) => Promise<Response>
83+
let GET: (req: NextRequest) => Promise<Response>
7984

8085
describe('API /api/check-crossword', () => {
8186
beforeAll(async () => {
@@ -86,118 +91,141 @@ describe('API /api/check-crossword', () => {
8691

8792
beforeEach(() => {
8893
vi.clearAllMocks()
94+
// Setup environment variables
8995
process.env.PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
9096
process.env.NEXT_PUBLIC_DEPLOYED_AT = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
97+
process.env.NEXT_PUBLIC_RPC_URL = 'http://localhost:8545'
9198

92-
mockSql.execute.mockResolvedValue({ rows: [{ id: 101, sufijoRuta: 'test-guide', proyectofinanciero_id: 1 }] })
99+
// Default successful mock resolutions
100+
mockGetCourseIdByPrefix.mockResolvedValue(1)
101+
mockGetGuideIdBySuffix.mockResolvedValue(1)
102+
mockGetActividadpfId.mockResolvedValue(101)
93103

94104
mockGetContract.mockReturnValue({
95-
address: '0xmockContractAddress',
96105
read: {
97-
vaults: vi.fn().mockResolvedValue([0n, 0n, 0n, 0n, 0n, 1n]),
106+
vaults: vi.fn().mockResolvedValue([0n, 0n, 0n, 0n, 0n, true]), // vault.exists = true
98107
studentCanSubmit: vi.fn().mockResolvedValue(true),
99-
getStudentGuideStatus: mockGetStudentGuideStatus,
108+
getStudentGuideStatus: vi.fn().mockResolvedValue([0n, true]),
100109
},
101110
write: {
102-
submitGuideResult: vi.fn().mockResolvedValue('0xmocktxhash'),
103-
},
111+
submitGuideResult: vi.fn()
112+
}
104113
})
114+
mockCallWriteFun.mockResolvedValue('0xmocktxhash')
105115
})
106116

107-
it('POST con respuestas correctas actualiza amountpaid desde el contrato', async () => {
108-
const PAID_AMOUNT = BigInt(500000000000000000) // 0.5 USDT en formato BigInt
109-
110-
mockExecuteTakeFirst
111-
.mockResolvedValueOnce({ billetera: '0x123', usuario_id: 1, token: 'TOK', answer_fib: 'TEST' })
112-
.mockResolvedValueOnce({ id: 1, profilescore: 60 })
113-
.mockResolvedValueOnce({ count: 1 })
114-
.mockResolvedValueOnce({ total_points: 0 })
115-
.mockResolvedValueOnce({ total_points: 0 })
116-
mockExecute.mockResolvedValueOnce([])
117-
mockExecuteTakeFirst.mockResolvedValueOnce({ points: 1 })
118-
mockGetStudentGuideStatus.mockResolvedValueOnce([PAID_AMOUNT, true])
119-
mockWaitForTransactionReceipt.mockResolvedValue({ status: 'success' })
120-
121-
const body = {
122-
courseId: 1, guideId: 1, lang: 'en', grid: [[{ userInput: 'T' }, { userInput: 'E' }, { userInput: 'S' }, { userInput: 'T' }]],
123-
placements: [{ row: 0, col: 0, direction: 'across' }], walletAddress: '0x123', token: 'TOK'
124-
}
125-
const req = new NextRequest('http://localhost/api/check-crossword', {
126-
method: 'POST', body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' },
127-
})
128-
117+
it('should return error if coursePrefix is invalid', async () => {
118+
mockGetCourseIdByPrefix.mockResolvedValue(null)
119+
const req = new NextRequest('http://localhost', {
120+
method: 'POST',
121+
headers: { 'Content-Type': 'application/json' },
122+
body: JSON.stringify({ coursePrefix: 'invalid', guideSuffix: 'guide1', walletAddress: '0x123', token: 'abc' }),
123+
});
129124
const res = await POST(req)
130-
const data = await res.json()
131-
132-
expect(res.status).toBe(200)
133-
expect(data.mistakesInCW).toEqual([])
134-
expect(data.scholarshipResult).toBe('0xmocktxhash')
135-
expect(mockUpdateTable).toHaveBeenCalled()
136-
expect(mockSet).toHaveBeenCalledWith({ amountpaid: PAID_AMOUNT.toString() })
137-
})
138-
139-
it('GET responde 400 indicando que espera POST', async () => {
140-
const req = new NextRequest('http://localhost:3000/api/check-crossword', { method: 'GET' })
141-
const res = await GET(req)
142125
expect(res.status).toBe(400)
143126
const data = await res.json()
144-
expect(data.error).toMatch(/Expecting POST/i)
127+
expect(data.error).toContain('Invalid course ID')
145128
})
146-
147-
it('POST sin walletAddress devuelve 400 y mensaje informativo (no califica)', async () => {
148-
const body = { courseId: 1, guideId: 1, lang: 'en', grid: [], placements: [], token: 'tok' }
149-
const req = new NextRequest('http://localhost:3000/api/check-crossword', {
150-
method: 'POST', body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' },
151-
})
129+
130+
it('should return error if guideSuffix is invalid', async () => {
131+
mockGetGuideIdBySuffix.mockResolvedValue(null)
132+
const req = new NextRequest('http://localhost', {
133+
method: 'POST',
134+
headers: { 'Content-Type': 'application/json' },
135+
body: JSON.stringify({ coursePrefix: 'valid', guideSuffix: 'invalid', walletAddress: '0x123', token: 'abc' }),
136+
});
152137
const res = await POST(req)
153138
expect(res.status).toBe(400)
154139
const data = await res.json()
155-
expect(data.error).toMatch(/will not be graded/i)
140+
expect(data.error).toContain('Invalid guide ID')
156141
})
157142

158-
it('POST con walletAddress y token que no coincide devuelve mensaje de token', async () => {
159-
mockExecuteTakeFirst.mockResolvedValueOnce({
160-
billetera: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
161-
token: 'otro',
162-
answer_fib: 'TEST'
163-
})
164-
const body = {
165-
guideId: 1, courseId: 1, lang: 'en', grid: [], placements: [],
166-
walletAddress: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', token: 'NOPE'
143+
it('should correctly process a valid crossword submission', async () => {
144+
const PAID_AMOUNT = BigInt(500000000000000000) // 0.5 USDT
145+
mockExecuteTakeFirst
146+
.mockResolvedValueOnce({ // billeteraUsuario
147+
billetera: '0x123',
148+
usuario_id: 1,
149+
token: 'TOK',
150+
answer_fib: 'TEST'
151+
})
152+
.mockResolvedValueOnce({ id: 1, profilescore: 60 }) // usuario
153+
.mockResolvedValueOnce(null) // existingGuide (returns null for new entry)
154+
155+
mockGetContract.mockReturnValue({
156+
read: {
157+
vaults: vi.fn().mockResolvedValue([0n, 0n, 0n, 0n, 0n, true]),
158+
studentCanSubmit: vi.fn().mockResolvedValue(true),
159+
getStudentGuideStatus: vi.fn().mockResolvedValue([PAID_AMOUNT, true]),
160+
},
161+
write: {
162+
submitGuideResult: vi.fn()
163+
}
164+
})
165+
166+
const body = {
167+
coursePrefix: 'test-course',
168+
guideSuffix: 'test-guide',
169+
lang: 'en',
170+
grid: [[{ userInput: 'T' }, { userInput: 'E' }, { userInput: 'S' }, { userInput: 'T' }]],
171+
placements: [{ row: 0, col: 0, direction: 'across' }],
172+
walletAddress: '0x123',
173+
token: 'TOK'
167174
}
168-
const req = new NextRequest('http://localhost:3000/api/check-crossword', {
169-
method: 'POST', body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' },
175+
const req = new NextRequest('http://localhost', {
176+
method: 'POST',
177+
headers: { 'Content-Type': 'application/json' },
178+
body: JSON.stringify(body),
170179
})
180+
171181
const res = await POST(req)
172-
expect(res.status).toBe(401)
173182
const data = await res.json()
174-
expect(data.error).toMatch(/Token stored for user doesn\'t match/i)
183+
184+
expect(res.status).toBe(200)
185+
expect(data.mistakesInCW).toEqual([])
186+
expect(data.scholarshipResult).toBe('0xmocktxhash')
187+
expect(mockInsertInto).toHaveBeenCalled()
188+
expect(mockUpdateUserAndCoursePoints).toHaveBeenCalled()
175189
})
176190

177-
it('POST con respuestas incorrectas devuelve probs con índice de palabra', async () => {
191+
it('should identify mistakes in the submission', async () => {
178192
mockExecuteTakeFirst
179193
.mockResolvedValueOnce({
180-
billetera: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
181-
usuario_id: 1, token: 'TOK', answer_fib: 'TEST'
194+
billetera: '0x123',
195+
usuario_id: 1,
196+
token: 'TOK',
197+
answer_fib: 'TEST'
182198
})
183199
.mockResolvedValueOnce({ id: 1, profilescore: 60 })
184-
mockExecute.mockResolvedValue([])
185-
mockGetStudentGuideStatus.mockResolvedValueOnce([0n, true])
186-
187-
const grid = [[{ userInput: 'X' }, { userInput: 'E' }, { userInput: 'S' }, { userInput: 'T' }]]
188-
const placements = [{ row: 0, col: 0, direction: 'across' }]
189-
const body = {
190-
courseId: 1, guideId: 1, lang: 'en', grid, placements,
191-
walletAddress: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', token: 'TOK'
200+
201+
const body = {
202+
coursePrefix: 'test-course',
203+
guideSuffix: 'test-guide',
204+
lang: 'en',
205+
grid: [[{ userInput: 'X' }, { userInput: 'E' }, { userInput: 'S' }, { userInput: 'T' }]],
206+
placements: [{ row: 0, col: 0, direction: 'across' }],
207+
walletAddress: '0x123',
208+
token: 'TOK'
192209
}
193-
const req = new NextRequest('http://localhost:3000/api/check-crossword', {
194-
method: 'POST', body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' },
210+
const req = new NextRequest('http://localhost', {
211+
method: 'POST',
212+
headers: { 'Content-Type': 'application/json' },
213+
body: JSON.stringify(body),
195214
})
215+
196216
const res = await POST(req)
197217
const data = await res.json()
218+
198219
expect(res.status).toBe(200)
199220
expect(data.mistakesInCW).toEqual([1])
200221
expect(data.message).toContain('Wrong answer')
201-
expect(data.scholarshipResult).toBe('0xmocktxhash')
222+
})
223+
224+
it('GET should return a 400 error', async () => {
225+
const req = new NextRequest('http://localhost')
226+
const res = await GET(req)
227+
expect(res.status).toBe(400)
228+
const data = await res.json()
229+
expect(data.error).toBe('Expecting POST request')
202230
})
203231
})

0 commit comments

Comments
 (0)