TF-2965 Add retry capability for OIDC check request
This commit is contained in:
@@ -2,4 +2,6 @@ class CanNotFoundOIDCAuthority implements Exception {}
|
|||||||
|
|
||||||
class CanNotFoundOIDCLinks implements Exception {}
|
class CanNotFoundOIDCLinks implements Exception {}
|
||||||
|
|
||||||
class CanNotFoundToken implements Exception {}
|
class CanNotFindToken implements Exception {}
|
||||||
|
|
||||||
|
class CanRetryOIDCException implements Exception {}
|
||||||
@@ -13,6 +13,7 @@ import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.d
|
|||||||
import 'package:tmail_ui_user/features/login/data/network/endpoint.dart';
|
import 'package:tmail_ui_user/features/login/data/network/endpoint.dart';
|
||||||
import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart';
|
import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart';
|
||||||
import 'package:tmail_ui_user/main/utils/app_config.dart';
|
import 'package:tmail_ui_user/main/utils/app_config.dart';
|
||||||
|
import 'package:dio/dio.dart' show DioError;
|
||||||
|
|
||||||
class OIDCHttpClient {
|
class OIDCHttpClient {
|
||||||
|
|
||||||
@@ -21,24 +22,29 @@ class OIDCHttpClient {
|
|||||||
OIDCHttpClient(this._dioClient);
|
OIDCHttpClient(this._dioClient);
|
||||||
|
|
||||||
Future<OIDCResponse> checkOIDCIsAvailable(OIDCRequest oidcRequest) async {
|
Future<OIDCResponse> checkOIDCIsAvailable(OIDCRequest oidcRequest) async {
|
||||||
final result = await _dioClient.get(
|
try {
|
||||||
|
final result = await _dioClient.get(
|
||||||
Endpoint.webFinger
|
Endpoint.webFinger
|
||||||
.generateOIDCPath(Uri.parse(oidcRequest.baseUrl))
|
.generateOIDCPath(Uri.parse(oidcRequest.baseUrl))
|
||||||
.withQueryParameters([
|
.withQueryParameters([
|
||||||
StringQueryParameter('resource', oidcRequest.resourceUrl),
|
StringQueryParameter('resource', oidcRequest.resourceUrl),
|
||||||
StringQueryParameter('rel', OIDCRequest.relUrl),
|
StringQueryParameter('rel', OIDCRequest.relUrl),
|
||||||
])
|
])
|
||||||
.generateEndpointPath()
|
.generateEndpointPath()
|
||||||
);
|
);
|
||||||
log('OIDCHttpClient::checkOIDCIsAvailable(): RESULT: $result');
|
log('OIDCHttpClient::checkOIDCIsAvailable(): RESULT: $result');
|
||||||
if (result != null) {
|
|
||||||
if (result is Map<String, dynamic>) {
|
if (result is Map<String, dynamic>) {
|
||||||
return OIDCResponse.fromJson(result);
|
return OIDCResponse.fromJson(result);
|
||||||
} else {
|
} else {
|
||||||
return OIDCResponse.fromJson(jsonDecode(result));
|
return OIDCResponse.fromJson(jsonDecode(result));
|
||||||
}
|
}
|
||||||
} else {
|
} on DioError catch (exception) {
|
||||||
throw CanNotFoundOIDCLinks();
|
if (exception.response?.statusCode == 404) {
|
||||||
|
throw CanNotFoundOIDCLinks();
|
||||||
|
}
|
||||||
|
throw CanRetryOIDCException();
|
||||||
|
} catch (_) {
|
||||||
|
throw CanRetryOIDCException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,8 +125,9 @@ class LoginController extends ReloadableController {
|
|||||||
log('LoginController::handleFailureViewState(): $failure');
|
log('LoginController::handleFailureViewState(): $failure');
|
||||||
if (failure is GetAuthenticationInfoFailure) {
|
if (failure is GetAuthenticationInfoFailure) {
|
||||||
getAuthenticatedAccountAction();
|
getAuthenticatedAccountAction();
|
||||||
} else if (failure is CheckOIDCIsAvailableFailure ||
|
} else if (failure is CheckOIDCIsAvailableFailure) {
|
||||||
failure is GetStoredOidcConfigurationFailure ||
|
_handleCheckOIDCIsAvailableFailure(failure);
|
||||||
|
} else if (failure is GetStoredOidcConfigurationFailure ||
|
||||||
failure is GetOIDCIsAvailableFailure ||
|
failure is GetOIDCIsAvailableFailure ||
|
||||||
failure is GetOIDCConfigurationFailure
|
failure is GetOIDCConfigurationFailure
|
||||||
) {
|
) {
|
||||||
@@ -174,8 +175,9 @@ class LoginController extends ReloadableController {
|
|||||||
@override
|
@override
|
||||||
void handleUrgentException({Failure? failure, Exception? exception}) {
|
void handleUrgentException({Failure? failure, Exception? exception}) {
|
||||||
logError('LoginController::handleUrgentException:Exception: $exception | Failure: $failure');
|
logError('LoginController::handleUrgentException:Exception: $exception | Failure: $failure');
|
||||||
if (failure is CheckOIDCIsAvailableFailure ||
|
if (failure is CheckOIDCIsAvailableFailure) {
|
||||||
failure is GetStoredOidcConfigurationFailure ||
|
_handleCheckOIDCIsAvailableFailure(failure);
|
||||||
|
} else if (failure is GetStoredOidcConfigurationFailure ||
|
||||||
failure is GetOIDCConfigurationFailure ||
|
failure is GetOIDCConfigurationFailure ||
|
||||||
failure is GetOIDCIsAvailableFailure) {
|
failure is GetOIDCIsAvailableFailure) {
|
||||||
_handleCommonOIDCFailure();
|
_handleCommonOIDCFailure();
|
||||||
@@ -197,6 +199,23 @@ class LoginController extends ReloadableController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleCheckOIDCIsAvailableFailure(CheckOIDCIsAvailableFailure failure) {
|
||||||
|
if (failure.exception is CanNotFoundOIDCLinks) {
|
||||||
|
_handleCommonOIDCFailure();
|
||||||
|
} else {
|
||||||
|
loginFormType.value = LoginFormType.retry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void retryCheckOidc() {
|
||||||
|
if (PlatformInfo.isMobile) {
|
||||||
|
loginFormType.value = LoginFormType.dnsLookupForm;
|
||||||
|
} else {
|
||||||
|
loginFormType.value = LoginFormType.none;
|
||||||
|
}
|
||||||
|
_checkOIDCIsAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
void _getAuthenticationInfo() {
|
void _getAuthenticationInfo() {
|
||||||
consumeState(_getAuthenticationInfoInteractor.execute());
|
consumeState(_getAuthenticationInfoInteractor.execute());
|
||||||
}
|
}
|
||||||
@@ -453,6 +472,9 @@ class LoginController extends ReloadableController {
|
|||||||
} else {
|
} else {
|
||||||
_username = UserName(value);
|
_username = UserName(value);
|
||||||
}
|
}
|
||||||
|
if (loginFormType.value == LoginFormType.retry && PlatformInfo.isMobile) {
|
||||||
|
loginFormType.value = LoginFormType.dnsLookupForm;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onPasswordChange(String value) {
|
void onPasswordChange(String value) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
enum LoginFormType {
|
enum LoginFormType {
|
||||||
none,
|
none,
|
||||||
|
retry,
|
||||||
baseUrlForm,
|
baseUrlForm,
|
||||||
credentialForm,
|
credentialForm,
|
||||||
passwordForm,
|
passwordForm,
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ class LoginView extends BaseLoginView {
|
|||||||
Obx(() {
|
Obx(() {
|
||||||
switch (controller.loginFormType.value) {
|
switch (controller.loginFormType.value) {
|
||||||
case LoginFormType.dnsLookupForm:
|
case LoginFormType.dnsLookupForm:
|
||||||
|
case LoginFormType.retry:
|
||||||
return DNSLookupInputForm(
|
return DNSLookupInputForm(
|
||||||
textEditingController: controller.usernameInputController,
|
textEditingController: controller.usernameInputController,
|
||||||
onTextChange: controller.onUsernameChange,
|
onTextChange: controller.onUsernameChange,
|
||||||
@@ -174,6 +175,9 @@ class LoginView extends BaseLoginView {
|
|||||||
style: const TextStyle(fontSize: 16, color: Colors.white)
|
style: const TextStyle(fontSize: 16, color: Colors.white)
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (controller.loginFormType.value == LoginFormType.retry) {
|
||||||
|
controller.loginFormType.value = LoginFormType.dnsLookupForm;
|
||||||
|
}
|
||||||
if (controller.loginFormType.value == LoginFormType.dnsLookupForm) {
|
if (controller.loginFormType.value == LoginFormType.dnsLookupForm) {
|
||||||
controller.invokeDNSLookupToGetJmapUrl();
|
controller.invokeDNSLookupToGetJmapUrl();
|
||||||
} else {
|
} else {
|
||||||
@@ -206,6 +210,7 @@ class LoginView extends BaseLoginView {
|
|||||||
switch (controller.loginFormType.value) {
|
switch (controller.loginFormType.value) {
|
||||||
case LoginFormType.dnsLookupForm:
|
case LoginFormType.dnsLookupForm:
|
||||||
case LoginFormType.baseUrlForm:
|
case LoginFormType.baseUrlForm:
|
||||||
|
case LoginFormType.retry:
|
||||||
return _buildNextButtonInContext(context);
|
return _buildNextButtonInContext(context);
|
||||||
case LoginFormType.passwordForm:
|
case LoginFormType.passwordForm:
|
||||||
case LoginFormType.credentialForm:
|
case LoginFormType.credentialForm:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:tmail_ui_user/features/login/presentation/base_login_view.dart';
|
|||||||
import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart';
|
import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart';
|
||||||
import 'package:tmail_ui_user/features/login/presentation/privacy_link_widget.dart';
|
import 'package:tmail_ui_user/features/login/presentation/privacy_link_widget.dart';
|
||||||
import 'package:tmail_ui_user/features/login/presentation/widgets/login_message_widget.dart';
|
import 'package:tmail_ui_user/features/login/presentation/widgets/login_message_widget.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/presentation/widgets/try_again_button.dart';
|
||||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||||
|
|
||||||
class LoginView extends BaseLoginView {
|
class LoginView extends BaseLoginView {
|
||||||
@@ -57,6 +58,11 @@ class LoginView extends BaseLoginView {
|
|||||||
switch (controller.loginFormType.value) {
|
switch (controller.loginFormType.value) {
|
||||||
case LoginFormType.credentialForm:
|
case LoginFormType.credentialForm:
|
||||||
return buildInputCredentialForm(context);
|
return buildInputCredentialForm(context);
|
||||||
|
case LoginFormType.retry:
|
||||||
|
return TryAgainButton(
|
||||||
|
onRetry: controller.retryCheckOidc,
|
||||||
|
responsiveUtils: controller.responsiveUtils,
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
@@ -199,6 +205,11 @@ class LoginView extends BaseLoginView {
|
|||||||
switch (controller.loginFormType.value) {
|
switch (controller.loginFormType.value) {
|
||||||
case LoginFormType.credentialForm:
|
case LoginFormType.credentialForm:
|
||||||
return buildInputCredentialForm(context);
|
return buildInputCredentialForm(context);
|
||||||
|
case LoginFormType.retry:
|
||||||
|
return TryAgainButton(
|
||||||
|
onRetry: controller.retryCheckOidc,
|
||||||
|
responsiveUtils: controller.responsiveUtils,
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:core/presentation/extensions/color_extension.dart';
|
||||||
|
import 'package:core/presentation/utils/responsive_utils.dart';
|
||||||
|
import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||||
|
|
||||||
|
class TryAgainButton extends StatelessWidget {
|
||||||
|
const TryAgainButton({
|
||||||
|
super.key,
|
||||||
|
required this.onRetry,
|
||||||
|
required this.responsiveUtils,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback onRetry;
|
||||||
|
final ResponsiveUtils responsiveUtils;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TMailButtonWidget.fromText(
|
||||||
|
text: AppLocalizations.of(context).tryAgain,
|
||||||
|
textStyle: const TextStyle(fontSize: 16, color: Colors.white),
|
||||||
|
backgroundColor: AppColor.primaryColor,
|
||||||
|
onTapActionCallback: onRetry,
|
||||||
|
borderRadius: 10,
|
||||||
|
margin: const EdgeInsetsDirectional.only(bottom: 16, start: 24, end: 24),
|
||||||
|
width: responsiveUtils.getDeviceWidth(context),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4005,5 +4005,17 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders_order": [],
|
"placeholders_order": [],
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"tryAgain": "Try again",
|
||||||
|
"@tryAgain": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders_order": [],
|
||||||
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"youAreOffline": "You are offline. It looks like you are not connected.",
|
||||||
|
"@youAreOffline": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders_order": [],
|
||||||
|
"placeholders": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4199,4 +4199,18 @@ class AppLocalizations {
|
|||||||
'You have not selected any action for the rule.',
|
'You have not selected any action for the rule.',
|
||||||
name: 'youHaveNotSelectedAnyActionForRule');
|
name: 'youHaveNotSelectedAnyActionForRule');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get tryAgain {
|
||||||
|
return Intl.message(
|
||||||
|
'Try again',
|
||||||
|
name: 'tryAgain',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get youAreOffline {
|
||||||
|
return Intl.message(
|
||||||
|
'You are offline. It looks like you are not connected.',
|
||||||
|
name: 'youAreOffline',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ class ToastManager {
|
|||||||
return AppLocalizations.of(context).requiredPassword;
|
return AppLocalizations.of(context).requiredPassword;
|
||||||
} else if (exception is CanNotFoundOIDCLinks) {
|
} else if (exception is CanNotFoundOIDCLinks) {
|
||||||
return AppLocalizations.of(context).ssoNotAvailable;
|
return AppLocalizations.of(context).ssoNotAvailable;
|
||||||
} else if (exception is CanNotFoundToken) {
|
} else if (exception is CanNotFindToken) {
|
||||||
return AppLocalizations.of(context).canNotGetToken;
|
return AppLocalizations.of(context).canNotGetToken;
|
||||||
} else if (exception is ConnectionTimeout || exception is BadGateway || exception is SocketError) {
|
} else if (exception is ConnectionTimeout || exception is BadGateway || exception is SocketError) {
|
||||||
return AppLocalizations.of(context).wrongUrlMessage;
|
return AppLocalizations.of(context).wrongUrlMessage;
|
||||||
@@ -40,6 +40,8 @@ class ToastManager {
|
|||||||
return '[${exception.code ?? ''}] ${exception.message}';
|
return '[${exception.code ?? ''}] ${exception.message}';
|
||||||
} else if (exception is NotFoundSessionException) {
|
} else if (exception is NotFoundSessionException) {
|
||||||
return AppLocalizations.of(context).notFoundSession;
|
return AppLocalizations.of(context).notFoundSession;
|
||||||
|
} else if (exception is NoNetworkError) {
|
||||||
|
return AppLocalizations.of(context).youAreOffline;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import 'package:core/data/network/dio_client.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'package:model/oidc/request/oidc_request.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart';
|
||||||
|
|
||||||
|
import 'oidc_http_client_test.mocks.dart';
|
||||||
|
|
||||||
|
@GenerateNiceMocks([MockSpec<DioClient>()])
|
||||||
|
void main() {
|
||||||
|
final dioClient = MockDioClient();
|
||||||
|
final oidcHttpClient = OIDCHttpClient(dioClient);
|
||||||
|
final requestOptions = RequestOptions();
|
||||||
|
final oidcRequest = OIDCRequest(baseUrl: '', resourceUrl: '');
|
||||||
|
|
||||||
|
group('oidc http client test:', () {
|
||||||
|
test(
|
||||||
|
'should throw CanNotFoundOIDCLinks '
|
||||||
|
'when checkOIDCIsAvailable() is called '
|
||||||
|
'and dioClient throw DioError '
|
||||||
|
'and status code is 404',
|
||||||
|
() {
|
||||||
|
// arrange
|
||||||
|
when(dioClient.get(any)).thenThrow(DioError(
|
||||||
|
requestOptions: requestOptions,
|
||||||
|
response: Response(requestOptions: requestOptions, statusCode: 404)));
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(
|
||||||
|
() => oidcHttpClient.checkOIDCIsAvailable(oidcRequest),
|
||||||
|
throwsA(isA<CanNotFoundOIDCLinks>()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'should throw CanRetryOIDCException '
|
||||||
|
'when checkOIDCIsAvailable() is called '
|
||||||
|
'and dioClient throw DioError '
|
||||||
|
'and status code is not 404',
|
||||||
|
() {
|
||||||
|
// arrange
|
||||||
|
when(dioClient.get(any)).thenThrow(DioError(
|
||||||
|
requestOptions: requestOptions,
|
||||||
|
response: Response(requestOptions: requestOptions, statusCode: 403)));
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(
|
||||||
|
() => oidcHttpClient.checkOIDCIsAvailable(oidcRequest),
|
||||||
|
throwsA(isA<CanRetryOIDCException>()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'should throw CanRetryOIDCException '
|
||||||
|
'when checkOIDCIsAvailable() is called '
|
||||||
|
'and dioClient throw exception that is not DioError',
|
||||||
|
() {
|
||||||
|
// arrange
|
||||||
|
when(dioClient.get(any)).thenThrow(Exception());
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(
|
||||||
|
() => oidcHttpClient.checkOIDCIsAvailable(oidcRequest),
|
||||||
|
throwsA(isA<CanRetryOIDCException>()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user