From abadbc7786f33040c498f37ec885b2f7e16ca397 Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Fri, 19 Apr 2024 16:33:45 +0700 Subject: [PATCH] TF-2810 Handle crashed when no browser available for OIDC --- .../domain/exceptions/login_exception.dart | 19 +- .../usecases/get_token_oidc_interactor.dart | 5 + .../login/presentation/login_controller.dart | 43 ++-- .../widgets/login_message_widget.dart | 4 + lib/l10n/intl_messages.arb | 8 +- lib/main/localizations/app_localizations.dart | 7 + pubspec.lock | 4 +- pubspec.yaml | 2 +- .../presentation/login_controller_test.dart | 209 ++++++++++++++++++ 9 files changed, 282 insertions(+), 19 deletions(-) create mode 100644 test/features/login/presentation/login_controller_test.dart diff --git a/lib/features/login/domain/exceptions/login_exception.dart b/lib/features/login/domain/exceptions/login_exception.dart index 665f20c1a4..a64fa11384 100644 --- a/lib/features/login/domain/exceptions/login_exception.dart +++ b/lib/features/login/domain/exceptions/login_exception.dart @@ -1,4 +1,21 @@ +import 'package:flutter/services.dart'; + class NotFoundDataResourceRecordException implements Exception {} -class NotFoundUrlException implements Exception {} \ No newline at end of file +class NotFoundUrlException implements Exception {} + +class NoSuitableBrowserForOIDCException implements Exception { + + static const noBrowserAvailableCode = 'no_browser_available'; + + static bool verifyNoSuitableBrowserOIDC(dynamic exception) { + if (exception is PlatformException) { + if (exception.code == noBrowserAvailableCode) { + return true; + } + } + return false; + } +} + diff --git a/lib/features/login/domain/usecases/get_token_oidc_interactor.dart b/lib/features/login/domain/usecases/get_token_oidc_interactor.dart index 9d8cdccf87..337a8daf84 100644 --- a/lib/features/login/domain/usecases/get_token_oidc_interactor.dart +++ b/lib/features/login/domain/usecases/get_token_oidc_interactor.dart @@ -7,6 +7,7 @@ import 'package:model/account/authentication_type.dart'; import 'package:model/account/personal_account.dart'; import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/token_oidc.dart'; +import 'package:tmail_ui_user/features/login/domain/exceptions/login_exception.dart'; import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; @@ -46,6 +47,10 @@ class GetTokenOIDCInteractor { yield Right(GetTokenOIDCSuccess(tokenOIDC, config)); } catch (e) { logError('GetTokenOIDCInteractor::execute(): $e'); + if (NoSuitableBrowserForOIDCException.verifyNoSuitableBrowserOIDC(e)) { + yield Left(GetTokenOIDCFailure(NoSuitableBrowserForOIDCException())); + return; + } yield Left(GetTokenOIDCFailure(e)); } } diff --git a/lib/features/login/presentation/login_controller.dart b/lib/features/login/presentation/login_controller.dart index 7072f95e9e..561ad3448e 100644 --- a/lib/features/login/presentation/login_controller.dart +++ b/lib/features/login/presentation/login_controller.dart @@ -17,6 +17,7 @@ import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dar import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart'; import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; +import 'package:tmail_ui_user/features/login/domain/exceptions/login_exception.dart'; import 'package:tmail_ui_user/features/login/domain/model/login_constants.dart'; import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart'; import 'package:tmail_ui_user/features/login/domain/model/recent_login_username.dart'; @@ -121,18 +122,18 @@ class LoginController extends ReloadableController { @override void handleFailureViewState(Failure failure) { + log('LoginController::handleFailureViewState(): $failure'); if (failure is GetAuthenticationInfoFailure) { getAuthenticatedAccountAction(); } else if (failure is CheckOIDCIsAvailableFailure || failure is GetStoredOidcConfigurationFailure || failure is GetOIDCIsAvailableFailure || - failure is GetOIDCConfigurationFailure || - failure is GetTokenOIDCFailure) { - if (PlatformInfo.isMobile && loginFormType.value == LoginFormType.dnsLookupForm) { - _showPasswordForm(); - } else { - _showCredentialForm(); - } + failure is GetOIDCConfigurationFailure + ) { + _handleCommonOIDCFailure(); + } else if (failure is GetTokenOIDCFailure) { + _handleNoSuitableBrowserOIDC(failure) + .map((stillFailed) => _handleCommonOIDCFailure()); } else if (failure is GetAuthenticatedAccountFailure) { _checkOIDCIsAvailable(); } else if (failure is GetSessionFailure) { @@ -176,13 +177,11 @@ class LoginController extends ReloadableController { if (failure is CheckOIDCIsAvailableFailure || failure is GetStoredOidcConfigurationFailure || failure is GetOIDCConfigurationFailure || - failure is GetOIDCIsAvailableFailure || - failure is GetTokenOIDCFailure) { - if (PlatformInfo.isMobile && loginFormType.value == LoginFormType.dnsLookupForm) { - _showPasswordForm(); - } else { - _showCredentialForm(); - } + failure is GetOIDCIsAvailableFailure) { + _handleCommonOIDCFailure(); + } else if (failure is GetTokenOIDCFailure) { + _handleNoSuitableBrowserOIDC(failure) + .map((stillFailed) => _handleCommonOIDCFailure()); } else if (failure is GetSessionFailure) { clearAllData(); } else { @@ -420,6 +419,22 @@ class LoginController extends ReloadableController { _checkOIDCIsAvailable(); } + void _handleCommonOIDCFailure() { + if (PlatformInfo.isMobile && loginFormType.value == LoginFormType.dnsLookupForm) { + _showPasswordForm(); + } else { + _showCredentialForm(); + } + } + + Option _handleNoSuitableBrowserOIDC(GetTokenOIDCFailure failure) { + if (failure.exception is NoSuitableBrowserForOIDCException) { + return const None(); + } else { + return Some(failure); + } + } + void _showBaseUrlForm() { clearState(); loginFormType.value = LoginFormType.baseUrlForm; diff --git a/lib/features/login/presentation/widgets/login_message_widget.dart b/lib/features/login/presentation/widgets/login_message_widget.dart index a51b72ba5f..52ffe7ad4f 100644 --- a/lib/features/login/presentation/widgets/login_message_widget.dart +++ b/lib/features/login/presentation/widgets/login_message_widget.dart @@ -6,8 +6,10 @@ import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/login/domain/exceptions/login_exception.dart'; import 'package:tmail_ui_user/features/login/domain/state/dns_lookup_to_get_jmap_url_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_oidc_configuration_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_token_oidc_state.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/message_toast_utils.dart'; @@ -46,6 +48,8 @@ class LoginMessageWidget extends StatelessWidget { return AppLocalizations.of(context).canNotVerifySSOConfiguration; } else if (failure is DNSLookupToGetJmapUrlFailure) { return AppLocalizations.of(context).dnsLookupLoginMessage; + } else if (failure is GetTokenOIDCFailure && failure.exception is NoSuitableBrowserForOIDCException) { + return AppLocalizations.of(context).noSuitableBrowserForOIDC; } else if (failure is FeatureFailure) { final errorMessage = MessageToastUtils.getMessageByException(context, failure.exception); return errorMessage ?? AppLocalizations.of(context).unknownError; diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 14d61f40a7..7d4629b125 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-03-25T10:51:29.520399", + "@@last_modified": "2024-04-19T16:31:35.757887", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3689,5 +3689,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "noSuitableBrowserForOIDC": "No suitable browser for OIDC, please check with your system administrator", + "@noSuitableBrowserForOIDC": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index b58d622d2f..86450eae04 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3844,4 +3844,11 @@ class AppLocalizations { name: 'selectAllMessagesOfThisPage', ); } + + String get noSuitableBrowserForOIDC { + return Intl.message( + 'No suitable browser for OIDC, please check with your system administrator', + name: 'noSuitableBrowserForOIDC' + ); + } } \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 3f1458becd..bc521ee671 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -602,10 +602,10 @@ packages: dependency: "direct main" description: name: flutter_appauth - sha256: "19031ea2438a513762e64ec7b58ae1f00cad6fb87f818e405325cd183dfb9fec" + sha256: f2696d4cf437f627fa09bc4864afdd8c80273f2e293fde544b18202a627754b1 url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.0.6" flutter_appauth_platform_interface: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 359994adf1..9758ab4b86 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -164,7 +164,7 @@ dependencies: better_open_file: 3.6.4 - flutter_appauth: 6.0.2 + flutter_appauth: 6.0.6 percent_indicator: 4.2.2 diff --git a/test/features/login/presentation/login_controller_test.dart b/test/features/login/presentation/login_controller_test.dart new file mode 100644 index 0000000000..bb89726679 --- /dev/null +++ b/test/features/login/presentation/login_controller_test.dart @@ -0,0 +1,209 @@ + +import 'package:core/data/network/config/dynamic_url_interceptors.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/app_toast.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get/get.dart'; +import 'package:mockito/annotations.dart'; +import 'package:tmail_ui_user/features/caching/caching_manager.dart'; +import 'package:tmail_ui_user/features/home/domain/usecases/get_session_interactor.dart'; +import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart'; +import 'package:tmail_ui_user/features/login/domain/exceptions/login_exception.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_token_oidc_state.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/authenticate_oidc_on_browser_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/check_oidc_is_available_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/dns_lookup_to_get_jmap_url_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_all_recent_login_url_on_mobile_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_all_recent_login_username_on_mobile_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_authentication_info_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_oidc_configuration_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_oidc_is_available_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_oidc_configuration_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_token_oidc_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/save_login_url_on_mobile_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/save_login_username_on_mobile_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; +import 'package:tmail_ui_user/features/login/presentation/login_controller.dart'; +import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; +import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; +import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; +import 'package:uuid/uuid.dart'; + +import '../../email/presentation/controller/single_email_controller_test.mocks.dart'; +import '../../mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.mocks.dart'; +import 'login_controller_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + late MockAuthenticationInteractor mockAuthenticationInteractor; + late MockCheckOIDCIsAvailableInteractor mockCheckOIDCIsAvailableInteractor; + late MockGetOIDCIsAvailableInteractor mockGetOIDCIsAvailableInteractor; + late MockGetOIDCConfigurationInteractor mockGetOIDCConfigurationInteractor; + late MockGetTokenOIDCInteractor mockGetTokenOIDCInteractor; + late MockAuthenticateOidcOnBrowserInteractor mockAuthenticateOidcOnBrowserInteractor; + late MockGetAuthenticationInfoInteractor mockGetAuthenticationInfoInteractor; + late MockGetStoredOidcConfigurationInteractor mockGetStoredOidcConfigurationInteractor; + late MockSaveLoginUrlOnMobileInteractor mockSaveLoginUrlOnMobileInteractor; + late MockGetAllRecentLoginUrlOnMobileInteractor mockGetAllRecentLoginUrlOnMobileInteractor; + late MockSaveLoginUsernameOnMobileInteractor mockSaveLoginUsernameOnMobileInteractor; + late MockGetAllRecentLoginUsernameOnMobileInteractor mockGetAllRecentLoginUsernameOnMobileInteractor; + late MockDNSLookupToGetJmapUrlInteractor mockDNSLookupToGetJmapUrlInteractor; + late MockGetSessionInteractor mockGetSessionInteractor; + late MockGetAuthenticatedAccountInteractor mockGetAuthenticatedAccountInteractor; + late MockUpdateAuthenticationAccountInteractor mockUpdateAuthenticationAccountInteractor; + late CachingManager mockCachingManager; + late LanguageCacheManager mockLanguageCacheManager; + late MockAuthorizationInterceptors mockAuthorizationInterceptors; + late MockDynamicUrlInterceptors mockDynamicUrlInterceptors; + late MockDeleteCredentialInteractor mockDeleteCredentialInteractor; + late MockLogoutOidcInteractor mockLogoutOidcInteractor; + late MockDeleteAuthorityOidcInteractor mockDeleteAuthorityOidcInteractor; + late MockAppToast mockAppToast; + late MockImagePaths mockImagePaths; + late MockResponsiveUtils mockResponsiveUtils; + late MockUuid mockUuid; + + late LoginController loginController; + + group('Test handleFailureViewState with GetTokenOIDCFailure', () { + setUp(() { + mockAuthenticationInteractor = MockAuthenticationInteractor(); + mockCheckOIDCIsAvailableInteractor = MockCheckOIDCIsAvailableInteractor(); + mockGetOIDCIsAvailableInteractor = MockGetOIDCIsAvailableInteractor(); + mockGetOIDCConfigurationInteractor = MockGetOIDCConfigurationInteractor(); + mockGetTokenOIDCInteractor = MockGetTokenOIDCInteractor(); + mockAuthenticateOidcOnBrowserInteractor = MockAuthenticateOidcOnBrowserInteractor(); + mockGetAuthenticationInfoInteractor = MockGetAuthenticationInfoInteractor(); + mockGetStoredOidcConfigurationInteractor = MockGetStoredOidcConfigurationInteractor(); + mockSaveLoginUrlOnMobileInteractor = MockSaveLoginUrlOnMobileInteractor(); + mockGetAllRecentLoginUrlOnMobileInteractor = MockGetAllRecentLoginUrlOnMobileInteractor(); + mockSaveLoginUsernameOnMobileInteractor = MockSaveLoginUsernameOnMobileInteractor(); + mockGetAllRecentLoginUsernameOnMobileInteractor = MockGetAllRecentLoginUsernameOnMobileInteractor(); + mockDNSLookupToGetJmapUrlInteractor = MockDNSLookupToGetJmapUrlInteractor(); + + // mock reloadable controller + mockGetSessionInteractor = MockGetSessionInteractor(); + mockGetAuthenticatedAccountInteractor = MockGetAuthenticatedAccountInteractor(); + mockUpdateAuthenticationAccountInteractor = MockUpdateAuthenticationAccountInteractor(); + + //mock base controller + mockCachingManager = MockCachingManager(); + mockLanguageCacheManager = MockLanguageCacheManager(); + mockAuthorizationInterceptors = MockAuthorizationInterceptors(); + mockDynamicUrlInterceptors = MockDynamicUrlInterceptors(); + mockDeleteCredentialInteractor = MockDeleteCredentialInteractor(); + mockLogoutOidcInteractor = MockLogoutOidcInteractor(); + mockDeleteAuthorityOidcInteractor = MockDeleteAuthorityOidcInteractor(); + mockAppToast = MockAppToast(); + mockImagePaths = MockImagePaths(); + mockResponsiveUtils = MockResponsiveUtils(); + mockUuid = MockUuid(); + + Get.put(mockGetSessionInteractor); + Get.put(mockGetAuthenticatedAccountInteractor); + Get.put(mockUpdateAuthenticationAccountInteractor); + Get.put(mockCachingManager); + Get.put(mockLanguageCacheManager); + Get.put(mockAuthorizationInterceptors); + Get.put( + mockAuthorizationInterceptors, + tag: BindingTag.isolateTag, + ); + Get.put(mockDynamicUrlInterceptors); + Get.put(mockDeleteCredentialInteractor); + Get.put(mockLogoutOidcInteractor); + Get.put(mockDeleteAuthorityOidcInteractor); + Get.put(mockAppToast); + Get.put(mockImagePaths); + Get.put(mockResponsiveUtils); + Get.put(mockUuid); + Get.testMode = true; + + loginController = LoginController( + mockAuthenticationInteractor, + mockCheckOIDCIsAvailableInteractor, + mockGetOIDCIsAvailableInteractor, + mockGetOIDCConfigurationInteractor, + mockGetTokenOIDCInteractor, + mockAuthenticateOidcOnBrowserInteractor, + mockGetAuthenticationInfoInteractor, + mockGetStoredOidcConfigurationInteractor, + mockSaveLoginUrlOnMobileInteractor, + mockGetAllRecentLoginUrlOnMobileInteractor, + mockSaveLoginUsernameOnMobileInteractor, + mockGetAllRecentLoginUsernameOnMobileInteractor, + mockDNSLookupToGetJmapUrlInteractor, + ); + + + }); + + test('WHEN handleFailureViewState is called with GetTokenOIDCFailure \n' + 'AND loginFormType is dnsLookup \n' + 'BUT EXCEPTION is not NoSuitableBrowserForOIDCException \n' + 'THEN loginFormType will change', () { + + loginController.loginFormType.value = LoginFormType.dnsLookupForm; + final failure = GetTokenOIDCFailure(NotFoundUrlException()); + loginController.handleFailureViewState(failure); + + expect(loginController.loginFormType.value, isNot(LoginFormType.dnsLookupForm)); + }); + + test('WHEN handleFailureViewState is called with GetTokenOIDCFailure \n' + 'AND loginFormType is dnsLookup \n' + 'BUT EXCEPTION is NoSuitableBrowserForOIDCException \n' + 'THEN loginFormType will not change', () { + + loginController.loginFormType.value = LoginFormType.dnsLookupForm; + final failure = GetTokenOIDCFailure(NoSuitableBrowserForOIDCException()); + loginController.handleFailureViewState(failure); + + expect(loginController.loginFormType.value, equals(LoginFormType.dnsLookupForm)); + }); + + test('WHEN handleFailureViewState is called with GetTokenOIDCFailure \n' + 'AND loginFormType is baseUrlForm \n' + 'BUT EXCEPTION is not NoSuitableBrowserForOIDCException \n' + 'THEN loginFormType will change', () { + + loginController.loginFormType.value = LoginFormType.baseUrlForm; + final failure = GetTokenOIDCFailure(NotFoundUrlException()); + loginController.handleFailureViewState(failure); + + expect(loginController.loginFormType.value, isNot(LoginFormType.baseUrlForm)); + }); + + test('WHEN handleFailureViewState is called with GetTokenOIDCFailure \n' + 'AND loginFormType is baseUrlForm \n' + 'BUT EXCEPTION is NoSuitableBrowserForOIDCException \n' + 'THEN loginFormType will not change', () { + + loginController.loginFormType.value = LoginFormType.baseUrlForm; + final failure = GetTokenOIDCFailure(NoSuitableBrowserForOIDCException()); + loginController.handleFailureViewState(failure); + + expect(loginController.loginFormType.value, equals(LoginFormType.baseUrlForm)); + }); + }); +} \ No newline at end of file