11import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart' ;
22import 'package:ht_api/src/services/jwt_auth_token_service.dart' ;
3+ // Import blacklist service
34import 'package:ht_shared/ht_shared.dart' ;
45import 'package:mocktail/mocktail.dart' ;
56import 'package:test/test.dart' ;
@@ -13,6 +14,8 @@ void main() {
1314 group ('JwtAuthTokenService' , () {
1415 late JwtAuthTokenService service;
1516 late MockUserRepository mockUserRepository;
17+ late MockTokenBlacklistService
18+ mockBlacklistService; // Add mock blacklist service
1619 late MockUuid mockUuid;
1720
1821 const testUser = User (
@@ -25,13 +28,17 @@ void main() {
2528 setUpAll (() {
2629 // Register fallback values for argument matchers
2730 registerFallbackValue (const User (id: 'fallback' , isAnonymous: true ));
31+ // Register fallback for DateTime if needed for blacklist mock
32+ registerFallbackValue (DateTime (2024 ));
2833 });
2934
3035 setUp (() {
3136 mockUserRepository = MockUserRepository ();
37+ mockBlacklistService = MockTokenBlacklistService (); // Instantiate mock
3238 mockUuid = MockUuid ();
3339 service = JwtAuthTokenService (
3440 userRepository: mockUserRepository,
41+ blacklistService: mockBlacklistService, // Provide mock
3542 uuidGenerator: mockUuid,
3643 );
3744
@@ -110,6 +117,9 @@ void main() {
110117 // Stub user repository to return the user when read is called
111118 when (() => mockUserRepository.read (testUser.id))
112119 .thenAnswer ((_) async => testUser);
120+ // Stub blacklist service to return false (not blacklisted) by default
121+ when (() => mockBlacklistService.isBlacklisted (any ()))
122+ .thenAnswer ((_) async => false );
113123 });
114124
115125 test ('successfully validates a correct token and returns user' , () async {
@@ -257,7 +267,160 @@ void main() {
257267 verify (() => mockUserRepository.read (testUser.id)).called (1 );
258268 });
259269 });
270+
271+ group ('invalidateToken' , () {
272+ late String validToken;
273+ late String validJti;
274+ late DateTime validExpiry;
275+
276+ setUp (() async {
277+ // Generate a valid token and extract its details for tests
278+ validToken = await service.generateToken (testUser);
279+ final jwt = JWT .verify (
280+ validToken,
281+ SecretKey (
282+ 'your-very-hardcoded-super-secret-key-replace-this-in-prod' ,
283+ ),
284+ );
285+ validJti = jwt.payload['jti' ] as String ;
286+ final expClaim = jwt.payload['exp' ] as int ;
287+ validExpiry =
288+ DateTime .fromMillisecondsSinceEpoch (expClaim * 1000 , isUtc: true );
289+
290+ // Default stub for blacklist success
291+ when (
292+ () => mockBlacklistService.blacklist (any (), any ()),
293+ ).thenAnswer ((_) async => Future .value ());
294+ });
295+
296+ test ('successfully invalidates a valid token' , () async {
297+ // Act
298+ await service.invalidateToken (validToken);
299+
300+ // Assert
301+ verify (
302+ () => mockBlacklistService.blacklist (validJti, validExpiry),
303+ ).called (1 );
304+ });
305+
306+ test ('throws InvalidInputException for invalid token signature' ,
307+ () async {
308+ // Arrange: Sign with a different key
309+ final jwt = JWT ({'sub' : testUser.id}, subject: testUser.id);
310+ final invalidSignatureToken = jwt.sign (SecretKey (_invalidSecretKey));
311+
312+ // Act & Assert
313+ await expectLater (
314+ () => service.invalidateToken (invalidSignatureToken),
315+ throwsA (
316+ isA <InvalidInputException >().having (
317+ (e) => e.message,
318+ 'message' ,
319+ contains ('Invalid token format' ),
320+ ),
321+ ),
322+ );
323+ verifyNever (() => mockBlacklistService.blacklist (any (), any ()));
324+ });
325+
326+ test ('throws InvalidInputException for token missing "jti" claim' ,
327+ () async {
328+ // Arrange: Create token without jti
329+ final jwt = JWT (
330+ {
331+ 'sub' : testUser.id,
332+ 'exp' : validExpiry.millisecondsSinceEpoch ~ / 1000 ,
333+ },
334+ subject: testUser.id,
335+ // No jti
336+ );
337+ final noJtiToken = jwt.sign (
338+ SecretKey (
339+ 'your-very-hardcoded-super-secret-key-replace-this-in-prod' ,
340+ ),
341+ );
342+
343+ // Act & Assert
344+ await expectLater (
345+ () => service.invalidateToken (noJtiToken),
346+ throwsA (
347+ isA <InvalidInputException >().having (
348+ (e) => e.message,
349+ 'message' ,
350+ 'Cannot invalidate token: Missing or empty JWT ID (jti) claim.' ,
351+ ),
352+ ),
353+ );
354+ verifyNever (() => mockBlacklistService.blacklist (any (), any ()));
355+ });
356+
357+ test ('throws InvalidInputException for token missing "exp" claim' ,
358+ () async {
359+ // Arrange: Create token without exp
360+ final jwt = JWT (
361+ {'sub' : testUser.id, 'jti' : testUuidValue},
362+ subject: testUser.id,
363+ jwtId: testUuidValue,
364+ // No exp
365+ );
366+ final noExpToken = jwt.sign (
367+ SecretKey (
368+ 'your-very-hardcoded-super-secret-key-replace-this-in-prod' ,
369+ ),
370+ );
371+
372+ // Act & Assert
373+ await expectLater (
374+ () => service.invalidateToken (noExpToken),
375+ throwsA (
376+ isA <InvalidInputException >().having (
377+ (e) => e.message,
378+ 'message' ,
379+ 'Cannot invalidate token: Missing or invalid expiry (exp) claim.' ,
380+ ),
381+ ),
382+ );
383+ verifyNever (() => mockBlacklistService.blacklist (any (), any ()));
384+ });
385+
386+ test ('rethrows HtHttpException from blacklist service' , () async {
387+ // Arrange
388+ const exception = ServerException ('Blacklist database error' );
389+ when (() => mockBlacklistService.blacklist (validJti, validExpiry))
390+ .thenThrow (exception);
391+
392+ // Act & Assert
393+ await expectLater (
394+ () => service.invalidateToken (validToken),
395+ throwsA (isA <ServerException >()),
396+ );
397+ verify (
398+ () => mockBlacklistService.blacklist (validJti, validExpiry),
399+ ).called (1 );
400+ });
401+
402+ test ('throws OperationFailedException for unexpected blacklist error' ,
403+ () async {
404+ // Arrange
405+ final exception = Exception ('Unexpected blacklist failure' );
406+ when (() => mockBlacklistService.blacklist (validJti, validExpiry))
407+ .thenThrow (exception);
408+
409+ // Act & Assert
410+ await expectLater (
411+ () => service.invalidateToken (validToken),
412+ throwsA (
413+ isA <OperationFailedException >().having (
414+ (e) => e.message,
415+ 'message' ,
416+ contains ('Token invalidation failed unexpectedly' ),
417+ ),
418+ ),
419+ );
420+ verify (
421+ () => mockBlacklistService.blacklist (validJti, validExpiry),
422+ ).called (1 );
423+ });
424+ });
260425 });
261426}
262-
263- // Removed the extension trying to access the private secret key.
0 commit comments