mirror of
https://github.com/bitwarden/android.git
synced 2024-11-27 12:00:19 +03:00
BIT-1286: Check for Internet connection before logging in via SSO (#695)
This commit is contained in:
parent
6dd4a31a57
commit
49b4c23466
8 changed files with 161 additions and 4 deletions
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package com.x8bit.bitwarden.data.platform.manager.di
|
package com.x8bit.bitwarden.data.platform.manager.di
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
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.AppForegroundManagerImpl
|
||||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
|
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManagerImpl
|
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.PushManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.PushManagerImpl
|
import com.x8bit.bitwarden.data.platform.manager.PushManagerImpl
|
||||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||||
|
@ -79,6 +82,14 @@ object PlatformManagerModule {
|
||||||
dispatcherManager = dispatcherManager,
|
dispatcherManager = dispatcherManager,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideNetworkConnectionManager(
|
||||||
|
application: Application,
|
||||||
|
): NetworkConnectionManager = NetworkConnectionManagerImpl(
|
||||||
|
context = application.applicationContext,
|
||||||
|
)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providePushManager(
|
fun providePushManager(
|
||||||
|
|
|
@ -68,7 +68,7 @@ fun EnterpriseSignOnScreen(
|
||||||
is EnterpriseSignOnState.DialogState.Error -> {
|
is EnterpriseSignOnState.DialogState.Error -> {
|
||||||
BitwardenBasicDialog(
|
BitwardenBasicDialog(
|
||||||
visibilityState = BasicDialogState.Shown(
|
visibilityState = BasicDialogState.Shown(
|
||||||
title = R.string.an_error_has_occurred.asText(),
|
title = dialog.title ?: R.string.an_error_has_occurred.asText(),
|
||||||
message = dialog.message,
|
message = dialog.message,
|
||||||
),
|
),
|
||||||
onDismissRequest = remember(viewModel) {
|
onDismissRequest = remember(viewModel) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.enterprisesignon
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import com.x8bit.bitwarden.R
|
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.BaseViewModel
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
@ -18,6 +19,7 @@ private const val KEY_STATE = "state"
|
||||||
*/
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class EnterpriseSignOnViewModel @Inject constructor(
|
class EnterpriseSignOnViewModel @Inject constructor(
|
||||||
|
private val networkConnectionManager: NetworkConnectionManager,
|
||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
) : BaseViewModel<EnterpriseSignOnState, EnterpriseSignOnEvent, EnterpriseSignOnAction>(
|
) : BaseViewModel<EnterpriseSignOnState, EnterpriseSignOnEvent, EnterpriseSignOnAction>(
|
||||||
initialState = savedStateHandle[KEY_STATE]
|
initialState = savedStateHandle[KEY_STATE]
|
||||||
|
@ -50,11 +52,23 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||||
// TODO BIT-816: submit request for single sign on
|
// TODO BIT-816: submit request for single sign on
|
||||||
sendEvent(EnterpriseSignOnEvent.ShowToast("Not yet implemented."))
|
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()) {
|
if (state.orgIdentifierInput.isBlank()) {
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
dialogState = EnterpriseSignOnState.DialogState.Error(
|
dialogState = EnterpriseSignOnState.DialogState.Error(
|
||||||
R.string.validation_field_required.asText(
|
message = R.string.validation_field_required.asText(
|
||||||
R.string.org_identifier.asText(),
|
R.string.org_identifier.asText(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -83,10 +97,12 @@ data class EnterpriseSignOnState(
|
||||||
*/
|
*/
|
||||||
sealed class DialogState : Parcelable {
|
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
|
@Parcelize
|
||||||
data class Error(
|
data class Error(
|
||||||
|
val title: Text? = null,
|
||||||
val message: Text,
|
val message: Text,
|
||||||
) : DialogState()
|
) : DialogState()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.enterprisesignon
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.x8bit.bitwarden.R
|
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.BaseViewModelTest
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
@ -67,7 +68,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
DEFAULT_STATE.copy(
|
DEFAULT_STATE.copy(
|
||||||
dialogState = EnterpriseSignOnState.DialogState.Error(
|
dialogState = EnterpriseSignOnState.DialogState.Error(
|
||||||
R.string.validation_field_required.asText(
|
message = R.string.validation_field_required.asText(
|
||||||
R.string.org_identifier.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
|
@Test
|
||||||
fun `OrgIdentifierInputChange should update organization identifier`() = runTest {
|
fun `OrgIdentifierInputChange should update organization identifier`() = runTest {
|
||||||
val input = "input"
|
val input = "input"
|
||||||
|
@ -141,7 +164,9 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||||
savedStateHandle: SavedStateHandle = SavedStateHandle(
|
savedStateHandle: SavedStateHandle = SavedStateHandle(
|
||||||
initialState = mapOf("state" to initialState),
|
initialState = mapOf("state" to initialState),
|
||||||
),
|
),
|
||||||
|
isNetworkConnected: Boolean = true,
|
||||||
): EnterpriseSignOnViewModel = EnterpriseSignOnViewModel(
|
): EnterpriseSignOnViewModel = EnterpriseSignOnViewModel(
|
||||||
|
networkConnectionManager = FakeNetworkConnectionManager(isNetworkConnected),
|
||||||
savedStateHandle = savedStateHandle,
|
savedStateHandle = savedStateHandle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue