adding test case around invalid deeplinks within the onboarding flow

This commit is contained in:
Adam Brown 2022-05-12 12:35:13 +01:00
parent 797e0ee706
commit 75d038b058
10 changed files with 151 additions and 14 deletions

View File

@ -187,7 +187,6 @@ object VectorStaticModule {
return analyticsConfig return analyticsConfig
} }
@Provides @Provides
@Singleton @Singleton
fun providesBuildMeta() = BuildMeta() fun providesBuildMeta() = BuildMeta()

View File

@ -16,6 +16,7 @@
package im.vector.app.core.extensions package im.vector.app.core.extensions
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.ConnectivityManager import android.net.ConnectivityManager
@ -30,11 +31,13 @@ import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import dagger.hilt.EntryPoints import dagger.hilt.EntryPoints
import im.vector.app.core.datastore.dataStoreProvider import im.vector.app.core.datastore.dataStoreProvider
import im.vector.app.core.di.SingletonEntryPoint import im.vector.app.core.di.SingletonEntryPoint
import im.vector.app.core.resources.BuildMeta
import java.io.OutputStream import java.io.OutputStream
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -88,9 +91,10 @@ fun Context.safeOpenOutputStream(uri: Uri): OutputStream? {
* @return true if no active connection is found * @return true if no active connection is found
*/ */
@Suppress("deprecation") @Suppress("deprecation")
fun Context.inferNoConnectivity(): Boolean { @SuppressLint("NewApi") // false positive
val connectivityManager: ConnectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager fun Context.inferNoConnectivity(buildMeta: BuildMeta): Boolean {
return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { val connectivityManager = getSystemService<ConnectivityManager>()!!
return if (buildMeta.sdkInt > Build.VERSION_CODES.M) {
val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
when { when {
networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> false networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> false

View File

@ -30,6 +30,7 @@ import im.vector.app.core.extensions.configureAndStart
import im.vector.app.core.extensions.inferNoConnectivity import im.vector.app.core.extensions.inferNoConnectivity
import im.vector.app.core.extensions.vectorStore import im.vector.app.core.extensions.vectorStore
import im.vector.app.core.platform.VectorViewModel 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.resources.StringProvider
import im.vector.app.core.utils.ensureProtocol import im.vector.app.core.utils.ensureProtocol
import im.vector.app.core.utils.ensureTrailingSlash import im.vector.app.core.utils.ensureTrailingSlash
@ -81,7 +82,8 @@ class OnboardingViewModel @AssistedInject constructor(
private val registrationActionHandler: RegistrationActionHandler, private val registrationActionHandler: RegistrationActionHandler,
private val directLoginUseCase: DirectLoginUseCase, private val directLoginUseCase: DirectLoginUseCase,
private val startAuthenticationFlowUseCase: StartAuthenticationFlowUseCase, private val startAuthenticationFlowUseCase: StartAuthenticationFlowUseCase,
private val vectorOverrides: VectorOverrides private val vectorOverrides: VectorOverrides,
private val buildMeta: BuildMeta
) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) { ) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
@ -638,18 +640,24 @@ class OnboardingViewModel @AssistedInject constructor(
private fun onAuthenticationStartError(it: Throwable, trigger: OnboardingAction.HomeServerChange) { private fun onAuthenticationStartError(it: Throwable, trigger: OnboardingAction.HomeServerChange) {
when { when {
it.isHomeserverUnavailable() && applicationContext.inferNoConnectivity() -> _viewEvents.post( it.isHomeserverUnavailable() && applicationContext.inferNoConnectivity(buildMeta) -> _viewEvents.post(
OnboardingViewEvents.Failure(it) OnboardingViewEvents.Failure(it)
) )
it.isHomeserverUnavailable() && trigger is OnboardingAction.HomeServerChange.SelectHomeServer -> _viewEvents.post( deeplinkUrlIsUnavailable(it, trigger) -> _viewEvents.post(
OnboardingViewEvents.DeeplinkAuthenticationFailure(retryAction = trigger.resetToDefaultUrl()) OnboardingViewEvents.DeeplinkAuthenticationFailure(
retryAction = (trigger as OnboardingAction.HomeServerChange.SelectHomeServer).resetToDefaultUrl()
)
) )
else -> _viewEvents.post( else -> _viewEvents.post(
OnboardingViewEvents.Failure(it) 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 fun OnboardingAction.HomeServerChange.SelectHomeServer.resetToDefaultUrl() = copy(homeServerUrl = defaultHomeserverUrl)
private suspend fun onAuthenticationStartedSuccess( private suspend fun onAuthenticationStartedSuccess(

View File

@ -17,17 +17,13 @@
package im.vector.app.features.onboarding.ftueauth package im.vector.app.features.onboarding.ftueauth
import android.widget.Button import android.widget.Button
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout 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.hasContentFlow
import im.vector.app.core.extensions.inferNoConnectivity
import im.vector.app.features.login.SignMode import im.vector.app.features.login.SignMode
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
fun SignMode.toAuthenticateAction(login: String, password: String, initialDeviceName: String): OnboardingAction.AuthenticateAction { fun SignMode.toAuthenticateAction(login: String, password: String, initialDeviceName: String): OnboardingAction.AuthenticateAction {
return when (this) { return when (this) {

View File

@ -18,6 +18,8 @@ package im.vector.app.features.onboarding
import android.net.Uri import android.net.Uri
import com.airbnb.mvrx.test.MvRxTestRule 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.LoginMode
import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.login.SignMode 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.FakeUriFilenameResolver
import im.vector.app.test.fakes.FakeVectorFeatures import im.vector.app.test.fakes.FakeVectorFeatures
import im.vector.app.test.fakes.FakeVectorOverrides 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.fixtures.aHomeServerCapabilities
import im.vector.app.test.test import im.vector.app.test.test
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -242,6 +246,28 @@ class OnboardingViewModelTest {
.finish() .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 @Test
fun `given in the sign up flow, when editing homeserver, then updates selected homeserver state and emits edited event`() = runTest { 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)) viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp))
@ -457,7 +483,8 @@ class OnboardingViewModelTest {
fakeRegisterActionHandler.instance, fakeRegisterActionHandler.instance,
fakeDirectLoginUseCase.instance, fakeDirectLoginUseCase.instance,
fakeStartAuthenticationFlowUseCase.instance, fakeStartAuthenticationFlowUseCase.instance,
FakeVectorOverrides() FakeVectorOverrides(),
aBuildMeta()
).also { ).also {
viewModel = it viewModel = it
initialState = state initialState = state

View File

@ -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<ConnectivityManager>()
fun givenNoActiveConnection() {
every { instance.activeNetwork } returns null
}
fun givenHasActiveConnection() {
val network = mockk<Network>()
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
}
}

View File

@ -18,6 +18,7 @@ package im.vector.app.test.fakes
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.net.ConnectivityManager
import android.net.Uri import android.net.Uri
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import io.mockk.every import io.mockk.every
@ -48,4 +49,21 @@ class FakeContext(
fun givenMissingSafeOutputStreamFor(uri: Uri) { fun givenMissingSafeOutputStreamFor(uri: Uri) {
every { contentResolver.openOutputStream(uri, "wt") } returns null 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 <T> givenService(name: String, klass: Class<T>, 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)
}
} }

View File

@ -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<NetworkCapabilities>()
fun givenTransports(vararg type: Int) {
every { instance.hasTransport(any()) } answers {
val input = it.invocation.args.first() as Int
type.contains(input)
}
}
}

View File

@ -18,9 +18,11 @@ package im.vector.app.test.fakes
import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase
import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult
import im.vector.app.test.fixtures.aHomeserverUnavailableError
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.mockk import io.mockk.mockk
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.failure.Failure
class FakeStartAuthenticationFlowUseCase { class FakeStartAuthenticationFlowUseCase {
@ -29,4 +31,8 @@ class FakeStartAuthenticationFlowUseCase {
fun givenResult(config: HomeServerConnectionConfig, result: StartAuthenticationResult) { fun givenResult(config: HomeServerConnectionConfig, result: StartAuthenticationResult) {
coEvery { instance.execute(config) } returns result coEvery { instance.execute(config) } returns result
} }
fun givenHomeserverUnavailable(config: HomeServerConnectionConfig) {
coEvery { instance.execute(config) } throws aHomeserverUnavailableError()
}
} }

View File

@ -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.Failure
import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixError
import java.net.UnknownHostException
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
fun a401ServerError() = Failure.ServerError( fun a401ServerError() = Failure.ServerError(
MatrixError(MatrixError.M_UNAUTHORIZED, ""), HttpsURLConnection.HTTP_UNAUTHORIZED MatrixError(MatrixError.M_UNAUTHORIZED, ""), HttpsURLConnection.HTTP_UNAUTHORIZED
) )
fun aHomeserverUnavailableError() = Failure.NetworkConnection(UnknownHostException())