diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/manager/AutofillCompletionManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/manager/AutofillCompletionManagerImpl.kt index 4a49f0b510..81baecadff 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/manager/AutofillCompletionManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/manager/AutofillCompletionManagerImpl.kt @@ -107,10 +107,11 @@ class AutofillCompletionManagerImpl( cipherView: CipherView, ) { val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true + val totpAvailableViaPremiumOrOrganization = isPremium || cipherView.organizationUseTotp val totpCode = cipherView.login?.totp val isTotpDisabled = settingsRepository.isAutoCopyTotpDisabled - if (!isTotpDisabled && isPremium && totpCode != null) { + if (!isTotpDisabled && totpAvailableViaPremiumOrOrganization && totpCode != null) { val totpResult = vaultRepository.generateTotp( time = DateTime.now(), totpCode = totpCode, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/TotpCodeManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/TotpCodeManagerImpl.kt index f7a3086934..3869c3d7d5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/TotpCodeManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/TotpCodeManagerImpl.kt @@ -121,6 +121,7 @@ class TotpCodeManagerImpl( CipherRepromptType.PASSWORD -> true CipherRepromptType.NONE -> false }, + orgUsesTotp = cipher.organizationUseTotp, ) } .onFailure { diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/model/VerificationCodeItem.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/model/VerificationCodeItem.kt index 7be2dd29b9..fadc0f357b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/model/VerificationCodeItem.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/model/VerificationCodeItem.kt @@ -15,6 +15,7 @@ import com.bitwarden.vault.LoginUriView * @property name The name of the cipher item. * @property username The username associated with the item. * @property hasPasswordReprompt Indicates whether this item has a master password reprompt. + * @property orgUsesTotp if the org providing the cipher uses TOTP. */ data class VerificationCodeItem( val code: String, @@ -27,4 +28,5 @@ data class VerificationCodeItem( val name: String, val username: String?, val hasPasswordReprompt: Boolean, + val orgUsesTotp: Boolean, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt index 75f75f81f4..0c2630467c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt @@ -117,7 +117,7 @@ fun VaultItemLoginContent( Spacer(modifier = Modifier.height(8.dp)) TotpField( totpCodeItemData = totpCodeItemData, - isPremiumUser = loginItemState.isPremiumUser, + enabled = loginItemState.canViewTotpCode, onCopyTotpClick = vaultLoginItemTypeHandlers.onCopyTotpCodeClick, modifier = Modifier .fillMaxWidth() @@ -366,11 +366,11 @@ private fun PasswordHistoryCount( @Composable private fun TotpField( totpCodeItemData: TotpCodeItemData, - isPremiumUser: Boolean, + enabled: Boolean, onCopyTotpClick: () -> Unit, modifier: Modifier = Modifier, ) { - if (isPremiumUser) { + if (enabled) { Row { BitwardenTextFieldWithActions( label = stringResource(id = R.string.verification_code_totp), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index 767ee6d707..243dbe08dc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt @@ -1275,8 +1275,16 @@ data class VaultItemState( * @property totpCodeItemData The optional data related the TOTP code. * @property isPremiumUser Indicates if the user has subscribed to a premium * account. + * @property canViewTotpCode Indicates if the user can view an associated TOTP code. * @property fido2CredentialCreationDateText Optional creation date and time of the * FIDO2 credential associated with the login item. + * + * **NOTE** [canViewTotpCode] currently supports a deprecated edge case where an + * organization supports TOTP but not through the current premium model. + * This additional field is added to allow for [isPremiumUser] to be an independent + * value. + * @see [CipherView.organizationUseTotp] + * */ @Parcelize data class Login( @@ -1287,6 +1295,7 @@ data class VaultItemState( val passwordRevisionDate: String?, val totpCodeItemData: TotpCodeItemData?, val isPremiumUser: Boolean, + val canViewTotpCode: Boolean, val fido2CredentialCreationDateText: Text?, ) : ItemType() { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt index dc03e559c8..4288c172e9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt @@ -105,6 +105,7 @@ fun CipherView.toViewState( ), passwordHistoryCount = passwordHistory?.count(), isPremiumUser = isPremiumUser, + canViewTotpCode = isPremiumUser || this.organizationUseTotp, totpCodeItemData = totpCodeItemData, fido2CredentialCreationDateText = loginValues .fido2Credentials diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt index edcabb6dc8..5c577b63a9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt @@ -64,11 +64,12 @@ fun VaultData.toViewState( return if (filteredCipherViewList.isEmpty()) { VaultState.ViewState.NoItems } else { + val totpItems = filteredCipherViewList.filter { it.login?.totp != null } VaultState.ViewState.Content( totpItemsCount = if (isPremium) { - filteredCipherViewList.count { it.login?.totp != null } + totpItems.count() } else { - 0 + totpItems.count { it.organizationUseTotp } }, loginItemsCount = filteredCipherViewList.count { it.type == CipherType.LOGIN }, cardItemsCount = filteredCipherViewList.count { it.type == CipherType.CARD }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModel.kt index dc2e0bb931..3bd87f8e4a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModel.kt @@ -3,6 +3,8 @@ package com.x8bit.bitwarden.ui.vault.feature.verificationcode import android.os.Parcelable import androidx.lifecycle.viewModelScope import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository @@ -18,6 +20,7 @@ import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -32,6 +35,7 @@ import javax.inject.Inject @Suppress("TooManyFunctions") @HiltViewModel class VerificationCodeViewModel @Inject constructor( + authRepository: AuthRepository, private val clipboardManager: BitwardenClipboardManager, private val environmentRepository: EnvironmentRepository, private val settingsRepository: SettingsRepository, @@ -64,6 +68,13 @@ class VerificationCodeViewModel @Inject constructor( vaultRepository .getAuthCodesFlow() + .combine(authRepository.userStateFlow) { listDataState, userState -> + if (listDataState is DataState.Loaded) { + filterAuthCodesForDataState(listDataState.data, userState?.activeAccount) + } else { + listDataState + } + } .onEach { sendAction( VerificationCodeAction.Internal.AuthCodesReceive(it), @@ -294,7 +305,23 @@ class VerificationCodeViewModel @Inject constructor( } } -//endregion VerificationCode Handlers + /** + * Filter verification codes in the event that the user is not a "premium" account but + * has TOTP codes associated with a legacy organization. + */ + private fun filterAuthCodesForDataState( + authCodes: List, + userAccount: UserState.Account?, + ): DataState> { + val filteredAuthCodes = authCodes.mapNotNull { authCode -> + if (userAccount?.isPremium == true) { + authCode + } else { + authCode.takeIf { it.orgUsesTotp } + } + } + return DataState.Loaded(filteredAuthCodes) + } } /** diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/manager/AutofillCompletionManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/manager/AutofillCompletionManagerTest.kt index 773f4f004f..5829333448 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/manager/AutofillCompletionManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/manager/AutofillCompletionManagerTest.kt @@ -224,7 +224,7 @@ class AutofillCompletionManagerTest { @Suppress("MaxLineLength") @Test - fun `completeAutofill when filled partition, premium active user, a totp code, and totp generated succesfully should build a dataset, place it in a result Intent, copy totp, and finish the Activity`() { + fun `completeAutofill when filled partition, premium active user, a totp code, and totp generated successfully should build a dataset, place it in a result Intent, copy totp, and finish the Activity`() { val filledData: FilledData = mockk { every { filledPartitions } returns listOf(filledPartition) } @@ -277,8 +277,94 @@ class AutofillCompletionManagerTest { verify { activity.setResult(Activity.RESULT_OK, resultIntent) activity.finish() + activity.intent + clipboardManager.setText(any()) + mockIntent.getAutofillAssistStructureOrNull() + autofillParser.parse( + autofillAppInfo = autofillAppInfo, + assistStructure = assistStructure, + ) + filledPartition.buildDataset( + authIntentSender = null, + autofillAppInfo = autofillAppInfo, + ) + settingsRepository.isAutoCopyTotpDisabled + createAutofillSelectionResultIntent(dataset = dataset) + Toast.makeText( + context, + R.string.verification_code_totp, + Toast.LENGTH_LONG, + ) + toast.show() + organizationEventManager.trackEvent( + event = OrganizationEvent.CipherClientAutoFilled(cipherId = "cipherId"), + ) } + coVerify { + filledDataBuilder.build(autofillRequest = fillableRequest) + vaultRepository.generateTotp( + time = any(), + totpCode = TOTP_CODE, + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `completeAutofill when filled partition, organization uses totp, a totp code, and totp generated successfully should build a dataset, place it in a result Intent, copy totp, and finish the Activity`() { + val filledData: FilledData = mockk { + every { filledPartitions } returns listOf(filledPartition) + } + val generateTotpResult = GenerateTotpResult.Success( + code = TOTP_RESULT_VALUE, + periodSeconds = 100, + ) + every { activity.intent } returns mockIntent + every { mockIntent.getAutofillAssistStructureOrNull() } returns assistStructure + every { + autofillParser.parse( + autofillAppInfo = autofillAppInfo, + assistStructure = assistStructure, + ) + } returns fillableRequest + every { cipherView.login?.totp } returns TOTP_CODE + every { cipherView.organizationUseTotp } returns true + mutableUserStateFlow.value = mockk { + every { activeAccount.isPremium } returns false + } + coEvery { + filledDataBuilder.build(autofillRequest = fillableRequest) + } returns filledData + every { + filledPartition.buildDataset( + authIntentSender = null, + autofillAppInfo = autofillAppInfo, + ) + } returns dataset + every { settingsRepository.isAutoCopyTotpDisabled } returns false + every { createAutofillSelectionResultIntent(dataset = dataset) } returns resultIntent + coEvery { + vaultRepository.generateTotp( + time = any(), + totpCode = TOTP_CODE, + ) + } returns generateTotpResult + every { + Toast.makeText( + context, + R.string.verification_code_totp, + Toast.LENGTH_LONG, + ) + } returns toast + + autofillCompletionManager.completeAutofill( + activity = activity, + cipherView = cipherView, + ) + verify { + activity.setResult(Activity.RESULT_OK, resultIntent) + activity.finish() activity.intent clipboardManager.setText(any()) mockIntent.getAutofillAssistStructureOrNull() @@ -446,7 +532,7 @@ class AutofillCompletionManagerTest { @Suppress("MaxLineLength") @Test - fun `completeAutofill when filled partition, no premium active user, and totp code should build a dataset, place it in a result Intent, and finish the Activity`() { + fun `completeAutofill when filled partition, no premium active user, organization does not use totp, and totp code should build a dataset, place it in a result Intent, and finish the Activity`() { val filledData: FilledData = mockk { every { filledPartitions } returns listOf(filledPartition) } @@ -459,6 +545,7 @@ class AutofillCompletionManagerTest { ) } returns fillableRequest every { cipherView.login?.totp } returns TOTP_CODE + every { cipherView.organizationUseTotp } returns false coEvery { filledDataBuilder.build(autofillRequest = fillableRequest) } returns filledData @@ -494,6 +581,7 @@ class AutofillCompletionManagerTest { authIntentSender = null, autofillAppInfo = autofillAppInfo, ) + cipherView.organizationUseTotp settingsRepository.isAutoCopyTotpDisabled createAutofillSelectionResultIntent(dataset = dataset) organizationEventManager.trackEvent( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt index 961836564b..479655a83a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt @@ -1214,6 +1214,7 @@ class VaultItemScreenTest : BaseComposeTest() { uris = emptyList(), passwordRevisionDate = null, isPremiumUser = true, + canViewTotpCode = true, totpCodeItemData = null, fido2CredentialCreationDateText = null, ), @@ -1416,7 +1417,7 @@ class VaultItemScreenTest : BaseComposeTest() { currentState.copy( viewState = DEFAULT_LOGIN_VIEW_STATE.copy( type = DEFAULT_LOGIN.copy( - isPremiumUser = false, + canViewTotpCode = false, ), ), ) @@ -2258,6 +2259,7 @@ private val DEFAULT_LOGIN: VaultItemState.ViewState.Content.ItemType.Login = totpCode = "testCode", ), fido2CredentialCreationDateText = null, + canViewTotpCode = true, ) private val DEFAULT_IDENTITY: VaultItemState.ViewState.Content.ItemType.Identity = @@ -2308,6 +2310,7 @@ private val EMPTY_LOGIN_TYPE: VaultItemState.ViewState.Content.ItemType.Login = passwordRevisionDate = null, totpCodeItemData = null, isPremiumUser = true, + canViewTotpCode = true, fido2CredentialCreationDateText = null, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index ab0bc433cd..c3db45c61e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -2539,6 +2539,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { periodSeconds = 30, ), fido2CredentialCreationDateText = null, + canViewTotpCode = true, ) private val DEFAULT_CARD_TYPE: VaultItemState.ViewState.Content.ItemType.Card = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt index be745325b0..44f773379c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensionsTest.kt @@ -142,7 +142,43 @@ class CipherViewExtensionsTest { VaultItemState.ViewState.Content( common = createCommonContent(isEmpty = false, isPremiumUser = isPremiumUser) .copy(currentCipher = cipherView), - type = createLoginContent(isEmpty = false).copy(isPremiumUser = isPremiumUser), + type = createLoginContent(isEmpty = false).copy( + isPremiumUser = isPremiumUser, + canViewTotpCode = false + ), + ), + viewState, + ) + } + + @Test + fun `toViewState should transform full CipherView into ViewState Login Content without premium but with org totp access`() { + val isPremiumUser = false + val cipherView = createCipherView( + type = CipherType.LOGIN, + isEmpty = false + ).copy(organizationUseTotp = true) + val viewState = cipherView.toViewState( + previousState = null, + isPremiumUser = isPremiumUser, + hasMasterPassword = true, + totpCodeItemData = TotpCodeItemData( + periodSeconds = 30, + timeLeftSeconds = 15, + verificationCode = "123456", + totpCode = "testCode", + ), + clock = fixedClock, + ) + + assertEquals( + VaultItemState.ViewState.Content( + common = createCommonContent(isEmpty = false, isPremiumUser = isPremiumUser) + .copy(currentCipher = cipherView), + type = createLoginContent(isEmpty = false).copy( + isPremiumUser = isPremiumUser, + canViewTotpCode = true + ), ), viewState, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt index 3dfdc94a0b..e8dbc22e14 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt @@ -240,6 +240,7 @@ fun createLoginContent(isEmpty: Boolean): VaultItemState.ViewState.Content.ItemT fido2CredentialCreationDateText = R.string.created_xy .asText("10/27/23", "12:00 PM") .takeUnless { isEmpty }, + canViewTotpCode = true, ) fun createIdentityContent( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt index 101bf0609b..89aea23718 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt @@ -317,6 +317,79 @@ class VaultDataExtensionsTest { ) } + @Suppress("MaxLineLength") + @Test + fun `toViewState should return 1 for totpItemsCount if user does not have premium and has at least 1 totp items with org TOTP true`() { + val vaultData = VaultData( + cipherViewList = listOf(createMockCipherView(number = 1).copy(organizationUseTotp = true)), + collectionViewList = listOf(), + folderViewList = listOf(), + sendViewList = listOf(), + ) + + val actual = vaultData.toViewState( + isPremium = false, + vaultFilterType = VaultFilterType.AllVaults, + isIconLoadingDisabled = false, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + hasMasterPassword = true, + ) + + assertEquals( + VaultState.ViewState.Content( + loginItemsCount = 1, + cardItemsCount = 0, + identityItemsCount = 0, + secureNoteItemsCount = 0, + favoriteItems = listOf(), + folderItems = listOf(), + collectionItems = listOf(), + noFolderItems = listOf(), + trashItemsCount = 0, + totpItemsCount = 1, + ), + actual, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `toViewState should omit non org related totp codes when user does not have premium`() { + val vaultData = VaultData( + cipherViewList = listOf( + createMockCipherView(number = 1).copy(organizationUseTotp = true), + createMockCipherView(number = 2), + ), + collectionViewList = listOf(), + folderViewList = listOf(), + sendViewList = listOf(), + ) + + val actual = vaultData.toViewState( + isPremium = false, + vaultFilterType = VaultFilterType.AllVaults, + isIconLoadingDisabled = false, + baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl, + hasMasterPassword = true, + ) + + assertEquals( + VaultState.ViewState.Content( + loginItemsCount = 2, + cardItemsCount = 0, + identityItemsCount = 0, + secureNoteItemsCount = 0, + favoriteItems = listOf(), + folderItems = listOf(), + collectionItems = listOf(), + noFolderItems = listOf(), + trashItemsCount = 0, + totpItemsCount = 1, + ), + actual, + ) + } + @Suppress("MaxLineLength") @Test fun `toLoginIconData should return a IconData Local type if isIconLoadingDisabled is true`() { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModelTest.kt index 325665a8d7..5dfa300056 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModelTest.kt @@ -4,6 +4,8 @@ import android.net.Uri import app.cash.turbine.test import com.bitwarden.vault.CipherRepromptType import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository @@ -53,6 +55,20 @@ class VerificationCodeViewModelTest : BaseViewModelTest() { every { environmentStateFlow } returns mockk() } + private val mockUserAccount: UserState.Account = mockk { + every { isPremium } returns true + } + + private val mockUserState: UserState = mockk { + every { activeAccount } returns mockUserAccount + } + + private val mutableUserStateFlow: MutableStateFlow = MutableStateFlow(mockUserState) + + private val authRepository: AuthRepository = mockk { + every { userStateFlow } returns mutableUserStateFlow + } + private val mutablePullToRefreshEnabledFlow = MutableStateFlow(false) private val mutableIsIconLoadingDisabledFlow = MutableStateFlow(false) private val settingsRepository: SettingsRepository = mockk { @@ -389,6 +405,47 @@ class VerificationCodeViewModelTest : BaseViewModelTest() { ) } + @Test + fun `AuthCodeState Loaded with non premium user and no org TOTP enabled should cause navigate back`() = + runTest { + setupMockUri() + every { mockUserAccount.isPremium } returns false + val viewModel = createViewModel() + mutableAuthCodeFlow.tryEmit( + value = DataState.Loaded( + data = listOf( + createVerificationCodeItem(number = 1), + createVerificationCodeItem(number = 2).copy(hasPasswordReprompt = true), + ), + ), + ) + + viewModel.eventFlow.test { + assertEquals(VerificationCodeEvent.NavigateBack, awaitItem()) + assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem()) + } + } + + @Test + fun `AuthCodeState Loaded with non premium user and one org TOTP enabled should return correct state`() = + runTest { + setupMockUri() + every { mockUserAccount.isPremium } returns false + val viewModel = createViewModel() + mutableAuthCodeFlow.tryEmit( + value = DataState.Loaded( + data = listOf( + createVerificationCodeItem(number = 1).copy(orgUsesTotp = true), + createVerificationCodeItem(number = 2).copy(hasPasswordReprompt = true), + ), + ), + ) + + val displayItems = + (viewModel.stateFlow.value.viewState as? VerificationCodeState.ViewState.Content)?.verificationCodeDisplayItems + assertEquals(1, displayItems?.size) + } + @Test fun `AuthCodeFlow Loading should update state to Loading`() = runTest { mutableAuthCodeFlow.tryEmit(value = DataState.Loading) @@ -451,6 +508,7 @@ class VerificationCodeViewModelTest : BaseViewModelTest() { vaultRepository = vaultRepository, environmentRepository = environmentRepository, settingsRepository = settingsRepository, + authRepository = authRepository, ) @Suppress("MaxLineLength") diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/util/VerificationCodeDataUtil.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/util/VerificationCodeDataUtil.kt index 566a009bc3..ba8fbe4014 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/util/VerificationCodeDataUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/util/VerificationCodeDataUtil.kt @@ -15,4 +15,5 @@ fun createVerificationCodeItem(number: Int = 1) = uriLoginViewList = createMockLoginView(1).uris, username = "mockUsername-$number", hasPasswordReprompt = false, + orgUsesTotp = false, )