adding test case around invalid deeplinks within the onboarding flow
This commit is contained in:
parent
797e0ee706
commit
75d038b058
|
@ -187,7 +187,6 @@ object VectorStaticModule {
|
||||||
return analyticsConfig
|
return analyticsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesBuildMeta() = BuildMeta()
|
fun providesBuildMeta() = BuildMeta()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
Loading…
Reference in New Issue