From 88197991e1210f839bc0df0bd17ca853bff21cd6 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 24 Mar 2022 12:44:57 +0000 Subject: [PATCH 1/4] extracting the direct login logic to its own use case along with viewmodel test case - will ensure we emit account sign in when going via direct login flow --- .../features/onboarding/DirectLoginUseCase.kt | 91 +++++++++++++++++++ .../onboarding/OnboardingViewModel.kt | 78 ++-------------- .../onboarding/OnboardingViewModelTest.kt | 31 ++++++- .../app/test/fakes/FakeDirectLoginUseCase.kt | 31 +++++++ 4 files changed, 159 insertions(+), 72 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeDirectLoginUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt b/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt new file mode 100644 index 0000000000..54ee1d3a52 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.onboarding + +import android.net.Uri +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.onboarding.OnboardingAction.LoginOrRegister +import org.matrix.android.sdk.api.MatrixPatterns.getDomain +import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject + +class DirectLoginUseCase @Inject constructor( + private val authenticationService: AuthenticationService, + private val stringProvider: StringProvider, +) { + + suspend fun execute(action: LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?): Result { + return fetchWellKnown(action.username, homeServerConnectionConfig) + .andThen { wellKnown -> createSessionFor(wellKnown, action, homeServerConnectionConfig) } + } + + private suspend fun fetchWellKnown(matrixId: String, config: HomeServerConnectionConfig?) = runCatching { + authenticationService.getWellKnownData(matrixId, config) + } + + private suspend fun createSessionFor(data: WellknownResult, action: LoginOrRegister, config: HomeServerConnectionConfig?) = when (data) { + is WellknownResult.Prompt -> loginDirect(action, data, config) + is WellknownResult.FailPrompt -> handleFailPrompt(data, action, config) + else -> onWellKnownError() + } + + private suspend fun handleFailPrompt(data: WellknownResult.FailPrompt, action: LoginOrRegister, config: HomeServerConnectionConfig?): Result { + // Relax on IS discovery if homeserver is valid + val isMissingInformationToLogin = data.homeServerUrl == null || data.wellKnown == null + return when { + isMissingInformationToLogin -> onWellKnownError() + else -> loginDirect(action, WellknownResult.Prompt(data.homeServerUrl!!, null, data.wellKnown!!), config) + } + } + + private suspend fun loginDirect(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt, config: HomeServerConnectionConfig?): Result { + val alteredHomeServerConnectionConfig = config?.updateWith(wellKnownPrompt) ?: fallbackConfig(action, wellKnownPrompt) + return runCatching { + authenticationService.directAuthentication( + alteredHomeServerConnectionConfig, + action.username, + action.password, + action.initialDeviceName + ) + } + } + + private fun HomeServerConnectionConfig.updateWith(wellKnownPrompt: WellknownResult.Prompt) = copy( + homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl), + identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) } + ) + + private fun fallbackConfig(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) = HomeServerConnectionConfig( + homeServerUri = Uri.parse("https://${action.username.getDomain()}"), + homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl), + identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) } + ) + + private fun onWellKnownError() = Result.failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error))) +} + +@Suppress("UNCHECKED_CAST") // We're casting null failure results to R +private inline fun Result.andThen(block: (T) -> Result): Result { + return when (val result = getOrNull()) { + null -> this as Result + else -> block(result) + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 3fb52619da..6d959ef124 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -17,7 +17,6 @@ package im.vector.app.features.onboarding import android.content.Context -import android.net.Uri import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -45,7 +44,6 @@ import im.vector.app.features.login.SignMode import kotlinx.coroutines.Job import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.MatrixPatterns.getDomain import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig @@ -55,9 +53,6 @@ import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.RegistrationResult import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.registration.Stage -import org.matrix.android.sdk.api.auth.wellknown.WellknownResult -import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.failure.MatrixIdFailure import org.matrix.android.sdk.api.session.Session import timber.log.Timber import java.util.UUID @@ -79,6 +74,7 @@ class OnboardingViewModel @AssistedInject constructor( private val analyticsTracker: AnalyticsTracker, private val uriFilenameResolver: UriFilenameResolver, private val registrationActionHandler: RegistrationActionHandler, + private val directLoginUseCase: DirectLoginUseCase, private val vectorOverrides: VectorOverrides ) : VectorViewModel(initialState) { @@ -470,74 +466,14 @@ class OnboardingViewModel @AssistedInject constructor( private fun handleDirectLogin(action: OnboardingAction.LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?) { setState { copy(isLoading = true) } - currentJob = viewModelScope.launch { - val data = try { - authenticationService.getWellKnownData(action.username, homeServerConnectionConfig) - } catch (failure: Throwable) { - onDirectLoginError(failure) - return@launch - } - when (data) { - is WellknownResult.Prompt -> - directLoginOnWellknownSuccess(action, data, homeServerConnectionConfig) - is WellknownResult.FailPrompt -> - // Relax on IS discovery if homeserver is valid - if (data.homeServerUrl != null && data.wellKnown != null) { - directLoginOnWellknownSuccess(action, WellknownResult.Prompt(data.homeServerUrl!!, null, data.wellKnown!!), homeServerConnectionConfig) - } else { - onWellKnownError() + directLoginUseCase.execute(action, homeServerConnectionConfig).fold( + onSuccess = { onSessionCreated(it, isAccountCreated = false) }, + onFailure = { + setState { copy(isLoading = false) } + _viewEvents.post(OnboardingViewEvents.Failure(it)) } - else -> { - onWellKnownError() - } - } - } - } - - private fun onWellKnownError() { - setState { copy(isLoading = false) } - _viewEvents.post(OnboardingViewEvents.Failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error)))) - } - - private suspend fun directLoginOnWellknownSuccess(action: OnboardingAction.LoginOrRegister, - wellKnownPrompt: WellknownResult.Prompt, - homeServerConnectionConfig: HomeServerConnectionConfig?) { - val alteredHomeServerConnectionConfig = homeServerConnectionConfig - ?.copy( - homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl), - identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) } - ) - ?: HomeServerConnectionConfig( - homeServerUri = Uri.parse("https://${action.username.getDomain()}"), - homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl), - identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) } - ) - - val data = try { - authenticationService.directAuthentication( - alteredHomeServerConnectionConfig, - action.username, - action.password, - action.initialDeviceName) - } catch (failure: Throwable) { - onDirectLoginError(failure) - return - } - onSessionCreated(data, isAccountCreated = false) - } - - private fun onDirectLoginError(failure: Throwable) { - when (failure) { - is MatrixIdFailure.InvalidMatrixId, - is Failure.UnrecognizedCertificateFailure -> { - setState { copy(isLoading = false) } - // Display this error in a dialog - _viewEvents.post(OnboardingViewEvents.Failure(failure)) - } - else -> { - setState { copy(isLoading = false) } - } + ) } } diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index df4e0de65e..8d4ee50f2f 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -24,6 +24,7 @@ import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeAnalyticsTracker import im.vector.app.test.fakes.FakeAuthenticationService import im.vector.app.test.fakes.FakeContext +import im.vector.app.test.fakes.FakeDirectLoginUseCase import im.vector.app.test.fakes.FakeHomeServerConnectionConfigFactory import im.vector.app.test.fakes.FakeHomeServerHistoryService import im.vector.app.test.fakes.FakeRegisterActionHandler @@ -44,6 +45,7 @@ import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import org.matrix.android.sdk.api.auth.registration.RegistrationResult import org.matrix.android.sdk.api.auth.registration.Stage +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities private const val A_DISPLAY_NAME = "a display name" @@ -55,6 +57,7 @@ private val A_RESULT_IGNORED_REGISTER_ACTION = RegisterAction.AddThreePid(Regist private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canChangeDisplayName = true, canChangeAvatar = true) private val AN_IGNORED_FLOW_RESULT = FlowResult(missingStages = emptyList(), completedStages = emptyList()) private val ANY_CONTINUING_REGISTRATION_RESULT = RegistrationResult.FlowResponse(AN_IGNORED_FLOW_RESULT) +private val A_LOGIN_OR_REGISTER_ACTION = OnboardingAction.LoginOrRegister("@a-user:id.org", "a-password", "a-device-name") class OnboardingViewModelTest { @@ -69,6 +72,7 @@ class OnboardingViewModelTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession) private val fakeAuthenticationService = FakeAuthenticationService() private val fakeRegisterActionHandler = FakeRegisterActionHandler() + private val fakeDirectLoginUseCase = FakeDirectLoginUseCase() lateinit var viewModel: OnboardingViewModel @@ -114,6 +118,26 @@ class OnboardingViewModelTest { .finish() } + @Test + fun `given has sign in with matrix id sign mode, when handling login or register action, then logs in directly`() = runTest { + val initialState = initialState.copy(signMode = SignMode.SignInWithMatrixId) + viewModel = createViewModel(initialState) + fakeDirectLoginUseCase.givenSuccessResult(A_LOGIN_OR_REGISTER_ACTION, config = null, result = fakeSession) + givenInitialisesSession(fakeSession) + val test = viewModel.test() + + viewModel.handle(A_LOGIN_OR_REGISTER_ACTION) + + test + .assertStatesChanges( + initialState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvents(OnboardingViewEvents.OnAccountSignedIn) + .finish() + } + @Test fun `when handling SignUp then sets sign mode to sign up and starts registration`() = runTest { givenRegistrationResultFor(RegisterAction.StartRegistration, ANY_CONTINUING_REGISTRATION_RESULT) @@ -344,6 +368,7 @@ class OnboardingViewModelTest { FakeAnalyticsTracker(), fakeUriFilenameResolver.instance, fakeRegisterActionHandler.instance, + fakeDirectLoginUseCase.instance, FakeVectorOverrides() ) } @@ -384,7 +409,11 @@ class OnboardingViewModelTest { private fun givenSuccessfullyCreatesAccount(homeServerCapabilities: HomeServerCapabilities) { fakeSession.fakeHomeServerCapabilitiesService.givenCapabilities(homeServerCapabilities) - fakeActiveSessionHolder.expectSetsActiveSession(fakeSession) + givenInitialisesSession(fakeSession) + } + + private fun givenInitialisesSession(session: Session) { + fakeActiveSessionHolder.expectSetsActiveSession(session) fakeAuthenticationService.expectReset() fakeSession.expectStartsSyncing() } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeDirectLoginUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeDirectLoginUseCase.kt new file mode 100644 index 0000000000..b3fba51354 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeDirectLoginUseCase.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import im.vector.app.features.onboarding.DirectLoginUseCase +import im.vector.app.features.onboarding.OnboardingAction +import io.mockk.coEvery +import io.mockk.mockk +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig + +class FakeDirectLoginUseCase { + val instance = mockk() + + fun givenSuccessResult(action: OnboardingAction.LoginOrRegister, config: HomeServerConnectionConfig?, result: FakeSession) { + coEvery { instance.execute(action, config) } returns Result.success(result) + } +} From 230c37597c67c33403d173ba1685928cbaa97af3 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 24 Mar 2022 15:41:53 +0000 Subject: [PATCH 2/4] adding happy path tests for the direct login use case --- .../android/sdk/internal/extensions/Result.kt | 8 +++ .../features/onboarding/DirectLoginUseCase.kt | 21 ++---- .../app/features/onboarding/UriFactory.kt | 27 +++++++ .../onboarding/DirectLoginUseCaseTest.kt | 71 +++++++++++++++++++ .../test/fakes/FakeAuthenticationService.kt | 11 +++ .../java/im/vector/app/test/fakes/FakeUri.kt | 15 +++- .../vector/app/test/fakes/FakeUriFactory.kt | 31 ++++++++ 7 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/onboarding/UriFactory.kt create mode 100644 vector/src/test/java/im/vector/app/features/onboarding/DirectLoginUseCaseTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeUriFactory.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt index 3734c5dc1d..12adf16cbc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt @@ -21,3 +21,11 @@ fun Result.foldToCallback(callback: MatrixCallback): Unit = fold( { callback.onSuccess(it) }, { callback.onFailure(it) } ) + +@Suppress("UNCHECKED_CAST") // We're casting null failure results to R +inline fun Result.andThen(block: (T) -> Result): Result { + return when (val result = getOrNull()) { + null -> this as Result + else -> block(result) + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt b/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt index 54ee1d3a52..7ef4dfb609 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt @@ -16,7 +16,6 @@ package im.vector.app.features.onboarding -import android.net.Uri import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.features.onboarding.OnboardingAction.LoginOrRegister @@ -25,11 +24,13 @@ import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.wellknown.WellknownResult import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.internal.extensions.andThen import javax.inject.Inject class DirectLoginUseCase @Inject constructor( private val authenticationService: AuthenticationService, private val stringProvider: StringProvider, + private val uriFactory: UriFactory ) { suspend fun execute(action: LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?): Result { @@ -69,23 +70,15 @@ class DirectLoginUseCase @Inject constructor( } private fun HomeServerConnectionConfig.updateWith(wellKnownPrompt: WellknownResult.Prompt) = copy( - homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl), - identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) } + homeServerUriBase = uriFactory.parse(wellKnownPrompt.homeServerUrl), + identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) } ) private fun fallbackConfig(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) = HomeServerConnectionConfig( - homeServerUri = Uri.parse("https://${action.username.getDomain()}"), - homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl), - identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) } + homeServerUri = uriFactory.parse("https://${action.username.getDomain()}"), + homeServerUriBase = uriFactory.parse(wellKnownPrompt.homeServerUrl), + identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) } ) private fun onWellKnownError() = Result.failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error))) } - -@Suppress("UNCHECKED_CAST") // We're casting null failure results to R -private inline fun Result.andThen(block: (T) -> Result): Result { - return when (val result = getOrNull()) { - null -> this as Result - else -> block(result) - } -} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/UriFactory.kt b/vector/src/main/java/im/vector/app/features/onboarding/UriFactory.kt new file mode 100644 index 0000000000..f9e7a3458c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/UriFactory.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.onboarding + +import android.net.Uri +import javax.inject.Inject + +class UriFactory @Inject constructor() { + + fun parse(value: String): Uri { + return Uri.parse(value) + } +} diff --git a/vector/src/test/java/im/vector/app/features/onboarding/DirectLoginUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/DirectLoginUseCaseTest.kt new file mode 100644 index 0000000000..c7bfc9b73d --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/onboarding/DirectLoginUseCaseTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.onboarding + +import im.vector.app.test.fakes.FakeAuthenticationService +import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fakes.FakeStringProvider +import im.vector.app.test.fakes.FakeUri +import im.vector.app.test.fakes.FakeUriFactory +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.MatrixPatterns.getDomain +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.auth.data.WellKnown +import org.matrix.android.sdk.api.auth.wellknown.WellknownResult + +private val A_LOGIN_OR_REGISTER_ACTION = OnboardingAction.LoginOrRegister("@a-user:id.org", "a-password", "a-device-name") +private val A_WELLKNOWN_SUCCESS_RESULT = WellknownResult.Prompt("https://homeserverurl.com", identityServerUrl = null, WellKnown()) +private val A_WELLKNOWN_FAILED_WITH_CONTENT_RESULT = WellknownResult.FailPrompt("https://homeserverurl.com", WellKnown()) +private val NO_HOMESERVER_CONFIG: HomeServerConnectionConfig? = null +private val A_FALLBACK_CONFIG: HomeServerConnectionConfig = HomeServerConnectionConfig( + homeServerUri = FakeUri("https://${A_LOGIN_OR_REGISTER_ACTION.username.getDomain()}").instance, + homeServerUriBase = FakeUri(A_WELLKNOWN_SUCCESS_RESULT.homeServerUrl).instance, + identityServerUri = null +) + +class DirectLoginUseCaseTest { + + private val fakeAuthenticationService = FakeAuthenticationService() + private val fakeStringProvider = FakeStringProvider() + private val fakeSession = FakeSession() + + private val useCase = DirectLoginUseCase(fakeAuthenticationService, fakeStringProvider.instance, FakeUriFactory().instance) + + @Test + fun `when logging in directly, then returns success with direct session result`() = runTest { + fakeAuthenticationService.givenWellKnown(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_SUCCESS_RESULT) + val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION + fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession) + + val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG) + + result shouldBeEqualTo Result.success(fakeSession) + } + + @Test + fun `given wellknown fails but has content, when logging in directly, then returns success with direct session result`() = runTest { + fakeAuthenticationService.givenWellKnown(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_FAILED_WITH_CONTENT_RESULT) + val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION + fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession) + + val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG) + + result shouldBeEqualTo Result.success(fakeSession) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt index 10daf5de1e..a59116a737 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt @@ -16,11 +16,14 @@ package im.vector.app.test.fakes +import io.mockk.coEvery import io.mockk.coJustRun import io.mockk.every import io.mockk.mockk import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.registration.RegistrationWizard +import org.matrix.android.sdk.api.auth.wellknown.WellknownResult class FakeAuthenticationService : AuthenticationService by mockk() { @@ -35,4 +38,12 @@ class FakeAuthenticationService : AuthenticationService by mockk() { fun expectReset() { coJustRun { reset() } } + + fun givenWellKnown(matrixId: String, config: HomeServerConnectionConfig?, result: WellknownResult) { + coEvery { getWellKnownData(matrixId, config) } returns result + } + + fun givenDirectAuthentication(config: HomeServerConnectionConfig, matrixId: String, password: String, deviceName: String, result: FakeSession) { + coEvery { directAuthentication(config, matrixId, password, deviceName) } returns result + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUri.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUri.kt index 99f2cf39aa..675401d72f 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeUri.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUri.kt @@ -20,9 +20,14 @@ import android.net.Uri import io.mockk.every import io.mockk.mockk -class FakeUri { +class FakeUri(contentEquals: String? = null) { + val instance = mockk() + init { + contentEquals?.let { givenEquals(it) } + } + fun givenNonHierarchical() { givenContent(schema = "mail", path = null) } @@ -31,4 +36,12 @@ class FakeUri { every { instance.scheme } returns schema every { instance.path } returns path } + + @Suppress("ReplaceCallWithBinaryOperator") + fun givenEquals(content: String) { + every { instance.equals(any()) } answers { + it.invocation.args.first() == content + } + every { instance.hashCode() } answers { content.hashCode() } + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUriFactory.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUriFactory.kt new file mode 100644 index 0000000000..90b615cb7c --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUriFactory.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.test.fakes + +import im.vector.app.features.onboarding.UriFactory +import io.mockk.every +import io.mockk.mockk + +class FakeUriFactory { + + val instance = mockk().also { + every { it.parse(any()) } answers { + val input = it.invocation.args.first() as String + FakeUri().also { it.givenEquals(input) }.instance + } + } +} From cfb3aa8a221a370b9c7ba0526bd2e06b70060917 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 24 Mar 2022 16:23:03 +0000 Subject: [PATCH 3/4] adding direct login error path tests --- .../onboarding/DirectLoginUseCaseTest.kt | 40 ++++++++++++++++++- .../onboarding/OnboardingViewModelTest.kt | 20 ++++++++++ .../test/fakes/FakeAuthenticationService.kt | 8 ++++ .../app/test/fakes/FakeDirectLoginUseCase.kt | 4 ++ .../app/test/fakes/FakeStringProvider.kt | 2 + 5 files changed, 73 insertions(+), 1 deletion(-) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/DirectLoginUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/DirectLoginUseCaseTest.kt index c7bfc9b73d..5a3c323316 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/DirectLoginUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/DirectLoginUseCaseTest.kt @@ -16,12 +16,15 @@ package im.vector.app.features.onboarding +import im.vector.app.R import im.vector.app.test.fakes.FakeAuthenticationService import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeUri import im.vector.app.test.fakes.FakeUriFactory +import im.vector.app.test.fakes.toTestString import kotlinx.coroutines.test.runTest +import org.amshove.kluent.should import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.matrix.android.sdk.api.MatrixPatterns.getDomain @@ -32,12 +35,14 @@ import org.matrix.android.sdk.api.auth.wellknown.WellknownResult private val A_LOGIN_OR_REGISTER_ACTION = OnboardingAction.LoginOrRegister("@a-user:id.org", "a-password", "a-device-name") private val A_WELLKNOWN_SUCCESS_RESULT = WellknownResult.Prompt("https://homeserverurl.com", identityServerUrl = null, WellKnown()) private val A_WELLKNOWN_FAILED_WITH_CONTENT_RESULT = WellknownResult.FailPrompt("https://homeserverurl.com", WellKnown()) +private val A_WELLKNOWN_FAILED_WITHOUT_CONTENT_RESULT = WellknownResult.FailPrompt(null, null) private val NO_HOMESERVER_CONFIG: HomeServerConnectionConfig? = null private val A_FALLBACK_CONFIG: HomeServerConnectionConfig = HomeServerConnectionConfig( homeServerUri = FakeUri("https://${A_LOGIN_OR_REGISTER_ACTION.username.getDomain()}").instance, homeServerUriBase = FakeUri(A_WELLKNOWN_SUCCESS_RESULT.homeServerUrl).instance, identityServerUri = null ) +private val AN_ERROR = RuntimeException() class DirectLoginUseCaseTest { @@ -59,7 +64,7 @@ class DirectLoginUseCaseTest { } @Test - fun `given wellknown fails but has content, when logging in directly, then returns success with direct session result`() = runTest { + fun `given wellknown fails with content, when logging in directly, then returns success with direct session result`() = runTest { fakeAuthenticationService.givenWellKnown(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_FAILED_WITH_CONTENT_RESULT) val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession) @@ -68,4 +73,37 @@ class DirectLoginUseCaseTest { result shouldBeEqualTo Result.success(fakeSession) } + + @Test + fun `given wellknown fails without content, when logging in directly, then returns well known error`() = runTest { + fakeAuthenticationService.givenWellKnown(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_FAILED_WITHOUT_CONTENT_RESULT) + val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION + fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession) + + val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG) + + result should { this.isFailure } + result should { this.exceptionOrNull() is Exception } + result should { this.exceptionOrNull()?.message == R.string.autodiscover_well_known_error.toTestString() } + } + + @Test + fun `given wellknown throws, when logging in directly, then returns failure result with original cause`() = runTest { + fakeAuthenticationService.givenWellKnownThrows(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, cause = AN_ERROR) + + val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG) + + result shouldBeEqualTo Result.failure(AN_ERROR) + } + + @Test + fun `given direct authentication throws, when logging in directly, then returns failure result with original cause`() = runTest { + fakeAuthenticationService.givenWellKnown(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_SUCCESS_RESULT) + val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION + fakeAuthenticationService.givenDirectAuthenticationThrows(A_FALLBACK_CONFIG, username, password, initialDeviceName, cause = AN_ERROR) + + val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG) + + result shouldBeEqualTo Result.failure(AN_ERROR) + } } diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index 8d4ee50f2f..118bf689d2 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -138,6 +138,26 @@ class OnboardingViewModelTest { .finish() } + @Test + fun `given has sign in with matrix id sign mode, when handling login or register action fails, then emits error`() = runTest { + val initialState = initialState.copy(signMode = SignMode.SignInWithMatrixId) + viewModel = createViewModel(initialState) + fakeDirectLoginUseCase.givenFailureResult(A_LOGIN_OR_REGISTER_ACTION, config = null, cause = AN_ERROR) + givenInitialisesSession(fakeSession) + val test = viewModel.test() + + viewModel.handle(A_LOGIN_OR_REGISTER_ACTION) + + test + .assertStatesChanges( + initialState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvents(OnboardingViewEvents.Failure(AN_ERROR)) + .finish() + } + @Test fun `when handling SignUp then sets sign mode to sign up and starts registration`() = runTest { givenRegistrationResultFor(RegisterAction.StartRegistration, ANY_CONTINUING_REGISTRATION_RESULT) diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt index a59116a737..9175fd3750 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt @@ -43,7 +43,15 @@ class FakeAuthenticationService : AuthenticationService by mockk() { coEvery { getWellKnownData(matrixId, config) } returns result } + fun givenWellKnownThrows(matrixId: String, config: HomeServerConnectionConfig?, cause: Throwable) { + coEvery { getWellKnownData(matrixId, config) } throws cause + } + fun givenDirectAuthentication(config: HomeServerConnectionConfig, matrixId: String, password: String, deviceName: String, result: FakeSession) { coEvery { directAuthentication(config, matrixId, password, deviceName) } returns result } + + fun givenDirectAuthenticationThrows(config: HomeServerConnectionConfig, matrixId: String, password: String, deviceName: String, cause: Throwable) { + coEvery { directAuthentication(config, matrixId, password, deviceName) } throws cause + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeDirectLoginUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeDirectLoginUseCase.kt index b3fba51354..8a5c6b1cee 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeDirectLoginUseCase.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeDirectLoginUseCase.kt @@ -28,4 +28,8 @@ class FakeDirectLoginUseCase { fun givenSuccessResult(action: OnboardingAction.LoginOrRegister, config: HomeServerConnectionConfig?, result: FakeSession) { coEvery { instance.execute(action, config) } returns Result.success(result) } + + fun givenFailureResult(action: OnboardingAction.LoginOrRegister, config: HomeServerConnectionConfig?, cause: Throwable) { + coEvery { instance.execute(action, config) } returns Result.failure(cause) + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt index f9001e3f8a..1a4f5cb85b 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt @@ -30,3 +30,5 @@ class FakeStringProvider { } } } + +fun Int.toTestString() = "test-$this" From 776cf2451621b6fb2beee7538544c08ede66f980 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 24 Mar 2022 16:24:17 +0000 Subject: [PATCH 4/4] adding changelog entry --- changelog.d/5628.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5628.misc diff --git a/changelog.d/5628.misc b/changelog.d/5628.misc new file mode 100644 index 0000000000..9c4894c164 --- /dev/null +++ b/changelog.d/5628.misc @@ -0,0 +1 @@ +Adds unit tests around the login with matrix id flow \ No newline at end of file