diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/CiphersApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/CiphersApi.kt index 7743120bb..b4915912b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/CiphersApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/CiphersApi.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.datasource.network.api import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import okhttp3.MultipartBody @@ -16,6 +17,7 @@ import retrofit2.http.Path /** * Defines raw calls under the /ciphers API with authentication applied. */ +@Suppress("TooManyFunctions") interface CiphersApi { /** @@ -24,6 +26,14 @@ interface CiphersApi { @POST("ciphers") suspend fun createCipher(@Body body: CipherJsonRequest): Result + /** + * Create a cipher that belongs to an organization. + */ + @POST("ciphers/create") + suspend fun createCipherInOrganization( + @Body body: CreateCipherInOrganizationJsonRequest, + ): Result + /** * Associates an attachment with a cipher. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/CreateCipherInOrganizationJsonRequest.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/CreateCipherInOrganizationJsonRequest.kt new file mode 100644 index 000000000..b0d4cade0 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/CreateCipherInOrganizationJsonRequest.kt @@ -0,0 +1,19 @@ +package com.x8bit.bitwarden.data.vault.datasource.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Represents a create cipher in organization request. + * + * @property cipher The cipher to create. + * @property collectionIds A list of collection ids associated with the cipher. + */ +@Serializable +data class CreateCipherInOrganizationJsonRequest( + @SerialName("Cipher") + val cipher: CipherJsonRequest, + + @SerialName("CollectionIds") + val collectionIds: List, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersService.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersService.kt index 14dea5bb8..6c163d9f7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersService.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersService.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.datasource.network.service import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson @@ -10,12 +11,20 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherRespo /** * Provides an API for querying ciphers endpoints. */ +@Suppress("TooManyFunctions") interface CiphersService { /** * Attempt to create a cipher. */ suspend fun createCipher(body: CipherJsonRequest): Result + /** + * Attempt to create a cipher that belongs to an organization. + */ + suspend fun createCipherInOrganization( + body: CreateCipherInOrganizationJsonRequest, + ): Result + /** * Attempt to upload an attachment file. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceImpl.kt index 436b6ef30..d2a9f47f1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceImpl.kt @@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.api.CiphersApi import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.FileUploadType import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson @@ -21,6 +22,7 @@ import java.time.ZoneOffset import java.time.ZonedDateTime import java.time.format.DateTimeFormatter +@Suppress("TooManyFunctions") class CiphersServiceImpl( private val azureApi: AzureApi, private val ciphersApi: CiphersApi, @@ -30,6 +32,10 @@ class CiphersServiceImpl( override suspend fun createCipher(body: CipherJsonRequest): Result = ciphersApi.createCipher(body = body) + override suspend fun createCipherInOrganization( + body: CreateCipherInOrganizationJsonRequest, + ): Result = ciphersApi.createCipherInOrganization(body = body) + override suspend fun createAttachment( cipherId: String, body: AttachmentJsonRequest, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt index 009b1d425..0e7dc3147 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt @@ -200,6 +200,14 @@ interface VaultRepository : VaultLockManager { */ suspend fun createCipher(cipherView: CipherView): CreateCipherResult + /** + * Attempt to create a cipher that belongs to an organization. + */ + suspend fun createCipherInOrganization( + cipherView: CipherView, + collectionIds: List, + ): CreateCipherResult + /** * Attempt to create an attachment for the given [cipherView]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index b58dac0de..87efec901 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -35,6 +35,7 @@ import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.platform.util.flatMap import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson @@ -584,6 +585,32 @@ class VaultRepositoryImpl( ) } + override suspend fun createCipherInOrganization( + cipherView: CipherView, + collectionIds: List, + ): CreateCipherResult { + val userId = activeUserId ?: return CreateCipherResult.Error + return vaultSdkSource + .encryptCipher( + userId = userId, + cipherView = cipherView, + ) + .flatMap { cipher -> + ciphersService + .createCipherInOrganization( + body = CreateCipherInOrganizationJsonRequest( + cipher = cipher.toEncryptedNetworkCipher(), + collectionIds = collectionIds, + ), + ) + } + .onSuccess { vaultDiskSource.saveCipher(userId = userId, cipher = it) } + .fold( + onFailure = { CreateCipherResult.Error }, + onSuccess = { CreateCipherResult.Success }, + ) + } + override suspend fun hardDeleteCipher(cipherId: String): DeleteCipherResult { val userId = activeUserId ?: return DeleteCipherResult.Error return ciphersService diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/components/CollectionItemSelector.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/components/CollectionItemSelector.kt new file mode 100644 index 000000000..22464edaf --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/components/CollectionItemSelector.kt @@ -0,0 +1,69 @@ +package com.x8bit.bitwarden.ui.vault.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch +import com.x8bit.bitwarden.ui.vault.model.VaultCollection + +/** + * A set of switches that a user can select [Collection]s with. + */ +fun LazyListScope.collectionItemsSelector( + collectionList: List?, + onCollectionSelect: (VaultCollection) -> Unit, +) { + item { + Spacer(modifier = Modifier.height(8.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.collections), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + + if (collectionList?.isNotEmpty() == true) { + items(collectionList) { + Spacer(modifier = Modifier.height(8.dp)) + BitwardenWideSwitch( + label = it.name, + isChecked = it.isSelected, + onCheckedChange = { _ -> + onCollectionSelect(it) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + } else { + item { + Spacer(modifier = Modifier.height(8.dp)) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(id = R.string.no_collections_to_list), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCardItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCardItems.kt index bac988d23..5fdfafc05 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCardItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCardItems.kt @@ -22,6 +22,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField import com.x8bit.bitwarden.ui.platform.components.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.BitwardenSwitchWithActions import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField +import com.x8bit.bitwarden.ui.vault.components.collectionItemsSelector import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand @@ -151,9 +152,18 @@ fun LazyListScope.vaultAddEditCardItems( Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.folder), - options = commonState.availableFolders.map { it.invoke() }.toImmutableList(), - selectedOption = commonState.folderName.invoke(), - onOptionSelected = commonHandlers.onFolderTextChange, + options = commonState + .availableFolders + .map { it.name } + .toImmutableList(), + selectedOption = commonState.selectedFolder?.name, + onOptionSelected = { selectedFolderName -> + commonHandlers.onFolderSelected( + commonState + .availableFolders + .first { it.name == selectedFolderName }, + ) + }, modifier = Modifier.padding(horizontal = 16.dp), ) } @@ -272,14 +282,29 @@ fun LazyListScope.vaultAddEditCardItems( Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.who_owns_this_item), - options = commonState.availableOwners.toImmutableList(), - selectedOption = commonState.ownership, - onOptionSelected = commonHandlers.onOwnershipTextChange, + options = commonState + .availableOwners + .map { it.name } + .toImmutableList(), + selectedOption = commonState.selectedOwner?.name, + onOptionSelected = { selectedOwnerName -> + commonHandlers.onOwnerSelected( + commonState + .availableOwners + .first { it.name == selectedOwnerName }, + ) + }, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), ) } + if (commonState.selectedOwnerId != null) { + collectionItemsSelector( + collectionList = commonState.selectedOwner?.collections, + onCollectionSelect = commonHandlers.onCollectionSelect, + ) + } } item { Spacer(modifier = Modifier.height(24.dp)) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditIdentityItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditIdentityItems.kt index 51a12048f..5e255cbf3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditIdentityItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditIdentityItems.kt @@ -21,6 +21,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenMultiSelectButton import com.x8bit.bitwarden.ui.platform.components.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.BitwardenSwitchWithActions import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField +import com.x8bit.bitwarden.ui.vault.components.collectionItemsSelector import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditIdentityTypeHandlers import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle @@ -260,9 +261,18 @@ fun LazyListScope.vaultAddEditIdentityItems( Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.folder), - options = commonState.availableFolders.map { it.invoke() }.toImmutableList(), - selectedOption = commonState.folderName.invoke(), - onOptionSelected = commonTypeHandlers.onFolderTextChange, + options = commonState + .availableFolders + .map { it.name } + .toImmutableList(), + selectedOption = commonState.selectedFolder?.name, + onOptionSelected = { selectedFolderName -> + commonTypeHandlers.onFolderSelected( + commonState + .availableFolders + .first { it.name == selectedFolderName }, + ) + }, modifier = Modifier.padding(horizontal = 16.dp), ) } @@ -394,14 +404,29 @@ fun LazyListScope.vaultAddEditIdentityItems( Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.who_owns_this_item), - options = commonState.availableOwners.toImmutableList(), - selectedOption = commonState.ownership, - onOptionSelected = commonTypeHandlers.onOwnershipTextChange, + options = commonState + .availableOwners + .map { it.name } + .toImmutableList(), + selectedOption = commonState.selectedOwner?.name, + onOptionSelected = { selectedOwnerName -> + commonTypeHandlers.onOwnerSelected( + commonState + .availableOwners + .first { it.name == selectedOwnerName }, + ) + }, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), ) } + if (commonState.selectedOwnerId != null) { + collectionItemsSelector( + collectionList = commonState.selectedOwner?.collections, + onCollectionSelect = commonTypeHandlers.onCollectionSelect, + ) + } } item { Spacer(modifier = Modifier.height(24.dp)) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt index fd6407c55..2ab114777 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt @@ -32,6 +32,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.BitwardenTextFieldWithActions import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.model.IconResource +import com.x8bit.bitwarden.ui.vault.components.collectionItemsSelector import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType @@ -199,9 +200,18 @@ fun LazyListScope.vaultAddEditLoginItems( Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.folder), - options = commonState.availableFolders.map { it.invoke() }.toImmutableList(), - selectedOption = commonState.folderName.invoke(), - onOptionSelected = commonActionHandler.onFolderTextChange, + options = commonState + .availableFolders + .map { it.name } + .toImmutableList(), + selectedOption = commonState.selectedFolder?.name, + onOptionSelected = { selectedFolderName -> + commonActionHandler.onFolderSelected( + commonState + .availableFolders + .first { it.name == selectedFolderName }, + ) + }, modifier = Modifier.padding(horizontal = 16.dp), ) } @@ -316,14 +326,30 @@ fun LazyListScope.vaultAddEditLoginItems( Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.who_owns_this_item), - options = commonState.availableOwners.toImmutableList(), - selectedOption = commonState.ownership, - onOptionSelected = commonActionHandler.onOwnershipTextChange, + options = commonState + .availableOwners + .map { it.name } + .toImmutableList(), + selectedOption = commonState.selectedOwner?.name, + onOptionSelected = { selectedOwnerName -> + commonActionHandler.onOwnerSelected( + commonState + .availableOwners + .first { it.name == selectedOwnerName }, + ) + }, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), ) } + + if (commonState.selectedOwnerId != null) { + collectionItemsSelector( + collectionList = commonState.selectedOwner?.collections, + onCollectionSelect = commonActionHandler.onCollectionSelect, + ) + } } item { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditSecureNotesItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditSecureNotesItems.kt index 0693f6921..281f59883 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditSecureNotesItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditSecureNotesItems.kt @@ -19,6 +19,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenMultiSelectButton import com.x8bit.bitwarden.ui.platform.components.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.BitwardenSwitchWithActions import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField +import com.x8bit.bitwarden.ui.vault.components.collectionItemsSelector import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType import kotlinx.collections.immutable.persistentListOf @@ -59,11 +60,19 @@ fun LazyListScope.vaultAddEditSecureNotesItems( Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.folder), - options = commonState.availableFolders.map { it.invoke() }.toImmutableList(), - selectedOption = commonState.folderName.invoke(), - onOptionSelected = commonTypeHandlers.onFolderTextChange, - modifier = Modifier - .padding(horizontal = 16.dp), + options = commonState + .availableFolders + .map { it.name } + .toImmutableList(), + selectedOption = commonState.selectedFolder?.name, + onOptionSelected = { selectedFolderName -> + commonTypeHandlers.onFolderSelected( + commonState + .availableFolders + .first { it.name == selectedFolderName }, + ) + }, + modifier = Modifier.padding(horizontal = 16.dp), ) } @@ -175,14 +184,30 @@ fun LazyListScope.vaultAddEditSecureNotesItems( Spacer(modifier = Modifier.height(8.dp)) BitwardenMultiSelectButton( label = stringResource(id = R.string.who_owns_this_item), - options = commonState.availableOwners.toImmutableList(), - selectedOption = commonState.ownership, - onOptionSelected = commonTypeHandlers.onOwnershipTextChange, + options = commonState + .availableOwners + .map { it.name } + .toImmutableList(), + selectedOption = commonState.selectedOwner?.name, + onOptionSelected = { selectedOwnerName -> + commonTypeHandlers.onOwnerSelected( + commonState + .availableOwners + .first { it.name == selectedOwnerName }, + ) + }, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), ) } + + if (commonState.selectedOwnerId != null) { + collectionItemsSelector( + collectionList = commonState.selectedOwner?.collections, + onCollectionSelect = commonTypeHandlers.onCollectionSelect, + ) + } } item { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt index a5a33f1a5..ff6189fb6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt @@ -7,6 +7,7 @@ import com.bitwarden.core.CipherView import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult +import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager @@ -20,6 +21,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult +import com.x8bit.bitwarden.data.vault.repository.model.VaultData 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 @@ -30,12 +32,15 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldAction import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField +import com.x8bit.bitwarden.ui.vault.feature.addedit.util.appendFolderAndOwnerData import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDefaultAddTypeContent import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState +import com.x8bit.bitwarden.ui.vault.feature.addedit.util.validateCipherOrReturnErrorState import com.x8bit.bitwarden.ui.vault.feature.vault.util.toCipherView import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth +import com.x8bit.bitwarden.ui.vault.model.VaultCollection import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType import dagger.hilt.android.lifecycle.HiltViewModel @@ -109,18 +114,17 @@ class VaultAddEditViewModel @Inject constructor( //region Initialization and Overrides init { - state - .vaultAddEditType - .vaultItemId - ?.let { itemId -> - vaultRepository - .getVaultItemStateFlow(itemId) - // We'll stop getting updates as soon as we get some loaded data. - .takeUntilLoaded() - .map { VaultAddEditAction.Internal.VaultDataReceive(it) } - .onEach(::sendAction) - .launchIn(viewModelScope) + vaultRepository + .vaultDataStateFlow + .takeUntilLoaded() + .map { vaultDataState -> + VaultAddEditAction.Internal.VaultDataReceive( + vaultData = vaultDataState, + userData = authRepository.userStateFlow.value, + ) } + .onEach(::sendAction) + .launchIn(viewModelScope) vaultRepository .totpCodeFlow @@ -186,6 +190,7 @@ class VaultAddEditViewModel @Inject constructor( is VaultAddEditAction.Common.CustomFieldActionSelect -> handleCustomFieldActionSelected( action, ) + is VaultAddEditAction.Common.CollectionSelect -> handleCollectionSelect(action) } } @@ -246,6 +251,7 @@ class VaultAddEditViewModel @Inject constructor( } } + @Suppress("LongMethod") private fun handleSaveClick() = onContent { content -> if (content.common.name.isBlank()) { mutableStateFlow.update { @@ -258,6 +264,19 @@ class VaultAddEditViewModel @Inject constructor( ) } return@onContent + } else if ( + content.common.selectedOwnerId != null && + content.common.selectedOwner?.collections?.all { !it.isSelected } == true + ) { + mutableStateFlow.update { + it.copy( + dialog = VaultAddEditState.DialogState.Generic( + title = R.string.an_error_has_occurred.asText(), + message = R.string.select_one_collection.asText(), + ), + ) + } + return@onContent } mutableStateFlow.update { @@ -271,7 +290,7 @@ class VaultAddEditViewModel @Inject constructor( viewModelScope.launch { when (val vaultAddEditType = state.vaultAddEditType) { VaultAddEditType.AddItem -> { - val result = vaultRepository.createCipher(cipherView = content.toCipherView()) + val result = content.createCipherForAddAndCloneItemStates() sendAction(VaultAddEditAction.Internal.CreateCipherResultReceive(result)) } @@ -284,7 +303,7 @@ class VaultAddEditViewModel @Inject constructor( } is VaultAddEditType.CloneItem -> { - val result = vaultRepository.createCipher(cipherView = content.toCipherView()) + val result = content.createCipherForAddAndCloneItemStates() sendAction(VaultAddEditAction.Internal.CreateCipherResultReceive(result)) } } @@ -437,7 +456,7 @@ class VaultAddEditViewModel @Inject constructor( action: VaultAddEditAction.Common.FolderChange, ) { updateCommonContent { commonContent -> - commonContent.copy(folderName = action.folder) + commonContent.copy(selectedFolderId = action.folder.id) } } @@ -469,7 +488,7 @@ class VaultAddEditViewModel @Inject constructor( action: VaultAddEditAction.Common.OwnershipChange, ) { updateCommonContent { commonContent -> - commonContent.copy(ownership = action.ownership) + commonContent.copy(selectedOwnerId = action.ownership.id) } } @@ -490,6 +509,22 @@ class VaultAddEditViewModel @Inject constructor( ) } + @Suppress("MaxLineLength") + private fun handleCollectionSelect( + action: VaultAddEditAction.Common.CollectionSelect, + ) { + updateCommonContent { currentCommonContentState -> + currentCommonContentState.copy( + availableOwners = currentCommonContentState + .availableOwners + .toUpdatedOwners( + selectedCollectionId = action.collection.id, + selectedOwnerId = currentCommonContentState.selectedOwnerId, + ), + ) + } + } + //endregion Common Handlers //region Add Login Item Type Handlers @@ -1000,7 +1035,7 @@ class VaultAddEditViewModel @Inject constructor( @Suppress("LongMethod") private fun handleVaultDataReceive(action: VaultAddEditAction.Internal.VaultDataReceive) { - when (val vaultDataState = action.vaultDataState) { + when (val vaultDataState = action.vaultData) { is DataState.Error -> { mutableStateFlow.update { it.copy( @@ -1012,17 +1047,10 @@ class VaultAddEditViewModel @Inject constructor( } is DataState.Loaded -> { - mutableStateFlow.update { - it.copy( - viewState = vaultDataState - .data - ?.toViewState( - isClone = it.isCloneMode, - resourceManager = resourceManager, - ) - ?: VaultAddEditState.ViewState.Error( - message = R.string.generic_error_message.asText(), - ), + mutableStateFlow.update { currentState -> + currentState.determineContentState( + vaultData = vaultDataState.data, + userData = action.userData, ) } } @@ -1046,23 +1074,43 @@ class VaultAddEditViewModel @Inject constructor( } is DataState.Pending -> { - mutableStateFlow.update { - it.copy( - viewState = vaultDataState - .data - ?.toViewState( - isClone = it.isCloneMode, - resourceManager = resourceManager, - ) - ?: VaultAddEditState.ViewState.Error( - message = R.string.generic_error_message.asText(), - ), + mutableStateFlow.update { currentState -> + currentState.determineContentState( + vaultData = vaultDataState.data, + userData = action.userData, ) } } } } + private fun VaultAddEditState.determineContentState( + vaultData: VaultData, + userData: UserState?, + ): VaultAddEditState = + copy( + viewState = vaultData.cipherViewList + .find { it.id == vaultAddEditType.vaultItemId } + .validateCipherOrReturnErrorState( + currentAccount = userData?.activeAccount, + vaultAddEditType = vaultAddEditType, + ) { currentAccount, cipherView -> + // Derive the view state from the current Cipher for Edit mode + // or use the current state for Add + (cipherView?.toViewState( + isClone = isCloneMode, + resourceManager = resourceManager, + ) ?: viewState) + .appendFolderAndOwnerData( + folderViewList = vaultData.folderViewList, + collectionViewList = vaultData.collectionViewList + .filter { !it.readOnly }, + activeAccount = currentAccount, + resourceManager = resourceManager, + ) + }, + ) + private fun handleVaultTotpCodeReceive(action: VaultAddEditAction.Internal.TotpCodeReceive) { when (action.totpResult) { is TotpCodeResult.Success -> { @@ -1243,6 +1291,46 @@ class VaultAddEditViewModel @Inject constructor( }, ) + @Suppress("MaxLineLength") + private suspend fun VaultAddEditState.ViewState.Content.createCipherForAddAndCloneItemStates(): CreateCipherResult { + return common.selectedOwner?.collections + ?.filter { it.isSelected } + ?.map { it.id } + ?.let { + vaultRepository.createCipherInOrganization( + cipherView = toCipherView(), + collectionIds = it, + ) + } + ?: vaultRepository.createCipher(cipherView = toCipherView()) + } + + private fun List.toUpdatedOwners( + selectedOwnerId: String?, + selectedCollectionId: String, + ): List = + map { owner -> + if (owner.id != selectedOwnerId) return@map owner + owner.copy( + collections = owner + .collections + .toUpdatedCollections(selectedCollectionId = selectedCollectionId), + ) + } + + private fun List.toUpdatedCollections( + selectedCollectionId: String, + ): List = + map { collection -> + collection.copy( + isSelected = if (selectedCollectionId == collection.id) { + !collection.isSelected + } else { + collection.isSelected + }, + ) + } + //endregion Utility Functions } @@ -1343,9 +1431,9 @@ data class VaultAddEditState( * @property favorite Indicates whether this item is marked as a favorite. * @property customFieldData Additional custom fields associated with the item. * @property notes Any additional notes or comments associated with the item. - * @property folderName The folder that this item belongs to. + * @property selectedFolderId The ID of the folder that this item belongs to. * @property availableFolders The list of folders that this item could be added too. - * @property ownership The ownership email associated with the item. + * @property selectedOwnerId The ID of the owner associated with the item. * @property availableOwners A list of available owners. */ @Parcelize @@ -1357,21 +1445,23 @@ data class VaultAddEditState( val favorite: Boolean = false, val customFieldData: List = emptyList(), val notes: String = "", - val folderName: Text = DEFAULT_FOLDER, - val availableFolders: List = listOf( - "Folder 1".asText(), - "Folder 2".asText(), - "Folder 3".asText(), - ), - // TODO: Update this property to get available owners from the data layer (BIT-501) - val ownership: String = DEFAULT_OWNERSHIP, - // TODO: Update this property to get available owners from the data layer (BIT-501) - val availableOwners: List = listOf("a@b.com", "c@d.com"), + val selectedFolderId: String? = null, + val availableFolders: List = emptyList(), + val selectedOwnerId: String? = null, + val availableOwners: List = emptyList(), ) : Parcelable { - companion object { - private val DEFAULT_FOLDER: Text = R.string.folder_none.asText() - private const val DEFAULT_OWNERSHIP: String = "placeholder@email.com" - } + + /** + * Helper to provide the currently selected owner. + */ + val selectedOwner: Owner? + get() = availableOwners.find { it.id == selectedOwnerId } + + /** + * Helper to provide the currently selected folder. + */ + val selectedFolder: Folder? + get() = availableFolders.find { it.id == selectedFolderId } } /** @@ -1547,6 +1637,32 @@ data class VaultAddEditState( ) : Custom() } + /** + * Models a folder that can be chosen by the user. + * + * @property id the folder id. + * @property name the folder name. + */ + @Parcelize + data class Folder( + val id: String?, + val name: String, + ) : Parcelable + + /** + * Models an owner that can be chosen by the user. + * + * @property id the id of the owner (nullable). + * @property name the name of the owner. + * @property collections the collections of the owner. + */ + @Parcelize + data class Owner( + val id: String?, + val name: String, + val collections: List, + ) : Parcelable + /** * Displays a dialog. */ @@ -1691,7 +1807,7 @@ sealed class VaultAddEditAction { * * @property folder The new folder text. */ - data class FolderChange(val folder: Text) : Common() + data class FolderChange(val folder: VaultAddEditState.Folder) : Common() /** * Fired when the Favorite toggle is changed. @@ -1720,7 +1836,7 @@ sealed class VaultAddEditAction { * * @property ownership The new ownership text. */ - data class OwnershipChange(val ownership: String) : Common() + data class OwnershipChange(val ownership: VaultAddEditState.Owner) : Common() /** * Represents the action to add a new custom field. @@ -1747,6 +1863,15 @@ sealed class VaultAddEditAction { * Represents the action to open tooltip */ data object TooltipClick : Common() + + /** + * The user has selected a collection. + * + * @property collection the collection selected. + */ + data class CollectionSelect( + val collection: VaultCollection, + ) : Common() } /** @@ -2039,7 +2164,8 @@ sealed class VaultAddEditAction { * Indicates that the vault item data has been received. */ data class VaultDataReceive( - val vaultDataState: DataState, + val vaultData: DataState, + val userData: UserState?, ) : Internal() /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditCommonHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditCommonHandlers.kt index 2ba3de10d..9ba4c0e79 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditCommonHandlers.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditCommonHandlers.kt @@ -1,40 +1,42 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit.handlers -import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditAction import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditViewModel import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldAction import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType +import com.x8bit.bitwarden.ui.vault.model.VaultCollection /** * A collection of handler functions for managing actions common * within the context of adding items to a vault. * * @property onNameTextChange Handles the action when the name text is changed. - * @property onFolderTextChange Handles the action when the folder text is changed. + * @property onFolderSelected Handles the action when a folder is selected. * @property onToggleFavorite Handles the action when the favorite toggle is changed. * @property onToggleMasterPasswordReprompt Handles the action when the master password * reprompt toggle is changed. * @property onNotesTextChange Handles the action when the notes text is changed. - * @property onOwnershipTextChange Handles the action when the ownership text is changed. + * @property onOwnerSelected Handles the action when a owner is selected. * @property onTooltipClick Handles the action when the tooltip button is clicked. * @property onAddNewCustomFieldClick Handles the action when the add new custom field * button is clicked. - * @property onCustomFieldValueChange Handles the action when the field's value changes + * @property onCustomFieldValueChange Handles the action when the field's value changes. + * @property onCollectionSelect Handles the action when a collection is selected. */ @Suppress("LongParameterList") data class VaultAddEditCommonHandlers( val onNameTextChange: (String) -> Unit, - val onFolderTextChange: (String) -> Unit, + val onFolderSelected: (VaultAddEditState.Folder) -> Unit, val onToggleFavorite: (Boolean) -> Unit, val onToggleMasterPasswordReprompt: (Boolean) -> Unit, val onNotesTextChange: (String) -> Unit, - val onOwnershipTextChange: (String) -> Unit, + val onOwnerSelected: (VaultAddEditState.Owner) -> Unit, val onTooltipClick: () -> Unit, val onAddNewCustomFieldClick: (CustomFieldType, String) -> Unit, val onCustomFieldValueChange: (VaultAddEditState.Custom) -> Unit, val onCustomFieldActionSelect: (CustomFieldAction, VaultAddEditState.Custom) -> Unit, + val onCollectionSelect: (VaultCollection) -> Unit, ) { companion object { @@ -50,10 +52,10 @@ data class VaultAddEditCommonHandlers( VaultAddEditAction.Common.NameTextChange(newName), ) }, - onFolderTextChange = { newFolder -> + onFolderSelected = { newFolder -> viewModel.trySendAction( VaultAddEditAction.Common.FolderChange( - newFolder.asText(), + newFolder, ), ) }, @@ -74,7 +76,7 @@ data class VaultAddEditCommonHandlers( VaultAddEditAction.Common.NotesTextChange(newNotes), ) }, - onOwnershipTextChange = { newOwnership -> + onOwnerSelected = { newOwnership -> viewModel.trySendAction( VaultAddEditAction.Common.OwnershipChange(newOwnership), ) @@ -107,6 +109,13 @@ data class VaultAddEditCommonHandlers( ), ) }, + onCollectionSelect = { selectedCollection -> + viewModel.trySendAction( + VaultAddEditAction.Common.CollectionSelect( + collection = selectedCollection, + ), + ) + }, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt index a32706a79..f4afff7b6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt @@ -1,18 +1,25 @@ +@file:Suppress("TooManyFunctions") + package com.x8bit.bitwarden.ui.vault.feature.addedit.util import com.bitwarden.core.CipherRepromptType import com.bitwarden.core.CipherType import com.bitwarden.core.CipherView +import com.bitwarden.core.CollectionView import com.bitwarden.core.FieldType import com.bitwarden.core.FieldView +import com.bitwarden.core.FolderView import com.bitwarden.core.LoginUriView import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem +import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth +import com.x8bit.bitwarden.ui.vault.model.VaultCollection import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType.Companion.fromId import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull @@ -76,17 +83,107 @@ fun CipherView.toViewState( favorite = this.favorite, masterPasswordReprompt = this.reprompt == CipherRepromptType.PASSWORD, notes = this.notes.orEmpty(), - // TODO: Update these properties to pull folder from data layer (BIT-501) - folderName = this.folderId?.asText() ?: R.string.folder_none.asText(), - availableFolders = emptyList(), - // TODO: Update this property to pull owner from data layer (BIT-501) - ownership = "", - // TODO: Update this property to pull available owners from data layer (BIT-501) availableOwners = emptyList(), customFieldData = this.fields.orEmpty().map { it.toCustomField() }, ), ) +/** + * Adds Folder and Owner data to [VaultAddEditState.ViewState]. + */ +fun VaultAddEditState.ViewState.appendFolderAndOwnerData( + folderViewList: List, + collectionViewList: List, + activeAccount: UserState.Account, + resourceManager: ResourceManager, +): VaultAddEditState.ViewState { + return (this as? VaultAddEditState.ViewState.Content)?.let { currentContentState -> + currentContentState.copy( + common = currentContentState.common.copy( + selectedFolderId = folderViewList.toSelectedFolderId( + cipherView = currentContentState.common.originalCipher, + ), + availableFolders = folderViewList.toAvailableFolders( + resourceManager = resourceManager, + ), + selectedOwnerId = activeAccount.toSelectedOwnerId( + cipherView = currentContentState.common.originalCipher, + ), + availableOwners = activeAccount.toAvailableOwners( + collectionViewList = collectionViewList, + ), + ), + ) + } ?: this +} + +/** + * Validates a [CipherView] otherwise returning a [VaultAddEditState.ViewState.Error]. + */ +fun CipherView?.validateCipherOrReturnErrorState( + currentAccount: UserState.Account?, + vaultAddEditType: VaultAddEditType, + lambda: ( + currentAccount: UserState.Account, + cipherView: CipherView?, + ) -> VaultAddEditState.ViewState, +): VaultAddEditState.ViewState = + if (currentAccount == null || + (vaultAddEditType is VaultAddEditType.EditItem && this == null) + ) { + VaultAddEditState.ViewState.Error(R.string.generic_error_message.asText()) + } else { + lambda(currentAccount, this) + } + +private fun List.toSelectedFolderId(cipherView: CipherView?): String? = + cipherView + ?.folderId + ?.takeIf { id -> id in map { it.id } } + +private fun List.toAvailableFolders( + resourceManager: ResourceManager, +): List = + listOf( + VaultAddEditState.Folder( + id = null, + name = resourceManager.getString(R.string.folder_none), + ), + ) + .plus( + map { VaultAddEditState.Folder(name = it.name, id = it.id.toString()) }, + ) + +private fun UserState.Account.toSelectedOwnerId(cipherView: CipherView?): String? = + cipherView + ?.organizationId + ?.takeIf { id -> id in organizations.map { it.id } } + +private fun UserState.Account.toAvailableOwners( + collectionViewList: List, +): List = + listOf(VaultAddEditState.Owner(name = email, id = null, collections = emptyList())) + .plus( + organizations.map { + VaultAddEditState.Owner( + name = it.name.orEmpty(), + id = it.id, + collections = collectionViewList + .filter { collection -> + collection.organizationId == it.id && + collection.id != null + } + .map { collection -> + VaultCollection( + id = collection.id.orEmpty(), + name = collection.name, + isSelected = false, + ) + }, + ) + }, + ) + private fun FieldView.toCustomField() = when (this.type) { FieldType.TEXT -> VaultAddEditState.Custom.TextField( 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 ff28a5914..13d5fc773 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 @@ -70,7 +70,6 @@ class VaultItemViewModel @Inject constructor( verificationCode = it.code, ) } - VaultItemAction.Internal.VaultDataReceive( userState = userState, vaultDataState = combineDataStates( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationContent.kt index bda07a6a9..8cc487bf1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationContent.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -16,9 +15,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText import com.x8bit.bitwarden.ui.platform.components.BitwardenMultiSelectButton -import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch +import com.x8bit.bitwarden.ui.vault.components.collectionItemsSelector +import com.x8bit.bitwarden.ui.vault.model.VaultCollection import kotlinx.collections.immutable.toImmutableList /** @@ -29,7 +28,7 @@ import kotlinx.collections.immutable.toImmutableList fun VaultMoveToOrganizationContent( state: VaultMoveToOrganizationState.ViewState.Content, organizationSelect: (VaultMoveToOrganizationState.ViewState.Content.Organization) -> Unit, - collectionSelect: (VaultMoveToOrganizationState.ViewState.Content.Collection) -> Unit, + collectionSelect: (VaultCollection) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn( @@ -70,45 +69,9 @@ fun VaultMoveToOrganizationContent( } } - item { - Spacer(modifier = Modifier.height(8.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.collections), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - } - if (state.selectedOrganization.collections.isNotEmpty()) { - items(state.selectedOrganization.collections) { - Spacer(modifier = Modifier.height(8.dp)) - BitwardenWideSwitch( - label = it.name, - isChecked = it.isSelected, - onCheckedChange = { _ -> - collectionSelect(it) - }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - } - } else { - item { - Spacer(modifier = Modifier.height(8.dp)) - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = stringResource(id = R.string.no_collections_to_list), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - } - } - } + collectionItemsSelector( + collectionList = state.selectedOrganization.collections, + onCollectionSelect = collectionSelect, + ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationScreen.kt index 46c95175d..6c21522a2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationScreen.kt @@ -29,6 +29,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState +import com.x8bit.bitwarden.ui.vault.model.VaultCollection /** * Displays the vault move to organization screen. @@ -78,7 +79,7 @@ private fun VaultMoveToOrganizationScaffold( moveClick: () -> Unit, dismissClick: () -> Unit, organizationSelect: (VaultMoveToOrganizationState.ViewState.Content.Organization) -> Unit, - collectionSelect: (VaultMoveToOrganizationState.ViewState.Content.Collection) -> Unit, + collectionSelect: (VaultCollection) -> Unit, ) { val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) when (val dialog = state.dialogState) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModel.kt index bd1c33a4f..064d0473d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModel.kt @@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.platform.repository.util.combineDataStates +import com.x8bit.bitwarden.data.platform.repository.util.map import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult import com.x8bit.bitwarden.ui.platform.base.BaseViewModel @@ -17,6 +18,7 @@ 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.concat import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.util.toViewState +import com.x8bit.bitwarden.ui.vault.model.VaultCollection import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn @@ -57,14 +59,14 @@ class VaultMoveToOrganizationViewModel @Inject constructor( ) { cipherViewState, collectionsState, userState -> VaultMoveToOrganizationAction.Internal.VaultDataReceive( vaultData = combineDataStates( - dataState1 = cipherViewState, + dataState1 = cipherViewState.map { Unit }, dataState2 = collectionsState, - dataState3 = DataState.Loaded(userState), - ) { ciphersData, collectionsData, userData -> + dataState3 = DataState.Loaded(userState).map { Unit }, + ) { _, collectionsData, _ -> Triple( - first = ciphersData, + first = cipherViewState.data, second = collectionsData, - third = userData, + third = userState, ) }, ) @@ -378,21 +380,7 @@ data class VaultMoveToOrganizationState( data class Organization( val id: String, val name: String, - val collections: List, - ) : Parcelable - - /** - * Models a collection. - * - * @property id the collection id. - * @property name the collection name. - * @property isSelected if the collection is selected or not. - */ - @Parcelize - data class Collection( - val id: String, - val name: String, - val isSelected: Boolean, + val collections: List, ) : Parcelable } @@ -457,7 +445,7 @@ sealed class VaultMoveToOrganizationAction { * @property collection the collection to select. */ data class CollectionSelect( - val collection: VaultMoveToOrganizationState.ViewState.Content.Collection, + val collection: VaultCollection, ) : VaultMoveToOrganizationAction() /** @@ -495,9 +483,9 @@ private fun List.to ) } -private fun List.toUpdatedCollections( +private fun List.toUpdatedCollections( selectedCollectionId: String, -): List = +): List = map { collection -> collection.copy( isSelected = if (selectedCollectionId == collection.id) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/util/VaultMoveToOrganizationViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/util/VaultMoveToOrganizationViewExtensions.kt index 5e87bb334..d7dc645f9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/util/VaultMoveToOrganizationViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/util/VaultMoveToOrganizationViewExtensions.kt @@ -6,6 +6,7 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.VaultMoveToOrganizationState +import com.x8bit.bitwarden.ui.vault.model.VaultCollection /** * Transforms a triple of [CipherView] (nullable), list of [CollectionView], @@ -46,7 +47,7 @@ fun Triple, UserState?>.toViewState(): collection.id != null } .map { collection -> - VaultMoveToOrganizationState.ViewState.Content.Collection( + VaultCollection( id = collection.id.orEmpty(), name = collection.name, isSelected = currentCipher diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt index 146854e80..76591e115 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensions.kt @@ -49,10 +49,8 @@ fun VaultAddEditState.ViewState.Content.toCipherView(): CipherView = name = common.name, notes = common.notes.orNullIfBlank(), favorite = common.favorite, - // TODO Use real folder ID (BIT-528) - folderId = common.originalCipher?.folderId, - // TODO Use real organization ID (BIT-780) - organizationId = common.originalCipher?.organizationId, + folderId = common.selectedFolderId, + organizationId = common.selectedOwnerId, reprompt = common.toCipherRepromptType(), fields = common.customFieldData.map { it.toFieldView() }, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/model/VaultCollection.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/model/VaultCollection.kt new file mode 100644 index 000000000..2c81918ff --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/model/VaultCollection.kt @@ -0,0 +1,18 @@ +package com.x8bit.bitwarden.ui.vault.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Models a collection. + * + * @property id the collection id. + * @property name the collection name. + * @property isSelected if the collection is selected or not. + */ +@Parcelize +data class VaultCollection( + val id: String, + val name: String, + val isSelected: Boolean, +) : Parcelable diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt index 257cfa6e5..e41867c5e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt @@ -4,6 +4,7 @@ import android.net.Uri import com.x8bit.bitwarden.data.platform.base.BaseServiceTest import com.x8bit.bitwarden.data.vault.datasource.network.api.AzureApi import com.x8bit.bitwarden.data.vault.datasource.network.api.CiphersApi +import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.FileUploadType import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson @@ -63,6 +64,21 @@ class CiphersServiceTest : BaseServiceTest() { ) } + @Test + fun `createCipherInOrganization should return the correct response`() = runTest { + server.enqueue(MockResponse().setBody(CREATE_UPDATE_CIPHER_SUCCESS_JSON)) + val result = ciphersService.createCipherInOrganization( + body = CreateCipherInOrganizationJsonRequest( + cipher = createMockCipherJsonRequest(number = 1), + collectionIds = listOf("12345"), + ), + ) + assertEquals( + createMockCipher(number = 1), + result.getOrThrow(), + ) + } + @Test fun `createAttachment should return the correct response`() = runTest { server.enqueue(MockResponse().setBody(CREATE_ATTACHMENT_SUCCESS_JSON)) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt index 6d8d3c131..c30f3cf6f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -37,6 +37,7 @@ import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.FileUploadType import com.x8bit.bitwarden.data.vault.datasource.network.model.FolderJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SendFileResponseJson @@ -1704,6 +1705,115 @@ class VaultRepositoryTest { ) } + @Test + fun `createCipherInOrganization with no active user should return CreateCipherResult Error`() = + runTest { + fakeAuthDiskSource.userState = null + + val result = vaultRepository.createCipherInOrganization( + cipherView = mockk(), + collectionIds = mockk(), + ) + + assertEquals( + CreateCipherResult.Error, + result, + ) + } + + @Test + @Suppress("MaxLineLength") + fun `createCipherInOrganization with encryptCipher failure should return CreateCipherResult Error`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val mockCipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = mockCipherView, + ) + } returns IllegalStateException().asFailure() + + val result = vaultRepository.createCipherInOrganization( + cipherView = mockCipherView, + collectionIds = mockk(), + ) + + assertEquals( + CreateCipherResult.Error, + result, + ) + } + + @Test + @Suppress("MaxLineLength") + fun `createCipherInOrganization with ciphersService createCipher failure should return CreateCipherResult Error`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val mockCipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = mockCipherView, + ) + } returns createMockSdkCipher(number = 1).asSuccess() + coEvery { + ciphersService.createCipherInOrganization( + body = CreateCipherInOrganizationJsonRequest( + cipher = createMockCipherJsonRequest(number = 1, hasNullUri = true), + collectionIds = listOf("mockId-1"), + ), + ) + } returns IllegalStateException().asFailure() + + val result = vaultRepository.createCipherInOrganization( + cipherView = mockCipherView, + collectionIds = listOf("mockId-1"), + ) + + assertEquals( + CreateCipherResult.Error, + result, + ) + } + + @Test + @Suppress("MaxLineLength") + fun `createCipherInOrganization with ciphersService createCipher success should return CreateCipherResult success`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val mockCipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = userId, + cipherView = mockCipherView, + ) + } returns createMockSdkCipher(number = 1).asSuccess() + val mockCipher = createMockCipher(number = 1) + coEvery { + ciphersService.createCipherInOrganization( + body = CreateCipherInOrganizationJsonRequest( + cipher = createMockCipherJsonRequest(number = 1, hasNullUri = true), + collectionIds = listOf("mockId-1"), + ), + ) + } returns mockCipher.asSuccess() + coEvery { vaultDiskSource.saveCipher(userId, mockCipher) } just runs + + val result = vaultRepository.createCipherInOrganization( + cipherView = mockCipherView, + collectionIds = listOf("mockId-1"), + ) + + assertEquals( + CreateCipherResult.Success, + result, + ) + } + @Test fun `updateCipher with no active user should return UpdateCipherResult Error`() = runTest { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt index c7944d33d..fe6a3ef8c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt @@ -1633,6 +1633,8 @@ class VaultAddEditScreenTest : BaseComposeTest() { @Test fun `clicking a Ownership option should send OwnershipChange action`() { + updateStateWithOwners() + // Opens the menu composeTestRule .onNodeWithContentDescriptionAfterScroll( @@ -1642,20 +1644,27 @@ class VaultAddEditScreenTest : BaseComposeTest() { // Choose the option from the menu composeTestRule - .onAllNodesWithText(text = "a@b.com") + .onAllNodesWithText(text = "mockOwnerName-2") .onLast() .performScrollTo() .performClick() verify { viewModel.trySendAction( - VaultAddEditAction.Common.OwnershipChange("a@b.com"), + VaultAddEditAction.Common.OwnershipChange( + VaultAddEditState.Owner( + id = "mockOwnerId-2", + name = "mockOwnerName-2", + collections = emptyList(), + ), + ), ) } } @Test fun `the Ownership control should display the text provided by the state`() { + updateStateWithOwners() composeTestRule .onNodeWithContentDescriptionAfterScroll( label = "Who owns this item?, placeholder@email.com", @@ -1663,11 +1672,11 @@ class VaultAddEditScreenTest : BaseComposeTest() { .assertIsDisplayed() mutableStateFlow.update { currentState -> - updateCommonContent(currentState) { copy(ownership = "Owner 2") } + updateCommonContent(currentState) { copy(selectedOwnerId = "mockOwnerId-2") } } composeTestRule - .onNodeWithContentDescriptionAfterScroll(label = "Who owns this item?, Owner 2") + .onNodeWithContentDescriptionAfterScroll(label = "Who owns this item?, mockOwnerName-2") .assertIsDisplayed() } @@ -1705,7 +1714,7 @@ class VaultAddEditScreenTest : BaseComposeTest() { @Test fun `clicking a Folder Option should send FolderChange action`() { - mutableStateFlow.value = DEFAULT_STATE_SECURE_NOTES + updateStateWithFolders() // Opens the menu composeTestRule @@ -1714,14 +1723,19 @@ class VaultAddEditScreenTest : BaseComposeTest() { // Choose the option from the menu composeTestRule - .onAllNodesWithText(text = "Folder 1") + .onAllNodesWithText(text = "mockFolderName-1") .onLast() .performScrollTo() .performClick() verify { viewModel.trySendAction( - VaultAddEditAction.Common.FolderChange("Folder 1".asText()), + VaultAddEditAction.Common.FolderChange( + VaultAddEditState.Folder( + id = "mockFolderId-1", + name = "mockFolderName-1", + ), + ), ) } } @@ -1729,18 +1743,18 @@ class VaultAddEditScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test fun `the folder control should display the text provided by the state`() { - mutableStateFlow.value = DEFAULT_STATE_SECURE_NOTES + updateStateWithFolders() composeTestRule .onNodeWithContentDescriptionAfterScroll(label = "Folder, No Folder") .assertIsDisplayed() mutableStateFlow.update { currentState -> - updateCommonContent(currentState) { copy(folderName = "Folder 2".asText()) } + updateCommonContent(currentState) { copy(selectedFolderId = "mockFolderId-1") } } composeTestRule - .onNodeWithContentDescriptionAfterScroll(label = "Folder, Folder 2") + .onNodeWithContentDescriptionAfterScroll(label = "Folder, mockFolderName-1") .assertIsDisplayed() } @@ -1870,7 +1884,9 @@ class VaultAddEditScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test fun `Ownership option should send OwnershipChange action`() { - mutableStateFlow.value = DEFAULT_STATE_SECURE_NOTES + mutableStateFlow.value = DEFAULT_STATE_SECURE_NOTES + + updateStateWithOwners() // Opens the menu composeTestRule @@ -1879,14 +1895,20 @@ class VaultAddEditScreenTest : BaseComposeTest() { // Choose the option from the menu composeTestRule - .onAllNodesWithText(text = "a@b.com") + .onAllNodesWithText(text = "mockOwnerName-2") .onLast() .performScrollTo() .performClick() verify { viewModel.trySendAction( - VaultAddEditAction.Common.OwnershipChange("a@b.com"), + VaultAddEditAction.Common.OwnershipChange( + VaultAddEditState.Owner( + id = "mockOwnerId-2", + name = "mockOwnerName-2", + collections = emptyList(), + ), + ), ) } } @@ -1894,18 +1916,18 @@ class VaultAddEditScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test fun `in ItemType_SecureNotes the Ownership control should display the text provided by the state`() { - mutableStateFlow.value = DEFAULT_STATE_SECURE_NOTES + updateStateWithOwners() composeTestRule .onNodeWithContentDescriptionAfterScroll(label = "Who owns this item?, placeholder@email.com") .assertIsDisplayed() mutableStateFlow.update { currentState -> - updateCommonContent(currentState) { copy(ownership = "Owner 2") } + updateCommonContent(currentState) { copy(selectedOwnerId = "mockOwnerId-2") } } composeTestRule - .onNodeWithContentDescriptionAfterScroll(label = "Who owns this item?, Owner 2") + .onNodeWithContentDescriptionAfterScroll(label = "Who owns this item?, mockOwnerName-2") .assertIsDisplayed() } @@ -2459,6 +2481,28 @@ class VaultAddEditScreenTest : BaseComposeTest() { return currentState.copy(viewState = updatedType) } + private fun updateStateWithOwners() { + mutableStateFlow.update { currentState -> + updateCommonContent(currentState) { + copy( + selectedOwnerId = null, + availableOwners = DEFAULT_OWNERS, + ) + } + } + } + + private fun updateStateWithFolders() { + mutableStateFlow.update { + updateCommonContent(it) { + copy( + selectedFolderId = null, + availableFolders = DEFAULT_FOLDERS, + ) + } + } + } + //endregion Helper functions companion object { @@ -2526,5 +2570,38 @@ class VaultAddEditScreenTest : BaseComposeTest() { ), dialog = null, ) + + private val DEFAULT_OWNERS = listOf( + VaultAddEditState.Owner( + id = null, + name = "placeholder@email.com", + collections = emptyList(), + ), + VaultAddEditState.Owner( + id = "mockOwnerId-1", + name = "mockOwnerName-1", + collections = emptyList(), + ), + VaultAddEditState.Owner( + id = "mockOwnerId-2", + name = "mockOwnerName-2", + collections = emptyList(), + ), + ) + + private val DEFAULT_FOLDERS = listOf( + VaultAddEditState.Folder( + id = null, + name = "No Folder", + ), + VaultAddEditState.Folder( + id = "mockFolderId-1", + name = "mockFolderName-1", + ), + VaultAddEditState.Folder( + id = "mockFolderId-2", + name = "mockFolderName-2", + ), + ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index f6f0535ea..14fe63631 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -3,16 +3,23 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.bitwarden.core.CipherView +import com.bitwarden.core.CollectionView +import com.bitwarden.core.FolderView +import com.bitwarden.core.SendView import com.bitwarden.core.UriMatchType import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult +import com.x8bit.bitwarden.data.auth.repository.model.Organization +import com.x8bit.bitwarden.data.auth.repository.model.UserState +import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.repository.model.DataState +import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository import com.x8bit.bitwarden.data.tools.generator.repository.util.FakeGeneratorRepository @@ -22,8 +29,8 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult +import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest -import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode @@ -60,7 +67,10 @@ import java.util.UUID @Suppress("LargeClass") class VaultAddEditViewModelTest : BaseViewModelTest() { - private val authRepository: AuthRepository = mockk() + private val mutableUserStateFlow = MutableStateFlow(createUserState()) + private val authRepository: AuthRepository = mockk { + every { userStateFlow } returns mutableUserStateFlow + } private val loginInitialState = createVaultAddItemState( typeContentViewState = createLoginTypeContentViewState(), @@ -72,11 +82,13 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { private val totpTestCodeFlow: MutableSharedFlow = bufferedMutableSharedFlow() - private val mutableVaultItemFlow = MutableStateFlow>(DataState.Loading) - private val resourceManager: ResourceManager = mockk() + private val mutableVaultDataFlow = MutableStateFlow>(DataState.Loading) + private val resourceManager: ResourceManager = mockk { + every { getString(R.string.folder_none) } returns "No Folder" + } private val clipboardManager: BitwardenClipboardManager = mockk() private val vaultRepository: VaultRepository = mockk { - every { getVaultItemStateFlow(DEFAULT_EDIT_ITEM_ID) } returns mutableVaultItemFlow + every { vaultDataStateFlow } returns mutableVaultDataFlow every { totpCodeFlow } returns totpTestCodeFlow } private val specialCircumstanceManager: SpecialCircumstanceManager = @@ -107,7 +119,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { ) viewModel.stateFlow.test { assertEquals( - loginInitialState, + loginInitialState.copy(viewState = VaultAddEditState.ViewState.Loading), awaitItem(), ) } @@ -123,9 +135,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { vaultAddEditType = vaultAddEditType, ), ) - assertEquals(initState, viewModel.stateFlow.value) - verify(exactly = 0) { - vaultRepository.getVaultItemStateFlow(DEFAULT_EDIT_ITEM_ID) + assertEquals( + initState.copy(viewState = VaultAddEditState.ViewState.Loading), + viewModel.stateFlow.value, + ) + verify(exactly = 1) { + vaultRepository.vaultDataStateFlow } } @@ -152,9 +167,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { vaultAddEditType = vaultAddEditType, ), ) - assertEquals(initState, viewModel.stateFlow.value) - verify(exactly = 0) { - vaultRepository.getVaultItemStateFlow(DEFAULT_EDIT_ITEM_ID) + assertEquals( + initState.copy(viewState = VaultAddEditState.ViewState.Loading), + viewModel.stateFlow.value, + ) + verify(exactly = 1) { + vaultRepository.vaultDataStateFlow } } @@ -173,7 +191,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { viewModel.stateFlow.value, ) verify(exactly = 1) { - vaultRepository.getVaultItemStateFlow(DEFAULT_EDIT_ITEM_ID) + vaultRepository.vaultDataStateFlow } } @@ -192,7 +210,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { viewModel.stateFlow.value, ) verify(exactly = 1) { - vaultRepository.getVaultItemStateFlow(DEFAULT_EDIT_ITEM_ID) + vaultRepository.vaultDataStateFlow } } @@ -266,20 +284,23 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") fun `ConfirmDeleteClick with DeleteCipherResult Success should emit ShowToast and NavigateBack`() = runTest { + val cipherView = createMockCipherView(1) val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID) val initState = createVaultAddItemState(vaultAddEditType = vaultAddEditType) + mutableVaultDataFlow.value = DataState.Loaded( + data = createVaultData(cipherView = cipherView), + ) val viewModel = createAddVaultItemViewModel( savedStateHandle = createSavedStateHandleWithState( state = initState, vaultAddEditType = vaultAddEditType, ), ) - mutableVaultItemFlow.value = DataState.Loaded(data = createMockCipherView(number = 1)) coEvery { vaultRepository.softDeleteCipher( cipherId = "mockId-1", - cipherView = createMockCipherView(number = 1), + cipherView = cipherView, ) } returns DeleteCipherResult.Success @@ -300,20 +321,24 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Test fun `ConfirmDeleteClick with DeleteCipherResult Failure should show generic error`() = runTest { + val cipherView = createMockCipherView(1) val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID) val initState = createVaultAddItemState(vaultAddEditType = vaultAddEditType) + mutableVaultDataFlow.value = DataState.Loaded( + data = createVaultData(cipherView = cipherView), + ) + val viewModel = createAddVaultItemViewModel( savedStateHandle = createSavedStateHandleWithState( state = initState, vaultAddEditType = vaultAddEditType, ), ) - mutableVaultItemFlow.value = DataState.Loaded(data = createMockCipherView(number = 1)) coEvery { vaultRepository.softDeleteCipher( cipherId = "mockId-1", - cipherView = createMockCipherView(number = 1), + cipherView = cipherView, ) } returns DeleteCipherResult.Error @@ -327,11 +352,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { ), commonContentViewState = createCommonContentViewState( name = "mockName-1", - folder = "mockId-1".asText(), - ownership = "", originalCipher = createMockCipherView(number = 1), - availableFolders = emptyList(), - availableOwners = emptyList(), notes = "mockNotes-1", customFieldData = listOf( VaultAddEditState.Custom.HiddenField( @@ -347,7 +368,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { uri = listOf(UriItem("testId", "www.mockuri1.com", UriMatchType.HOST)), totpCode = "mockTotp-1", canViewPassword = false, - ), + ) + .copy(totp = "mockTotp-1"), ), viewModel.stateFlow.value, ) @@ -362,17 +384,21 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { R.string.saving.asText(), ), commonContentViewState = createCommonContentViewState( - name = "mockName", + name = "mockName-1", ), ) val stateWithName = createVaultAddItemState( vaultAddEditType = VaultAddEditType.AddItem, commonContentViewState = createCommonContentViewState( - name = "mockName", + name = "mockName-1", ), ) + mutableVaultDataFlow.value = DataState.Loaded( + createVaultData(), + ) + val viewModel = createAddVaultItemViewModel( createSavedStateHandleWithState( state = stateWithName, @@ -381,7 +407,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { ) coEvery { - vaultRepository.createCipher(any()) + vaultRepository.createCipherInOrganization(any(), any()) } returns CreateCipherResult.Success viewModel.stateFlow.test { @@ -392,7 +418,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { } coVerify(exactly = 1) { - vaultRepository.createCipher(any()) + vaultRepository.createCipherInOrganization(any(), any()) } } @@ -401,10 +427,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { val stateWithName = createVaultAddItemState( vaultAddEditType = VaultAddEditType.AddItem, commonContentViewState = createCommonContentViewState( - name = "mockName", + name = "mockName-1", ), ) + mutableVaultDataFlow.value = DataState.Loaded(createVaultData()) + val viewModel = createAddVaultItemViewModel( createSavedStateHandleWithState( state = stateWithName, @@ -413,7 +441,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { ) coEvery { - vaultRepository.createCipher(any()) + vaultRepository.createCipherInOrganization(any(), any()) } returns CreateCipherResult.Success viewModel.eventFlow.test { viewModel.actionChannel.trySend(VaultAddEditAction.Common.SaveClick) @@ -423,12 +451,14 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Test fun `in add mode, SaveClick createCipher error should emit ShowToast`() = runTest { + val stateWithName = createVaultAddItemState( vaultAddEditType = VaultAddEditType.AddItem, commonContentViewState = createCommonContentViewState( - name = "mockName", + name = "mockName-1", ), ) + mutableVaultDataFlow.value = DataState.Loaded(createVaultData()) val viewModel = createAddVaultItemViewModel( createSavedStateHandleWithState( @@ -438,7 +468,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { ) coEvery { - vaultRepository.createCipher(any()) + vaultRepository.createCipherInOrganization(any(), any()) } returns CreateCipherResult.Error viewModel.eventFlow.test { viewModel.actionChannel.trySend(VaultAddEditAction.Common.SaveClick) @@ -449,7 +479,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Test fun `in edit mode, SaveClick should show dialog, and remove it once an item is saved`() = runTest { - val cipherView = mockk() + val cipherView = createMockCipherView(1) val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID) val stateWithDialog = createVaultAddItemState( vaultAddEditType = vaultAddEditType, @@ -457,14 +487,32 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { R.string.saving.asText(), ), commonContentViewState = createCommonContentViewState( - name = "mockName", + name = "mockName-1", + originalCipher = cipherView, + customFieldData = listOf( + VaultAddEditState.Custom.HiddenField( + itemId = "testId", + name = "mockName-1", + value = "mockValue-1", + ), + ), + notes = "mockNotes-1", ), ) val stateWithName = createVaultAddItemState( vaultAddEditType = vaultAddEditType, commonContentViewState = createCommonContentViewState( - name = "mockName", + name = "mockName-1", + originalCipher = cipherView, + customFieldData = listOf( + VaultAddEditState.Custom.HiddenField( + itemId = "testId", + name = "mockName-1", + value = "mockValue-1", + ), + ), + notes = "mockNotes-1", ), ) every { @@ -473,7 +521,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { resourceManager = resourceManager, ) } returns stateWithName.viewState - mutableVaultItemFlow.value = DataState.Loaded(cipherView) + mutableVaultDataFlow.value = DataState.Loaded( + data = createVaultData(cipherView = cipherView), + ) val viewModel = createAddVaultItemViewModel( createSavedStateHandleWithState( @@ -506,12 +556,21 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Test fun `in edit mode, SaveClick updateCipher error with a null message should show an error dialog with a generic message`() = runTest { - val cipherView = mockk() + val cipherView = createMockCipherView(1) val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID) val stateWithName = createVaultAddItemState( vaultAddEditType = vaultAddEditType, commonContentViewState = createCommonContentViewState( - name = "mockName", + name = "mockName-1", + originalCipher = cipherView, + customFieldData = listOf( + VaultAddEditState.Custom.HiddenField( + itemId = "testId", + name = "mockName-1", + value = "mockValue-1", + ), + ), + notes = "mockNotes-1", ), ) @@ -524,7 +583,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { coEvery { vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any()) } returns UpdateCipherResult.Error(errorMessage = null) - mutableVaultItemFlow.value = DataState.Loaded(cipherView) + mutableVaultDataFlow.value = DataState.Loaded( + data = createVaultData(cipherView = cipherView), + ) val viewModel = createAddVaultItemViewModel( createSavedStateHandleWithState( @@ -553,26 +614,34 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Test fun `in edit mode, SaveClick updateCipher error with a non-null message should show an error dialog with that message`() = runTest { - val cipherView = mockk() + val cipherView = createMockCipherView(1) val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID) val stateWithName = createVaultAddItemState( vaultAddEditType = vaultAddEditType, commonContentViewState = createCommonContentViewState( - name = "mockName", + name = "mockName-1", + originalCipher = cipherView, + customFieldData = listOf( + VaultAddEditState.Custom.HiddenField( + itemId = "testId", + name = "mockName-1", + value = "mockValue-1", + ), + ), + notes = "mockNotes-1", ), ) val errorMessage = "You do not have permission to edit this." every { - cipherView.toViewState( - isClone = false, - resourceManager = resourceManager, - ) + cipherView.toViewState(isClone = false, resourceManager = resourceManager) } returns stateWithName.viewState coEvery { vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any()) } returns UpdateCipherResult.Error(errorMessage = errorMessage) - mutableVaultItemFlow.value = DataState.Loaded(cipherView) + mutableVaultDataFlow.value = DataState.Loaded( + createVaultData(cipherView = cipherView), + ) val viewModel = createAddVaultItemViewModel( createSavedStateHandleWithState( @@ -599,6 +668,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Test fun `Saving item with an empty name field will cause a dialog to show up`() = runTest { + mutableVaultDataFlow.value = DataState.Loaded( + createVaultData(cipherView = createMockCipherView(1)), + ) val stateWithNoName = createVaultAddItemState( commonContentViewState = createCommonContentViewState(name = ""), ) @@ -627,6 +699,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Test fun `HandleDialogDismiss will remove the current dialog`() = runTest { + mutableVaultDataFlow.value = DataState.Loaded( + createVaultData(cipherView = createMockCipherView(1)), + ) val errorState = createVaultAddItemState( vaultAddEditType = VaultAddEditType.AddItem, dialogState = VaultAddEditState.DialogState.Generic( @@ -652,6 +727,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Test fun `TypeOptionSelect LOGIN should switch to LoginItem`() = runTest { + mutableVaultDataFlow.value = DataState.Loaded( + createVaultData(cipherView = createMockCipherView(1)), + ) val viewModel = createAddVaultItemViewModel() val action = VaultAddEditAction.Common.TypeOptionSelect( VaultAddEditState.ItemTypeOption.LOGIN, @@ -678,6 +756,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Test fun `TypeOptionSelect CARD should switch to CardItem`() = runTest { + mutableVaultDataFlow.value = DataState.Loaded( + createVaultData(cipherView = createMockCipherView(1)), + ) val viewModel = createAddVaultItemViewModel() val action = VaultAddEditAction.Common.TypeOptionSelect( VaultAddEditState.ItemTypeOption.CARD, @@ -704,6 +785,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Test fun `TypeOptionSelect IDENTITY should switch to IdentityItem`() = runTest { + mutableVaultDataFlow.value = DataState.Loaded( + createVaultData(cipherView = createMockCipherView(1)), + ) val viewModel = createAddVaultItemViewModel() val action = VaultAddEditAction.Common.TypeOptionSelect( VaultAddEditState.ItemTypeOption.IDENTITY, @@ -730,6 +814,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Test fun `TypeOptionSelect SECURE_NOTES should switch to SecureNotesItem`() = runTest { + mutableVaultDataFlow.value = DataState.Loaded( + createVaultData(cipherView = createMockCipherView(1)), + ) val viewModel = createAddVaultItemViewModel() val action = VaultAddEditAction.Common.TypeOptionSelect( VaultAddEditState.ItemTypeOption.SECURE_NOTES, @@ -760,6 +847,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { + mutableVaultDataFlow.value = DataState.Loaded( + createVaultData(cipherView = createMockCipherView(1)), + ) + viewModel = createAddVaultItemViewModel() } @@ -846,7 +937,11 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { ), ) - mutableVaultItemFlow.value = DataState.Loaded(data = cipherView) + mutableVaultDataFlow.value = DataState.Loaded( + data = createVaultData( + cipherView = cipherView, + ), + ) val breachCount = 5 coEvery { @@ -1053,6 +1148,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { + mutableVaultDataFlow.value = DataState.Loaded( + createVaultData(cipherView = createMockCipherView(1)), + ) vaultAddItemInitialState = createVaultAddItemState( typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.Identity(), ) @@ -1344,6 +1442,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { + mutableVaultDataFlow.value = DataState.Loaded( + createVaultData(cipherView = createMockCipherView(1)), + ) vaultAddItemInitialState = createVaultAddItemState( typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.Card(), ) @@ -1456,6 +1557,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { + mutableVaultDataFlow.value = DataState.Loaded( + createVaultData(cipherView = createMockCipherView(1)), + ) vaultAddItemInitialState = createVaultAddItemState() secureNotesInitialSavedStateHandle = createSavedStateHandleWithState( state = vaultAddItemInitialState, @@ -1493,16 +1597,18 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Test fun `FolderChange should update folder`() = runTest { val action = VaultAddEditAction.Common.FolderChange( - "newFolder".asText(), + VaultAddEditState.Folder( + id = "mockId-1", + name = "Folder 1", + ), ) viewModel.actionChannel.trySend(action) val expectedState = vaultAddItemInitialState.copy( viewState = VaultAddEditState.ViewState.Content( - common = createCommonContentViewState( - folder = "newFolder".asText(), - ), + common = createCommonContentViewState() + .copy(selectedFolderId = "mockId-1"), type = createLoginTypeContentViewState(), ), ) @@ -1570,15 +1676,20 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Test fun `OwnershipChange should update ownership`() = runTest { - val action = VaultAddEditAction.Common.OwnershipChange(ownership = "newOwner") + val action = VaultAddEditAction.Common.OwnershipChange( + ownership = VaultAddEditState.Owner( + id = "mockId-1", + name = "a@b.com", + collections = emptyList(), + ), + ) viewModel.actionChannel.trySend(action) val expectedState = vaultAddItemInitialState.copy( viewState = VaultAddEditState.ViewState.Content( - common = createCommonContentViewState( - ownership = "newOwner", - ), + common = createCommonContentViewState() + .copy(selectedOwnerId = "mockId-1"), type = createLoginTypeContentViewState(), ), ) @@ -1917,28 +2028,38 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Suppress("LongParameterList") private fun createCommonContentViewState( name: String = "", - folder: Text = R.string.folder_none.asText(), favorite: Boolean = false, masterPasswordReprompt: Boolean = false, notes: String = "", customFieldData: List = listOf(), - ownership: String = "placeholder@email.com", originalCipher: CipherView? = null, - availableFolders: List = listOf( - "Folder 1".asText(), - "Folder 2".asText(), - "Folder 3".asText(), + availableFolders: List = listOf( + VaultAddEditState.Folder( + id = null, + name = "No Folder", + ), + ), + availableOwners: List = listOf( + VaultAddEditState.Owner( + id = null, + name = "activeEmail", + collections = emptyList(), + ), + VaultAddEditState.Owner( + id = "organizationId", + name = "organizationName", + collections = emptyList(), + ), ), - availableOwners: List = listOf("a@b.com", "c@d.com"), ): VaultAddEditState.ViewState.Content.Common = VaultAddEditState.ViewState.Content.Common( name = name, - folderName = folder, + selectedFolderId = null, favorite = favorite, customFieldData = customFieldData, masterPasswordReprompt = masterPasswordReprompt, notes = notes, - ownership = ownership, + selectedOwnerId = null, originalCipher = originalCipher, availableFolders = availableFolders, availableOwners = availableOwners, @@ -1992,6 +2113,45 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { authRepository = authRepository, ) + private fun createVaultData( + cipherView: CipherView? = null, + collectionViewList: List = emptyList(), + folderViewList: List = emptyList(), + sendViewList: List = emptyList(), + ): VaultData = + VaultData( + cipherViewList = cipherView?.let { listOf(it) } ?: emptyList(), + collectionViewList = collectionViewList, + folderViewList = folderViewList, + sendViewList = sendViewList, + ) + + fun createUserState(): UserState = + UserState( + activeUserId = "activeUserId", + accounts = listOf( + UserState.Account( + userId = "activeUserId", + name = "activeName", + email = "activeEmail", + avatarColorHex = "#ffecbc49", + environment = Environment.Eu, + isPremium = true, + isLoggedIn = false, + isVaultUnlocked = false, + organizations = listOf( + Organization( + id = "organizationId", + name = "organizationName", + ), + ), + isBiometricsEnabled = true, + vaultUnlockType = VaultUnlockType.MASTER_PASSWORD, + ), + ), + hasPendingAccountAddition = false, + ) + /** * A function to test the changes in custom fields for each type. */ @@ -2135,4 +2295,4 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { private const val TEST_ID = "testId" -private const val DEFAULT_EDIT_ITEM_ID: String = "edit_item_id" +private const val DEFAULT_EDIT_ITEM_ID: String = "mockId-1" diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt index 57abbe902..3eb9e6718 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt @@ -13,7 +13,6 @@ import com.bitwarden.core.PasswordHistoryView import com.bitwarden.core.SecureNoteType import com.bitwarden.core.SecureNoteView import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem @@ -61,11 +60,9 @@ class CipherViewExtensionsTest { common = VaultAddEditState.ViewState.Content.Common( originalCipher = cipherView, name = "cipher", - folderName = R.string.folder_none.asText(), favorite = false, masterPasswordReprompt = true, notes = "Lots of notes", - ownership = "", customFieldData = listOf( VaultAddEditState.Custom.BooleanField(TEST_ID, "TestBoolean", false), VaultAddEditState.Custom.TextField(TEST_ID, "TestText", "TestText"), @@ -105,11 +102,9 @@ class CipherViewExtensionsTest { common = VaultAddEditState.ViewState.Content.Common( originalCipher = cipherView, name = "cipher", - folderName = R.string.folder_none.asText(), favorite = false, masterPasswordReprompt = true, notes = "Lots of notes", - ownership = "", customFieldData = listOf( VaultAddEditState.Custom.BooleanField(TEST_ID, "TestBoolean", false), VaultAddEditState.Custom.TextField(TEST_ID, "TestText", "TestText"), @@ -154,11 +149,9 @@ class CipherViewExtensionsTest { common = VaultAddEditState.ViewState.Content.Common( originalCipher = cipherView, name = "cipher", - folderName = R.string.folder_none.asText(), favorite = false, masterPasswordReprompt = true, notes = "Lots of notes", - ownership = "", availableFolders = emptyList(), availableOwners = emptyList(), customFieldData = listOf( @@ -198,11 +191,9 @@ class CipherViewExtensionsTest { common = VaultAddEditState.ViewState.Content.Common( originalCipher = cipherView, name = "cipher", - folderName = R.string.folder_none.asText(), favorite = false, masterPasswordReprompt = true, notes = "Lots of notes", - ownership = "", customFieldData = listOf( VaultAddEditState.Custom.BooleanField(TEST_ID, "TestBoolean", false), VaultAddEditState.Custom.TextField(TEST_ID, "TestText", "TestText"), @@ -231,11 +222,9 @@ class CipherViewExtensionsTest { common = VaultAddEditState.ViewState.Content.Common( originalCipher = cipherView, name = "cipher - Clone", - folderName = R.string.folder_none.asText(), favorite = false, masterPasswordReprompt = true, notes = "Lots of notes", - ownership = "", customFieldData = listOf( VaultAddEditState.Custom.BooleanField(TEST_ID, "TestBoolean", false), VaultAddEditState.Custom.TextField(TEST_ID, "TestText", "TestText"), diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationScreenTest.kt index 398caecab..bcd5df5c2 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationScreenTest.kt @@ -18,6 +18,7 @@ import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.util.onNodeWithContentDescriptionAfterScroll import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.util.createMockOrganizationList +import com.x8bit.bitwarden.ui.vault.model.VaultCollection import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -100,7 +101,7 @@ class VaultMoveToOrganizationScreenTest : BaseComposeTest() { id = "mockOrganizationId-2", name = "mockOrganizationName-2", collections = listOf( - VaultMoveToOrganizationState.ViewState.Content.Collection( + VaultCollection( id = "mockId-2", name = "mockName-2", isSelected = false, @@ -141,7 +142,7 @@ class VaultMoveToOrganizationScreenTest : BaseComposeTest() { verify { viewModel.trySendAction( VaultMoveToOrganizationAction.CollectionSelect( - VaultMoveToOrganizationState.ViewState.Content.Collection( + VaultCollection( id = "mockId-1", name = "mockName-1", isSelected = true, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModelTest.kt index de4b08438..c41c667cd 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModelTest.kt @@ -18,6 +18,7 @@ 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.concat import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.util.createMockOrganizationList +import com.x8bit.bitwarden.ui.vault.model.VaultCollection import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -119,7 +120,7 @@ class VaultMoveToOrganizationViewModelTest : BaseViewModelTest() { mutableCollectionFlow.tryEmit(value = DataState.Loaded(DEFAULT_COLLECTIONS)) mutableVaultItemFlow.tryEmit(value = DataState.Loaded(createMockCipherView(number = 1))) val unselectCollection1Action = VaultMoveToOrganizationAction.CollectionSelect( - VaultMoveToOrganizationState.ViewState.Content.Collection( + VaultCollection( id = "mockId-1", name = "mockName-1", isSelected = true, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/util/VaultMoveToOrganizationTestUtil.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/util/VaultMoveToOrganizationTestUtil.kt index f0f7fccac..9a856d6b8 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/util/VaultMoveToOrganizationTestUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/util/VaultMoveToOrganizationTestUtil.kt @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.ui.vault.feature.movetoorganization.util import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.VaultMoveToOrganizationState +import com.x8bit.bitwarden.ui.vault.model.VaultCollection /** * Creates a list of mock [VaultMoveToOrganizationState.ViewState.Content.Organization]. @@ -24,7 +25,7 @@ fun createMockOrganization( id = "mockOrganizationId-$number", name = "mockOrganizationName-$number", collections = listOf( - VaultMoveToOrganizationState.ViewState.Content.Collection( + VaultCollection( id = "mockId-$number", name = "mockName-$number", isSelected = isCollectionSelected, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt index 409d430ae..cc11bb91a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt @@ -12,7 +12,6 @@ import com.bitwarden.core.PasswordHistoryView import com.bitwarden.core.SecureNoteType import com.bitwarden.core.SecureNoteView import com.bitwarden.core.UriMatchType -import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle @@ -42,11 +41,11 @@ class VaultAddItemStateExtensionsTest { val loginItemType = VaultAddEditState.ViewState.Content( common = VaultAddEditState.ViewState.Content.Common( name = "mockName-1", - folderName = "mockFolder-1".asText(), + selectedFolderId = "mockFolderId-1", favorite = false, masterPasswordReprompt = false, notes = "mockNotes-1", - ownership = "mockOwnership-1", + selectedOwnerId = "mockOwnerId-1", ), type = VaultAddEditState.ViewState.Content.ItemType.Login( username = "mockUsername-1", @@ -61,8 +60,8 @@ class VaultAddItemStateExtensionsTest { assertEquals( CipherView( id = null, - organizationId = null, - folderId = null, + organizationId = "mockOwnerId-1", + folderId = "mockFolderId-1", collectionIds = emptyList(), key = null, name = "mockName-1", @@ -109,7 +108,7 @@ class VaultAddItemStateExtensionsTest { common = VaultAddEditState.ViewState.Content.Common( originalCipher = cipherView, name = "mockName-1", - folderName = "mockFolder-1".asText(), + selectedFolderId = "mockFolderId-1", favorite = true, masterPasswordReprompt = false, customFieldData = listOf( @@ -123,7 +122,7 @@ class VaultAddItemStateExtensionsTest { ), ), notes = "mockNotes-1", - ownership = "mockOwnership-1", + selectedOwnerId = "mockOwnerId-1", ), type = VaultAddEditState.ViewState.Content.ItemType.Login( username = "mockUsername-1", @@ -141,6 +140,8 @@ class VaultAddItemStateExtensionsTest { name = "mockName-1", notes = "mockNotes-1", type = CipherType.LOGIN, + folderId = "mockFolderId-1", + organizationId = "mockOwnerId-1", login = LoginView( username = "mockUsername-1", password = "mockPassword-1", @@ -200,11 +201,11 @@ class VaultAddItemStateExtensionsTest { val viewState = VaultAddEditState.ViewState.Content( common = VaultAddEditState.ViewState.Content.Common( name = "mockName-1", - folderName = "mockFolder-1".asText(), + selectedFolderId = "mockId-1", favorite = false, masterPasswordReprompt = false, notes = "mockNotes-1", - ownership = "mockOwnership-1", + selectedOwnerId = "mockOwnership-1", customFieldData = listOf( VaultAddEditState.Custom.BooleanField("testId", "TestBoolean", false), VaultAddEditState.Custom.TextField("testId", "TestText", "TestText"), @@ -219,8 +220,8 @@ class VaultAddItemStateExtensionsTest { assertEquals( CipherView( id = null, - organizationId = null, - folderId = null, + organizationId = "mockOwnership-1", + folderId = "mockId-1", collectionIds = emptyList(), key = null, name = "mockName-1", @@ -273,11 +274,11 @@ class VaultAddItemStateExtensionsTest { common = VaultAddEditState.ViewState.Content.Common( originalCipher = cipherView, name = "mockName-1", - folderName = "mockFolder-1".asText(), + selectedFolderId = "mockId-1", favorite = false, masterPasswordReprompt = true, notes = "mockNotes-1", - ownership = "mockOwnership-1", + selectedOwnerId = "mockOwnerId-1", customFieldData = emptyList(), ), type = VaultAddEditState.ViewState.Content.ItemType.SecureNotes, @@ -289,6 +290,8 @@ class VaultAddItemStateExtensionsTest { cipherView.copy( name = "mockName-1", notes = "mockNotes-1", + organizationId = "mockOwnerId-1", + folderId = "mockId-1", type = CipherType.SECURE_NOTE, secureNote = SecureNoteView(SecureNoteType.GENERIC), reprompt = CipherRepromptType.PASSWORD, @@ -305,11 +308,11 @@ class VaultAddItemStateExtensionsTest { val viewState = VaultAddEditState.ViewState.Content( common = VaultAddEditState.ViewState.Content.Common( name = "mockName-1", - folderName = "mockFolder-1".asText(), + selectedFolderId = "mockId-1", favorite = false, masterPasswordReprompt = false, notes = "mockNotes-1", - ownership = "mockOwnership-1", + selectedOwnerId = "mockOwnerId-1", ), type = VaultAddEditState.ViewState.Content.ItemType.Identity( selectedTitle = VaultIdentityTitle.MR, @@ -338,8 +341,8 @@ class VaultAddItemStateExtensionsTest { assertEquals( CipherView( id = null, - organizationId = null, - folderId = null, + organizationId = "mockOwnerId-1", + folderId = "mockId-1", collectionIds = emptyList(), key = null, name = "mockName-1", @@ -392,7 +395,7 @@ class VaultAddItemStateExtensionsTest { common = VaultAddEditState.ViewState.Content.Common( originalCipher = cipherView, name = "mockName-1", - folderName = "mockFolder-1".asText(), + selectedFolderId = "mockId-1", favorite = true, masterPasswordReprompt = false, customFieldData = listOf( @@ -406,7 +409,7 @@ class VaultAddItemStateExtensionsTest { ), ), notes = "mockNotes-1", - ownership = "mockOwnership-1", + selectedOwnerId = "mockOwnerId-1", ), type = VaultAddEditState.ViewState.Content.ItemType.Identity( selectedTitle = VaultIdentityTitle.MR, @@ -437,6 +440,8 @@ class VaultAddItemStateExtensionsTest { cipherView.copy( name = "mockName-1", notes = "mockNotes-1", + organizationId = "mockOwnerId-1", + folderId = "mockId-1", type = CipherType.IDENTITY, identity = IdentityView( title = "MR",