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
}
@Provides
@Singleton
fun providesBuildMeta() = BuildMeta()

View file

@ -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<ConnectivityManager>()!!
return if (buildMeta.sdkInt > Build.VERSION_CODES.M) {
val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
when {
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.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<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(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(

View file

@ -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) {

View file

@ -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

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.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 <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.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()
}
}

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.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())