1
1
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart' ;
2
2
import 'package:ht_api/src/services/jwt_auth_token_service.dart' ;
3
+ // Import blacklist service
3
4
import 'package:ht_shared/ht_shared.dart' ;
4
5
import 'package:mocktail/mocktail.dart' ;
5
6
import 'package:test/test.dart' ;
@@ -13,6 +14,8 @@ void main() {
13
14
group ('JwtAuthTokenService' , () {
14
15
late JwtAuthTokenService service;
15
16
late MockUserRepository mockUserRepository;
17
+ late MockTokenBlacklistService
18
+ mockBlacklistService; // Add mock blacklist service
16
19
late MockUuid mockUuid;
17
20
18
21
const testUser = User (
@@ -25,13 +28,17 @@ void main() {
25
28
setUpAll (() {
26
29
// Register fallback values for argument matchers
27
30
registerFallbackValue (const User (id: 'fallback' , isAnonymous: true ));
31
+ // Register fallback for DateTime if needed for blacklist mock
32
+ registerFallbackValue (DateTime (2024 ));
28
33
});
29
34
30
35
setUp (() {
31
36
mockUserRepository = MockUserRepository ();
37
+ mockBlacklistService = MockTokenBlacklistService (); // Instantiate mock
32
38
mockUuid = MockUuid ();
33
39
service = JwtAuthTokenService (
34
40
userRepository: mockUserRepository,
41
+ blacklistService: mockBlacklistService, // Provide mock
35
42
uuidGenerator: mockUuid,
36
43
);
37
44
@@ -110,6 +117,9 @@ void main() {
110
117
// Stub user repository to return the user when read is called
111
118
when (() => mockUserRepository.read (testUser.id))
112
119
.thenAnswer ((_) async => testUser);
120
+ // Stub blacklist service to return false (not blacklisted) by default
121
+ when (() => mockBlacklistService.isBlacklisted (any ()))
122
+ .thenAnswer ((_) async => false );
113
123
});
114
124
115
125
test ('successfully validates a correct token and returns user' , () async {
@@ -257,7 +267,160 @@ void main() {
257
267
verify (() => mockUserRepository.read (testUser.id)).called (1 );
258
268
});
259
269
});
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
+ });
260
425
});
261
426
}
262
-
263
- // Removed the extension trying to access the private secret key.
0 commit comments