mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
[PM-10405] Add SSH key cipher type (#4158)
This commit is contained in:
parent
6f26ae50ea
commit
78e7adfbc1
73 changed files with 2447 additions and 33 deletions
|
@ -21,7 +21,6 @@ private const val CARD_DIGITS_DISPLAYED: Int = 4
|
||||||
val CipherView.subtitle: String?
|
val CipherView.subtitle: String?
|
||||||
get() = when (type) {
|
get() = when (type) {
|
||||||
CipherType.LOGIN -> this.login?.username.orEmpty()
|
CipherType.LOGIN -> this.login?.username.orEmpty()
|
||||||
CipherType.SECURE_NOTE -> null
|
|
||||||
CipherType.CARD -> {
|
CipherType.CARD -> {
|
||||||
this
|
this
|
||||||
.card
|
.card
|
||||||
|
@ -45,6 +44,10 @@ val CipherView.subtitle: String?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CipherType.SECURE_NOTE,
|
||||||
|
CipherType.SSH_KEY,
|
||||||
|
-> null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -51,6 +51,9 @@ data class CipherJsonRequest(
|
||||||
@SerialName("secureNote")
|
@SerialName("secureNote")
|
||||||
val secureNote: SyncResponseJson.Cipher.SecureNote?,
|
val secureNote: SyncResponseJson.Cipher.SecureNote?,
|
||||||
|
|
||||||
|
@SerialName("sshKey")
|
||||||
|
val sshKey: SyncResponseJson.Cipher.SshKey?,
|
||||||
|
|
||||||
@SerialName("folderId")
|
@SerialName("folderId")
|
||||||
val folderId: String?,
|
val folderId: String?,
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,12 @@ enum class CipherTypeJson {
|
||||||
*/
|
*/
|
||||||
@SerialName("4")
|
@SerialName("4")
|
||||||
IDENTITY,
|
IDENTITY,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A SSH key.
|
||||||
|
*/
|
||||||
|
@SerialName("5")
|
||||||
|
SSH_KEY,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
|
|
|
@ -472,6 +472,9 @@ data class SyncResponseJson(
|
||||||
@SerialName("identity")
|
@SerialName("identity")
|
||||||
val identity: Identity?,
|
val identity: Identity?,
|
||||||
|
|
||||||
|
@SerialName("sshKey")
|
||||||
|
val sshKey: SshKey?,
|
||||||
|
|
||||||
@SerialName("collectionIds")
|
@SerialName("collectionIds")
|
||||||
val collectionIds: List<String>?,
|
val collectionIds: List<String>?,
|
||||||
|
|
||||||
|
@ -718,6 +721,25 @@ data class SyncResponseJson(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a SSH key in the vault response.
|
||||||
|
*
|
||||||
|
* @property publicKey The public key of the SSH key.
|
||||||
|
* @property privateKey The private key of the SSH key.
|
||||||
|
* @property keyFingerprint The key fingerprint of the SSH key.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class SshKey(
|
||||||
|
@SerialName("publicKey")
|
||||||
|
val publicKey: String?,
|
||||||
|
|
||||||
|
@SerialName("privateKey")
|
||||||
|
val privateKey: String?,
|
||||||
|
|
||||||
|
@SerialName("keyFingerprint")
|
||||||
|
val keyFingerprint: String?,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents password history in the vault response.
|
* Represents password history in the vault response.
|
||||||
*
|
*
|
||||||
|
|
|
@ -17,6 +17,7 @@ import com.bitwarden.vault.LoginUri
|
||||||
import com.bitwarden.vault.PasswordHistory
|
import com.bitwarden.vault.PasswordHistory
|
||||||
import com.bitwarden.vault.SecureNote
|
import com.bitwarden.vault.SecureNote
|
||||||
import com.bitwarden.vault.SecureNoteType
|
import com.bitwarden.vault.SecureNoteType
|
||||||
|
import com.bitwarden.vault.SshKey
|
||||||
import com.bitwarden.vault.UriMatchType
|
import com.bitwarden.vault.UriMatchType
|
||||||
import com.x8bit.bitwarden.data.platform.util.SpecialCharWithPrecedenceComparator
|
import com.x8bit.bitwarden.data.platform.util.SpecialCharWithPrecedenceComparator
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest
|
||||||
|
@ -55,6 +56,7 @@ fun Cipher.toEncryptedNetworkCipher(): CipherJsonRequest =
|
||||||
isFavorite = favorite,
|
isFavorite = favorite,
|
||||||
card = card?.toEncryptedNetworkCard(),
|
card = card?.toEncryptedNetworkCard(),
|
||||||
key = key,
|
key = key,
|
||||||
|
sshKey = sshKey?.toEncryptedNetworkSshKey(),
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,6 +79,7 @@ fun Cipher.toEncryptedNetworkCipherResponse(): SyncResponseJson.Cipher =
|
||||||
isFavorite = favorite,
|
isFavorite = favorite,
|
||||||
card = card?.toEncryptedNetworkCard(),
|
card = card?.toEncryptedNetworkCard(),
|
||||||
attachments = attachments?.toNetworkAttachmentList(),
|
attachments = attachments?.toNetworkAttachmentList(),
|
||||||
|
sshKey = sshKey?.toEncryptedNetworkSshKey(),
|
||||||
shouldOrganizationUseTotp = organizationUseTotp,
|
shouldOrganizationUseTotp = organizationUseTotp,
|
||||||
shouldEdit = edit,
|
shouldEdit = edit,
|
||||||
revisionDate = ZonedDateTime.ofInstant(revisionDate, ZoneOffset.UTC),
|
revisionDate = ZonedDateTime.ofInstant(revisionDate, ZoneOffset.UTC),
|
||||||
|
@ -102,6 +105,13 @@ private fun Card.toEncryptedNetworkCard(): SyncResponseJson.Cipher.Card =
|
||||||
brand = brand,
|
brand = brand,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun SshKey.toEncryptedNetworkSshKey(): SyncResponseJson.Cipher.SshKey =
|
||||||
|
SyncResponseJson.Cipher.SshKey(
|
||||||
|
publicKey = publicKey,
|
||||||
|
privateKey = privateKey,
|
||||||
|
keyFingerprint = fingerprint,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a list of Bitwarden SDK [Field] objects to a corresponding
|
* Converts a list of Bitwarden SDK [Field] objects to a corresponding
|
||||||
* list of [SyncResponseJson.Cipher.Field] objects.
|
* list of [SyncResponseJson.Cipher.Field] objects.
|
||||||
|
@ -309,6 +319,7 @@ private fun CipherType.toNetworkCipherType(): CipherTypeJson =
|
||||||
CipherType.SECURE_NOTE -> CipherTypeJson.SECURE_NOTE
|
CipherType.SECURE_NOTE -> CipherTypeJson.SECURE_NOTE
|
||||||
CipherType.CARD -> CipherTypeJson.CARD
|
CipherType.CARD -> CipherTypeJson.CARD
|
||||||
CipherType.IDENTITY -> CipherTypeJson.IDENTITY
|
CipherType.IDENTITY -> CipherTypeJson.IDENTITY
|
||||||
|
CipherType.SSH_KEY -> CipherTypeJson.SSH_KEY
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -334,6 +345,7 @@ fun SyncResponseJson.Cipher.toEncryptedSdkCipher(): Cipher =
|
||||||
type = type.toSdkCipherType(),
|
type = type.toSdkCipherType(),
|
||||||
login = login?.toSdkLogin(),
|
login = login?.toSdkLogin(),
|
||||||
identity = identity?.toSdkIdentity(),
|
identity = identity?.toSdkIdentity(),
|
||||||
|
sshKey = sshKey?.toSdkSshKey(),
|
||||||
card = card?.toSdkCard(),
|
card = card?.toSdkCard(),
|
||||||
secureNote = secureNote?.toSdkSecureNote(),
|
secureNote = secureNote?.toSdkSecureNote(),
|
||||||
favorite = isFavorite,
|
favorite = isFavorite,
|
||||||
|
@ -432,6 +444,17 @@ fun SyncResponseJson.Cipher.SecureNote.toSdkSecureNote(): SecureNote =
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms a [SyncResponseJson.Cipher.SshKey] into
|
||||||
|
* the corresponding Bitwarden SDK [SshKey].
|
||||||
|
*/
|
||||||
|
fun SyncResponseJson.Cipher.SshKey.toSdkSshKey(): SshKey =
|
||||||
|
SshKey(
|
||||||
|
publicKey = publicKey,
|
||||||
|
privateKey = privateKey,
|
||||||
|
fingerprint = keyFingerprint,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms a list of [SyncResponseJson.Cipher.Login.Uri] into
|
* Transforms a list of [SyncResponseJson.Cipher.Login.Uri] into
|
||||||
* a corresponding list of Bitwarden SDK [LoginUri].
|
* a corresponding list of Bitwarden SDK [LoginUri].
|
||||||
|
@ -517,6 +540,7 @@ fun CipherTypeJson.toSdkCipherType(): CipherType =
|
||||||
CipherTypeJson.SECURE_NOTE -> CipherType.SECURE_NOTE
|
CipherTypeJson.SECURE_NOTE -> CipherType.SECURE_NOTE
|
||||||
CipherTypeJson.CARD -> CipherType.CARD
|
CipherTypeJson.CARD -> CipherType.CARD
|
||||||
CipherTypeJson.IDENTITY -> CipherType.IDENTITY
|
CipherTypeJson.IDENTITY -> CipherType.IDENTITY
|
||||||
|
CipherTypeJson.SSH_KEY -> CipherType.SSH_KEY
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -26,6 +26,7 @@ private const val SEARCH_TYPE_VAULT_TRASH: String = "search_type_vault_trash"
|
||||||
private const val SEARCH_TYPE_VAULT_VERIFICATION_CODES: String =
|
private const val SEARCH_TYPE_VAULT_VERIFICATION_CODES: String =
|
||||||
"search_type_vault_verification_codes"
|
"search_type_vault_verification_codes"
|
||||||
private const val SEARCH_TYPE_ID: String = "search_type_id"
|
private const val SEARCH_TYPE_ID: String = "search_type_id"
|
||||||
|
private const val SEARCH_TYPE_VAULT_SSH_KEYS: String = "search_type_vault_ssh_keys"
|
||||||
|
|
||||||
private const val SEARCH_ROUTE_PREFIX: String = "search"
|
private const val SEARCH_ROUTE_PREFIX: String = "search"
|
||||||
private const val SEARCH_ROUTE: String = "$SEARCH_ROUTE_PREFIX/{$SEARCH_TYPE}/{$SEARCH_TYPE_ID}"
|
private const val SEARCH_ROUTE: String = "$SEARCH_ROUTE_PREFIX/{$SEARCH_TYPE}/{$SEARCH_TYPE_ID}"
|
||||||
|
@ -104,6 +105,7 @@ private fun determineSearchType(
|
||||||
SEARCH_TYPE_VAULT_FOLDER -> SearchType.Vault.Folder(requireNotNull(id))
|
SEARCH_TYPE_VAULT_FOLDER -> SearchType.Vault.Folder(requireNotNull(id))
|
||||||
SEARCH_TYPE_VAULT_TRASH -> SearchType.Vault.Trash
|
SEARCH_TYPE_VAULT_TRASH -> SearchType.Vault.Trash
|
||||||
SEARCH_TYPE_VAULT_VERIFICATION_CODES -> SearchType.Vault.VerificationCodes
|
SEARCH_TYPE_VAULT_VERIFICATION_CODES -> SearchType.Vault.VerificationCodes
|
||||||
|
SEARCH_TYPE_VAULT_SSH_KEYS -> SearchType.Vault.SshKeys
|
||||||
else -> throw IllegalArgumentException("Invalid Search Type")
|
else -> throw IllegalArgumentException("Invalid Search Type")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,6 +124,7 @@ private fun SearchType.toTypeString(): String =
|
||||||
SearchType.Vault.SecureNotes -> SEARCH_TYPE_VAULT_SECURE_NOTES
|
SearchType.Vault.SecureNotes -> SEARCH_TYPE_VAULT_SECURE_NOTES
|
||||||
SearchType.Vault.Trash -> SEARCH_TYPE_VAULT_TRASH
|
SearchType.Vault.Trash -> SEARCH_TYPE_VAULT_TRASH
|
||||||
SearchType.Vault.VerificationCodes -> SEARCH_TYPE_VAULT_VERIFICATION_CODES
|
SearchType.Vault.VerificationCodes -> SEARCH_TYPE_VAULT_VERIFICATION_CODES
|
||||||
|
SearchType.Vault.SshKeys -> SEARCH_TYPE_VAULT_SSH_KEYS
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun SearchType.toIdOrNull(): String? =
|
private fun SearchType.toIdOrNull(): String? =
|
||||||
|
@ -139,4 +142,5 @@ private fun SearchType.toIdOrNull(): String? =
|
||||||
SearchType.Vault.SecureNotes -> null
|
SearchType.Vault.SecureNotes -> null
|
||||||
SearchType.Vault.Trash -> null
|
SearchType.Vault.Trash -> null
|
||||||
SearchType.Vault.VerificationCodes -> null
|
SearchType.Vault.VerificationCodes -> null
|
||||||
|
SearchType.Vault.SshKeys -> null
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
|
||||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
|
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||||
|
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||||
|
@ -876,6 +877,7 @@ sealed class SearchTypeData : Parcelable {
|
||||||
* Indicates that we should be searching vault items.
|
* Indicates that we should be searching vault items.
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@OmitFromCoverage
|
||||||
sealed class Vault : SearchTypeData() {
|
sealed class Vault : SearchTypeData() {
|
||||||
/**
|
/**
|
||||||
* Indicates that we should be searching all vault items.
|
* Indicates that we should be searching all vault items.
|
||||||
|
@ -924,6 +926,16 @@ sealed class SearchTypeData : Parcelable {
|
||||||
.concat(R.string.secure_notes.asText())
|
.concat(R.string.secure_notes.asText())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that we should be searching only ssh key ciphers.
|
||||||
|
*/
|
||||||
|
data object SshKeys : Vault() {
|
||||||
|
override val title: Text
|
||||||
|
get() = R.string.search.asText()
|
||||||
|
.concat(" ".asText())
|
||||||
|
.concat(R.string.ssh_keys.asText())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that we should be searching only ciphers in the given collection.
|
* Indicates that we should be searching only ciphers in the given collection.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -58,6 +58,11 @@ sealed class SearchType : Parcelable {
|
||||||
*/
|
*/
|
||||||
data object SecureNotes : Vault()
|
data object SecureNotes : Vault()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that we should be searching only SSH key ciphers.
|
||||||
|
*/
|
||||||
|
data object SshKeys : Vault()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that we should be searching only ciphers in the given collection.
|
* Indicates that we should be searching only ciphers in the given collection.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -62,6 +62,7 @@ fun SearchTypeData.updateWithAdditionalDataIfNecessary(
|
||||||
SearchTypeData.Vault.SecureNotes -> this
|
SearchTypeData.Vault.SecureNotes -> this
|
||||||
SearchTypeData.Vault.Trash -> this
|
SearchTypeData.Vault.Trash -> this
|
||||||
SearchTypeData.Vault.VerificationCodes -> this
|
SearchTypeData.Vault.VerificationCodes -> this
|
||||||
|
SearchTypeData.Vault.SshKeys -> this
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -114,6 +115,7 @@ private fun CipherView.filterBySearchType(
|
||||||
is SearchTypeData.Vault.Identities -> type == CipherType.IDENTITY && deletedDate == null
|
is SearchTypeData.Vault.Identities -> type == CipherType.IDENTITY && deletedDate == null
|
||||||
is SearchTypeData.Vault.Logins -> type == CipherType.LOGIN && deletedDate == null
|
is SearchTypeData.Vault.Logins -> type == CipherType.LOGIN && deletedDate == null
|
||||||
is SearchTypeData.Vault.SecureNotes -> type == CipherType.SECURE_NOTE && deletedDate == null
|
is SearchTypeData.Vault.SecureNotes -> type == CipherType.SECURE_NOTE && deletedDate == null
|
||||||
|
is SearchTypeData.Vault.SshKeys -> type == CipherType.SSH_KEY && deletedDate == null
|
||||||
is SearchTypeData.Vault.VerificationCodes -> login?.totp != null && deletedDate == null
|
is SearchTypeData.Vault.VerificationCodes -> login?.totp != null && deletedDate == null
|
||||||
is SearchTypeData.Vault.Trash -> deletedDate != null
|
is SearchTypeData.Vault.Trash -> deletedDate != null
|
||||||
}
|
}
|
||||||
|
@ -255,6 +257,7 @@ private val CipherType.iconRes: Int
|
||||||
CipherType.SECURE_NOTE -> R.drawable.ic_note
|
CipherType.SECURE_NOTE -> R.drawable.ic_note
|
||||||
CipherType.CARD -> R.drawable.ic_payment_card
|
CipherType.CARD -> R.drawable.ic_payment_card
|
||||||
CipherType.IDENTITY -> R.drawable.ic_id_card
|
CipherType.IDENTITY -> R.drawable.ic_id_card
|
||||||
|
CipherType.SSH_KEY -> R.drawable.ic_ssh_key
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,4 +21,5 @@ fun SearchType.toSearchTypeData(): SearchTypeData =
|
||||||
SearchType.Vault.SecureNotes -> SearchTypeData.Vault.SecureNotes
|
SearchType.Vault.SecureNotes -> SearchTypeData.Vault.SecureNotes
|
||||||
SearchType.Vault.Trash -> SearchTypeData.Vault.Trash
|
SearchType.Vault.Trash -> SearchTypeData.Vault.Trash
|
||||||
SearchType.Vault.VerificationCodes -> SearchTypeData.Vault.VerificationCodes
|
SearchType.Vault.VerificationCodes -> SearchTypeData.Vault.VerificationCodes
|
||||||
|
SearchType.Vault.SshKeys -> SearchTypeData.Vault.SshKeys
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,21 +21,24 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTyp
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
|
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.feature.addedit.handlers.VaultAddEditIdentityTypeHandlers
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditSshKeyTypeHandlers
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The top level content UI state for the [VaultAddEditScreen].
|
* The top level content UI state for the [VaultAddEditScreen].
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||||
fun VaultAddEditContent(
|
fun VaultAddEditContent(
|
||||||
state: VaultAddEditState.ViewState.Content,
|
state: VaultAddEditState.ViewState.Content,
|
||||||
isAddItemMode: Boolean,
|
isAddItemMode: Boolean,
|
||||||
|
typeOptions: List<VaultAddEditState.ItemTypeOption>,
|
||||||
onTypeOptionClicked: (VaultAddEditState.ItemTypeOption) -> Unit,
|
onTypeOptionClicked: (VaultAddEditState.ItemTypeOption) -> Unit,
|
||||||
commonTypeHandlers: VaultAddEditCommonHandlers,
|
commonTypeHandlers: VaultAddEditCommonHandlers,
|
||||||
loginItemTypeHandlers: VaultAddEditLoginTypeHandlers,
|
loginItemTypeHandlers: VaultAddEditLoginTypeHandlers,
|
||||||
identityItemTypeHandlers: VaultAddEditIdentityTypeHandlers,
|
identityItemTypeHandlers: VaultAddEditIdentityTypeHandlers,
|
||||||
cardItemTypeHandlers: VaultAddEditCardTypeHandlers,
|
cardItemTypeHandlers: VaultAddEditCardTypeHandlers,
|
||||||
|
sshKeyItemTypeHandlers: VaultAddEditSshKeyTypeHandlers,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
permissionsManager: PermissionsManager,
|
permissionsManager: PermissionsManager,
|
||||||
) {
|
) {
|
||||||
|
@ -45,6 +48,7 @@ fun VaultAddEditContent(
|
||||||
is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> Unit
|
is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> Unit
|
||||||
is VaultAddEditState.ViewState.Content.ItemType.Card -> Unit
|
is VaultAddEditState.ViewState.Content.ItemType.Card -> Unit
|
||||||
is VaultAddEditState.ViewState.Content.ItemType.Identity -> Unit
|
is VaultAddEditState.ViewState.Content.ItemType.Identity -> Unit
|
||||||
|
is VaultAddEditState.ViewState.Content.ItemType.SshKey -> Unit
|
||||||
is VaultAddEditState.ViewState.Content.ItemType.Login -> {
|
is VaultAddEditState.ViewState.Content.ItemType.Login -> {
|
||||||
loginItemTypeHandlers.onSetupTotpClick(isGranted)
|
loginItemTypeHandlers.onSetupTotpClick(isGranted)
|
||||||
}
|
}
|
||||||
|
@ -77,6 +81,7 @@ fun VaultAddEditContent(
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
TypeOptionsItem(
|
TypeOptionsItem(
|
||||||
|
entries = typeOptions,
|
||||||
itemType = state.type,
|
itemType = state.type,
|
||||||
onTypeOptionClicked = onTypeOptionClicked,
|
onTypeOptionClicked = onTypeOptionClicked,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -131,6 +136,15 @@ fun VaultAddEditContent(
|
||||||
commonTypeHandlers = commonTypeHandlers,
|
commonTypeHandlers = commonTypeHandlers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is VaultAddEditState.ViewState.Content.ItemType.SshKey -> {
|
||||||
|
vaultAddEditSshKeyItems(
|
||||||
|
commonState = state.common,
|
||||||
|
sshKeyState = state.type,
|
||||||
|
commonTypeHandlers = commonTypeHandlers,
|
||||||
|
sshKeyTypeHandlers = sshKeyItemTypeHandlers,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
|
@ -141,12 +155,12 @@ fun VaultAddEditContent(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TypeOptionsItem(
|
private fun TypeOptionsItem(
|
||||||
|
entries: List<VaultAddEditState.ItemTypeOption>,
|
||||||
itemType: VaultAddEditState.ViewState.Content.ItemType,
|
itemType: VaultAddEditState.ViewState.Content.ItemType,
|
||||||
onTypeOptionClicked: (VaultAddEditState.ItemTypeOption) -> Unit,
|
onTypeOptionClicked: (VaultAddEditState.ItemTypeOption) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val possibleMainStates = VaultAddEditState.ItemTypeOption.entries.toList()
|
val optionsWithStrings = entries.associateWith { stringResource(id = it.labelRes) }
|
||||||
val optionsWithStrings = possibleMainStates.associateWith { stringResource(id = it.labelRes) }
|
|
||||||
|
|
||||||
BitwardenMultiSelectButton(
|
BitwardenMultiSelectButton(
|
||||||
label = stringResource(id = R.string.type),
|
label = stringResource(id = R.string.type),
|
||||||
|
|
|
@ -21,6 +21,7 @@ private const val LOGIN: String = "login"
|
||||||
private const val CARD: String = "card"
|
private const val CARD: String = "card"
|
||||||
private const val IDENTITY: String = "identity"
|
private const val IDENTITY: String = "identity"
|
||||||
private const val SECURE_NOTE: String = "secure_note"
|
private const val SECURE_NOTE: String = "secure_note"
|
||||||
|
private const val SSH_KEY: String = "ssh_key"
|
||||||
private const val ADD_ITEM_TYPE: String = "vault_add_item_type"
|
private const val ADD_ITEM_TYPE: String = "vault_add_item_type"
|
||||||
|
|
||||||
private const val ADD_EDIT_ITEM_PREFIX: String = "vault_add_edit_item"
|
private const val ADD_EDIT_ITEM_PREFIX: String = "vault_add_edit_item"
|
||||||
|
@ -127,6 +128,7 @@ private fun VaultItemCipherType.toTypeString(): String =
|
||||||
VaultItemCipherType.CARD -> CARD
|
VaultItemCipherType.CARD -> CARD
|
||||||
VaultItemCipherType.IDENTITY -> IDENTITY
|
VaultItemCipherType.IDENTITY -> IDENTITY
|
||||||
VaultItemCipherType.SECURE_NOTE -> SECURE_NOTE
|
VaultItemCipherType.SECURE_NOTE -> SECURE_NOTE
|
||||||
|
VaultItemCipherType.SSH_KEY -> SSH_KEY
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.toVaultItemCipherType(): VaultItemCipherType =
|
private fun String.toVaultItemCipherType(): VaultItemCipherType =
|
||||||
|
@ -135,6 +137,7 @@ private fun String.toVaultItemCipherType(): VaultItemCipherType =
|
||||||
CARD -> VaultItemCipherType.CARD
|
CARD -> VaultItemCipherType.CARD
|
||||||
IDENTITY -> VaultItemCipherType.IDENTITY
|
IDENTITY -> VaultItemCipherType.IDENTITY
|
||||||
SECURE_NOTE -> VaultItemCipherType.SECURE_NOTE
|
SECURE_NOTE -> VaultItemCipherType.SECURE_NOTE
|
||||||
|
SSH_KEY -> VaultItemCipherType.SSH_KEY
|
||||||
else -> throw IllegalStateException(
|
else -> throw IllegalStateException(
|
||||||
"Edit Item string arguments for VaultAddEditNavigation must match!",
|
"Edit Item string arguments for VaultAddEditNavigation must match!",
|
||||||
)
|
)
|
||||||
|
|
|
@ -58,6 +58,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTyp
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
|
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.feature.addedit.handlers.VaultAddEditIdentityTypeHandlers
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditSshKeyTypeHandlers
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditUserVerificationHandlers
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditUserVerificationHandlers
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -155,6 +156,10 @@ fun VaultAddEditScreen(
|
||||||
VaultAddEditCardTypeHandlers.create(viewModel = viewModel)
|
VaultAddEditCardTypeHandlers.create(viewModel = viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sshKeyItemTypeHandlers = remember(viewModel) {
|
||||||
|
VaultAddEditSshKeyTypeHandlers.create(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
val confirmDeleteClickAction = remember(viewModel) {
|
val confirmDeleteClickAction = remember(viewModel) {
|
||||||
{ viewModel.trySendAction(VaultAddEditAction.Common.ConfirmDeleteClick) }
|
{ viewModel.trySendAction(VaultAddEditAction.Common.ConfirmDeleteClick) }
|
||||||
}
|
}
|
||||||
|
@ -321,6 +326,7 @@ fun VaultAddEditScreen(
|
||||||
VaultAddEditContent(
|
VaultAddEditContent(
|
||||||
state = viewState,
|
state = viewState,
|
||||||
isAddItemMode = state.isAddItemMode,
|
isAddItemMode = state.isAddItemMode,
|
||||||
|
typeOptions = state.supportedItemTypes,
|
||||||
onTypeOptionClicked = remember(viewModel) {
|
onTypeOptionClicked = remember(viewModel) {
|
||||||
{ viewModel.trySendAction(VaultAddEditAction.Common.TypeOptionSelect(it)) }
|
{ viewModel.trySendAction(VaultAddEditAction.Common.TypeOptionSelect(it)) }
|
||||||
},
|
},
|
||||||
|
@ -329,6 +335,7 @@ fun VaultAddEditScreen(
|
||||||
permissionsManager = permissionsManager,
|
permissionsManager = permissionsManager,
|
||||||
identityItemTypeHandlers = identityItemTypeHandlers,
|
identityItemTypeHandlers = identityItemTypeHandlers,
|
||||||
cardItemTypeHandlers = cardItemTypeHandlers,
|
cardItemTypeHandlers = cardItemTypeHandlers,
|
||||||
|
sshKeyItemTypeHandlers = sshKeyItemTypeHandlers,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.addedit
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||||
|
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditSshKeyTypeHandlers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The UI for adding and editing a SSH key cipher.
|
||||||
|
*/
|
||||||
|
fun LazyListScope.vaultAddEditSshKeyItems(
|
||||||
|
commonState: VaultAddEditState.ViewState.Content.Common,
|
||||||
|
sshKeyState: VaultAddEditState.ViewState.Content.ItemType.SshKey,
|
||||||
|
commonTypeHandlers: VaultAddEditCommonHandlers,
|
||||||
|
sshKeyTypeHandlers: VaultAddEditSshKeyTypeHandlers,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
BitwardenTextField(
|
||||||
|
label = stringResource(id = R.string.name),
|
||||||
|
value = commonState.name,
|
||||||
|
onValueChange = commonTypeHandlers.onNameTextChange,
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag("ItemNameEntry")
|
||||||
|
.fillMaxWidth()
|
||||||
|
.standardHorizontalMargin(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
BitwardenTextField(
|
||||||
|
label = stringResource(id = R.string.public_key),
|
||||||
|
value = sshKeyState.publicKey,
|
||||||
|
onValueChange = sshKeyTypeHandlers.onPublicKeyTextChange,
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag("PublicKeyEntry")
|
||||||
|
.fillMaxWidth()
|
||||||
|
.standardHorizontalMargin(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
BitwardenPasswordField(
|
||||||
|
label = stringResource(id = R.string.private_key),
|
||||||
|
value = sshKeyState.privateKey,
|
||||||
|
onValueChange = sshKeyTypeHandlers.onPrivateKeyTextChange,
|
||||||
|
showPassword = sshKeyState.showPrivateKey,
|
||||||
|
showPasswordChange = { sshKeyTypeHandlers.onPrivateKeyVisibilityChange(it) },
|
||||||
|
showPasswordTestTag = "ViewPrivateKeyButton",
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag("PrivateKeyEntry")
|
||||||
|
.fillMaxWidth()
|
||||||
|
.standardHorizontalMargin(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
BitwardenTextField(
|
||||||
|
label = stringResource(id = R.string.fingerprint),
|
||||||
|
value = sshKeyState.fingerprint,
|
||||||
|
onValueChange = sshKeyTypeHandlers.onFingerprintTextChange,
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag("FingerprintEntry")
|
||||||
|
.fillMaxWidth()
|
||||||
|
.standardHorizontalMargin(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun VaultAddEditSshKeyItems_preview() {
|
||||||
|
BitwardenTheme {
|
||||||
|
LazyColumn {
|
||||||
|
vaultAddEditSshKeyItems(
|
||||||
|
commonState = VaultAddEditState.ViewState.Content.Common(
|
||||||
|
name = "SSH Key",
|
||||||
|
),
|
||||||
|
sshKeyState = VaultAddEditState.ViewState.Content.ItemType.SshKey(
|
||||||
|
publicKey = "public key",
|
||||||
|
privateKey = "private key",
|
||||||
|
fingerprint = "fingerprint",
|
||||||
|
showPublicKey = false,
|
||||||
|
showPrivateKey = false,
|
||||||
|
showFingerprint = false,
|
||||||
|
),
|
||||||
|
commonTypeHandlers = VaultAddEditCommonHandlers(
|
||||||
|
onNameTextChange = { },
|
||||||
|
onFolderSelected = { },
|
||||||
|
onToggleFavorite = { },
|
||||||
|
onToggleMasterPasswordReprompt = { },
|
||||||
|
onNotesTextChange = { },
|
||||||
|
onOwnerSelected = { },
|
||||||
|
onTooltipClick = { },
|
||||||
|
onAddNewCustomFieldClick = { _, _ -> },
|
||||||
|
onCustomFieldValueChange = { },
|
||||||
|
onCustomFieldActionSelect = { _, _ -> },
|
||||||
|
onCollectionSelect = { },
|
||||||
|
onHiddenFieldVisibilityChange = { },
|
||||||
|
),
|
||||||
|
sshKeyTypeHandlers = VaultAddEditSshKeyTypeHandlers(
|
||||||
|
onPublicKeyTextChange = { },
|
||||||
|
onPrivateKeyTextChange = { },
|
||||||
|
onPrivateKeyVisibilityChange = { },
|
||||||
|
onFingerprintTextChange = { },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,10 +16,12 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||||
import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement
|
import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement
|
||||||
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
|
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||||
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSaveItemOrNull
|
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSaveItemOrNull
|
||||||
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrNull
|
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrNull
|
||||||
|
@ -101,6 +103,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
private val resourceManager: ResourceManager,
|
private val resourceManager: ResourceManager,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
private val organizationEventManager: OrganizationEventManager,
|
private val organizationEventManager: OrganizationEventManager,
|
||||||
|
private val featureFlagManager: FeatureFlagManager,
|
||||||
) : 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]
|
||||||
|
@ -162,6 +165,11 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
// Set special conditions for autofill and fido2 save
|
// Set special conditions for autofill and fido2 save
|
||||||
shouldShowCloseButton = autofillSaveItem == null && fido2AttestationOptions == null,
|
shouldShowCloseButton = autofillSaveItem == null && fido2AttestationOptions == null,
|
||||||
shouldExitOnSave = shouldExitOnSave,
|
shouldExitOnSave = shouldExitOnSave,
|
||||||
|
supportedItemTypes = getSupportedItemTypeOptions(
|
||||||
|
isSshKeyVaultItemSupported = featureFlagManager.getFeatureFlag(
|
||||||
|
key = FlagKey.SshKeyCipherItems,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
@ -203,6 +211,11 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
.onEach(::sendAction)
|
.onEach(::sendAction)
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
featureFlagManager.getFeatureFlagFlow(FlagKey.SshKeyCipherItems)
|
||||||
|
.map { VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive(it) }
|
||||||
|
.onEach(::sendAction)
|
||||||
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleAction(action: VaultAddEditAction) {
|
override fun handleAction(action: VaultAddEditAction) {
|
||||||
|
@ -211,6 +224,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
is VaultAddEditAction.ItemType.LoginType -> handleAddLoginTypeAction(action)
|
is VaultAddEditAction.ItemType.LoginType -> handleAddLoginTypeAction(action)
|
||||||
is VaultAddEditAction.ItemType.IdentityType -> handleIdentityTypeActions(action)
|
is VaultAddEditAction.ItemType.IdentityType -> handleIdentityTypeActions(action)
|
||||||
is VaultAddEditAction.ItemType.CardType -> handleCardTypeActions(action)
|
is VaultAddEditAction.ItemType.CardType -> handleCardTypeActions(action)
|
||||||
|
is VaultAddEditAction.ItemType.SshKeyType -> handleSshKeyTypeActions(action)
|
||||||
is VaultAddEditAction.Internal -> handleInternalActions(action)
|
is VaultAddEditAction.Internal -> handleInternalActions(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -320,6 +334,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
VaultAddEditState.ItemTypeOption.CARD -> handleSwitchToAddCardItem()
|
VaultAddEditState.ItemTypeOption.CARD -> handleSwitchToAddCardItem()
|
||||||
VaultAddEditState.ItemTypeOption.IDENTITY -> handleSwitchToAddIdentityItem()
|
VaultAddEditState.ItemTypeOption.IDENTITY -> handleSwitchToAddIdentityItem()
|
||||||
VaultAddEditState.ItemTypeOption.SECURE_NOTES -> handleSwitchToAddSecureNotesItem()
|
VaultAddEditState.ItemTypeOption.SECURE_NOTES -> handleSwitchToAddSecureNotesItem()
|
||||||
|
VaultAddEditState.ItemTypeOption.SSH_KEYS -> handleSwitchToSshKeyItem()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,6 +386,18 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleSwitchToSshKeyItem() {
|
||||||
|
updateContent { currentContent ->
|
||||||
|
currentContent.copy(
|
||||||
|
common = currentContent.clearNonSharedData(),
|
||||||
|
type = currentContent.previousItemTypeOrDefault(
|
||||||
|
itemType = VaultAddEditState.ItemTypeOption.SSH_KEYS,
|
||||||
|
),
|
||||||
|
previousItemTypes = currentContent.toUpdatedPreviousItemTypes(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
private fun handleSaveClick() = onContent { content ->
|
private fun handleSaveClick() = onContent { content ->
|
||||||
if (content.common.name.isBlank()) {
|
if (content.common.name.isBlank()) {
|
||||||
|
@ -1363,6 +1390,54 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
|
|
||||||
//endregion Card Type Handlers
|
//endregion Card Type Handlers
|
||||||
|
|
||||||
|
//region SSH Key Type Handlers
|
||||||
|
|
||||||
|
private fun handleSshKeyTypeActions(action: VaultAddEditAction.ItemType.SshKeyType) {
|
||||||
|
when (action) {
|
||||||
|
is VaultAddEditAction.ItemType.SshKeyType.PublicKeyTextChange -> {
|
||||||
|
handlePublicKeyTextChange(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
is VaultAddEditAction.ItemType.SshKeyType.PrivateKeyTextChange -> {
|
||||||
|
handlePrivateKeyTextChange(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
is VaultAddEditAction.ItemType.SshKeyType.PrivateKeyVisibilityChange -> {
|
||||||
|
handlePrivateKeyVisibilityChange(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
is VaultAddEditAction.ItemType.SshKeyType.FingerprintTextChange -> {
|
||||||
|
handleSshKeyFingerprintTextChange(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePublicKeyTextChange(
|
||||||
|
action: VaultAddEditAction.ItemType.SshKeyType.PublicKeyTextChange,
|
||||||
|
) {
|
||||||
|
updateSshKeyContent { it.copy(publicKey = action.publicKey) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePrivateKeyTextChange(
|
||||||
|
action: VaultAddEditAction.ItemType.SshKeyType.PrivateKeyTextChange,
|
||||||
|
) {
|
||||||
|
updateSshKeyContent { it.copy(privateKey = action.privateKey) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePrivateKeyVisibilityChange(
|
||||||
|
action: VaultAddEditAction.ItemType.SshKeyType.PrivateKeyVisibilityChange,
|
||||||
|
) {
|
||||||
|
updateSshKeyContent { it.copy(showPrivateKey = action.isVisible) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSshKeyFingerprintTextChange(
|
||||||
|
action: VaultAddEditAction.ItemType.SshKeyType.FingerprintTextChange,
|
||||||
|
) {
|
||||||
|
updateSshKeyContent { it.copy(fingerprint = action.fingerprint) }
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion SSH Key Type Handlers
|
||||||
|
|
||||||
//region Internal Type Handlers
|
//region Internal Type Handlers
|
||||||
|
|
||||||
private fun handleInternalActions(action: VaultAddEditAction.Internal) {
|
private fun handleInternalActions(action: VaultAddEditAction.Internal) {
|
||||||
|
@ -1397,6 +1472,10 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
is VaultAddEditAction.Internal.ValidateFido2PinResultReceive -> {
|
is VaultAddEditAction.Internal.ValidateFido2PinResultReceive -> {
|
||||||
handleValidateFido2PinResultReceive(action)
|
handleValidateFido2PinResultReceive(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive -> {
|
||||||
|
handleSshKeyCipherItemsFeatureFlagReceive(action)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1707,6 +1786,18 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
getRequestAndRegisterCredential()
|
getRequestAndRegisterCredential()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleSshKeyCipherItemsFeatureFlagReceive(
|
||||||
|
action: VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive,
|
||||||
|
) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
supportedItemTypes = getSupportedItemTypeOptions(
|
||||||
|
isSshKeyVaultItemSupported = action.enabled,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//endregion Internal Type Handlers
|
//endregion Internal Type Handlers
|
||||||
|
|
||||||
//region Utility Functions
|
//region Utility Functions
|
||||||
|
@ -1818,6 +1909,19 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inline fun updateSshKeyContent(
|
||||||
|
crossinline block: (VaultAddEditState.ViewState.Content.ItemType.SshKey) ->
|
||||||
|
VaultAddEditState.ViewState.Content.ItemType.SshKey,
|
||||||
|
) {
|
||||||
|
updateContent { currentContent ->
|
||||||
|
(currentContent.type as? VaultAddEditState.ViewState.Content.ItemType.SshKey)?.let {
|
||||||
|
currentContent.copy(
|
||||||
|
type = block(it),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun VaultAddEditState.ViewState.Content.clearNonSharedData():
|
private fun VaultAddEditState.ViewState.Content.clearNonSharedData():
|
||||||
VaultAddEditState.ViewState.Content.Common =
|
VaultAddEditState.ViewState.Content.Common =
|
||||||
common.copy(
|
common.copy(
|
||||||
|
@ -1852,6 +1956,10 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
VaultAddEditState.ItemTypeOption.SECURE_NOTES -> {
|
VaultAddEditState.ItemTypeOption.SECURE_NOTES -> {
|
||||||
VaultAddEditState.ViewState.Content.ItemType.SecureNotes
|
VaultAddEditState.ViewState.Content.ItemType.SecureNotes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VaultAddEditState.ItemTypeOption.SSH_KEYS -> {
|
||||||
|
VaultAddEditState.ViewState.Content.ItemType.SshKey()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1911,6 +2019,7 @@ data class VaultAddEditState(
|
||||||
val viewState: ViewState,
|
val viewState: ViewState,
|
||||||
val dialog: DialogState?,
|
val dialog: DialogState?,
|
||||||
val shouldShowCloseButton: Boolean = true,
|
val shouldShowCloseButton: Boolean = true,
|
||||||
|
val supportedItemTypes: List<ItemTypeOption>,
|
||||||
// Internal
|
// Internal
|
||||||
val shouldExitOnSave: Boolean = false,
|
val shouldExitOnSave: Boolean = false,
|
||||||
val totpData: TotpData? = null,
|
val totpData: TotpData? = null,
|
||||||
|
@ -1957,6 +2066,7 @@ data class VaultAddEditState(
|
||||||
CARD(R.string.type_card),
|
CARD(R.string.type_card),
|
||||||
IDENTITY(R.string.type_identity),
|
IDENTITY(R.string.type_identity),
|
||||||
SECURE_NOTES(R.string.type_secure_note),
|
SECURE_NOTES(R.string.type_secure_note),
|
||||||
|
SSH_KEYS(R.string.type_ssh_key),
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2162,6 +2272,25 @@ data class VaultAddEditState(
|
||||||
data object SecureNotes : ItemType() {
|
data object SecureNotes : ItemType() {
|
||||||
override val itemTypeOption: ItemTypeOption get() = ItemTypeOption.SECURE_NOTES
|
override val itemTypeOption: ItemTypeOption get() = ItemTypeOption.SECURE_NOTES
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the `SshKey` item type.
|
||||||
|
*
|
||||||
|
* @property publicKey The public key for the SSH key item.
|
||||||
|
* @property privateKey The private key for the SSH key item.
|
||||||
|
* @property fingerprint The fingerprint for the SSH key item.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class SshKey(
|
||||||
|
val publicKey: String = "",
|
||||||
|
val privateKey: String = "",
|
||||||
|
val fingerprint: String = "",
|
||||||
|
val showPublicKey: Boolean = false,
|
||||||
|
val showPrivateKey: Boolean = false,
|
||||||
|
val showFingerprint: Boolean = false,
|
||||||
|
) : ItemType() {
|
||||||
|
override val itemTypeOption: ItemTypeOption get() = ItemTypeOption.SSH_KEYS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2928,6 +3057,31 @@ sealed class VaultAddEditAction {
|
||||||
*/
|
*/
|
||||||
data class SecurityCodeVisibilityChange(val isVisible: Boolean) : CardType()
|
data class SecurityCodeVisibilityChange(val isVisible: Boolean) : CardType()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents actions specific to the SSH Key type.
|
||||||
|
*/
|
||||||
|
sealed class SshKeyType : ItemType() {
|
||||||
|
/**
|
||||||
|
* Fired when the public key text input is changed.
|
||||||
|
*/
|
||||||
|
data class PublicKeyTextChange(val publicKey: String) : SshKeyType()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when the private key text input is changed.
|
||||||
|
*/
|
||||||
|
data class PrivateKeyTextChange(val privateKey: String) : SshKeyType()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when the private key's visibility has changed.
|
||||||
|
*/
|
||||||
|
data class PrivateKeyVisibilityChange(val isVisible: Boolean) : SshKeyType()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when the fingerprint text input is changed.
|
||||||
|
*/
|
||||||
|
data class FingerprintTextChange(val fingerprint: String) : SshKeyType()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2952,6 +3106,13 @@ sealed class VaultAddEditAction {
|
||||||
val generatorResult: GeneratorResult,
|
val generatorResult: GeneratorResult,
|
||||||
) : Internal()
|
) : Internal()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the the SSH key vault item feature flag state has been received.
|
||||||
|
*/
|
||||||
|
data class SshKeyCipherItemsFeatureFlagReceive(
|
||||||
|
val enabled: Boolean,
|
||||||
|
) : Internal()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that the vault item data has been received.
|
* Indicates that the vault item data has been received.
|
||||||
*/
|
*/
|
||||||
|
@ -3005,3 +3166,8 @@ sealed class VaultAddEditAction {
|
||||||
) : Internal()
|
) : Internal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getSupportedItemTypeOptions(
|
||||||
|
isSshKeyVaultItemSupported: Boolean,
|
||||||
|
) = VaultAddEditState.ItemTypeOption.entries
|
||||||
|
.filter { isSshKeyVaultItemSupported || it != VaultAddEditState.ItemTypeOption.SSH_KEYS }
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.addedit.handlers
|
||||||
|
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditAction
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a set of handlers for interactions related to SSH key types within the vault add/edit
|
||||||
|
* screen.
|
||||||
|
*
|
||||||
|
* These handlers are used to update the ViewModel with user actions such as text changes and
|
||||||
|
* visibility changes for different SSH key fields (public key, private key, fingerprint).
|
||||||
|
*
|
||||||
|
* @property onPublicKeyTextChange Handler for changes in the public key text field.
|
||||||
|
* @property onPrivateKeyTextChange Handler for changes in the private key text field.
|
||||||
|
* @property onPrivateKeyVisibilityChange Handler for toggling the visibility of the private key.
|
||||||
|
* @property onFingerprintTextChange Handler for changes in the fingerprint text field.
|
||||||
|
*/
|
||||||
|
data class VaultAddEditSshKeyTypeHandlers(
|
||||||
|
val onPublicKeyTextChange: (String) -> Unit,
|
||||||
|
val onPrivateKeyTextChange: (String) -> Unit,
|
||||||
|
val onPrivateKeyVisibilityChange: (Boolean) -> Unit,
|
||||||
|
val onFingerprintTextChange: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
@Suppress("UndocumentedPublicClass")
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Creates an instance of [VaultAddEditSshKeyTypeHandlers] with handlers that dispatch
|
||||||
|
* actions to the provided ViewModel.
|
||||||
|
*
|
||||||
|
* @param viewModel The ViewModel to which actions will be dispatched.
|
||||||
|
*/
|
||||||
|
fun create(viewModel: VaultAddEditViewModel): VaultAddEditSshKeyTypeHandlers =
|
||||||
|
VaultAddEditSshKeyTypeHandlers(
|
||||||
|
onPublicKeyTextChange = { newPublicKey ->
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultAddEditAction.ItemType.SshKeyType.PublicKeyTextChange(
|
||||||
|
publicKey = newPublicKey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onPrivateKeyTextChange = { newPrivateKey ->
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultAddEditAction.ItemType.SshKeyType.PrivateKeyTextChange(
|
||||||
|
privateKey = newPrivateKey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onPrivateKeyVisibilityChange = {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultAddEditAction.ItemType.SshKeyType.PrivateKeyVisibilityChange(
|
||||||
|
isVisible = it,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onFingerprintTextChange = { newFingerprint ->
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultAddEditAction.ItemType.SshKeyType.FingerprintTextChange(
|
||||||
|
fingerprint = newFingerprint,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -66,4 +66,5 @@ private val VaultAddEditState.ViewState.Content.ItemType.defaultLinkedFieldTypeO
|
||||||
is VaultAddEditState.ViewState.Content.ItemType.Identity -> VaultLinkedFieldType.TITLE
|
is VaultAddEditState.ViewState.Content.ItemType.Identity -> VaultLinkedFieldType.TITLE
|
||||||
is VaultAddEditState.ViewState.Content.ItemType.Login -> VaultLinkedFieldType.USERNAME
|
is VaultAddEditState.ViewState.Content.ItemType.Login -> VaultLinkedFieldType.USERNAME
|
||||||
is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> null
|
is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> null
|
||||||
|
is VaultAddEditState.ViewState.Content.ItemType.SshKey -> null
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ private const val PASSKEY_CREATION_TIME_PATTERN: String = "hh:mm a"
|
||||||
/**
|
/**
|
||||||
* Transforms [CipherView] into [VaultAddEditState.ViewState].
|
* Transforms [CipherView] into [VaultAddEditState.ViewState].
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongMethod")
|
||||||
fun CipherView.toViewState(
|
fun CipherView.toViewState(
|
||||||
isClone: Boolean,
|
isClone: Boolean,
|
||||||
isIndividualVaultDisabled: Boolean,
|
isIndividualVaultDisabled: Boolean,
|
||||||
|
@ -88,6 +89,12 @@ fun CipherView.toViewState(
|
||||||
zip = identity?.postalCode.orEmpty(),
|
zip = identity?.postalCode.orEmpty(),
|
||||||
country = identity?.country.orEmpty(),
|
country = identity?.country.orEmpty(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CipherType.SSH_KEY -> VaultAddEditState.ViewState.Content.ItemType.SshKey(
|
||||||
|
publicKey = sshKey?.publicKey.orEmpty(),
|
||||||
|
privateKey = sshKey?.privateKey.orEmpty(),
|
||||||
|
fingerprint = sshKey?.fingerprint.orEmpty(),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
common = VaultAddEditState.ViewState.Content.Common(
|
common = VaultAddEditState.ViewState.Content.Common(
|
||||||
originalCipher = this,
|
originalCipher = this,
|
||||||
|
|
|
@ -25,4 +25,5 @@ fun VaultItemCipherType.toItemType(): VaultAddEditState.ViewState.Content.ItemTy
|
||||||
VaultItemCipherType.CARD -> VaultAddEditState.ViewState.Content.ItemType.Card()
|
VaultItemCipherType.CARD -> VaultAddEditState.ViewState.Content.ItemType.Card()
|
||||||
VaultItemCipherType.IDENTITY -> VaultAddEditState.ViewState.Content.ItemType.Identity()
|
VaultItemCipherType.IDENTITY -> VaultAddEditState.ViewState.Content.ItemType.Identity()
|
||||||
VaultItemCipherType.SECURE_NOTE -> VaultAddEditState.ViewState.Content.ItemType.SecureNotes
|
VaultItemCipherType.SECURE_NOTE -> VaultAddEditState.ViewState.Content.ItemType.SecureNotes
|
||||||
|
VaultItemCipherType.SSH_KEY -> VaultAddEditState.ViewState.Content.ItemType.SshKey()
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
|
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers
|
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the vault item screen.
|
* Displays the vault item screen.
|
||||||
|
@ -265,6 +266,9 @@ fun VaultItemScreen(
|
||||||
vaultCardItemTypeHandlers = remember(viewModel) {
|
vaultCardItemTypeHandlers = remember(viewModel) {
|
||||||
VaultCardItemTypeHandlers.create(viewModel = viewModel)
|
VaultCardItemTypeHandlers.create(viewModel = viewModel)
|
||||||
},
|
},
|
||||||
|
vaultSshKeyItemTypeHandlers = remember(viewModel) {
|
||||||
|
VaultSshKeyItemTypeHandlers.create(viewModel = viewModel)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -342,6 +346,7 @@ private fun VaultItemContent(
|
||||||
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
|
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
|
||||||
vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers,
|
vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers,
|
||||||
vaultCardItemTypeHandlers: VaultCardItemTypeHandlers,
|
vaultCardItemTypeHandlers: VaultCardItemTypeHandlers,
|
||||||
|
vaultSshKeyItemTypeHandlers: VaultSshKeyItemTypeHandlers,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when (viewState) {
|
when (viewState) {
|
||||||
|
@ -389,6 +394,15 @@ private fun VaultItemContent(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is VaultItemState.ViewState.Content.ItemType.SshKey -> {
|
||||||
|
VaultItemSshKeyContent(
|
||||||
|
commonState = viewState.common,
|
||||||
|
sshKeyItemState = viewState.type,
|
||||||
|
vaultSshKeyItemTypeHandlers = vaultSshKeyItemTypeHandlers,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.item
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top level content UI state for the [VaultItemScreen] when viewing a SSH key cipher.
|
||||||
|
*/
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
@Composable
|
||||||
|
fun VaultItemSshKeyContent(
|
||||||
|
commonState: VaultItemState.ViewState.Content.Common,
|
||||||
|
sshKeyItemState: VaultItemState.ViewState.Content.ItemType.SshKey,
|
||||||
|
vaultSshKeyItemTypeHandlers: VaultSshKeyItemTypeHandlers,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
LazyColumn(modifier = modifier) {
|
||||||
|
item {
|
||||||
|
BitwardenListHeaderText(
|
||||||
|
label = stringResource(id = R.string.item_information),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
BitwardenTextField(
|
||||||
|
label = stringResource(id = R.string.name),
|
||||||
|
value = commonState.name,
|
||||||
|
onValueChange = { },
|
||||||
|
readOnly = true,
|
||||||
|
singleLine = false,
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag("SshKeyItemNameEntry")
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sshKeyItemState.publicKey?.let { publicKey ->
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
BitwardenTextField(
|
||||||
|
label = stringResource(id = R.string.public_key),
|
||||||
|
value = publicKey,
|
||||||
|
onValueChange = { },
|
||||||
|
singleLine = false,
|
||||||
|
readOnly = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag("SshKeyItemPublicKeyEntry")
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sshKeyItemState.privateKey?.let { privateKey ->
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
BitwardenPasswordField(
|
||||||
|
label = stringResource(id = R.string.private_key),
|
||||||
|
value = privateKey,
|
||||||
|
onValueChange = { },
|
||||||
|
singleLine = false,
|
||||||
|
readOnly = true,
|
||||||
|
showPassword = sshKeyItemState.showPrivateKey,
|
||||||
|
showPasswordTestTag = "ViewPrivateKeyButton",
|
||||||
|
showPasswordChange = vaultSshKeyItemTypeHandlers.onShowPrivateKeyClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag("SshKeyItemPrivateKeyEntry")
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sshKeyItemState.fingerprint?.let { fingerprint ->
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
BitwardenTextField(
|
||||||
|
label = stringResource(id = R.string.fingerprint),
|
||||||
|
value = fingerprint,
|
||||||
|
onValueChange = { },
|
||||||
|
singleLine = false,
|
||||||
|
readOnly = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag("SshKeyItemFingerprintEntry")
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
VaultItemUpdateText(
|
||||||
|
header = "${stringResource(id = R.string.date_updated)}: ",
|
||||||
|
text = commonState.lastUpdated,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.testTag("SshKeyItemLastUpdated"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(88.dp))
|
||||||
|
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -116,6 +116,7 @@ class VaultItemViewModel @Inject constructor(
|
||||||
when (action) {
|
when (action) {
|
||||||
is VaultItemAction.ItemType.Login -> handleLoginTypeActions(action)
|
is VaultItemAction.ItemType.Login -> handleLoginTypeActions(action)
|
||||||
is VaultItemAction.ItemType.Card -> handleCardTypeActions(action)
|
is VaultItemAction.ItemType.Card -> handleCardTypeActions(action)
|
||||||
|
is VaultItemAction.ItemType.SshKey -> handleSshKeyTypeActions(action)
|
||||||
is VaultItemAction.Common -> handleCommonActions(action)
|
is VaultItemAction.Common -> handleCommonActions(action)
|
||||||
is VaultItemAction.Internal -> handleInternalAction(action)
|
is VaultItemAction.Internal -> handleInternalAction(action)
|
||||||
}
|
}
|
||||||
|
@ -753,6 +754,32 @@ class VaultItemViewModel @Inject constructor(
|
||||||
|
|
||||||
//endregion Card Type Handlers
|
//endregion Card Type Handlers
|
||||||
|
|
||||||
|
//region SSH Key Type Handlers
|
||||||
|
|
||||||
|
private fun handleSshKeyTypeActions(action: VaultItemAction.ItemType.SshKey) {
|
||||||
|
when (action) {
|
||||||
|
is VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked -> {
|
||||||
|
handlePrivateKeyVisibilityClicked(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePrivateKeyVisibilityClicked(
|
||||||
|
action: VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked,
|
||||||
|
) {
|
||||||
|
onSshKeyContent { content, sshKey ->
|
||||||
|
mutableStateFlow.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
viewState = content.copy(
|
||||||
|
type = sshKey.copy(showPrivateKey = action.isVisible),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion SSH Key Type Handlers
|
||||||
|
|
||||||
//region Internal Type Handlers
|
//region Internal Type Handlers
|
||||||
|
|
||||||
private fun handleInternalAction(action: VaultItemAction.Internal) {
|
private fun handleInternalAction(action: VaultItemAction.Internal) {
|
||||||
|
@ -1057,6 +1084,21 @@ class VaultItemViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inline fun onSshKeyContent(
|
||||||
|
crossinline block: (
|
||||||
|
VaultItemState.ViewState.Content,
|
||||||
|
VaultItemState.ViewState.Content.ItemType.SshKey,
|
||||||
|
) -> Unit,
|
||||||
|
) {
|
||||||
|
state.viewState.asContentOrNull()
|
||||||
|
?.let { content ->
|
||||||
|
(content.type as? VaultItemState.ViewState.Content.ItemType.SshKey)
|
||||||
|
?.let { sshKeyContent ->
|
||||||
|
block(content, sshKeyContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1359,6 +1401,20 @@ data class VaultItemState(
|
||||||
val isVisible: Boolean,
|
val isVisible: Boolean,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the data for displaying an `SSHKey` item type.
|
||||||
|
*
|
||||||
|
* @property name The name of the key.
|
||||||
|
* @property privateKey The SSH private key.
|
||||||
|
*/
|
||||||
|
data class SshKey(
|
||||||
|
val name: String?,
|
||||||
|
val publicKey: String?,
|
||||||
|
val privateKey: String?,
|
||||||
|
val fingerprint: String?,
|
||||||
|
val showPrivateKey: Boolean,
|
||||||
|
) : ItemType()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1697,6 +1753,16 @@ sealed class VaultItemAction {
|
||||||
*/
|
*/
|
||||||
data class NumberVisibilityClick(val isVisible: Boolean) : Card()
|
data class NumberVisibilityClick(val isVisible: Boolean) : Card()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents actions specific to the SshKey type.
|
||||||
|
*/
|
||||||
|
sealed class SshKey : ItemType() {
|
||||||
|
/**
|
||||||
|
* The user has clicked to display the private key.
|
||||||
|
*/
|
||||||
|
data class PrivateKeyVisibilityClicked(val isVisible: Boolean) : SshKey()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.item.handlers
|
||||||
|
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemAction
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of handler functions for managing actions within the context of viewing SSH key
|
||||||
|
* items in a vault.
|
||||||
|
*/
|
||||||
|
data class VaultSshKeyItemTypeHandlers(
|
||||||
|
val onShowPrivateKeyClick: (isVisible: Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Suppress("UndocumentedPublicClass")
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the [VaultSshKeyItemTypeHandlers] using the [viewModel] to send desired actions.
|
||||||
|
*/
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
fun create(viewModel: VaultItemViewModel): VaultSshKeyItemTypeHandlers =
|
||||||
|
VaultSshKeyItemTypeHandlers(
|
||||||
|
onShowPrivateKeyClick = {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked(
|
||||||
|
isVisible = it,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -156,6 +156,19 @@ fun CipherView.toViewState(
|
||||||
address = identity?.identityAddress,
|
address = identity?.identityAddress,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CipherType.SSH_KEY -> {
|
||||||
|
val sshKeyValues = requireNotNull(sshKey)
|
||||||
|
VaultItemState.ViewState.Content.ItemType.SshKey(
|
||||||
|
name = name,
|
||||||
|
publicKey = sshKeyValues.publicKey,
|
||||||
|
privateKey = sshKeyValues.privateKey,
|
||||||
|
fingerprint = sshKeyValues.fingerprint,
|
||||||
|
showPrivateKey = (previousState?.type as?
|
||||||
|
VaultItemState.ViewState.Content.ItemType.SshKey)
|
||||||
|
?.showPrivateKey == true,
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ private const val COLLECTION: String = "collection"
|
||||||
private const val FOLDER: String = "folder"
|
private const val FOLDER: String = "folder"
|
||||||
private const val IDENTITY: String = "identity"
|
private const val IDENTITY: String = "identity"
|
||||||
private const val LOGIN: String = "login"
|
private const val LOGIN: String = "login"
|
||||||
|
private const val SSH_KEY: String = "ssh_key"
|
||||||
private const val SECURE_NOTE: String = "secure_note"
|
private const val SECURE_NOTE: String = "secure_note"
|
||||||
private const val SEND_FILE: String = "send_file"
|
private const val SEND_FILE: String = "send_file"
|
||||||
private const val SEND_TEXT: String = "send_text"
|
private const val SEND_TEXT: String = "send_text"
|
||||||
|
@ -234,6 +235,7 @@ private fun VaultItemListingType.toTypeString(): String {
|
||||||
is VaultItemListingType.Trash -> TRASH
|
is VaultItemListingType.Trash -> TRASH
|
||||||
is VaultItemListingType.SendFile -> SEND_FILE
|
is VaultItemListingType.SendFile -> SEND_FILE
|
||||||
is VaultItemListingType.SendText -> SEND_TEXT
|
is VaultItemListingType.SendText -> SEND_TEXT
|
||||||
|
is VaultItemListingType.SshKey -> SSH_KEY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,6 +250,7 @@ private fun VaultItemListingType.toIdOrNull(): String? =
|
||||||
is VaultItemListingType.Trash -> null
|
is VaultItemListingType.Trash -> null
|
||||||
is VaultItemListingType.SendFile -> null
|
is VaultItemListingType.SendFile -> null
|
||||||
is VaultItemListingType.SendText -> null
|
is VaultItemListingType.SendText -> null
|
||||||
|
is VaultItemListingType.SshKey -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun determineVaultItemListingType(
|
private fun determineVaultItemListingType(
|
||||||
|
@ -259,6 +262,7 @@ private fun determineVaultItemListingType(
|
||||||
CARD -> VaultItemListingType.Card
|
CARD -> VaultItemListingType.Card
|
||||||
IDENTITY -> VaultItemListingType.Identity
|
IDENTITY -> VaultItemListingType.Identity
|
||||||
SECURE_NOTE -> VaultItemListingType.SecureNote
|
SECURE_NOTE -> VaultItemListingType.SecureNote
|
||||||
|
SSH_KEY -> VaultItemListingType.SshKey
|
||||||
TRASH -> VaultItemListingType.Trash
|
TRASH -> VaultItemListingType.Trash
|
||||||
FOLDER -> VaultItemListingType.Folder(folderId = id)
|
FOLDER -> VaultItemListingType.Folder(folderId = id)
|
||||||
COLLECTION -> VaultItemListingType.Collection(collectionId = requireNotNull(id))
|
COLLECTION -> VaultItemListingType.Collection(collectionId = requireNotNull(id))
|
||||||
|
|
|
@ -2058,6 +2058,14 @@ data class VaultItemListingState(
|
||||||
override val hasFab: Boolean get() = true
|
override val hasFab: Boolean get() = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A SSH key item listing.
|
||||||
|
*/
|
||||||
|
data object SshKey : Vault() {
|
||||||
|
override val titleText: Text get() = R.string.ssh_keys.asText()
|
||||||
|
override val hasFab: Boolean get() = true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Secure Trash item listing.
|
* A Secure Trash item listing.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -70,6 +70,10 @@ fun CipherView.determineListingPredicate(
|
||||||
type == CipherType.SECURE_NOTE && deletedDate == null
|
type == CipherType.SECURE_NOTE && deletedDate == null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is VaultItemListingState.ItemListingType.Vault.SshKey -> {
|
||||||
|
type == CipherType.SSH_KEY && deletedDate == null
|
||||||
|
}
|
||||||
|
|
||||||
is VaultItemListingState.ItemListingType.Vault.Trash -> {
|
is VaultItemListingState.ItemListingType.Vault.Trash -> {
|
||||||
deletedDate != null
|
deletedDate != null
|
||||||
}
|
}
|
||||||
|
@ -272,6 +276,7 @@ fun VaultItemListingState.ItemListingType.updateWithAdditionalDataIfNecessary(
|
||||||
is VaultItemListingState.ItemListingType.Vault.Trash -> this
|
is VaultItemListingState.ItemListingType.Vault.Trash -> this
|
||||||
is VaultItemListingState.ItemListingType.Send.SendFile -> this
|
is VaultItemListingState.ItemListingType.Send.SendFile -> this
|
||||||
is VaultItemListingState.ItemListingType.Send.SendText -> this
|
is VaultItemListingState.ItemListingType.Send.SendText -> this
|
||||||
|
is VaultItemListingState.ItemListingType.Vault.SshKey -> this
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
|
@ -374,6 +379,7 @@ private fun CipherView.toIconTestTag(): String =
|
||||||
CipherType.SECURE_NOTE -> "SecureNoteCipherIcon"
|
CipherType.SECURE_NOTE -> "SecureNoteCipherIcon"
|
||||||
CipherType.CARD -> "CardCipherIcon"
|
CipherType.CARD -> "CardCipherIcon"
|
||||||
CipherType.IDENTITY -> "IdentityCipherIcon"
|
CipherType.IDENTITY -> "IdentityCipherIcon"
|
||||||
|
CipherType.SSH_KEY -> "SshKeyCipherIcon"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CipherView.toIconData(
|
private fun CipherView.toIconData(
|
||||||
|
@ -431,4 +437,5 @@ private val CipherType.iconRes: Int
|
||||||
CipherType.SECURE_NOTE -> R.drawable.ic_note
|
CipherType.SECURE_NOTE -> R.drawable.ic_note
|
||||||
CipherType.CARD -> R.drawable.ic_payment_card
|
CipherType.CARD -> R.drawable.ic_payment_card
|
||||||
CipherType.IDENTITY -> R.drawable.ic_id_card
|
CipherType.IDENTITY -> R.drawable.ic_id_card
|
||||||
|
CipherType.SSH_KEY -> R.drawable.ic_ssh_key
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ fun VaultItemListingState.ItemListingType.toSearchType(): SearchType =
|
||||||
is VaultItemListingState.ItemListingType.Vault.Identity -> SearchType.Vault.Identities
|
is VaultItemListingState.ItemListingType.Vault.Identity -> SearchType.Vault.Identities
|
||||||
is VaultItemListingState.ItemListingType.Vault.Login -> SearchType.Vault.Logins
|
is VaultItemListingState.ItemListingType.Vault.Login -> SearchType.Vault.Logins
|
||||||
is VaultItemListingState.ItemListingType.Vault.SecureNote -> SearchType.Vault.SecureNotes
|
is VaultItemListingState.ItemListingType.Vault.SecureNote -> SearchType.Vault.SecureNotes
|
||||||
|
is VaultItemListingState.ItemListingType.Vault.SshKey -> SearchType.Vault.SshKeys
|
||||||
is VaultItemListingState.ItemListingType.Vault.Trash -> SearchType.Vault.Trash
|
is VaultItemListingState.ItemListingType.Vault.Trash -> SearchType.Vault.Trash
|
||||||
is VaultItemListingState.ItemListingType.Vault.Collection -> {
|
is VaultItemListingState.ItemListingType.Vault.Collection -> {
|
||||||
SearchType.Vault.Collection(collectionId = collectionId)
|
SearchType.Vault.Collection(collectionId = collectionId)
|
||||||
|
@ -36,6 +37,7 @@ fun VaultItemListingState.ItemListingType.Vault.toVaultItemCipherType(): VaultIt
|
||||||
is VaultItemListingState.ItemListingType.Vault.Card -> VaultItemCipherType.CARD
|
is VaultItemListingState.ItemListingType.Vault.Card -> VaultItemCipherType.CARD
|
||||||
is VaultItemListingState.ItemListingType.Vault.Identity -> VaultItemCipherType.IDENTITY
|
is VaultItemListingState.ItemListingType.Vault.Identity -> VaultItemCipherType.IDENTITY
|
||||||
is VaultItemListingState.ItemListingType.Vault.SecureNote -> VaultItemCipherType.SECURE_NOTE
|
is VaultItemListingState.ItemListingType.Vault.SecureNote -> VaultItemCipherType.SECURE_NOTE
|
||||||
|
is VaultItemListingState.ItemListingType.Vault.SshKey -> VaultItemCipherType.SSH_KEY
|
||||||
is VaultItemListingState.ItemListingType.Vault.Login -> VaultItemCipherType.LOGIN
|
is VaultItemListingState.ItemListingType.Vault.Login -> VaultItemCipherType.LOGIN
|
||||||
is VaultItemListingState.ItemListingType.Vault.Collection -> VaultItemCipherType.LOGIN
|
is VaultItemListingState.ItemListingType.Vault.Collection -> VaultItemCipherType.LOGIN
|
||||||
is VaultItemListingState.ItemListingType.Vault.Trash,
|
is VaultItemListingState.ItemListingType.Vault.Trash,
|
||||||
|
|
|
@ -16,6 +16,7 @@ fun VaultItemListingType.toItemListingType(): VaultItemListingState.ItemListingT
|
||||||
is VaultItemListingType.Identity -> VaultItemListingState.ItemListingType.Vault.Identity
|
is VaultItemListingType.Identity -> VaultItemListingState.ItemListingType.Vault.Identity
|
||||||
is VaultItemListingType.Login -> VaultItemListingState.ItemListingType.Vault.Login
|
is VaultItemListingType.Login -> VaultItemListingState.ItemListingType.Vault.Login
|
||||||
is VaultItemListingType.SecureNote -> VaultItemListingState.ItemListingType.Vault.SecureNote
|
is VaultItemListingType.SecureNote -> VaultItemListingState.ItemListingType.Vault.SecureNote
|
||||||
|
is VaultItemListingType.SshKey -> VaultItemListingState.ItemListingType.Vault.SshKey
|
||||||
is VaultItemListingType.Trash -> VaultItemListingState.ItemListingType.Vault.Trash
|
is VaultItemListingType.Trash -> VaultItemListingState.ItemListingType.Vault.Trash
|
||||||
is VaultItemListingType.Collection -> {
|
is VaultItemListingType.Collection -> {
|
||||||
VaultItemListingState.ItemListingType.Vault.Collection(collectionId = collectionId)
|
VaultItemListingState.ItemListingType.Vault.Collection(collectionId = collectionId)
|
||||||
|
|
|
@ -31,6 +31,7 @@ fun VaultContent(
|
||||||
state: VaultState.ViewState.Content,
|
state: VaultState.ViewState.Content,
|
||||||
vaultHandlers: VaultHandlers,
|
vaultHandlers: VaultHandlers,
|
||||||
onOverflowOptionClick: (action: ListingItemOverflowAction.VaultAction) -> Unit,
|
onOverflowOptionClick: (action: ListingItemOverflowAction.VaultAction) -> Unit,
|
||||||
|
showSshKeys: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
@ -122,7 +123,7 @@ fun VaultContent(
|
||||||
item {
|
item {
|
||||||
BitwardenListHeaderText(
|
BitwardenListHeaderText(
|
||||||
label = stringResource(id = R.string.types),
|
label = stringResource(id = R.string.types),
|
||||||
supportingLabel = "4",
|
supportingLabel = state.itemTypesCount.toString(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
|
@ -193,6 +194,23 @@ fun VaultContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showSshKeys) {
|
||||||
|
item {
|
||||||
|
BitwardenGroupItem(
|
||||||
|
startIcon = rememberVectorPainter(id = R.drawable.ic_ssh_key),
|
||||||
|
startIconTestTag = "SshKeyCipherIcon",
|
||||||
|
label = stringResource(id = R.string.type_ssh_key),
|
||||||
|
supportingLabel = state.sshKeyItemsCount.toString(),
|
||||||
|
onClick = vaultHandlers.sshKeyGroupClick,
|
||||||
|
showDivider = false,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag("SshKeyFilter")
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (state.folderItems.isNotEmpty()) {
|
if (state.folderItems.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
BitwardenHorizontalDivider(
|
BitwardenHorizontalDivider(
|
||||||
|
|
|
@ -321,6 +321,7 @@ private fun VaultScreenScaffold(
|
||||||
when (val viewState = state.viewState) {
|
when (val viewState = state.viewState) {
|
||||||
is VaultState.ViewState.Content -> VaultContent(
|
is VaultState.ViewState.Content -> VaultContent(
|
||||||
state = viewState,
|
state = viewState,
|
||||||
|
showSshKeys = state.showSshKeys,
|
||||||
vaultHandlers = vaultHandlers,
|
vaultHandlers = vaultHandlers,
|
||||||
onOverflowOptionClick = { masterPasswordRepromptAction = it },
|
onOverflowOptionClick = { masterPasswordRepromptAction = it },
|
||||||
modifier = innerModifier,
|
modifier = innerModifier,
|
||||||
|
|
|
@ -89,6 +89,7 @@ class VaultViewModel @Inject constructor(
|
||||||
.any(),
|
.any(),
|
||||||
)
|
)
|
||||||
val appBarTitle = vaultFilterData.toAppBarTitle()
|
val appBarTitle = vaultFilterData.toAppBarTitle()
|
||||||
|
val showSshKeys = featureFlagManager.getFeatureFlag(FlagKey.SshKeyCipherItems)
|
||||||
VaultState(
|
VaultState(
|
||||||
appBarTitle = appBarTitle,
|
appBarTitle = appBarTitle,
|
||||||
initials = activeAccountSummary.initials,
|
initials = activeAccountSummary.initials,
|
||||||
|
@ -104,6 +105,7 @@ class VaultViewModel @Inject constructor(
|
||||||
hideNotificationsDialog = isBuildVersionBelow(Build.VERSION_CODES.TIRAMISU) || isFdroid,
|
hideNotificationsDialog = isBuildVersionBelow(Build.VERSION_CODES.TIRAMISU) || isFdroid,
|
||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
showImportActionCard = false,
|
showImportActionCard = false,
|
||||||
|
showSshKeys = showSshKeys,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
@ -131,9 +133,16 @@ class VaultViewModel @Inject constructor(
|
||||||
.onEach(::sendAction)
|
.onEach(::sendAction)
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
|
|
||||||
vaultRepository
|
combine(
|
||||||
.vaultDataStateFlow
|
vaultRepository.vaultDataStateFlow,
|
||||||
.onEach { sendAction(VaultAction.Internal.VaultDataReceive(vaultData = it)) }
|
featureFlagManager.getFeatureFlagFlow(FlagKey.SshKeyCipherItems),
|
||||||
|
) { vaultData, sshKeyCipherItemsEnabled ->
|
||||||
|
VaultAction.Internal.VaultDataReceive(
|
||||||
|
vaultData = vaultData,
|
||||||
|
showSshKeys = sshKeyCipherItemsEnabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onEach(::sendAction)
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
|
|
||||||
authRepository
|
authRepository
|
||||||
|
@ -177,6 +186,7 @@ class VaultViewModel @Inject constructor(
|
||||||
is VaultAction.ExitConfirmationClick -> handleExitConfirmationClick()
|
is VaultAction.ExitConfirmationClick -> handleExitConfirmationClick()
|
||||||
is VaultAction.VaultFilterTypeSelect -> handleVaultFilterTypeSelect(action)
|
is VaultAction.VaultFilterTypeSelect -> handleVaultFilterTypeSelect(action)
|
||||||
is VaultAction.SecureNoteGroupClick -> handleSecureNoteClick()
|
is VaultAction.SecureNoteGroupClick -> handleSecureNoteClick()
|
||||||
|
is VaultAction.SshKeyGroupClick -> handleSshKeyClick()
|
||||||
is VaultAction.TrashClick -> handleTrashClick()
|
is VaultAction.TrashClick -> handleTrashClick()
|
||||||
is VaultAction.VaultItemClick -> handleVaultItemClick(action)
|
is VaultAction.VaultItemClick -> handleVaultItemClick(action)
|
||||||
is VaultAction.TryAgainClick -> handleTryAgainClick()
|
is VaultAction.TryAgainClick -> handleTryAgainClick()
|
||||||
|
@ -211,7 +221,10 @@ class VaultViewModel @Inject constructor(
|
||||||
it.copy(isIconLoadingDisabled = action.isIconLoadingDisabled)
|
it.copy(isIconLoadingDisabled = action.isIconLoadingDisabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateViewState(vaultRepository.vaultDataStateFlow.value)
|
updateViewState(
|
||||||
|
vaultData = vaultRepository.vaultDataStateFlow.value,
|
||||||
|
showSshKeys = state.showSshKeys,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
//region VaultAction Handlers
|
//region VaultAction Handlers
|
||||||
|
@ -311,7 +324,10 @@ class VaultViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-process the current vault data with the new filter
|
// Re-process the current vault data with the new filter
|
||||||
updateViewState(vaultData = vaultRepository.vaultDataStateFlow.value)
|
updateViewState(
|
||||||
|
vaultData = vaultRepository.vaultDataStateFlow.value,
|
||||||
|
showSshKeys = state.showSshKeys,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleTrashClick() {
|
private fun handleTrashClick() {
|
||||||
|
@ -322,6 +338,10 @@ class VaultViewModel @Inject constructor(
|
||||||
sendEvent(VaultEvent.NavigateToItemListing(VaultItemListingType.SecureNote))
|
sendEvent(VaultEvent.NavigateToItemListing(VaultItemListingType.SecureNote))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleSshKeyClick() {
|
||||||
|
sendEvent(VaultEvent.NavigateToItemListing(VaultItemListingType.SshKey))
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleVaultItemClick(action: VaultAction.VaultItemClick) {
|
private fun handleVaultItemClick(action: VaultAction.VaultItemClick) {
|
||||||
sendEvent(VaultEvent.NavigateToVaultItem(action.vaultItem.id))
|
sendEvent(VaultEvent.NavigateToVaultItem(action.vaultItem.id))
|
||||||
}
|
}
|
||||||
|
@ -517,6 +537,7 @@ class VaultViewModel @Inject constructor(
|
||||||
val appBarTitle = vaultFilterData.toAppBarTitle()
|
val appBarTitle = vaultFilterData.toAppBarTitle()
|
||||||
val shouldShowImportActionCard = action.importLoginsFlowEnabled &&
|
val shouldShowImportActionCard = action.importLoginsFlowEnabled &&
|
||||||
firstTimeState.showImportLoginsCard
|
firstTimeState.showImportLoginsCard
|
||||||
|
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
val accountSummaries = userState.toAccountSummaries()
|
val accountSummaries = userState.toAccountSummaries()
|
||||||
val activeAccountSummary = userState.toActiveAccountSummary()
|
val activeAccountSummary = userState.toActiveAccountSummary()
|
||||||
|
@ -537,13 +558,20 @@ class VaultViewModel @Inject constructor(
|
||||||
// navigating.
|
// navigating.
|
||||||
if (state.isSwitchingAccounts) return
|
if (state.isSwitchingAccounts) return
|
||||||
|
|
||||||
updateViewState(vaultData = action.vaultData)
|
updateViewState(
|
||||||
|
vaultData = action.vaultData,
|
||||||
|
showSshKeys = action.showSshKeys,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateViewState(vaultData: DataState<VaultData>) {
|
private fun updateViewState(vaultData: DataState<VaultData>, showSshKeys: Boolean) {
|
||||||
when (vaultData) {
|
when (vaultData) {
|
||||||
is DataState.Error -> vaultErrorReceive(vaultData = vaultData)
|
is DataState.Error -> vaultErrorReceive(vaultData = vaultData)
|
||||||
is DataState.Loaded -> vaultLoadedReceive(vaultData = vaultData)
|
is DataState.Loaded -> vaultLoadedReceive(
|
||||||
|
vaultData = vaultData,
|
||||||
|
showSshKeys = showSshKeys,
|
||||||
|
)
|
||||||
|
|
||||||
is DataState.Loading -> vaultLoadingReceive()
|
is DataState.Loading -> vaultLoadingReceive()
|
||||||
is DataState.NoNetwork -> vaultNoNetworkReceive(vaultData = vaultData)
|
is DataState.NoNetwork -> vaultNoNetworkReceive(vaultData = vaultData)
|
||||||
is DataState.Pending -> vaultPendingReceive(vaultData = vaultData)
|
is DataState.Pending -> vaultPendingReceive(vaultData = vaultData)
|
||||||
|
@ -564,7 +592,7 @@ class VaultViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun vaultLoadedReceive(vaultData: DataState.Loaded<VaultData>) {
|
private fun vaultLoadedReceive(vaultData: DataState.Loaded<VaultData>, showSshKeys: Boolean) {
|
||||||
if (state.dialog == VaultState.DialogState.Syncing) {
|
if (state.dialog == VaultState.DialogState.Syncing) {
|
||||||
sendEvent(
|
sendEvent(
|
||||||
VaultEvent.ShowToast(
|
VaultEvent.ShowToast(
|
||||||
|
@ -580,9 +608,11 @@ class VaultViewModel @Inject constructor(
|
||||||
isPremium = state.isPremium,
|
isPremium = state.isPremium,
|
||||||
hasMasterPassword = state.hasMasterPassword,
|
hasMasterPassword = state.hasMasterPassword,
|
||||||
vaultFilterType = vaultFilterTypeOrDefault,
|
vaultFilterType = vaultFilterTypeOrDefault,
|
||||||
|
showSshKeys = showSshKeys,
|
||||||
),
|
),
|
||||||
dialog = null,
|
dialog = null,
|
||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
|
showSshKeys = showSshKeys,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -614,6 +644,7 @@ class VaultViewModel @Inject constructor(
|
||||||
isPremium = state.isPremium,
|
isPremium = state.isPremium,
|
||||||
hasMasterPassword = state.hasMasterPassword,
|
hasMasterPassword = state.hasMasterPassword,
|
||||||
vaultFilterType = vaultFilterTypeOrDefault,
|
vaultFilterType = vaultFilterTypeOrDefault,
|
||||||
|
showSshKeys = state.showSshKeys,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -685,6 +716,7 @@ data class VaultState(
|
||||||
val hideNotificationsDialog: Boolean,
|
val hideNotificationsDialog: Boolean,
|
||||||
val isRefreshing: Boolean,
|
val isRefreshing: Boolean,
|
||||||
val showImportActionCard: Boolean,
|
val showImportActionCard: Boolean,
|
||||||
|
val showSshKeys: Boolean,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -767,11 +799,13 @@ data class VaultState(
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Content(
|
data class Content(
|
||||||
|
val itemTypesCount: Int,
|
||||||
val totpItemsCount: Int,
|
val totpItemsCount: Int,
|
||||||
val loginItemsCount: Int,
|
val loginItemsCount: Int,
|
||||||
val cardItemsCount: Int,
|
val cardItemsCount: Int,
|
||||||
val identityItemsCount: Int,
|
val identityItemsCount: Int,
|
||||||
val secureNoteItemsCount: Int,
|
val secureNoteItemsCount: Int,
|
||||||
|
val sshKeyItemsCount: Int,
|
||||||
val favoriteItems: List<VaultItem>,
|
val favoriteItems: List<VaultItem>,
|
||||||
val folderItems: List<FolderItem>,
|
val folderItems: List<FolderItem>,
|
||||||
val noFolderItems: List<VaultItem>,
|
val noFolderItems: List<VaultItem>,
|
||||||
|
@ -944,6 +978,29 @@ data class VaultState(
|
||||||
) : VaultItem() {
|
) : VaultItem() {
|
||||||
override val supportingLabel: Text? get() = null
|
override val supportingLabel: Text? get() = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a SSH key item within the vault, designed to store SSH keys.
|
||||||
|
*
|
||||||
|
* @property publicKey The public key associated with this SSH key item.
|
||||||
|
* @property privateKey The private key associated with this SSH key item.
|
||||||
|
* @property fingerprint The fingerprint associated with this SSH key item.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class SshKey(
|
||||||
|
override val id: String,
|
||||||
|
override val name: Text,
|
||||||
|
override val startIcon: IconData = IconData.Local(R.drawable.ic_ssh_key),
|
||||||
|
override val startIconTestTag: String = "SshKeyCipherIcon",
|
||||||
|
override val extraIconList: List<IconRes> = emptyList(),
|
||||||
|
override val overflowOptions: List<ListingItemOverflowAction.VaultAction>,
|
||||||
|
override val shouldShowMasterPasswordReprompt: Boolean,
|
||||||
|
val publicKey: Text?,
|
||||||
|
val privateKey: Text?,
|
||||||
|
val fingerprint: Text?,
|
||||||
|
) : VaultItem() {
|
||||||
|
override val supportingLabel: Text? get() = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1154,6 +1211,11 @@ sealed class VaultAction {
|
||||||
*/
|
*/
|
||||||
data object SecureNoteGroupClick : VaultAction()
|
data object SecureNoteGroupClick : VaultAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User clicked the SSH key types button.
|
||||||
|
*/
|
||||||
|
data object SshKeyGroupClick : VaultAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User clicked the trash button.
|
* User clicked the trash button.
|
||||||
*/
|
*/
|
||||||
|
@ -1232,6 +1294,7 @@ sealed class VaultAction {
|
||||||
*/
|
*/
|
||||||
data class VaultDataReceive(
|
data class VaultDataReceive(
|
||||||
val vaultData: DataState<VaultData>,
|
val vaultData: DataState<VaultData>,
|
||||||
|
val showSshKeys: Boolean,
|
||||||
) : Internal()
|
) : Internal()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1272,6 +1335,7 @@ private fun MutableStateFlow<VaultState>.updateToErrorStateOrDialog(
|
||||||
hasMasterPassword = hasMasterPassword,
|
hasMasterPassword = hasMasterPassword,
|
||||||
vaultFilterType = vaultFilterType,
|
vaultFilterType = vaultFilterType,
|
||||||
isIconLoadingDisabled = isIconLoadingDisabled,
|
isIconLoadingDisabled = isIconLoadingDisabled,
|
||||||
|
showSshKeys = it.showSshKeys,
|
||||||
),
|
),
|
||||||
dialog = VaultState.DialogState.Error(
|
dialog = VaultState.DialogState.Error(
|
||||||
title = errorTitle,
|
title = errorTitle,
|
||||||
|
|
|
@ -29,6 +29,7 @@ data class VaultHandlers(
|
||||||
val cardGroupClick: () -> Unit,
|
val cardGroupClick: () -> Unit,
|
||||||
val identityGroupClick: () -> Unit,
|
val identityGroupClick: () -> Unit,
|
||||||
val secureNoteGroupClick: () -> Unit,
|
val secureNoteGroupClick: () -> Unit,
|
||||||
|
val sshKeyGroupClick: () -> Unit,
|
||||||
val trashClick: () -> Unit,
|
val trashClick: () -> Unit,
|
||||||
val tryAgainClick: () -> Unit,
|
val tryAgainClick: () -> Unit,
|
||||||
val dialogDismiss: () -> Unit,
|
val dialogDismiss: () -> Unit,
|
||||||
|
@ -77,6 +78,7 @@ data class VaultHandlers(
|
||||||
secureNoteGroupClick = {
|
secureNoteGroupClick = {
|
||||||
viewModel.trySendAction(VaultAction.SecureNoteGroupClick)
|
viewModel.trySendAction(VaultAction.SecureNoteGroupClick)
|
||||||
},
|
},
|
||||||
|
sshKeyGroupClick = { viewModel.trySendAction(VaultAction.SshKeyGroupClick) },
|
||||||
trashClick = { viewModel.trySendAction(VaultAction.TrashClick) },
|
trashClick = { viewModel.trySendAction(VaultAction.TrashClick) },
|
||||||
tryAgainClick = { viewModel.trySendAction(VaultAction.TryAgainClick) },
|
tryAgainClick = { viewModel.trySendAction(VaultAction.TryAgainClick) },
|
||||||
dialogDismiss = { viewModel.trySendAction(VaultAction.DialogDismiss) },
|
dialogDismiss = { viewModel.trySendAction(VaultAction.DialogDismiss) },
|
||||||
|
|
|
@ -15,6 +15,7 @@ import com.bitwarden.vault.LoginView
|
||||||
import com.bitwarden.vault.PasswordHistoryView
|
import com.bitwarden.vault.PasswordHistoryView
|
||||||
import com.bitwarden.vault.SecureNoteType
|
import com.bitwarden.vault.SecureNoteType
|
||||||
import com.bitwarden.vault.SecureNoteView
|
import com.bitwarden.vault.SecureNoteView
|
||||||
|
import com.bitwarden.vault.SshKeyView
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
|
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
|
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.feature.addedit.model.UriItem
|
||||||
|
@ -49,6 +50,7 @@ fun VaultAddEditState.ViewState.Content.toCipherView(): CipherView =
|
||||||
secureNote = type.toSecureNotesView(),
|
secureNote = type.toSecureNotesView(),
|
||||||
login = type.toLoginView(common = common),
|
login = type.toLoginView(common = common),
|
||||||
card = type.toCardView(),
|
card = type.toCardView(),
|
||||||
|
sshKey = type.toSshKeyView(),
|
||||||
|
|
||||||
// Fields we always grab from the UI
|
// Fields we always grab from the UI
|
||||||
name = common.name,
|
name = common.name,
|
||||||
|
@ -66,6 +68,16 @@ private fun VaultAddEditState.ViewState.Content.ItemType.toCipherType(): CipherT
|
||||||
is VaultAddEditState.ViewState.Content.ItemType.Identity -> CipherType.IDENTITY
|
is VaultAddEditState.ViewState.Content.ItemType.Identity -> CipherType.IDENTITY
|
||||||
is VaultAddEditState.ViewState.Content.ItemType.Login -> CipherType.LOGIN
|
is VaultAddEditState.ViewState.Content.ItemType.Login -> CipherType.LOGIN
|
||||||
is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> CipherType.SECURE_NOTE
|
is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> CipherType.SECURE_NOTE
|
||||||
|
is VaultAddEditState.ViewState.Content.ItemType.SshKey -> CipherType.SSH_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun VaultAddEditState.ViewState.Content.ItemType.toSshKeyView(): SshKeyView? =
|
||||||
|
(this as? VaultAddEditState.ViewState.Content.ItemType.SshKey)?.let {
|
||||||
|
SshKeyView(
|
||||||
|
publicKey = it.publicKey.orNullIfBlank(),
|
||||||
|
privateKey = it.privateKey.orNullIfBlank(),
|
||||||
|
fingerprint = it.fingerprint.orNullIfBlank(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun VaultAddEditState.ViewState.Content.ItemType.toCardView(): CardView? =
|
private fun VaultAddEditState.ViewState.Content.ItemType.toCardView(): CardView? =
|
||||||
|
|
|
@ -32,13 +32,14 @@ private const val NO_FOLDER_ITEM_THRESHOLD: Int = 100
|
||||||
/**
|
/**
|
||||||
* Transforms [VaultData] into [VaultState.ViewState] using the given [vaultFilterType].
|
* Transforms [VaultData] into [VaultState.ViewState] using the given [vaultFilterType].
|
||||||
*/
|
*/
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod", "LongParameterList")
|
||||||
fun VaultData.toViewState(
|
fun VaultData.toViewState(
|
||||||
isPremium: Boolean,
|
isPremium: Boolean,
|
||||||
hasMasterPassword: Boolean,
|
hasMasterPassword: Boolean,
|
||||||
isIconLoadingDisabled: Boolean,
|
isIconLoadingDisabled: Boolean,
|
||||||
baseIconUrl: String,
|
baseIconUrl: String,
|
||||||
vaultFilterType: VaultFilterType,
|
vaultFilterType: VaultFilterType,
|
||||||
|
showSshKeys: Boolean,
|
||||||
): VaultState.ViewState {
|
): VaultState.ViewState {
|
||||||
|
|
||||||
val filteredCipherViewListWithDeletedItems =
|
val filteredCipherViewListWithDeletedItems =
|
||||||
|
@ -46,6 +47,7 @@ fun VaultData.toViewState(
|
||||||
|
|
||||||
val filteredCipherViewList = filteredCipherViewListWithDeletedItems
|
val filteredCipherViewList = filteredCipherViewListWithDeletedItems
|
||||||
.filter { it.deletedDate == null }
|
.filter { it.deletedDate == null }
|
||||||
|
.filterSshKeysIfNecessary(showSshKeys)
|
||||||
|
|
||||||
val filteredFolderViewList = folderViewList
|
val filteredFolderViewList = folderViewList
|
||||||
.toFilteredList(
|
.toFilteredList(
|
||||||
|
@ -61,11 +63,19 @@ fun VaultData.toViewState(
|
||||||
val noFolderItems = filteredCipherViewList
|
val noFolderItems = filteredCipherViewList
|
||||||
.filter { it.folderId.isNullOrBlank() }
|
.filter { it.folderId.isNullOrBlank() }
|
||||||
|
|
||||||
|
val itemTypesCount: Int = if (showSshKeys) {
|
||||||
|
CipherType.entries
|
||||||
|
} else {
|
||||||
|
CipherType.entries.filterNot { it == CipherType.SSH_KEY }
|
||||||
|
}
|
||||||
|
.size
|
||||||
|
|
||||||
return if (filteredCipherViewListWithDeletedItems.isEmpty()) {
|
return if (filteredCipherViewListWithDeletedItems.isEmpty()) {
|
||||||
VaultState.ViewState.NoItems
|
VaultState.ViewState.NoItems
|
||||||
} else {
|
} else {
|
||||||
val totpItems = filteredCipherViewList.filter { it.login?.totp != null }
|
val totpItems = filteredCipherViewList.filter { it.login?.totp != null }
|
||||||
VaultState.ViewState.Content(
|
VaultState.ViewState.Content(
|
||||||
|
itemTypesCount = itemTypesCount,
|
||||||
totpItemsCount = if (isPremium) {
|
totpItemsCount = if (isPremium) {
|
||||||
totpItems.count()
|
totpItems.count()
|
||||||
} else {
|
} else {
|
||||||
|
@ -76,6 +86,7 @@ fun VaultData.toViewState(
|
||||||
identityItemsCount = filteredCipherViewList.count { it.type == CipherType.IDENTITY },
|
identityItemsCount = filteredCipherViewList.count { it.type == CipherType.IDENTITY },
|
||||||
secureNoteItemsCount = filteredCipherViewList
|
secureNoteItemsCount = filteredCipherViewList
|
||||||
.count { it.type == CipherType.SECURE_NOTE },
|
.count { it.type == CipherType.SECURE_NOTE },
|
||||||
|
sshKeyItemsCount = filteredCipherViewList.count { it.type == CipherType.SSH_KEY },
|
||||||
favoriteItems = filteredCipherViewList
|
favoriteItems = filteredCipherViewList
|
||||||
.filter { it.favorite }
|
.filter { it.favorite }
|
||||||
.mapNotNull {
|
.mapNotNull {
|
||||||
|
@ -259,6 +270,26 @@ private fun CipherView.toVaultItemOrNull(
|
||||||
extraIconList = toLabelIcons(),
|
extraIconList = toLabelIcons(),
|
||||||
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
|
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CipherType.SSH_KEY -> VaultState.ViewState.VaultItem.SshKey(
|
||||||
|
id = id,
|
||||||
|
name = name.asText(),
|
||||||
|
publicKey = sshKey
|
||||||
|
?.publicKey
|
||||||
|
?.asText(),
|
||||||
|
privateKey = sshKey
|
||||||
|
?.privateKey
|
||||||
|
?.asText(),
|
||||||
|
fingerprint = sshKey
|
||||||
|
?.fingerprint
|
||||||
|
?.asText(),
|
||||||
|
overflowOptions = toOverflowActions(
|
||||||
|
hasMasterPassword = hasMasterPassword,
|
||||||
|
isPremiumUser = isPremiumUser,
|
||||||
|
),
|
||||||
|
extraIconList = toLabelIcons(),
|
||||||
|
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,3 +352,16 @@ fun List<CollectionView>.toFilteredList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters out all [CipherView]s that are of type [CipherType.SSH_KEY] if [showSshKeys] is false.
|
||||||
|
*
|
||||||
|
* @param showSshKeys Whether to show SSH keys in the vault.
|
||||||
|
*/
|
||||||
|
@JvmName("filterSshKeysIfNecessary")
|
||||||
|
fun List<CipherView>.filterSshKeysIfNecessary(showSshKeys: Boolean): List<CipherView> =
|
||||||
|
if (showSshKeys) {
|
||||||
|
this
|
||||||
|
} else {
|
||||||
|
filter { it.type != CipherType.SSH_KEY }
|
||||||
|
}
|
||||||
|
|
|
@ -24,4 +24,9 @@ enum class VaultItemCipherType {
|
||||||
* A secure note cipher.
|
* A secure note cipher.
|
||||||
*/
|
*/
|
||||||
SECURE_NOTE,
|
SECURE_NOTE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A SSH key cipher.
|
||||||
|
*/
|
||||||
|
SSH_KEY,
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,11 @@ sealed class VaultItemListingType {
|
||||||
*/
|
*/
|
||||||
data object Card : VaultItemListingType()
|
data object Card : VaultItemListingType()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A SSH key listing.
|
||||||
|
*/
|
||||||
|
data object SshKey : VaultItemListingType()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Trash listing.
|
* A Trash listing.
|
||||||
*/
|
*/
|
||||||
|
|
14
app/src/main/res/drawable/ic_ssh_key.xml
Normal file
14
app/src/main/res/drawable/ic_ssh_key.xml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M14.838,11.797L15.439,11.916C16.826,12.191 18.316,11.79 19.387,10.719C21.096,9.01 21.096,6.24 19.387,4.531C17.679,2.823 14.909,2.823 13.2,4.531C12.128,5.603 11.728,7.092 12.003,8.48L12.136,9.153L4.375,16.574L3.601,19.67H6.024L6.649,17.17H8.524L9.149,14.67H11.533L14.838,11.797ZM12,15.92H10.125L9.5,18.42H7.625L7,20.92H2L3.25,15.92L10.776,8.723C10.424,6.943 10.937,5.027 12.316,3.648C14.513,1.451 18.075,1.451 20.271,3.648C22.468,5.844 22.468,9.406 20.271,11.602C18.892,12.981 16.975,13.495 15.196,13.142L12,15.92Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M16.736,6.299C16.492,6.543 16.492,6.939 16.736,7.183C16.98,7.427 17.376,7.427 17.62,7.183C17.864,6.939 17.864,6.543 17.62,6.299C17.376,6.055 16.98,6.055 16.736,6.299ZM18.504,5.415C19.236,6.148 19.236,7.335 18.504,8.067C17.771,8.799 16.584,8.799 15.852,8.067C15.12,7.335 15.12,6.148 15.852,5.415C16.584,4.683 17.771,4.683 18.504,5.415Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
|
@ -1075,4 +1075,8 @@ Do you want to switch to this account?</string>
|
||||||
<string name="no_logins_were_imported">No logins were imported</string>
|
<string name="no_logins_were_imported">No logins were imported</string>
|
||||||
<string name="logins_imported">Logins imported</string>
|
<string name="logins_imported">Logins imported</string>
|
||||||
<string name="remember_to_delete_your_imported_password_file_from_your_computer">Remember to delete your imported password file from your computer</string>
|
<string name="remember_to_delete_your_imported_password_file_from_your_computer">Remember to delete your imported password file from your computer</string>
|
||||||
|
<string name="type_ssh_key">SSH key</string>
|
||||||
|
<string name="public_key">Public key</string>
|
||||||
|
<string name="private_key">Private key</string>
|
||||||
|
<string name="ssh_keys">SSH keys</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -310,6 +310,18 @@ class CipherViewExtensionsTest {
|
||||||
assertEquals(expected, actual)
|
assertEquals(expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `subtitle should return null when type is IDENTITY and identity is null`() {
|
||||||
|
val cipherView: CipherView = mockk {
|
||||||
|
every { identity } returns null
|
||||||
|
every { type } returns CipherType.IDENTITY
|
||||||
|
}
|
||||||
|
|
||||||
|
val actual = cipherView.subtitle
|
||||||
|
|
||||||
|
assertNull(actual)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `subtitle should return null when type is IDENTITY and first and last name are null`() {
|
fun `subtitle should return null when type is IDENTITY and first and last name are null`() {
|
||||||
|
@ -329,4 +341,15 @@ class CipherViewExtensionsTest {
|
||||||
// Verify
|
// Verify
|
||||||
assertNull(actual)
|
assertNull(actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `subtitle should return null when type is SSH_KEY`() {
|
||||||
|
val cipherView: CipherView = mockk {
|
||||||
|
every { type } returns CipherType.SSH_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
val actual = cipherView.subtitle
|
||||||
|
|
||||||
|
assertNull(actual)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -407,7 +407,12 @@ private const val CIPHER_JSON = """
|
||||||
"cardholderName": "mockCardholderName-1",
|
"cardholderName": "mockCardholderName-1",
|
||||||
"brand": "mockBrand-1"
|
"brand": "mockBrand-1"
|
||||||
},
|
},
|
||||||
"key": "mockKey-1"
|
"key": "mockKey-1",
|
||||||
|
"sshKey": {
|
||||||
|
"publicKey": "mockPublicKey-1",
|
||||||
|
"privateKey": "mockPrivateKey-1",
|
||||||
|
"keyFingerprint": "mockKeyFingerprint-1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ fun createMockCipherJsonRequest(number: Int, hasNullUri: Boolean = false): Ciphe
|
||||||
type = CipherTypeJson.LOGIN,
|
type = CipherTypeJson.LOGIN,
|
||||||
login = createMockLogin(number = number, hasNullUri = hasNullUri),
|
login = createMockLogin(number = number, hasNullUri = hasNullUri),
|
||||||
card = createMockCard(number = number),
|
card = createMockCard(number = number),
|
||||||
|
sshKey = createMockSshKey(number = number),
|
||||||
fields = listOf(createMockField(number = number)),
|
fields = listOf(createMockField(number = number)),
|
||||||
identity = createMockIdentity(number = number),
|
identity = createMockIdentity(number = number),
|
||||||
isFavorite = false,
|
isFavorite = false,
|
||||||
|
|
|
@ -38,6 +38,7 @@ fun createMockCipher(
|
||||||
card = createMockCard(number = number),
|
card = createMockCard(number = number),
|
||||||
fields = listOf(createMockField(number = number)),
|
fields = listOf(createMockField(number = number)),
|
||||||
identity = createMockIdentity(number = number),
|
identity = createMockIdentity(number = number),
|
||||||
|
sshKey = createMockSshKey(number = number),
|
||||||
isFavorite = false,
|
isFavorite = false,
|
||||||
passwordHistory = listOf(createMockPasswordHistory(number = number)),
|
passwordHistory = listOf(createMockPasswordHistory(number = number)),
|
||||||
reprompt = CipherRepromptTypeJson.NONE,
|
reprompt = CipherRepromptTypeJson.NONE,
|
||||||
|
@ -148,6 +149,18 @@ fun createMockLogin(
|
||||||
fido2Credentials = fido2Credentials,
|
fido2Credentials = fido2Credentials,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock [SyncResponseJson.Cipher.SshKey] with a given [number].
|
||||||
|
*/
|
||||||
|
fun createMockSshKey(number: Int) = SyncResponseJson.Cipher.SshKey(
|
||||||
|
publicKey = "mockPublicKey-$number",
|
||||||
|
privateKey = "mockPrivateKey-$number",
|
||||||
|
keyFingerprint = "mockKeyFingerprint-$number",
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock [SyncResponseJson.Cipher.Fido2Credential] with a given [number].
|
||||||
|
*/
|
||||||
fun createMockFido2Credential(number: Int) = SyncResponseJson.Cipher.Fido2Credential(
|
fun createMockFido2Credential(number: Int) = SyncResponseJson.Cipher.Fido2Credential(
|
||||||
credentialId = "mockCredentialId-$number",
|
credentialId = "mockCredentialId-$number",
|
||||||
keyType = "mockKeyType-$number",
|
keyType = "mockKeyType-$number",
|
||||||
|
|
|
@ -465,7 +465,12 @@ private const val CREATE_ATTACHMENT_SUCCESS_JSON = """
|
||||||
"cardholderName": "mockCardholderName-1",
|
"cardholderName": "mockCardholderName-1",
|
||||||
"brand": "mockBrand-1"
|
"brand": "mockBrand-1"
|
||||||
},
|
},
|
||||||
"key": "mockKey-1"
|
"key": "mockKey-1",
|
||||||
|
"sshKey": {
|
||||||
|
"publicKey": "mockPublicKey-1",
|
||||||
|
"privateKey": "mockPrivateKey-1",
|
||||||
|
"keyFingerprint": "mockKeyFingerprint-1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
@ -576,7 +581,12 @@ private const val CREATE_RESTORE_UPDATE_CIPHER_SUCCESS_JSON = """
|
||||||
"cardholderName": "mockCardholderName-1",
|
"cardholderName": "mockCardholderName-1",
|
||||||
"brand": "mockBrand-1"
|
"brand": "mockBrand-1"
|
||||||
},
|
},
|
||||||
"key": "mockKey-1"
|
"key": "mockKey-1",
|
||||||
|
"sshKey": {
|
||||||
|
"publicKey": "mockPublicKey-1",
|
||||||
|
"privateKey": "mockPrivateKey-1",
|
||||||
|
"keyFingerprint": "mockKeyFingerprint-1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -302,7 +302,12 @@ private const val SYNC_SUCCESS_JSON = """
|
||||||
"cardholderName": "mockCardholderName-1",
|
"cardholderName": "mockCardholderName-1",
|
||||||
"brand": "mockBrand-1"
|
"brand": "mockBrand-1"
|
||||||
},
|
},
|
||||||
"key": "mockKey-1"
|
"key": "mockKey-1",
|
||||||
|
"sshKey": {
|
||||||
|
"publicKey": "mockPublicKey-1",
|
||||||
|
"privateKey": "mockPrivateKey-1",
|
||||||
|
"keyFingerprint": "mockKeyFingerprint-1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"domains": {
|
"domains": {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import com.bitwarden.vault.LoginView
|
||||||
import com.bitwarden.vault.PasswordHistoryView
|
import com.bitwarden.vault.PasswordHistoryView
|
||||||
import com.bitwarden.vault.SecureNoteType
|
import com.bitwarden.vault.SecureNoteType
|
||||||
import com.bitwarden.vault.SecureNoteView
|
import com.bitwarden.vault.SecureNoteView
|
||||||
|
import com.bitwarden.vault.SshKeyView
|
||||||
import com.bitwarden.vault.UriMatchType
|
import com.bitwarden.vault.UriMatchType
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
@ -47,6 +48,7 @@ fun createMockCipherView(
|
||||||
folderId: String? = "mockId-$number",
|
folderId: String? = "mockId-$number",
|
||||||
clock: Clock = FIXED_CLOCK,
|
clock: Clock = FIXED_CLOCK,
|
||||||
fido2Credentials: List<Fido2Credential>? = null,
|
fido2Credentials: List<Fido2Credential>? = null,
|
||||||
|
sshKey: SshKeyView? = createMockSshKeyView(number = number),
|
||||||
): CipherView =
|
): CipherView =
|
||||||
CipherView(
|
CipherView(
|
||||||
id = "mockId-$number",
|
id = "mockId-$number",
|
||||||
|
@ -77,6 +79,7 @@ fun createMockCipherView(
|
||||||
identity = createMockIdentityView(number = number).takeIf {
|
identity = createMockIdentityView(number = number).takeIf {
|
||||||
cipherType == CipherType.IDENTITY
|
cipherType == CipherType.IDENTITY
|
||||||
},
|
},
|
||||||
|
sshKey = sshKey.takeIf { cipherType == CipherType.SSH_KEY },
|
||||||
favorite = false,
|
favorite = false,
|
||||||
passwordHistory = listOf(createMockPasswordHistoryView(number = number, clock)),
|
passwordHistory = listOf(createMockPasswordHistoryView(number = number, clock)),
|
||||||
reprompt = repromptType,
|
reprompt = repromptType,
|
||||||
|
@ -223,6 +226,16 @@ fun createMockIdentityView(number: Int): IdentityView =
|
||||||
username = "mockUsername-$number",
|
username = "mockUsername-$number",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock [SshKeyView] with a given [number].
|
||||||
|
*/
|
||||||
|
fun createMockSshKeyView(number: Int): SshKeyView =
|
||||||
|
SshKeyView(
|
||||||
|
publicKey = "mockPublicKey-$number",
|
||||||
|
privateKey = "mockPrivateKey-$number",
|
||||||
|
fingerprint = "mockKeyFingerprint-$number",
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a mock [PasswordHistoryView] with a given [number].
|
* Create a mock [PasswordHistoryView] with a given [number].
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -13,6 +13,7 @@ import com.bitwarden.vault.LoginUri
|
||||||
import com.bitwarden.vault.PasswordHistory
|
import com.bitwarden.vault.PasswordHistory
|
||||||
import com.bitwarden.vault.SecureNote
|
import com.bitwarden.vault.SecureNote
|
||||||
import com.bitwarden.vault.SecureNoteType
|
import com.bitwarden.vault.SecureNoteType
|
||||||
|
import com.bitwarden.vault.SshKey
|
||||||
import com.bitwarden.vault.UriMatchType
|
import com.bitwarden.vault.UriMatchType
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
@ -49,6 +50,7 @@ fun createMockSdkCipher(number: Int, clock: Clock = FIXED_CLOCK): Cipher =
|
||||||
card = createMockSdkCard(number = number),
|
card = createMockSdkCard(number = number),
|
||||||
fields = listOf(createMockSdkField(number = number)),
|
fields = listOf(createMockSdkField(number = number)),
|
||||||
identity = createMockSdkIdentity(number = number),
|
identity = createMockSdkIdentity(number = number),
|
||||||
|
sshKey = createMockSdkSshKey(number = number),
|
||||||
favorite = false,
|
favorite = false,
|
||||||
passwordHistory = listOf(createMockSdkPasswordHistory(number = number, clock = clock)),
|
passwordHistory = listOf(createMockSdkPasswordHistory(number = number, clock = clock)),
|
||||||
reprompt = CipherRepromptType.NONE,
|
reprompt = CipherRepromptType.NONE,
|
||||||
|
@ -101,6 +103,16 @@ fun createMockSdkIdentity(number: Int): Identity =
|
||||||
username = "mockUsername-$number",
|
username = "mockUsername-$number",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock [SshKey] with a given [number].
|
||||||
|
*/
|
||||||
|
fun createMockSdkSshKey(number: Int): SshKey =
|
||||||
|
SshKey(
|
||||||
|
publicKey = "mockPublicKey-$number",
|
||||||
|
privateKey = "mockPrivateKey-$number",
|
||||||
|
fingerprint = "mockKeyFingerprint-$number",
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a mock [Field] with a given [number].
|
* Create a mock [Field] with a given [number].
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -18,6 +18,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockIdentit
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockLogin
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockLogin
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPasswordHistory
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPasswordHistory
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSecureNote
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSecureNote
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSshKey
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockUri
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockUri
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkAttachment
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkAttachment
|
||||||
|
@ -28,6 +29,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkIdentity
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkLogin
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkLogin
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkPasswordHistory
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkPasswordHistory
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkSecureNote
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkSecureNote
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkSshKey
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkUri
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkUri
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -152,6 +154,16 @@ class VaultSdkCipherExtensionsTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toSdkSshKey should convert a SyncResponseJson Cipher SshKey to a SshKey`() {
|
||||||
|
val syncSshKey = createMockSshKey(number = 1)
|
||||||
|
val sdkSshKey = syncSshKey.toSdkSshKey()
|
||||||
|
assertEquals(
|
||||||
|
createMockSdkSshKey(number = 1),
|
||||||
|
sdkSshKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `toSdkLoginUriList should convert list of LoginUri to List of Sdk LoginUri`() {
|
fun `toSdkLoginUriList should convert list of LoginUri to List of Sdk LoginUri`() {
|
||||||
val syncLoginUris = listOf(
|
val syncLoginUris = listOf(
|
||||||
|
|
|
@ -485,6 +485,31 @@ class SearchScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
composeTestRule.onNodeWithText(text = "Search Verification codes").assertIsDisplayed()
|
composeTestRule.onNodeWithText(text = "Search Verification codes").assertIsDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(searchType = SearchTypeData.Vault.SshKeys)
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithText(text = "Search SSH keys").assertIsDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(searchType = SearchTypeData.Vault.Logins)
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithText(text = "Search Logins").assertIsDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(searchType = SearchTypeData.Vault.NoFolder)
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithText(text = "Search No Folder").assertIsDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
searchType = SearchTypeData.Vault.Collection(
|
||||||
|
collectionId = "mockId",
|
||||||
|
collectionName = "mockName",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithText(text = "Search mockName").assertIsDisplayed()
|
||||||
|
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
searchType = SearchTypeData.Vault.Folder(
|
searchType = SearchTypeData.Vault.Folder(
|
||||||
|
|
|
@ -1444,6 +1444,7 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||||
SearchTypeData.Sends.Texts -> "search_type_sends_text"
|
SearchTypeData.Sends.Texts -> "search_type_sends_text"
|
||||||
SearchTypeData.Vault.All -> "search_type_vault_all"
|
SearchTypeData.Vault.All -> "search_type_vault_all"
|
||||||
SearchTypeData.Vault.Cards -> "search_type_vault_cards"
|
SearchTypeData.Vault.Cards -> "search_type_vault_cards"
|
||||||
|
SearchTypeData.Vault.SshKeys -> "search_type_vault_ssh_keys"
|
||||||
is SearchTypeData.Vault.Collection -> "search_type_vault_collection"
|
is SearchTypeData.Vault.Collection -> "search_type_vault_collection"
|
||||||
is SearchTypeData.Vault.Folder -> "search_type_vault_folder"
|
is SearchTypeData.Vault.Folder -> "search_type_vault_folder"
|
||||||
SearchTypeData.Vault.Identities -> "search_type_vault_identities"
|
SearchTypeData.Vault.Identities -> "search_type_vault_identities"
|
||||||
|
@ -1463,6 +1464,7 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||||
SearchTypeData.Sends.Texts -> null
|
SearchTypeData.Sends.Texts -> null
|
||||||
SearchTypeData.Vault.All -> null
|
SearchTypeData.Vault.All -> null
|
||||||
SearchTypeData.Vault.Cards -> null
|
SearchTypeData.Vault.Cards -> null
|
||||||
|
SearchTypeData.Vault.SshKeys -> null
|
||||||
is SearchTypeData.Vault.Collection -> searchType.collectionId
|
is SearchTypeData.Vault.Collection -> searchType.collectionId
|
||||||
is SearchTypeData.Vault.Folder -> searchType.folderId
|
is SearchTypeData.Vault.Folder -> searchType.folderId
|
||||||
SearchTypeData.Vault.Identities -> null
|
SearchTypeData.Vault.Identities -> null
|
||||||
|
|
|
@ -203,6 +203,32 @@ class SearchTypeDataExtensionsTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `updateWithAdditionalDataIfNecessary should return the searchTypeData unchanged for Vault SshKeys`() {
|
||||||
|
val searchTypeData = SearchTypeData.Vault.SshKeys
|
||||||
|
assertEquals(
|
||||||
|
searchTypeData,
|
||||||
|
searchTypeData.updateWithAdditionalDataIfNecessary(
|
||||||
|
folderList = listOf(),
|
||||||
|
collectionList = emptyList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `updateWithAdditionalDataIfNecessary should return the searchTypeData unchanged for Vault VerificationCodes`() {
|
||||||
|
val searchTypeData = SearchTypeData.Vault.VerificationCodes
|
||||||
|
assertEquals(
|
||||||
|
searchTypeData,
|
||||||
|
searchTypeData.updateWithAdditionalDataIfNecessary(
|
||||||
|
folderList = listOf(),
|
||||||
|
collectionList = emptyList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `updateWithAdditionalDataIfNecessary should return the searchTypeData unchanged for Vault Trash`() {
|
fun `updateWithAdditionalDataIfNecessary should return the searchTypeData unchanged for Vault Trash`() {
|
||||||
|
|
|
@ -80,4 +80,18 @@ class SearchTypeExtensionsTest {
|
||||||
fun `toSearchTypeData should return Vault Trash then SearchType is Vault Trash`() {
|
fun `toSearchTypeData should return Vault Trash then SearchType is Vault Trash`() {
|
||||||
assertEquals(SearchTypeData.Vault.Trash, SearchType.Vault.Trash.toSearchTypeData())
|
assertEquals(SearchTypeData.Vault.Trash, SearchType.Vault.Trash.toSearchTypeData())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `toSearchTypeData should return Vault VerificationCodes then SearchType is Vault VerificationCodes`() {
|
||||||
|
assertEquals(
|
||||||
|
SearchTypeData.Vault.VerificationCodes,
|
||||||
|
SearchType.Vault.VerificationCodes.toSearchTypeData(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toSearchTypeData should return Vault SshKeys then SearchType is Vault SshKeys`() {
|
||||||
|
assertEquals(SearchTypeData.Vault.SshKeys, SearchType.Vault.SshKeys.toSearchTypeData())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,6 +186,36 @@ fun createMockDisplayItemForCipher(
|
||||||
isTotp = false,
|
isTotp = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CipherType.SSH_KEY -> {
|
||||||
|
SearchState.DisplayItem(
|
||||||
|
id = "mockId-$number",
|
||||||
|
title = "mockName-$number",
|
||||||
|
titleTestTag = "CipherNameLabel",
|
||||||
|
subtitle = "mockPublicKey-$number",
|
||||||
|
subtitleTestTag = "CipherSubTitleLabel",
|
||||||
|
iconData = IconData.Local(R.drawable.ic_ssh_key),
|
||||||
|
extraIconList = listOf(
|
||||||
|
IconRes(
|
||||||
|
iconRes = R.drawable.ic_collections,
|
||||||
|
contentDescription = R.string.collections.asText(),
|
||||||
|
testTag = "CipherInCollectionIcon",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
overflowOptions = listOf(
|
||||||
|
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
|
||||||
|
ListingItemOverflowAction.VaultAction.EditClick(
|
||||||
|
cipherId = "mockId-$number",
|
||||||
|
requiresPasswordReprompt = true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
overflowTestTag = "CipherOptionsButton",
|
||||||
|
totpCode = null,
|
||||||
|
autofillSelectionOptions = emptyList(),
|
||||||
|
shouldDisplayMasterPasswordReprompt = false,
|
||||||
|
isTotp = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3322,6 +3322,77 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `in ItemType_SshKeys changing the public key should trigger PublicKeyTextChange`() {
|
||||||
|
mutableStateFlow.value = DEFAULT_STATE_SSH_KEYS
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithTextAfterScroll("Public key")
|
||||||
|
.performTextInput("TestPublicKey")
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultAddEditAction.ItemType.SshKeyType.PublicKeyTextChange(
|
||||||
|
publicKey = "TestPublicKey",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `in ItemType_SshKeys changing the private key should trigger PrivateKeyTextChange`() {
|
||||||
|
mutableStateFlow.value = DEFAULT_STATE_SSH_KEYS
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithTextAfterScroll("Private key")
|
||||||
|
.performTextInput("TestPrivateKey")
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultAddEditAction.ItemType.SshKeyType.PrivateKeyTextChange(
|
||||||
|
privateKey = "TestPrivateKey",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `in ItemType_SshKeys changing the fingerprint should trigger FingerprintTextChange`() {
|
||||||
|
mutableStateFlow.value = DEFAULT_STATE_SSH_KEYS
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithTextAfterScroll("Fingerprint")
|
||||||
|
.performTextInput("TestFingerprint")
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultAddEditAction.ItemType.SshKeyType.FingerprintTextChange(
|
||||||
|
fingerprint = "TestFingerprint",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `in ItemType_SshKeys changing the private key visibility should trigger PrivateKeyVisibilityChange`() {
|
||||||
|
mutableStateFlow.value = DEFAULT_STATE_SSH_KEYS
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithTextAfterScroll(text = "Private key")
|
||||||
|
.assertExists()
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescriptionAfterScroll(label = "Show")
|
||||||
|
.assertExists()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultAddEditAction.ItemType.SshKeyType.PrivateKeyVisibilityChange(true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//region Helper functions
|
//region Helper functions
|
||||||
|
|
||||||
private fun updateLoginType(
|
private fun updateLoginType(
|
||||||
|
@ -3444,6 +3515,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
dialog = VaultAddEditState.DialogState.Generic(message = "test".asText()),
|
dialog = VaultAddEditState.DialogState.Generic(message = "test".asText()),
|
||||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||||
|
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_STATE_LOGIN = VaultAddEditState(
|
private val DEFAULT_STATE_LOGIN = VaultAddEditState(
|
||||||
|
@ -3454,6 +3526,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||||
isIndividualVaultDisabled = false,
|
isIndividualVaultDisabled = false,
|
||||||
),
|
),
|
||||||
dialog = null,
|
dialog = null,
|
||||||
|
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_STATE_IDENTITY = VaultAddEditState(
|
private val DEFAULT_STATE_IDENTITY = VaultAddEditState(
|
||||||
|
@ -3464,6 +3537,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||||
isIndividualVaultDisabled = false,
|
isIndividualVaultDisabled = false,
|
||||||
),
|
),
|
||||||
dialog = null,
|
dialog = null,
|
||||||
|
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_STATE_CARD = VaultAddEditState(
|
private val DEFAULT_STATE_CARD = VaultAddEditState(
|
||||||
|
@ -3474,6 +3548,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||||
isIndividualVaultDisabled = false,
|
isIndividualVaultDisabled = false,
|
||||||
),
|
),
|
||||||
dialog = null,
|
dialog = null,
|
||||||
|
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
|
@ -3495,6 +3570,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||||
),
|
),
|
||||||
dialog = null,
|
dialog = null,
|
||||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.SECURE_NOTE),
|
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.SECURE_NOTE),
|
||||||
|
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_STATE_SECURE_NOTES = VaultAddEditState(
|
private val DEFAULT_STATE_SECURE_NOTES = VaultAddEditState(
|
||||||
|
@ -3505,6 +3581,18 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||||
isIndividualVaultDisabled = false,
|
isIndividualVaultDisabled = false,
|
||||||
),
|
),
|
||||||
dialog = null,
|
dialog = null,
|
||||||
|
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val DEFAULT_STATE_SSH_KEYS = VaultAddEditState(
|
||||||
|
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.SSH_KEY),
|
||||||
|
viewState = VaultAddEditState.ViewState.Content(
|
||||||
|
common = VaultAddEditState.ViewState.Content.Common(),
|
||||||
|
type = VaultAddEditState.ViewState.Content.ItemType.SshKey(),
|
||||||
|
isIndividualVaultDisabled = false,
|
||||||
|
),
|
||||||
|
dialog = null,
|
||||||
|
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val ALTERED_COLLECTIONS = listOf(
|
private val ALTERED_COLLECTIONS = listOf(
|
||||||
|
|
|
@ -25,12 +25,14 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialRe
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||||
|
@ -152,6 +154,15 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
private val organizationEventManager = mockk<OrganizationEventManager> {
|
private val organizationEventManager = mockk<OrganizationEventManager> {
|
||||||
every { trackEvent(event = any()) } just runs
|
every { trackEvent(event = any()) } just runs
|
||||||
}
|
}
|
||||||
|
private val mutableSshVaultItemsFeatureFlagFlow = MutableStateFlow<Boolean>(true)
|
||||||
|
private val featureFlagManager = mockk<FeatureFlagManager> {
|
||||||
|
every {
|
||||||
|
getFeatureFlagFlow(key = FlagKey.SshKeyCipherItems)
|
||||||
|
} returns mutableSshVaultItemsFeatureFlagFlow
|
||||||
|
every {
|
||||||
|
getFeatureFlag(key = FlagKey.SshKeyCipherItems)
|
||||||
|
} returns mutableSshVaultItemsFeatureFlagFlow.value
|
||||||
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
@ -250,6 +261,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
type = VaultAddEditState.ViewState.Content.ItemType.Login(),
|
type = VaultAddEditState.ViewState.Content.ItemType.Login(),
|
||||||
),
|
),
|
||||||
dialog = null,
|
dialog = null,
|
||||||
|
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
|
@ -365,6 +377,56 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial add state should be correct when SSH key feature flag is enabled`() {
|
||||||
|
mutableSshVaultItemsFeatureFlagFlow.value = true
|
||||||
|
val vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN)
|
||||||
|
val initState = createVaultAddItemState(vaultAddEditType = vaultAddEditType)
|
||||||
|
val viewModel = createAddVaultItemViewModel(
|
||||||
|
savedStateHandle = createSavedStateHandleWithState(
|
||||||
|
state = initState,
|
||||||
|
vaultAddEditType = vaultAddEditType,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
initState,
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial add state should be correct when SSH key feature flag is disabled`() {
|
||||||
|
mutableSshVaultItemsFeatureFlagFlow.value = false
|
||||||
|
every {
|
||||||
|
featureFlagManager.getFeatureFlag(key = FlagKey.SshKeyCipherItems)
|
||||||
|
} returns false
|
||||||
|
val vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN)
|
||||||
|
val expectedState = VaultAddEditState(
|
||||||
|
vaultAddEditType = vaultAddEditType,
|
||||||
|
viewState = VaultAddEditState.ViewState.Content(
|
||||||
|
common = VaultAddEditState.ViewState.Content.Common(),
|
||||||
|
isIndividualVaultDisabled = false,
|
||||||
|
type = VaultAddEditState.ViewState.Content.ItemType.Login(),
|
||||||
|
),
|
||||||
|
dialog = null,
|
||||||
|
totpData = null,
|
||||||
|
shouldShowCloseButton = true,
|
||||||
|
shouldExitOnSave = false,
|
||||||
|
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries
|
||||||
|
.filter { it != VaultAddEditState.ItemTypeOption.SSH_KEYS },
|
||||||
|
)
|
||||||
|
val viewModel = createAddVaultItemViewModel(
|
||||||
|
savedStateHandle = createSavedStateHandleWithState(
|
||||||
|
state = null,
|
||||||
|
vaultAddEditType = vaultAddEditType,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
expectedState,
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial edit state should be correct`() = runTest {
|
fun `initial edit state should be correct`() = runTest {
|
||||||
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
|
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
|
||||||
|
@ -1792,6 +1854,36 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `TypeOptionSelect SSH_KEYS should switch to SshKeysItem`() = runTest {
|
||||||
|
mutableVaultDataFlow.value = DataState.Loaded(
|
||||||
|
createVaultData(cipherView = createMockCipherView(1)),
|
||||||
|
)
|
||||||
|
val viewModel = createAddVaultItemViewModel()
|
||||||
|
val action = VaultAddEditAction.Common.TypeOptionSelect(
|
||||||
|
VaultAddEditState.ItemTypeOption.SSH_KEYS,
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.trySendAction(action)
|
||||||
|
|
||||||
|
val expectedState = loginInitialState.copy(
|
||||||
|
viewState = VaultAddEditState.ViewState.Content(
|
||||||
|
common = createCommonContentViewState(),
|
||||||
|
isIndividualVaultDisabled = false,
|
||||||
|
type = VaultAddEditState.ViewState.Content.ItemType.SshKey(),
|
||||||
|
previousItemTypes = mapOf(
|
||||||
|
VaultAddEditState.ItemTypeOption.LOGIN
|
||||||
|
to VaultAddEditState.ViewState.Content.ItemType.Login(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
expectedState,
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class VaultAddEditLoginTypeItemActions {
|
inner class VaultAddEditLoginTypeItemActions {
|
||||||
private lateinit var viewModel: VaultAddEditViewModel
|
private lateinit var viewModel: VaultAddEditViewModel
|
||||||
|
@ -2610,6 +2702,90 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class VaultAddEditSshKeyTypeItemActions {
|
||||||
|
private lateinit var viewModel: VaultAddEditViewModel
|
||||||
|
private lateinit var vaultAddItemInitialState: VaultAddEditState
|
||||||
|
private lateinit var sshKeyInitialSavedStateHandle: SavedStateHandle
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
mutableVaultDataFlow.value = DataState.Loaded(
|
||||||
|
createVaultData(cipherView = createMockCipherView(1)),
|
||||||
|
)
|
||||||
|
vaultAddItemInitialState = createVaultAddItemState(
|
||||||
|
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.SshKey(),
|
||||||
|
)
|
||||||
|
sshKeyInitialSavedStateHandle = createSavedStateHandleWithState(
|
||||||
|
state = vaultAddItemInitialState,
|
||||||
|
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.SSH_KEY),
|
||||||
|
)
|
||||||
|
viewModel = createAddVaultItemViewModel(
|
||||||
|
savedStateHandle = sshKeyInitialSavedStateHandle,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `PublicKeyTextChange should update public key`() = runTest {
|
||||||
|
val action = VaultAddEditAction.ItemType.SshKeyType.PublicKeyTextChange(
|
||||||
|
publicKey = "newPublicKey",
|
||||||
|
)
|
||||||
|
val expectedState = createVaultAddItemState(
|
||||||
|
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.SshKey(
|
||||||
|
publicKey = "newPublicKey",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
viewModel.trySendAction(action)
|
||||||
|
|
||||||
|
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `PrivateKeyTextChange should update private key`() = runTest {
|
||||||
|
val action = VaultAddEditAction.ItemType.SshKeyType.PrivateKeyTextChange(
|
||||||
|
privateKey = "newPrivateKey",
|
||||||
|
)
|
||||||
|
val expectedState = createVaultAddItemState(
|
||||||
|
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.SshKey(
|
||||||
|
privateKey = "newPrivateKey",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
viewModel.trySendAction(action)
|
||||||
|
|
||||||
|
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `PrivateKeyVisibilityChange should update private key visibility`() = runTest {
|
||||||
|
val action = VaultAddEditAction.ItemType.SshKeyType.PrivateKeyVisibilityChange(
|
||||||
|
isVisible = true,
|
||||||
|
)
|
||||||
|
val expectedState = createVaultAddItemState(
|
||||||
|
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.SshKey(
|
||||||
|
showPrivateKey = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
viewModel.trySendAction(action)
|
||||||
|
|
||||||
|
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `FingerprintTextChange should update fingerprint`() = runTest {
|
||||||
|
val action = VaultAddEditAction.ItemType.SshKeyType.FingerprintTextChange(
|
||||||
|
fingerprint = "newFingerprint",
|
||||||
|
)
|
||||||
|
val expectedState = createVaultAddItemState(
|
||||||
|
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.SshKey(
|
||||||
|
fingerprint = "newFingerprint",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
viewModel.trySendAction(action)
|
||||||
|
|
||||||
|
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `NumberVisibilityChange should log an event when in edit mode and password is visible`() =
|
fun `NumberVisibilityChange should log an event when in edit mode and password is visible`() =
|
||||||
runTest {
|
runTest {
|
||||||
|
@ -2686,6 +2862,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
resourceManager = resourceManager,
|
resourceManager = resourceManager,
|
||||||
clock = fixedClock,
|
clock = fixedClock,
|
||||||
organizationEventManager = organizationEventManager,
|
organizationEventManager = organizationEventManager,
|
||||||
|
featureFlagManager = featureFlagManager,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3781,6 +3958,30 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `SshKeyCipherItemsFeatureFlagReceive should update supportedItemTypes`() = runTest {
|
||||||
|
// Verify SSH keys is supported when feature flag is enabled.
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive(enabled = true),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
VaultAddEditState.ItemTypeOption.entries,
|
||||||
|
viewModel.stateFlow.value.supportedItemTypes,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify SSH keys is not supported when feature flag is disabled.
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive(enabled = false),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
VaultAddEditState.ItemTypeOption.entries.filterNot {
|
||||||
|
it == VaultAddEditState.ItemTypeOption.SSH_KEYS
|
||||||
|
},
|
||||||
|
viewModel.stateFlow.value.supportedItemTypes,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//region Helper functions
|
//region Helper functions
|
||||||
|
@ -3794,6 +3995,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
typeContentViewState: VaultAddEditState.ViewState.Content.ItemType = createLoginTypeContentViewState(),
|
typeContentViewState: VaultAddEditState.ViewState.Content.ItemType = createLoginTypeContentViewState(),
|
||||||
dialogState: VaultAddEditState.DialogState? = null,
|
dialogState: VaultAddEditState.DialogState? = null,
|
||||||
totpData: TotpData? = null,
|
totpData: TotpData? = null,
|
||||||
|
supportedItemTypes: List<VaultAddEditState.ItemTypeOption> = VaultAddEditState.ItemTypeOption.entries,
|
||||||
): VaultAddEditState =
|
): VaultAddEditState =
|
||||||
VaultAddEditState(
|
VaultAddEditState(
|
||||||
vaultAddEditType = vaultAddEditType,
|
vaultAddEditType = vaultAddEditType,
|
||||||
|
@ -3805,6 +4007,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
dialog = dialogState,
|
dialog = dialogState,
|
||||||
shouldExitOnSave = shouldExitOnSave,
|
shouldExitOnSave = shouldExitOnSave,
|
||||||
totpData = totpData,
|
totpData = totpData,
|
||||||
|
supportedItemTypes = supportedItemTypes,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
|
@ -3879,6 +4082,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
VaultItemCipherType.CARD -> "card"
|
VaultItemCipherType.CARD -> "card"
|
||||||
VaultItemCipherType.IDENTITY -> "identity"
|
VaultItemCipherType.IDENTITY -> "identity"
|
||||||
VaultItemCipherType.SECURE_NOTE -> "secure_note"
|
VaultItemCipherType.SECURE_NOTE -> "secure_note"
|
||||||
|
VaultItemCipherType.SSH_KEY -> "ssh_key"
|
||||||
null -> null
|
null -> null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -3906,6 +4110,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
resourceManager = bitwardenResourceManager,
|
resourceManager = bitwardenResourceManager,
|
||||||
clock = clock,
|
clock = clock,
|
||||||
organizationEventManager = organizationEventManager,
|
organizationEventManager = organizationEventManager,
|
||||||
|
featureFlagManager = featureFlagManager,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createVaultData(
|
private fun createVaultData(
|
||||||
|
|
|
@ -12,6 +12,7 @@ import com.bitwarden.vault.LoginView
|
||||||
import com.bitwarden.vault.PasswordHistoryView
|
import com.bitwarden.vault.PasswordHistoryView
|
||||||
import com.bitwarden.vault.SecureNoteType
|
import com.bitwarden.vault.SecureNoteType
|
||||||
import com.bitwarden.vault.SecureNoteView
|
import com.bitwarden.vault.SecureNoteView
|
||||||
|
import com.bitwarden.vault.SshKeyView
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||||
|
@ -313,6 +314,50 @@ class CipherViewExtensionsTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toViewState should create SSH Key ViewState`() {
|
||||||
|
val cipherView = DEFAULT_SSH_KEY_CIPHER_VIEW
|
||||||
|
|
||||||
|
val result = cipherView.toViewState(
|
||||||
|
isClone = false,
|
||||||
|
isIndividualVaultDisabled = false,
|
||||||
|
totpData = null,
|
||||||
|
resourceManager = resourceManager,
|
||||||
|
clock = FIXED_CLOCK,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultAddEditState.ViewState.Content(
|
||||||
|
common = VaultAddEditState.ViewState.Content.Common(
|
||||||
|
originalCipher = cipherView,
|
||||||
|
name = "cipher",
|
||||||
|
favorite = false,
|
||||||
|
masterPasswordReprompt = true,
|
||||||
|
notes = "Lots of notes",
|
||||||
|
customFieldData = listOf(
|
||||||
|
VaultAddEditState.Custom.BooleanField(TEST_ID, "TestBoolean", false),
|
||||||
|
VaultAddEditState.Custom.TextField(TEST_ID, "TestText", "TestText"),
|
||||||
|
VaultAddEditState.Custom.HiddenField(TEST_ID, "TestHidden", "TestHidden"),
|
||||||
|
VaultAddEditState.Custom.LinkedField(
|
||||||
|
TEST_ID,
|
||||||
|
"TestLinked",
|
||||||
|
VaultLinkedFieldType.USERNAME,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
availableFolders = emptyList(),
|
||||||
|
availableOwners = emptyList(),
|
||||||
|
),
|
||||||
|
isIndividualVaultDisabled = false,
|
||||||
|
type = VaultAddEditState.ViewState.Content.ItemType.SshKey(
|
||||||
|
publicKey = "PublicKey",
|
||||||
|
privateKey = "PrivateKey",
|
||||||
|
fingerprint = "Fingerprint",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `toViewState with isClone true should append clone text to the cipher name`() {
|
fun `toViewState with isClone true should append clone text to the cipher name`() {
|
||||||
val cipherView = DEFAULT_SECURE_NOTES_CIPHER_VIEW
|
val cipherView = DEFAULT_SECURE_NOTES_CIPHER_VIEW
|
||||||
|
@ -578,6 +623,7 @@ private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView(
|
||||||
creationDate = FIXED_CLOCK.instant(),
|
creationDate = FIXED_CLOCK.instant(),
|
||||||
deletedDate = null,
|
deletedDate = null,
|
||||||
revisionDate = FIXED_CLOCK.instant(),
|
revisionDate = FIXED_CLOCK.instant(),
|
||||||
|
sshKey = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_CARD_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
|
private val DEFAULT_CARD_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
|
||||||
|
@ -660,4 +706,13 @@ private val DEFAULT_SECURE_NOTES_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_V
|
||||||
secureNote = SecureNoteView(type = SecureNoteType.GENERIC),
|
secureNote = SecureNoteView(type = SecureNoteType.GENERIC),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val DEFAULT_SSH_KEY_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
|
||||||
|
type = CipherType.SSH_KEY,
|
||||||
|
sshKey = SshKeyView(
|
||||||
|
publicKey = "PublicKey",
|
||||||
|
privateKey = "PrivateKey",
|
||||||
|
fingerprint = "Fingerprint",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
private const val TEST_ID = "testID"
|
private const val TEST_ID = "testID"
|
||||||
|
|
|
@ -31,6 +31,7 @@ class VaultAddEditExtensionsTest {
|
||||||
VaultItemCipherType.CARD,
|
VaultItemCipherType.CARD,
|
||||||
VaultItemCipherType.SECURE_NOTE,
|
VaultItemCipherType.SECURE_NOTE,
|
||||||
VaultItemCipherType.IDENTITY,
|
VaultItemCipherType.IDENTITY,
|
||||||
|
VaultItemCipherType.SSH_KEY,
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = vaultItemCipherTypeList.map { it.toItemType() }
|
val result = vaultItemCipherTypeList.map { it.toItemType() }
|
||||||
|
@ -41,6 +42,7 @@ class VaultAddEditExtensionsTest {
|
||||||
VaultAddEditState.ViewState.Content.ItemType.Card(),
|
VaultAddEditState.ViewState.Content.ItemType.Card(),
|
||||||
VaultAddEditState.ViewState.Content.ItemType.SecureNotes,
|
VaultAddEditState.ViewState.Content.ItemType.SecureNotes,
|
||||||
VaultAddEditState.ViewState.Content.ItemType.Identity(),
|
VaultAddEditState.ViewState.Content.ItemType.Identity(),
|
||||||
|
VaultAddEditState.ViewState.Content.ItemType.SshKey(),
|
||||||
),
|
),
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1867,6 +1867,8 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
//endregion identity
|
//endregion identity
|
||||||
|
|
||||||
|
//region card
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `in card state, cardholderName should be displayed according to state`() {
|
fun `in card state, cardholderName should be displayed according to state`() {
|
||||||
val cardholderName = "the cardholder name"
|
val cardholderName = "the cardholder name"
|
||||||
|
@ -2139,6 +2141,79 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopySecurityCodeClick)
|
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopySecurityCodeClick)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//endregion card
|
||||||
|
|
||||||
|
//region ssh key
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `in ssh key state, public key should be displayed according to state`() {
|
||||||
|
val publicKey = "the public key"
|
||||||
|
mutableStateFlow.update { it.copy(viewState = DEFAULT_SSH_KEY_VIEW_STATE) }
|
||||||
|
composeTestRule.onNodeWithTextAfterScroll(publicKey).assertIsDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update { currentState ->
|
||||||
|
updateSshKeyType(currentState) { copy(publicKey = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.assertScrollableNodeDoesNotExist(publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `in ssh key state, private key should be displayed according to state`() {
|
||||||
|
val privateKey = "the private key"
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
viewState = DEFAULT_SSH_KEY_VIEW_STATE
|
||||||
|
.copy(
|
||||||
|
type = DEFAULT_SSH_KEY.copy(showPrivateKey = true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(privateKey)
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update { currentState ->
|
||||||
|
updateSshKeyType(currentState) { copy(privateKey = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.assertScrollableNodeDoesNotExist(privateKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `in ssh key state, on show private key click should send ShowPrivateKeyClick`() {
|
||||||
|
mutableStateFlow.update { it.copy(viewState = DEFAULT_SSH_KEY_VIEW_STATE) }
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithTextAfterScroll("Private key")
|
||||||
|
.onChildren()
|
||||||
|
.filterToOne(hasContentDescription("Show"))
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked(
|
||||||
|
isVisible = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `in ssh key state, fingerprint should be displayed according to state`() {
|
||||||
|
val fingerprint = "the fingerprint"
|
||||||
|
mutableStateFlow.update { it.copy(viewState = DEFAULT_SSH_KEY_VIEW_STATE) }
|
||||||
|
composeTestRule.onNodeWithTextAfterScroll(fingerprint).assertIsDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update { currentState ->
|
||||||
|
updateSshKeyType(currentState) { copy(fingerprint = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.assertScrollableNodeDoesNotExist(fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion ssh key
|
||||||
}
|
}
|
||||||
|
|
||||||
//region Helper functions
|
//region Helper functions
|
||||||
|
@ -2212,6 +2287,29 @@ private fun updateCardType(
|
||||||
return currentState.copy(viewState = updatedType)
|
return currentState.copy(viewState = updatedType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateSshKeyType(
|
||||||
|
currentState: VaultItemState,
|
||||||
|
transform: VaultItemState.ViewState.Content.ItemType.SshKey.() ->
|
||||||
|
VaultItemState.ViewState.Content.ItemType.SshKey,
|
||||||
|
): VaultItemState {
|
||||||
|
val updatedType = when (val viewState = currentState.viewState) {
|
||||||
|
is VaultItemState.ViewState.Content -> {
|
||||||
|
when (val type = viewState.type) {
|
||||||
|
is VaultItemState.ViewState.Content.ItemType.SshKey -> {
|
||||||
|
viewState.copy(
|
||||||
|
type = type.transform(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> viewState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> viewState
|
||||||
|
}
|
||||||
|
return currentState.copy(viewState = updatedType)
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateCommonContent(
|
private fun updateCommonContent(
|
||||||
currentState: VaultItemState,
|
currentState: VaultItemState,
|
||||||
transform: VaultItemState.ViewState.Content.Common.()
|
transform: VaultItemState.ViewState.Content.Common.()
|
||||||
|
@ -2333,6 +2431,15 @@ private val DEFAULT_CARD: VaultItemState.ViewState.Content.ItemType.Card =
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val DEFAULT_SSH_KEY: VaultItemState.ViewState.Content.ItemType.SshKey =
|
||||||
|
VaultItemState.ViewState.Content.ItemType.SshKey(
|
||||||
|
name = "the ssh key name",
|
||||||
|
publicKey = "the public key",
|
||||||
|
privateKey = "the private key",
|
||||||
|
fingerprint = "the fingerprint",
|
||||||
|
showPrivateKey = false,
|
||||||
|
)
|
||||||
|
|
||||||
private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common =
|
private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common =
|
||||||
VaultItemState.ViewState.Content.Common(
|
VaultItemState.ViewState.Content.Common(
|
||||||
name = "cipher",
|
name = "cipher",
|
||||||
|
@ -2433,6 +2540,12 @@ private val DEFAULT_SECURE_NOTE_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||||
type = VaultItemState.ViewState.Content.ItemType.SecureNote,
|
type = VaultItemState.ViewState.Content.ItemType.SecureNote,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val DEFAULT_SSH_KEY_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||||
|
VaultItemState.ViewState.Content(
|
||||||
|
common = DEFAULT_COMMON,
|
||||||
|
type = DEFAULT_SSH_KEY,
|
||||||
|
)
|
||||||
|
|
||||||
private val EMPTY_VIEW_STATES = listOf(
|
private val EMPTY_VIEW_STATES = listOf(
|
||||||
EMPTY_LOGIN_VIEW_STATE,
|
EMPTY_LOGIN_VIEW_STATE,
|
||||||
EMPTY_IDENTITY_VIEW_STATE,
|
EMPTY_IDENTITY_VIEW_STATE,
|
||||||
|
|
|
@ -2336,6 +2336,60 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class SshKeyActions {
|
||||||
|
private lateinit var viewModel: VaultItemViewModel
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
viewModel = createViewModel(
|
||||||
|
state = DEFAULT_STATE.copy(
|
||||||
|
viewState = SSH_KEY_VIEW_STATE,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `on PrivateKeyVisibilityClick should show password dialog when re-prompt is required`() =
|
||||||
|
runTest {
|
||||||
|
val sshKeyViewState = createViewState(
|
||||||
|
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||||
|
)
|
||||||
|
val sshKeyState = DEFAULT_STATE.copy(viewState = SSH_KEY_VIEW_STATE)
|
||||||
|
val mockCipherView = mockk<CipherView> {
|
||||||
|
every {
|
||||||
|
toViewState(
|
||||||
|
previousState = null,
|
||||||
|
isPremiumUser = true,
|
||||||
|
hasMasterPassword = true,
|
||||||
|
totpCodeItemData = null,
|
||||||
|
)
|
||||||
|
} returns SSH_KEY_VIEW_STATE
|
||||||
|
}
|
||||||
|
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||||
|
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||||
|
|
||||||
|
assertEquals(sshKeyState, viewModel.stateFlow.value)
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked(
|
||||||
|
isVisible = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
sshKeyState.copy(
|
||||||
|
viewState = sshKeyViewState.copy(
|
||||||
|
common = DEFAULT_COMMON,
|
||||||
|
type = DEFAULT_SSH_KEY_TYPE.copy(
|
||||||
|
showPrivateKey = true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class VaultItemFlow {
|
inner class VaultItemFlow {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
|
@ -2628,6 +2682,15 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val DEFAULT_SSH_KEY_TYPE: VaultItemState.ViewState.Content.ItemType.SshKey =
|
||||||
|
VaultItemState.ViewState.Content.ItemType.SshKey(
|
||||||
|
name = "mockName",
|
||||||
|
publicKey = "mockPublicKey",
|
||||||
|
privateKey = "mockPrivateKey",
|
||||||
|
fingerprint = "mockFingerprint",
|
||||||
|
showPrivateKey = false,
|
||||||
|
)
|
||||||
|
|
||||||
private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common =
|
private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common =
|
||||||
VaultItemState.ViewState.Content.Common(
|
VaultItemState.ViewState.Content.Common(
|
||||||
name = "login cipher",
|
name = "login cipher",
|
||||||
|
@ -2684,5 +2747,11 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||||
common = DEFAULT_COMMON,
|
common = DEFAULT_COMMON,
|
||||||
type = DEFAULT_CARD_TYPE,
|
type = DEFAULT_CARD_TYPE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val SSH_KEY_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||||
|
VaultItemState.ViewState.Content(
|
||||||
|
common = DEFAULT_COMMON,
|
||||||
|
type = DEFAULT_SSH_KEY_TYPE,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -374,4 +374,32 @@ class CipherViewExtensionsTest {
|
||||||
|
|
||||||
assertEquals(expectedState, viewState)
|
assertEquals(expectedState, viewState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toViewState should transform full CipherView into ViewState SSH Key Content`() {
|
||||||
|
val cipherView = createCipherView(type = CipherType.SSH_KEY, isEmpty = false)
|
||||||
|
val viewState = cipherView.toViewState(
|
||||||
|
previousState = null,
|
||||||
|
isPremiumUser = true,
|
||||||
|
hasMasterPassword = true,
|
||||||
|
totpCodeItemData = null,
|
||||||
|
clock = fixedClock,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
VaultItemState.ViewState.Content(
|
||||||
|
common = createCommonContent(isEmpty = false, isPremiumUser = true).copy(
|
||||||
|
currentCipher = cipherView.copy(
|
||||||
|
name = "mockName",
|
||||||
|
sshKey = cipherView.sshKey?.copy(
|
||||||
|
publicKey = "publicKey",
|
||||||
|
privateKey = "privateKey",
|
||||||
|
fingerprint = "fingerprint",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
type = createSshKeyContent(isEmpty = false),
|
||||||
|
),
|
||||||
|
viewState,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import com.bitwarden.vault.IdentityView
|
||||||
import com.bitwarden.vault.LoginUriView
|
import com.bitwarden.vault.LoginUriView
|
||||||
import com.bitwarden.vault.LoginView
|
import com.bitwarden.vault.LoginView
|
||||||
import com.bitwarden.vault.PasswordHistoryView
|
import com.bitwarden.vault.PasswordHistoryView
|
||||||
|
import com.bitwarden.vault.SshKeyView
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
@ -72,6 +73,13 @@ fun createIdentityView(isEmpty: Boolean): IdentityView =
|
||||||
licenseNumber = "licenseNumber".takeUnless { isEmpty },
|
licenseNumber = "licenseNumber".takeUnless { isEmpty },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun createSshKeyView(isEmpty: Boolean): SshKeyView =
|
||||||
|
SshKeyView(
|
||||||
|
privateKey = "privateKey".takeUnless { isEmpty },
|
||||||
|
publicKey = "publicKey".takeUnless { isEmpty },
|
||||||
|
fingerprint = "fingerprint".takeUnless { isEmpty },
|
||||||
|
)
|
||||||
|
|
||||||
fun createCipherView(type: CipherType, isEmpty: Boolean): CipherView =
|
fun createCipherView(type: CipherType, isEmpty: Boolean): CipherView =
|
||||||
CipherView(
|
CipherView(
|
||||||
id = null,
|
id = null,
|
||||||
|
@ -146,6 +154,7 @@ fun createCipherView(type: CipherType, isEmpty: Boolean): CipherView =
|
||||||
creationDate = Instant.ofEpochSecond(1_000L),
|
creationDate = Instant.ofEpochSecond(1_000L),
|
||||||
deletedDate = null,
|
deletedDate = null,
|
||||||
revisionDate = Instant.ofEpochSecond(1_000L),
|
revisionDate = Instant.ofEpochSecond(1_000L),
|
||||||
|
sshKey = createSshKeyView(isEmpty = isEmpty),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun createCommonContent(
|
fun createCommonContent(
|
||||||
|
@ -259,3 +268,12 @@ fun createIdentityContent(
|
||||||
phone = "phone".takeUnless { isEmpty },
|
phone = "phone".takeUnless { isEmpty },
|
||||||
address = address.takeUnless { isEmpty },
|
address = address.takeUnless { isEmpty },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun createSshKeyContent(isEmpty: Boolean): VaultItemState.ViewState.Content.ItemType.SshKey =
|
||||||
|
VaultItemState.ViewState.Content.ItemType.SshKey(
|
||||||
|
name = "mockName".takeUnless { isEmpty },
|
||||||
|
privateKey = "privateKey".takeUnless { isEmpty },
|
||||||
|
publicKey = "publicKey".takeUnless { isEmpty },
|
||||||
|
fingerprint = "fingerprint".takeUnless { isEmpty },
|
||||||
|
showPrivateKey = false,
|
||||||
|
)
|
||||||
|
|
|
@ -1199,6 +1199,13 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
||||||
.onNodeWithText(text = "Identities")
|
.onNodeWithText(text = "Identities")
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(itemListingType = VaultItemListingState.ItemListingType.Vault.SshKey)
|
||||||
|
}
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "SSH keys")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(itemListingType = VaultItemListingState.ItemListingType.Vault.Trash)
|
it.copy(itemListingType = VaultItemListingState.ItemListingType.Vault.Trash)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3971,6 +3971,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||||
is VaultItemListingType.Trash -> "trash"
|
is VaultItemListingType.Trash -> "trash"
|
||||||
is VaultItemListingType.SendFile -> "send_file"
|
is VaultItemListingType.SendFile -> "send_file"
|
||||||
is VaultItemListingType.SendText -> "send_text"
|
is VaultItemListingType.SendText -> "send_text"
|
||||||
|
is VaultItemListingType.SshKey -> "ssh_key"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
set(
|
set(
|
||||||
|
@ -3985,6 +3986,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||||
is VaultItemListingType.Trash -> null
|
is VaultItemListingType.Trash -> null
|
||||||
is VaultItemListingType.SendFile -> null
|
is VaultItemListingType.SendFile -> null
|
||||||
is VaultItemListingType.SendText -> null
|
is VaultItemListingType.SendText -> null
|
||||||
|
is VaultItemListingType.SshKey -> null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -254,6 +254,66 @@ class VaultItemListingDataExtensionsTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `determineListingPredicate should return the correct predicate for a trash SshKey cipherView`() {
|
||||||
|
val cipherView = createMockCipherView(
|
||||||
|
number = 1,
|
||||||
|
isDeleted = true,
|
||||||
|
cipherType = CipherType.SSH_KEY,
|
||||||
|
)
|
||||||
|
|
||||||
|
mapOf(
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Login to false,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Card to false,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.SecureNote to false,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Identity to false,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Trash to true,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Folder(folderId = "mockId-1") to false,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Collection(collectionId = "mockId-1") to false,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.SshKey to false,
|
||||||
|
)
|
||||||
|
.forEach { (type, expected) ->
|
||||||
|
val result = cipherView.determineListingPredicate(
|
||||||
|
itemListingType = type,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
expected,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `determineListingPredicate should return the correct predicate for a non trash SshKey cipherView`() {
|
||||||
|
val cipherView = createMockCipherView(
|
||||||
|
number = 1,
|
||||||
|
isDeleted = false,
|
||||||
|
cipherType = CipherType.SSH_KEY,
|
||||||
|
)
|
||||||
|
|
||||||
|
mapOf(
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Login to false,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Card to false,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.SecureNote to false,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Identity to false,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Trash to false,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.SshKey to true,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Folder(folderId = "mockId-1") to true,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Collection(collectionId = "mockId-1") to true,
|
||||||
|
)
|
||||||
|
.forEach { (type, expected) ->
|
||||||
|
val result = cipherView.determineListingPredicate(
|
||||||
|
itemListingType = type,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
expected,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
fun `determineListingPredicate should return the correct predicate for item not in a folder`() {
|
fun `determineListingPredicate should return the correct predicate for item not in a folder`() {
|
||||||
|
@ -873,15 +933,76 @@ class VaultItemListingDataExtensionsTest {
|
||||||
createMockCollectionView(number = 3),
|
createMockCollectionView(number = 3),
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = VaultItemListingState.ItemListingType.Vault.Login
|
assertEquals(
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Identity,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Identity
|
||||||
.updateWithAdditionalDataIfNecessary(
|
.updateWithAdditionalDataIfNecessary(
|
||||||
folderList = folderViewList,
|
folderList = folderViewList,
|
||||||
collectionList = collectionViewList,
|
collectionList = collectionViewList,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
VaultItemListingState.ItemListingType.Vault.Login,
|
VaultItemListingState.ItemListingType.Vault.Login,
|
||||||
result,
|
VaultItemListingState.ItemListingType.Vault.Login
|
||||||
|
.updateWithAdditionalDataIfNecessary(
|
||||||
|
folderList = folderViewList,
|
||||||
|
collectionList = collectionViewList,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultItemListingState.ItemListingType.Vault.SecureNote,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.SecureNote
|
||||||
|
.updateWithAdditionalDataIfNecessary(
|
||||||
|
folderList = folderViewList,
|
||||||
|
collectionList = collectionViewList,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Trash,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Trash
|
||||||
|
.updateWithAdditionalDataIfNecessary(
|
||||||
|
folderList = folderViewList,
|
||||||
|
collectionList = collectionViewList,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultItemListingState.ItemListingType.Vault.SshKey,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.SshKey
|
||||||
|
.updateWithAdditionalDataIfNecessary(
|
||||||
|
folderList = folderViewList,
|
||||||
|
collectionList = collectionViewList,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Card,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Card
|
||||||
|
.updateWithAdditionalDataIfNecessary(
|
||||||
|
folderList = folderViewList,
|
||||||
|
collectionList = collectionViewList,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultItemListingState.ItemListingType.Send.SendFile,
|
||||||
|
VaultItemListingState.ItemListingType.Send.SendFile
|
||||||
|
.updateWithAdditionalDataIfNecessary(
|
||||||
|
folderList = folderViewList,
|
||||||
|
collectionList = collectionViewList,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultItemListingState.ItemListingType.Send.SendText,
|
||||||
|
VaultItemListingState.ItemListingType.Send.SendText
|
||||||
|
.updateWithAdditionalDataIfNecessary(
|
||||||
|
folderList = folderViewList,
|
||||||
|
collectionList = collectionViewList,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -202,6 +202,44 @@ fun createMockDisplayItemForCipher(
|
||||||
isTotp = false,
|
isTotp = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CipherType.SSH_KEY -> {
|
||||||
|
VaultItemListingState.DisplayItem(
|
||||||
|
id = "mockId-$number",
|
||||||
|
title = "mockName-$number",
|
||||||
|
titleTestTag = "CipherNameLabel",
|
||||||
|
secondSubtitle = null,
|
||||||
|
secondSubtitleTestTag = secondSubtitleTestTag,
|
||||||
|
subtitle = subtitle,
|
||||||
|
subtitleTestTag = "CipherSubTitleLabel",
|
||||||
|
iconData = IconData.Local(R.drawable.ic_ssh_key),
|
||||||
|
extraIconList = listOf(
|
||||||
|
IconRes(
|
||||||
|
iconRes = R.drawable.ic_collections,
|
||||||
|
contentDescription = R.string.collections.asText(),
|
||||||
|
testTag = "CipherInCollectionIcon",
|
||||||
|
),
|
||||||
|
IconRes(
|
||||||
|
iconRes = R.drawable.ic_paperclip,
|
||||||
|
contentDescription = R.string.attachments.asText(),
|
||||||
|
testTag = "CipherWithAttachmentsIcon",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
overflowOptions = listOf(
|
||||||
|
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
|
||||||
|
ListingItemOverflowAction.VaultAction.EditClick(
|
||||||
|
cipherId = "mockId-$number",
|
||||||
|
requiresPasswordReprompt = requiresPasswordReprompt,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
optionsTestTag = "CipherOptionsButton",
|
||||||
|
isAutofill = false,
|
||||||
|
isFido2Creation = false,
|
||||||
|
shouldShowMasterPasswordReprompt = false,
|
||||||
|
iconTestTag = "SshKeyCipherIcon",
|
||||||
|
isTotp = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -111,6 +111,16 @@ class VaultItemListingStateExtensionsTest {
|
||||||
assertEquals(expected, result)
|
assertEquals(expected, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toSearchType should return SshKey when item type is SshKey`() {
|
||||||
|
val expected = SearchType.Vault.SshKeys
|
||||||
|
val itemType = VaultItemListingState.ItemListingType.Vault.SshKey
|
||||||
|
|
||||||
|
val result = itemType.toSearchType()
|
||||||
|
|
||||||
|
assertEquals(expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `toVaultItemCipherType should return the correct response`() {
|
fun `toVaultItemCipherType should return the correct response`() {
|
||||||
val itemListingTypes = listOf(
|
val itemListingTypes = listOf(
|
||||||
|
@ -119,6 +129,7 @@ class VaultItemListingStateExtensionsTest {
|
||||||
VaultItemListingState.ItemListingType.Vault.SecureNote,
|
VaultItemListingState.ItemListingType.Vault.SecureNote,
|
||||||
VaultItemListingState.ItemListingType.Vault.Login,
|
VaultItemListingState.ItemListingType.Vault.Login,
|
||||||
VaultItemListingState.ItemListingType.Vault.Collection(collectionId = "mockId"),
|
VaultItemListingState.ItemListingType.Vault.Collection(collectionId = "mockId"),
|
||||||
|
VaultItemListingState.ItemListingType.Vault.SshKey,
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = itemListingTypes.map { it.toVaultItemCipherType() }
|
val result = itemListingTypes.map { it.toVaultItemCipherType() }
|
||||||
|
@ -130,6 +141,7 @@ class VaultItemListingStateExtensionsTest {
|
||||||
VaultItemCipherType.SECURE_NOTE,
|
VaultItemCipherType.SECURE_NOTE,
|
||||||
VaultItemCipherType.LOGIN,
|
VaultItemCipherType.LOGIN,
|
||||||
VaultItemCipherType.LOGIN,
|
VaultItemCipherType.LOGIN,
|
||||||
|
VaultItemCipherType.SSH_KEY,
|
||||||
),
|
),
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,6 +14,13 @@ class VaultItemListingTypeExtensionsTest {
|
||||||
VaultItemListingType.Folder(folderId = "mock"),
|
VaultItemListingType.Folder(folderId = "mock"),
|
||||||
VaultItemListingType.Trash,
|
VaultItemListingType.Trash,
|
||||||
VaultItemListingType.Collection(collectionId = "collectionId"),
|
VaultItemListingType.Collection(collectionId = "collectionId"),
|
||||||
|
VaultItemListingType.SshKey,
|
||||||
|
VaultItemListingType.SendFile,
|
||||||
|
VaultItemListingType.SendText,
|
||||||
|
VaultItemListingType.Card,
|
||||||
|
VaultItemListingType.Identity,
|
||||||
|
VaultItemListingType.Login,
|
||||||
|
VaultItemListingType.SecureNote,
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = itemListingTypeList.map { it.toItemListingType() }
|
val result = itemListingTypeList.map { it.toItemListingType() }
|
||||||
|
@ -25,6 +32,13 @@ class VaultItemListingTypeExtensionsTest {
|
||||||
VaultItemListingState.ItemListingType.Vault.Collection(
|
VaultItemListingState.ItemListingType.Vault.Collection(
|
||||||
collectionId = "collectionId",
|
collectionId = "collectionId",
|
||||||
),
|
),
|
||||||
|
VaultItemListingState.ItemListingType.Vault.SshKey,
|
||||||
|
VaultItemListingState.ItemListingType.Send.SendFile,
|
||||||
|
VaultItemListingState.ItemListingType.Send.SendText,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Card,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Identity,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.Login,
|
||||||
|
VaultItemListingState.ItemListingType.Vault.SecureNote,
|
||||||
),
|
),
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,6 +9,8 @@ import androidx.compose.ui.test.hasClickAction
|
||||||
import androidx.compose.ui.test.hasScrollToNodeAction
|
import androidx.compose.ui.test.hasScrollToNodeAction
|
||||||
import androidx.compose.ui.test.hasText
|
import androidx.compose.ui.test.hasText
|
||||||
import androidx.compose.ui.test.isDialog
|
import androidx.compose.ui.test.isDialog
|
||||||
|
import androidx.compose.ui.test.isDisplayed
|
||||||
|
import androidx.compose.ui.test.isNotDisplayed
|
||||||
import androidx.compose.ui.test.isPopup
|
import androidx.compose.ui.test.isPopup
|
||||||
import androidx.compose.ui.test.onAllNodesWithText
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
|
@ -704,6 +706,13 @@ class VaultScreenTest : BaseComposeTest() {
|
||||||
assertEquals(VaultItemListingType.SecureNote, onNavigateToVaultItemListingType)
|
assertEquals(VaultItemListingType.SecureNote, onNavigateToVaultItemListingType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `NavigateToItemListing event for SshKey type should call onNavigateToVaultItemListingType with SshKey type`() {
|
||||||
|
mutableEventFlow.tryEmit(VaultEvent.NavigateToItemListing(VaultItemListingType.SshKey))
|
||||||
|
assertEquals(VaultItemListingType.SshKey, onNavigateToVaultItemListingType)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
fun `NavigateToItemListing event for Trash type should call onNavigateToVaultItemListingType with Trash type`() {
|
fun `NavigateToItemListing event for Trash type should call onNavigateToVaultItemListingType with Trash type`() {
|
||||||
|
@ -1207,6 +1216,62 @@ class VaultScreenTest : BaseComposeTest() {
|
||||||
mutableEventFlow.tryEmit(VaultEvent.ShowSnackbar(data))
|
mutableEventFlow.tryEmit(VaultEvent.ShowSnackbar(data))
|
||||||
composeTestRule.onNodeWithText("message").assertIsDisplayed()
|
composeTestRule.onNodeWithText("message").assertIsDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SSH key group header should display correctly based on state`() {
|
||||||
|
val count = 1
|
||||||
|
// Verify SSH key group is displayed when showSshKeys is true
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
showSshKeys = true,
|
||||||
|
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||||
|
sshKeyItemsCount = count,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("SSH key")
|
||||||
|
.assertTextEquals("SSH key", count.toString())
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
// Verify SSH key group is hidden when showSshKeys is false
|
||||||
|
mutableStateFlow.update { it.copy(showSshKeys = false) }
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("SSH key")
|
||||||
|
.assertIsNotDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SSH key vault items should display correctly based on state`() {
|
||||||
|
// Verify SSH key vault items are displayed when showSshKeys is true
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
showSshKeys = true,
|
||||||
|
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||||
|
noFolderItems = listOf(
|
||||||
|
VaultState.ViewState.VaultItem.SshKey(
|
||||||
|
id = "mockId",
|
||||||
|
name = "mockSshKey".asText(),
|
||||||
|
publicKey = "mockPublicKey".asText(),
|
||||||
|
privateKey = "mockPrivateKey".asText(),
|
||||||
|
fingerprint = "mockFingerprint".asText(),
|
||||||
|
overflowOptions = emptyList(),
|
||||||
|
shouldShowMasterPasswordReprompt = false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithTextAfterScroll("mockSshKey")
|
||||||
|
.isDisplayed()
|
||||||
|
|
||||||
|
// Verify SSH key vault items are hidden when showSshKeys is false
|
||||||
|
mutableStateFlow.update { it.copy(showSshKeys = false) }
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("mockSshKey")
|
||||||
|
.isNotDisplayed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
|
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
|
||||||
|
@ -1262,6 +1327,7 @@ private val DEFAULT_STATE: VaultState = VaultState(
|
||||||
hideNotificationsDialog = true,
|
hideNotificationsDialog = true,
|
||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
showImportActionCard = false,
|
showImportActionCard = false,
|
||||||
|
showSshKeys = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_CONTENT_VIEW_STATE: VaultState.ViewState.Content = VaultState.ViewState.Content(
|
private val DEFAULT_CONTENT_VIEW_STATE: VaultState.ViewState.Content = VaultState.ViewState.Content(
|
||||||
|
@ -1275,4 +1341,6 @@ private val DEFAULT_CONTENT_VIEW_STATE: VaultState.ViewState.Content = VaultStat
|
||||||
collectionItems = emptyList(),
|
collectionItems = emptyList(),
|
||||||
trashItemsCount = 0,
|
trashItemsCount = 0,
|
||||||
totpItemsCount = 0,
|
totpItemsCount = 0,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.x8bit.bitwarden.ui.vault.feature.vault
|
package com.x8bit.bitwarden.ui.vault.feature.vault
|
||||||
|
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
|
import com.bitwarden.vault.CipherType
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
|
@ -124,10 +125,17 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mutableImportLoginsFeatureFlow = MutableStateFlow(true)
|
private val mutableImportLoginsFeatureFlow = MutableStateFlow(true)
|
||||||
|
private val mutableSshKeyVaultItemsEnabledFlow = MutableStateFlow(false)
|
||||||
private val featureFlagManager: FeatureFlagManager = mockk {
|
private val featureFlagManager: FeatureFlagManager = mockk {
|
||||||
every {
|
every {
|
||||||
getFeatureFlagFlow(FlagKey.ImportLoginsFlow)
|
getFeatureFlagFlow(FlagKey.ImportLoginsFlow)
|
||||||
} returns mutableImportLoginsFeatureFlow
|
} returns mutableImportLoginsFeatureFlow
|
||||||
|
every {
|
||||||
|
getFeatureFlagFlow(FlagKey.SshKeyCipherItems)
|
||||||
|
} returns mutableSshKeyVaultItemsEnabledFlow
|
||||||
|
every {
|
||||||
|
getFeatureFlag(FlagKey.SshKeyCipherItems)
|
||||||
|
} returns mutableSshKeyVaultItemsEnabledFlow.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -526,6 +534,7 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
isIconLoadingDisabled = viewModel.stateFlow.value.isIconLoadingDisabled,
|
isIconLoadingDisabled = viewModel.stateFlow.value.isIconLoadingDisabled,
|
||||||
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
|
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.copy(
|
.copy(
|
||||||
|
@ -550,6 +559,7 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
isIconLoadingDisabled = viewModel.stateFlow.value.isIconLoadingDisabled,
|
isIconLoadingDisabled = viewModel.stateFlow.value.isIconLoadingDisabled,
|
||||||
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
|
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
|
@ -559,12 +569,31 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `vaultDataStateFlow Loaded with items should update state to Content`() = runTest {
|
fun `vaultDataStateFlow Loaded with items should update state to Content`() = runTest {
|
||||||
|
mutableSshKeyVaultItemsEnabledFlow.value = true
|
||||||
mutableVaultDataStateFlow.tryEmit(
|
mutableVaultDataStateFlow.tryEmit(
|
||||||
value = DataState.Loaded(
|
value = DataState.Loaded(
|
||||||
data = VaultData(
|
data = VaultData(
|
||||||
cipherViewList = listOf(createMockCipherView(number = 1)),
|
cipherViewList = listOf(
|
||||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
createMockCipherView(number = 1, cipherType = CipherType.LOGIN),
|
||||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
createMockCipherView(number = 2, cipherType = CipherType.CARD),
|
||||||
|
createMockCipherView(number = 3, cipherType = CipherType.IDENTITY),
|
||||||
|
createMockCipherView(number = 4, cipherType = CipherType.SECURE_NOTE),
|
||||||
|
createMockCipherView(number = 5, cipherType = CipherType.SSH_KEY),
|
||||||
|
),
|
||||||
|
collectionViewList = listOf(
|
||||||
|
createMockCollectionView(number = 1),
|
||||||
|
createMockCollectionView(number = 2),
|
||||||
|
createMockCollectionView(number = 3),
|
||||||
|
createMockCollectionView(number = 4),
|
||||||
|
createMockCollectionView(number = 5),
|
||||||
|
),
|
||||||
|
folderViewList = listOf(
|
||||||
|
createMockFolderView(number = 1),
|
||||||
|
createMockFolderView(number = 2),
|
||||||
|
createMockFolderView(number = 3),
|
||||||
|
createMockFolderView(number = 4),
|
||||||
|
createMockFolderView(number = 5),
|
||||||
|
),
|
||||||
sendViewList = listOf(createMockSendView(number = 1)),
|
sendViewList = listOf(createMockSendView(number = 1)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -576,9 +605,9 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
createMockVaultState(
|
createMockVaultState(
|
||||||
viewState = VaultState.ViewState.Content(
|
viewState = VaultState.ViewState.Content(
|
||||||
loginItemsCount = 1,
|
loginItemsCount = 1,
|
||||||
cardItemsCount = 0,
|
cardItemsCount = 1,
|
||||||
identityItemsCount = 0,
|
identityItemsCount = 1,
|
||||||
secureNoteItemsCount = 0,
|
secureNoteItemsCount = 1,
|
||||||
favoriteItems = listOf(),
|
favoriteItems = listOf(),
|
||||||
folderItems = listOf(
|
folderItems = listOf(
|
||||||
VaultState.ViewState.FolderItem(
|
VaultState.ViewState.FolderItem(
|
||||||
|
@ -586,6 +615,26 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
name = "mockName-1".asText(),
|
name = "mockName-1".asText(),
|
||||||
itemCount = 1,
|
itemCount = 1,
|
||||||
),
|
),
|
||||||
|
VaultState.ViewState.FolderItem(
|
||||||
|
id = "mockId-2",
|
||||||
|
name = "mockName-2".asText(),
|
||||||
|
itemCount = 1,
|
||||||
|
),
|
||||||
|
VaultState.ViewState.FolderItem(
|
||||||
|
id = "mockId-3",
|
||||||
|
name = "mockName-3".asText(),
|
||||||
|
itemCount = 1,
|
||||||
|
),
|
||||||
|
VaultState.ViewState.FolderItem(
|
||||||
|
id = "mockId-4",
|
||||||
|
name = "mockName-4".asText(),
|
||||||
|
itemCount = 1,
|
||||||
|
),
|
||||||
|
VaultState.ViewState.FolderItem(
|
||||||
|
id = "mockId-5",
|
||||||
|
name = "mockName-5".asText(),
|
||||||
|
itemCount = 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
collectionItems = listOf(
|
collectionItems = listOf(
|
||||||
VaultState.ViewState.CollectionItem(
|
VaultState.ViewState.CollectionItem(
|
||||||
|
@ -593,11 +642,34 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
name = "mockName-1",
|
name = "mockName-1",
|
||||||
itemCount = 1,
|
itemCount = 1,
|
||||||
),
|
),
|
||||||
|
VaultState.ViewState.CollectionItem(
|
||||||
|
id = "mockId-2",
|
||||||
|
name = "mockName-2",
|
||||||
|
itemCount = 1,
|
||||||
|
),
|
||||||
|
VaultState.ViewState.CollectionItem(
|
||||||
|
id = "mockId-3",
|
||||||
|
name = "mockName-3",
|
||||||
|
itemCount = 1,
|
||||||
|
),
|
||||||
|
VaultState.ViewState.CollectionItem(
|
||||||
|
id = "mockId-4",
|
||||||
|
name = "mockName-4",
|
||||||
|
itemCount = 1,
|
||||||
|
),
|
||||||
|
VaultState.ViewState.CollectionItem(
|
||||||
|
id = "mockId-5",
|
||||||
|
name = "mockName-5",
|
||||||
|
itemCount = 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 0,
|
trashItemsCount = 0,
|
||||||
totpItemsCount = 1,
|
totpItemsCount = 1,
|
||||||
|
itemTypesCount = CipherType.entries.size,
|
||||||
|
sshKeyItemsCount = 1,
|
||||||
),
|
),
|
||||||
|
showSshKeys = true,
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
|
@ -619,6 +691,8 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 0,
|
trashItemsCount = 0,
|
||||||
totpItemsCount = 1,
|
totpItemsCount = 1,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
@ -731,6 +805,8 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 0,
|
trashItemsCount = 0,
|
||||||
totpItemsCount = 1,
|
totpItemsCount = 1,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
|
@ -829,6 +905,8 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 0,
|
trashItemsCount = 0,
|
||||||
totpItemsCount = 1,
|
totpItemsCount = 1,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
),
|
),
|
||||||
dialog = VaultState.DialogState.Error(
|
dialog = VaultState.DialogState.Error(
|
||||||
title = R.string.an_error_has_occurred.asText(),
|
title = R.string.an_error_has_occurred.asText(),
|
||||||
|
@ -927,6 +1005,8 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 0,
|
trashItemsCount = 0,
|
||||||
totpItemsCount = 1,
|
totpItemsCount = 1,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
),
|
),
|
||||||
dialog = VaultState.DialogState.Error(
|
dialog = VaultState.DialogState.Error(
|
||||||
title = R.string.internet_connection_required_title.asText(),
|
title = R.string.internet_connection_required_title.asText(),
|
||||||
|
@ -999,6 +1079,88 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow Loaded should exclude SSH key vault items when showSshKeys is false`() =
|
||||||
|
runTest {
|
||||||
|
mutableVaultDataStateFlow.tryEmit(
|
||||||
|
value = DataState.Loaded(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = listOf(
|
||||||
|
createMockCipherView(number = 1),
|
||||||
|
createMockCipherView(number = 1, cipherType = CipherType.SSH_KEY),
|
||||||
|
),
|
||||||
|
collectionViewList = listOf(),
|
||||||
|
folderViewList = listOf(),
|
||||||
|
sendViewList = listOf(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
createMockVaultState(
|
||||||
|
viewState = VaultState.ViewState.Content(
|
||||||
|
loginItemsCount = 1,
|
||||||
|
cardItemsCount = 0,
|
||||||
|
identityItemsCount = 0,
|
||||||
|
secureNoteItemsCount = 0,
|
||||||
|
favoriteItems = listOf(),
|
||||||
|
folderItems = listOf(),
|
||||||
|
collectionItems = listOf(),
|
||||||
|
noFolderItems = listOf(),
|
||||||
|
trashItemsCount = 0,
|
||||||
|
totpItemsCount = 1,
|
||||||
|
itemTypesCount = CipherType.entries.size - 1,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `vaultDataStateFlow Loaded should include SSH key vault items when showSshKeys is true`() =
|
||||||
|
runTest {
|
||||||
|
mutableSshKeyVaultItemsEnabledFlow.value = true
|
||||||
|
mutableVaultDataStateFlow.tryEmit(
|
||||||
|
value = DataState.Loaded(
|
||||||
|
data = VaultData(
|
||||||
|
cipherViewList = listOf(
|
||||||
|
createMockCipherView(number = 1),
|
||||||
|
createMockCipherView(number = 1, cipherType = CipherType.SSH_KEY),
|
||||||
|
),
|
||||||
|
collectionViewList = listOf(),
|
||||||
|
folderViewList = listOf(),
|
||||||
|
sendViewList = listOf(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
createMockVaultState(
|
||||||
|
viewState = VaultState.ViewState.Content(
|
||||||
|
loginItemsCount = 1,
|
||||||
|
cardItemsCount = 0,
|
||||||
|
identityItemsCount = 0,
|
||||||
|
secureNoteItemsCount = 0,
|
||||||
|
favoriteItems = listOf(),
|
||||||
|
folderItems = listOf(),
|
||||||
|
collectionItems = listOf(),
|
||||||
|
noFolderItems = listOf(),
|
||||||
|
trashItemsCount = 0,
|
||||||
|
totpItemsCount = 1,
|
||||||
|
itemTypesCount = CipherType.entries.size,
|
||||||
|
sshKeyItemsCount = 1,
|
||||||
|
),
|
||||||
|
showSshKeys = true,
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `VerificationCodesClick should emit NavigateToVerificationCodeScreen`() = runTest {
|
fun `VerificationCodesClick should emit NavigateToVerificationCodeScreen`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
@ -1112,6 +1274,18 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SshKeyGroupClick should emit NavigateToItemListing event with SshKey type`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(VaultAction.SshKeyGroupClick)
|
||||||
|
assertEquals(
|
||||||
|
VaultEvent.NavigateToItemListing(VaultItemListingType.SshKey),
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `TrashClick should emit NavigateToItemListing event with Trash type`() = runTest {
|
fun `TrashClick should emit NavigateToItemListing event with Trash type`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
@ -1720,6 +1894,7 @@ private val DEFAULT_USER_STATE = UserState(
|
||||||
private fun createMockVaultState(
|
private fun createMockVaultState(
|
||||||
viewState: VaultState.ViewState,
|
viewState: VaultState.ViewState,
|
||||||
dialog: VaultState.DialogState? = null,
|
dialog: VaultState.DialogState? = null,
|
||||||
|
showSshKeys: Boolean = false,
|
||||||
): VaultState =
|
): VaultState =
|
||||||
VaultState(
|
VaultState(
|
||||||
appBarTitle = R.string.my_vault.asText(),
|
appBarTitle = R.string.my_vault.asText(),
|
||||||
|
@ -1758,4 +1933,5 @@ private fun createMockVaultState(
|
||||||
hideNotificationsDialog = true,
|
hideNotificationsDialog = true,
|
||||||
showImportActionCard = true,
|
showImportActionCard = true,
|
||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
|
showSshKeys = showSshKeys,
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import com.bitwarden.vault.LoginView
|
||||||
import com.bitwarden.vault.PasswordHistoryView
|
import com.bitwarden.vault.PasswordHistoryView
|
||||||
import com.bitwarden.vault.SecureNoteType
|
import com.bitwarden.vault.SecureNoteType
|
||||||
import com.bitwarden.vault.SecureNoteView
|
import com.bitwarden.vault.SecureNoteView
|
||||||
|
import com.bitwarden.vault.SshKeyView
|
||||||
import com.bitwarden.vault.UriMatchType
|
import com.bitwarden.vault.UriMatchType
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
|
||||||
|
@ -112,6 +113,7 @@ class VaultAddItemStateExtensionsTest {
|
||||||
creationDate = Instant.MIN,
|
creationDate = Instant.MIN,
|
||||||
deletedDate = null,
|
deletedDate = null,
|
||||||
revisionDate = Instant.MIN,
|
revisionDate = Instant.MIN,
|
||||||
|
sshKey = null,
|
||||||
),
|
),
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
|
@ -295,6 +297,7 @@ class VaultAddItemStateExtensionsTest {
|
||||||
creationDate = Instant.MIN,
|
creationDate = Instant.MIN,
|
||||||
deletedDate = null,
|
deletedDate = null,
|
||||||
revisionDate = Instant.MIN,
|
revisionDate = Instant.MIN,
|
||||||
|
sshKey = null,
|
||||||
),
|
),
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
|
@ -426,6 +429,7 @@ class VaultAddItemStateExtensionsTest {
|
||||||
creationDate = Instant.MIN,
|
creationDate = Instant.MIN,
|
||||||
deletedDate = null,
|
deletedDate = null,
|
||||||
revisionDate = Instant.MIN,
|
revisionDate = Instant.MIN,
|
||||||
|
sshKey = null,
|
||||||
),
|
),
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
|
@ -611,6 +615,7 @@ class VaultAddItemStateExtensionsTest {
|
||||||
creationDate = Instant.MIN,
|
creationDate = Instant.MIN,
|
||||||
deletedDate = null,
|
deletedDate = null,
|
||||||
revisionDate = Instant.MIN,
|
revisionDate = Instant.MIN,
|
||||||
|
sshKey = null,
|
||||||
),
|
),
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
|
@ -703,6 +708,65 @@ class VaultAddItemStateExtensionsTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toCipherView should transform SSH Key ItemType to CipherView`() {
|
||||||
|
mockkStatic(Instant::class)
|
||||||
|
every { Instant.now() } returns Instant.MIN
|
||||||
|
val viewState = VaultAddEditState.ViewState.Content(
|
||||||
|
common = VaultAddEditState.ViewState.Content.Common(
|
||||||
|
name = "mockName-1",
|
||||||
|
selectedFolderId = "mockId-1",
|
||||||
|
favorite = false,
|
||||||
|
masterPasswordReprompt = false,
|
||||||
|
notes = "mockNotes-1",
|
||||||
|
selectedOwnerId = "mockOwnerId-1",
|
||||||
|
),
|
||||||
|
isIndividualVaultDisabled = false,
|
||||||
|
type = VaultAddEditState.ViewState.Content.ItemType.SshKey(
|
||||||
|
publicKey = "mockPublicKey-1",
|
||||||
|
privateKey = "mockPrivateKey-1",
|
||||||
|
fingerprint = "mockFingerprint-1",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = viewState.toCipherView()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
CipherView(
|
||||||
|
id = null,
|
||||||
|
organizationId = "mockOwnerId-1",
|
||||||
|
folderId = "mockId-1",
|
||||||
|
collectionIds = emptyList(),
|
||||||
|
key = null,
|
||||||
|
name = "mockName-1",
|
||||||
|
notes = "mockNotes-1",
|
||||||
|
type = CipherType.SSH_KEY,
|
||||||
|
login = null,
|
||||||
|
identity = null,
|
||||||
|
card = null,
|
||||||
|
secureNote = null,
|
||||||
|
favorite = false,
|
||||||
|
reprompt = CipherRepromptType.NONE,
|
||||||
|
organizationUseTotp = false,
|
||||||
|
edit = true,
|
||||||
|
viewPassword = true,
|
||||||
|
localData = null,
|
||||||
|
attachments = null,
|
||||||
|
fields = emptyList(),
|
||||||
|
passwordHistory = null,
|
||||||
|
creationDate = Instant.MIN,
|
||||||
|
deletedDate = null,
|
||||||
|
revisionDate = Instant.MIN,
|
||||||
|
sshKey = SshKeyView(
|
||||||
|
publicKey = "mockPublicKey-1",
|
||||||
|
privateKey = "mockPrivateKey-1",
|
||||||
|
fingerprint = "mockFingerprint-1",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `toLoginView should transform Login ItemType to LoginView deleting fido2Credentials with original cipher`() {
|
fun `toLoginView should transform Login ItemType to LoginView deleting fido2Credentials with original cipher`() {
|
||||||
|
@ -911,6 +975,7 @@ private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView(
|
||||||
creationDate = Instant.MIN,
|
creationDate = Instant.MIN,
|
||||||
deletedDate = null,
|
deletedDate = null,
|
||||||
revisionDate = Instant.MIN,
|
revisionDate = Instant.MIN,
|
||||||
|
sshKey = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_LOGIN_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
|
private val DEFAULT_LOGIN_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.x8bit.bitwarden.ui.vault.feature.vault.util
|
package com.x8bit.bitwarden.ui.vault.feature.vault.util
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import com.bitwarden.vault.CipherRepromptType
|
||||||
import com.bitwarden.vault.CipherType
|
import com.bitwarden.vault.CipherType
|
||||||
import com.bitwarden.vault.FolderView
|
import com.bitwarden.vault.FolderView
|
||||||
import com.bitwarden.vault.LoginUriView
|
import com.bitwarden.vault.LoginUriView
|
||||||
|
@ -12,9 +13,12 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSshKeyView
|
||||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.model.IconRes
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
|
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
@ -57,6 +61,7 @@ class VaultDataExtensionsTest {
|
||||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||||
vaultFilterType = VaultFilterType.AllVaults,
|
vaultFilterType = VaultFilterType.AllVaults,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -94,6 +99,8 @@ class VaultDataExtensionsTest {
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 0,
|
trashItemsCount = 0,
|
||||||
totpItemsCount = 1,
|
totpItemsCount = 1,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
),
|
),
|
||||||
actual,
|
actual,
|
||||||
)
|
)
|
||||||
|
@ -118,6 +125,7 @@ class VaultDataExtensionsTest {
|
||||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||||
vaultFilterType = VaultFilterType.MyVault,
|
vaultFilterType = VaultFilterType.MyVault,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -138,6 +146,8 @@ class VaultDataExtensionsTest {
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 0,
|
trashItemsCount = 0,
|
||||||
totpItemsCount = 1,
|
totpItemsCount = 1,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
),
|
),
|
||||||
actual,
|
actual,
|
||||||
)
|
)
|
||||||
|
@ -171,6 +181,7 @@ class VaultDataExtensionsTest {
|
||||||
organizationName = "Mock Organization 1",
|
organizationName = "Mock Organization 1",
|
||||||
),
|
),
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -197,6 +208,8 @@ class VaultDataExtensionsTest {
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 0,
|
trashItemsCount = 0,
|
||||||
totpItemsCount = 1,
|
totpItemsCount = 1,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
),
|
),
|
||||||
actual,
|
actual,
|
||||||
)
|
)
|
||||||
|
@ -217,6 +230,7 @@ class VaultDataExtensionsTest {
|
||||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||||
vaultFilterType = VaultFilterType.AllVaults,
|
vaultFilterType = VaultFilterType.AllVaults,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -240,6 +254,7 @@ class VaultDataExtensionsTest {
|
||||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||||
vaultFilterType = VaultFilterType.AllVaults,
|
vaultFilterType = VaultFilterType.AllVaults,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -264,6 +279,7 @@ class VaultDataExtensionsTest {
|
||||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||||
vaultFilterType = VaultFilterType.AllVaults,
|
vaultFilterType = VaultFilterType.AllVaults,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -278,6 +294,8 @@ class VaultDataExtensionsTest {
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 0,
|
trashItemsCount = 0,
|
||||||
totpItemsCount = 1,
|
totpItemsCount = 1,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
),
|
),
|
||||||
actual,
|
actual,
|
||||||
)
|
)
|
||||||
|
@ -299,6 +317,7 @@ class VaultDataExtensionsTest {
|
||||||
isIconLoadingDisabled = false,
|
isIconLoadingDisabled = false,
|
||||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -313,6 +332,8 @@ class VaultDataExtensionsTest {
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 0,
|
trashItemsCount = 0,
|
||||||
totpItemsCount = 0,
|
totpItemsCount = 0,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
),
|
),
|
||||||
actual,
|
actual,
|
||||||
)
|
)
|
||||||
|
@ -334,6 +355,7 @@ class VaultDataExtensionsTest {
|
||||||
isIconLoadingDisabled = false,
|
isIconLoadingDisabled = false,
|
||||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -348,6 +370,8 @@ class VaultDataExtensionsTest {
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 0,
|
trashItemsCount = 0,
|
||||||
totpItemsCount = 1,
|
totpItemsCount = 1,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
),
|
),
|
||||||
actual,
|
actual,
|
||||||
)
|
)
|
||||||
|
@ -371,6 +395,7 @@ class VaultDataExtensionsTest {
|
||||||
isIconLoadingDisabled = false,
|
isIconLoadingDisabled = false,
|
||||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -385,6 +410,8 @@ class VaultDataExtensionsTest {
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 0,
|
trashItemsCount = 0,
|
||||||
totpItemsCount = 1,
|
totpItemsCount = 1,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
),
|
),
|
||||||
actual,
|
actual,
|
||||||
)
|
)
|
||||||
|
@ -592,6 +619,7 @@ class VaultDataExtensionsTest {
|
||||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||||
vaultFilterType = VaultFilterType.AllVaults,
|
vaultFilterType = VaultFilterType.AllVaults,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -606,6 +634,8 @@ class VaultDataExtensionsTest {
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 2,
|
trashItemsCount = 2,
|
||||||
totpItemsCount = 1,
|
totpItemsCount = 1,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
),
|
),
|
||||||
actual,
|
actual,
|
||||||
)
|
)
|
||||||
|
@ -629,6 +659,7 @@ class VaultDataExtensionsTest {
|
||||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||||
vaultFilterType = VaultFilterType.AllVaults,
|
vaultFilterType = VaultFilterType.AllVaults,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -643,6 +674,8 @@ class VaultDataExtensionsTest {
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 2,
|
trashItemsCount = 2,
|
||||||
totpItemsCount = 0,
|
totpItemsCount = 0,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
),
|
),
|
||||||
actual,
|
actual,
|
||||||
)
|
)
|
||||||
|
@ -669,6 +702,7 @@ class VaultDataExtensionsTest {
|
||||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||||
vaultFilterType = VaultFilterType.AllVaults,
|
vaultFilterType = VaultFilterType.AllVaults,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -689,6 +723,8 @@ class VaultDataExtensionsTest {
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 0,
|
trashItemsCount = 0,
|
||||||
totpItemsCount = 100,
|
totpItemsCount = 100,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
),
|
),
|
||||||
actual,
|
actual,
|
||||||
)
|
)
|
||||||
|
@ -722,6 +758,7 @@ class VaultDataExtensionsTest {
|
||||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||||
vaultFilterType = VaultFilterType.AllVaults,
|
vaultFilterType = VaultFilterType.AllVaults,
|
||||||
hasMasterPassword = true,
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -764,8 +801,195 @@ class VaultDataExtensionsTest {
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 0,
|
trashItemsCount = 0,
|
||||||
totpItemsCount = 1,
|
totpItemsCount = 1,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
|
),
|
||||||
|
actual,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toViewState should exclude SSH keys if showSshKeys is false`() {
|
||||||
|
val vaultData = VaultData(
|
||||||
|
cipherViewList = listOf(
|
||||||
|
createMockCipherView(number = 1),
|
||||||
|
createMockCipherView(number = 2, cipherType = CipherType.SSH_KEY),
|
||||||
|
),
|
||||||
|
collectionViewList = listOf(),
|
||||||
|
folderViewList = listOf(),
|
||||||
|
sendViewList = listOf(),
|
||||||
|
fido2CredentialAutofillViewList = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
val actual = vaultData.toViewState(
|
||||||
|
isPremium = true,
|
||||||
|
isIconLoadingDisabled = false,
|
||||||
|
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||||
|
vaultFilterType = VaultFilterType.AllVaults,
|
||||||
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultState.ViewState.Content(
|
||||||
|
loginItemsCount = 1,
|
||||||
|
cardItemsCount = 0,
|
||||||
|
identityItemsCount = 0,
|
||||||
|
secureNoteItemsCount = 0,
|
||||||
|
// Verify SSH key vault items are not counted when showSshKeys is false.
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
|
favoriteItems = listOf(),
|
||||||
|
collectionItems = listOf(),
|
||||||
|
folderItems = listOf(),
|
||||||
|
noFolderItems = listOf(),
|
||||||
|
trashItemsCount = 0,
|
||||||
|
totpItemsCount = 1,
|
||||||
|
// Verify item types count excludes CipherType.SSH_KEY when showSshKeys is false.
|
||||||
|
itemTypesCount = 4,
|
||||||
|
),
|
||||||
|
actual,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toViewState should include SSH key vault items and type count if showSshKeys is true`() {
|
||||||
|
val vaultData = VaultData(
|
||||||
|
cipherViewList = listOf(
|
||||||
|
createMockCipherView(number = 1),
|
||||||
|
createMockCipherView(number = 2, cipherType = CipherType.SSH_KEY),
|
||||||
|
),
|
||||||
|
collectionViewList = listOf(),
|
||||||
|
folderViewList = listOf(),
|
||||||
|
sendViewList = listOf(),
|
||||||
|
fido2CredentialAutofillViewList = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
val actual = vaultData.toViewState(
|
||||||
|
isPremium = true,
|
||||||
|
isIconLoadingDisabled = false,
|
||||||
|
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||||
|
vaultFilterType = VaultFilterType.AllVaults,
|
||||||
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultState.ViewState.Content(
|
||||||
|
loginItemsCount = 1,
|
||||||
|
cardItemsCount = 0,
|
||||||
|
identityItemsCount = 0,
|
||||||
|
secureNoteItemsCount = 0,
|
||||||
|
// Verify SSH key vault items are counted
|
||||||
|
sshKeyItemsCount = 1,
|
||||||
|
favoriteItems = listOf(),
|
||||||
|
collectionItems = listOf(),
|
||||||
|
folderItems = listOf(),
|
||||||
|
noFolderItems = listOf(),
|
||||||
|
trashItemsCount = 0,
|
||||||
|
totpItemsCount = 1,
|
||||||
|
// Verify item types count includes all CipherTypes when showSshKeys is true.
|
||||||
|
itemTypesCount = CipherType.entries.size,
|
||||||
|
),
|
||||||
|
actual,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toViewState should transform SSH key vault items into correct vault item`() {
|
||||||
|
val vaultData = VaultData(
|
||||||
|
cipherViewList = listOf(
|
||||||
|
createMockCipherView(number = 1, cipherType = CipherType.SSH_KEY, folderId = null),
|
||||||
|
createMockCipherView(
|
||||||
|
number = 2,
|
||||||
|
cipherType = CipherType.SSH_KEY,
|
||||||
|
repromptType = CipherRepromptType.PASSWORD,
|
||||||
|
folderId = null,
|
||||||
|
sshKey = createMockSshKeyView(number = 1)
|
||||||
|
.copy(
|
||||||
|
publicKey = null,
|
||||||
|
privateKey = null,
|
||||||
|
fingerprint = null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
createMockCipherView(
|
||||||
|
number = 3,
|
||||||
|
cipherType = CipherType.SSH_KEY,
|
||||||
|
folderId = null,
|
||||||
|
sshKey = null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
collectionViewList = listOf(),
|
||||||
|
folderViewList = listOf(),
|
||||||
|
sendViewList = listOf(),
|
||||||
|
)
|
||||||
|
val actual = vaultData.toViewState(
|
||||||
|
isPremium = true,
|
||||||
|
isIconLoadingDisabled = false,
|
||||||
|
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||||
|
vaultFilterType = VaultFilterType.AllVaults,
|
||||||
|
hasMasterPassword = true,
|
||||||
|
showSshKeys = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
VaultState.ViewState.Content(
|
||||||
|
loginItemsCount = 0,
|
||||||
|
cardItemsCount = 0,
|
||||||
|
identityItemsCount = 0,
|
||||||
|
secureNoteItemsCount = 0,
|
||||||
|
sshKeyItemsCount = 3,
|
||||||
|
favoriteItems = listOf(),
|
||||||
|
collectionItems = listOf(),
|
||||||
|
folderItems = listOf(),
|
||||||
|
noFolderItems = listOf(
|
||||||
|
createMockSshKeyVaultItem(number = 1),
|
||||||
|
createMockSshKeyVaultItem(number = 2)
|
||||||
|
.copy(
|
||||||
|
publicKey = null,
|
||||||
|
privateKey = null,
|
||||||
|
fingerprint = null,
|
||||||
|
shouldShowMasterPasswordReprompt = true,
|
||||||
|
),
|
||||||
|
createMockSshKeyVaultItem(number = 3)
|
||||||
|
.copy(
|
||||||
|
publicKey = null,
|
||||||
|
privateKey = null,
|
||||||
|
fingerprint = null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trashItemsCount = 0,
|
||||||
|
totpItemsCount = 0,
|
||||||
|
itemTypesCount = CipherType.entries.size,
|
||||||
),
|
),
|
||||||
actual,
|
actual,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createMockSshKeyVaultItem(number: Int): VaultState.ViewState.VaultItem.SshKey =
|
||||||
|
VaultState.ViewState.VaultItem.SshKey(
|
||||||
|
id = "mockId-$number",
|
||||||
|
name = "mockName-$number".asText(),
|
||||||
|
publicKey = "mockPublicKey-$number".asText(),
|
||||||
|
privateKey = "mockPrivateKey-$number".asText(),
|
||||||
|
fingerprint = "mockKeyFingerprint-$number".asText(),
|
||||||
|
overflowOptions = listOf(
|
||||||
|
ListingItemOverflowAction.VaultAction.ViewClick("mockId-$number"),
|
||||||
|
ListingItemOverflowAction.VaultAction.EditClick("mockId-$number", true),
|
||||||
|
),
|
||||||
|
startIcon = IconData.Local(iconRes = R.drawable.ic_ssh_key),
|
||||||
|
startIconTestTag = "SshKeyCipherIcon",
|
||||||
|
extraIconList = listOf(
|
||||||
|
IconRes(
|
||||||
|
iconRes = R.drawable.ic_collections,
|
||||||
|
contentDescription = R.string.collections.asText(),
|
||||||
|
testTag = "CipherInCollectionIcon",
|
||||||
|
),
|
||||||
|
IconRes(
|
||||||
|
iconRes = R.drawable.ic_paperclip,
|
||||||
|
contentDescription = R.string.attachments.asText(),
|
||||||
|
testTag = "CipherWithAttachmentsIcon",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
shouldShowMasterPasswordReprompt = false,
|
||||||
|
)
|
||||||
|
|
|
@ -83,5 +83,7 @@ class VaultStateExtensionsTest {
|
||||||
noFolderItems = listOf(),
|
noFolderItems = listOf(),
|
||||||
trashItemsCount = 0,
|
trashItemsCount = 0,
|
||||||
totpItemsCount = 1,
|
totpItemsCount = 1,
|
||||||
|
itemTypesCount = 4,
|
||||||
|
sshKeyItemsCount = 0,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ androidxSplash = "1.1.0-rc01"
|
||||||
androidXAppCompat = "1.7.0"
|
androidXAppCompat = "1.7.0"
|
||||||
androdixAutofill = "1.1.0"
|
androdixAutofill = "1.1.0"
|
||||||
androidxWork = "2.9.1"
|
androidxWork = "2.9.1"
|
||||||
bitwardenSdk = "1.0.0-20240924.112512-21"
|
bitwardenSdk = "1.0.0-20241021.160919-71"
|
||||||
crashlytics = "3.0.2"
|
crashlytics = "3.0.2"
|
||||||
detekt = "1.23.7"
|
detekt = "1.23.7"
|
||||||
firebaseBom = "33.5.1"
|
firebaseBom = "33.5.1"
|
||||||
|
|
Loading…
Reference in a new issue