diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerImpl.kt index b022680dae..c12efd6955 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerImpl.kt @@ -28,7 +28,6 @@ import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.MigratePersonalVaultResult import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher -import com.x8bit.bitwarden.data.vault.repository.util.updateFromMiniResponse import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -298,8 +297,8 @@ class VaultMigrationManagerImpl( encryptedCiphersMap[miniResponse.id]?.let { vaultDiskSource.saveCipher( userId = userId, - cipher = it.updateFromMiniResponse( - miniResponse = miniResponse, + cipher = miniResponse.toEncryptedNetworkCipher( + originalCipher = it, collectionIds = collectionIds, ), ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt index d6096fbb05..a36431efe6 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt @@ -125,29 +125,51 @@ fun Cipher.toEncryptedNetworkCipherResponse( ) /** - * Updates a [SyncResponseJson.Cipher] with metadata from a - * [CipherMiniResponseJson.CipherMiniResponse]. + * Converts a [CipherMiniResponseJson.CipherMiniResponse] into a [SyncResponseJson.Cipher], + * taking fields the mini response lacks (folder, favorite, edit, permissions, view password, + * encrypted for) from [originalCipher]. * This is useful for updating local cipher data after bulk operations that return mini responses. * - * @param miniResponse The mini response containing updated cipher metadata. - * @param collectionIds Optional list of collection IDs to update. - * If null, keeps existing collection IDs. - * @return A new [SyncResponseJson.Cipher] with updated fields from the mini response. + * @param originalCipher The local cipher to take fields missing from the mini response from. + * @param collectionIds Optional list of collection IDs to set. + * If null, keeps the collection IDs of [originalCipher]. + * @return A new [SyncResponseJson.Cipher] built from the mini response. */ -fun SyncResponseJson.Cipher.updateFromMiniResponse( - miniResponse: CipherMiniResponseJson.CipherMiniResponse, +fun CipherMiniResponseJson.CipherMiniResponse.toEncryptedNetworkCipher( + originalCipher: SyncResponseJson.Cipher, collectionIds: List? = null, -): SyncResponseJson.Cipher = copy( - organizationId = miniResponse.organizationId, - collectionIds = collectionIds ?: this.collectionIds, - revisionDate = miniResponse.revisionDate, - key = miniResponse.key, - attachments = miniResponse.attachments, - archivedDate = miniResponse.archivedDate, - deletedDate = miniResponse.deletedDate, - reprompt = miniResponse.reprompt, - shouldOrganizationUseTotp = miniResponse.shouldOrganizationUseTotp, - type = miniResponse.type, +): SyncResponseJson.Cipher = SyncResponseJson.Cipher( + id = id, + organizationId = organizationId, + folderId = originalCipher.folderId, + collectionIds = collectionIds ?: originalCipher.collectionIds, + key = key, + name = name, + notes = notes, + type = type, + login = login, + identity = identity, + card = card, + secureNote = secureNote, + sshKey = sshKey, + bankAccount = bankAccount, + driversLicense = driversLicense, + passport = passport, + isFavorite = originalCipher.isFavorite, + reprompt = reprompt, + shouldOrganizationUseTotp = shouldOrganizationUseTotp, + shouldEdit = originalCipher.shouldEdit, + permissions = originalCipher.permissions, + shouldViewPassword = originalCipher.shouldViewPassword, + encryptedFor = originalCipher.encryptedFor, + attachments = attachments, + fields = fields, + passwordHistory = passwordHistory, + creationDate = creationDate, + deletedDate = deletedDate, + revisionDate = revisionDate, + archivedDate = archivedDate, + data = data, ) /** diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt index c641affb6c..cd06a3c3b7 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt @@ -554,7 +554,7 @@ class VaultSdkCipherExtensionsTest { } @Test - fun `updateFromMiniResponse should update cipher with mini response data`() { + fun `toEncryptedNetworkCipher from mini response should use mini response data`() { val originalCipher = createMockCipher( number = 1, organizationId = null, @@ -563,8 +563,8 @@ class VaultSdkCipherExtensionsTest { val miniResponse = createMockCipherMiniResponse(number = 2) - val result = originalCipher.updateFromMiniResponse( - miniResponse = miniResponse, + val result = miniResponse.toEncryptedNetworkCipher( + originalCipher = originalCipher, collectionIds = listOf("collection-1"), ) @@ -577,14 +577,32 @@ class VaultSdkCipherExtensionsTest { assertEquals(miniResponse.deletedDate, result.deletedDate) assertEquals(miniResponse.reprompt, result.reprompt) assertEquals(miniResponse.shouldOrganizationUseTotp, result.shouldOrganizationUseTotp) - // Verify unchanged fields remain the same - assertEquals(originalCipher.name, result.name) - assertEquals(originalCipher.notes, result.notes) - assertEquals(originalCipher.id, result.id) + assertEquals(miniResponse.data, result.data) + assertEquals(miniResponse.creationDate, result.creationDate) + assertEquals(miniResponse.name, result.name) + assertEquals(miniResponse.notes, result.notes) + assertEquals(miniResponse.login, result.login) + assertEquals(miniResponse.card, result.card) + assertEquals(miniResponse.identity, result.identity) + assertEquals(miniResponse.secureNote, result.secureNote) + assertEquals(miniResponse.sshKey, result.sshKey) + assertEquals(miniResponse.bankAccount, result.bankAccount) + assertEquals(miniResponse.driversLicense, result.driversLicense) + assertEquals(miniResponse.passport, result.passport) + assertEquals(miniResponse.fields, result.fields) + assertEquals(miniResponse.passwordHistory, result.passwordHistory) + assertEquals(miniResponse.id, result.id) + // Verify fields absent from the mini response remain the same + assertEquals(originalCipher.folderId, result.folderId) + assertEquals(originalCipher.isFavorite, result.isFavorite) + assertEquals(originalCipher.shouldEdit, result.shouldEdit) + assertEquals(originalCipher.permissions, result.permissions) + assertEquals(originalCipher.shouldViewPassword, result.shouldViewPassword) + assertEquals(originalCipher.encryptedFor, result.encryptedFor) } @Test - fun `updateFromMiniResponse should preserve existing collectionIds when not provided`() { + fun `toEncryptedNetworkCipher from mini response should keep collectionIds when null`() { val originalCipher = createMockCipher( number = 1, collectionIds = listOf("original-collection-1", "original-collection-2"), @@ -592,8 +610,8 @@ class VaultSdkCipherExtensionsTest { val miniResponse = createMockCipherMiniResponse(number = 2) - val result = originalCipher.updateFromMiniResponse( - miniResponse = miniResponse, + val result = miniResponse.toEncryptedNetworkCipher( + originalCipher = originalCipher, collectionIds = null, ) diff --git a/network/src/main/kotlin/com/bitwarden/network/model/CipherMiniResponseJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/CipherMiniResponseJson.kt index 702e3311e8..d7d7736980 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/CipherMiniResponseJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/CipherMiniResponseJson.kt @@ -7,7 +7,8 @@ import java.time.Instant /** * Represents a minimal cipher response from the API, typically returned from bulk operations. - * Contains core cipher metadata without detailed type-specific fields. + * Contains cipher metadata and encrypted fields, but not user-specific fields (folder, + * favorite, permissions). * * @property cipherMiniResponse The list of mini responses. */ @@ -29,6 +30,18 @@ data class CipherMiniResponseJson( * @property reprompt The reprompt type. * @property key The cipher key (nullable). * @property archivedDate The archived date (nullable). + * @property name The encrypted name (nullable, `null` for blob-encrypted ciphers). + * @property notes The encrypted notes (nullable). + * @property login The login data (nullable). + * @property card The card data (nullable). + * @property identity The identity data (nullable). + * @property secureNote The secure note data (nullable). + * @property sshKey The SSH key data (nullable). + * @property bankAccount The bank account data (nullable). + * @property driversLicense The driver's license data (nullable). + * @property passport The passport data (nullable). + * @property fields List of custom fields (nullable). + * @property passwordHistory List of password history entries (nullable). */ @Serializable data class CipherMiniResponse( @@ -71,5 +84,41 @@ data class CipherMiniResponseJson( @SerialName("archivedDate") @Contextual val archivedDate: Instant?, + + @SerialName("name") + val name: String?, + + @SerialName("notes") + val notes: String?, + + @SerialName("login") + val login: SyncResponseJson.Cipher.Login?, + + @SerialName("card") + val card: SyncResponseJson.Cipher.Card?, + + @SerialName("identity") + val identity: SyncResponseJson.Cipher.Identity?, + + @SerialName("secureNote") + val secureNote: SyncResponseJson.Cipher.SecureNote?, + + @SerialName("sshKey") + val sshKey: SyncResponseJson.Cipher.SshKey?, + + @SerialName("bankAccount") + val bankAccount: SyncResponseJson.Cipher.BankAccount?, + + @SerialName("driversLicense") + val driversLicense: SyncResponseJson.Cipher.DriversLicense?, + + @SerialName("passport") + val passport: SyncResponseJson.Cipher.Passport?, + + @SerialName("fields") + val fields: List?, + + @SerialName("passwordHistory") + val passwordHistory: List?, ) } diff --git a/network/src/testFixtures/kotlin/com/bitwarden/network/model/CipherMiniResponseJsonUtil.kt b/network/src/testFixtures/kotlin/com/bitwarden/network/model/CipherMiniResponseJsonUtil.kt index 662ce7933f..838f3bc074 100644 --- a/network/src/testFixtures/kotlin/com/bitwarden/network/model/CipherMiniResponseJsonUtil.kt +++ b/network/src/testFixtures/kotlin/com/bitwarden/network/model/CipherMiniResponseJsonUtil.kt @@ -20,6 +20,18 @@ fun createMockCipherMiniResponse( reprompt = CipherRepromptTypeJson.NONE, key = "mockKey-$number", archivedDate = null, + name = "mockName-$number", + notes = "mockNotes-$number", + login = createMockLogin(number = number), + card = createMockCard(number = number), + identity = createMockIdentity(number = number), + secureNote = createMockSecureNote(), + sshKey = createMockSshKey(number = number), + bankAccount = createMockBankAccount(number = number), + driversLicense = createMockDriversLicense(number = number), + passport = createMockPassport(number = number), + fields = listOf(createMockField(number = number)), + passwordHistory = listOf(createMockPasswordHistory(number = number)), ) /**