[PM-25664] Add CredentialExchangeImportManager for CXF payload import (#5872)

This commit is contained in:
Patrick Honkonen 2025-09-16 17:30:24 -04:00 committed by GitHub
parent 766e6b1bb9
commit f22f4399be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 450 additions and 35 deletions

View File

@ -224,6 +224,7 @@ dependencies {
implementation(project(":annotation")) implementation(project(":annotation"))
implementation(project(":core")) implementation(project(":core"))
implementation(project(":cxf"))
implementation(project(":data")) implementation(project(":data"))
implementation(project(":network")) implementation(project(":network"))
implementation(project(":ui")) implementation(project(":ui"))
@ -245,6 +246,8 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.credentials)
implementation(libs.androidx.credentials.providerevents)
implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.lifecycle.process) implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.runtime.compose)
@ -258,7 +261,6 @@ dependencies {
implementation(libs.androidx.work.runtime.ktx) implementation(libs.androidx.work.runtime.ktx)
implementation(libs.bitwarden.sdk) implementation(libs.bitwarden.sdk)
implementation(libs.bumptech.glide) implementation(libs.bumptech.glide)
implementation(libs.androidx.credentials)
implementation(libs.google.hilt.android) implementation(libs.google.hilt.android)
ksp(libs.google.hilt.compiler) ksp(libs.google.hilt.compiler)
implementation(libs.kotlinx.collections.immutable) implementation(libs.kotlinx.collections.immutable)

View File

@ -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
}

View File

@ -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) },
)
}

View File

@ -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.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.manager.CipherManager import com.x8bit.bitwarden.data.vault.manager.CipherManager
import com.x8bit.bitwarden.data.vault.manager.CipherManagerImpl 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.FileManager
import com.x8bit.bitwarden.data.vault.manager.FileManagerImpl import com.x8bit.bitwarden.data.vault.manager.FileManagerImpl
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
@ -134,4 +136,14 @@ object VaultManagerModule {
userLogoutManager = userLogoutManager, userLogoutManager = userLogoutManager,
clock = clock, clock = clock,
) )
@Provides
@Singleton
fun provideCredentialExchangeImportManager(
vaultSdkSource: VaultSdkSource,
ciphersService: CiphersService,
): CredentialExchangeImportManager = CredentialExchangeImportManagerImpl(
vaultSdkSource = vaultSdkSource,
ciphersService = ciphersService,
)
} }

View File

