From 6e2fbea0c01b9ff7b7207a9e42cef68d3eb143f1 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 5 Feb 2026 15:24:12 +0700 Subject: [PATCH] test(auth): add unit tests for handling 400 Bad Request during token refresh --- .../authorization_interceptor_test.dart | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/test/features/interceptor/authorization_interceptor_test.dart b/test/features/interceptor/authorization_interceptor_test.dart index c102c8afa..0d2dd3bf3 100644 --- a/test/features/interceptor/authorization_interceptor_test.dart +++ b/test/features/interceptor/authorization_interceptor_test.dart @@ -9,12 +9,14 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http_mock_adapter/http_mock_adapter.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:model/account/authentication_type.dart'; import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart'; import '../../fixtures/account_fixtures.dart'; @@ -51,6 +53,20 @@ void main() { type: DioExceptionType.badResponse, ); + final dioErrorRefresh400 = DioException( + error: { + 'error': 'invalid_grant', + 'error_description': 'Refresh token expired', + }, + requestOptions: RequestOptions(path: '/token', method: 'POST'), + response: Response( + statusCode: 400, + requestOptions: RequestOptions(path: '/token'), + data: {'error': 'invalid_grant'}, + ), + type: DioExceptionType.badResponse, + ); + final dataRequestSuccessfully = {'message': 'Request successfully!'}; setUp(() { @@ -375,6 +391,110 @@ void main() { }); }); + group('Refresh Token Failed Handling (400 Bad Request)', () { + test( + 'WHEN make a request with `tokenOidcExpiredTime`\n' + 'AND returns error `dioError401`\n' + 'THEN refresh token is called\n' + 'AND refresh token call returns 400 (Invalid Grant)\n' + 'THEN interceptor SHOULD reject with `RefreshTokenFailedException`\n' + 'AND interceptor state SHOULD be cleared', + () async { + authorizationInterceptors.setTokenAndAuthorityOidc( + newToken: OIDCFixtures.tokenOidcExpiredTime, + newConfig: OIDCFixtures.oidcConfiguration, + ); + + dioAdapter.onPost( + baseUrl, + (server) => server.throws(responseStatusCode401, dioError401), + ); + + when(authenticationClient.refreshingTokensOIDC( + OIDCFixtures.oidcConfiguration.clientId, + OIDCFixtures.oidcConfiguration.redirectUrl, + OIDCFixtures.oidcConfiguration.discoveryUrl, + OIDCFixtures.oidcConfiguration.scopes, + OIDCFixtures.tokenOidcExpiredTime.refreshToken, + )).thenThrow(dioErrorRefresh400); + + when(accountCacheManager.getCurrentAccount()) + .thenAnswer((_) async => AccountFixtures.aliceAccount); + + await expectLater( + () async => await dio.post(baseUrl), + throwsA(predicate((e) { + final isCorrectType = e.type == DioExceptionType.badResponse; + final isCustomError = e.error is RefreshTokenFailedException; + final has400Response = e.response?.statusCode == 400; + + return isCorrectType && isCustomError && has400Response; + })), + ); + + verify(authenticationClient.refreshingTokensOIDC( + OIDCFixtures.oidcConfiguration.clientId, + OIDCFixtures.oidcConfiguration.redirectUrl, + OIDCFixtures.oidcConfiguration.discoveryUrl, + OIDCFixtures.oidcConfiguration.scopes, + OIDCFixtures.tokenOidcExpiredTime.refreshToken, + )).called(1); + + expect( + authorizationInterceptors.authenticationType, + AuthenticationType.none, + ); + }, + ); + + test( + 'WHEN refresh token fails with other error (e.g., 500)\n' + 'THEN interceptor SHOULD NOT throw `RefreshTokenFailedException`\n' + 'AND SHOULD return original error from super.onError', + () async { + authorizationInterceptors.setTokenAndAuthorityOidc( + newToken: OIDCFixtures.tokenOidcExpiredTime, + newConfig: OIDCFixtures.oidcConfiguration, + ); + + dioAdapter.onPost( + baseUrl, + (server) => server.throws(responseStatusCode401, dioError401), + ); + + final dioErrorRefresh500 = DioException( + requestOptions: RequestOptions(path: '/token'), + response: Response( + statusCode: 500, + requestOptions: RequestOptions(path: '/token'), + ), + type: DioExceptionType.badResponse, + ); + + when(authenticationClient.refreshingTokensOIDC( + OIDCFixtures.oidcConfiguration.clientId, + OIDCFixtures.oidcConfiguration.redirectUrl, + OIDCFixtures.oidcConfiguration.discoveryUrl, + OIDCFixtures.oidcConfiguration.scopes, + OIDCFixtures.tokenOidcExpiredTime.refreshToken, + )).thenThrow(dioErrorRefresh500); + + await expectLater( + () async => await dio.post(baseUrl), + throwsA(predicate((e) { + return e.response?.statusCode == 401 || + e.response?.statusCode == 500; + })), + ); + + expect( + authorizationInterceptors.authenticationType, + AuthenticationType.oidc, + ); + }, + ); + }); + tearDown(() { dioAdapter.close(); dio.close();