Skip to content

Commit

Permalink
TF-2965 Add retry capability for OIDC check request
Browse files Browse the repository at this point in the history
  • Loading branch information
tddang-linagora authored and hoangdat committed Sep 17, 2024
1 parent 2739a28 commit 0d4c266
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 18 deletions.
4 changes: 3 additions & 1 deletion lib/features/login/data/network/oidc_error.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ class CanNotFoundOIDCAuthority implements Exception {}

class CanNotFoundOIDCLinks implements Exception {}

class CanNotFoundToken implements Exception {}
class CanNotFindToken implements Exception {}

class CanRetryOIDCException implements Exception {}
30 changes: 18 additions & 12 deletions lib/features/login/data/network/oidc_http_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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();
}
}

Expand Down
30 changes: 26 additions & 4 deletions lib/features/login/presentation/login_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
Expand Down Expand Up @@ -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();
Expand All @@ -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());
}
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions lib/features/login/presentation/login_form_type.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
enum LoginFormType {
none,
retry,
baseUrlForm,
credentialForm,
passwordForm,
Expand Down
5 changes: 5 additions & 0 deletions lib/features/login/presentation/login_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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:
Expand Down
11 changes: 11 additions & 0 deletions lib/features/login/presentation/login_view_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
}
Expand Down
31 changes: 31 additions & 0 deletions lib/features/login/presentation/widgets/try_again_button.dart
Original file line number Diff line number Diff line change
@@ -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),
);
}
}
12 changes: 12 additions & 0 deletions lib/l10n/intl_messages.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
}
14 changes: 14 additions & 0 deletions lib/main/localizations/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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',
);
}
}
4 changes: 3 additions & 1 deletion lib/main/utils/toast_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down
68 changes: 68 additions & 0 deletions test/features/login/data/network/oidc_http_client_test.dart
Original file line number Diff line number Diff line change
@@ -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>()));
});
});
}

0 comments on commit 0d4c266

Please sign in to comment.