diff --git a/lib/features/login/data/network/interceptors/authorization_interceptors.dart b/lib/features/login/data/network/interceptors/authorization_interceptors.dart index 1caeb75a4..c797e3298 100644 --- a/lib/features/login/data/network/interceptors/authorization_interceptors.dart +++ b/lib/features/login/data/network/interceptors/authorization_interceptors.dart @@ -77,7 +77,7 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { case AuthenticationType.none: break; } - log('AuthorizationInterceptors::onRequest(): URL = ${options.uri} | HEADER = ${options.headers} | DATA = ${options.data}'); + log('AuthorizationInterceptors::onRequest(): URL = ${options.uri} | HEADER = ${options.headers} | DATA = ${options.data} | METHOD = ${options.method}'); super.onRequest(options, handler); } @@ -89,7 +89,7 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper { @override void onError(DioError err, ErrorInterceptorHandler handler) async { - logError('AuthorizationInterceptors::onError(): DIO_ERROR = $err'); + logError('AuthorizationInterceptors::onError(): DIO_ERROR = $err | METHOD = ${err.requestOptions.method}'); try { if (validateToRefreshToken(responseStatusCode: err.response?.statusCode)) { log('AuthorizationInterceptors::onError:_validateToRefreshToken'); diff --git a/pubspec.lock b/pubspec.lock index b9f7426d2..10776dc42 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1039,7 +1039,7 @@ packages: source: hosted version: "1.1.0" http_mock_adapter: - dependency: transitive + dependency: "direct dev" description: name: http_mock_adapter sha256: "0e7eaa5d77a273af1c2b5ec5066578faaa73039b63ccda5263c200756f24441a" diff --git a/pubspec.yaml b/pubspec.yaml index c7e623d17..411bc0d21 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -249,6 +249,8 @@ dev_dependencies: json_serializable: 6.6.1 + http_mock_adapter: 0.4.2 + dependency_overrides: pointer_interceptor: 0.9.1 diff --git a/test/features/interceptor/authorization_interceptor_test.dart b/test/features/interceptor/authorization_interceptor_test.dart new file mode 100644 index 000000000..d17cb3260 --- /dev/null +++ b/test/features/interceptor/authorization_interceptor_test.dart @@ -0,0 +1,229 @@ + +import 'dart:io'; + +import 'package:core/data/constants/constant.dart'; +import 'package:core/data/network/dio_client.dart'; +import 'package:dio/dio.dart'; +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: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/extensions/oidc_configuration_extensions.dart'; +import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart'; + +import '../../fixtures/account_fixtures.dart'; +import '../../fixtures/oidc_fixtures.dart'; +import 'authorization_interceptor_test.mocks.dart'; + +@GenerateMocks([ + AuthenticationClientBase, + TokenOidcCacheManager, + AccountCacheManager, + IOSSharingManager +]) +void main() { + late Dio dio; + late DioAdapter dioAdapter; + late AuthenticationClientBase authenticationClient; + late TokenOidcCacheManager tokenOidcCacheManager; + late AccountCacheManager accountCacheManager; + late IOSSharingManager iosSharingManager; + late AuthorizationInterceptors authorizationInterceptors; + + const baseUrl = 'http://domain.com/jmap'; + const responseStatusCode401 = 401; + const responseStatusCode500 = 500; + const responseStatusCode200 = 200; + + final dioError401 = DioError( + error: {'message': 'Token Expired'}, + requestOptions: RequestOptions(path: baseUrl, method: 'POST'), + response: Response( + statusCode: responseStatusCode401, + requestOptions: RequestOptions(path: baseUrl) + ), + type: DioErrorType.badResponse, + ); + + final dataRequestSuccessfully = {'message': 'Request successfully!'}; + + setUp(() { + final headers = { + HttpHeaders.acceptHeader: DioClient.jmapHeader, + HttpHeaders.contentTypeHeader: Constant.contentTypeHeaderDefault + }; + final baseOption = BaseOptions(headers: headers); + dio = Dio(baseOption) + ..options.baseUrl = baseUrl; + + authenticationClient = MockAuthenticationClientBase(); + tokenOidcCacheManager = MockTokenOidcCacheManager(); + accountCacheManager = MockAccountCacheManager(); + iosSharingManager = MockIOSSharingManager(); + + authorizationInterceptors = AuthorizationInterceptors( + dio, + authenticationClient, + tokenOidcCacheManager, + accountCacheManager, + iosSharingManager); + + dio.interceptors.add(authorizationInterceptors); + + dioAdapter = DioAdapter(dio: dio); + }); + + group('AuthorizationInterceptor test', () { + + group("validateToRefreshToken method test", () { + test('validateToRefreshToken should return true when conditions are met', () async { + authorizationInterceptors.setTokenAndAuthorityOidc( + newToken: OIDCFixtures.tokenOidcExpiredTime, + newConfig: OIDCFixtures.oidcConfiguration); + + final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode401); + + expect(result, true); + }); + + test('validateToRefreshToken should return false when condition `responseStatusCode == 500`', () async { + authorizationInterceptors.setTokenAndAuthorityOidc( + newToken: OIDCFixtures.tokenOidcExpiredTime, + newConfig: OIDCFixtures.oidcConfiguration); + + final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode500); + + expect(result, false); + }); + + test('validateToRefreshToken should return false when condition `OidcConfiguration is null`', () async { + authorizationInterceptors.setTokenAndAuthorityOidc( + newToken: OIDCFixtures.tokenOidcExpiredTime, + newConfig: null); + + final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode401); + + expect(result, false); + }); + + test('validateToRefreshToken should return false when condition `token is empty`', () async { + authorizationInterceptors.setTokenAndAuthorityOidc( + newToken: OIDCFixtures.tokenOidcExpiredTimeAndTokenEmpty, + newConfig: OIDCFixtures.oidcConfiguration); + + final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode401); + + expect(result, false); + }); + + test('validateToRefreshToken should return false when condition `refreshToken is empty`', () async { + authorizationInterceptors.setTokenAndAuthorityOidc( + newToken: OIDCFixtures.tokenOidcExpiredTimeAndRefreshTokenEmpty, + newConfig: OIDCFixtures.oidcConfiguration); + + final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode401); + + expect(result, false); + }); + + test('validateToRefreshToken should return false when condition `Time not expired`', () async { + authorizationInterceptors.setTokenAndAuthorityOidc( + newToken: OIDCFixtures.newTokenOidc, + newConfig: OIDCFixtures.oidcConfiguration); + + final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode401); + + expect(result, false); + }); + }); + + group('QueuedInterceptorsWrapper test', () { + test( + 'WHEN make a request with `tokenOidcExpiredTime`\n' + 'AND returns error `dioError401`\n' + 'THEN refresh token successfully received `newTokenOidc`\n' + 'AND re-execute request with `newTokenOidc`\n' + 'THEN response data SHOULD return `dataRequestSuccessfully`\n', + () 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 + )).thenAnswer((_) async { + dioAdapter.onPost( + baseUrl, + (server) => server.reply(responseStatusCode200, dataRequestSuccessfully) + ); + + return OIDCFixtures.newTokenOidc; + }); + + when(accountCacheManager.getCurrentAccount()).thenAnswer((_) async => AccountFixtures.aliceAccount); + + final response = await dio.post(baseUrl); + + expect(response.statusCode, responseStatusCode200); + expect(response.data, dataRequestSuccessfully); + }); + + test( + 'WHEN make a request with `tokenOidcExpiredTime`\n' + 'AND returns error `dioError401`\n' + 'THEN refresh token successfully received `newTokenOidc`\n' + 'AND `newTokenOidc` equals `tokenOidcExpiredTime` \n' + 'AND re-execute request with `newTokenOidc`\n' + 'THEN return error SHOULD `dioError401`\n', + () 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 + )).thenAnswer((_) async { + dioAdapter.onPost( + baseUrl, + (server) => server.throws(responseStatusCode401, dioError401) + ); + return OIDCFixtures.tokenOidcExpiredTime; + }); + + when(accountCacheManager.getCurrentAccount()).thenAnswer((_) async => AccountFixtures.aliceAccount); + + expect( + () async => await dio.post(baseUrl), + throwsA(predicate((error) => error.response?.statusCode == responseStatusCode401)) + ); + }); + }); + }); + + tearDown(() { + dioAdapter.close(); + dio.close(); + }); +} \ No newline at end of file diff --git a/test/fixtures/account_fixtures.dart b/test/fixtures/account_fixtures.dart index 1d435333b..3b2dcc313 100644 --- a/test/fixtures/account_fixtures.dart +++ b/test/fixtures/account_fixtures.dart @@ -1,6 +1,17 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/account/authentication_type.dart'; +import 'package:model/account/personal_account.dart'; class AccountFixtures { static final aliceAccountId = AccountId(Id('411ce')); + static final aliceAccount = PersonalAccount( + 'dab', + AuthenticationType.oidc, + isSelected: true, + accountId: aliceAccountId, + apiUrl: 'https://domain.com/jmap', + userName: UserName('Alice') + ); } \ No newline at end of file diff --git a/test/fixtures/oidc_fixtures.dart b/test/fixtures/oidc_fixtures.dart new file mode 100644 index 000000000..b31b0a409 --- /dev/null +++ b/test/fixtures/oidc_fixtures.dart @@ -0,0 +1,35 @@ + +import 'package:model/oidc/oidc_configuration.dart'; +import 'package:model/oidc/token_id.dart'; +import 'package:model/oidc/token_oidc.dart'; + +class OIDCFixtures { + static final tokenOidcExpiredTime = TokenOIDC( + 'dab123', + TokenId('dab123'), + 'dab456', + expiredTime: DateTime.now().subtract(const Duration(days: 1))); + + static final tokenOidcExpiredTimeAndRefreshTokenEmpty = TokenOIDC( + 'dab123', + TokenId('dab123'), + '', + expiredTime: DateTime.now().subtract(const Duration(days: 1))); + + static final tokenOidcExpiredTimeAndTokenEmpty = TokenOIDC( + '', + TokenId('dab123'), + 'dab456', + expiredTime: DateTime.now().subtract(const Duration(days: 1))); + + static final newTokenOidc = TokenOIDC( + 'test123', + TokenId('test123'), + 'test456', + expiredTime: DateTime.now().add(const Duration(days: 1))); + + static final oidcConfiguration = OIDCConfiguration( + authority: 'https://example.com', + clientId: 'client-id', + scopes: ['scope1', 'scope2', 'scope3']); +} \ No newline at end of file