@@ -4,6 +4,7 @@ import { ConfigService } from '@nestjs/config';
44import { UnauthorizedException } from '@nestjs/common' ;
55import { AuthService } from './auth.service.js' ;
66import { WalletStrategy } from './strategies/wallet.strategy.js' ;
7+ import { TokenBlacklistService } from './services/token-blacklist.service.js' ;
78import { v4 as uuidv4 } from 'uuid' ;
89
910jest . mock ( 'uuid' , ( ) => {
@@ -28,13 +29,19 @@ describe('AuthService', () => {
2829 verifyAsync : jest . fn ( ) ,
2930 } ;
3031
32+ const mockTokenBlacklistService = {
33+ blacklist : jest . fn ( ) ,
34+ isBlacklisted : jest . fn ( ) ,
35+ } ;
36+
3137 beforeEach ( async ( ) => {
3238 const module : TestingModule = await Test . createTestingModule ( {
3339 providers : [
3440 AuthService ,
3541 { provide : WalletStrategy , useValue : mockWalletStrategy } ,
3642 { provide : ConfigService , useValue : mockConfigService } ,
3743 { provide : JwtService , useValue : mockJwtService } ,
44+ { provide : TokenBlacklistService , useValue : mockTokenBlacklistService } ,
3845 ] ,
3946 } ) . compile ( ) ;
4047
@@ -58,17 +65,6 @@ describe('AuthService', () => {
5865 } ) ;
5966 } ) ;
6067
61- describe ( 'logout' , ( ) => {
62- it ( 'should not throw when called with a jti' , ( ) => {
63- expect ( ( ) => service . logout ( 'some-jti' ) ) . not . toThrow ( ) ;
64- } ) ;
65-
66- it ( 'isTokenRevoked should return true after logout' , ( ) => {
67- service . logout ( 'jti-abc' ) ;
68- expect ( service . isTokenRevoked ( 'jti-abc' ) ) . toBe ( true ) ;
69- } ) ;
70- } ) ;
71-
7268 describe ( 'login' , ( ) => {
7369 it ( 'should issue an access and refresh token pair for a valid signature' , async ( ) => {
7470 mockWalletStrategy . generateNonce . mockReturnValue ( {
@@ -157,4 +153,63 @@ describe('AuthService', () => {
157153 ) ;
158154 } ) ;
159155 } ) ;
156+
157+ describe ( 'logout' , ( ) => {
158+ it ( 'should blacklist the access token jti for its remaining lifetime' , async ( ) => {
159+ const nowSeconds = Math . floor ( Date . now ( ) / 1000 ) ;
160+
161+ await service . logout ( 'access-jti' , nowSeconds + 120 ) ;
162+
163+ expect ( mockTokenBlacklistService . blacklist ) . toHaveBeenCalledWith (
164+ 'access-jti' ,
165+ expect . any ( Number ) ,
166+ ) ;
167+ const [ , ttl ] = mockTokenBlacklistService . blacklist . mock . calls [ 0 ] as [
168+ string ,
169+ number ,
170+ ] ;
171+ expect ( ttl ) . toBeGreaterThan ( 0 ) ;
172+ expect ( ttl ) . toBeLessThanOrEqual ( 120 ) ;
173+ } ) ;
174+
175+ it ( 'should fall back to the configured access TTL when no expiry is given' , async ( ) => {
176+ await service . logout ( 'access-jti' ) ;
177+
178+ expect ( mockTokenBlacklistService . blacklist ) . toHaveBeenCalledWith (
179+ 'access-jti' ,
180+ 900 ,
181+ ) ;
182+ } ) ;
183+
184+ it ( 'isTokenRevoked should return true after logout' , async ( ) => {
185+ await service . logout ( 'jti-abc' ) ;
186+ expect ( service . isTokenRevoked ( 'jti-abc' ) ) . toBe ( true ) ;
187+ } ) ;
188+
189+ it ( 'should mark a stored refresh token as revoked' , async ( ) => {
190+ mockWalletStrategy . generateNonce . mockReturnValue ( {
191+ nonce : 'nonce-1' ,
192+ expiresAt : Date . now ( ) + 60_000 ,
193+ } ) ;
194+ service . requestNonce ( 'test-wallet' ) ;
195+ mockWalletStrategy . verify . mockReturnValue ( true ) ;
196+ mockJwtService . signAsync
197+ . mockResolvedValueOnce ( 'access-token' )
198+ . mockResolvedValueOnce ( 'refresh-token' ) ;
199+ await service . login ( 'test-wallet' , 'nonce-1' ) ;
200+ const issuedRefreshJti = ( uuidv4 as jest . Mock ) . mock . results . at ( - 1 )
201+ ?. value as string ;
202+
203+ await service . logout ( 'access-jti' , undefined , issuedRefreshJti ) ;
204+
205+ mockJwtService . verifyAsync . mockResolvedValue ( {
206+ sub : 'test-wallet' ,
207+ jti : issuedRefreshJti ,
208+ type : 'refresh' ,
209+ } ) ;
210+ await expect ( service . refresh ( 'refresh-token' ) ) . rejects . toThrow (
211+ 'Refresh token has been revoked' ,
212+ ) ;
213+ } ) ;
214+ } ) ;
160215} ) ;
0 commit comments