BIT-526: Clone vault item (#713)

This commit is contained in:
Ramsey Smith 2024-01-22 15:03:20 -07:00 committed by Álison Fernandes
parent 3ec95b0ffd
commit a760127711
13 changed files with 235 additions and 30 deletions

View file

@ -92,8 +92,15 @@ fun NavGraphBuilder.vaultUnlockedGraph(
) )
vaultItemDestination( vaultItemDestination(
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
onNavigateToVaultEditItem = { onNavigateToVaultEditItem = { vaultItemId, isClone ->
navController.navigateToVaultAddEdit(VaultAddEditType.EditItem(it)) navController.navigateToVaultAddEdit(
if (isClone) {
VaultAddEditType.CloneItem(vaultItemId)
} else {
VaultAddEditType.EditItem(vaultItemId)
},
)
}, },
onNavigateToMoveToOrganization = { onNavigateToMoveToOrganization = {
navController.navigateToVaultMoveToOrganization(it) navController.navigateToVaultMoveToOrganization(it)

View file

@ -3,11 +3,14 @@ package com.x8bit.bitwarden.ui.platform.manager.di
import android.content.Context import android.content.Context
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManagerImpl import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManagerImpl
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManagerImpl
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/** /**
* Provides UI-based managers in the platform package. * Provides UI-based managers in the platform package.
@ -15,11 +18,18 @@ import dagger.hilt.components.SingletonComponent
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class PlatformUiManagerModule { class PlatformUiManagerModule {
@Provides @Provides
@Singleton
fun provideIntentManager( fun provideIntentManager(
@ApplicationContext context: Context, @ApplicationContext context: Context,
): IntentManager = ): IntentManager =
IntentManagerImpl( IntentManagerImpl(
context = context, context = context,
) )
@Provides
@Singleton
fun provideResourceManager(@ApplicationContext context: Context): ResourceManager =
ResourceManagerImpl(context = context)
} }

View file

@ -0,0 +1,19 @@
package com.x8bit.bitwarden.ui.platform.manager.resource
import androidx.annotation.StringRes
/**
* Interface for managing resources.
*/
interface ResourceManager {
/**
* Method for returning a permission string from a [resId].
*/
fun getString(@StringRes resId: Int): String
/**
* Method for returning a permission string from a [resId] with [formatArgs].
*/
fun getString(@StringRes resId: Int, vararg formatArgs: Any): String
}

View file

@ -0,0 +1,17 @@
package com.x8bit.bitwarden.ui.platform.manager.resource
import android.content.Context
import androidx.annotation.StringRes
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
/**
* Primary implementation of [ResourceManager].
*/
@OmitFromCoverage
class ResourceManagerImpl(private val context: Context) : ResourceManager {
override fun getString(@StringRes resId: Int): String =
context.getString(resId)
override fun getString(@StringRes resId: Int, vararg formatArgs: Any): String =
context.getString(resId, *formatArgs)
}

View file

@ -13,6 +13,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
private const val ADD_TYPE: String = "add" private const val ADD_TYPE: String = "add"
private const val EDIT_TYPE: String = "edit" private const val EDIT_TYPE: String = "edit"
private const val CLONE_TYPE: String = "clone"
private const val EDIT_ITEM_ID: String = "vault_edit_id" private const val EDIT_ITEM_ID: String = "vault_edit_id"
private const val ADD_EDIT_ITEM_PREFIX: String = "vault_add_edit_item" private const val ADD_EDIT_ITEM_PREFIX: String = "vault_add_edit_item"
@ -32,6 +33,7 @@ data class VaultAddEditArgs(
vaultAddEditType = when (requireNotNull(savedStateHandle[ADD_EDIT_ITEM_TYPE])) { vaultAddEditType = when (requireNotNull(savedStateHandle[ADD_EDIT_ITEM_TYPE])) {
ADD_TYPE -> VaultAddEditType.AddItem ADD_TYPE -> VaultAddEditType.AddItem
EDIT_TYPE -> VaultAddEditType.EditItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID])) EDIT_TYPE -> VaultAddEditType.EditItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID]))
CLONE_TYPE -> VaultAddEditType.CloneItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID]))
else -> throw IllegalStateException("Unknown VaultAddEditType.") else -> throw IllegalStateException("Unknown VaultAddEditType.")
}, },
) )
@ -79,7 +81,12 @@ private fun VaultAddEditType.toTypeString(): String =
when (this) { when (this) {
is VaultAddEditType.AddItem -> ADD_TYPE is VaultAddEditType.AddItem -> ADD_TYPE
is VaultAddEditType.EditItem -> EDIT_TYPE is VaultAddEditType.EditItem -> EDIT_TYPE
is VaultAddEditType.CloneItem -> CLONE_TYPE
} }
private fun VaultAddEditType.toIdOrNull(): String? = private fun VaultAddEditType.toIdOrNull(): String? =
(this as? VaultAddEditType.EditItem)?.vaultItemId when (this) {
is VaultAddEditType.AddItem -> null
is VaultAddEditType.CloneItem -> vaultItemId
is VaultAddEditType.EditItem -> vaultItemId
}

