TF-3826 Try guessing webfinger when dns look up fail
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
# 66. Robust OIDC Guessing
|
||||||
|
|
||||||
|
Date: 2025-06-27
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
- The users are forced to manually enter the server url when the app fails to lookup the oidc configuration
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
- The app will try to guess the oidc url based on the email address with common prefixes
|
||||||
|
- `email.domain`
|
||||||
|
- `jmap.email.domain`
|
||||||
|
- `autodiscover.email.domain`
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The oidc discovery is more robust
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
|
||||||
|
|
||||||
|
enum OidcGuessingOrigin {
|
||||||
|
empty(origin: ''),
|
||||||
|
autoDiscover(origin: 'autodiscover'),
|
||||||
|
jmap(origin: 'jmap');
|
||||||
|
|
||||||
|
const OidcGuessingOrigin({required this.origin});
|
||||||
|
|
||||||
|
final String origin;
|
||||||
|
|
||||||
|
String url(String email) {
|
||||||
|
if (!EmailUtils.isEmailAddressValid(email)) {
|
||||||
|
throw ArgumentError('Invalid email address: $email');
|
||||||
|
}
|
||||||
|
final emailDomain = email.split('@').last;
|
||||||
|
return switch (this) {
|
||||||
|
OidcGuessingOrigin.empty => 'https://$emailDomain',
|
||||||
|
_ => origin.trim().isEmpty
|
||||||
|
? OidcGuessingOrigin.empty.url(email)
|
||||||
|
: 'https://$origin.$emailDomain',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,10 @@ class DNSLookupToGetJmapUrlSuccess extends UIState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DNSLookupToGetJmapUrlFailure extends FeatureFailure {
|
class DNSLookupToGetJmapUrlFailure extends FeatureFailure {
|
||||||
|
DNSLookupToGetJmapUrlFailure(
|
||||||
|
dynamic exception, {
|
||||||
|
required this.email,
|
||||||
|
}) : super(exception: exception);
|
||||||
|
|
||||||
DNSLookupToGetJmapUrlFailure(dynamic exception) : super(exception: exception);
|
final String email;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:core/presentation/state/failure.dart';
|
||||||
|
import 'package:core/presentation/state/success.dart';
|
||||||
|
import 'package:model/oidc/response/oidc_response.dart';
|
||||||
|
|
||||||
|
class TryingGuessingWebFinger extends LoadingState {}
|
||||||
|
|
||||||
|
class TryGuessingWebFingerSuccess extends UIState {
|
||||||
|
TryGuessingWebFingerSuccess(this.oidcResponse);
|
||||||
|
|
||||||
|
final OIDCResponse oidcResponse;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [oidcResponse];
|
||||||
|
}
|
||||||
|
|
||||||
|
class TryGuessingWebFingerFailure extends FeatureFailure {
|
||||||
|
TryGuessingWebFingerFailure({super.exception});
|
||||||
|
}
|
||||||
@@ -15,7 +15,10 @@ class DNSLookupToGetJmapUrlInteractor {
|
|||||||
final jmapUrl = await _loginRepository.dnsLookupToGetJmapUrl(emailAddress);
|
final jmapUrl = await _loginRepository.dnsLookupToGetJmapUrl(emailAddress);
|
||||||
yield Right<Failure, Success>(DNSLookupToGetJmapUrlSuccess(jmapUrl));
|
yield Right<Failure, Success>(DNSLookupToGetJmapUrlSuccess(jmapUrl));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
yield Left<Failure, Success>(DNSLookupToGetJmapUrlFailure(e));
|
yield Left<Failure, Success>(DNSLookupToGetJmapUrlFailure(
|
||||||
|
e,
|
||||||
|
email: emailAddress,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
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:model/oidc/request/oidc_request.dart';
|
||||||
|
import 'package:model/oidc/response/oidc_response.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/domain/state/try_guessing_web_finger_state.dart';
|
||||||
|
|
||||||
|
class TryGuessingWebFingerInteractor {
|
||||||
|
const TryGuessingWebFingerInteractor(this._authenticationOIDCRepository);
|
||||||
|
|
||||||
|
final AuthenticationOIDCRepository _authenticationOIDCRepository;
|
||||||
|
|
||||||
|
Stream<Either<Failure, Success>> execute(
|
||||||
|
List<OIDCRequest> oidcRequests,
|
||||||
|
) async* {
|
||||||
|
try {
|
||||||
|
yield Right(TryingGuessingWebFinger());
|
||||||
|
final futures = oidcRequests.map(_checkOIDCAvailableFromOidcRequest).toList();
|
||||||
|
final completer = Completer<OIDCResponse?>();
|
||||||
|
|
||||||
|
for (final future in futures) {
|
||||||
|
future.then((response) {
|
||||||
|
if (response != null && !completer.isCompleted) {
|
||||||
|
completer.complete(response);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future.wait(futures).then((_) {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final firstNonNullResponse = await completer.future;
|
||||||
|
|
||||||
|
if (firstNonNullResponse == null) {
|
||||||
|
yield Left(TryGuessingWebFingerFailure());
|
||||||
|
} else {
|
||||||
|
yield Right(TryGuessingWebFingerSuccess(firstNonNullResponse));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logError('$runtimeType::execute(): Exception = $e');
|
||||||
|
yield Left(TryGuessingWebFingerFailure(exception: e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<OIDCResponse?> _checkOIDCAvailableFromOidcRequest(
|
||||||
|
OIDCRequest oidcRequest,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
return await _authenticationOIDCRepository.checkOIDCIsAvailable(
|
||||||
|
oidcRequest,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:model/oidc/request/oidc_request.dart';
|
||||||
|
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/domain/model/oidc_guessing_origin.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/presentation/login_controller.dart';
|
||||||
|
|
||||||
|
extension GenerateOidcGuessingUrls on LoginController {
|
||||||
|
List<OIDCRequest> generateOidcGuessingUrls(String email) {
|
||||||
|
if (!EmailUtils.isEmailAddressValid(email)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return OidcGuessingOrigin.values.map(
|
||||||
|
(guessingOrigin) => OIDCRequest(
|
||||||
|
baseUrl: guessingOrigin.url(email),
|
||||||
|
resourceUrl: guessingOrigin.url(email)
|
||||||
|
)
|
||||||
|
).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_oidc_con
|
|||||||
import 'package:tmail_ui_user/features/login/domain/usecases/get_token_oidc_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_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/save_login_username_on_mobile_interactor.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/domain/usecases/try_guessing_web_finger_interactor.dart';
|
||||||
import 'package:tmail_ui_user/features/login/presentation/login_controller.dart';
|
import 'package:tmail_ui_user/features/login/presentation/login_controller.dart';
|
||||||
import 'package:tmail_ui_user/features/starting_page/data/datasource/saas_authentication_datasource.dart';
|
import 'package:tmail_ui_user/features/starting_page/data/datasource/saas_authentication_datasource.dart';
|
||||||
import 'package:tmail_ui_user/features/starting_page/data/datasource_impl/saas_authentication_datasource_impl.dart';
|
import 'package:tmail_ui_user/features/starting_page/data/datasource_impl/saas_authentication_datasource_impl.dart';
|
||||||
@@ -52,6 +53,7 @@ class LoginBindings extends BaseBindings {
|
|||||||
Get.find<GetAllRecentLoginUsernameOnMobileInteractor>(),
|
Get.find<GetAllRecentLoginUsernameOnMobileInteractor>(),
|
||||||
Get.find<DNSLookupToGetJmapUrlInteractor>(),
|
Get.find<DNSLookupToGetJmapUrlInteractor>(),
|
||||||
Get.find<SignInTwakeWorkplaceInteractor>(),
|
Get.find<SignInTwakeWorkplaceInteractor>(),
|
||||||
|
Get.find<TryGuessingWebFingerInteractor>(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +114,9 @@ class LoginBindings extends BaseBindings {
|
|||||||
Get.find<AccountRepository>(),
|
Get.find<AccountRepository>(),
|
||||||
Get.find<CredentialRepository>(),
|
Get.find<CredentialRepository>(),
|
||||||
));
|
));
|
||||||
|
Get.lazyPut(() => TryGuessingWebFingerInteractor(
|
||||||
|
Get.find<AuthenticationOIDCRepository>(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import 'package:tmail_ui_user/features/login/domain/state/get_authentication_inf
|
|||||||
import 'package:tmail_ui_user/features/login/domain/state/get_oidc_configuration_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_stored_oidc_configuration_state.dart';
|
import 'package:tmail_ui_user/features/login/domain/state/get_stored_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/domain/state/get_token_oidc_state.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/domain/state/try_guessing_web_finger_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/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/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/check_oidc_is_available_interactor.dart';
|
||||||
@@ -50,6 +51,8 @@ import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_oidc_con
|
|||||||
import 'package:tmail_ui_user/features/login/domain/usecases/get_token_oidc_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_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/save_login_username_on_mobile_interactor.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/domain/usecases/try_guessing_web_finger_interactor.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/presentation/extensions/generate_oidc_guessing_urls.dart';
|
||||||
import 'package:tmail_ui_user/features/login/presentation/extensions/handle_openid_configuration.dart';
|
import 'package:tmail_ui_user/features/login/presentation/extensions/handle_openid_configuration.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/model/login_arguments.dart';
|
import 'package:tmail_ui_user/features/login/presentation/model/login_arguments.dart';
|
||||||
@@ -58,6 +61,7 @@ import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_in_twak
|
|||||||
import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart';
|
import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart';
|
||||||
import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart';
|
import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart';
|
||||||
import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_data.dart';
|
import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_data.dart';
|
||||||
|
import 'package:tmail_ui_user/main/exceptions/remote_exception.dart';
|
||||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||||
import 'package:tmail_ui_user/main/routes/app_routes.dart';
|
import 'package:tmail_ui_user/main/routes/app_routes.dart';
|
||||||
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
||||||
@@ -80,6 +84,7 @@ class LoginController extends ReloadableController {
|
|||||||
final GetAllRecentLoginUsernameOnMobileInteractor _getAllRecentLoginUsernameOnMobileInteractor;
|
final GetAllRecentLoginUsernameOnMobileInteractor _getAllRecentLoginUsernameOnMobileInteractor;
|
||||||
final DNSLookupToGetJmapUrlInteractor _dnsLookupToGetJmapUrlInteractor;
|
final DNSLookupToGetJmapUrlInteractor _dnsLookupToGetJmapUrlInteractor;
|
||||||
final SignInTwakeWorkplaceInteractor _signInTwakeWorkplaceInteractor;
|
final SignInTwakeWorkplaceInteractor _signInTwakeWorkplaceInteractor;
|
||||||
|
final TryGuessingWebFingerInteractor _tryGuessingWebFingerInteractor;
|
||||||
|
|
||||||
final TextEditingController urlInputController = TextEditingController();
|
final TextEditingController urlInputController = TextEditingController();
|
||||||
final TextEditingController usernameInputController = TextEditingController();
|
final TextEditingController usernameInputController = TextEditingController();
|
||||||
@@ -111,6 +116,7 @@ class LoginController extends ReloadableController {
|
|||||||
this._getAllRecentLoginUsernameOnMobileInteractor,
|
this._getAllRecentLoginUsernameOnMobileInteractor,
|
||||||
this._dnsLookupToGetJmapUrlInteractor,
|
this._dnsLookupToGetJmapUrlInteractor,
|
||||||
this._signInTwakeWorkplaceInteractor,
|
this._signInTwakeWorkplaceInteractor,
|
||||||
|
this._tryGuessingWebFingerInteractor,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -164,6 +170,8 @@ class LoginController extends ReloadableController {
|
|||||||
SmartDialog.dismiss();
|
SmartDialog.dismiss();
|
||||||
clearAllData();
|
clearAllData();
|
||||||
} else if (failure is DNSLookupToGetJmapUrlFailure) {
|
} else if (failure is DNSLookupToGetJmapUrlFailure) {
|
||||||
|
_handleDNSLookupToGetJmapUrlFailure(failure);
|
||||||
|
} else if (failure is TryGuessingWebFingerFailure) {
|
||||||
_username = null;
|
_username = null;
|
||||||
_clearTextInputField();
|
_clearTextInputField();
|
||||||
_showBaseUrlForm();
|
_showBaseUrlForm();
|
||||||
@@ -188,6 +196,9 @@ class LoginController extends ReloadableController {
|
|||||||
_loginSuccessAction(success);
|
_loginSuccessAction(success);
|
||||||
} else if (success is DNSLookupToGetJmapUrlSuccess) {
|
} else if (success is DNSLookupToGetJmapUrlSuccess) {
|
||||||
_handleDNSLookupToGetJmapUrlSuccess(success);
|
_handleDNSLookupToGetJmapUrlSuccess(success);
|
||||||
|
} else if (success is TryGuessingWebFingerSuccess) {
|
||||||
|
onBaseUrlChange(success.oidcResponse.subject);
|
||||||
|
getOIDCConfiguration(success.oidcResponse);
|
||||||
} else if (success is SignInTwakeWorkplaceSuccess) {
|
} else if (success is SignInTwakeWorkplaceSuccess) {
|
||||||
_synchronizeTokenAndGetSession(
|
_synchronizeTokenAndGetSession(
|
||||||
baseUri: success.baseUri,
|
baseUri: success.baseUri,
|
||||||
@@ -217,6 +228,10 @@ class LoginController extends ReloadableController {
|
|||||||
} else if (failure is GetSessionFailure) {
|
} else if (failure is GetSessionFailure) {
|
||||||
SmartDialog.dismiss();
|
SmartDialog.dismiss();
|
||||||
clearAllData();
|
clearAllData();
|
||||||
|
} else if (connectionErrorWhenLookupDns(failure)) {
|
||||||
|
_handleDNSLookupToGetJmapUrlFailure(
|
||||||
|
failure as DNSLookupToGetJmapUrlFailure,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
super.handleUrgentException(failure: failure, exception: exception);
|
super.handleUrgentException(failure: failure, exception: exception);
|
||||||
}
|
}
|
||||||
@@ -232,6 +247,19 @@ class LoginController extends ReloadableController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool connectionErrorWhenLookupDns(Failure? failure) {
|
||||||
|
return failure is DNSLookupToGetJmapUrlFailure &&
|
||||||
|
failure.exception is ConnectionError;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDNSLookupToGetJmapUrlFailure(
|
||||||
|
DNSLookupToGetJmapUrlFailure failure,
|
||||||
|
) {
|
||||||
|
consumeState(_tryGuessingWebFingerInteractor.execute(
|
||||||
|
generateOidcGuessingUrls(failure.email),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
void _registerDeepLinks() {
|
void _registerDeepLinks() {
|
||||||
_deepLinksManager = getBinding<DeepLinksManager>();
|
_deepLinksManager = getBinding<DeepLinksManager>();
|
||||||
_deepLinksManager?.clearPendingDeepLinkData();
|
_deepLinksManager?.clearPendingDeepLinkData();
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/domain/model/oidc_guessing_origin.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('OidcGuessingOrigin enum', () {
|
||||||
|
test('url() returns correct URL for empty origin', () {
|
||||||
|
const email = 'user@example.com';
|
||||||
|
const origin = OidcGuessingOrigin.empty;
|
||||||
|
expect(origin.url(email), 'https://example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('url() returns correct URL for autoDiscover origin', () {
|
||||||
|
const email = 'user@example.com';
|
||||||
|
const origin = OidcGuessingOrigin.autoDiscover;
|
||||||
|
expect(origin.url(email), 'https://autodiscover.example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('url() returns correct URL for jmap origin', () {
|
||||||
|
const email = 'user@example.com';
|
||||||
|
const origin = OidcGuessingOrigin.jmap;
|
||||||
|
expect(origin.url(email), 'https://jmap.example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('url() throws error for invalid email', () {
|
||||||
|
const email = 'invalid_email@@example.com';
|
||||||
|
const origin = OidcGuessingOrigin.empty;
|
||||||
|
expect(() => origin.url(email), throwsArgumentError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('url() returns correct URL for jmap origin with quoted local part', () {
|
||||||
|
const email = '"user"@example.com';
|
||||||
|
const origin = OidcGuessingOrigin.jmap;
|
||||||
|
expect(origin.url(email), 'https://jmap.example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('url() returns correct URL for jmap origin with quoted local part containing @', () {
|
||||||
|
const email = '"user@local"@example.com';
|
||||||
|
const origin = OidcGuessingOrigin.jmap;
|
||||||
|
expect(origin.url(email), 'https://jmap.example.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockito/annotations.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/domain/usecases/try_guessing_web_finger_interactor.dart';
|
||||||
|
import 'package:core/presentation/state/failure.dart';
|
||||||
|
import 'package:core/presentation/state/success.dart';
|
||||||
|
import 'package:model/oidc/request/oidc_request.dart';
|
||||||
|
import 'package:model/oidc/response/oidc_response.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/domain/state/try_guessing_web_finger_state.dart';
|
||||||
|
|
||||||
|
@GenerateMocks([AuthenticationOIDCRepository])
|
||||||
|
import 'try_guessing_web_finger_interactor_test.mocks.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late MockAuthenticationOIDCRepository mockRepository;
|
||||||
|
late TryGuessingWebFingerInteractor interactor;
|
||||||
|
|
||||||
|
final oidcRequest1 = OIDCRequest(
|
||||||
|
baseUrl: 'https://example1.com',
|
||||||
|
resourceUrl: 'https://example1.com/.well-known/openid-configuration'
|
||||||
|
);
|
||||||
|
final oidcRequest2 = OIDCRequest(
|
||||||
|
baseUrl: 'https://example2.com',
|
||||||
|
resourceUrl: 'https://example2.com/.well-known/openid-configuration'
|
||||||
|
);
|
||||||
|
final successResponse = OIDCResponse('subject', []);
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockRepository = MockAuthenticationOIDCRepository();
|
||||||
|
interactor = TryGuessingWebFingerInteractor(mockRepository);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return first successful response immediately', () async {
|
||||||
|
// Arrange
|
||||||
|
final completer1 = Completer<OIDCResponse>();
|
||||||
|
final completer2 = Completer<OIDCResponse>();
|
||||||
|
|
||||||
|
when(mockRepository.checkOIDCIsAvailable(oidcRequest1))
|
||||||
|
.thenAnswer((_) => completer1.future);
|
||||||
|
when(mockRepository.checkOIDCIsAvailable(oidcRequest2))
|
||||||
|
.thenAnswer((_) => completer2.future);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
final stream = interactor.execute([oidcRequest1, oidcRequest2]);
|
||||||
|
final futureResults = stream.toList();
|
||||||
|
|
||||||
|
completer1.complete(successResponse);
|
||||||
|
await Future.delayed(Duration.zero); // Allow stream to process
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
final results = await futureResults;
|
||||||
|
expect(results, [
|
||||||
|
Right(TryingGuessingWebFinger()),
|
||||||
|
Right(TryGuessingWebFingerSuccess(successResponse)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
verifyInOrder([
|
||||||
|
mockRepository.checkOIDCIsAvailable(oidcRequest1),
|
||||||
|
mockRepository.checkOIDCIsAvailable(oidcRequest2),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify second request was made but not completed
|
||||||
|
expect(completer2.isCompleted, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return failure when all responses are null', () async {
|
||||||
|
// Arrange
|
||||||
|
when(mockRepository.checkOIDCIsAvailable(any))
|
||||||
|
.thenAnswer((_) async => throw Exception());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
final results = await interactor.execute([oidcRequest1, oidcRequest2]).toList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results[0], Right(TryingGuessingWebFinger()));
|
||||||
|
expect(results[1], isA<Left<Failure, Success>>());
|
||||||
|
expect((results[1] as Left<Failure, Success>).value, isA<TryGuessingWebFingerFailure>());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle async response order correctly', () async {
|
||||||
|
// Arrange
|
||||||
|
final completer1 = Completer<OIDCResponse>();
|
||||||
|
final completer2 = Completer<OIDCResponse>();
|
||||||
|
|
||||||
|
when(mockRepository.checkOIDCIsAvailable(oidcRequest1))
|
||||||
|
.thenAnswer((_) => completer1.future);
|
||||||
|
when(mockRepository.checkOIDCIsAvailable(oidcRequest2))
|
||||||
|
.thenAnswer((_) => completer2.future);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
final stream = interactor.execute([oidcRequest1, oidcRequest2]);
|
||||||
|
final futureResults = stream.toList();
|
||||||
|
|
||||||
|
completer2.complete(successResponse);
|
||||||
|
await Future.delayed(Duration.zero);
|
||||||
|
completer1.completeError(Exception('Timeout'));
|
||||||
|
|
||||||
|
final results = await futureResults;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results, [
|
||||||
|
Right(TryingGuessingWebFinger()),
|
||||||
|
Right(TryGuessingWebFingerSuccess(successResponse)),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle exceptions properly', () async {
|
||||||
|
// Arrange
|
||||||
|
when(mockRepository.checkOIDCIsAvailable(any))
|
||||||
|
.thenAnswer((_) => Future<OIDCResponse>.error(Exception('Test error')));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
final results = await interactor.execute([oidcRequest1]).toList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(results[0], Right(TryingGuessingWebFinger()));
|
||||||
|
expect(results[1], isA<Left<Failure, Success>>());
|
||||||
|
expect((results[1] as Left<Failure, Success>).value, isA<TryGuessingWebFingerFailure>());
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_oidc_con
|
|||||||
import 'package:tmail_ui_user/features/login/domain/usecases/get_token_oidc_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_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/save_login_username_on_mobile_interactor.dart';
|
||||||
|
import 'package:tmail_ui_user/features/login/domain/usecases/try_guessing_web_finger_interactor.dart';
|
||||||
import 'package:tmail_ui_user/features/login/domain/usecases/update_account_cache_interactor.dart';
|
import 'package:tmail_ui_user/features/login/domain/usecases/update_account_cache_interactor.dart';
|
||||||
import 'package:tmail_ui_user/features/login/presentation/extensions/handle_openid_configuration.dart';
|
import 'package:tmail_ui_user/features/login/presentation/extensions/handle_openid_configuration.dart';
|
||||||
import 'package:tmail_ui_user/features/login/presentation/login_controller.dart';
|
import 'package:tmail_ui_user/features/login/presentation/login_controller.dart';
|
||||||
@@ -71,6 +72,7 @@ import 'login_controller_test.mocks.dart';
|
|||||||
MockSpec<GetAllRecentLoginUsernameOnMobileInteractor>(),
|
MockSpec<GetAllRecentLoginUsernameOnMobileInteractor>(),
|
||||||
MockSpec<DNSLookupToGetJmapUrlInteractor>(),
|
MockSpec<DNSLookupToGetJmapUrlInteractor>(),
|
||||||
MockSpec<SignInTwakeWorkplaceInteractor>(),
|
MockSpec<SignInTwakeWorkplaceInteractor>(),
|
||||||
|
MockSpec<TryGuessingWebFingerInteractor>(),
|
||||||
MockSpec<GetSessionInteractor>(),
|
MockSpec<GetSessionInteractor>(),
|
||||||
MockSpec<GetAuthenticatedAccountInteractor>(),
|
MockSpec<GetAuthenticatedAccountInteractor>(),
|
||||||
MockSpec<UpdateAccountCacheInteractor>(),
|
MockSpec<UpdateAccountCacheInteractor>(),
|
||||||
@@ -94,6 +96,7 @@ void main() {
|
|||||||
late MockGetAllRecentLoginUsernameOnMobileInteractor mockGetAllRecentLoginUsernameOnMobileInteractor;
|
late MockGetAllRecentLoginUsernameOnMobileInteractor mockGetAllRecentLoginUsernameOnMobileInteractor;
|
||||||
late MockDNSLookupToGetJmapUrlInteractor mockDNSLookupToGetJmapUrlInteractor;
|
late MockDNSLookupToGetJmapUrlInteractor mockDNSLookupToGetJmapUrlInteractor;
|
||||||
late MockSignInTwakeWorkplaceInteractor mockSignInTwakeWorkplaceInteractor;
|
late MockSignInTwakeWorkplaceInteractor mockSignInTwakeWorkplaceInteractor;
|
||||||
|
late MockTryGuessingWebFingerInteractor mockTryGuessingWebFingerInteractor;
|
||||||
late MockGetSessionInteractor mockGetSessionInteractor;
|
late MockGetSessionInteractor mockGetSessionInteractor;
|
||||||
late MockGetAuthenticatedAccountInteractor mockGetAuthenticatedAccountInteractor;
|
late MockGetAuthenticatedAccountInteractor mockGetAuthenticatedAccountInteractor;
|
||||||
late MockUpdateAccountCacheInteractor mockUpdateAccountCacheInteractor;
|
late MockUpdateAccountCacheInteractor mockUpdateAccountCacheInteractor;
|
||||||
@@ -128,6 +131,7 @@ void main() {
|
|||||||
mockGetAllRecentLoginUsernameOnMobileInteractor = MockGetAllRecentLoginUsernameOnMobileInteractor();
|
mockGetAllRecentLoginUsernameOnMobileInteractor = MockGetAllRecentLoginUsernameOnMobileInteractor();
|
||||||
mockDNSLookupToGetJmapUrlInteractor = MockDNSLookupToGetJmapUrlInteractor();
|
mockDNSLookupToGetJmapUrlInteractor = MockDNSLookupToGetJmapUrlInteractor();
|
||||||
mockSignInTwakeWorkplaceInteractor = MockSignInTwakeWorkplaceInteractor();
|
mockSignInTwakeWorkplaceInteractor = MockSignInTwakeWorkplaceInteractor();
|
||||||
|
mockTryGuessingWebFingerInteractor = MockTryGuessingWebFingerInteractor();
|
||||||
|
|
||||||
// mock reloadable controller
|
// mock reloadable controller
|
||||||
mockGetSessionInteractor = MockGetSessionInteractor();
|
mockGetSessionInteractor = MockGetSessionInteractor();
|
||||||
@@ -191,6 +195,7 @@ void main() {
|
|||||||
mockGetAllRecentLoginUsernameOnMobileInteractor,
|
mockGetAllRecentLoginUsernameOnMobileInteractor,
|
||||||
mockDNSLookupToGetJmapUrlInteractor,
|
mockDNSLookupToGetJmapUrlInteractor,
|
||||||
mockSignInTwakeWorkplaceInteractor,
|
mockSignInTwakeWorkplaceInteractor,
|
||||||
|
mockTryGuessingWebFingerInteractor,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user