@@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
22import { INestApplication , ValidationPipe } from '@nestjs/common' ;
33import { FastifyAdapter , NestFastifyApplication } from '@nestjs/platform-fastify' ;
44import * as request from 'supertest' ;
5+ import * as jwt from 'jsonwebtoken' ;
56import { ConfigModule , ConfigService } from '@nestjs/config' ;
67import { AuthModule } from '../../../../src/modules/auth/auth.module' ;
78import { UsersModule } from '../../../../src/modules/users/users.module' ;
@@ -512,4 +513,164 @@ describe('AuthController (e2e)', () => {
512513 expect ( new Date ( session . expires_at ) . getTime ( ) ) . toBeGreaterThan ( Date . now ( ) ) ;
513514 } ) ;
514515 } ) ;
516+
517+ describe ( 'POST /auth/refresh' , ( ) => {
518+ it ( 'should return new tokens on happy path (valid refresh token)' , async ( ) => {
519+ const keypair = createTestKeypair ( ) ;
520+ const wallet = keypair . publicKey ( ) ;
521+ testWallets . push ( wallet ) ;
522+
523+ // Complete auth flow to get refresh token
524+ const nonceResponse = await request ( app . getHttpServer ( ) )
525+ . post ( '/auth/nonce' )
526+ . send ( { wallet } )
527+ . expect ( 201 ) ;
528+
529+ const nonce = nonceResponse . body . nonce ;
530+ const signature = signMessage ( keypair , nonce ) ;
531+
532+ const verifyResponse = await request ( app . getHttpServer ( ) )
533+ . post ( '/auth/verify' )
534+ . send ( { wallet, nonce, signature } )
535+ . expect ( 200 ) ;
536+
537+ const originalRefreshToken = verifyResponse . body . refreshToken ;
538+
539+ // Use refresh token to get new tokens
540+ const refreshResponse = await request ( app . getHttpServer ( ) )
541+ . post ( '/auth/refresh' )
542+ . send ( { refreshToken : originalRefreshToken } )
543+ . expect ( 200 ) ;
544+
545+ expect ( refreshResponse . body ) . toHaveProperty ( 'accessToken' ) ;
546+ expect ( refreshResponse . body ) . toHaveProperty ( 'refreshToken' ) ;
547+ expect ( refreshResponse . body ) . toHaveProperty ( 'expiresIn' ) ;
548+ expect ( refreshResponse . body ) . toHaveProperty ( 'tokenType' , 'Bearer' ) ;
549+
550+ // New tokens should be different from the original ones
551+ expect ( refreshResponse . body . accessToken ) . not . toBe ( verifyResponse . body . accessToken ) ;
552+ expect ( refreshResponse . body . refreshToken ) . not . toBe ( originalRefreshToken ) ;
553+
554+ // New access token should work for protected endpoints
555+ await request ( app . getHttpServer ( ) )
556+ . get ( '/users/me' )
557+ . set ( 'Authorization' , `Bearer ${ refreshResponse . body . accessToken } ` )
558+ . expect ( 200 ) ;
559+ } ) ;
560+
561+ it ( 'should return 401 with an expired refresh token' , async ( ) => {
562+ const keypair = createTestKeypair ( ) ;
563+ const wallet = keypair . publicKey ( ) ;
564+ testWallets . push ( wallet ) ;
565+
566+ // Complete auth flow to create a user in the database
567+ const nonceResponse = await request ( app . getHttpServer ( ) )
568+ . post ( '/auth/nonce' )
569+ . send ( { wallet } )
570+ . expect ( 201 ) ;
571+
572+ const nonce = nonceResponse . body . nonce ;
573+ const signature = signMessage ( keypair , nonce ) ;
574+
575+ await request ( app . getHttpServer ( ) )
576+ . post ( '/auth/verify' )
577+ . send ( { wallet, nonce, signature } )
578+ . expect ( 200 ) ;
579+
580+ // Create an expired refresh token signed with the same secret
581+ const configService = app . get ( ConfigService ) ;
582+ const refreshSecret = configService . get < string > ( 'JWT_REFRESH_SECRET' ) ;
583+ const expiredRefreshToken = jwt . sign (
584+ { wallet, type : 'refresh' } ,
585+ refreshSecret ,
586+ { expiresIn : '0s' } ,
587+ ) ;
588+
589+ // Wait a moment to ensure the token is expired
590+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
591+
592+ const response = await request ( app . getHttpServer ( ) )
593+ . post ( '/auth/refresh' )
594+ . send ( { refreshToken : expiredRefreshToken } )
595+ . expect ( 401 ) ;
596+
597+ expect ( response . body ) . toHaveProperty ( 'message' ) ;
598+ } ) ;
599+
600+ it ( 'should return 401 on reuse of a refresh token (rotation detection)' , async ( ) => {
601+ const keypair = createTestKeypair ( ) ;
602+ const wallet = keypair . publicKey ( ) ;
603+ testWallets . push ( wallet ) ;
604+
605+ // Get refresh token via verify
606+ const nonceResponse = await request ( app . getHttpServer ( ) )
607+ . post ( '/auth/nonce' )
608+ . send ( { wallet } )
609+ . expect ( 201 ) ;
610+
611+ const nonce = nonceResponse . body . nonce ;
612+ const signature = signMessage ( keypair , nonce ) ;
613+
614+ const verifyResponse = await request ( app . getHttpServer ( ) )
615+ . post ( '/auth/verify' )
616+ . send ( { wallet, nonce, signature } )
617+ . expect ( 200 ) ;
618+
619+ const refreshToken = verifyResponse . body . refreshToken ;
620+
621+ // First use — should succeed
622+ await request ( app . getHttpServer ( ) )
623+ . post ( '/auth/refresh' )
624+ . send ( { refreshToken } )
625+ . expect ( 200 ) ;
626+
627+ // Second use with the same token — should fail (session was deleted)
628+ await request ( app . getHttpServer ( ) )
629+ . post ( '/auth/refresh' )
630+ . send ( { refreshToken } )
631+ . expect ( 401 ) ;
632+ } ) ;
633+
634+ it ( 'should return 401 with a malformed refresh token' , async ( ) => {
635+ await request ( app . getHttpServer ( ) )
636+ . post ( '/auth/refresh' )
637+ . send ( { refreshToken : 'invalid-token-string' } )
638+ . expect ( 401 ) ;
639+ } ) ;
640+
641+ it ( 'should return 401 with an empty refresh token' , async ( ) => {
642+ await request ( app . getHttpServer ( ) )
643+ . post ( '/auth/refresh' )
644+ . send ( { refreshToken : '' } )
645+ . expect ( 401 ) ;
646+ } ) ;
647+
648+ it ( 'should return 401 when refresh token has wrong type (access token instead of refresh token)' , async ( ) => {
649+ const keypair = createTestKeypair ( ) ;
650+ const wallet = keypair . publicKey ( ) ;
651+ testWallets . push ( wallet ) ;
652+
653+ // Get an access token via verify
654+ const nonceResponse = await request ( app . getHttpServer ( ) )
655+ . post ( '/auth/nonce' )
656+ . send ( { wallet } )
657+ . expect ( 201 ) ;
658+
659+ const nonce = nonceResponse . body . nonce ;
660+ const signature = signMessage ( keypair , nonce ) ;
661+
662+ const verifyResponse = await request ( app . getHttpServer ( ) )
663+ . post ( '/auth/verify' )
664+ . send ( { wallet, nonce, signature } )
665+ . expect ( 200 ) ;
666+
667+ const accessToken = verifyResponse . body . accessToken ;
668+
669+ // Try to use access token as refresh token
670+ await request ( app . getHttpServer ( ) )
671+ . post ( '/auth/refresh' )
672+ . send ( { refreshToken : accessToken } )
673+ . expect ( 401 ) ;
674+ } ) ;
675+ } ) ;
515676} ) ;
0 commit comments