BIT-1286: Check for Internet connection before logging in via SSO (#695)

This commit is contained in:
Sean Weiser 2024-01-20 11:06:55 -06:00 committed by Álison Fernandes
parent 6dd4a31a57
commit 49b4c23466
8 changed files with 161 additions and 4 deletions

View file

@ -0,0 +1,12 @@
package com.x8bit.bitwarden.data.platform.manager
/**
* Manager to detect and handle changes to network connectivity.
*/
interface NetworkConnectionManager {
/**
* Returns `true` if the application has a network connection and access to the Internet is
* available.
*/
val isNetworkConnected: Boolean
}

View file

@ -0,0 +1,22 @@
package com.x8bit.bitwarden.data.platform.manager
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
/**
* Primary implementation of [NetworkConnectionManager].
*/
class NetworkConnectionManagerImpl(
context: Context,
) : NetworkConnectionManager {
private val connectivityManager: ConnectivityManager = context
.applicationContext
.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
override val isNetworkConnected: Boolean
get() = connectivityManager
.getNetworkCapabilities(connectivityManager.activeNetwork)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
?: false
}

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.manager.di
import android.app.Application
import android.content.Context
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
@ -12,6 +13,8 @@ import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManagerImpl
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManagerImpl
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManagerImpl
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.PushManagerImpl
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
@ -79,6 +82,14 @@ object PlatformManagerModule {
dispatcherManager = dispatcherManager,
)
@Provides
@Singleton
fun provideNetworkConnectionManager(
application: Application,
): NetworkConnectionManager = NetworkConnectionManagerImpl(
context = application.applicationContext,
)
@Provides
@Singleton
fun providePushManager(

View file

@ -68,7 +68,7 @@ fun EnterpriseSignOnScreen(
is EnterpriseSignOnState.DialogState.Error -> {
BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(),
title = dialog.title ?: R.string.an_error_has_occurred.asText(),
message = dialog.message,
),
onDismissRequest = remember(viewModel) {

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.enterprisesignon
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
@ -18,6 +19,7 @@ private const val KEY_STATE = "state"
*/
@HiltViewModel
class EnterpriseSignOnViewModel @Inject constructor(
private val networkConnectionManager: NetworkConnectionManager,
savedStateHandle: SavedStateHandle,
) : BaseViewModel<EnterpriseSignOnState, EnterpriseSignOnEvent, EnterpriseSignOnAction>(
initialState = savedStateHandle[KEY_STATE]
@ -50,11 +52,23 @@ class EnterpriseSignOnViewModel @Inject constructor(
// TODO BIT-816: submit request for single sign on
sendEvent(EnterpriseSignOnEvent.ShowToast("Not yet implemented."))
if (!networkConnectionManager.isNetworkConnected) {
mutableStateFlow.update {
it.copy(
dialogState = EnterpriseSignOnState.DialogState.Error(
title = R.string.internet_connection_required_title.asText(),
message = R.string.internet_connection_required_message.asText(),
),
)
}
return
}
if (state.orgIdentifierInput.isBlank()) {
mutableStateFlow.update {
it.copy(
dialogState = EnterpriseSignOnState.DialogState.Error(
R.string.validation_field_required.asText(
message = R.string.validation_field_required.asText(
R.string.org_identifier.asText(),
),
),
@ -83,10 +97,12 @@ data class EnterpriseSignOnState(
*/
sealed class DialogState : Parcelable {
/**
* Represents an error dialog with the given [message].
* Represents an error dialog with the given [message] and optional [title]. It no title
* is specified a default will be provided.
*/
@Parcelize
data class Error(
val title: Text? = null,
val message: Text,
) : DialogState()

View file

@ -0,0 +1,64 @@
package com.x8bit.bitwarden.data.platform.manager
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import io.mockk.every
import io.mockk.mockk
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
class NetworkConnectionManagerTest {
@Test
fun `isNetworkConnected should return false if no active network`() {
val connectivityManager: ConnectivityManager = mockk {
every { activeNetwork } returns null
every { getNetworkCapabilities(any()) } returns null
}
val networkConnectionManager = createNetworkConnectionManager(connectivityManager)
assertFalse(networkConnectionManager.isNetworkConnected)
}
@Test
fun `isNetworkConnected should return false if active network has no Internet capabilities`() {
val network: Network = mockk()
val networkCapabilities: NetworkCapabilities = mockk {
every { hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) } returns false
}
val connectivityManager: ConnectivityManager = mockk {
every { activeNetwork } returns network
every { getNetworkCapabilities(network) } returns networkCapabilities
}
val networkConnectionManager = createNetworkConnectionManager(connectivityManager)
assertFalse(networkConnectionManager.isNetworkConnected)
}
@Test
fun `isNetworkConnected should return true if active network has Internet capabilities`() {
val network: Network = mockk()
val networkCapabilities: NetworkCapabilities = mockk {
every { hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) } returns true
}
val connectivityManager: ConnectivityManager = mockk {
every { activeNetwork } returns network
every { getNetworkCapabilities(network) } returns networkCapabilities
}
val networkConnectionManager = createNetworkConnectionManager(connectivityManager)
assertTrue(networkConnectionManager.isNetworkConnected)
}
private fun createNetworkConnectionManager(
connectivityManager: ConnectivityManager,
): NetworkConnectionManager {
val appContext: Context = mockk {
every { getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager
}
val context: Context = mockk {
every { applicationContext } returns appContext
}
return NetworkConnectionManagerImpl(context)
}
}

View file

@ -0,0 +1,7 @@
package com.x8bit.bitwarden.data.platform.manager.util
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
class FakeNetworkConnectionManager(
override val isNetworkConnected: Boolean,
) : NetworkConnectionManager

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.enterprisesignon
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.util.FakeNetworkConnectionManager
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import kotlinx.coroutines.test.runTest
@ -67,7 +68,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
assertEquals(
DEFAULT_STATE.copy(
dialogState = EnterpriseSignOnState.DialogState.Error(
R.string.validation_field_required.asText(
message = R.string.validation_field_required.asText(
R.string.org_identifier.asText(),
),
),
@ -81,6 +82,28 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `LogInClick with no Internet should emit ShowToast and show error dialog`() = runTest {
val viewModel = createViewModel(isNetworkConnected = false)
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(EnterpriseSignOnAction.LogInClick)
assertEquals(
DEFAULT_STATE.copy(
dialogState = EnterpriseSignOnState.DialogState.Error(
title = R.string.internet_connection_required_title.asText(),
message = R.string.internet_connection_required_message.asText(),
),
),
viewModel.stateFlow.value,
)
assertEquals(
EnterpriseSignOnEvent.ShowToast("Not yet implemented."),
awaitItem(),
)
}
}
@Test
fun `OrgIdentifierInputChange should update organization identifier`() = runTest {
val input = "input"
@ -141,7 +164,9 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
savedStateHandle: SavedStateHandle = SavedStateHandle(
initialState = mapOf("state" to initialState),
),
isNetworkConnected: Boolean = true,
): EnterpriseSignOnViewModel = EnterpriseSignOnViewModel(
networkConnectionManager = FakeNetworkConnectionManager(isNetworkConnected),
savedStateHandle = savedStateHandle,
)