@@ -115,6 +115,17 @@ Deno.test("request otp for existing user", async () => {
115115 . where ( "id" , "=" , userId )
116116 . executeTakeFirstOrThrow ( ) ;
117117
118+ // set otp_requested_at in the past to make sure the user can request an OTP again
119+ await db
120+ . updateTable ( "user" )
121+ . set ( {
122+ otp_requested_at : new Date (
123+ Date . now ( ) - settings . OTP . REQUEST_INTERVAL_SECONDS * 1000 ,
124+ ) ,
125+ } )
126+ . where ( "id" , "=" , userId )
127+ . execute ( ) ;
128+
118129 assertEquals ( user . email , email ) ;
119130 assertEquals ( user . otp , null ) ;
120131 assertEquals ( user . otp_expires_at , null ) ;
@@ -159,7 +170,12 @@ Deno.test("verify otp for new user", async () => {
159170 const email = `test-${ crypto . randomUUID ( ) } @example.com` ;
160171
161172 const authService = new AuthService ( ) ;
162- const otp = await authService . requestOtp ( email ) ;
173+ await authService . requestOtp ( email ) ;
174+ const { otp } = await db . selectFrom ( "user" )
175+ . where ( "email" , "=" , email )
176+ . select ( "otp" )
177+ . executeTakeFirstOrThrow ( ) ;
178+ if ( ! otp ) throw new Error ( "Failed to get OTP" ) ;
163179
164180 const requestInfo = prepareVerifyOtp ( { email, otp } ) ;
165181
@@ -229,6 +245,74 @@ Deno.test("verify otp for new user", async () => {
229245 }
230246} ) ;
231247
248+ Deno . test ( "request otp - should prevent spam requests within interval" , async ( ) => {
249+ initKyselyDb ( ) ;
250+ await resetTables ( ) ;
251+
252+ try {
253+ const email = `test-${ crypto . randomUUID ( ) } @example.com` ;
254+
255+ // First OTP request should succeed
256+ const firstRequestInfo = prepareRequestOtp ( { email } ) ;
257+ const firstResponse = await api . handle (
258+ new Request ( `http://localhost:3000${ firstRequestInfo . url } ` , {
259+ method : firstRequestInfo . method ,
260+ body : JSON . stringify ( firstRequestInfo . body ) ,
261+ headers : {
262+ "Content-Type" : "application/json" ,
263+ } ,
264+ } ) ,
265+ ) ;
266+
267+ assertEquals ( firstResponse ! . status , 200 ) ;
268+ const firstResponseBody = await firstResponse ?. json ( ) ;
269+ assertEquals ( firstResponseBody . message , "OTP sent successfully" ) ;
270+
271+ // Verify user was created with OTP
272+ const user = await db
273+ . selectFrom ( "user" )
274+ . selectAll ( )
275+ . where ( "email" , "=" , email )
276+ . executeTakeFirst ( ) ;
277+
278+ assertNotEquals ( user , undefined ) ;
279+ assertNotEquals ( user ?. otp , null ) ;
280+ assertNotEquals ( user ?. otp_requested_at , null ) ;
281+
282+ // Second OTP request immediately after should be rejected
283+ const secondRequestInfo = prepareRequestOtp ( { email } ) ;
284+ const secondResponse = await api . handle (
285+ new Request ( `http://localhost:3000${ secondRequestInfo . url } ` , {
286+ method : secondRequestInfo . method ,
287+ body : JSON . stringify ( secondRequestInfo . body ) ,
288+ headers : {
289+ "Content-Type" : "application/json" ,
290+ } ,
291+ } ) ,
292+ ) ;
293+
294+ assertEquals ( secondResponse ! . status , 400 ) ;
295+ const secondResponseBody = await secondResponse ?. json ( ) ;
296+ assertEquals ( secondResponseBody . error , "otp_already_requested" ) ;
297+
298+ // Verify the OTP and timestamp weren't updated
299+ const userAfterSecondRequest = await db
300+ . selectFrom ( "user" )
301+ . selectAll ( )
302+ . where ( "email" , "=" , email )
303+ . executeTakeFirst ( ) ;
304+
305+ assertEquals ( userAfterSecondRequest ?. otp , user ?. otp ) ;
306+ assertEquals (
307+ userAfterSecondRequest ?. otp_requested_at ?. getTime ( ) ,
308+ user ?. otp_requested_at ?. getTime ( ) ,
309+ ) ;
310+ } finally {
311+ await resetTables ( ) ;
312+ await destroyKyselyDb ( ) ;
313+ }
314+ } ) ;
315+
232316Deno . test ( "verify otp for existing user" , async ( ) => {
233317 initKyselyDb ( ) ;
234318 await resetTables ( ) ;
@@ -270,8 +354,24 @@ Deno.test("verify otp for existing user", async () => {
270354
271355 assertEquals ( personalWorkspaceMember . role , ADMIN_ROLE ) ;
272356
357+ // set otp_requested_at in the past to make sure the user can request an OTP again
358+ await db
359+ . updateTable ( "user" )
360+ . set ( {
361+ otp_requested_at : new Date (
362+ Date . now ( ) - settings . OTP . REQUEST_INTERVAL_SECONDS * 1000 ,
363+ ) ,
364+ } )
365+ . where ( "id" , "=" , userId )
366+ . execute ( ) ;
367+
273368 const authService = new AuthService ( ) ;
274- const otp = await authService . requestOtp ( email ) ;
369+ await authService . requestOtp ( email ) ;
370+ const { otp } = await db . selectFrom ( "user" )
371+ . where ( "email" , "=" , email )
372+ . select ( "otp" )
373+ . executeTakeFirstOrThrow ( ) ;
374+ if ( ! otp ) throw new Error ( "Failed to get OTP" ) ;
275375
276376 const requestInfo = prepareVerifyOtp ( { email, otp } ) ;
277377
@@ -332,7 +432,13 @@ Deno.test("verify otp - should block after max failed attempts", async () => {
332432 const authService = new AuthService ( ) ;
333433
334434 // Request OTP
335- const validOtp = await authService . requestOtp ( email ) ;
435+ await authService . requestOtp ( email ) ;
436+ const { otp : validOtp } = await db . selectFrom ( "user" )
437+ . where ( "email" , "=" , email )
438+ . select ( "otp" )
439+ . executeTakeFirstOrThrow ( ) ;
440+ if ( ! validOtp ) throw new Error ( "Failed to get OTP" ) ;
441+
336442 const wrongOtp = "000000" ; // Wrong OTP
337443
338444 // Make 3 failed attempts (assuming MAX_ATTEMPTS is 3)
@@ -391,79 +497,98 @@ Deno.test("verify otp - should block after max failed attempts", async () => {
391497 }
392498} ) ;
393499
394- Deno . test ( "verify otp - should reset attempts on new OTP request" , async ( ) => {
395- initKyselyDb ( ) ;
396- await resetTables ( ) ;
500+ Deno . test (
501+ "verify otp - should reset attempts on new OTP request" ,
502+ async ( ) => {
503+ initKyselyDb ( ) ;
504+ await resetTables ( ) ;
397505
398- try {
399- const email = `test-${ crypto . randomUUID ( ) } @example.com` ;
506+ try {
507+ const email = `test-${ crypto . randomUUID ( ) } @example.com` ;
400508
401- // Request initial OTP
402- const requestOtpInfo = prepareRequestOtp ( { email } ) ;
403- await api . handle (
404- new Request ( `http://localhost:3000${ requestOtpInfo . url } ` , {
405- method : requestOtpInfo . method ,
406- body : JSON . stringify ( requestOtpInfo . body ) ,
407- headers : {
408- "Content-Type" : "application/json" ,
409- } ,
410- } ) ,
411- ) ;
412-
413- // Make some failed attempts
414- const wrongOtp = "000000" ;
415- for ( let i = 0 ; i < 2 ; i ++ ) {
416- const verifyInfo = prepareVerifyOtp ( { email, otp : wrongOtp } ) ;
509+ // Request initial OTP
510+ const requestOtpInfo = prepareRequestOtp ( { email } ) ;
417511 await api . handle (
418- new Request ( `http://localhost:3000${ verifyInfo . url } ` , {
419- method : verifyInfo . method ,
420- body : JSON . stringify ( verifyInfo . body ) ,
512+ new Request ( `http://localhost:3000${ requestOtpInfo . url } ` , {
513+ method : requestOtpInfo . method ,
514+ body : JSON . stringify ( requestOtpInfo . body ) ,
421515 headers : {
422516 "Content-Type" : "application/json" ,
423517 } ,
424518 } ) ,
425519 ) ;
426- }
427-
428- // Verify attempts were recorded
429- let user = await db
430- . selectFrom ( "user" )
431- . selectAll ( )
432- . where ( "email" , "=" , email )
433- . executeTakeFirstOrThrow ( ) ;
434- assertEquals ( user . otp_attempts , 2 ) ;
435-
436- // Request new OTP - should reset attempts
437- const authService = new AuthService ( ) ;
438- const newOtp = await authService . requestOtp ( email ) ;
439-
440- user = await db
441- . selectFrom ( "user" )
442- . selectAll ( )
443- . where ( "email" , "=" , email )
444- . executeTakeFirstOrThrow ( ) ;
445- assertEquals ( user . otp_attempts , 0 ) ; // Should be reset
446520
447- // Should be able to verify with new OTP
448- const newVerifyInfo = prepareVerifyOtp ( { email, otp : newOtp } ) ;
449- const response = await api . handle (
450- new Request ( `http://localhost:3000${ newVerifyInfo . url } ` , {
451- method : newVerifyInfo . method ,
452- body : JSON . stringify ( newVerifyInfo . body ) ,
453- headers : {
454- "Content-Type" : "application/json" ,
455- } ,
456- } ) ,
457- ) ;
521+ // Make some failed attempts
522+ const wrongOtp = "000000" ;
523+ for ( let i = 0 ; i < 2 ; i ++ ) {
524+ const verifyInfo = prepareVerifyOtp ( { email, otp : wrongOtp } ) ;
525+ await api . handle (
526+ new Request ( `http://localhost:3000${ verifyInfo . url } ` , {
527+ method : verifyInfo . method ,
528+ body : JSON . stringify ( verifyInfo . body ) ,
529+ headers : {
530+ "Content-Type" : "application/json" ,
531+ } ,
532+ } ) ,
533+ ) ;
534+ }
535+
536+ // Verify attempts were recorded
537+ let user = await db
538+ . selectFrom ( "user" )
539+ . selectAll ( )
540+ . where ( "email" , "=" , email )
541+ . executeTakeFirstOrThrow ( ) ;
542+ assertEquals ( user . otp_attempts , 2 ) ;
543+
544+ // set otp_requested_at in the past to make sure the user can request an OTP again
545+ await db
546+ . updateTable ( "user" )
547+ . set ( {
548+ otp_requested_at : new Date (
549+ Date . now ( ) - settings . OTP . REQUEST_INTERVAL_SECONDS * 1000 ,
550+ ) ,
551+ } )
552+ . where ( "id" , "=" , user . id )
553+ . execute ( ) ;
554+
555+ // Request new OTP - should reset attempts
556+ const authService = new AuthService ( ) ;
557+ await authService . requestOtp ( email ) ;
558+ const { otp } = await db . selectFrom ( "user" )
559+ . where ( "email" , "=" , email )
560+ . selectAll ( )
561+ . executeTakeFirstOrThrow ( ) ;
562+ if ( ! otp ) throw new Error ( "Failed to get OTP" ) ;
563+
564+ user = await db
565+ . selectFrom ( "user" )
566+ . selectAll ( )
567+ . where ( "email" , "=" , email )
568+ . executeTakeFirstOrThrow ( ) ;
569+ assertEquals ( user . otp_attempts , 0 ) ; // Should be reset
570+
571+ // Should be able to verify with new OTP
572+ const newVerifyInfo = prepareVerifyOtp ( { email, otp } ) ;
573+ const response = await api . handle (
574+ new Request ( `http://localhost:3000${ newVerifyInfo . url } ` , {
575+ method : newVerifyInfo . method ,
576+ body : JSON . stringify ( newVerifyInfo . body ) ,
577+ headers : {
578+ "Content-Type" : "application/json" ,
579+ } ,
580+ } ) ,
581+ ) ;
458582
459- assertEquals ( response ! . status , 200 ) ;
460- const responseBody = await response ! . json ( ) ;
461- assertGreater ( responseBody . token . length , 0 ) ;
462- } finally {
463- await resetTables ( ) ;
464- await destroyKyselyDb ( ) ;
465- }
466- } ) ;
583+ assertEquals ( response ! . status , 200 ) ;
584+ const responseBody = await response ! . json ( ) ;
585+ assertGreater ( responseBody . token . length , 0 ) ;
586+ } finally {
587+ await resetTables ( ) ;
588+ await destroyKyselyDb ( ) ;
589+ }
590+ } ,
591+ ) ;
467592
468593Deno . test ( "verify otp - should track failed attempts correctly" , async ( ) => {
469594 initKyselyDb ( ) ;
0 commit comments