From 75d038b05863422c16eee51141dfc76feadf359f Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 12 May 2022 12:35:13 +0100 Subject: [PATCH] adding test case around invalid deeplinks within the onboarding flow --- .../im/vector/app/core/di/SingletonModule.kt | 1 - .../im/vector/app/core/extensions/Context.kt | 10 +++-- .../onboarding/OnboardingViewModel.kt | 18 +++++--- .../onboarding/ftueauth/FtueExtensions.kt | 4 -- .../onboarding/OnboardingViewModelTest.kt | 29 +++++++++++- .../app/test/fakes/FakeConnectivityManager.kt | 44 +++++++++++++++++++ .../im/vector/app/test/fakes/FakeContext.kt | 18 ++++++++ .../app/test/fakes/FakeNetworkCapabilities.kt | 32 ++++++++++++++ .../FakeStartAuthenticationFlowUseCase.kt | 6 +++ .../app/test/fixtures/FailureFixture.kt | 3 ++ 10 files changed, 151 insertions(+), 14 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeConnectivityManager.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeNetworkCapabilities.kt diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt index 22ce4ee0ce..44f8bb1b3e 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -187,7 +187,6 @@ object VectorStaticModule { return analyticsConfig } - @Provides @Singleton fun providesBuildMeta() = BuildMeta() diff --git a/vector/src/main/java/im/vector/app/core/extensions/Context.kt b/vector/src/main/java/im/vector/app/core/extensions/Context.kt index 35bc01ef29..81844a403b 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Context.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Context.kt @@ -16,6 +16,7 @@ package im.vector.app.core.extensions +import android.annotation.SuppressLint import android.content.Context import android.graphics.drawable.Drawable import android.net.ConnectivityManager @@ -30,11 +31,13 @@ import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.FloatRange import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import dagger.hilt.EntryPoints import im.vector.app.core.datastore.dataStoreProvider import im.vector.app.core.di.SingletonEntryPoint +import im.vector.app.core.resources.BuildMeta import java.io.OutputStream import kotlin.math.roundToInt @@ -88,9 +91,10 @@ fun Context.safeOpenOutputStream(uri: Uri): OutputStream? { * @return true if no active connection is found */ @Suppress("deprecation") -fun Context.inferNoConnectivity(): Boolean { - val connectivityManager: ConnectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { +@SuppressLint("NewApi") // false positive +fun Context.inferNoConnectivity(buildMeta: BuildMeta): Boolean { + val connectivityManager = getSystemService()!! + return if (buildMeta.sdkInt > Build.VERSION_CODES.M) { val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) when { networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> false 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 2a5f57ac70..5af4fa138b 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 @@ -30,6 +30,7 @@ import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.inferNoConnectivity import im.vector.app.core.extensions.vectorStore import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.BuildMeta import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.ensureProtocol import im.vector.app.core.utils.ensureTrailingSlash @@ -81,7 +82,8 @@ class OnboardingViewModel @AssistedInject constructor( private val registrationActionHandler: RegistrationActionHandler, private val directLoginUseCase: DirectLoginUseCase, private val startAuthenticationFlowUseCase: StartAuthenticationFlowUseCase, - private val vectorOverrides: VectorOverrides + private val vectorOverrides: VectorOverrides, + private val buildMeta: BuildMeta ) : VectorViewModel(initialState) { @AssistedFactory @@ -638,18 +640,24 @@ class OnboardingViewModel @AssistedInject constructor( private fun onAuthenticationStartError(it: Throwable, trigger: OnboardingAction.HomeServerChange) { when { - it.isHomeserverUnavailable() && applicationContext.inferNoConnectivity() -> _viewEvents.post( + it.isHomeserverUnavailable() && applicationContext.inferNoConnectivity(buildMeta) -> _viewEvents.post( OnboardingViewEvents.Failure(it) ) - it.isHomeserverUnavailable() && trigger is OnboardingAction.HomeServerChange.SelectHomeServer -> _viewEvents.post( - OnboardingViewEvents.DeeplinkAuthenticationFailure(retryAction = trigger.resetToDefaultUrl()) + deeplinkUrlIsUnavailable(it, trigger) -> _viewEvents.post( + OnboardingViewEvents.DeeplinkAuthenticationFailure( + retryAction = (trigger as OnboardingAction.HomeServerChange.SelectHomeServer).resetToDefaultUrl() + ) ) - else -> _viewEvents.post( + else -> _viewEvents.post( OnboardingViewEvents.Failure(it) ) } } + private fun deeplinkUrlIsUnavailable(error: Throwable, trigger: OnboardingAction.HomeServerChange) = error.isHomeserverUnavailable() && + loginConfig != null && + trigger is OnboardingAction.HomeServerChange.SelectHomeServer + private fun OnboardingAction.HomeServerChange.SelectHomeServer.resetToDefaultUrl() = copy(homeServerUrl = defaultHomeserverUrl) private suspend fun onAuthenticationStartedSuccess( diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt index 5228e289bc..8d63fbf547 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueExtensions.kt @@ -17,17 +17,13 @@ package im.vector.app.features.onboarding.ftueauth import android.widget.Button -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputLayout -import im.vector.app.R import im.vector.app.core.extensions.hasContentFlow -import im.vector.app.core.extensions.inferNoConnectivity import im.vector.app.features.login.SignMode import im.vector.app.features.onboarding.OnboardingAction import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.onEach -import org.matrix.android.sdk.api.failure.isHomeserverUnavailable fun SignMode.toAuthenticateAction(login: String, password: String, initialDeviceName: String): OnboardingAction.AuthenticateAction { return when (this) { 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 e4e687536c..3c394181d1 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 @@ -18,6 +18,8 @@ package im.vector.app.features.onboarding import android.net.Uri import com.airbnb.mvrx.test.MvRxTestRule +import im.vector.app.R +import im.vector.app.features.login.LoginConfig import im.vector.app.features.login.LoginMode import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.SignMode @@ -38,6 +40,8 @@ import im.vector.app.test.fakes.FakeUri import im.vector.app.test.fakes.FakeUriFilenameResolver import im.vector.app.test.fakes.FakeVectorFeatures import im.vector.app.test.fakes.FakeVectorOverrides +import im.vector.app.test.fakes.toTestString +import im.vector.app.test.fixtures.aBuildMeta import im.vector.app.test.fixtures.aHomeServerCapabilities import im.vector.app.test.test import kotlinx.coroutines.test.runTest @@ -242,6 +246,28 @@ class OnboardingViewModelTest { .finish() } + @Test + fun `given unavailable deeplink, when selecting homeserver, then emits failure with default homeserver as retry action`() = runTest { + fakeContext.givenHasConnection() + fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG) + fakeStartAuthenticationFlowUseCase.givenHomeserverUnavailable(A_HOMESERVER_CONFIG) + val test = viewModel.test() + + viewModel.handle(OnboardingAction.InitWith(LoginConfig(A_HOMESERVER_URL, null))) + viewModel.handle(OnboardingAction.HomeServerChange.SelectHomeServer(A_HOMESERVER_URL)) + + val expectedRetryAction = OnboardingAction.HomeServerChange.SelectHomeServer("${R.string.matrix_org_server_url.toTestString()}/") + test + .assertStatesChanges( + initialState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + + ) + .assertEvents(OnboardingViewEvents.DeeplinkAuthenticationFailure(expectedRetryAction)) + .finish() + } + @Test fun `given in the sign up flow, when editing homeserver, then updates selected homeserver state and emits edited event`() = runTest { viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp)) @@ -457,7 +483,8 @@ class OnboardingViewModelTest { fakeRegisterActionHandler.instance, fakeDirectLoginUseCase.instance, fakeStartAuthenticationFlowUseCase.instance, - FakeVectorOverrides() + FakeVectorOverrides(), + aBuildMeta() ).also { viewModel = it initialState = state diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeConnectivityManager.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeConnectivityManager.kt new file mode 100644 index 0000000000..d565105f81 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeConnectivityManager.kt @@ -0,0 +1,44 @@ +/* + * 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 android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import io.mockk.every +import io.mockk.mockk + +class FakeConnectivityManager { + val instance = mockk() + + fun givenNoActiveConnection() { + every { instance.activeNetwork } returns null + } + + fun givenHasActiveConnection() { + val network = mockk() + every { instance.activeNetwork } returns network + + val networkCapabilities = FakeNetworkCapabilities() + networkCapabilities.givenTransports( + NetworkCapabilities.TRANSPORT_CELLULAR, + NetworkCapabilities.TRANSPORT_WIFI, + NetworkCapabilities.TRANSPORT_VPN + ) + every { instance.getNetworkCapabilities(network) } returns networkCapabilities.instance + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt index 2a50c34ca3..eb491c9e0c 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt @@ -18,6 +18,7 @@ package im.vector.app.test.fakes import android.content.ContentResolver import android.content.Context +import android.net.ConnectivityManager import android.net.Uri import android.os.ParcelFileDescriptor import io.mockk.every @@ -48,4 +49,21 @@ class FakeContext( fun givenMissingSafeOutputStreamFor(uri: Uri) { every { contentResolver.openOutputStream(uri, "wt") } returns null } + + fun givenNoConnection() { + val connectivityManager = FakeConnectivityManager() + connectivityManager.givenNoActiveConnection() + givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance) + } + + private fun givenService(name: String, klass: Class, service: T) { + every { instance.getSystemService(name) } returns service + every { instance.getSystemService(klass) } returns service + } + + fun givenHasConnection() { + val connectivityManager = FakeConnectivityManager() + connectivityManager.givenHasActiveConnection() + givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance) + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeNetworkCapabilities.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeNetworkCapabilities.kt new file mode 100644 index 0000000000..36add7128c --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeNetworkCapabilities.kt @@ -0,0 +1,32 @@ +/* + * 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 android.net.NetworkCapabilities +import io.mockk.every +import io.mockk.mockk + +class FakeNetworkCapabilities { + val instance = mockk() + + fun givenTransports(vararg type: Int) { + every { instance.hasTransport(any()) } answers { + val input = it.invocation.args.first() as Int + type.contains(input) + } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeStartAuthenticationFlowUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeStartAuthenticationFlowUseCase.kt index 697de6bf25..88ad1a7a6b 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeStartAuthenticationFlowUseCase.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeStartAuthenticationFlowUseCase.kt @@ -18,9 +18,11 @@ package im.vector.app.test.fakes import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult +import im.vector.app.test.fixtures.aHomeserverUnavailableError import io.mockk.coEvery import io.mockk.mockk import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.failure.Failure class FakeStartAuthenticationFlowUseCase { @@ -29,4 +31,8 @@ class FakeStartAuthenticationFlowUseCase { fun givenResult(config: HomeServerConnectionConfig, result: StartAuthenticationResult) { coEvery { instance.execute(config) } returns result } + + fun givenHomeserverUnavailable(config: HomeServerConnectionConfig) { + coEvery { instance.execute(config) } throws aHomeserverUnavailableError() + } } diff --git a/vector/src/test/java/im/vector/app/test/fixtures/FailureFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/FailureFixture.kt index 39c139c208..9ac851ef5e 100644 --- a/vector/src/test/java/im/vector/app/test/fixtures/FailureFixture.kt +++ b/vector/src/test/java/im/vector/app/test/fixtures/FailureFixture.kt @@ -18,8 +18,11 @@ package im.vector.app.test.fixtures import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError +import java.net.UnknownHostException import javax.net.ssl.HttpsURLConnection fun a401ServerError() = Failure.ServerError( MatrixError(MatrixError.M_UNAUTHORIZED, ""), HttpsURLConnection.HTTP_UNAUTHORIZED ) + +fun aHomeserverUnavailableError() = Failure.NetworkConnection(UnknownHostException())