mirror of
https://github.com/bitwarden/android.git
synced 2026-05-09 13:29:18 -05:00
Compare commits
5 Commits
retro-agen
...
v2025.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cd289cc89 | ||
|
|
cccf3a2904 | ||
|
|
ea6d561b0d | ||
|
|
ffc3784bbb | ||
|
|
d63d6bd33c |
@@ -159,6 +159,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
get() = settingsDiskSource
|
||||
.getShouldShowAddLoginCoachMarkFlow()
|
||||
.map { it ?: true }
|
||||
.mapFalseIfAnyLoginCiphersAvailable()
|
||||
.combine(
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
|
||||
) { shouldShow, featureIsEnabled ->
|
||||
@@ -172,6 +173,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
get() = settingsDiskSource
|
||||
.getShouldShowGeneratorCoachMarkFlow()
|
||||
.map { it ?: true }
|
||||
.mapFalseIfAnyLoginCiphersAvailable()
|
||||
.combine(
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
|
||||
) { shouldShow, featureFlagEnabled ->
|
||||
@@ -294,4 +296,23 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
return settingsDiskSource.getShowAutoFillSettingBadge(userId) ?: false &&
|
||||
!autofillEnabledManager.isAutofillEnabled
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are any existing "Login" type ciphers then we'll map the current value
|
||||
* of the receiver Flow to `false`.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun Flow<Boolean>.mapFalseIfAnyLoginCiphersAvailable(): Flow<Boolean> =
|
||||
authDiskSource
|
||||
.activeUserIdChangesFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { activeUserId ->
|
||||
combine(
|
||||
flow = this,
|
||||
flow2 = vaultDiskSource.getCiphers(activeUserId),
|
||||
) { currentValue, ciphers ->
|
||||
currentValue && ciphers.none { it.login != null }
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
@@ -93,13 +93,21 @@ class PolicyManagerImpl(
|
||||
organization: SyncResponseJson.Profile.Organization,
|
||||
policyType: PolicyTypeJson,
|
||||
): Boolean =
|
||||
if (policyType == PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT) {
|
||||
organization.type == OrganizationType.OWNER
|
||||
} else if (policyType == PolicyTypeJson.PASSWORD_GENERATOR) {
|
||||
false
|
||||
} else {
|
||||
(organization.type == OrganizationType.OWNER ||
|
||||
organization.type == OrganizationType.ADMIN) ||
|
||||
organization.permissions.shouldManagePolicies
|
||||
when (policyType) {
|
||||
PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT -> {
|
||||
organization.type == OrganizationType.OWNER
|
||||
}
|
||||
|
||||
PolicyTypeJson.PASSWORD_GENERATOR,
|
||||
PolicyTypeJson.REMOVE_UNLOCK_WITH_PIN,
|
||||
-> {
|
||||
false
|
||||
}
|
||||
|
||||
else -> {
|
||||
(organization.type == OrganizationType.OWNER ||
|
||||
organization.type == OrganizationType.ADMIN) ||
|
||||
organization.permissions.shouldManagePolicies
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,10 +189,12 @@ class TwoFactorLoginViewModel @Inject constructor(
|
||||
* Update the state with the new text and enable or disable the continue button.
|
||||
*/
|
||||
private fun handleCodeInputChanged(action: TwoFactorLoginAction.CodeInputChanged) {
|
||||
@Suppress("MagicNumber")
|
||||
val minLength = if (state.isNewDeviceVerification) 8 else 6
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
codeInput = action.input,
|
||||
isContinueButtonEnabled = action.input.length >= 6,
|
||||
isContinueButtonEnabled = action.input.length >= minLength,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
@@ -22,7 +23,9 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconRes
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemScreen
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingsAction.Internal
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -36,7 +39,7 @@ private const val KEY_STATE = "state"
|
||||
/**
|
||||
* View model for the send screen.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class SendViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
@@ -45,6 +48,7 @@ class SendViewModel @Inject constructor(
|
||||
settingsRepo: SettingsRepository,
|
||||
private val vaultRepo: VaultRepository,
|
||||
policyManager: PolicyManager,
|
||||
private val networkConnectionManager: NetworkConnectionManager,
|
||||
) : BaseViewModel<SendState, SendEvent, SendAction>(
|
||||
// We load the state from the savedStateHandle for testing purposes.
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
@@ -109,6 +113,22 @@ class SendViewModel @Inject constructor(
|
||||
is SendAction.Internal.SendDataReceive -> handleSendDataReceive(action)
|
||||
|
||||
is SendAction.Internal.PolicyUpdateReceive -> handlePolicyUpdateReceive(action)
|
||||
|
||||
SendAction.Internal.InternetConnectionErrorReceived -> {
|
||||
handleInternetConnectionErrorReceived()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInternetConnectionErrorReceived() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
isRefreshing = false,
|
||||
dialogState = SendState.DialogState.Error(
|
||||
R.string.internet_connection_required_title.asText(),
|
||||
R.string.internet_connection_required_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePullToRefreshEnableReceive(
|
||||
@@ -250,10 +270,25 @@ class SendViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleSyncClick() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = SendState.DialogState.Loading(R.string.syncing.asText()))
|
||||
if (networkConnectionManager.isNetworkConnected) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = SendState.DialogState.Loading(
|
||||
message = R.string.syncing.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
vaultRepo.sync(forced = true)
|
||||
} else {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = SendState.DialogState.Error(
|
||||
R.string.internet_connection_required_title.asText(),
|
||||
R.string.internet_connection_required_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
vaultRepo.sync(forced = true)
|
||||
}
|
||||
|
||||
private fun handleCopyClick(action: SendAction.CopyClick) {
|
||||
@@ -307,11 +342,17 @@ class SendViewModel @Inject constructor(
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun handleRefreshPull() {
|
||||
mutableStateFlow.update { it.copy(isRefreshing = true) }
|
||||
// The Pull-To-Refresh composable is already in the refreshing state.
|
||||
// We will reset that state when sendDataStateFlow emits later on.
|
||||
vaultRepo.sync(forced = false)
|
||||
viewModelScope.launch {
|
||||
delay(250)
|
||||
if (networkConnectionManager.isNetworkConnected) {
|
||||
vaultRepo.sync(forced = false)
|
||||
} else {
|
||||
sendAction(SendAction.Internal.InternetConnectionErrorReceived)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,6 +600,11 @@ sealed class SendAction {
|
||||
data class PolicyUpdateReceive(
|
||||
val policyDisablesSend: Boolean,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the there is not internet connection.
|
||||
*/
|
||||
data object InternetConnectionErrorReceived : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingMa
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.toFido2AssertionRequestOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.toFido2CreateRequestOrNull
|
||||
@@ -83,6 +84,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
import com.x8bit.bitwarden.ui.vault.util.toVaultItemCipherType
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -114,6 +116,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
private val fido2OriginManager: Fido2OriginManager,
|
||||
private val fido2CredentialManager: Fido2CredentialManager,
|
||||
private val organizationEventManager: OrganizationEventManager,
|
||||
private val networkConnectionManager: NetworkConnectionManager,
|
||||
) : BaseViewModel<VaultItemListingState, VaultItemListingEvent, VaultItemListingsAction>(
|
||||
initialState = run {
|
||||
val userState = requireNotNull(authRepository.userStateFlow.value)
|
||||
@@ -306,11 +309,17 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
vaultRepository.sync(forced = true)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun handleRefreshPull() {
|
||||
mutableStateFlow.update { it.copy(isRefreshing = true) }
|
||||
// The Pull-To-Refresh composable is already in the refreshing state.
|
||||
// We will reset that state when sendDataStateFlow emits later on.
|
||||
vaultRepository.sync(forced = false)
|
||||
viewModelScope.launch {
|
||||
delay(250)
|
||||
if (networkConnectionManager.isNetworkConnected) {
|
||||
vaultRepository.sync(forced = false)
|
||||
} else {
|
||||
sendAction(VaultItemListingsAction.Internal.InternetConnectionErrorReceived)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleConfirmOverwriteExistingPasskeyClick(
|
||||
@@ -1018,14 +1027,25 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleSyncClick() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VaultItemListingState.DialogState.Loading(
|
||||
message = R.string.syncing.asText(),
|
||||
),
|
||||
)
|
||||
if (networkConnectionManager.isNetworkConnected) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VaultItemListingState.DialogState.Loading(
|
||||
message = R.string.syncing.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
vaultRepository.sync(forced = true)
|
||||
} else {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VaultItemListingState.DialogState.Error(
|
||||
R.string.internet_connection_required_title.asText(),
|
||||
R.string.internet_connection_required_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
vaultRepository.sync(forced = true)
|
||||
}
|
||||
|
||||
private fun handleSearchIconClick() {
|
||||
@@ -1150,6 +1170,22 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
is VaultItemListingsAction.Internal.Fido2AssertionResultReceive -> {
|
||||
handleFido2AssertionResultReceive(action)
|
||||
}
|
||||
|
||||
VaultItemListingsAction.Internal.InternetConnectionErrorReceived -> {
|
||||
handleInternetConnectionErrorReceived()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInternetConnectionErrorReceived() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
isRefreshing = false,
|
||||
dialogState = VaultItemListingState.DialogState.Error(
|
||||
R.string.internet_connection_required_title.asText(),
|
||||
R.string.internet_connection_required_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2768,6 +2804,11 @@ sealed class VaultItemListingsAction {
|
||||
data class Fido2AssertionResultReceive(
|
||||
val result: Fido2CredentialAssertionResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the there is not internet connection.
|
||||
*/
|
||||
data object InternetConnectionErrorReceived : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -303,10 +303,10 @@ class FirstTimeActionManagerTest {
|
||||
|
||||
@Test
|
||||
fun `shouldShowAddLoginCoachMarkFlow updates when disk source updates`() = runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
// Enable the feature for this test.
|
||||
mutableOnboardingFeatureFlow.update { true }
|
||||
firstTimeActionManager.shouldShowAddLoginCoachMarkFlow.test {
|
||||
// Null will be mapped to false.
|
||||
assertTrue(awaitItem())
|
||||
fakeSettingsDiskSource.storeShouldShowAddLoginCoachMark(shouldShow = false)
|
||||
assertFalse(awaitItem())
|
||||
@@ -316,15 +316,45 @@ class FirstTimeActionManagerTest {
|
||||
@Test
|
||||
fun `shouldShowAddLoginCoachMarkFlow updates when feature flag for onboarding updates`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
firstTimeActionManager.shouldShowAddLoginCoachMarkFlow.test {
|
||||
// Null will be mapped to false but feature being "off" will override to true.
|
||||
assertFalse(awaitItem())
|
||||
mutableOnboardingFeatureFlow.update { true }
|
||||
// Will use the value from disk source (null ?: false).
|
||||
assertTrue(awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `if there are any login ciphers available for the active user should not show add login coach marks`() =
|
||||
runTest {
|
||||
val mockJsonWithNoLogin = mockk<SyncResponseJson.Cipher> {
|
||||
every { login } returns null
|
||||
}
|
||||
val mockJsonWithLogin = mockk<SyncResponseJson.Cipher> {
|
||||
every { login } returns mockk()
|
||||
}
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
// Enable feature flag so flow emits updates from disk.
|
||||
mutableOnboardingFeatureFlow.update { true }
|
||||
mutableCiphersListFlow.update {
|
||||
listOf(
|
||||
mockJsonWithNoLogin,
|
||||
mockJsonWithNoLogin,
|
||||
)
|
||||
}
|
||||
firstTimeActionManager.shouldShowAddLoginCoachMarkFlow.test {
|
||||
assertTrue(awaitItem())
|
||||
mutableCiphersListFlow.update {
|
||||
listOf(
|
||||
mockJsonWithLogin,
|
||||
mockJsonWithNoLogin,
|
||||
)
|
||||
}
|
||||
assertFalse(awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `markCoachMarkTourCompleted for the ADD_LOGIN type sets the value to true in the disk source for should show add logins coach mark`() {
|
||||
@@ -335,10 +365,10 @@ class FirstTimeActionManagerTest {
|
||||
|
||||
@Test
|
||||
fun `shouldShowGeneratorCoachMarkFlow updates when disk source updates`() = runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
// Enable feature flag so flow emits updates from disk.
|
||||
mutableOnboardingFeatureFlow.update { true }
|
||||
firstTimeActionManager.shouldShowGeneratorCoachMarkFlow.test {
|
||||
// Null will be mapped to false.
|
||||
assertTrue(awaitItem())
|
||||
fakeSettingsDiskSource.storeShouldShowGeneratorCoachMark(shouldShow = false)
|
||||
assertFalse(awaitItem())
|
||||
@@ -348,8 +378,8 @@ class FirstTimeActionManagerTest {
|
||||
@Test
|
||||
fun `shouldShowGeneratorCoachMarkFlow updates when onboarding feature value changes`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
firstTimeActionManager.shouldShowGeneratorCoachMarkFlow.test {
|
||||
// Null will be mapped to false
|
||||
assertFalse(awaitItem())
|
||||
mutableOnboardingFeatureFlow.update { true }
|
||||
// Take the value from disk.
|
||||
@@ -357,6 +387,37 @@ class FirstTimeActionManagerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `if there are any login ciphers available for the active user should not show generator coach marks`() =
|
||||
runTest {
|
||||
val mockJsonWithNoLogin = mockk<SyncResponseJson.Cipher> {
|
||||
every { login } returns null
|
||||
}
|
||||
val mockJsonWithLogin = mockk<SyncResponseJson.Cipher> {
|
||||
every { login } returns mockk()
|
||||
}
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
// Enable feature flag so flow emits updates from disk.
|
||||
mutableOnboardingFeatureFlow.update { true }
|
||||
mutableCiphersListFlow.update {
|
||||
listOf(
|
||||
mockJsonWithNoLogin,
|
||||
mockJsonWithNoLogin,
|
||||
)
|
||||
}
|
||||
firstTimeActionManager.shouldShowGeneratorCoachMarkFlow.test {
|
||||
assertTrue(awaitItem())
|
||||
mutableCiphersListFlow.update {
|
||||
listOf(
|
||||
mockJsonWithLogin,
|
||||
mockJsonWithNoLogin,
|
||||
)
|
||||
}
|
||||
assertFalse(awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `markCoachMarkTourCompleted for the GENERATOR type sets the value to true in the disk source for should show generator coach mark`() {
|
||||
|
||||
@@ -310,6 +310,33 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `Continue buttons should only be enabled when code is 8 digit enough on isNewDeviceVerification`() {
|
||||
val initialState = DEFAULT_STATE.copy(isNewDeviceVerification = true)
|
||||
val viewModel = createViewModel(initialState)
|
||||
viewModel.trySendAction(TwoFactorLoginAction.CodeInputChanged("123456"))
|
||||
|
||||
// 6 digit should be false when isNewDeviceVerification is true.
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
codeInput = "123456",
|
||||
isContinueButtonEnabled = false,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
// Set it to true.
|
||||
viewModel.trySendAction(TwoFactorLoginAction.CodeInputChanged("12345678"))
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
codeInput = "12345678",
|
||||
isContinueButtonEnabled = true,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueButtonClick login returns success should update loadingDialogState`() = runTest {
|
||||
coEvery {
|
||||
|
||||
@@ -6,6 +6,7 @@ import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
@@ -24,6 +25,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson.Policy
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy
|
||||
@@ -177,6 +179,79 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||
createMockPolicy(
|
||||
isEnabled = true,
|
||||
type = PolicyTypeJson.REMOVE_UNLOCK_WITH_PIN,
|
||||
organizationId = "organizationUser",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
removeUnlockWithPinPolicyEnabled = true,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remove pin policy is true when user role is ADMIN`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
mutableRemovePinPolicyFlow.emit(
|
||||
listOf(
|
||||
createMockPolicy(
|
||||
organizationId = "organizationAdmin",
|
||||
isEnabled = true,
|
||||
type = PolicyTypeJson.REMOVE_UNLOCK_WITH_PIN,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
removeUnlockWithPinPolicyEnabled = true,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remove pin policy is true when user role is OWNER`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
mutableRemovePinPolicyFlow.emit(
|
||||
listOf(
|
||||
createMockPolicy(
|
||||
organizationId = "organizationOwner",
|
||||
isEnabled = true,
|
||||
type = PolicyTypeJson.REMOVE_UNLOCK_WITH_PIN,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
removeUnlockWithPinPolicyEnabled = true,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remove pin policy is true when user role is CUSTOM with manage policies`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
mutableRemovePinPolicyFlow.emit(
|
||||
listOf(
|
||||
createMockPolicy(
|
||||
organizationId = "organizationCustom",
|
||||
isEnabled = true,
|
||||
type = PolicyTypeJson.REMOVE_UNLOCK_WITH_PIN,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -909,7 +984,36 @@ private val DEFAULT_USER_STATE = UserState(
|
||||
isVaultUnlocked = true,
|
||||
needsPasswordReset = false,
|
||||
isBiometricsEnabled = false,
|
||||
organizations = emptyList(),
|
||||
organizations = listOf(
|
||||
Organization(
|
||||
id = "organizationUser",
|
||||
name = "Organization User",
|
||||
shouldUseKeyConnector = false,
|
||||
shouldManageResetPassword = false,
|
||||
role = OrganizationType.USER,
|
||||
),
|
||||
Organization(
|
||||
id = "organizationAdmin",
|
||||
name = "Organization Admin",
|
||||
shouldUseKeyConnector = false,
|
||||
shouldManageResetPassword = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
Organization(
|
||||
id = "organizationOwner",
|
||||
name = "Organization Owner",
|
||||
shouldUseKeyConnector = false,
|
||||
shouldManageResetPassword = false,
|
||||
role = OrganizationType.OWNER,
|
||||
),
|
||||
Organization(
|
||||
id = "organizationCustom",
|
||||
name = "Organization Owner",
|
||||
shouldUseKeyConnector = false,
|
||||
shouldManageResetPassword = false,
|
||||
role = OrganizationType.CUSTOM,
|
||||
),
|
||||
),
|
||||
needsMasterPassword = false,
|
||||
trustedDevice = null,
|
||||
hasMasterPassword = true,
|
||||
|
||||
@@ -5,6 +5,7 @@ import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
@@ -27,8 +28,10 @@ import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -58,6 +61,10 @@ class SendViewModelTest : BaseViewModelTest() {
|
||||
every { getActivePoliciesFlow(type = PolicyTypeJson.DISABLE_SEND) } returns emptyFlow()
|
||||
}
|
||||
|
||||
private val networkConnectionManager: NetworkConnectionManager = mockk {
|
||||
every { isNetworkConnected } returns true
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(SendData::toViewState)
|
||||
@@ -240,6 +247,27 @@ class SendViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on SyncClick should show the no network dialog if no connection is available`() {
|
||||
val viewModel = createViewModel()
|
||||
every {
|
||||
networkConnectionManager.isNetworkConnected
|
||||
} returns false
|
||||
viewModel.trySendAction(SendAction.SyncClick)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = SendState.DialogState.Error(
|
||||
R.string.internet_connection_required_title.asText(),
|
||||
R.string.internet_connection_required_message.asText(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify(exactly = 0) {
|
||||
vaultRepo.sync(forced = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CopyClick should call setText on the ClipboardManager`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
@@ -414,18 +442,44 @@ class SendViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `RefreshPull should call vault repository sync`() {
|
||||
fun `RefreshPull should call vault repository sync`() = runTest {
|
||||
every { vaultRepo.sync(forced = false) } just runs
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.trySendAction(SendAction.RefreshPull)
|
||||
|
||||
advanceTimeBy(300)
|
||||
verify(exactly = 1) {
|
||||
vaultRepo.sync(forced = false)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `RefreshPull should show network error if no internet connection`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
every {
|
||||
networkConnectionManager.isNetworkConnected
|
||||
} returns false
|
||||
|
||||
viewModel.trySendAction(SendAction.RefreshPull)
|
||||
advanceTimeBy(300)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
isRefreshing = false,
|
||||
dialogState = SendState.DialogState.Error(
|
||||
R.string.internet_connection_required_title.asText(),
|
||||
R.string.internet_connection_required_message.asText(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify(exactly = 0) {
|
||||
vaultRepo.sync(forced = false)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PullToRefreshEnableReceive should update isPullToRefreshEnabled`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
@@ -457,6 +511,7 @@ class SendViewModelTest : BaseViewModelTest() {
|
||||
settingsRepo = settingsRepository,
|
||||
vaultRepo = vaultRepository,
|
||||
policyManager = policyManager,
|
||||
networkConnectionManager = networkConnectionManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
@@ -89,8 +90,10 @@ import io.mockk.runs
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
@@ -185,6 +188,10 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
every { trackEvent(event = any()) } just runs
|
||||
}
|
||||
|
||||
private val networkConnectionManager: NetworkConnectionManager = mockk {
|
||||
every { isNetworkConnected } returns true
|
||||
}
|
||||
|
||||
private val initialState = createVaultItemListingState()
|
||||
private val initialSavedStateHandle = createSavedStateHandleWithVaultItemListingType(
|
||||
vaultItemListingType = VaultItemListingType.Login,
|
||||
@@ -363,6 +370,27 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on SyncClick should show the no network dialog if no connection is available`() {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
every {
|
||||
networkConnectionManager.isNetworkConnected
|
||||
} returns false
|
||||
viewModel.trySendAction(VaultItemListingsAction.SyncClick)
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
dialogState = VaultItemListingState.DialogState.Error(
|
||||
R.string.internet_connection_required_title.asText(),
|
||||
R.string.internet_connection_required_message.asText(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify(exactly = 0) {
|
||||
vaultRepository.sync(forced = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `ItemClick for vault item when accessibility autofill should post to the accessibilitySelectionManager`() =
|
||||
@@ -2451,17 +2479,43 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
assertTrue(viewModel.stateFlow.value.isIconLoadingDisabled)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `RefreshPull should call vault repository sync`() {
|
||||
fun `RefreshPull should call vault repository sync`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
viewModel.trySendAction(VaultItemListingsAction.RefreshPull)
|
||||
|
||||
advanceTimeBy(300)
|
||||
verify(exactly = 1) {
|
||||
vaultRepository.sync(forced = false)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `RefreshPull should show network error if no internet connection`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
every {
|
||||
networkConnectionManager.isNetworkConnected
|
||||
} returns false
|
||||
|
||||
viewModel.trySendAction(VaultItemListingsAction.RefreshPull)
|
||||
advanceTimeBy(300)
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
isRefreshing = false,
|
||||
dialogState = VaultItemListingState.DialogState.Error(
|
||||
R.string.internet_connection_required_title.asText(),
|
||||
R.string.internet_connection_required_message.asText(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify(exactly = 0) {
|
||||
vaultRepository.sync(forced = false)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PullToRefreshEnableReceive should update isPullToRefreshEnabled`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
@@ -4461,6 +4515,25 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `InternetConnectionErrorReceived should show network error if no internet connection`() =
|
||||
runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.Internal.InternetConnectionErrorReceived,
|
||||
)
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
isRefreshing = false,
|
||||
dialogState = VaultItemListingState.DialogState.Error(
|
||||
R.string.internet_connection_required_title.asText(),
|
||||
R.string.internet_connection_required_message.asText(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun createSavedStateHandleWithVaultItemListingType(
|
||||
vaultItemListingType: VaultItemListingType,
|
||||
@@ -4523,6 +4596,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
fido2CredentialManager = fido2CredentialManager,
|
||||
organizationEventManager = organizationEventManager,
|
||||
fido2OriginManager = fido2OriginManager,
|
||||
networkConnectionManager = networkConnectionManager,
|
||||
)
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
|
||||
Reference in New Issue
Block a user