mirror of
https://github.com/bitwarden/android.git
synced 2025-02-16 20:09:59 +03:00
BIT-502: Save the updated ciphers from the edit screen (#371)
This commit is contained in:
parent
65b9005cbe
commit
f4db50b700
8 changed files with 843 additions and 98 deletions
|
@ -1,6 +1,8 @@
|
|||
package com.x8bit.bitwarden.data.platform.repository.util
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.transformWhile
|
||||
|
||||
/**
|
||||
* Maps the data inside a [DataState] with the given [transform].
|
||||
|
@ -14,3 +16,11 @@ inline fun <T : Any?, R : Any?> DataState<T>.map(
|
|||
is DataState.Error -> DataState.Error(error, data?.let(transform))
|
||||
is DataState.NoNetwork -> DataState.NoNetwork(data?.let(transform))
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits all values of a [DataState] [Flow] until it emits a [DataState.Loaded].
|
||||
*/
|
||||
fun <T : Any?> Flow<DataState<T>>.takeUntilLoaded(): Flow<DataState<T>> = transformWhile {
|
||||
emit(it)
|
||||
it !is DataState.Loaded
|
||||
}
|
||||
|
|
|
@ -4,19 +4,27 @@ import android.os.Parcelable
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.takeUntilLoaded
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
|
||||
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
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||
import com.x8bit.bitwarden.ui.vault.feature.additem.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toCipherView
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -55,6 +63,19 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
|
||||
init {
|
||||
stateFlow.onEach { savedStateHandle[KEY_STATE] = it }.launchIn(viewModelScope)
|
||||
|
||||
when (val vaultAddEditType = state.vaultAddEditType) {
|
||||
VaultAddEditType.AddItem -> Unit
|
||||
is VaultAddEditType.EditItem -> {
|
||||
vaultRepository
|
||||
.getVaultItemStateFlow(vaultAddEditType.vaultItemId)
|
||||
// We'll stop getting updates as soon as we get some loaded data.
|
||||
.takeUntilLoaded()
|
||||
.map { VaultAddItemAction.Internal.VaultDataReceive(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleAction(action: VaultAddItemAction) {
|
||||
|
@ -83,9 +104,21 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
handleAddSecureNoteTypeAction(action)
|
||||
}
|
||||
|
||||
is VaultAddItemAction.Internal -> handleInternalActions(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInternalActions(action: VaultAddItemAction.Internal) {
|
||||
when (action) {
|
||||
is VaultAddItemAction.Internal.CreateCipherResultReceive -> {
|
||||
handleCreateCipherResultReceive(action)
|
||||
}
|
||||
|
||||
is VaultAddItemAction.Internal.UpdateCipherResultReceive -> {
|
||||
handleUpdateCipherResultReceive(action)
|
||||
}
|
||||
|
||||
is VaultAddItemAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,13 +148,20 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
sendAction(
|
||||
action = VaultAddItemAction.Internal.CreateCipherResultReceive(
|
||||
createCipherResult = vaultRepository.createCipher(
|
||||
when (val vaultAddEditType = state.vaultAddEditType) {
|
||||
VaultAddEditType.AddItem -> {
|
||||
val result = vaultRepository.createCipher(cipherView = content.toCipherView())
|
||||
sendAction(VaultAddItemAction.Internal.CreateCipherResultReceive(result))
|
||||
}
|
||||
|
||||
is VaultAddEditType.EditItem -> {
|
||||
val result = vaultRepository.updateCipher(
|
||||
cipherId = vaultAddEditType.vaultItemId,
|
||||
cipherView = content.toCipherView(),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
sendAction(VaultAddItemAction.Internal.UpdateCipherResultReceive(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -533,6 +573,80 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleUpdateCipherResultReceive(
|
||||
action: VaultAddItemAction.Internal.UpdateCipherResultReceive,
|
||||
) {
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
when (action.updateCipherResult) {
|
||||
is UpdateCipherResult.Error -> {
|
||||
// TODO Display error dialog BIT-501
|
||||
sendEvent(VaultAddItemEvent.ShowToast(message = "Save Item Failure"))
|
||||
}
|
||||
|
||||
is UpdateCipherResult.Success -> {
|
||||
sendEvent(VaultAddItemEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVaultDataReceive(action: VaultAddItemAction.Internal.VaultDataReceive) {
|
||||
when (val vaultDataState = action.vaultDataState) {
|
||||
is DataState.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultAddItemState.ViewState.Error(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is DataState.Loaded -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = vaultDataState
|
||||
.data
|
||||
?.toViewState()
|
||||
?: VaultAddItemState.ViewState.Error(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DataState.Loading -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = VaultAddItemState.ViewState.Loading)
|
||||
}
|
||||
}
|
||||
|
||||
is DataState.NoNetwork -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultAddItemState.ViewState.Error(
|
||||
message = R.string.internet_connection_required_title
|
||||
.asText()
|
||||
.concat(R.string.internet_connection_required_message.asText()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is DataState.Pending -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = vaultDataState
|
||||
.data
|
||||
?.toViewState()
|
||||
?: VaultAddItemState.ViewState.Error(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Internal Type Handlers
|
||||
|
||||
//region Utility Functions
|
||||
|
@ -638,6 +752,14 @@ data class VaultAddItemState(
|
|||
*/
|
||||
@Parcelize
|
||||
sealed class Content : ViewState() {
|
||||
/**
|
||||
* The original cipher from the vault that the user is editing.
|
||||
*
|
||||
* This is only present when editing a pre-existing cipher.
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
abstract val originalCipher: CipherView?
|
||||
|
||||
/**
|
||||
* Represents the resource ID for the display string. This is an abstract property
|
||||
* that must be overridden by each subclass to provide the appropriate string resource
|
||||
|
@ -680,6 +802,8 @@ data class VaultAddItemState(
|
|||
*/
|
||||
@Parcelize
|
||||
data class Login(
|
||||
@IgnoredOnParcel
|
||||
override val originalCipher: CipherView? = null,
|
||||
override val name: String = "",
|
||||
val username: String = "",
|
||||
val password: String = "",
|
||||
|
@ -706,6 +830,8 @@ data class VaultAddItemState(
|
|||
*/
|
||||
@Parcelize
|
||||
data class Card(
|
||||
@IgnoredOnParcel
|
||||
override val originalCipher: CipherView? = null,
|
||||
override val name: String = "",
|
||||
override val masterPasswordReprompt: Boolean = false,
|
||||
override val ownership: String = DEFAULT_OWNERSHIP,
|
||||
|
@ -719,6 +845,8 @@ data class VaultAddItemState(
|
|||
*/
|
||||
@Parcelize
|
||||
data class Identity(
|
||||
@IgnoredOnParcel
|
||||
override val originalCipher: CipherView? = null,
|
||||
override val name: String = "",
|
||||
override val masterPasswordReprompt: Boolean = false,
|
||||
override val ownership: String = DEFAULT_OWNERSHIP,
|
||||
|
@ -737,6 +865,8 @@ data class VaultAddItemState(
|
|||
*/
|
||||
@Parcelize
|
||||
data class SecureNotes(
|
||||
@IgnoredOnParcel
|
||||
override val originalCipher: CipherView? = null,
|
||||
override val name: String = "",
|
||||
val folderName: Text = DEFAULT_FOLDER,
|
||||
val favorite: Boolean = false,
|
||||
|
@ -1006,6 +1136,12 @@ sealed class VaultAddItemAction {
|
|||
* Models actions that the [VaultAddItemViewModel] itself might send.
|
||||
*/
|
||||
sealed class Internal : VaultAddItemAction() {
|
||||
/**
|
||||
* Indicates that the vault item data has been received.
|
||||
*/
|
||||
data class VaultDataReceive(
|
||||
val vaultDataState: DataState<CipherView?>,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates a result for creating a cipher has been received.
|
||||
|
@ -1013,5 +1149,12 @@ sealed class VaultAddItemAction {
|
|||
data class CreateCipherResultReceive(
|
||||
val createCipherResult: CreateCipherResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates a result for updating a cipher has been received.
|
||||
*/
|
||||
data class UpdateCipherResultReceive(
|
||||
val updateCipherResult: UpdateCipherResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.additem.util
|
||||
|
||||
import com.bitwarden.core.CipherRepromptType
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.vault.feature.additem.VaultAddItemState
|
||||
|
||||
/**
|
||||
* Transforms [CipherView] into [VaultAddItemState.ViewState].
|
||||
*/
|
||||
fun CipherView.toViewState(): VaultAddItemState.ViewState =
|
||||
when (type) {
|
||||
CipherType.LOGIN -> {
|
||||
val loginView = requireNotNull(this.login)
|
||||
VaultAddItemState.ViewState.Content.Login(
|
||||
originalCipher = this,
|
||||
name = this.name,
|
||||
username = loginView.username.orEmpty(),
|
||||
password = loginView.password.orEmpty(),
|
||||
uri = loginView.uris?.firstOrNull()?.uri.orEmpty(),
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
||||
CipherType.SECURE_NOTE -> {
|
||||
VaultAddItemState.ViewState.Content.SecureNotes(
|
||||
originalCipher = this,
|
||||
name = this.name,
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
||||
CipherType.CARD -> VaultAddItemState.ViewState.Error(
|
||||
message = "Not yet implemented.".asText(),
|
||||
)
|
||||
|
||||
CipherType.IDENTITY -> VaultAddItemState.ViewState.Error(
|
||||
message = "Not yet implemented.".asText(),
|
||||
)
|
||||
}
|
|
@ -99,53 +99,53 @@ fun VaultAddItemState.ViewState.Content.toCipherView(): CipherView =
|
|||
*/
|
||||
private fun VaultAddItemState.ViewState.Content.Login.toLoginCipherView(): CipherView =
|
||||
CipherView(
|
||||
id = null,
|
||||
// TODO use real organization id BIT-780
|
||||
organizationId = null,
|
||||
// TODO use real folder id BIT-528
|
||||
folderId = null,
|
||||
collectionIds = emptyList(),
|
||||
key = null,
|
||||
name = name,
|
||||
notes = notes,
|
||||
// Pulled from original cipher when editing, otherwise uses defaults
|
||||
id = this.originalCipher?.id,
|
||||
collectionIds = this.originalCipher?.collectionIds.orEmpty(),
|
||||
key = this.originalCipher?.key,
|
||||
edit = this.originalCipher?.edit ?: true,
|
||||
viewPassword = this.originalCipher?.viewPassword ?: true,
|
||||
localData = this.originalCipher?.localData,
|
||||
attachments = this.originalCipher?.attachments,
|
||||
organizationUseTotp = this.originalCipher?.organizationUseTotp ?: false,
|
||||
passwordHistory = this.originalCipher?.passwordHistory,
|
||||
creationDate = this.originalCipher?.creationDate ?: Instant.now(),
|
||||
deletedDate = this.originalCipher?.deletedDate,
|
||||
revisionDate = this.originalCipher?.revisionDate ?: Instant.now(),
|
||||
|
||||
// Type specific section
|
||||
type = CipherType.LOGIN,
|
||||
login = LoginView(
|
||||
username = username,
|
||||
password = password,
|
||||
passwordRevisionDate = null,
|
||||
username = this.username,
|
||||
password = this.password,
|
||||
passwordRevisionDate = this.originalCipher?.login?.passwordRevisionDate,
|
||||
uris = listOf(
|
||||
// TODO Implement URI list (BIT-1094)
|
||||
LoginUriView(
|
||||
uri = uri,
|
||||
// TODO implement uri settings in BIT-1094
|
||||
uri = this.uri,
|
||||
// TODO Implement URI settings in (BIT-1094)
|
||||
match = UriMatchType.DOMAIN,
|
||||
),
|
||||
),
|
||||
// TODO implement totp in BIT-1066
|
||||
totp = null,
|
||||
autofillOnPageLoad = false,
|
||||
totp = this.originalCipher?.login?.totp,
|
||||
autofillOnPageLoad = this.originalCipher?.login?.autofillOnPageLoad,
|
||||
),
|
||||
identity = null,
|
||||
card = null,
|
||||
secureNote = null,
|
||||
favorite = favorite,
|
||||
reprompt = if (masterPasswordReprompt) {
|
||||
CipherRepromptType.PASSWORD
|
||||
} else {
|
||||
CipherRepromptType.NONE
|
||||
},
|
||||
organizationUseTotp = false,
|
||||
edit = true,
|
||||
viewPassword = true,
|
||||
localData = null,
|
||||
attachments = null,
|
||||
// TODO implement custom fields BIT-529
|
||||
|
||||
// Fields we always grab from the UI
|
||||
name = this.name,
|
||||
notes = this.notes,
|
||||
favorite = this.favorite,
|
||||
// TODO Use real folder ID (BIT-528)
|
||||
folderId = this.originalCipher?.folderId,
|
||||
// TODO Use real organization ID (BIT-780)
|
||||
organizationId = this.originalCipher?.organizationId,
|
||||
reprompt = this.toCipherRepromptType(),
|
||||
// TODO Implement custom fields (BIT-529)
|
||||
fields = null,
|
||||
passwordHistory = null,
|
||||
creationDate = Instant.now(),
|
||||
deletedDate = null,
|
||||
// This is a throw away value.
|
||||
// The SDK will eventually remove revisionDate via encryption.
|
||||
revisionDate = Instant.now(),
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -153,39 +153,38 @@ private fun VaultAddItemState.ViewState.Content.Login.toLoginCipherView(): Ciphe
|
|||
*/
|
||||
private fun VaultAddItemState.ViewState.Content.SecureNotes.toSecureNotesCipherView(): CipherView =
|
||||
CipherView(
|
||||
id = null,
|
||||
// TODO use real organization id BIT-780
|
||||
organizationId = null,
|
||||
// TODO use real folder id BIT-528
|
||||
folderId = null,
|
||||
collectionIds = emptyList(),
|
||||
key = null,
|
||||
name = name,
|
||||
notes = notes,
|
||||
// Pulled from original cipher when editing, otherwise uses defaults
|
||||
id = this.originalCipher?.id,
|
||||
collectionIds = this.originalCipher?.collectionIds.orEmpty(),
|
||||
key = this.originalCipher?.key,
|
||||
edit = this.originalCipher?.edit ?: true,
|
||||
viewPassword = this.originalCipher?.viewPassword ?: true,
|
||||
localData = this.originalCipher?.localData,
|
||||
attachments = this.originalCipher?.attachments,
|
||||
organizationUseTotp = this.originalCipher?.organizationUseTotp ?: false,
|
||||
passwordHistory = this.originalCipher?.passwordHistory,
|
||||
creationDate = this.originalCipher?.creationDate ?: Instant.now(),
|
||||
deletedDate = this.originalCipher?.deletedDate,
|
||||
revisionDate = this.originalCipher?.revisionDate ?: Instant.now(),
|
||||
|
||||
// Type specific section
|
||||
type = CipherType.SECURE_NOTE,
|
||||
secureNote = SecureNoteView(SecureNoteType.GENERIC),
|
||||
secureNote = SecureNoteView(type = SecureNoteType.GENERIC),
|
||||
login = null,
|
||||
identity = null,
|
||||
card = null,
|
||||
favorite = favorite,
|
||||
reprompt = if (masterPasswordReprompt) {
|
||||
CipherRepromptType.PASSWORD
|
||||
} else {
|
||||
CipherRepromptType.NONE
|
||||
},
|
||||
organizationUseTotp = false,
|
||||
edit = true,
|
||||
viewPassword = true,
|
||||
localData = null,
|
||||
attachments = null,
|
||||
// TODO implement custom fields BIT-529
|
||||
|
||||
// Fields we always grab from the UI
|
||||
name = this.name,
|
||||
notes = this.notes,
|
||||
favorite = this.favorite,
|
||||
// TODO Use real folder ID (BIT-528)
|
||||
folderId = this.originalCipher?.folderId,
|
||||
// TODO Use real organization ID (BIT-780)
|
||||
organizationId = this.originalCipher?.organizationId,
|
||||
reprompt = this.toCipherRepromptType(),
|
||||
// TODO Implement custom fields (BIT-529)
|
||||
fields = null,
|
||||
passwordHistory = null,
|
||||
creationDate = Instant.now(),
|
||||
deletedDate = null,
|
||||
// This is a throw away value.
|
||||
// The SDK will eventually remove revisionDate via encryption.
|
||||
revisionDate = Instant.now(),
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -199,3 +198,10 @@ private fun VaultAddItemState.ViewState.Content.Identity.toIdentityCipherView():
|
|||
*/
|
||||
private fun VaultAddItemState.ViewState.Content.Card.toCardCipherView(): CipherView =
|
||||
TODO("create Card CipherView BIT-668")
|
||||
|
||||
private fun VaultAddItemState.ViewState.Content.toCipherRepromptType(): CipherRepromptType =
|
||||
if (this.masterPasswordReprompt) {
|
||||
CipherRepromptType.PASSWORD
|
||||
} else {
|
||||
CipherRepromptType.NONE
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package com.x8bit.bitwarden.data.platform.repository.util
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class DataStateExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `takeUtilLoaded should complete after a Loaded state is emitted`() = runTest {
|
||||
val mutableStateFlow = MutableStateFlow<DataState<Unit>>(DataState.Loading)
|
||||
mutableStateFlow
|
||||
.takeUntilLoaded()
|
||||
.test {
|
||||
assertEquals(DataState.Loading, awaitItem())
|
||||
mutableStateFlow.value = DataState.NoNetwork(Unit)
|
||||
assertEquals(DataState.NoNetwork(Unit), awaitItem())
|
||||
mutableStateFlow.value = DataState.Loaded(Unit)
|
||||
assertEquals(DataState.Loaded(Unit), awaitItem())
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,16 +2,27 @@ package com.x8bit.bitwarden.ui.vault.feature.additem
|
|||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
|
||||
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.vault.feature.additem.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
|
@ -24,7 +35,20 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
state = initialState,
|
||||
vaultAddEditType = VaultAddEditType.AddItem,
|
||||
)
|
||||
private val vaultRepository: VaultRepository = mockk()
|
||||
private val mutableVaultItemFlow = MutableStateFlow<DataState<CipherView?>>(DataState.Loading)
|
||||
private val vaultRepository: VaultRepository = mockk {
|
||||
every { getVaultItemStateFlow(DEFAULT_EDIT_ITEM_ID) } returns mutableVaultItemFlow
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(CIPHER_VIEW_EXTENSIONS_PATH)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(CIPHER_VIEW_EXTENSIONS_PATH)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when state is null`() = runTest {
|
||||
|
@ -50,6 +74,9 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
),
|
||||
)
|
||||
assertEquals(initState, viewModel.stateFlow.value)
|
||||
verify(exactly = 0) {
|
||||
vaultRepository.getVaultItemStateFlow(DEFAULT_EDIT_ITEM_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -62,7 +89,13 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
vaultAddEditType = vaultAddEditType,
|
||||
),
|
||||
)
|
||||
assertEquals(initState, viewModel.stateFlow.value)
|
||||
assertEquals(
|
||||
initState.copy(viewState = VaultAddItemState.ViewState.Loading),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify(exactly = 1) {
|
||||
vaultRepository.getVaultItemStateFlow(DEFAULT_EDIT_ITEM_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -75,38 +108,44 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `SaveClick should show dialog, and remove it once an item is saved`() = runTest {
|
||||
val stateWithDialog = createVaultAddLoginItemState(
|
||||
name = "tester",
|
||||
dialogState = VaultAddItemState.DialogState.Loading(
|
||||
R.string.saving.asText(),
|
||||
),
|
||||
)
|
||||
fun `in add mode, SaveClick should show dialog, and remove it once an item is saved`() =
|
||||
runTest {
|
||||
val stateWithDialog = createVaultAddLoginItemState(
|
||||
name = "tester",
|
||||
dialogState = VaultAddItemState.DialogState.Loading(
|
||||
R.string.saving.asText(),
|
||||
),
|
||||
)
|
||||
|
||||
val stateWithName = createVaultAddLoginItemState(
|
||||
name = "tester",
|
||||
)
|
||||
val stateWithName = createVaultAddLoginItemState(
|
||||
name = "tester",
|
||||
)
|
||||
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
createSavedStateHandleWithState(
|
||||
state = stateWithName,
|
||||
vaultAddEditType = VaultAddEditType.AddItem,
|
||||
),
|
||||
)
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
createSavedStateHandleWithState(
|
||||
state = stateWithName,
|
||||
vaultAddEditType = VaultAddEditType.AddItem,
|
||||
),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
vaultRepository.createCipher(any())
|
||||
} returns CreateCipherResult.Success
|
||||
viewModel.stateFlow.test {
|
||||
viewModel.actionChannel.trySend(VaultAddItemAction.SaveClick)
|
||||
assertEquals(stateWithName, awaitItem())
|
||||
assertEquals(stateWithDialog, awaitItem())
|
||||
assertEquals(stateWithName, awaitItem())
|
||||
coEvery {
|
||||
vaultRepository.createCipher(any())
|
||||
} returns CreateCipherResult.Success
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
viewModel.actionChannel.trySend(VaultAddItemAction.SaveClick)
|
||||
assertEquals(stateWithName, awaitItem())
|
||||
assertEquals(stateWithDialog, awaitItem())
|
||||
assertEquals(stateWithName, awaitItem())
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultRepository.createCipher(any())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SaveClick should update value to loading`() = runTest {
|
||||
fun `in add mode, SaveClick should update value to loading`() = runTest {
|
||||
val stateWithName = createVaultAddLoginItemState(
|
||||
name = "tester",
|
||||
)
|
||||
|
@ -128,7 +167,7 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `SaveClick createCipher error should emit ShowToast`() = runTest {
|
||||
fun `in add mode, SaveClick createCipher error should emit ShowToast`() = runTest {
|
||||
val stateWithName = createVaultAddLoginItemState(
|
||||
name = "tester",
|
||||
)
|
||||
|
@ -149,6 +188,82 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in edit mode, SaveClick should show dialog, and remove it once an item is saved`() =
|
||||
runTest {
|
||||
val cipherView = mockk<CipherView>()
|
||||
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
|
||||
val stateWithDialog = createVaultAddLoginItemState(
|
||||
vaultAddEditType = vaultAddEditType,
|
||||
name = "tester",
|
||||
dialogState = VaultAddItemState.DialogState.Loading(
|
||||
R.string.saving.asText(),
|
||||
),
|
||||
)
|
||||
|
||||
val stateWithName = createVaultAddLoginItemState(
|
||||
vaultAddEditType = vaultAddEditType,
|
||||
name = "tester",
|
||||
)
|
||||
every { cipherView.toViewState() } returns stateWithName.viewState
|
||||
mutableVaultItemFlow.value = DataState.Loaded(cipherView)
|
||||
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
createSavedStateHandleWithState(
|
||||
state = stateWithName,
|
||||
vaultAddEditType = vaultAddEditType,
|
||||
),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any())
|
||||
} returns UpdateCipherResult.Success
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(stateWithName, awaitItem())
|
||||
viewModel.actionChannel.trySend(VaultAddItemAction.SaveClick)
|
||||
assertEquals(stateWithDialog, awaitItem())
|
||||
assertEquals(stateWithName, awaitItem())
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
cipherView.toViewState()
|
||||
vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in edit mode, SaveClick createCipher error should emit ShowToast`() = runTest {
|
||||
val cipherView = mockk<CipherView>()
|
||||
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
|
||||
val stateWithName = createVaultAddLoginItemState(
|
||||
vaultAddEditType = vaultAddEditType,
|
||||
name = "tester",
|
||||
)
|
||||
|
||||
every { cipherView.toViewState() } returns stateWithName.viewState
|
||||
coEvery {
|
||||
vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any())
|
||||
} returns UpdateCipherResult.Error
|
||||
mutableVaultItemFlow.value = DataState.Loaded(cipherView)
|
||||
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
createSavedStateHandleWithState(
|
||||
state = stateWithName,
|
||||
vaultAddEditType = vaultAddEditType,
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(VaultAddItemAction.SaveClick)
|
||||
assertEquals(VaultAddItemEvent.ShowToast("Save Item Failure"), awaitItem())
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Saving item with an empty name field will cause a dialog to show up`() = runTest {
|
||||
val stateWithNoName = createVaultAddSecureNotesItemState(name = "")
|
||||
|
@ -712,4 +827,7 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
private const val CIPHER_VIEW_EXTENSIONS_PATH: String =
|
||||
"com.x8bit.bitwarden.ui.vault.feature.additem.util.CipherViewExtensionsKt"
|
||||
|
||||
private const val DEFAULT_EDIT_ITEM_ID: String = "edit_item_id"
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.additem.util
|
||||
|
||||
import com.bitwarden.core.CardView
|
||||
import com.bitwarden.core.CipherRepromptType
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.bitwarden.core.FieldType
|
||||
import com.bitwarden.core.FieldView
|
||||
import com.bitwarden.core.IdentityView
|
||||
import com.bitwarden.core.LoginUriView
|
||||
import com.bitwarden.core.LoginView
|
||||
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.vault.feature.additem.VaultAddItemState
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Instant
|
||||
|
||||
class CipherViewExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `toViewState should create a Card ViewState`() {
|
||||
val cipherView = DEFAULT_CARD_CIPHER_VIEW
|
||||
|
||||
val result = cipherView.toViewState()
|
||||
|
||||
assertEquals(
|
||||
VaultAddItemState.ViewState.Error(message = "Not yet implemented.".asText()),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should create a Identity ViewState`() {
|
||||
val cipherView = DEFAULT_IDENTITY_CIPHER_VIEW
|
||||
|
||||
val result = cipherView.toViewState()
|
||||
|
||||
assertEquals(
|
||||
VaultAddItemState.ViewState.Error(message = "Not yet implemented.".asText()),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should create a Login ViewState`() {
|
||||
val cipherView = DEFAULT_LOGIN_CIPHER_VIEW
|
||||
|
||||
val result = cipherView.toViewState()
|
||||
|
||||
assertEquals(
|
||||
VaultAddItemState.ViewState.Content.Login(
|
||||
originalCipher = cipherView,
|
||||
name = "cipher",
|
||||
username = "username",
|
||||
password = "password",
|
||||
uri = "www.example.com",
|
||||
folderName = R.string.folder_none.asText(),
|
||||
favorite = false,
|
||||
masterPasswordReprompt = true,
|
||||
notes = "Lots of notes",
|
||||
ownership = "",
|
||||
availableFolders = emptyList(),
|
||||
availableOwners = emptyList(),
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should create a Secure Notes ViewState`() {
|
||||
val cipherView = DEFAULT_SECURE_NOTES_CIPHER_VIEW
|
||||
|
||||
val result = cipherView.toViewState()
|
||||
|
||||
assertEquals(
|
||||
VaultAddItemState.ViewState.Content.SecureNotes(
|
||||
originalCipher = cipherView,
|
||||
name = "cipher",
|
||||
folderName = R.string.folder_none.asText(),
|
||||
favorite = false,
|
||||
masterPasswordReprompt = true,
|
||||
notes = "Lots of notes",
|
||||
ownership = "",
|
||||
availableFolders = emptyList(),
|
||||
availableOwners = emptyList(),
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView(
|
||||
id = "id1234",
|
||||
organizationId = null,
|
||||
folderId = null,
|
||||
collectionIds = emptyList(),
|
||||
key = null,
|
||||
name = "cipher",
|
||||
notes = "Lots of notes",
|
||||
type = CipherType.LOGIN,
|
||||
login = null,
|
||||
identity = null,
|
||||
card = null,
|
||||
secureNote = null,
|
||||
favorite = false,
|
||||
reprompt = CipherRepromptType.PASSWORD,
|
||||
organizationUseTotp = false,
|
||||
edit = false,
|
||||
viewPassword = false,
|
||||
localData = null,
|
||||
attachments = null,
|
||||
fields = listOf(
|
||||
FieldView(
|
||||
name = "text",
|
||||
value = "value",
|
||||
type = FieldType.TEXT,
|
||||
linkedId = null,
|
||||
),
|
||||
FieldView(
|
||||
name = "hidden",
|
||||
value = "value",
|
||||
type = FieldType.HIDDEN,
|
||||
linkedId = null,
|
||||
),
|
||||
FieldView(
|
||||
name = "boolean",
|
||||
value = "true",
|
||||
type = FieldType.BOOLEAN,
|
||||
linkedId = null,
|
||||
),
|
||||
FieldView(
|
||||
name = "linked username",
|
||||
value = null,
|
||||
type = FieldType.LINKED,
|
||||
linkedId = 100U,
|
||||
),
|
||||
FieldView(
|
||||
name = "linked password",
|
||||
value = null,
|
||||
type = FieldType.LINKED,
|
||||
linkedId = 101U,
|
||||
),
|
||||
),
|
||||
passwordHistory = listOf(
|
||||
PasswordHistoryView(
|
||||
password = "old_password",
|
||||
lastUsedDate = Instant.ofEpochSecond(1_000L),
|
||||
),
|
||||
),
|
||||
creationDate = Instant.ofEpochSecond(1_000L),
|
||||
deletedDate = null,
|
||||
revisionDate = Instant.ofEpochSecond(1_000L),
|
||||
)
|
||||
|
||||
private val DEFAULT_CARD_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
|
||||
type = CipherType.CARD,
|
||||
card = CardView(
|
||||
cardholderName = "Bit Warden",
|
||||
expMonth = "04",
|
||||
expYear = "2030",
|
||||
code = "123",
|
||||
brand = "Visa",
|
||||
number = "4012888888881881",
|
||||
),
|
||||
)
|
||||
|
||||
private val DEFAULT_IDENTITY_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
|
||||
type = CipherType.IDENTITY,
|
||||
identity = IdentityView(
|
||||
title = "Dr.",
|
||||
firstName = "John",
|
||||
lastName = "Smith",
|
||||
middleName = "Richard",
|
||||
address1 = null,
|
||||
address2 = null,
|
||||
address3 = null,
|
||||
city = "Minneapolis",
|
||||
state = "MN",
|
||||
postalCode = null,
|
||||
country = "USA",
|
||||
company = "Bitwarden",
|
||||
email = "placeholde@email.com",
|
||||
phone = "555-555-5555",
|
||||
ssn = null,
|
||||
username = "Dr. JSR",
|
||||
passportNumber = null,
|
||||
licenseNumber = null,
|
||||
),
|
||||
)
|
||||
|
||||
private val DEFAULT_LOGIN_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
|
||||
type = CipherType.LOGIN,
|
||||
login = LoginView(
|
||||
username = "username",
|
||||
password = "password",
|
||||
passwordRevisionDate = Instant.ofEpochSecond(1_000L),
|
||||
uris = listOf(
|
||||
LoginUriView(
|
||||
uri = "www.example.com",
|
||||
match = null,
|
||||
),
|
||||
),
|
||||
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
|
||||
autofillOnPageLoad = false,
|
||||
),
|
||||
)
|
||||
|
||||
private val DEFAULT_SECURE_NOTES_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
|
||||
type = CipherType.SECURE_NOTE,
|
||||
secureNote = SecureNoteView(type = SecureNoteType.GENERIC),
|
||||
)
|
|
@ -3,8 +3,11 @@ package com.x8bit.bitwarden.ui.vault.feature.vault.util
|
|||
import com.bitwarden.core.CipherRepromptType
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.bitwarden.core.FieldType
|
||||
import com.bitwarden.core.FieldView
|
||||
import com.bitwarden.core.LoginUriView
|
||||
import com.bitwarden.core.LoginView
|
||||
import com.bitwarden.core.PasswordHistoryView
|
||||
import com.bitwarden.core.SecureNoteType
|
||||
import com.bitwarden.core.SecureNoteView
|
||||
import com.bitwarden.core.UriMatchType
|
||||
|
@ -144,7 +147,7 @@ class VaultDataExtensionsTest {
|
|||
),
|
||||
),
|
||||
totp = null,
|
||||
autofillOnPageLoad = false,
|
||||
autofillOnPageLoad = null,
|
||||
),
|
||||
identity = null,
|
||||
card = null,
|
||||
|
@ -166,6 +169,57 @@ class VaultDataExtensionsTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toCipherView should transform Login ItemType to CipherView with original cipher`() {
|
||||
val cipherView = DEFAULT_LOGIN_CIPHER_VIEW
|
||||
val loginItemType = VaultAddItemState.ViewState.Content.Login(
|
||||
originalCipher = cipherView,
|
||||
name = "mockName-1",
|
||||
username = "mockUsername-1",
|
||||
password = "mockPassword-1",
|
||||
uri = "mockUri-1",
|
||||
folderName = "mockFolder-1".asText(),
|
||||
favorite = true,
|
||||
masterPasswordReprompt = false,
|
||||
notes = "mockNotes-1",
|
||||
ownership = "mockOwnership-1",
|
||||
)
|
||||
|
||||
val result = loginItemType.toCipherView()
|
||||
|
||||
assertEquals(
|
||||
@Suppress("MaxLineLength")
|
||||
cipherView.copy(
|
||||
name = "mockName-1",
|
||||
notes = "mockNotes-1",
|
||||
type = CipherType.LOGIN,
|
||||
login = LoginView(
|
||||
username = "mockUsername-1",
|
||||
password = "mockPassword-1",
|
||||
passwordRevisionDate = Instant.ofEpochSecond(1_000L),
|
||||
uris = listOf(
|
||||
LoginUriView(
|
||||
uri = "mockUri-1",
|
||||
match = UriMatchType.DOMAIN,
|
||||
),
|
||||
),
|
||||
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
|
||||
autofillOnPageLoad = false,
|
||||
),
|
||||
favorite = true,
|
||||
reprompt = CipherRepromptType.NONE,
|
||||
fields = null,
|
||||
passwordHistory = listOf(
|
||||
PasswordHistoryView(
|
||||
password = "old_password",
|
||||
lastUsedDate = Instant.ofEpochSecond(1_000L),
|
||||
),
|
||||
),
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toCipherView should transform SecureNotes ItemType to CipherView`() {
|
||||
mockkStatic(Instant::class)
|
||||
|
@ -211,4 +265,117 @@ class VaultDataExtensionsTest {
|
|||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toCipherView should transform SecureNotes ItemType to CipherView with original cipher`() {
|
||||
val cipherView = DEFAULT_SECURE_NOTES_CIPHER_VIEW
|
||||
val secureNotesItemType = VaultAddItemState.ViewState.Content.SecureNotes(
|
||||
originalCipher = cipherView,
|
||||
name = "mockName-1",
|
||||
folderName = "mockFolder-1".asText(),
|
||||
favorite = false,
|
||||
masterPasswordReprompt = true,
|
||||
notes = "mockNotes-1",
|
||||
ownership = "mockOwnership-1",
|
||||
)
|
||||
|
||||
val result = secureNotesItemType.toCipherView()
|
||||
|
||||
assertEquals(
|
||||
cipherView.copy(
|
||||
name = "mockName-1",
|
||||
notes = "mockNotes-1",
|
||||
type = CipherType.SECURE_NOTE,
|
||||
secureNote = SecureNoteView(SecureNoteType.GENERIC),
|
||||
reprompt = CipherRepromptType.PASSWORD,
|
||||
fields = null,
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView(
|
||||
id = "id1234",
|
||||
organizationId = null,
|
||||
folderId = null,
|
||||
collectionIds = emptyList(),
|
||||
key = null,
|
||||
name = "cipher",
|
||||
notes = "Lots of notes",
|
||||
type = CipherType.LOGIN,
|
||||
login = null,
|
||||
identity = null,
|
||||
card = null,
|
||||
secureNote = null,
|
||||
favorite = false,
|
||||
reprompt = CipherRepromptType.PASSWORD,
|
||||
organizationUseTotp = false,
|
||||
edit = false,
|
||||
viewPassword = false,
|
||||
localData = null,
|
||||
attachments = null,
|
||||
fields = listOf(
|
||||
FieldView(
|
||||
name = "text",
|
||||
value = "value",
|
||||
type = FieldType.TEXT,
|
||||
linkedId = null,
|
||||
),
|
||||
FieldView(
|
||||
name = "hidden",
|
||||
value = "value",
|
||||
type = FieldType.HIDDEN,
|
||||
linkedId = null,
|
||||
),
|
||||
FieldView(
|
||||
name = "boolean",
|
||||
value = "true",
|
||||
type = FieldType.BOOLEAN,
|
||||
linkedId = null,
|
||||
),
|
||||
FieldView(
|
||||
name = "linked username",
|
||||
value = null,
|
||||
type = FieldType.LINKED,
|
||||
linkedId = 100U,
|
||||
),
|
||||
FieldView(
|
||||
name = "linked password",
|
||||
value = null,
|
||||
type = FieldType.LINKED,
|
||||
linkedId = 101U,
|
||||
),
|
||||
),
|
||||
passwordHistory = listOf(
|
||||
PasswordHistoryView(
|
||||
password = "old_password",
|
||||
lastUsedDate = Instant.ofEpochSecond(1_000L),
|
||||
),
|
||||
),
|
||||
creationDate = Instant.ofEpochSecond(1_000L),
|
||||
deletedDate = null,
|
||||
revisionDate = Instant.ofEpochSecond(1_000L),
|
||||
)
|
||||
|
||||
private val DEFAULT_LOGIN_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
|
||||
type = CipherType.LOGIN,
|
||||
login = LoginView(
|
||||
username = "username",
|
||||
password = "password",
|
||||
passwordRevisionDate = Instant.ofEpochSecond(1_000L),
|
||||
uris = listOf(
|
||||
LoginUriView(
|
||||
uri = "www.example.com",
|
||||
match = null,
|
||||
),
|
||||
),
|
||||
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
|
||||
autofillOnPageLoad = false,
|
||||
),
|
||||
)
|
||||
|
||||
private val DEFAULT_SECURE_NOTES_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
|
||||
type = CipherType.SECURE_NOTE,
|
||||
secureNote = SecureNoteView(type = SecureNoteType.GENERIC),
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue