11import { describe , it , expect , beforeEach , vi , beforeAll } from 'vitest'
22import { 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
4352vi . 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' , ( ) => ( { } ) )
5358vi . mock ( '@/.config/kysely.config.ts' , ( ) => ( {
5459 newKyselyPostgresql : ( ) => new MockKysely ( ) ,
5560} ) )
5661
62+ // Mock viem and crypto functions
5763const 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
6266vi . 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
8085describe ( '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 ( / E x p e c t i n g P O S T / 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 ( / w i l l n o t b e g r a d e d / 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 ( / T o k e n s t o r e d f o r u s e r d o e s n \' t m a t c h / 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