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 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/oidc_error.dart';
|
||||
import 'package:tmail_ui_user/main/utils/app_config.dart';
|
||||
import 'package:dio/dio.dart' show DioError;
|
||||
|
||||
class OIDCHttpClient {
|
||||
|
||||
@@ -21,24 +22,29 @@ class OIDCHttpClient {
|
||||
OIDCHttpClient(this._dioClient);
|
||||
|
||||
Future<OIDCResponse> checkOIDCIsAvailable(OIDCRequest oidcRequest) async {
|
||||
final result = await _dioClient.get(
|
||||
try {
|
||||
final result = await _dioClient.get(
|
||||
Endpoint.webFinger
|
||||
.generateOIDCPath(Uri.parse(oidcRequest.baseUrl))
|
||||
.withQueryParameters([
|
||||
StringQueryParameter('resource', oidcRequest.resourceUrl),
|
||||
StringQueryParameter('rel', OIDCRequest.relUrl),
|
||||
])
|
||||
.generateEndpointPath()
|
||||
);
|
||||
log('OIDCHttpClient::checkOIDCIsAvailable(): RESULT: $result');
|
||||
if (result != null) {
|
||||
.generateOIDCPath(Uri.parse(oidcRequest.baseUrl))
|
||||
.withQueryParameters([
|
||||
StringQueryParameter('resource', oidcRequest.resourceUrl),
|
||||
StringQueryParameter('rel', OIDCRequest.relUrl),
|
||||
])
|
||||
.generateEndpointPath()
|
||||
);
|
||||
log('OIDCHttpClient::checkOIDCIsAvailable(): RESULT: $result');
|
||||
if (result is Map<String, dynamic>) {
|
||||
return OIDCResponse.fromJson(result);
|
||||
} else {
|
||||
return OIDCResponse.fromJson(jsonDecode(result));
|
||||
}
|
||||
} else {
|
||||
throw CanNotFoundOIDCLinks();
|
||||
} on DioError catch (exception) {
|
||||
if (exception.response?.statusCode == 404) {
|
||||
throw CanNotFoundOIDCLinks();
|
||||
}
|
||||
throw CanRetryOIDCException();
|
||||
} catch (_) {
|
||||
throw CanRetryOIDCException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,8 +125,9 @@ class LoginController extends ReloadableController {
|
||||
log('LoginController::handleFailureViewState(): $failure');
|
||||
if (failure is GetAuthenticationInfoFailure) {
|
||||
getAuthenticatedAccountAction();
|
||||
} else if (failure is CheckOIDCIsAvailableFailure ||
|
||||
failure is GetStoredOidcConfigurationFailure ||
|
||||
} else if (failure is CheckOIDCIsAvailableFailure) {
|
||||
_handleCheckOIDCIsAvailableFailure(failure);
|
||||
} else if (failure is GetStoredOidcConfigurationFailure ||
|
||||
failure is GetOIDCIsAvailableFailure ||
|
||||
failure is GetOIDCConfigurationFailure
|
||||
) {
|
||||
@@ -174,8 +175,9 @@ class LoginController extends ReloadableController {
|
||||
@override
|
||||
void handleUrgentException({Failure? failure, Exception? exception}) {
|
||||
logError('LoginController::handleUrgentException:Exception: $exception | Failure: $failure');
|
||||
if (failure is CheckOIDCIsAvailableFailure ||
|
||||
failure is GetStoredOidcConfigurationFailure ||
|
||||
if (failure is CheckOIDCIsAvailableFailure) {
|
||||
_handleCheckOIDCIsAvailableFailure(failure);
|
||||
} else if (failure is GetStoredOidcConfigurationFailure ||
|
||||
failure is GetOIDCConfigurationFailure ||
|
||||
failure is GetOIDCIsAvailableFailure) {
|
||||
_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() {
|
||||
consumeState(_getAuthenticationInfoInteractor.execute());
|
||||
}
|
||||
@@ -453,6 +472,9 @@ class LoginController extends ReloadableController {
|
||||
} else {
|
||||
_username = UserName(value);
|
||||
}
|
||||
if (loginFormType.value == LoginFormType.retry && PlatformInfo.isMobile) {
|
||||
loginFormType.value = LoginFormType.dnsLookupForm;
|
||||
}
|
||||
}
|
||||
|
||||
void onPasswordChange(String value) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
enum LoginFormType {
|
||||
none,
|
||||
retry,
|
||||
baseUrlForm,
|
||||
credentialForm,
|
||||
passwordForm,
|
||||
|
||||
@@ -88,6 +88,7 @@ class LoginView extends BaseLoginView {
|
||||
Obx(() {
|
||||
switch (controller.loginFormType.value) {
|
||||
case LoginFormType.dnsLookupForm:
|
||||
case LoginFormType.retry:
|
||||
return DNSLookupInputForm(
|
||||
textEditingController: controller.usernameInputController,
|
||||
onTextChange: controller.onUsernameChange,
|
||||
@@ -174,6 +175,9 @@ class LoginView extends BaseLoginView {
|
||||
style: const TextStyle(fontSize: 16, color: Colors.white)
|
||||
),
|
||||
onPressed: () {
|
||||
if (controller.loginFormType.value == LoginFormType.retry) {
|
||||
controller.loginFormType.value = LoginFormType.dnsLookupForm;
|
||||
}
|
||||
if (controller.loginFormType.value == LoginFormType.dnsLookupForm) {
|
||||
controller.invokeDNSLookupToGetJmapUrl();
|
||||
} else {
|
||||
@@ -206,6 +210,7 @@ class LoginView extends BaseLoginView {
|
||||
switch (controller.loginFormType.value) {
|
||||
case LoginFormType.dnsLookupForm:
|
||||
case LoginFormType.baseUrlForm:
|
||||
case LoginFormType.retry:
|
||||
return _buildNextButtonInContext(context);
|
||||
case LoginFormType.passwordForm:
|
||||
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/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/try_again_button.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
|
||||
class LoginView extends BaseLoginView {
|
||||
@@ -57,6 +58,11 @@ class LoginView extends BaseLoginView {
|
||||
switch (controller.loginFormType.value) {
|
||||
case LoginFormType.credentialForm:
|
||||
return buildInputCredentialForm(context);
|
||||
case LoginFormType.retry:
|
||||
return TryAgainButton(
|
||||
onRetry: controller.retryCheckOidc,
|
||||
responsiveUtils: controller.responsiveUtils,
|
||||
);
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
@@ -199,6 +205,11 @@ class LoginView extends BaseLoginView {
|
||||
switch (controller.loginFormType.value) {
|
||||
case LoginFormType.credentialForm:
|
||||
return buildInputCredentialForm(context);
|
||||
case LoginFormType.retry:
|
||||
return TryAgainButton(
|
||||
onRetry: controller.retryCheckOidc,
|
||||
responsiveUtils: controller.responsiveUtils,
|
||||
);
|
||||
default:
|
||||
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",
|
||||
"placeholders_order": [],
|
||||
"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.',
|
||||
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;
|
||||
} else if (exception is CanNotFoundOIDCLinks) {
|
||||
return AppLocalizations.of(context).ssoNotAvailable;
|
||||
} else if (exception is CanNotFoundToken) {
|
||||
} else if (exception is CanNotFindToken) {
|
||||
return AppLocalizations.of(context).canNotGetToken;
|
||||
} else if (exception is ConnectionTimeout || exception is BadGateway || exception is SocketError) {
|
||||
return AppLocalizations.of(context).wrongUrlMessage;
|
||||
@@ -40,6 +40,8 @@ class ToastManager {
|
||||
return '[${exception.code ?? ''}] ${exception.message}';
|
||||
} else if (exception is NotFoundSessionException) {
|
||||
return AppLocalizations.of(context).notFoundSession;
|
||||
} else if (exception is NoNetworkError) {
|
||||
return AppLocalizations.of(context).youAreOffline;
|
||||
} else {
|
||||
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