@ -1,6 +1,4 @@
package com.x8bit.bitwarden.data.vault.repository.model package com.x8bit.bitwarden.data.vault.manager.model
import com.bitwarden.vault.Cipher
/** /**
* Models result of the vault data being imported from a CXF payload. * 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. * The vault data has been successfully imported.
*/ */
data class Success(val ciphers: List<Cipher>) : ImportCxfPayloadResult() data object Success : ImportCxfPayloadResult()
/**
* There are no items to import.
*/
data object NoItems : ImportCxfPayloadResult()
/** /**
* There was an error importing the vault data. * There was an error importing the vault data.

View File

@ -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.DomainsData
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult 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.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.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
@ -264,7 +264,7 @@ interface VaultRepository : CipherManager, VaultLockManager {
* *
* @param payload The CXF payload to import. * @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. * Attempt to export the vault data to a CXF file.

View File

@ -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.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.manager.CipherManager 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.FileManager
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager 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.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.SyncVaultDataResult
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult 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.DomainsData
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult 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.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.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
@ -152,6 +154,7 @@ class VaultRepositoryImpl(
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
private val reviewPromptManager: ReviewPromptManager, private val reviewPromptManager: ReviewPromptManager,
private val vaultSyncManager: VaultSyncManager, private val vaultSyncManager: VaultSyncManager,
private val credentialExchangeImportManager: CredentialExchangeImportManager,
) : VaultRepository, ) : VaultRepository,
CipherManager by cipherManager, CipherManager by cipherManager,
VaultLockManager by vaultLockManager { 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 val userId = activeUserId
?: return ImportCxfPayloadResult.Error(error = NoActiveUserException()) ?: return ImportCredentialsResult.Error(error = NoActiveUserException())
return vaultSdkSource val importResult = credentialExchangeImportManager
.importCxf( .importCxfPayload(
userId = userId, userId = userId,
payload = payload, payload = payload,
) )
.fold( return when (importResult) {
onSuccess = { ImportCxfPayloadResult.Success(it) }, is ImportCxfPayloadResult.Error -> {
onFailure = { ImportCxfPayloadResult.Error(error = it) }, 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( override suspend fun exportVaultDataToCxf(

View File

@ -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.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.manager.CipherManager 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.FileManager
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
@ -52,6 +53,7 @@ object VaultRepositoryModule {
clock: Clock, clock: Clock,
reviewPromptManager: ReviewPromptManager, reviewPromptManager: ReviewPromptManager,
vaultSyncManager: VaultSyncManager, vaultSyncManager: VaultSyncManager,
credentialExchangeImportManager: CredentialExchangeImportManager,
): VaultRepository = VaultRepositoryImpl( ): VaultRepository = VaultRepositoryImpl(
sendsService = sendsService, sendsService = sendsService,
ciphersService = ciphersService, ciphersService = ciphersService,
@ -70,5 +72,6 @@ object VaultRepositoryModule {
clock = clock, clock = clock,
reviewPromptManager = reviewPromptManager, reviewPromptManager = reviewPromptManager,
vaultSyncManager = vaultSyncManager, vaultSyncManager = vaultSyncManager,
credentialExchangeImportManager = credentialExchangeImportManager,
) )
} }

View File

@ -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()
}

View File

@ -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<ImportCiphersJsonRequest>()
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<Cipher>().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<Cipher> = listOf(DEFAULT_CIPHER)

View File

@ -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.createMockSdkSend
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
import com.x8bit.bitwarden.data.vault.manager.CipherManager 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.FileManager
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager 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.SyncVaultDataResult
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult 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.DomainsData
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult 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.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.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
@ -216,6 +218,7 @@ class VaultRepositoryTest {
every { syncFolderUpsertFlow } returns mutableSyncFolderUpsertFlow every { syncFolderUpsertFlow } returns mutableSyncFolderUpsertFlow
} }
private val vaultSyncManager: VaultSyncManager = mockk() private val vaultSyncManager: VaultSyncManager = mockk()
private val credentialExchangeImportManager: CredentialExchangeImportManager = mockk()
private val vaultRepository = VaultRepositoryImpl( private val vaultRepository = VaultRepositoryImpl(
sendsService = sendsService, sendsService = sendsService,
@ -235,6 +238,7 @@ class VaultRepositoryTest {
databaseSchemeManager = databaseSchemeManager, databaseSchemeManager = databaseSchemeManager,
reviewPromptManager = reviewPromptManager, reviewPromptManager = reviewPromptManager,
vaultSyncManager = vaultSyncManager, vaultSyncManager = vaultSyncManager,
credentialExchangeImportManager = credentialExchangeImportManager,
) )
@BeforeEach @BeforeEach
@ -4080,44 +4084,113 @@ class VaultRepositoryTest {
) )
} }
@Suppress("MaxLineLength")
@Test @Test
fun `importCxfPayload should return success result`() = runTest { fun `importCxfPayload should return success result when payload is successfully imported and vault sync is successful`() =
val userId = "mockId-1" runTest {
val payload = "payload" val userId = "mockId-1"
val ciphers = listOf(createMockSdkCipher(number = 1)) val payload = "payload"
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery { coEvery {
vaultSdkSource.importCxf( credentialExchangeImportManager.importCxfPayload(
userId = userId, userId = userId,
payload = payload, 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() coVerify(exactly = 1) {
val result = vaultRepository.importCxfPayload(payload) vaultSyncManager.sync(userId = userId, forced = true)
}
}
assertEquals( @Test
ImportCxfPayloadResult.Success(ciphers), fun `importCxfPayload should return error result when activeUserId is null`() = runTest {
result, val result = vaultRepository.importCxfPayload("")
assertTrue(
(result as? ImportCredentialsResult.Error)?.error is NoActiveUserException,
) )
} }
@Test @Test
fun `importCxfPayload should return error result`() = runTest { fun `importCxfPayload should return error result when payload import fails`() = runTest {
val userId = "mockId-1" val userId = "mockId-1"
val payload = "payload" val payload = "payload"
val expected = Throwable() val expected = Throwable()
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery { coEvery {
vaultSdkSource.importCxf( credentialExchangeImportManager.importCxfPayload(
userId = userId, userId = userId,
payload = payload, payload = payload,
) )
} returns expected.asFailure() } returns ImportCxfPayloadResult.Error(expected)
val result = vaultRepository.importCxfPayload(payload) val result = vaultRepository.importCxfPayload(payload)
assertEquals( 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, result,
) )
} }