diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a4a55d63f2..4fa74e8db0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -224,6 +224,7 @@ dependencies { implementation(project(":annotation")) implementation(project(":core")) + implementation(project(":cxf")) implementation(project(":data")) implementation(project(":network")) implementation(project(":ui")) @@ -245,6 +246,8 @@ dependencies { implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.core.ktx) + implementation(libs.androidx.credentials) + implementation(libs.androidx.credentials.providerevents) implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.lifecycle.process) implementation(libs.androidx.lifecycle.runtime.compose) @@ -258,7 +261,6 @@ dependencies { implementation(libs.androidx.work.runtime.ktx) implementation(libs.bitwarden.sdk) implementation(libs.bumptech.glide) - implementation(libs.androidx.credentials) implementation(libs.google.hilt.android) ksp(libs.google.hilt.compiler) implementation(libs.kotlinx.collections.immutable) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CredentialExchangeImportManager.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CredentialExchangeImportManager.kt new file mode 100644 index 0000000000..1e34b35c81 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CredentialExchangeImportManager.kt @@ -0,0 +1,20 @@ +package com.x8bit.bitwarden.data.vault.manager + +import com.x8bit.bitwarden.data.vault.manager.model.ImportCxfPayloadResult + +/** + * Manages the import process for Credential Exchange Format (CXF) payloads. + * + * This interface provides a contract for importing credential data from a standardized + * CXF string, associating it with a specific user. It handles the parsing, decryption, + * and storage of the credentials contained within the payload. + */ +interface CredentialExchangeImportManager { + + /** + * Attempt to import a CXF payload. + * + * @param payload The CXF payload to import. + */ + suspend fun importCxfPayload(userId: String, payload: String): ImportCxfPayloadResult +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CredentialExchangeImportManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CredentialExchangeImportManagerImpl.kt new file mode 100644 index 0000000000..35c1469cc1 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CredentialExchangeImportManagerImpl.kt @@ -0,0 +1,64 @@ +package com.x8bit.bitwarden.data.vault.manager + +import androidx.credentials.providerevents.exception.ImportCredentialsUnknownErrorException +import com.bitwarden.core.data.util.asFailure +import com.bitwarden.core.data.util.asSuccess +import com.bitwarden.core.data.util.flatMap +import com.bitwarden.network.model.ImportCiphersJsonRequest +import com.bitwarden.network.model.ImportCiphersResponseJson +import com.bitwarden.network.service.CiphersService +import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource +import com.x8bit.bitwarden.data.vault.manager.model.ImportCxfPayloadResult +import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher + +/** + * Default implementation of [CredentialExchangeImportManager]. + */ +class CredentialExchangeImportManagerImpl( + private val vaultSdkSource: VaultSdkSource, + private val ciphersService: CiphersService, +) : CredentialExchangeImportManager { + + override suspend fun importCxfPayload( + userId: String, + payload: String, + ): ImportCxfPayloadResult = vaultSdkSource + .importCxf( + userId = userId, + payload = payload, + ) + .flatMap { cipherList -> + if (cipherList.isEmpty()) { + // If no ciphers were returned, we can skip the remaining steps and return the + // appropriate result. + return ImportCxfPayloadResult.NoItems + } + ciphersService.importCiphers( + request = ImportCiphersJsonRequest( + ciphers = cipherList.map { + it.toEncryptedNetworkCipher( + encryptedFor = userId, + ) + }, + folders = emptyList(), + folderRelationships = emptyMap(), + ), + ) + } + .flatMap { importCiphersResponseJson -> + when (importCiphersResponseJson) { + is ImportCiphersResponseJson.Invalid -> { + ImportCredentialsUnknownErrorException().asFailure() + } + + ImportCiphersResponseJson.Success -> { + ImportCxfPayloadResult.Success + .asSuccess() + } + } + } + .fold( + onSuccess = { it }, + onFailure = { ImportCxfPayloadResult.Error(error = it) }, + ) +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt index 0198e3cdbb..db249bb4e7 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt @@ -18,6 +18,8 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.manager.CipherManager import com.x8bit.bitwarden.data.vault.manager.CipherManagerImpl +import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager +import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManagerImpl import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.manager.FileManagerImpl import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager @@ -134,4 +136,14 @@ object VaultManagerModule { userLogoutManager = userLogoutManager, clock = clock, ) + + @Provides + @Singleton + fun provideCredentialExchangeImportManager( + vaultSdkSource: VaultSdkSource, + ciphersService: CiphersService, + ): CredentialExchangeImportManager = CredentialExchangeImportManagerImpl( + vaultSdkSource = vaultSdkSource, + ciphersService = ciphersService, + ) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/model/ImportCxfPayloadResult.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/model/ImportCxfPayloadResult.kt similarity index 65% rename from app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/model/ImportCxfPayloadResult.kt rename to app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/model/ImportCxfPayloadResult.kt index 19c2c20aef..666fe49aaa 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/model/ImportCxfPayloadResult.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/model/ImportCxfPayloadResult.kt @@ -1,6 +1,4 @@ -package com.x8bit.bitwarden.data.vault.repository.model - -import com.bitwarden.vault.Cipher +package com.x8bit.bitwarden.data.vault.manager.model /** * Models result of the vault data being imported from a CXF payload. @@ -10,7 +8,12 @@ sealed class ImportCxfPayloadResult { /** * The vault data has been successfully imported. */ - data class Success(val ciphers: List) : ImportCxfPayloadResult() + data object Success : ImportCxfPayloadResult() + + /** + * There are no items to import. + */ + data object NoItems : ImportCxfPayloadResult() /** * There was an error importing the vault data. diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt index 10c5522949..0d90698c00 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt @@ -25,7 +25,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.DomainsData import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult -import com.x8bit.bitwarden.data.vault.repository.model.ImportCxfPayloadResult +import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult import com.x8bit.bitwarden.data.vault.repository.model.SendData import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult @@ -264,7 +264,7 @@ interface VaultRepository : CipherManager, VaultLockManager { * * @param payload The CXF payload to import. */ - suspend fun importCxfPayload(payload: String): ImportCxfPayloadResult + suspend fun importCxfPayload(payload: String): ImportCredentialsResult /** * Attempt to export the vault data to a CXF file. diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index 57c1e781ab..776e2e65a0 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -55,11 +55,13 @@ import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAn import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.manager.CipherManager +import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult +import com.x8bit.bitwarden.data.vault.manager.model.ImportCxfPayloadResult import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult @@ -69,7 +71,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.DomainsData import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult -import com.x8bit.bitwarden.data.vault.repository.model.ImportCxfPayloadResult +import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult import com.x8bit.bitwarden.data.vault.repository.model.SendData import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult @@ -152,6 +154,7 @@ class VaultRepositoryImpl( dispatcherManager: DispatcherManager, private val reviewPromptManager: ReviewPromptManager, private val vaultSyncManager: VaultSyncManager, + private val credentialExchangeImportManager: CredentialExchangeImportManager, ) : VaultRepository, CipherManager by cipherManager, VaultLockManager by vaultLockManager { @@ -968,18 +971,37 @@ class VaultRepositoryImpl( ) } - override suspend fun importCxfPayload(payload: String): ImportCxfPayloadResult { + override suspend fun importCxfPayload( + payload: String, + ): ImportCredentialsResult { val userId = activeUserId - ?: return ImportCxfPayloadResult.Error(error = NoActiveUserException()) - return vaultSdkSource - .importCxf( + ?: return ImportCredentialsResult.Error(error = NoActiveUserException()) + val importResult = credentialExchangeImportManager + .importCxfPayload( userId = userId, payload = payload, ) - .fold( - onSuccess = { ImportCxfPayloadResult.Success(it) }, - onFailure = { ImportCxfPayloadResult.Error(error = it) }, - ) + return when (importResult) { + is ImportCxfPayloadResult.Error -> { + ImportCredentialsResult.Error(error = importResult.error) + } + + ImportCxfPayloadResult.NoItems -> { + ImportCredentialsResult.NoItems + } + + ImportCxfPayloadResult.Success -> { + when (val syncResult = syncInternal(userId = userId, forced = true)) { + is SyncVaultDataResult.Error -> { + ImportCredentialsResult.SyncFailed(error = syncResult.throwable) + } + + is SyncVaultDataResult.Success -> { + ImportCredentialsResult.Success + } + } + } + } } override suspend fun exportVaultDataToCxf( diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt index ee4a0e90bd..c4f0754163 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt @@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.manager.CipherManager +import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager @@ -52,6 +53,7 @@ object VaultRepositoryModule { clock: Clock, reviewPromptManager: ReviewPromptManager, vaultSyncManager: VaultSyncManager, + credentialExchangeImportManager: CredentialExchangeImportManager, ): VaultRepository = VaultRepositoryImpl( sendsService = sendsService, ciphersService = ciphersService, @@ -70,5 +72,6 @@ object VaultRepositoryModule { clock = clock, reviewPromptManager = reviewPromptManager, vaultSyncManager = vaultSyncManager, + credentialExchangeImportManager = credentialExchangeImportManager, ) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/model/ImportCredentialsResult.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/model/ImportCredentialsResult.kt new file mode 100644 index 0000000000..7be7d0dec5 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/model/ImportCredentialsResult.kt @@ -0,0 +1,30 @@ +package com.x8bit.bitwarden.data.vault.repository.model + +/** + * Represents the result of importing credentials from a credential manager. + */ +sealed class ImportCredentialsResult { + + /** + * Indicates the vault data has been successfully imported. + */ + data object Success : ImportCredentialsResult() + + /** + * Indicates there are no items to import. + */ + data object NoItems : ImportCredentialsResult() + + /** + * Indicates the vault data has been successfully uploaded, but there was an error syncing the + * vault data. + */ + data class SyncFailed(val error: Throwable) : ImportCredentialsResult() + + /** + * Indicates there was an error importing the vault data. + * + * @param error The error that occurred during import. + */ + data class Error(val error: Throwable) : ImportCredentialsResult() +} diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/CredentialExchangeImportManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/CredentialExchangeImportManagerTest.kt new file mode 100644 index 0000000000..48361349bf --- /dev/null +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/CredentialExchangeImportManagerTest.kt @@ -0,0 +1,186 @@ +package com.x8bit.bitwarden.data.vault.manager + +import androidx.credentials.providerevents.exception.ImportCredentialsUnknownErrorException +import com.bitwarden.core.data.util.asFailure +import com.bitwarden.core.data.util.asSuccess +import com.bitwarden.network.model.ImportCiphersJsonRequest +import com.bitwarden.network.model.ImportCiphersResponseJson +import com.bitwarden.network.service.CiphersService +import com.bitwarden.vault.Cipher +import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCipher +import com.x8bit.bitwarden.data.vault.manager.model.ImportCxfPayloadResult +import io.mockk.awaits +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertNotNull + +class CredentialExchangeImportManagerTest { + + private val vaultSdkSource: VaultSdkSource = mockk() + private val ciphersService: CiphersService = mockk(relaxed = true) + + private val importManager = CredentialExchangeImportManagerImpl( + vaultSdkSource = vaultSdkSource, + ciphersService = ciphersService, + ) + + @Test + fun `when vaultSdkSource importCxf fails, should return Error`() = runTest { + val exception = RuntimeException("SDK import failed") + coEvery { + vaultSdkSource.importCxf( + userId = DEFAULT_USER_ID, + payload = DEFAULT_PAYLOAD, + ) + } returns exception.asFailure() + + coEvery { + ciphersService.importCiphers(any()) + } just awaits + + val result = importManager.importCxfPayload(DEFAULT_USER_ID, DEFAULT_PAYLOAD) + + assertEquals(ImportCxfPayloadResult.Error(exception), result) + coVerify(exactly = 1) { + vaultSdkSource.importCxf(DEFAULT_USER_ID, DEFAULT_PAYLOAD) + } + coVerify(exactly = 0) { + ciphersService.importCiphers(any()) + } + } + + @Test + fun `when ciphersService importCiphers fails, should return Error`() = runTest { + coEvery { + vaultSdkSource.importCxf( + userId = DEFAULT_USER_ID, + payload = DEFAULT_PAYLOAD, + ) + } returns DEFAULT_CIPHER_LIST.asSuccess() + + val exception = RuntimeException("Network import failed") + val capturedRequest = slot() + coEvery { + ciphersService.importCiphers(capture(capturedRequest)) + } returns exception.asFailure() + + val result = importManager.importCxfPayload(DEFAULT_USER_ID, DEFAULT_PAYLOAD) + + assertEquals(ImportCxfPayloadResult.Error(exception), result) + assertEquals(1, capturedRequest.captured.ciphers.size) + coVerify(exactly = 1) { + vaultSdkSource.importCxf(DEFAULT_USER_ID, DEFAULT_PAYLOAD) + ciphersService.importCiphers(any()) + } + } + + @Test + fun `when ciphersService importCiphers returns Invalid, should return Error`() = runTest { + coEvery { + vaultSdkSource.importCxf( + userId = DEFAULT_USER_ID, + payload = DEFAULT_PAYLOAD, + ) + } returns DEFAULT_CIPHER_LIST.asSuccess() + + coEvery { + ciphersService.importCiphers(any()) + } returns ImportCiphersResponseJson + .Invalid(validationErrors = emptyMap()) + .asSuccess() + + val result = importManager.importCxfPayload(DEFAULT_USER_ID, DEFAULT_PAYLOAD) + + val error = (result as? ImportCxfPayloadResult.Error)?.error + assertNotNull(error) + assertTrue(error is ImportCredentialsUnknownErrorException) + coVerify(exactly = 1) { + vaultSdkSource.importCxf(DEFAULT_USER_ID, DEFAULT_PAYLOAD) + ciphersService.importCiphers(any()) + } + } + + @Suppress("MaxLineLength") + @Test + fun `when ciphersService importCiphers is Success should return Success`() = + runTest { + coEvery { + vaultSdkSource.importCxf( + userId = DEFAULT_USER_ID, + payload = DEFAULT_PAYLOAD, + ) + } returns DEFAULT_CIPHER_LIST.asSuccess() + + coEvery { + ciphersService.importCiphers(any()) + } returns ImportCiphersResponseJson.Success.asSuccess() + + val result = importManager.importCxfPayload(DEFAULT_USER_ID, DEFAULT_PAYLOAD) + + assertTrue(result is ImportCxfPayloadResult.Success) + coVerify(exactly = 1) { + vaultSdkSource.importCxf(DEFAULT_USER_ID, DEFAULT_PAYLOAD) + ciphersService.importCiphers(any()) + } + } + + @Test + fun `when all steps succeed, should return Success`() = runTest { + coEvery { + vaultSdkSource.importCxf( + userId = DEFAULT_USER_ID, + payload = DEFAULT_PAYLOAD, + ) + } returns DEFAULT_CIPHER_LIST.asSuccess() + + coEvery { + ciphersService.importCiphers(any()) + } returns ImportCiphersResponseJson.Success.asSuccess() + + val result = importManager.importCxfPayload(DEFAULT_USER_ID, DEFAULT_PAYLOAD) + + assertEquals(ImportCxfPayloadResult.Success, result) + coVerify(exactly = 1) { + vaultSdkSource.importCxf(DEFAULT_USER_ID, DEFAULT_PAYLOAD) + ciphersService.importCiphers(any()) + } + } + + @Suppress("MaxLineLength") + @Test + fun `when importCxf returns empty cipher list, should skip importCiphers and sync and return NoItems`() = + runTest { + coEvery { + vaultSdkSource.importCxf( + userId = DEFAULT_USER_ID, + payload = DEFAULT_PAYLOAD, + ) + } returns emptyList().asSuccess() + coEvery { + ciphersService.importCiphers(any()) + } just awaits + + val result = importManager.importCxfPayload(DEFAULT_USER_ID, DEFAULT_PAYLOAD) + + assertEquals(ImportCxfPayloadResult.NoItems, result) + coVerify(exactly = 1) { + vaultSdkSource.importCxf(DEFAULT_USER_ID, DEFAULT_PAYLOAD) + } + coVerify(exactly = 0) { + ciphersService.importCiphers(any()) + } + } +} + +private const val DEFAULT_USER_ID = "mockId-1" +private const val DEFAULT_PAYLOAD = "mockPayload-1" +private val DEFAULT_CIPHER: Cipher = createMockSdkCipher(number = 1) +private val DEFAULT_CIPHER_LIST: List = listOf(DEFAULT_CIPHER) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt index cc13f13d02..ba71c8245f 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -74,10 +74,12 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFolder import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkSend import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView import com.x8bit.bitwarden.data.vault.manager.CipherManager +import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager +import com.x8bit.bitwarden.data.vault.manager.model.ImportCxfPayloadResult import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult @@ -87,7 +89,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult import com.x8bit.bitwarden.data.vault.repository.model.DomainsData import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult -import com.x8bit.bitwarden.data.vault.repository.model.ImportCxfPayloadResult +import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult import com.x8bit.bitwarden.data.vault.repository.model.SendData import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult @@ -216,6 +218,7 @@ class VaultRepositoryTest { every { syncFolderUpsertFlow } returns mutableSyncFolderUpsertFlow } private val vaultSyncManager: VaultSyncManager = mockk() + private val credentialExchangeImportManager: CredentialExchangeImportManager = mockk() private val vaultRepository = VaultRepositoryImpl( sendsService = sendsService, @@ -235,6 +238,7 @@ class VaultRepositoryTest { databaseSchemeManager = databaseSchemeManager, reviewPromptManager = reviewPromptManager, vaultSyncManager = vaultSyncManager, + credentialExchangeImportManager = credentialExchangeImportManager, ) @BeforeEach @@ -4080,44 +4084,113 @@ class VaultRepositoryTest { ) } + @Suppress("MaxLineLength") @Test - fun `importCxfPayload should return success result`() = runTest { - val userId = "mockId-1" - val payload = "payload" - val ciphers = listOf(createMockSdkCipher(number = 1)) - fakeAuthDiskSource.userState = MOCK_USER_STATE + fun `importCxfPayload should return success result when payload is successfully imported and vault sync is successful`() = + runTest { + val userId = "mockId-1" + val payload = "payload" + fakeAuthDiskSource.userState = MOCK_USER_STATE - coEvery { - vaultSdkSource.importCxf( - userId = userId, - payload = payload, + coEvery { + credentialExchangeImportManager.importCxfPayload( + userId = userId, + payload = payload, + ) + } returns ImportCxfPayloadResult.Success + coEvery { + vaultSyncManager.sync(userId = userId, forced = true) + } returns SyncVaultDataResult.Success(itemsAvailable = true) + val result = vaultRepository.importCxfPayload(payload) + + assertEquals( + ImportCredentialsResult.Success, + result, ) - } returns ciphers.asSuccess() - val result = vaultRepository.importCxfPayload(payload) + coVerify(exactly = 1) { + vaultSyncManager.sync(userId = userId, forced = true) + } + } - assertEquals( - ImportCxfPayloadResult.Success(ciphers), - result, + @Test + fun `importCxfPayload should return error result when activeUserId is null`() = runTest { + val result = vaultRepository.importCxfPayload("") + assertTrue( + (result as? ImportCredentialsResult.Error)?.error is NoActiveUserException, ) } @Test - fun `importCxfPayload should return error result`() = runTest { + fun `importCxfPayload should return error result when payload import fails`() = runTest { val userId = "mockId-1" val payload = "payload" val expected = Throwable() fakeAuthDiskSource.userState = MOCK_USER_STATE coEvery { - vaultSdkSource.importCxf( + credentialExchangeImportManager.importCxfPayload( userId = userId, payload = payload, ) - } returns expected.asFailure() + } returns ImportCxfPayloadResult.Error(expected) + val result = vaultRepository.importCxfPayload(payload) assertEquals( - ImportCxfPayloadResult.Error(expected), + ImportCredentialsResult.Error(expected), + result, + ) + coVerify(exactly = 0) { + vaultSyncManager.sync(userId = userId, forced = true) + } + } + + @Test + fun `importCxfPayload should return NoItems when payload contains no credentials`() = runTest { + val userId = "mockId-1" + val payload = "payload" + fakeAuthDiskSource.userState = MOCK_USER_STATE + + coEvery { + credentialExchangeImportManager.importCxfPayload( + userId = userId, + payload = payload, + ) + } returns ImportCxfPayloadResult.NoItems + + val result = vaultRepository.importCxfPayload(payload) + + assertEquals( + ImportCredentialsResult.NoItems, + result, + ) + coVerify(exactly = 0) { + vaultSyncManager.sync(userId = userId, forced = true) + } + } + + @Test + fun `importCxfPayload should return SyncFailed when sync fails`() = runTest { + val userId = "mockId-1" + val payload = "payload" + val throwable = Throwable() + fakeAuthDiskSource.userState = MOCK_USER_STATE + + coEvery { + credentialExchangeImportManager.importCxfPayload( + userId = userId, + payload = payload, + ) + } returns ImportCxfPayloadResult.Success + + coEvery { + vaultSyncManager.sync(userId = userId, forced = true) + } returns SyncVaultDataResult.Error(throwable) + + val result = vaultRepository.importCxfPayload(payload) + + assertEquals( + ImportCredentialsResult.SyncFailed(throwable), result, ) }