Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TF-2965 Add retry capability to OIDC check request #3143

Merged
merged 1 commit into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
hoangdat marked this conversation as resolved.
Show resolved Hide resolved
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>()));
});
});
}
Loading