Expose adding/updating sends from vault repo (#516)

This commit is contained in:
David Perez 2024-01-06 22:14:09 -06:00 committed by Álison Fernandes
parent d00e7d69ea
commit f54af724b1
9 changed files with 369 additions and 2 deletions

View file

@ -4,10 +4,13 @@ import com.bitwarden.core.CipherView
import com.bitwarden.core.CollectionView
import com.bitwarden.core.FolderView
import com.bitwarden.core.Kdf
import com.bitwarden.core.SendView
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
@ -143,4 +146,17 @@ interface VaultRepository {
cipherId: String,
cipherView: CipherView,
): UpdateCipherResult
/**
* Attempt to create a send.
*/
suspend fun createSend(sendView: SendView): CreateSendResult
/**
* Attempt to update a send.
*/
suspend fun updateSend(
sendId: String,
sendView: SendView,
): UpdateSendResult
}

View file

@ -7,6 +7,7 @@ import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.InitUserCryptoRequest
import com.bitwarden.core.Kdf
import com.bitwarden.core.SendView
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson
@ -23,17 +24,22 @@ import com.x8bit.bitwarden.data.platform.util.flatMap
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateSendResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkSend
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
@ -68,10 +74,11 @@ private const val STOP_TIMEOUT_DELAY_MS: Long = 1000L
/**
* Default implementation of [VaultRepository].
*/
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LongParameterList")
class VaultRepositoryImpl(
private val syncService: SyncService,
private val ciphersService: CiphersService,
private val sendsService: SendsService,
private val vaultDiskSource: VaultDiskSource,
private val vaultSdkSource: VaultSdkSource,
private val authDiskSource: AuthDiskSource,
@ -416,6 +423,52 @@ class VaultRepositoryImpl(
},
)
override suspend fun createSend(sendView: SendView): CreateSendResult =
vaultSdkSource
.encryptSend(
userId = requireNotNull(activeUserId),
sendView = sendView,
)
.flatMap { send -> sendsService.createSend(body = send.toEncryptedNetworkSend()) }
.fold(
onFailure = { CreateSendResult.Error },
onSuccess = {
sync()
CreateSendResult.Success
},
)
override suspend fun updateSend(
sendId: String,
sendView: SendView,
): UpdateSendResult =
vaultSdkSource
.encryptSend(
userId = requireNotNull(activeUserId),
sendView = sendView,
)
.flatMap { send ->
sendsService.updateSend(
sendId = sendId,
body = send.toEncryptedNetworkSend(),
)
}
.fold(
onFailure = { UpdateSendResult.Error(errorMessage = null) },
onSuccess = { response ->
when (response) {
is UpdateSendResponseJson.Invalid -> {
UpdateSendResult.Error(errorMessage = response.message)
}
is UpdateSendResponseJson.Success -> {
sync()
UpdateSendResult.Success
}
}
},
)
// TODO: This is temporary. Eventually this needs to be based on the presence of various
// user keys but this will likely require SDK updates to support this (BIT-1190).
private fun setVaultToUnlocked(userId: String) {

View file

@ -4,6 +4,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
@ -25,6 +26,7 @@ object VaultRepositoryModule {
@Singleton
fun providesVaultRepository(
syncService: SyncService,
sendsService: SendsService,
ciphersService: CiphersService,
vaultDiskSource: VaultDiskSource,
vaultSdkSource: VaultSdkSource,
@ -32,6 +34,7 @@ object VaultRepositoryModule {
dispatcherManager: DispatcherManager,
): VaultRepository = VaultRepositoryImpl(
syncService = syncService,
sendsService = sendsService,
ciphersService = ciphersService,
vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource,

View file

@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.vault.repository.model
/**
* Models result of creating a send.
*/
sealed class CreateSendResult {
/**
* send created successfully.
*/
data object Success : CreateSendResult()
/**
* Generic error while creating a send.
*/
data object Error : CreateSendResult()
}

View file

@ -0,0 +1,18 @@
package com.x8bit.bitwarden.data.vault.repository.model
/**
* Models result of updating a send.
*/
sealed class UpdateSendResult {
/**
* Send updated successfully.
*/
data object Success : UpdateSendResult()
/**
* Generic error while updating a send. The optional [errorMessage] may be displayed directly
* in the UI when present.
*/
data class Error(val errorMessage: String?) : UpdateSendResult()
}

View file

@ -4,8 +4,61 @@ import com.bitwarden.core.Send
import com.bitwarden.core.SendFile
import com.bitwarden.core.SendText
import com.bitwarden.core.SendType
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import java.time.ZoneOffset
import java.time.ZonedDateTime
/**
* Converts a Bitwarden SDK [Send] object to a corresponding [SyncResponseJson.Send] object.
*/
fun Send.toEncryptedNetworkSend(): SendJsonRequest =
SendJsonRequest(
type = type.toNetworkSendType(),
name = name,
notes = notes,
key = key,
maxAccessCount = maxAccessCount?.toInt(),
expirationDate = expirationDate?.let { ZonedDateTime.ofInstant(it, ZoneOffset.UTC) },
deletionDate = ZonedDateTime.ofInstant(deletionDate, ZoneOffset.UTC),
file = file?.toNetworkSendFile(),
text = text?.toNetworkSendText(),
password = password,
isDisabled = disabled,
shouldHideEmail = hideEmail,
)
/**
* Converts a Bitwarden SDK [SendFile] object to a corresponding [SyncResponseJson.Send.File]
* object.
*/
private fun SendFile.toNetworkSendFile(): SyncResponseJson.Send.File =
SyncResponseJson.Send.File(
fileName = fileName,
size = size.toInt(),
sizeName = sizeName,
id = id,
)
/**
* Converts a Bitwarden SDK [SendText] object to a corresponding [SyncResponseJson.Send.Text]
* object.
*/
private fun SendText.toNetworkSendText(): SyncResponseJson.Send.Text =
SyncResponseJson.Send.Text(
isHidden = hidden,
text = text,
)
/**
* Converts a Bitwarden SDK [SendType] object to a corresponding [SendTypeJson] object.
*/
private fun SendType.toNetworkSendType(): SendTypeJson =
when (this) {
SendType.TEXT -> SendTypeJson.TEXT
SendType.FILE -> SendTypeJson.FILE
}
/**
* Converts a list of [SyncResponseJson.Send] objects to a list of corresponding

View file

@ -7,7 +7,7 @@ import java.time.ZonedDateTime
*/
fun createMockSendJsonRequest(
number: Int,
type: SendTypeJson = SendTypeJson.TEXT,
type: SendTypeJson = SendTypeJson.FILE,
): SendJsonRequest =
SendJsonRequest(
name = "mockName-$number",

View file

@ -22,6 +22,7 @@ import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateSendResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCollection
@ -29,8 +30,10 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockFolder
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganizationKeys
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSend
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSendJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSyncResponse
import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
@ -43,8 +46,10 @@ 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.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
@ -76,6 +81,7 @@ class VaultRepositoryTest {
private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
private val fakeAuthDiskSource = FakeAuthDiskSource()
private val syncService: SyncService = mockk()
private val sendsService: SendsService = mockk()
private val ciphersService: CiphersService = mockk()
private val vaultDiskSource: VaultDiskSource = mockk()
private val vaultSdkSource: VaultSdkSource = mockk {
@ -83,6 +89,7 @@ class VaultRepositoryTest {
}
private val vaultRepository = VaultRepositoryImpl(
syncService = syncService,
sendsService = sendsService,
ciphersService = ciphersService,
vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource,
@ -1915,6 +1922,198 @@ class VaultRepositoryTest {
assertEquals(UpdateCipherResult.Success, result)
}
@Test
fun `createSend with encryptSend failure should return CreateSendResult failure`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val mockSendView = createMockSendView(number = 1)
coEvery {
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
} returns IllegalStateException().asFailure()
val result = vaultRepository.createSend(sendView = mockSendView)
assertEquals(CreateSendResult.Error, result)
}
@Test
@Suppress("MaxLineLength")
fun `createSend with sendsService createSend failure should return CreateSendResult failure`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val mockSendView = createMockSendView(number = 1)
coEvery {
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
} returns createMockSdkSend(number = 1).asSuccess()
coEvery {
sendsService.createSend(body = createMockSendJsonRequest(number = 1))
} returns IllegalStateException().asFailure()
val result = vaultRepository.createSend(sendView = mockSendView)
assertEquals(CreateSendResult.Error, result)
}
@Test
@Suppress("MaxLineLength")
fun `createSend with sendsService createSend success should return CreateSendResult success`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val mockSendView = createMockSendView(number = 1)
coEvery {
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
} returns createMockSdkSend(number = 1).asSuccess()
coEvery {
sendsService.createSend(body = createMockSendJsonRequest(number = 1))
} returns createMockSend(number = 1).asSuccess()
coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(1))
coEvery {
vaultDiskSource.replaceVaultData(
userId = userId,
vault = createMockSyncResponse(1),
)
} just runs
coEvery {
vaultSdkSource.initializeOrganizationCrypto(
userId = userId,
request = InitOrgCryptoRequest(
organizationKeys = createMockOrganizationKeys(1),
),
)
} returns InitializeCryptoResult.Success.asSuccess()
val result = vaultRepository.createSend(sendView = mockSendView)
assertEquals(CreateSendResult.Success, result)
}
@Test
fun `updateSend with encryptSend failure should return UpdateSendResult failure`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val sendId = "sendId1234"
val mockSendView = createMockSendView(number = 1)
coEvery {
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
} returns IllegalStateException().asFailure()
val result = vaultRepository.updateSend(
sendId = sendId,
sendView = mockSendView,
)
assertEquals(UpdateSendResult.Error(errorMessage = null), result)
}
@Test
@Suppress("MaxLineLength")
fun `updateSend with sendsService updateSend failure should return UpdateSendResult Error with a null message`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val sendId = "sendId1234"
val mockSendView = createMockSendView(number = 1)
coEvery {
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
} returns createMockSdkSend(number = 1).asSuccess()
coEvery {
sendsService.updateSend(
sendId = sendId,
body = createMockSendJsonRequest(number = 1),
)
} returns IllegalStateException().asFailure()
val result = vaultRepository.updateSend(
sendId = sendId,
sendView = mockSendView,
)
assertEquals(UpdateSendResult.Error(errorMessage = null), result)
}
@Test
@Suppress("MaxLineLength")
fun `updateSend with sendsService updateSend Invalid response should return UpdateSendResult Error with a non-null message`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val sendId = "sendId1234"
val mockSendView = createMockSendView(number = 1)
coEvery {
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
} returns createMockSdkSend(number = 1).asSuccess()
coEvery {
sendsService.updateSend(
sendId = sendId,
body = createMockSendJsonRequest(number = 1),
)
} returns UpdateSendResponseJson
.Invalid(
message = "You do not have permission to edit this.",
validationErrors = null,
)
.asSuccess()
val result = vaultRepository.updateSend(
sendId = sendId,
sendView = mockSendView,
)
assertEquals(
UpdateSendResult.Error(
errorMessage = "You do not have permission to edit this.",
),
result,
)
}
@Test
@Suppress("MaxLineLength")
fun `updateSend with sendsService updateSend Success response should return UpdateSendResult success`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val sendId = "sendId1234"
val mockSendView = createMockSendView(number = 1)
coEvery {
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
} returns createMockSdkSend(number = 1).asSuccess()
coEvery {
sendsService.updateSend(
sendId = sendId,
body = createMockSendJsonRequest(number = 1),
)
} returns UpdateSendResponseJson
.Success(send = createMockSend(number = 1))
.asSuccess()
coEvery {
syncService.sync()
} returns Result.success(createMockSyncResponse(1))
coEvery {
vaultDiskSource.replaceVaultData(
userId = userId,
vault = createMockSyncResponse(1),
)
} just runs
coEvery {
vaultSdkSource.initializeOrganizationCrypto(
userId = userId,
request = InitOrgCryptoRequest(
organizationKeys = createMockOrganizationKeys(1),
),
)
} returns InitializeCryptoResult.Success.asSuccess()
val result = vaultRepository.updateSend(
sendId = sendId,
sendView = mockSendView,
)
assertEquals(UpdateSendResult.Success, result)
}
//region Helper functions
/**

View file

@ -1,12 +1,20 @@
package com.x8bit.bitwarden.data.vault.repository.util
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSend
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSendJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkSend
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class VaultSdkSendExtensionsTest {
@Test
fun `toEncryptedNetworkSend should convert a SDK-based Send to network-based Send`() {
val sdkSend = createMockSdkSend(number = 1)
val networkSend = sdkSend.toEncryptedNetworkSend()
assertEquals(createMockSendJsonRequest(number = 1), networkSend)
}
@Test
fun `toEncryptedSdkSend should convert a network-based Send to SDK-based Send`() {
val syncSend = createMockSend(number = 1)