View file

@ -18,6 +18,7 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.concat import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField
@ -56,6 +57,7 @@ class VaultAddEditViewModel @Inject constructor(
private val clipboardManager: BitwardenClipboardManager, private val clipboardManager: BitwardenClipboardManager,
private val vaultRepository: VaultRepository, private val vaultRepository: VaultRepository,
private val generatorRepository: GeneratorRepository, private val generatorRepository: GeneratorRepository,
private val resourceManager: ResourceManager,
) : BaseViewModel<VaultAddEditState, VaultAddEditEvent, VaultAddEditAction>( ) : BaseViewModel<VaultAddEditState, VaultAddEditEvent, VaultAddEditAction>(
// We load the state from the savedStateHandle for testing purposes. // We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE] initialState = savedStateHandle[KEY_STATE]
@ -70,6 +72,7 @@ class VaultAddEditViewModel @Inject constructor(
) )
is VaultAddEditType.EditItem -> VaultAddEditState.ViewState.Loading is VaultAddEditType.EditItem -> VaultAddEditState.ViewState.Loading
is VaultAddEditType.CloneItem -> VaultAddEditState.ViewState.Loading
}, },
dialog = null, dialog = null,
) )
@ -79,18 +82,18 @@ class VaultAddEditViewModel @Inject constructor(
//region Initialization and Overrides //region Initialization and Overrides
init { init {
when (val vaultAddEditType = state.vaultAddEditType) { state
VaultAddEditType.AddItem -> Unit .vaultAddEditType
is VaultAddEditType.EditItem -> { .vaultItemId
?.let { itemId ->
vaultRepository vaultRepository
.getVaultItemStateFlow(vaultAddEditType.vaultItemId) .getVaultItemStateFlow(itemId)
// We'll stop getting updates as soon as we get some loaded data. // We'll stop getting updates as soon as we get some loaded data.
.takeUntilLoaded() .takeUntilLoaded()
.map { VaultAddEditAction.Internal.VaultDataReceive(it) } .map { VaultAddEditAction.Internal.VaultDataReceive(it) }
.onEach(::sendAction) .onEach(::sendAction)
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
}
vaultRepository vaultRepository
.totpCodeFlow .totpCodeFlow
@ -240,6 +243,11 @@ class VaultAddEditViewModel @Inject constructor(
) )
sendAction(VaultAddEditAction.Internal.UpdateCipherResultReceive(result)) sendAction(VaultAddEditAction.Internal.UpdateCipherResultReceive(result))
} }
is VaultAddEditType.CloneItem -> {
val result = vaultRepository.createCipher(cipherView = content.toCipherView())
sendAction(VaultAddEditAction.Internal.CreateCipherResultReceive(result))
}
} }
} }
} }
@ -810,6 +818,7 @@ class VaultAddEditViewModel @Inject constructor(
} }
} }
@Suppress("LongMethod")
private fun handleVaultDataReceive(action: VaultAddEditAction.Internal.VaultDataReceive) { private fun handleVaultDataReceive(action: VaultAddEditAction.Internal.VaultDataReceive) {
when (val vaultDataState = action.vaultDataState) { when (val vaultDataState = action.vaultDataState) {
is DataState.Error -> { is DataState.Error -> {
@ -827,7 +836,10 @@ class VaultAddEditViewModel @Inject constructor(
it.copy( it.copy(
viewState = vaultDataState viewState = vaultDataState
.data .data
?.toViewState() ?.toViewState(
isClone = it.isCloneMode,
resourceManager = resourceManager,
)
?: VaultAddEditState.ViewState.Error( ?: VaultAddEditState.ViewState.Error(
message = R.string.generic_error_message.asText(), message = R.string.generic_error_message.asText(),
), ),
@ -858,7 +870,10 @@ class VaultAddEditViewModel @Inject constructor(
it.copy( it.copy(
viewState = vaultDataState viewState = vaultDataState
.data .data
?.toViewState() ?.toViewState(
isClone = it.isCloneMode,
resourceManager = resourceManager,
)
?: VaultAddEditState.ViewState.Error( ?: VaultAddEditState.ViewState.Error(
message = R.string.generic_error_message.asText(), message = R.string.generic_error_message.asText(),
), ),
@ -1041,6 +1056,7 @@ data class VaultAddEditState(
get() = when (vaultAddEditType) { get() = when (vaultAddEditType) {
VaultAddEditType.AddItem -> R.string.add_item.asText() VaultAddEditType.AddItem -> R.string.add_item.asText()
is VaultAddEditType.EditItem -> R.string.edit_item.asText() is VaultAddEditType.EditItem -> R.string.edit_item.asText()
is VaultAddEditType.CloneItem -> R.string.add_item.asText()
} }
/** /**
@ -1048,6 +1064,11 @@ data class VaultAddEditState(
*/ */
val isAddItemMode: Boolean get() = vaultAddEditType == VaultAddEditType.AddItem val isAddItemMode: Boolean get() = vaultAddEditType == VaultAddEditType.AddItem
/**
* Helper to determine if the UI should display the content in clone mode.
*/
val isCloneMode: Boolean get() = vaultAddEditType is VaultAddEditType.CloneItem
/** /**
* Enum representing the main type options for the vault, such as LOGIN, CARD, etc. * Enum representing the main type options for the vault, such as LOGIN, CARD, etc.
* *

View file

@ -7,6 +7,7 @@ import com.bitwarden.core.FieldType
import com.bitwarden.core.FieldView import com.bitwarden.core.FieldView
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.asText 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.VaultAddEditState
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
@ -18,7 +19,10 @@ import java.util.UUID
/** /**
* Transforms [CipherView] into [VaultAddEditState.ViewState]. * Transforms [CipherView] into [VaultAddEditState.ViewState].
*/ */
fun CipherView.toViewState(): VaultAddEditState.ViewState = fun CipherView.toViewState(
isClone: Boolean,
resourceManager: ResourceManager,
): VaultAddEditState.ViewState =
VaultAddEditState.ViewState.Content( VaultAddEditState.ViewState.Content(
type = when (type) { type = when (type) {
CipherType.LOGIN -> { CipherType.LOGIN -> {
@ -63,7 +67,10 @@ fun CipherView.toViewState(): VaultAddEditState.ViewState =
}, },
common = VaultAddEditState.ViewState.Content.Common( common = VaultAddEditState.ViewState.Content.Common(
originalCipher = this, originalCipher = this,
name = this.name, name = name.appendCloneTextIfRequired(
isClone = isClone,
resourceManager = resourceManager,
),
favorite = this.favorite, favorite = this.favorite,
masterPasswordReprompt = this.reprompt == CipherRepromptType.PASSWORD, masterPasswordReprompt = this.reprompt == CipherRepromptType.PASSWORD,
notes = this.notes.orEmpty(), notes = this.notes.orEmpty(),
@ -121,3 +128,13 @@ private fun String?.toExpirationMonthOrDefault(): VaultCardExpirationMonth =
.entries .entries
.find { it.number == this } .find { it.number == this }
?: VaultCardExpirationMonth.SELECT ?: VaultCardExpirationMonth.SELECT
private fun String.appendCloneTextIfRequired(
isClone: Boolean,
resourceManager: ResourceManager,
): String =
if (isClone) {
plus(" - ${resourceManager.getString(R.string.clone)}")
} else {
this
}

View file

@ -28,7 +28,7 @@ data class VaultItemArgs(val vaultItemId: String) {
*/ */
fun NavGraphBuilder.vaultItemDestination( fun NavGraphBuilder.vaultItemDestination(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToVaultEditItem: (vaultItemId: String) -> Unit, onNavigateToVaultEditItem: (vaultItemId: String, isClone: Boolean) -> Unit,
onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit, onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit,
) { ) {
composableWithSlideTransitions( composableWithSlideTransitions(

View file

@ -59,7 +59,7 @@ fun VaultItemScreen(
viewModel: VaultItemViewModel = hiltViewModel(), viewModel: VaultItemViewModel = hiltViewModel(),
intentManager: IntentManager = LocalIntentManager.current, intentManager: IntentManager = LocalIntentManager.current,
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToVaultAddEditItem: (vaultItemId: String) -> Unit, onNavigateToVaultAddEditItem: (vaultItemId: String, isClone: Boolean) -> Unit,
onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit, onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit,
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@ -75,8 +75,7 @@ fun VaultItemScreen(
VaultItemEvent.NavigateBack -> onNavigateBack() VaultItemEvent.NavigateBack -> onNavigateBack()
is VaultItemEvent.NavigateToAddEdit -> { is VaultItemEvent.NavigateToAddEdit -> {
// TODO Implement cloning in BIT-526 onNavigateToVaultAddEditItem(event.itemId, event.isClone)
onNavigateToVaultAddEditItem(event.itemId)
} }
is VaultItemEvent.NavigateToPasswordHistory -> { is VaultItemEvent.NavigateToPasswordHistory -> {

View file

@ -7,19 +7,34 @@ import kotlinx.parcelize.Parcelize
* Represents the difference between create a completely new cipher and editing an existing one. * Represents the difference between create a completely new cipher and editing an existing one.
*/ */
sealed class VaultAddEditType : Parcelable { sealed class VaultAddEditType : Parcelable {
/**
* The ID of the vault item (nullable).
*/
abstract val vaultItemId: String?
/** /**
* Indicates that we want to create a completely new vault item. * Indicates that we want to create a completely new vault item.
*/ */
@Parcelize @Parcelize
data object AddItem : VaultAddEditType() data object AddItem : VaultAddEditType() {
override val vaultItemId: String?
get() = null
}
/** /**
* Indicates that we want to edit an existing item. * Indicates that we want to edit an existing item.
*
* @param vaultItemId The ID of the vault item to edit.
*/ */
@Parcelize @Parcelize
data class EditItem( data class EditItem(
val vaultItemId: String, override val vaultItemId: String,
) : VaultAddEditType()
/**
* Indicates that we want to clone an existing item.
*/
@Parcelize
data class CloneItem(
override val vaultItemId: String,
) : VaultAddEditType() ) : VaultAddEditType()
} }

View file

@ -16,6 +16,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest 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.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText 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 import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField
@ -58,7 +59,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
private val totpTestCodeFlow: MutableSharedFlow<TotpCodeResult> = bufferedMutableSharedFlow() private val totpTestCodeFlow: MutableSharedFlow<TotpCodeResult> = bufferedMutableSharedFlow()
private val mutableVaultItemFlow = MutableStateFlow<DataState<CipherView?>>(DataState.Loading) private val mutableVaultItemFlow = MutableStateFlow<DataState<CipherView?>>(DataState.Loading)
private val resourceManager: ResourceManager = mockk()
private val clipboardManager: BitwardenClipboardManager = mockk() private val clipboardManager: BitwardenClipboardManager = mockk()
private val vaultRepository: VaultRepository = mockk { private val vaultRepository: VaultRepository = mockk {
every { getVaultItemStateFlow(DEFAULT_EDIT_ITEM_ID) } returns mutableVaultItemFlow every { getVaultItemStateFlow(DEFAULT_EDIT_ITEM_ID) } returns mutableVaultItemFlow
@ -131,6 +132,25 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
} }
} }
@Test
fun `initial clone state should be correct`() = runTest {
val vaultAddEditType = VaultAddEditType.CloneItem(DEFAULT_EDIT_ITEM_ID)
val initState = createVaultAddItemState(vaultAddEditType = vaultAddEditType)
val viewModel = createAddVaultItemViewModel(
savedStateHandle = createSavedStateHandleWithState(
state = initState,
vaultAddEditType = vaultAddEditType,
),
)
assertEquals(
initState.copy(viewState = VaultAddEditState.ViewState.Loading),
viewModel.stateFlow.value,
)
verify(exactly = 1) {
vaultRepository.getVaultItemStateFlow(DEFAULT_EDIT_ITEM_ID)
}
}
@Test @Test
fun `CloseClick should emit NavigateBack`() = runTest { fun `CloseClick should emit NavigateBack`() = runTest {
val viewModel = createAddVaultItemViewModel() val viewModel = createAddVaultItemViewModel()
@ -254,7 +274,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
name = "mockName", name = "mockName",
), ),
) )
every { cipherView.toViewState() } returns stateWithName.viewState every {
cipherView.toViewState(
isClone = false,
resourceManager = resourceManager,
)
} returns stateWithName.viewState
mutableVaultItemFlow.value = DataState.Loaded(cipherView) mutableVaultItemFlow.value = DataState.Loaded(cipherView)
val viewModel = createAddVaultItemViewModel( val viewModel = createAddVaultItemViewModel(
@ -276,7 +301,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
} }
coVerify(exactly = 1) { coVerify(exactly = 1) {
cipherView.toViewState() cipherView.toViewState(
isClone = false,
resourceManager = resourceManager,
)
vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any()) vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any())
} }
} }
@ -294,7 +322,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
), ),
) )
every { cipherView.toViewState() } returns stateWithName.viewState every {
cipherView.toViewState(
isClone = false,
resourceManager = resourceManager,
)
} returns stateWithName.viewState
coEvery { coEvery {
vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any()) vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any())
} returns UpdateCipherResult.Error(errorMessage = null) } returns UpdateCipherResult.Error(errorMessage = null)
@ -336,7 +369,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
) )
val errorMessage = "You do not have permission to edit this." val errorMessage = "You do not have permission to edit this."
every { cipherView.toViewState() } returns stateWithName.viewState every {
cipherView.toViewState(
isClone = false,
resourceManager = resourceManager,
)
} returns stateWithName.viewState
coEvery { coEvery {
vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any()) vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any())
} returns UpdateCipherResult.Error(errorMessage = errorMessage) } returns UpdateCipherResult.Error(errorMessage = errorMessage)
@ -1192,6 +1230,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
clipboardManager = clipboardManager, clipboardManager = clipboardManager,
vaultRepository = vaultRepository, vaultRepository = vaultRepository,
generatorRepository = generatorRepository, generatorRepository = generatorRepository,
resourceManager = resourceManager,
) )
} }
@ -1484,6 +1523,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
when (vaultAddEditType) { when (vaultAddEditType) {
VaultAddEditType.AddItem -> "add" VaultAddEditType.AddItem -> "add"
is VaultAddEditType.EditItem -> "edit" is VaultAddEditType.EditItem -> "edit"
is VaultAddEditType.CloneItem -> "clone"
}, },
) )
set("vault_edit_id", (vaultAddEditType as? VaultAddEditType.EditItem)?.vaultItemId) set("vault_edit_id", (vaultAddEditType as? VaultAddEditType.EditItem)?.vaultItemId)
@ -1494,12 +1534,14 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
bitwardenClipboardManager: BitwardenClipboardManager = clipboardManager, bitwardenClipboardManager: BitwardenClipboardManager = clipboardManager,
vaultRepo: VaultRepository = vaultRepository, vaultRepo: VaultRepository = vaultRepository,
generatorRepo: GeneratorRepository = generatorRepository, generatorRepo: GeneratorRepository = generatorRepository,
bitwardenResourceManager: ResourceManager = resourceManager,
): VaultAddEditViewModel = ): VaultAddEditViewModel =
VaultAddEditViewModel( VaultAddEditViewModel(
savedStateHandle = savedStateHandle, savedStateHandle = savedStateHandle,
clipboardManager = bitwardenClipboardManager, clipboardManager = bitwardenClipboardManager,
vaultRepository = vaultRepo, vaultRepository = vaultRepo,
generatorRepository = generatorRepo, generatorRepository = generatorRepo,
resourceManager = bitwardenResourceManager,
) )
/** /**

View file

@ -14,10 +14,12 @@ import com.bitwarden.core.SecureNoteType
import com.bitwarden.core.SecureNoteView import com.bitwarden.core.SecureNoteView
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.asText 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.VaultAddEditState
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
import io.mockk.every import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.unmockkStatic import io.mockk.unmockkStatic
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
@ -29,6 +31,10 @@ import java.util.UUID
class CipherViewExtensionsTest { class CipherViewExtensionsTest {
private val resourceManager: ResourceManager = mockk {
every { getString(R.string.clone) } returns "Clone"
}
@BeforeEach @BeforeEach
fun setup() { fun setup() {
mockkStatic(UUID::randomUUID) mockkStatic(UUID::randomUUID)
@ -44,7 +50,10 @@ class CipherViewExtensionsTest {
fun `toViewState should create a Card ViewState`() { fun `toViewState should create a Card ViewState`() {
val cipherView = DEFAULT_CARD_CIPHER_VIEW val cipherView = DEFAULT_CARD_CIPHER_VIEW
val result = cipherView.toViewState() val result = cipherView.toViewState(
isClone = false,
resourceManager = resourceManager,
)
assertEquals( assertEquals(
VaultAddEditState.ViewState.Content( VaultAddEditState.ViewState.Content(
@ -85,7 +94,10 @@ class CipherViewExtensionsTest {
fun `toViewState should create a Identity ViewState`() { fun `toViewState should create a Identity ViewState`() {
val cipherView = DEFAULT_IDENTITY_CIPHER_VIEW val cipherView = DEFAULT_IDENTITY_CIPHER_VIEW
val result = cipherView.toViewState() val result = cipherView.toViewState(
isClone = false,
resourceManager = resourceManager,
)
assertEquals( assertEquals(
VaultAddEditState.ViewState.Content( VaultAddEditState.ViewState.Content(
@ -131,7 +143,10 @@ class CipherViewExtensionsTest {
fun `toViewState should create a Login ViewState`() { fun `toViewState should create a Login ViewState`() {
val cipherView = DEFAULT_LOGIN_CIPHER_VIEW val cipherView = DEFAULT_LOGIN_CIPHER_VIEW
val result = cipherView.toViewState() val result = cipherView.toViewState(
isClone = false,
resourceManager = resourceManager,
)
assertEquals( assertEquals(
VaultAddEditState.ViewState.Content( VaultAddEditState.ViewState.Content(
@ -172,7 +187,10 @@ class CipherViewExtensionsTest {
fun `toViewState should create a Secure Notes ViewState`() { fun `toViewState should create a Secure Notes ViewState`() {
val cipherView = DEFAULT_SECURE_NOTES_CIPHER_VIEW val cipherView = DEFAULT_SECURE_NOTES_CIPHER_VIEW
val result = cipherView.toViewState() val result = cipherView.toViewState(
isClone = false,
resourceManager = resourceManager,
)
assertEquals( assertEquals(
VaultAddEditState.ViewState.Content( VaultAddEditState.ViewState.Content(
@ -197,6 +215,39 @@ class CipherViewExtensionsTest {
result, result,
) )
} }
@Test
fun `toViewState with isClone true should append clone text to the cipher name`() {
val cipherView = DEFAULT_SECURE_NOTES_CIPHER_VIEW
val result = cipherView.toViewState(
isClone = true,
resourceManager = resourceManager,
)
assertEquals(
VaultAddEditState.ViewState.Content(
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"),
VaultAddEditState.Custom.HiddenField(TEST_ID, "TestHidden", "TestHidden"),
),
availableFolders = emptyList(),
availableOwners = emptyList(),
),
type = VaultAddEditState.ViewState.Content.ItemType.SecureNotes,
),
result,
)
}
} }
private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView( private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView(

View file

@ -70,7 +70,7 @@ class VaultItemScreenTest : BaseComposeTest() {
VaultItemScreen( VaultItemScreen(
viewModel = viewModel, viewModel = viewModel,
onNavigateBack = { onNavigateBackCalled = true }, onNavigateBack = { onNavigateBackCalled = true },
onNavigateToVaultAddEditItem = { onNavigateToVaultEditItemId = it }, onNavigateToVaultAddEditItem = { id, _ -> onNavigateToVaultEditItemId = id },
onNavigateToMoveToOrganization = { onNavigateToMoveToOrganizationItemId = it }, onNavigateToMoveToOrganization = { onNavigateToMoveToOrganizationItemId = it },
intentManager = intentManager, intentManager = intentManager,
) )