BIT-1309: Ownership in the add item screen (#831)

This commit is contained in:
Ramsey Smith 2024-01-30 09:11:46 -07:00 committed by Álison Fernandes
parent f1a799955c
commit b3f23ab172
30 changed files with 1095 additions and 286 deletions

View file

@ -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<SyncResponseJson.Cipher>
/**
* Create a cipher that belongs to an organization.
*/
@POST("ciphers/create")
suspend fun createCipherInOrganization(
@Body body: CreateCipherInOrganizationJsonRequest,
): Result<SyncResponseJson.Cipher>
/**
* Associates an attachment with a cipher.
*/

View file

@ -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<String>,
)

View file

@ -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<SyncResponseJson.Cipher>
/**
* Attempt to create a cipher that belongs to an organization.
*/
suspend fun createCipherInOrganization(
body: CreateCipherInOrganizationJsonRequest,
): Result<SyncResponseJson.Cipher>
/**
* Attempt to upload an attachment file.
*/

View file

@ -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<SyncResponseJson.Cipher> =
ciphersApi.createCipher(body = body)
override suspend fun createCipherInOrganization(
body: CreateCipherInOrganizationJsonRequest,
): Result<SyncResponseJson.Cipher> = ciphersApi.createCipherInOrganization(body = body)
override suspend fun createAttachment(
cipherId: String,
body: AttachmentJsonRequest,

View file

@ -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<String>,
): CreateCipherResult
/**
* Attempt to create an attachment for the given [cipherView].
*/

View file

@ -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<String>,
): 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

View file

@ -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<VaultCollection>?,
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),
)
}
}
}
}

View file

@ -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))

View file

@ -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))

View file

@ -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 {

View file

@ -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 {

View file

@ -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<VaultAddEditState.Owner>.toUpdatedOwners(
selectedOwnerId: String?,
selectedCollectionId: String,
): List<VaultAddEditState.Owner> =
map { owner ->
if (owner.id != selectedOwnerId) return@map owner
owner.copy(
collections = owner
.collections
.toUpdatedCollections(selectedCollectionId = selectedCollectionId),
)
}
private fun List<VaultCollection>.toUpdatedCollections(
selectedCollectionId: String,
): List<VaultCollection> =
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<Custom> = emptyList(),
val notes: String = "",
val folderName: Text = DEFAULT_FOLDER,
val availableFolders: List<Text> = 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<String> = listOf("a@b.com", "c@d.com"),
val selectedFolderId: String? = null,
val availableFolders: List<Folder> = emptyList(),
val selectedOwnerId: String? = null,
val availableOwners: List<Owner> = 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<VaultCollection>,
) : 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<CipherView?>,
val vaultData: DataState<VaultData>,
val userData: UserState?,
) : Internal()
/**

View file

@ -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,
),
)
},
)
}
}

View file

@ -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<FolderView>,
collectionViewList: List<CollectionView>,
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<FolderView>.toSelectedFolderId(cipherView: CipherView?): String? =
cipherView
?.folderId
?.takeIf { id -> id in map { it.id } }
private fun List<FolderView>.toAvailableFolders(
resourceManager: ResourceManager,
): List<VaultAddEditState.Folder> =
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<CollectionView>,
): List<VaultAddEditState.Owner> =
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(

View file

@ -70,7 +70,6 @@ class VaultItemViewModel @Inject constructor(
verificationCode = it.code,
)
}
VaultItemAction.Internal.VaultDataReceive(
userState = userState,
vaultDataState = combineDataStates(

View file

@ -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,
)
}
}

View file

@ -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) {

View file

@ -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<Collection>,
) : 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<VaultCollection>,
) : 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<VaultMoveToOrganizationState.ViewState.Content.Organization>.to
)
}
private fun List<VaultMoveToOrganizationState.ViewState.Content.Collection>.toUpdatedCollections(
private fun List<VaultCollection>.toUpdatedCollections(
selectedCollectionId: String,
): List<VaultMoveToOrganizationState.ViewState.Content.Collection> =
): List<VaultCollection> =
map { collection ->
collection.copy(
isSelected = if (selectedCollectionId == collection.id) {

View file

@ -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<CipherView?, List<CollectionView>, UserState?>.toViewState():
collection.id != null
}
.map { collection ->
VaultMoveToOrganizationState.ViewState.Content.Collection(
VaultCollection(
id = collection.id.orEmpty(),
name = collection.name,
isSelected = currentCipher

View file

@ -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() },
)

View file

@ -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

View file

@ -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))

View file

@ -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 {

View file

@ -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",
),
)
}
}

View file

@ -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<UserState?>(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<TotpCodeResult> = bufferedMutableSharedFlow()
private val mutableVaultItemFlow = MutableStateFlow<DataState<CipherView?>>(DataState.Loading)
private val resourceManager: ResourceManager = mockk()
private val mutableVaultDataFlow = MutableStateFlow<DataState<VaultData>>(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<CipherView>()
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<CipherView>()
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<CipherView>()
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<VaultAddEditState.Custom> = listOf(),
ownership: String = "placeholder@email.com",
originalCipher: CipherView? = null,
availableFolders: List<Text> = listOf(
"Folder 1".asText(),
"Folder 2".asText(),
"Folder 3".asText(),
availableFolders: List<VaultAddEditState.Folder> = listOf(
VaultAddEditState.Folder(
id = null,
name = "No Folder",
),
),
availableOwners: List<VaultAddEditState.Owner> = listOf(
VaultAddEditState.Owner(
id = null,
name = "activeEmail",
collections = emptyList(),
),
VaultAddEditState.Owner(
id = "organizationId",
name = "organizationName",
collections = emptyList(),
),
),
availableOwners: List<String> = 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<CollectionView> = emptyList(),
folderViewList: List<FolderView> = emptyList(),
sendViewList: List<SendView> = 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"

View file

@ -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"),

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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",