TF-2810 Handle crashed when no browser available for OIDC

This commit is contained in:
Dat PHAM HOANG
2024-04-19 16:33:45 +07:00
committed by Dat H. Pham
parent 5a22ea0509
commit d2fdbfab0f
9 changed files with 286 additions and 19 deletions
@@ -1,4 +1,21 @@
import 'package:flutter/services.dart';
class NotFoundDataResourceRecordException implements Exception {}
class NotFoundUrlException implements Exception {}
class NotFoundUrlException implements Exception {}
class NoSuitableBrowserForOIDCException implements Exception {
static const noBrowserAvailableCode = 'no_browser_available';
static bool verifyException(dynamic exception) {
if (exception is PlatformException) {
if (exception.code == noBrowserAvailableCode) {
return true;
}
}
return false;
}
}
@@ -3,10 +3,12 @@ import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/utils/app_logger.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter/services.dart';
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';
@@ -44,6 +46,13 @@ class GetTokenOIDCInteractor {
)
);
yield Right<Failure, Success>(GetTokenOIDCSuccess(tokenOIDC, config));
} on PlatformException catch (e) {
logError('GetTokenOIDCInteractor::execute(): PlatformException ${e.message} - ${e.stacktrace}');
if (NoSuitableBrowserForOIDCException.verifyException(e)) {
yield Left<Failure, Success>(GetTokenOIDCFailure(NoSuitableBrowserForOIDCException()));
} else {
yield Left<Failure, Success>(GetTokenOIDCFailure(e));
}
} catch (e) {
logError('GetTokenOIDCInteractor::execute(): $e');
yield Left<Failure, Success>(GetTokenOIDCFailure(e));
@@ -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<Failure> _handleNoSuitableBrowserOIDC(GetTokenOIDCFailure failure) {
if (failure.exception is NoSuitableBrowserForOIDCException) {
return const None();
} else {
return Some(failure);
}
}
void _showBaseUrlForm() {
clearState();
loginFormType.value = LoginFormType.baseUrlForm;
@@ -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;
+7 -1
View File
@@ -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": {}
}
}
@@ -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'
);
}
}
+2 -2
View File
@@ -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:
+1 -1
View File
@@ -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
@@ -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<AuthenticationInteractor>(),
MockSpec<CheckOIDCIsAvailableInteractor>(),
MockSpec<GetOIDCIsAvailableInteractor>(),
MockSpec<GetOIDCConfigurationInteractor>(),
MockSpec<GetTokenOIDCInteractor>(),
MockSpec<AuthenticateOidcOnBrowserInteractor>(),
MockSpec<GetAuthenticationInfoInteractor>(),
MockSpec<GetStoredOidcConfigurationInteractor>(),
MockSpec<SaveLoginUrlOnMobileInteractor>(),
MockSpec<GetAllRecentLoginUrlOnMobileInteractor>(),
MockSpec<SaveLoginUsernameOnMobileInteractor>(),
MockSpec<GetAllRecentLoginUsernameOnMobileInteractor>(),
MockSpec<DNSLookupToGetJmapUrlInteractor>(),
])
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<GetSessionInteractor>(mockGetSessionInteractor);
Get.put<GetAuthenticatedAccountInteractor>(mockGetAuthenticatedAccountInteractor);
Get.put<UpdateAuthenticationAccountInteractor>(mockUpdateAuthenticationAccountInteractor);
Get.put<CachingManager>(mockCachingManager);
Get.put<LanguageCacheManager>(mockLanguageCacheManager);
Get.put<AuthorizationInterceptors>(mockAuthorizationInterceptors);
Get.put<AuthorizationInterceptors>(
mockAuthorizationInterceptors,
tag: BindingTag.isolateTag,
);
Get.put<DynamicUrlInterceptors>(mockDynamicUrlInterceptors);
Get.put<DeleteCredentialInteractor>(mockDeleteCredentialInteractor);
Get.put<LogoutOidcInteractor>(mockLogoutOidcInteractor);
Get.put<DeleteAuthorityOidcInteractor>(mockDeleteAuthorityOidcInteractor);
Get.put<AppToast>(mockAppToast);
Get.put<ImagePaths>(mockImagePaths);
Get.put<ResponsiveUtils>(mockResponsiveUtils);
Get.put<Uuid>(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));
});
});
}