From be127f5d490ce10e93f85dbbf77ba43a4adbfb4e Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:35:35 -0400 Subject: [PATCH] BIT-2046 Display passkey fields in Vault (#1143) --- .../network/model/SyncResponseJson.kt | 1 + .../feature/addedit/VaultAddEditLoginItems.kt | 28 ++++++ .../feature/addedit/VaultAddEditViewModel.kt | 6 ++ .../addedit/util/CipherViewExtensions.kt | 34 ++++++- .../feature/item/VaultItemLoginContent.kt | 27 +++++ .../ui/vault/feature/item/VaultItemScreen.kt | 22 ++++- .../vault/feature/item/VaultItemViewModel.kt | 51 +++++++++- .../feature/item/util/CipherViewExtensions.kt | 31 +++++- .../datasource/disk/VaultDiskSourceTest.kt | 2 +- .../network/model/SyncResponseCipherUtil.kt | 17 ++-- .../network/service/CiphersServiceTest.kt | 4 +- .../network/service/SyncServiceTest.kt | 2 +- .../datasource/sdk/model/CipherViewUtil.kt | 82 ++++++++------- .../sdk/model/VaultSdkCipherUtil.kt | 44 +++++---- .../data/vault/manager/TotpCodeManagerTest.kt | 4 +- .../vault/repository/VaultRepositoryTest.kt | 60 +++++------ .../util/VaultSdkCipherExtensionsTest.kt | 31 ++++-- .../feature/search/SearchViewModelTest.kt | 6 +- .../addedit/VaultAddEditViewModelTest.kt | 23 +++++ .../addedit/util/CipherViewExtensionsTest.kt | 44 +++++++-- .../vault/feature/item/VaultItemScreenTest.kt | 62 +++++++++++- .../feature/item/VaultItemViewModelTest.kt | 99 +++++++++++++++++++ .../feature/item/util/VaultItemTestUtil.kt | 29 +++++- .../util/VaultAddItemStateExtensionsTest.kt | 1 + 24 files changed, 582 insertions(+), 128 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseJson.kt index d40678b869..feec343aaa 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseJson.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseJson.kt @@ -711,6 +711,7 @@ data class SyncResponseJson( * @property shouldAutofillOnPageLoad If autofill is used on page load (nullable). * @property uri The URI (nullable). * @property username The username (nullable). + * @property fido2Credentials A list of FIDO 2 credentials (nullable). */ @Serializable data class Login( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt index 4fcc10a02e..0bf48c316e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledTonalButton import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledTonalButtonWithIcon import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog @@ -81,6 +82,18 @@ fun LazyListScope.vaultAddEditLoginItems( ) } + loginState.fido2CredentialCreationDateTime?.let { creationDateTime -> + item { + Spacer(modifier = Modifier.height(8.dp)) + PasskeyField( + creationDateTime = creationDateTime, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + } + item { Spacer(modifier = Modifier.height(24.dp)) BitwardenListHeaderText( @@ -476,3 +489,18 @@ private fun PasswordRow( ) } } + +@Composable +private fun PasskeyField( + creationDateTime: Text, + modifier: Modifier = Modifier, +) { + BitwardenTextField( + label = stringResource(id = R.string.passkey), + value = creationDateTime.invoke(), + onValueChange = { }, + readOnly = true, + singleLine = true, + modifier = modifier, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt index 7d39fb4789..3b2b1f1875 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt @@ -55,6 +55,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize +import java.time.Clock import java.util.Collections import java.util.UUID import javax.inject.Inject @@ -82,6 +83,7 @@ class VaultAddEditViewModel @Inject constructor( private val settingsRepository: SettingsRepository, private val specialCircumstanceManager: SpecialCircumstanceManager, private val resourceManager: ResourceManager, + private val clock: Clock, ) : BaseViewModel( // We load the state from the savedStateHandle for testing purposes. initialState = savedStateHandle[KEY_STATE] @@ -1164,6 +1166,7 @@ class VaultAddEditViewModel @Inject constructor( isClone = isCloneMode, isIndividualVaultDisabled = isIndividualVaultDisabled, resourceManager = resourceManager, + clock = clock, ) ?: viewState) .appendFolderAndOwnerData( folderViewList = vaultData.folderViewList, @@ -1557,6 +1560,8 @@ data class VaultAddEditState( * @property totp The current TOTP (if applicable). * @property canViewPassword Indicates whether the current user can view and copy * passwords associated with the login item. + * @property fido2CredentialCreationDateTime Date and time the FIDO 2 credential was + * created. */ @Parcelize data class Login( @@ -1567,6 +1572,7 @@ data class VaultAddEditState( val uriList: List = listOf( UriItem(id = UUID.randomUUID().toString(), uri = "", match = null), ), + val fido2CredentialCreationDateTime: Text? = null, ) : ItemType() { override val itemTypeOption: ItemTypeOption get() = ItemTypeOption.LOGIN } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt index a2dd91b086..9ac729d28c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt @@ -6,6 +6,7 @@ import com.bitwarden.core.CipherRepromptType import com.bitwarden.core.CipherType import com.bitwarden.core.CipherView import com.bitwarden.core.CollectionView +import com.bitwarden.core.Fido2Credential import com.bitwarden.core.FieldType import com.bitwarden.core.FieldView import com.bitwarden.core.FolderView @@ -14,6 +15,7 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager +import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType @@ -23,8 +25,12 @@ import com.x8bit.bitwarden.ui.vault.model.VaultCollection import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType.Companion.fromId import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull +import java.time.Clock import java.util.UUID +private const val PASSKEY_CREATION_DATE_PATTERN: String = "M/d/yy" +private const val PASSKEY_CREATION_TIME_PATTERN: String = "hh:mm a" + /** * Transforms [CipherView] into [VaultAddEditState.ViewState]. */ @@ -32,6 +38,7 @@ fun CipherView.toViewState( isClone: Boolean, isIndividualVaultDisabled: Boolean, resourceManager: ResourceManager, + clock: Clock, ): VaultAddEditState.ViewState = VaultAddEditState.ViewState.Content( type = when (type) { @@ -39,9 +46,13 @@ fun CipherView.toViewState( VaultAddEditState.ViewState.Content.ItemType.Login( username = login?.username.orEmpty(), password = login?.password.orEmpty(), - uriList = login?.uris.toUriItems(), totp = login?.totp, canViewPassword = this.viewPassword, + uriList = login?.uris.toUriItems(), + fido2CredentialCreationDateTime = login + ?.fido2Credentials + .getPrimaryFido2CredentialOrNull(isClone) + ?.getCreationDateTime(clock), ) } @@ -272,3 +283,24 @@ private fun List?.toUriItems(): List = ) } } + +/** + * Retrieves the cipher's primary (first) FIDO2 credential, or null if there is no FIDO2 credential + * assigned. + */ +private fun List?.getPrimaryFido2CredentialOrNull( + isClone: Boolean, +): Fido2Credential? { + if (isNullOrEmpty() || isClone) return null + + return first() +} + +/** + * Return the creation date and time of the primary FIDO2 credential, formatted as + * "M/d/yy, hh:mm a". + */ +private fun Fido2Credential.getCreationDateTime(clock: Clock) = R.string.created_xy.asText( + creationDate.toFormattedPattern(pattern = PASSKEY_CREATION_DATE_PATTERN, clock = clock), + creationDate.toFormattedPattern(pattern = PASSKEY_CREATION_TIME_PATTERN, clock = clock), +) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt index 980e370e63..ae0f58373a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt @@ -100,6 +100,18 @@ fun VaultItemLoginContent( } } + loginItemState.fido2CredentialCreationDateText?.let { creationDate -> + item { + Spacer(modifier = Modifier.height(8.dp)) + Fido2CredentialField( + creationDate = creationDate.toString(), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + } + loginItemState.totpCodeItemData?.let { totpCodeItemData -> item { Spacer(modifier = Modifier.height(8.dp)) @@ -247,6 +259,21 @@ fun VaultItemLoginContent( } } +@Composable +private fun Fido2CredentialField( + creationDate: String, + modifier: Modifier = Modifier, +) { + BitwardenTextField( + label = stringResource(id = R.string.passkey), + value = creationDate, + onValueChange = { }, + readOnly = true, + singleLine = true, + modifier = modifier, + ) +} + @Composable private fun NotesField( notes: String, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt index 8611d1e7db..b3557e9d90 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt @@ -137,13 +137,20 @@ fun VaultItemScreen( ) } }, - onConfirmDeleteClick = remember { + onConfirmDeleteClick = remember(viewModel) { { viewModel.trySendAction( VaultItemAction.Common.ConfirmDeleteClick, ) } }, + onConfirmCloneWithoutFido2Credential = remember(viewModel) { + { + viewModel.trySendAction( + VaultItemAction.Common.ConfirmCloneWithoutFido2CredentialClick, + ) + } + }, ) if (pendingRestoreCipher) { @@ -291,6 +298,7 @@ private fun VaultItemDialogs( onDismissRequest: () -> Unit, onConfirmDeleteClick: () -> Unit, onSubmitMasterPassword: (masterPassword: String, action: PasswordRepromptAction) -> Unit, + onConfirmCloneWithoutFido2Credential: () -> Unit, ) { when (dialog) { is VaultItemState.DialogState.Generic -> BitwardenBasicDialog( @@ -324,6 +332,18 @@ private fun VaultItemDialogs( ) } + is VaultItemState.DialogState.Fido2CredentialCannotBeCopiedConfirmationPrompt -> { + BitwardenTwoButtonDialog( + title = stringResource(id = R.string.passkey_will_not_be_copied), + message = dialog.message.invoke(), + confirmButtonText = stringResource(id = R.string.yes), + dismissButtonText = stringResource(id = R.string.no), + onConfirmClick = onConfirmCloneWithoutFido2Credential, + onDismissClick = onDismissRequest, + onDismissRequest = onDismissRequest, + ) + } + null -> Unit } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index 6263b3936b..4936be5af9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt @@ -151,6 +151,9 @@ class VaultItemViewModel @Inject constructor( is VaultItemAction.Common.ConfirmDeleteClick -> handleConfirmDeleteClick() is VaultItemAction.Common.ConfirmRestoreClick -> handleConfirmRestoreClick() is VaultItemAction.Common.DeleteClick -> handleDeleteClick() + is VaultItemAction.Common.ConfirmCloneWithoutFido2CredentialClick -> { + handleConfirmCloneClick() + } } } @@ -373,9 +376,19 @@ class VaultItemViewModel @Inject constructor( } } + @Suppress("MaxLineLength") private fun handleCloneClick() { onContent { content -> - if (content.common.requiresReprompt) { + if (content.common.requiresCloneConfirmation) { + mutableStateFlow.update { + it.copy( + dialog = VaultItemState.DialogState.Fido2CredentialCannotBeCopiedConfirmationPrompt( + message = R.string.the_passkey_will_not_be_copied_to_the_cloned_item_do_you_want_to_continue_cloning_this_item.asText(), + ), + ) + } + return@onContent + } else if (content.common.requiresReprompt) { mutableStateFlow.update { it.copy( dialog = VaultItemState.DialogState.MasterPasswordDialog( @@ -389,6 +402,23 @@ class VaultItemViewModel @Inject constructor( } } + private fun handleConfirmCloneClick() { + onContent { content -> + mutableStateFlow.update { + it.copy( + dialog = null, + viewState = content.copy( + common = content.common.copy( + requiresCloneConfirmation = false, + ), + ), + ) + } + + trySendAction(VaultItemAction.Common.CloneClick) + } + } + private fun handleMoveToOrganizationClick() { onContent { content -> if (content.common.requiresReprompt) { @@ -1030,6 +1060,8 @@ data class VaultItemState( * @property customFields A list of custom fields that user has added. * @property requiresReprompt Indicates if a master password prompt is required to view * secure fields. + * @property requiresCloneConfirmation Indicates user confirmation is required when + * cloning a cipher. * @property currentCipher The cipher that is currently being viewed (nullable). */ @Parcelize @@ -1039,6 +1071,7 @@ data class VaultItemState( val notes: String?, val customFields: List, val requiresReprompt: Boolean, + val requiresCloneConfirmation: Boolean, @IgnoredOnParcel val currentCipher: CipherView? = null, val attachments: List?, @@ -1121,6 +1154,8 @@ data class VaultItemState( * @property totpCodeItemData The optional data related the TOTP code. * @property isPremiumUser Indicates if the user has subscribed to a premium * account. + * @property fido2CredentialCreationDateText Optional creation date and time of the + * FIDO2 credential associated with the login item. */ @Parcelize data class Login( @@ -1131,6 +1166,7 @@ data class VaultItemState( val passwordRevisionDate: String?, val totpCodeItemData: TotpCodeItemData?, val isPremiumUser: Boolean, + val fido2CredentialCreationDateText: Text?, ) : ItemType() { /** @@ -1246,6 +1282,14 @@ data class VaultItemState( data class DeleteConfirmationPrompt( val message: Text, ) : DialogState() + + /** + * Displays the dialog for cloning without copying FIDO2 credentials to the user. + */ + @Parcelize + data class Fido2CredentialCannotBeCopiedConfirmationPrompt( + val message: Text, + ) : DialogState() } } @@ -1430,6 +1474,11 @@ sealed class VaultItemAction { * The user skipped selecting a location for the attachment file. */ data object NoAttachmentFileLocationReceive : Common() + + /** + * The user confirmed cloning a cipher without its FIDO 2 credentials. + */ + data object ConfirmCloneWithoutFido2CredentialClick : Common() } /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt index 887913a8ae..85588af074 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/util/CipherViewExtensions.kt @@ -4,11 +4,15 @@ import com.bitwarden.core.CardView import com.bitwarden.core.CipherRepromptType import com.bitwarden.core.CipherType import com.bitwarden.core.CipherView +import com.bitwarden.core.Fido2Credential import com.bitwarden.core.FieldType import com.bitwarden.core.FieldView import com.bitwarden.core.IdentityView import com.bitwarden.core.LoginUriView +import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.vault.repository.model.VaultData +import com.x8bit.bitwarden.ui.platform.base.util.Text +import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.capitalize import com.x8bit.bitwarden.ui.platform.base.util.nullIfAllEqual import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank @@ -22,7 +26,9 @@ import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull import java.time.Clock -private const val DATE_TIME_PATTERN: String = "M/d/yy hh:mm a" +private const val LAST_UPDATED_DATE_TIME_PATTERN: String = "M/d/yy hh:mm a" +private const val FIDO2_CREDENTIAL_CREATION_DATE_PATTERN: String = "M/d/yy" +private const val FIDO2_CREDENTIAL_CREATION_TIME_PATTERN: String = "h:mm a" /** * Transforms [VaultData] into [VaultState.ViewState]. @@ -40,10 +46,11 @@ fun CipherView.toViewState( requiresReprompt = reprompt == CipherRepromptType.PASSWORD, customFields = fields.orEmpty().map { it.toCustomField() }, lastUpdated = revisionDate.toFormattedPattern( - pattern = DATE_TIME_PATTERN, + pattern = LAST_UPDATED_DATE_TIME_PATTERN, clock = clock, ), notes = notes, + requiresCloneConfirmation = login?.fido2Credentials?.any() ?: false, attachments = attachments ?.mapNotNull { @Suppress("ComplexCondition") @@ -87,12 +94,16 @@ fun CipherView.toViewState( passwordRevisionDate = loginValues .passwordRevisionDate ?.toFormattedPattern( - pattern = DATE_TIME_PATTERN, + pattern = LAST_UPDATED_DATE_TIME_PATTERN, clock = clock, ), passwordHistoryCount = passwordHistory?.count(), isPremiumUser = isPremiumUser, totpCodeItemData = totpCodeItemData, + fido2CredentialCreationDateText = loginValues + .fido2Credentials + ?.firstOrNull() + ?.getCreationDateText(clock), ) } @@ -159,6 +170,20 @@ private fun LoginUriView.toUriData() = isLaunchable = !uri.isNullOrBlank(), ) +private fun Fido2Credential?.getCreationDateText(clock: Clock): Text? = + this?.let { + R.string.created_xy.asText( + creationDate.toFormattedPattern( + pattern = FIDO2_CREDENTIAL_CREATION_DATE_PATTERN, + clock = clock, + ), + creationDate.toFormattedPattern( + pattern = FIDO2_CREDENTIAL_CREATION_TIME_PATTERN, + clock = clock, + ), + ) + } + private val IdentityView.identityAddress: String? get() = listOfNotNull( address1, diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt index 6b701877cc..349ece2fe9 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt @@ -352,7 +352,7 @@ private const val CIPHER_JSON = """ "userDisplayName": "mockUserDisplayName-1", "counter": "mockCounter-1", "discoverable": "mockDiscoverable-1", - "creationDate": "2024-03-12T20:20:16.456Z" + "creationDate": "2023-10-27T12:00:00.000Z" } ] }, diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseCipherUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseCipherUtil.kt index ccc11d5348..0813b78c1a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseCipherUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseCipherUtil.kt @@ -2,6 +2,11 @@ package com.x8bit.bitwarden.data.vault.datasource.network.model import java.time.ZonedDateTime +/** + * Constant date time used for [ZonedDateTime] properties of mock objects. + */ +private val MOCK_ZONED_DATE_TIME = ZonedDateTime.parse("2023-10-27T12:00:00Z") + /** * Create a mock [SyncResponseJson.Cipher] with a given [number]. */ @@ -15,9 +20,9 @@ fun createMockCipher(number: Int, hasNullUri: Boolean = false): SyncResponseJson notes = "mockNotes-$number", type = CipherTypeJson.LOGIN, login = createMockLogin(number = number, hasNullUri = hasNullUri), - creationDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"), - deletedDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"), - revisionDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"), + creationDate = MOCK_ZONED_DATE_TIME, + deletedDate = MOCK_ZONED_DATE_TIME, + revisionDate = MOCK_ZONED_DATE_TIME, attachments = listOf(createMockAttachment(number = number)), card = createMockCard(number = number), fields = listOf(createMockField(number = number)), @@ -89,7 +94,7 @@ fun createMockCard(number: Int): SyncResponseJson.Cipher.Card = fun createMockPasswordHistory(number: Int): SyncResponseJson.Cipher.PasswordHistory = SyncResponseJson.Cipher.PasswordHistory( password = "mockPassword-$number", - lastUsedDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"), + lastUsedDate = MOCK_ZONED_DATE_TIME, ) /** @@ -118,7 +123,7 @@ fun createMockLogin(number: Int, hasNullUri: Boolean = false): SyncResponseJson. SyncResponseJson.Cipher.Login( username = "mockUsername-$number", password = "mockPassword-$number", - passwordRevisionDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"), + passwordRevisionDate = MOCK_ZONED_DATE_TIME, shouldAutofillOnPageLoad = false, uri = if (hasNullUri) null else "mockUri-$number", uris = listOf(createMockUri(number = number)), @@ -139,7 +144,7 @@ fun createMockFido2Credential(number: Int) = SyncResponseJson.Cipher.Fido2Creden userDisplayName = "mockUserDisplayName-$number", counter = "mockCounter-$number", discoverable = "mockDiscoverable-$number", - creationDate = ZonedDateTime.parse("2024-03-12T20:20:16.456Z"), + creationDate = MOCK_ZONED_DATE_TIME, ) /** diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt index ecf0c7f0dd..5eeb511739 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt @@ -329,7 +329,7 @@ private const val CREATE_ATTACHMENT_SUCCESS_JSON = """ "userDisplayName": "mockUserDisplayName-1", "counter": "mockCounter-1", "discoverable": "mockDiscoverable-1", - "creationDate": "2024-03-12T20:20:16.456Z" + "creationDate": "2023-10-27T12:00:00.00Z" } ] }, @@ -439,7 +439,7 @@ private const val CREATE_UPDATE_CIPHER_SUCCESS_JSON = """ "userDisplayName": "mockUserDisplayName-1", "counter": "mockCounter-1", "discoverable": "mockDiscoverable-1", - "creationDate": "2024-03-12T20:20:16.456Z" + "creationDate": "2023-10-27T12:00:00.00Z" } ] }, diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceTest.kt index e58f134590..a7d00e2bb8 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceTest.kt @@ -245,7 +245,7 @@ private const val SYNC_SUCCESS_JSON = """ "userDisplayName": "mockUserDisplayName-1", "counter": "mockCounter-1", "discoverable": "mockDiscoverable-1", - "creationDate": "2024-03-12T20:20:16.456Z" + "creationDate": "2023-10-27T12:00:00.00Z" } ] }, diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt index dea9df65ec..213ed8cb59 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt @@ -15,8 +15,20 @@ import com.bitwarden.core.PasswordHistoryView import com.bitwarden.core.SecureNoteType import com.bitwarden.core.SecureNoteView import com.bitwarden.core.UriMatchType +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset import java.time.ZonedDateTime +/** + * Default date time used for [ZonedDateTime] properties of mock objects. + */ +private const val DEFAULT_TIMESTAMP = "2023-10-27T12:00:00Z" +private val FIXED_CLOCK: Clock = Clock.fixed( + Instant.parse(DEFAULT_TIMESTAMP), + ZoneOffset.UTC, +) + /** * Create a mock [CipherView]. * @@ -24,12 +36,14 @@ import java.time.ZonedDateTime * @param isDeleted whether or not the cipher has been deleted. * @param cipherType the type of cipher to create. */ +@Suppress("LongParameterList") fun createMockCipherView( number: Int, isDeleted: Boolean = false, cipherType: CipherType = CipherType.LOGIN, totp: String? = "mockTotp-$number", folderId: String? = "mockId-$number", + clock: Clock = FIXED_CLOCK, ): CipherView = CipherView( id = "mockId-$number", @@ -43,21 +57,16 @@ fun createMockCipherView( login = createMockLoginView( number = number, totp = totp, + clock = clock, ) .takeIf { cipherType == CipherType.LOGIN }, - creationDate = ZonedDateTime - .parse("2023-10-27T12:00:00Z") - .toInstant(), + creationDate = clock.instant(), deletedDate = if (isDeleted) { - ZonedDateTime - .parse("2023-10-27T12:00:00Z") - .toInstant() + clock.instant() } else { null }, - revisionDate = ZonedDateTime - .parse("2023-10-27T12:00:00Z") - .toInstant(), + revisionDate = clock.instant(), attachments = listOf(createMockAttachmentView(number = number)), card = createMockCardView(number = number).takeIf { cipherType == CipherType.CARD }, fields = listOf(createMockFieldView(number = number)), @@ -65,7 +74,7 @@ fun createMockCipherView( cipherType == CipherType.IDENTITY }, favorite = false, - passwordHistory = listOf(createMockPasswordHistoryView(number = number)), + passwordHistory = listOf(createMockPasswordHistoryView(number = number, clock)), reprompt = CipherRepromptType.NONE, secureNote = createMockSecureNoteView().takeIf { cipherType == CipherType.SECURE_NOTE }, edit = false, @@ -80,40 +89,39 @@ fun createMockCipherView( fun createMockLoginView( number: Int, totp: String? = "mockTotp-$number", + clock: Clock = FIXED_CLOCK, ): LoginView = LoginView( username = "mockUsername-$number", password = "mockPassword-$number", - passwordRevisionDate = ZonedDateTime - .parse("2023-10-27T12:00:00Z") - .toInstant(), + passwordRevisionDate = clock.instant(), autofillOnPageLoad = false, uris = listOf(createMockUriView(number = number)), totp = totp, - fido2Credentials = createMockSdkFido2CredentialList(number), + fido2Credentials = createMockSdkFido2CredentialList(number, clock), ) -fun createMockSdkFido2CredentialList(number: Int) = - listOf(createMockSdkFido2CredentialView(number)) +fun createMockSdkFido2CredentialList(number: Int, clock: Clock = FIXED_CLOCK) = + listOf(createMockSdkFido2CredentialView(number, clock)) -fun createMockSdkFido2CredentialView(number: Int) = - Fido2Credential( - credentialId = "mockCredentialId-$number", - keyType = "mockKeyType-$number", - keyAlgorithm = "mockKeyAlgorithm-$number", - keyCurve = "mockKeyCurve-$number", - keyValue = "mockKeyValue-$number", - rpId = "mockRpId-$number", - userHandle = "mockUserHandle-$number", - userName = "mockUserName-$number", - counter = "mockCounter-$number", - rpName = "mockRpName-$number", - userDisplayName = "mockUserDisplayName-$number", - discoverable = "mockDiscoverable-$number", - creationDate = ZonedDateTime - .parse("2024-03-12T20:20:16.456Z") - .toInstant(), - ) +fun createMockSdkFido2CredentialView( + number: Int, + clock: Clock = FIXED_CLOCK, +) = Fido2Credential( + credentialId = "mockCredentialId-$number", + keyType = "mockKeyType-$number", + keyAlgorithm = "mockKeyAlgorithm-$number", + keyCurve = "mockKeyCurve-$number", + keyValue = "mockKeyValue-$number", + rpId = "mockRpId-$number", + userHandle = "mockUserHandle-$number", + userName = "mockUserName-$number", + counter = "mockCounter-$number", + rpName = "mockRpName-$number", + userDisplayName = "mockUserDisplayName-$number", + discoverable = "mockDiscoverable-$number", + creationDate = clock.instant(), +) /** * Create a mock [LoginUriView] with a given [number]. @@ -189,12 +197,10 @@ fun createMockIdentityView(number: Int): IdentityView = /** * Create a mock [PasswordHistoryView] with a given [number]. */ -fun createMockPasswordHistoryView(number: Int): PasswordHistoryView = +fun createMockPasswordHistoryView(number: Int, clock: Clock = FIXED_CLOCK): PasswordHistoryView = PasswordHistoryView( password = "mockPassword-$number", - lastUsedDate = ZonedDateTime - .parse("2023-10-27T12:00:00Z") - .toInstant(), + lastUsedDate = clock.instant(), ) /** diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt index 9572ef26e9..8d657a816f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkCipherUtil.kt @@ -14,12 +14,24 @@ import com.bitwarden.core.PasswordHistory import com.bitwarden.core.SecureNote import com.bitwarden.core.SecureNoteType import com.bitwarden.core.UriMatchType +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset import java.time.ZonedDateTime +/** + * Default date time used for [ZonedDateTime] properties of mock objects. + */ +private const val DEFAULT_TIMESTAMP = "2023-10-27T12:00:00Z" +private val FIXED_CLOCK: Clock = Clock.fixed( + Instant.parse(DEFAULT_TIMESTAMP), + ZoneOffset.UTC, +) + /** * Create a mock [Cipher] with a given [number]. */ -fun createMockSdkCipher(number: Int): Cipher = +fun createMockSdkCipher(number: Int, clock: Clock = FIXED_CLOCK): Cipher = Cipher( id = "mockId-$number", organizationId = "mockOrganizationId-$number", @@ -29,22 +41,16 @@ fun createMockSdkCipher(number: Int): Cipher = name = "mockName-$number", notes = "mockNotes-$number", type = CipherType.LOGIN, - login = createMockSdkLogin(number = number), - creationDate = ZonedDateTime - .parse("2023-10-27T12:00:00Z") - .toInstant(), - deletedDate = ZonedDateTime - .parse("2023-10-27T12:00:00Z") - .toInstant(), - revisionDate = ZonedDateTime - .parse("2023-10-27T12:00:00Z") - .toInstant(), + login = createMockSdkLogin(number = number, clock = clock), + creationDate = clock.instant(), + deletedDate = clock.instant(), + revisionDate = clock.instant(), attachments = listOf(createMockSdkAttachment(number = number)), card = createMockSdkCard(number = number), fields = listOf(createMockSdkField(number = number)), identity = createMockSdkIdentity(number = number), favorite = false, - passwordHistory = listOf(createMockSdkPasswordHistory(number = number)), + passwordHistory = listOf(createMockSdkPasswordHistory(number = number, clock = clock)), reprompt = CipherRepromptType.NONE, secureNote = createMockSdkSecureNote(), edit = false, @@ -64,12 +70,10 @@ fun createMockSdkSecureNote(): SecureNote = /** * Create a mock [PasswordHistory] with a given [number]. */ -fun createMockSdkPasswordHistory(number: Int): PasswordHistory = +fun createMockSdkPasswordHistory(number: Int, clock: Clock): PasswordHistory = PasswordHistory( password = "mockPassword-$number", - lastUsedDate = ZonedDateTime - .parse("2023-10-27T12:00:00Z") - .toInstant(), + lastUsedDate = clock.instant(), ) /** @@ -137,17 +141,15 @@ fun createMockSdkAttachment(number: Int): Attachment = /** * Create a mock [Login] with a given [number]. */ -fun createMockSdkLogin(number: Int): Login = +fun createMockSdkLogin(number: Int, clock: Clock): Login = Login( username = "mockUsername-$number", password = "mockPassword-$number", - passwordRevisionDate = ZonedDateTime - .parse("2023-10-27T12:00:00Z") - .toInstant(), + passwordRevisionDate = clock.instant(), autofillOnPageLoad = false, uris = listOf(createMockSdkUri(number = number)), totp = "mockTotp-$number", - fido2Credentials = createMockSdkFido2CredentialList(number), + fido2Credentials = createMockSdkFido2CredentialList(number, clock), ) /** diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/TotpCodeManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/TotpCodeManagerTest.kt index db69e78e72..f7e8260472 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/TotpCodeManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/TotpCodeManagerTest.kt @@ -62,7 +62,7 @@ class TotpCodeManagerTest { ) val cipherView = createMockCipherView(1).copy( - login = createMockLoginView(1).copy( + login = createMockLoginView(number = 1, clock = clock).copy( totp = null, ), ) @@ -82,7 +82,7 @@ class TotpCodeManagerTest { ) val cipherView = createMockCipherView(1).copy( - login = createMockLoginView(1).copy( + login = createMockLoginView(number = 1, clock = clock).copy( totp = null, ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt index c58427d6de..847df88c16 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -280,7 +280,7 @@ class VaultRepositoryTest { coEvery { vaultSdkSource.decryptCipherList( userId = userId, - cipherList = listOf(createMockSdkCipher(1)), + cipherList = listOf(createMockSdkCipher(1, clock)), ) } returns listOf(createMockCipherView(number = 1)).asSuccess() coEvery { @@ -1834,7 +1834,7 @@ class VaultRepositoryTest { userId = userId, cipherView = mockCipherView, ) - } returns createMockSdkCipher(number = 1).asSuccess() + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() coEvery { ciphersService.createCipher( body = createMockCipherJsonRequest(number = 1, hasNullUri = true), @@ -1861,7 +1861,7 @@ class VaultRepositoryTest { userId = userId, cipherView = mockCipherView, ) - } returns createMockSdkCipher(number = 1).asSuccess() + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() val mockCipher = createMockCipher(number = 1) coEvery { ciphersService.createCipher( @@ -1931,7 +1931,7 @@ class VaultRepositoryTest { userId = userId, cipherView = mockCipherView, ) - } returns createMockSdkCipher(number = 1).asSuccess() + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() coEvery { ciphersService.createCipherInOrganization( body = CreateCipherInOrganizationJsonRequest( @@ -1964,7 +1964,7 @@ class VaultRepositoryTest { userId = userId, cipherView = mockCipherView, ) - } returns createMockSdkCipher(number = 1).asSuccess() + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() val mockCipher = createMockCipher(number = 1) coEvery { ciphersService.createCipherInOrganization( @@ -2043,7 +2043,7 @@ class VaultRepositoryTest { userId = userId, cipherView = mockCipherView, ) - } returns createMockSdkCipher(number = 1).asSuccess() + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() coEvery { ciphersService.updateCipher( cipherId = cipherId, @@ -2072,7 +2072,7 @@ class VaultRepositoryTest { userId = userId, cipherView = mockCipherView, ) - } returns createMockSdkCipher(number = 1).asSuccess() + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() coEvery { ciphersService.updateCipher( cipherId = cipherId, @@ -2111,7 +2111,7 @@ class VaultRepositoryTest { userId = userId, cipherView = mockCipherView, ) - } returns createMockSdkCipher(number = 1).asSuccess() + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() val mockCipher = createMockCipher(number = 1) coEvery { ciphersService.updateCipher( @@ -2221,7 +2221,7 @@ class VaultRepositoryTest { runTest { mockkStatic(Cipher::toEncryptedNetworkCipherResponse) every { - createMockSdkCipher(number = 1).toEncryptedNetworkCipherResponse() + createMockSdkCipher(number = 1, clock = clock).toEncryptedNetworkCipherResponse() } returns createMockCipher(number = 1) val fixedInstant = Instant.parse("2021-01-01T00:00:00Z") val userId = "mockId-1" @@ -2234,7 +2234,7 @@ class VaultRepositoryTest { deletedDate = fixedInstant, ), ) - } returns createMockSdkCipher(number = 1).asSuccess() + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() fakeAuthDiskSource.userState = MOCK_USER_STATE coEvery { ciphersService.softDeleteCipher(cipherId = cipherId) } returns Unit.asSuccess() coEvery { @@ -2303,7 +2303,7 @@ class VaultRepositoryTest { runTest { mockkStatic(Cipher::toEncryptedNetworkCipherResponse) every { - createMockSdkCipher(number = 1).toEncryptedNetworkCipherResponse() + createMockSdkCipher(number = 1, clock = clock).toEncryptedNetworkCipherResponse() } returns createMockCipher(number = 1) val fixedInstant = Instant.parse("2021-01-01T00:00:00Z") val userId = "mockId-1" @@ -2316,7 +2316,7 @@ class VaultRepositoryTest { attachments = emptyList(), ), ) - } returns createMockSdkCipher(number = 1).asSuccess() + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() fakeAuthDiskSource.userState = MOCK_USER_STATE coEvery { ciphersService.deleteCipherAttachment( @@ -2385,7 +2385,7 @@ class VaultRepositoryTest { runTest { mockkStatic(Cipher::toEncryptedNetworkCipherResponse) every { - createMockSdkCipher(number = 1).toEncryptedNetworkCipherResponse() + createMockSdkCipher(number = 1, clock = clock).toEncryptedNetworkCipherResponse() } returns createMockCipher(number = 1) val fixedInstant = Instant.parse("2021-01-01T00:00:00Z") val userId = "mockId-1" @@ -2398,7 +2398,7 @@ class VaultRepositoryTest { deletedDate = null, ), ) - } returns createMockSdkCipher(number = 1).asSuccess() + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() fakeAuthDiskSource.userState = MOCK_USER_STATE coEvery { ciphersService.restoreCipher(cipherId = cipherId) } returns Unit.asSuccess() coEvery { @@ -2957,7 +2957,7 @@ class VaultRepositoryTest { userId = userId, cipherView = createMockCipherView(number = 1), ) - } returns createMockSdkCipher(number = 1).asSuccess() + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() coEvery { ciphersService.shareCipher( cipherId = "mockId-1", @@ -2998,7 +2998,7 @@ class VaultRepositoryTest { userId = userId, cipherView = createMockCipherView(number = 1), ) - } returns createMockSdkCipher(number = 1).asSuccess() + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() coEvery { ciphersService.shareCipher( cipherId = "mockId-1", @@ -3085,7 +3085,7 @@ class VaultRepositoryTest { userId = userId, cipherView = createMockCipherView(number = 1), ) - } returns createMockSdkCipher(number = 1).asSuccess() + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() coEvery { ciphersService.updateCipherCollections( cipherId = "mockId-1", @@ -3120,7 +3120,7 @@ class VaultRepositoryTest { userId = userId, cipherView = createMockCipherView(number = 1), ) - } returns createMockSdkCipher(number = 1).asSuccess() + } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() coEvery { ciphersService.updateCipherCollections( cipherId = "mockId-1", @@ -3233,7 +3233,7 @@ class VaultRepositoryTest { val cipherId = "cipherId-1" val mockUri = setupMockUri(url = "www.test.com") val mockCipherView = createMockCipherView(number = 1) - val mockCipher = createMockSdkCipher(number = 1) + val mockCipher = createMockSdkCipher(number = 1, clock = clock) val mockFileName = "mockFileName-1" val mockFileSize = "1" val mockAttachmentView = createMockAttachmentView(number = 1).copy( @@ -3278,7 +3278,7 @@ class VaultRepositoryTest { val cipherId = "cipherId-1" val mockUri = setupMockUri(url = "www.test.com") val mockCipherView = createMockCipherView(number = 1) - val mockCipher = createMockSdkCipher(number = 1) + val mockCipher = createMockSdkCipher(number = 1, clock = clock) val mockFileName = "mockFileName-1" val mockFileSize = "1" coEvery { @@ -3308,7 +3308,7 @@ class VaultRepositoryTest { val cipherId = "cipherId-1" val mockUri = setupMockUri(url = "www.test.com") val mockCipherView = createMockCipherView(number = 1) - val mockCipher = createMockSdkCipher(number = 1) + val mockCipher = createMockSdkCipher(number = 1, clock = clock) val mockFileName = "mockFileName-1" val mockFileSize = "1" val mockAttachmentView = createMockAttachmentView(number = 1).copy( @@ -3364,7 +3364,7 @@ class VaultRepositoryTest { val cipherId = "cipherId-1" val mockUri = setupMockUri(url = "www.test.com") val mockCipherView = createMockCipherView(number = 1) - val mockCipher = createMockSdkCipher(number = 1) + val mockCipher = createMockSdkCipher(number = 1, clock = clock) val mockFileName = "mockFileName-1" val mockFileSize = "1" val mockAttachmentView = createMockAttachmentView(number = 1).copy( @@ -3427,7 +3427,7 @@ class VaultRepositoryTest { val cipherId = "cipherId-1" val mockUri = setupMockUri(url = "www.test.com") val mockCipherView = createMockCipherView(number = 1) - val mockCipher = createMockSdkCipher(number = 1) + val mockCipher = createMockSdkCipher(number = 1, clock = clock) val mockFileName = "mockFileName-1" val mockFileSize = "1" val mockAttachmentView = createMockAttachmentView(number = 1).copy( @@ -3503,7 +3503,7 @@ class VaultRepositoryTest { val cipherId = "cipherId-1" val mockUri = setupMockUri(url = "www.test.com") val mockCipherView = createMockCipherView(number = 1) - val mockCipher = createMockSdkCipher(number = 1) + val mockCipher = createMockSdkCipher(number = 1, clock = clock) val mockFileName = "mockFileName-1" val mockFileSize = "1" val mockAttachmentView = createMockAttachmentView(number = 1).copy( @@ -4456,7 +4456,7 @@ class VaultRepositoryTest { coEvery { vaultSdkSource.decryptCipherList( userId = MOCK_USER_STATE.activeUserId, - cipherList = listOf(createMockSdkCipher(number = number)), + cipherList = listOf(createMockSdkCipher(number = number, clock = clock)), ) } returns listOf(cipherView).asSuccess() @@ -4499,7 +4499,7 @@ class VaultRepositoryTest { coEvery { vaultSdkSource.decryptCipherList( userId = MOCK_USER_STATE.activeUserId, - cipherList = listOf(createMockSdkCipher(number = number)), + cipherList = listOf(createMockSdkCipher(number = number, clock = clock)), ) } returns listOf(cipherView).asSuccess() val collectionView = createMockCollectionView(number = number) @@ -4678,7 +4678,7 @@ class VaultRepositoryTest { coEvery { vaultSdkSource.decryptCipherList( userId = MOCK_USER_STATE.activeUserId, - cipherList = listOf(createMockSdkCipher(number = number)), + cipherList = listOf(createMockSdkCipher(number = number, clock = clock)), ) } returns listOf(cipherView).asSuccess() @@ -4734,7 +4734,7 @@ class VaultRepositoryTest { coEvery { vaultSdkSource.decryptCipherList( userId = MOCK_USER_STATE.activeUserId, - cipherList = listOf(createMockSdkCipher(number = number)), + cipherList = listOf(createMockSdkCipher(number = number, clock = clock)), ) } returns listOf(cipherView).asSuccess() @@ -4888,7 +4888,7 @@ class VaultRepositoryTest { coEvery { vaultSdkSource.decryptCipherList( userId = MOCK_USER_STATE.activeUserId, - cipherList = listOf(createMockSdkCipher(number = number)), + cipherList = listOf(createMockSdkCipher(number = number, clock = clock)), ) } returns listOf(cipherView).asSuccess() @@ -5734,7 +5734,7 @@ class VaultRepositoryTest { coEvery { vaultSdkSource.decryptCipherList( userId = userId, - cipherList = listOf(createMockSdkCipher(1)), + cipherList = listOf(createMockSdkCipher(1, clock)), ) } returns listOf(createMockCipherView(1)).asSuccess() coEvery { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt index 2f30b0d472..2abe2ce0dd 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt @@ -30,12 +30,25 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkSecureNo import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkUri import org.junit.Assert.assertEquals import org.junit.Test +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset +import java.time.ZonedDateTime + +/** + * Default date time used for [ZonedDateTime] properties of mock objects. + */ +private const val DEFAULT_TIMESTAMP = "2023-10-27T12:00:00Z" +private val FIXED_CLOCK: Clock = Clock.fixed( + Instant.parse(DEFAULT_TIMESTAMP), + ZoneOffset.UTC, +) class VaultSdkCipherExtensionsTest { @Test fun `toEncryptedNetworkCipherResponse should convert an Sdk Cipher to a cipher`() { - val sdkCipher = createMockSdkCipher(number = 1) + val sdkCipher = createMockSdkCipher(number = 1, clock = FIXED_CLOCK) val result = sdkCipher.toEncryptedNetworkCipherResponse() @@ -50,7 +63,7 @@ class VaultSdkCipherExtensionsTest { @Test fun `toEncryptedNetworkCipher should convert an Sdk Cipher to a Network Cipher`() { - val sdkCipher = createMockSdkCipher(number = 1) + val sdkCipher = createMockSdkCipher(number = 1, clock = FIXED_CLOCK) val syncCipher = sdkCipher.toEncryptedNetworkCipher() assertEquals( createMockCipherJsonRequest( @@ -70,8 +83,8 @@ class VaultSdkCipherExtensionsTest { val sdkCiphers = syncCiphers.toEncryptedSdkCipherList() assertEquals( listOf( - createMockSdkCipher(number = 1), - createMockSdkCipher(number = 2), + createMockSdkCipher(number = 1, clock = FIXED_CLOCK), + createMockSdkCipher(number = 2, clock = FIXED_CLOCK), ), sdkCiphers, ) @@ -82,7 +95,7 @@ class VaultSdkCipherExtensionsTest { val syncCipher = createMockCipher(number = 1) val sdkCipher = syncCipher.toEncryptedSdkCipher() assertEquals( - createMockSdkCipher(number = 1), + createMockSdkCipher(number = 1, clock = FIXED_CLOCK), sdkCipher, ) } @@ -92,7 +105,7 @@ class VaultSdkCipherExtensionsTest { val syncLogin = createMockLogin(number = 1) val sdkLogin = syncLogin.toSdkLogin() assertEquals( - createMockSdkLogin(number = 1), + createMockSdkLogin(number = 1, clock = FIXED_CLOCK), sdkLogin, ) } @@ -215,8 +228,8 @@ class VaultSdkCipherExtensionsTest { val sdkPasswordHistories = syncPasswordHistories.toSdkPasswordHistoryList() assertEquals( listOf( - createMockSdkPasswordHistory(number = 1), - createMockSdkPasswordHistory(number = 2), + createMockSdkPasswordHistory(number = 1, FIXED_CLOCK), + createMockSdkPasswordHistory(number = 2, FIXED_CLOCK), ), sdkPasswordHistories, ) @@ -227,7 +240,7 @@ class VaultSdkCipherExtensionsTest { val syncPasswordHistory = createMockPasswordHistory(number = 1) val sdkPasswordHistory = syncPasswordHistory.toSdkPasswordHistory() assertEquals( - createMockSdkPasswordHistory(number = 1), + createMockSdkPasswordHistory(number = 1, clock = FIXED_CLOCK), sdkPasswordHistory, ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt index 1b9b43bf7b..f9e597886f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt @@ -210,7 +210,7 @@ class SearchViewModelTest : BaseViewModelTest() { val cipherId = CIPHER_ID val errorMessage = "Server error" val updatedCipherView = cipherView.copy( - login = createMockLoginView(1).copy( + login = createMockLoginView(number = 1, clock = clock).copy( uris = listOf(createMockUriView(number = 1)) + LoginUriView( uri = AUTOFILL_URI, @@ -261,7 +261,7 @@ class SearchViewModelTest : BaseViewModelTest() { val cipherView = setupForAutofill() val cipherId = CIPHER_ID val updatedCipherView = cipherView.copy( - login = createMockLoginView(1).copy( + login = createMockLoginView(number = 1, clock = clock).copy( uris = listOf(createMockUriView(number = 1)) + LoginUriView( uri = AUTOFILL_URI, @@ -420,7 +420,7 @@ class SearchViewModelTest : BaseViewModelTest() { val cipherId = CIPHER_ID val password = "password" val updatedCipherView = cipherView.copy( - login = createMockLoginView(1).copy( + login = createMockLoginView(number = 1, clock = clock).copy( uris = listOf(createMockUriView(number = 1)) + LoginUriView( uri = AUTOFILL_URI, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index ae9867a35b..77930beb4e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -37,6 +37,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest +import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode @@ -70,11 +71,18 @@ import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset import java.util.UUID @Suppress("LargeClass") class VaultAddEditViewModelTest : BaseViewModelTest() { + private val fixedClock: Clock = Clock.fixed( + Instant.parse("2023-10-27T12:00:00Z"), + ZoneOffset.UTC, + ) private val settingsRepository: SettingsRepository = mockk { every { initialAutofillDialogShown = any() } just runs every { initialAutofillDialogShown } returns true @@ -476,6 +484,10 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { uri = listOf(UriItem("testId", "www.mockuri1.com", UriMatchType.HOST)), totpCode = "mockTotp-1", canViewPassword = true, + fido2CredentialCreationDateTime = R.string.created_xy.asText( + "10/27/23", + "12:00 PM", + ), ) .copy(totp = "mockTotp-1"), ), @@ -752,6 +764,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { isClone = false, isIndividualVaultDisabled = false, resourceManager = resourceManager, + clock = fixedClock, ) } returns stateWithName.viewState mutableVaultDataFlow.value = DataState.Loaded( @@ -781,6 +794,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { isClone = false, isIndividualVaultDisabled = false, resourceManager = resourceManager, + clock = fixedClock, ) vaultRepository.updateCipher(DEFAULT_EDIT_ITEM_ID, any()) } @@ -813,6 +827,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { isClone = false, isIndividualVaultDisabled = false, resourceManager = resourceManager, + clock = fixedClock, ) } returns stateWithName.viewState coEvery { @@ -873,6 +888,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { isClone = false, isIndividualVaultDisabled = false, resourceManager = resourceManager, + clock = fixedClock, ) } returns stateWithName.viewState coEvery { @@ -1857,6 +1873,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { resourceManager = resourceManager, authRepository = authRepository, settingsRepository = settingsRepository, + clock = fixedClock, ) } @@ -2369,12 +2386,14 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { availableOwners = availableOwners, ) + @Suppress("LongParameterList") private fun createLoginTypeContentViewState( username: String = "", password: String = "", uri: List = listOf(UriItem("testId", "", null)), totpCode: String? = null, canViewPassword: Boolean = true, + fido2CredentialCreationDateTime: Text? = null, ): VaultAddEditState.ViewState.Content.ItemType.Login = VaultAddEditState.ViewState.Content.ItemType.Login( username = username, @@ -2382,6 +2401,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { uriList = uri, totp = totpCode, canViewPassword = canViewPassword, + fido2CredentialCreationDateTime = fido2CredentialCreationDateTime, ) private fun createSavedStateHandleWithState( @@ -2400,12 +2420,14 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { set("vault_edit_id", (vaultAddEditType as? VaultAddEditType.EditItem)?.vaultItemId) } + @Suppress("LongParameterList") private fun createAddVaultItemViewModel( savedStateHandle: SavedStateHandle = loginInitialSavedStateHandle, bitwardenClipboardManager: BitwardenClipboardManager = clipboardManager, vaultRepo: VaultRepository = vaultRepository, generatorRepo: GeneratorRepository = generatorRepository, bitwardenResourceManager: ResourceManager = resourceManager, + clock: Clock = fixedClock, ): VaultAddEditViewModel = VaultAddEditViewModel( savedStateHandle = savedStateHandle, @@ -2417,6 +2439,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { resourceManager = bitwardenResourceManager, authRepository = authRepository, settingsRepository = settingsRepository, + clock = clock, ) private fun createVaultData( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt index 0eb17e625d..60738429bd 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt @@ -4,6 +4,7 @@ import com.bitwarden.core.CardView import com.bitwarden.core.CipherRepromptType import com.bitwarden.core.CipherType import com.bitwarden.core.CipherView +import com.bitwarden.core.Fido2Credential import com.bitwarden.core.FieldType import com.bitwarden.core.FieldView import com.bitwarden.core.IdentityView @@ -20,7 +21,6 @@ import com.x8bit.bitwarden.data.platform.repository.model.Environment 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.createMockFolderView -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.manager.resource.ResourceManager import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState @@ -37,7 +37,9 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import java.time.Clock import java.time.Instant +import java.time.ZoneOffset import java.util.UUID class CipherViewExtensionsTest { @@ -66,6 +68,7 @@ class CipherViewExtensionsTest { isClone = false, isIndividualVaultDisabled = false, resourceManager = resourceManager, + clock = FIXED_CLOCK, ) assertEquals( @@ -110,6 +113,7 @@ class CipherViewExtensionsTest { isClone = false, isIndividualVaultDisabled = true, resourceManager = resourceManager, + clock = FIXED_CLOCK, ) assertEquals( @@ -159,6 +163,7 @@ class CipherViewExtensionsTest { isClone = false, isIndividualVaultDisabled = false, resourceManager = resourceManager, + clock = FIXED_CLOCK, ) assertEquals( @@ -189,6 +194,10 @@ class CipherViewExtensionsTest { uriList = listOf(UriItem(TEST_ID, "www.example.com", null)), totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example", canViewPassword = false, + fido2CredentialCreationDateTime = R.string.created_xy.asText( + "10/27/23", + "12:00 PM", + ), ), ), result, @@ -203,6 +212,7 @@ class CipherViewExtensionsTest { isClone = false, isIndividualVaultDisabled = true, resourceManager = resourceManager, + clock = FIXED_CLOCK, ) assertEquals( @@ -236,6 +246,7 @@ class CipherViewExtensionsTest { isClone = true, isIndividualVaultDisabled = false, resourceManager = resourceManager, + clock = FIXED_CLOCK, ) assertEquals( @@ -423,6 +434,11 @@ class CipherViewExtensionsTest { ) } +private val FIXED_CLOCK: Clock = Clock.fixed( + Instant.parse("2023-10-27T12:00:00Z"), + ZoneOffset.UTC, +) + private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView( id = "id1234", organizationId = null, @@ -472,12 +488,12 @@ private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView( passwordHistory = listOf( PasswordHistoryView( password = "old_password", - lastUsedDate = Instant.ofEpochSecond(1_000L), + lastUsedDate = FIXED_CLOCK.instant(), ), ), - creationDate = Instant.ofEpochSecond(1_000L), + creationDate = FIXED_CLOCK.instant(), deletedDate = null, - revisionDate = Instant.ofEpochSecond(1_000L), + revisionDate = FIXED_CLOCK.instant(), ) private val DEFAULT_CARD_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy( @@ -521,7 +537,7 @@ private val DEFAULT_LOGIN_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.cop login = LoginView( username = "username", password = "password", - passwordRevisionDate = Instant.ofEpochSecond(1_000L), + passwordRevisionDate = FIXED_CLOCK.instant(), uris = listOf( LoginUriView( uri = "www.example.com", @@ -530,7 +546,23 @@ private val DEFAULT_LOGIN_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.cop ), totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example", autofillOnPageLoad = false, - fido2Credentials = createMockSdkFido2CredentialList(number = 1), + fido2Credentials = listOf( + Fido2Credential( + credentialId = "mockCredentialId", + keyType = "mockKeyType", + keyAlgorithm = "mockKeyAlgorithm", + keyCurve = "mockKeyCurve", + keyValue = "mockKeyValue", + rpId = "mockRpId", + userHandle = "mockUserHandle", + userName = "mockUserName", + counter = "mockCounter", + rpName = "mockRpName", + userDisplayName = "mockUserDisplayName", + discoverable = "mockDiscoverable", + creationDate = FIXED_CLOCK.instant(), + ), + ), ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt index 1d0ba3805d..80e5be977b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onSiblings import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextInput import androidx.core.net.toUri import com.x8bit.bitwarden.R @@ -1214,6 +1215,7 @@ class VaultItemScreenTest : BaseComposeTest() { passwordRevisionDate = null, isPremiumUser = true, totpCodeItemData = null, + fido2CredentialCreationDateText = null, ), ), ) @@ -1301,6 +1303,53 @@ class VaultItemScreenTest : BaseComposeTest() { } } + @Test + fun `in login state, the Passkey field should exist based on the state`() { + mutableStateFlow.update { currentState -> + currentState.copy( + viewState = EMPTY_LOGIN_VIEW_STATE.copy( + type = EMPTY_LOGIN_TYPE.copy( + fido2CredentialCreationDateText = DEFAULT_PASSKEY, + ), + ), + ) + } + + composeTestRule + .onNode(isProgressBar) + .assertDoesNotExist() + + composeTestRule + .onNodeWithText("Passkey") + .assertIsDisplayed() + } + + @Test + fun `in login state, the Passkey field should not exist based on state`() { + mutableStateFlow.update { it } + + composeTestRule + .onNodeWithText("Passkey") + .assertDoesNotExist() + } + + @Test + fun `in login state, the Passkey field text should display creation date`() { + mutableStateFlow.update { currentState -> + currentState.copy( + viewState = EMPTY_LOGIN_VIEW_STATE.copy( + type = EMPTY_LOGIN_TYPE.copy( + fido2CredentialCreationDateText = DEFAULT_PASSKEY, + ), + ), + ) + } + + composeTestRule + .onNodeWithText(text = DEFAULT_PASSKEY.toString(), substring = true) + .assertIsDisplayed() + } + @Test fun `in login state, the TOTP field should exist based on the state`() { mutableStateFlow.update { currentState -> @@ -1338,6 +1387,7 @@ class VaultItemScreenTest : BaseComposeTest() { composeTestRule .onNode(isProgressBar) + .performScrollTo() .assertIsDisplayed() composeTestRule @@ -1355,6 +1405,7 @@ class VaultItemScreenTest : BaseComposeTest() { composeTestRule .onNode(isProgressBar) + .performScrollTo() .assertIsDisplayed() composeTestRule @@ -2034,8 +2085,8 @@ private val DEFAULT_STATE: VaultItemState = VaultItemState( private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common = VaultItemState.ViewState.Content.Common( - lastUpdated = "12/31/69 06:16 PM", name = "cipher", + lastUpdated = "12/31/69 06:16 PM", notes = "Lots of notes", customFields = listOf( VaultItemState.ViewState.Content.Common.Custom.TextField( @@ -2055,6 +2106,7 @@ private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common = ), ), requiresReprompt = true, + requiresCloneConfirmation = false, attachments = listOf( VaultItemState.ViewState.Content.Common.AttachmentItem( id = "attachment-id", @@ -2067,6 +2119,11 @@ private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common = ), ) +private val DEFAULT_PASSKEY = R.string.created_xy.asText( + "3/13/24", + "3:56 PM", +) + private val DEFAULT_LOGIN: VaultItemState.ViewState.Content.ItemType.Login = VaultItemState.ViewState.Content.ItemType.Login( passwordHistoryCount = 1, @@ -2091,6 +2148,7 @@ private val DEFAULT_LOGIN: VaultItemState.ViewState.Content.ItemType.Login = verificationCode = "123456", totpCode = "testCode", ), + fido2CredentialCreationDateText = null, ) private val DEFAULT_IDENTITY: VaultItemState.ViewState.Content.ItemType.Identity = @@ -2122,6 +2180,7 @@ private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common = notes = null, customFields = emptyList(), requiresReprompt = true, + requiresCloneConfirmation = false, attachments = emptyList(), ) @@ -2134,6 +2193,7 @@ private val EMPTY_LOGIN_TYPE: VaultItemState.ViewState.Content.ItemType.Login = passwordRevisionDate = null, totpCodeItemData = null, isPremiumUser = true, + fido2CredentialCreationDateText = null, ) private val EMPTY_IDENTITY_TYPE: VaultItemState.ViewState.Content.ItemType.Identity = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index 1e4b093650..d438ad6bbd 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -932,6 +932,103 @@ class VaultItemViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `on CloneClick should show confirmation when cipher contains a passkey`() = runTest { + val loginViewState = DEFAULT_VIEW_STATE.copy( + common = DEFAULT_COMMON.copy( + requiresReprompt = false, + requiresCloneConfirmation = true, + ), + ) + val loginState = DEFAULT_STATE.copy(viewState = loginViewState) + val mockCipherView = mockk { + every { + toViewState( + isPremiumUser = true, + totpCodeItemData = null, + ) + } returns loginViewState + } + mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) + mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + + assertEquals(loginState, viewModel.stateFlow.value) + + viewModel.trySendAction(VaultItemAction.Common.CloneClick) + + assertEquals( + loginState.copy( + dialog = VaultItemState.DialogState.Fido2CredentialCannotBeCopiedConfirmationPrompt( + R.string.the_passkey_will_not_be_copied_to_the_cloned_item_do_you_want_to_continue_cloning_this_item.asText(), + ), + ), + viewModel.stateFlow.value, + ) + + verify(exactly = 1) { + mockCipherView.toViewState( + isPremiumUser = true, + totpCodeItemData = null, + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `on CloneClick should show confirmation before re-prompt when both are required`() { + val loginViewState = DEFAULT_VIEW_STATE.copy( + common = DEFAULT_COMMON.copy( + requiresReprompt = true, + requiresCloneConfirmation = true, + ), + ) + val loginState = DEFAULT_STATE.copy( + viewState = loginViewState, + ) + val mockCipherView = mockk { + every { + toViewState( + isPremiumUser = true, + totpCodeItemData = null, + ) + } returns loginViewState + } + mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) + mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + + assertEquals(loginState, viewModel.stateFlow.value) + viewModel.trySendAction(VaultItemAction.Common.CloneClick) + + // Assert clone confirmation dialog is triggered + assertEquals( + loginState.copy( + dialog = VaultItemState.DialogState.Fido2CredentialCannotBeCopiedConfirmationPrompt( + R.string.the_passkey_will_not_be_copied_to_the_cloned_item_do_you_want_to_continue_cloning_this_item.asText(), + ), + ), + viewModel.stateFlow.value, + ) + + // Simulate confirmation click. + viewModel.trySendAction(VaultItemAction.Common.ConfirmCloneWithoutFido2CredentialClick) + + // Assert MP dialog is triggered. + assertEquals( + loginState.copy( + dialog = VaultItemState.DialogState.MasterPasswordDialog( + action = PasswordRepromptAction.CloneClick, + ), + viewState = loginViewState.copy( + common = loginViewState.common.copy( + requiresCloneConfirmation = false, + ), + ), + ), + viewModel.stateFlow.value, + ) + } + @Test fun `on CloneClick should show password dialog when re-prompt is required`() = runTest { val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) @@ -1946,6 +2043,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { timeLeftSeconds = 15, periodSeconds = 30, ), + fido2CredentialCreationDateText = null, ) private val DEFAULT_CARD_TYPE: VaultItemState.ViewState.Content.ItemType.Card = @@ -1988,6 +2086,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { ), ), requiresReprompt = true, + requiresCloneConfirmation = false, currentCipher = createMockCipherView(number = 1), attachments = listOf( VaultItemState.ViewState.Content.Common.AttachmentItem( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt index 14a4635eac..ab3a5cd9d4 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/util/VaultItemTestUtil.kt @@ -4,13 +4,15 @@ import com.bitwarden.core.AttachmentView import com.bitwarden.core.CipherRepromptType import com.bitwarden.core.CipherType import com.bitwarden.core.CipherView +import com.bitwarden.core.Fido2Credential import com.bitwarden.core.FieldType import com.bitwarden.core.FieldView import com.bitwarden.core.IdentityView import com.bitwarden.core.LoginUriView import com.bitwarden.core.LoginView import com.bitwarden.core.PasswordHistoryView -import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType @@ -43,7 +45,23 @@ fun createLoginView(isEmpty: Boolean): LoginView = totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" .takeUnless { isEmpty }, autofillOnPageLoad = false, - fido2Credentials = createMockSdkFido2CredentialList(number = 1), + fido2Credentials = listOf( + Fido2Credential( + credentialId = "mockCredentialId", + keyType = "mockKeyType", + keyAlgorithm = "mockKeyAlgorithm", + keyCurve = "mockKeyCurve", + keyValue = "mockKeyValue", + rpId = "mockRpId", + userHandle = "mockUserHandle", + userName = "mockUserName", + counter = "mockCounter", + rpName = "mockRpName", + userDisplayName = "mockUserDisplayName", + discoverable = "mockDiscoverable", + creationDate = Instant.ofEpochSecond(1_000L), + ), + ).takeUnless { isEmpty }, ) @Suppress("CyclomaticComplexMethod") @@ -156,6 +174,7 @@ fun createCommonContent( notes = null, customFields = emptyList(), requiresReprompt = true, + requiresCloneConfirmation = false, attachments = emptyList(), ) } else { @@ -189,6 +208,7 @@ fun createCommonContent( ), ), requiresReprompt = true, + requiresCloneConfirmation = true, attachments = listOf( VaultItemState.ViewState.Content.Common.AttachmentItem( id = "attachment-id", @@ -232,6 +252,11 @@ fun createLoginContent(isEmpty: Boolean): VaultItemState.ViewState.Content.ItemT totpCode = "testCode", ) .takeUnless { isEmpty }, + fido2CredentialCreationDateText = R.string.created_xy.asText( + "1/1/70", + "12:16 AM", + ) + .takeUnless { isEmpty }, ) fun createIdentityContent( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt index 5f4001e86e..3c1fbccb62 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultAddItemStateExtensionsTest.kt @@ -57,6 +57,7 @@ class VaultAddItemStateExtensionsTest { password = "mockPassword-1", uriList = listOf(UriItem("testId", "mockUri-1", UriMatchType.DOMAIN)), totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example", + fido2CredentialCreationDateTime = null, ), )