Compare commits

...

1 Commits

Author SHA1 Message Date
David Perez
727cf0e585 Remove Retrofit dependency from app module 2026-05-08 15:37:05 -05:00
16 changed files with 161 additions and 53 deletions

View File

@@ -285,8 +285,6 @@ dependencies {
implementation(libs.kotlinx.serialization)
implementation(platform(libs.square.okhttp.bom))
implementation(libs.square.okhttp)
implementation(platform(libs.square.retrofit.bom))
implementation(libs.square.retrofit)
implementation(libs.timber)
// For now we are restricted to running Compose tests for debug builds only

View File

@@ -12,6 +12,7 @@ import com.bitwarden.network.model.ArchiveCipherResponseJson
import com.bitwarden.network.model.AttachmentJsonResponse
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
import com.bitwarden.network.model.CreateCipherResponseJson
import com.bitwarden.network.model.GetCipherResponse
import com.bitwarden.network.model.ShareCipherJsonRequest
import com.bitwarden.network.model.UnarchiveCipherResponseJson
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
@@ -48,7 +49,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import retrofit2.HttpException
import java.io.File
import java.time.Clock
@@ -763,6 +763,7 @@ class CipherManagerImpl(
* are met. If the resource cannot be found cloud-side, and it was updated, delete it from disk
* for now.
*/
@Suppress("CyclomaticComplexMethod")
private suspend fun syncCipherIfNecessary(syncCipherUpsertData: SyncCipherUpsertData) {
val userId = syncCipherUpsertData.userId
val cipherId = syncCipherUpsertData.cipherId
@@ -817,15 +818,22 @@ class CipherManagerImpl(
ciphersService
.getCipher(cipherId = cipherId)
.fold(
onSuccess = { vaultDiskSource.saveCipher(userId = userId, cipher = it) },
onFailure = {
// Delete any updates if it's missing from the server
val httpException = it as? HttpException
@Suppress("MagicNumber")
if (httpException?.code() == 404 && isUpdate) {
vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId)
onSuccess = { response ->
when (response) {
is GetCipherResponse.NotFound -> {
if (isUpdate) {
vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId)
}
}
is GetCipherResponse.Success -> {
vaultDiskSource.saveCipher(userId = userId, cipher = response.cipher)
}
}
},
onFailure = {
// no-op
},
)
}
}

View File

@@ -8,6 +8,7 @@ import com.bitwarden.core.data.util.flatMap
import com.bitwarden.data.manager.file.FileManager
import com.bitwarden.network.model.CreateFileSendResponse
import com.bitwarden.network.model.CreateSendJsonResponse
import com.bitwarden.network.model.GetSendResponse
import com.bitwarden.network.model.UpdateSendResponseJson
import com.bitwarden.network.service.SendsService
import com.bitwarden.send.Send
@@ -32,7 +33,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import retrofit2.HttpException
/**
* The default implementation of the [SendManager].
@@ -291,15 +291,22 @@ class SendManagerImpl(
sendsService
.getSend(sendId = sendId)
.fold(
onSuccess = { vaultDiskSource.saveSend(userId = userId, send = it) },
onFailure = {
// Delete any updates if it's missing from the server
val httpException = it as? HttpException
@Suppress("MagicNumber")
if (httpException?.code() == 404 && isUpdate) {
vaultDiskSource.deleteSend(userId = userId, sendId = sendId)
onSuccess = { response ->
when (response) {
is GetSendResponse.NotFound -> {
if (isUpdate) {
vaultDiskSource.deleteSend(userId = userId, sendId = sendId)
}
}
is GetSendResponse.Success -> {
vaultDiskSource.saveSend(userId = userId, send = response.send)
}
}
},
onFailure = {
// no-op
},
)
}
}

View File

@@ -13,6 +13,7 @@ import com.bitwarden.network.model.ArchiveCipherResponseJson
import com.bitwarden.network.model.AttachmentJsonRequest
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
import com.bitwarden.network.model.CreateCipherResponseJson
import com.bitwarden.network.model.GetCipherResponse
import com.bitwarden.network.model.ShareCipherJsonRequest
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.UnarchiveCipherResponseJson
@@ -80,7 +81,6 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import retrofit2.HttpException
import java.io.File
import java.time.Clock
import java.time.Instant
@@ -2610,6 +2610,7 @@ class CipherManagerTest {
revisionDate = clock.instant().minus(5, ChronoUnit.MINUTES),
)
val updatedCipher = mockk<SyncResponseJson.Cipher>()
val updatedCipherResponse = GetCipherResponse.Success(cipher = updatedCipher)
fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery {
@@ -2620,7 +2621,7 @@ class CipherManagerTest {
} returns MutableStateFlow(listOf(collection))
coEvery {
ciphersService.getCipher(cipherId = cipherId)
} returns updatedCipher.asSuccess()
} returns updatedCipherResponse.asSuccess()
coEvery {
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
} just runs
@@ -2655,6 +2656,7 @@ class CipherManagerTest {
revisionDate = clock.instant().minus(5, ChronoUnit.MINUTES),
)
val updatedCipher = mockk<SyncResponseJson.Cipher>()
val updatedCipherResponse = GetCipherResponse.Success(cipher = updatedCipher)
val collection = createMockCollection(number = number)
fakeAuthDiskSource.userState = MOCK_USER_STATE
@@ -2663,7 +2665,7 @@ class CipherManagerTest {
vaultDiskSource.getCollectionsFlow(userId = userId)
} returns MutableStateFlow(listOf(collection))
coEvery { ciphersService.getCipher(cipherId) } returns updatedCipher.asSuccess()
coEvery { ciphersService.getCipher(cipherId) } returns updatedCipherResponse.asSuccess()
coEvery {
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
} just runs
@@ -2761,10 +2763,9 @@ class CipherManagerTest {
coEvery {
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
} returns createMockCipher(number = number)
val response: HttpException = mockk {
every { code() } returns 404
}
coEvery { ciphersService.getCipher(cipherId = cipherId) } returns response.asFailure()
val response = GetCipherResponse.NotFound(throwable = Throwable("Fail"))
coEvery { ciphersService.getCipher(cipherId = cipherId) } returns response.asSuccess()
coEvery { vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId) } just runs
fakeAuthDiskSource.userState = MOCK_USER_STATE
@@ -2794,10 +2795,8 @@ class CipherManagerTest {
val cipherId = "mockId-1"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val response: HttpException = mockk {
every { code() } returns 404
}
coEvery { ciphersService.getCipher(cipherId = cipherId) } returns response.asFailure()
val response = GetCipherResponse.NotFound(throwable = Throwable("Fail"))
coEvery { ciphersService.getCipher(cipherId = cipherId) } returns response.asSuccess()
coEvery { vaultDiskSource.getCipher(userId = userId, cipherId = cipherId) } returns null
mutableSyncCipherUpsertFlow.tryEmit(
@@ -2828,6 +2827,7 @@ class CipherManagerTest {
val userId = MOCK_USER_STATE.activeUserId
val cipherId = "mockId-$number"
val updatedCipher = mockk<SyncResponseJson.Cipher>()
val updatedCipherResponse = GetCipherResponse.Success(cipher = updatedCipher)
fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery {
@@ -2835,7 +2835,7 @@ class CipherManagerTest {
} returns null
coEvery {
ciphersService.getCipher(cipherId = cipherId)
} returns updatedCipher.asSuccess()
} returns updatedCipherResponse.asSuccess()
coEvery {
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
} just runs
@@ -2869,12 +2869,13 @@ class CipherManagerTest {
every { revisionDate } returns clock.instant().minus(5, ChronoUnit.MINUTES)
}
val updatedCipher = mockk<SyncResponseJson.Cipher>()
val updatedCipherResponse = GetCipherResponse.Success(cipher = updatedCipher)
fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery {
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
} returns originalCipher
coEvery { ciphersService.getCipher(cipherId) } returns updatedCipher.asSuccess()
coEvery { ciphersService.getCipher(cipherId) } returns updatedCipherResponse.asSuccess()
coEvery {
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
} just runs

View File

@@ -10,6 +10,7 @@ import com.bitwarden.data.manager.file.FileManager
import com.bitwarden.network.exception.CookieRedirectException
import com.bitwarden.network.model.CreateFileSendResponse
import com.bitwarden.network.model.CreateSendJsonResponse
import com.bitwarden.network.model.GetSendResponse
import com.bitwarden.network.model.SendTypeJson
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.UpdateSendResponseJson
@@ -53,7 +54,6 @@ 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 retrofit2.HttpException
import java.io.File
import java.time.Clock
import java.time.Instant
@@ -186,10 +186,11 @@ class SendManagerTest {
revisionDate = FIXED_CLOCK.instant(),
)
val updatedSend = createMockSend(number = number)
val updatedSendResponse = GetSendResponse.Success(send = updatedSend)
coEvery {
vaultDiskSource.getSendsFlow(userId = userId)
} returns MutableStateFlow(listOf(send))
coEvery { sendsService.getSend(sendId = sendId) } returns updatedSend.asSuccess()
coEvery { sendsService.getSend(sendId = sendId) } returns updatedSendResponse.asSuccess()
coEvery { vaultDiskSource.saveSend(userId = userId, send = updatedSend) } just runs
mutableSyncSendUpsertFlow.tryEmit(
@@ -216,10 +217,8 @@ class SendManagerTest {
val sendId = "mockId-$number"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val response: HttpException = mockk {
every { code() } returns 404
}
coEvery { sendsService.getSend(sendId = sendId) } returns response.asFailure()
val response = GetSendResponse.NotFound(throwable = Throwable("Fail"))
coEvery { sendsService.getSend(sendId = sendId) } returns response.asSuccess()
coEvery {
vaultDiskSource.deleteSend(userId = userId, sendId = sendId)
} just runs
@@ -255,10 +254,8 @@ class SendManagerTest {
val sendId = "mockId-1"
fakeAuthDiskSource.userState = MOCK_USER_STATE
val response: HttpException = mockk {
every { code() } returns 404
}
coEvery { sendsService.getSend(sendId = sendId) } returns response.asFailure()
val response = GetSendResponse.NotFound(throwable = Throwable("Fail"))
coEvery { sendsService.getSend(sendId = sendId) } returns response.asSuccess()
coEvery {
vaultDiskSource.getSendsFlow(userId = userId)
} returns MutableStateFlow(emptyList())
@@ -293,7 +290,10 @@ class SendManagerTest {
vaultDiskSource.getSendsFlow(userId = userId)
} returns MutableStateFlow(emptyList())
val send = mockk<SyncResponseJson.Send>()
coEvery { sendsService.getSend(sendId = sendId) } returns send.asSuccess()
val updatedSendResponse = GetSendResponse.Success(send = send)
coEvery {
sendsService.getSend(sendId = sendId)
} returns updatedSendResponse.asSuccess()
coEvery { vaultDiskSource.saveSend(userId = userId, send = send) } just runs
mutableSyncSendUpsertFlow.tryEmit(
@@ -329,7 +329,10 @@ class SendManagerTest {
} returns MutableStateFlow(listOf(sendView))
val send = mockk<SyncResponseJson.Send>()
coEvery { sendsService.getSend(sendId = sendId) } returns send.asSuccess()
val updatedSendResponse = GetSendResponse.Success(send = send)
coEvery {
sendsService.getSend(sendId = sendId)
} returns updatedSendResponse.asSuccess()
coEvery { vaultDiskSource.saveSend(userId = userId, send = send) } just runs
mutableSyncSendUpsertFlow.tryEmit(

View File

@@ -0,0 +1,20 @@
package com.bitwarden.network.model
/**
* Models response body from fetching a Cipher.
*/
sealed class GetCipherResponse {
/**
* Models response of a successful Get Cipher request.
*/
data class Success(
val cipher: SyncResponseJson.Cipher,
) : GetCipherResponse()
/**
* Models the response of an unfindable Get Cipher request.
*/
data class NotFound(
val throwable: Throwable,
) : GetCipherResponse()
}

View File

@@ -0,0 +1,20 @@
package com.bitwarden.network.model
/**
* Models response body from fetching a Send.
*/
sealed class GetSendResponse {
/**
* Models response of a successful Get Send request.
*/
data class Success(
val send: SyncResponseJson.Send,
) : GetSendResponse()
/**
* Models the response of an unfindable Get Send request.
*/
data class NotFound(
val throwable: Throwable,
) : GetSendResponse()
}

View File

@@ -9,6 +9,7 @@ import com.bitwarden.network.model.CipherJsonRequest
import com.bitwarden.network.model.CipherMiniResponseJson
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
import com.bitwarden.network.model.CreateCipherResponseJson
import com.bitwarden.network.model.GetCipherResponse
import com.bitwarden.network.model.ImportCiphersJsonRequest
import com.bitwarden.network.model.ImportCiphersResponseJson
import com.bitwarden.network.model.ShareCipherJsonRequest
@@ -128,7 +129,7 @@ interface CiphersService {
/**
* Attempt to retrieve a cipher.
*/
suspend fun getCipher(cipherId: String): Result<SyncResponseJson.Cipher>
suspend fun getCipher(cipherId: String): Result<GetCipherResponse>
/**
* Attempt to retrieve a cipher's attachment data.

View File

@@ -13,6 +13,7 @@ import com.bitwarden.network.model.CipherMiniResponseJson
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
import com.bitwarden.network.model.CreateCipherResponseJson
import com.bitwarden.network.model.FileUploadType
import com.bitwarden.network.model.GetCipherResponse
import com.bitwarden.network.model.ImportCiphersJsonRequest
import com.bitwarden.network.model.ImportCiphersResponseJson
import com.bitwarden.network.model.ShareCipherJsonRequest
@@ -22,6 +23,7 @@ import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
import com.bitwarden.network.model.UpdateCipherResponseJson
import com.bitwarden.network.model.toBitwardenError
import com.bitwarden.network.util.NetworkErrorCode
import com.bitwarden.network.util.isErrorCode
import com.bitwarden.network.util.parseErrorBodyOrNull
import com.bitwarden.network.util.toResult
import kotlinx.serialization.json.Json
@@ -269,10 +271,19 @@ internal class CiphersServiceImpl(
override suspend fun getCipher(
cipherId: String,
): Result<SyncResponseJson.Cipher> =
): Result<GetCipherResponse> =
ciphersApi
.getCipher(cipherId = cipherId)
.toResult()
.map { GetCipherResponse.Success(cipher = it) }
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
if (bitwardenError.isErrorCode(code = NetworkErrorCode.NOT_FOUND)) {
GetCipherResponse.NotFound(throwable = throwable)
} else {
throw throwable
}
}
override suspend fun getCipherAttachment(
cipherId: String,

View File

@@ -121,6 +121,7 @@ internal class IdentityServiceImpl(
}
NetworkErrorCode.BAD_REQUEST,
NetworkErrorCode.NOT_FOUND,
NetworkErrorCode.TOO_MANY_REQUESTS,
null,
-> throw throwable

View File

@@ -3,6 +3,7 @@ package com.bitwarden.network.service
import com.bitwarden.network.model.CreateFileSendResponse
import com.bitwarden.network.model.CreateFileSendResponseJson
import com.bitwarden.network.model.CreateSendJsonResponse
import com.bitwarden.network.model.GetSendResponse
import com.bitwarden.network.model.SendJsonRequest
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.UpdateSendResponseJson
@@ -43,7 +44,7 @@ interface SendsService {
): Result<UpdateSendResponseJson>
/**
* Attempt to delete a send.
* Attempt to delete a Send.
*/
suspend fun deleteSend(
sendId: String,
@@ -57,7 +58,7 @@ interface SendsService {
): Result<UpdateSendResponseJson>
/**
* Attempt to retrieve a send.
* Attempt to retrieve a Send.
*/
suspend fun getSend(sendId: String): Result<SyncResponseJson.Send>
suspend fun getSend(sendId: String): Result<GetSendResponse>
}

View File

@@ -7,11 +7,13 @@ import com.bitwarden.network.model.CreateFileSendResponse
import com.bitwarden.network.model.CreateFileSendResponseJson
import com.bitwarden.network.model.CreateSendJsonResponse
import com.bitwarden.network.model.FileUploadType
import com.bitwarden.network.model.GetSendResponse
import com.bitwarden.network.model.SendJsonRequest
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.UpdateSendResponseJson
import com.bitwarden.network.model.toBitwardenError
import com.bitwarden.network.util.NetworkErrorCode
import com.bitwarden.network.util.isErrorCode
import com.bitwarden.network.util.parseErrorBodyOrNull
import com.bitwarden.network.util.toResult
import kotlinx.serialization.json.Json
@@ -152,8 +154,17 @@ internal class SendsServiceImpl(
override suspend fun getSend(
sendId: String,
): Result<SyncResponseJson.Send> =
): Result<GetSendResponse> =
sendsApi
.getSend(sendId = sendId)
.toResult()
.map { GetSendResponse.Success(send = it) }
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
if (bitwardenError.isErrorCode(code = NetworkErrorCode.NOT_FOUND)) {
GetSendResponse.NotFound(throwable = throwable)
} else {
throw throwable
}
}
}

View File

@@ -13,6 +13,13 @@ internal fun BitwardenError.getNetworkErrorCodeOrNull(): NetworkErrorCode? =
NetworkErrorCode.entries.firstOrNull { httpError.code == it.code }
}
/**
* Returns the `true` if the underlying error is from the [code] provided.
*/
internal fun BitwardenError.isErrorCode(
code: NetworkErrorCode,
): Boolean = (this as? BitwardenError.Http)?.let { code.code == this.code } == true
/**
* Attempt to parse the error body to serializable type [T].
*

View File

@@ -9,5 +9,6 @@ internal enum class NetworkErrorCode(
BAD_REQUEST(code = 400),
UNAUTHORIZED(code = 401),
FORBIDDEN(code = 403),
NOT_FOUND(code = 404),
TOO_MANY_REQUESTS(code = 429),
}

View File

@@ -10,6 +10,7 @@ import com.bitwarden.network.model.BulkShareCiphersJsonRequest
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
import com.bitwarden.network.model.CreateCipherResponseJson
import com.bitwarden.network.model.FileUploadType
import com.bitwarden.network.model.GetCipherResponse
import com.bitwarden.network.model.ImportCiphersJsonRequest
import com.bitwarden.network.model.ImportCiphersResponseJson
import com.bitwarden.network.model.ShareCipherJsonRequest
@@ -35,6 +36,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertInstanceOf
import retrofit2.create
import java.io.File
import java.time.Clock
@@ -440,15 +442,22 @@ class CiphersServiceTest : BaseServiceTest() {
}
@Test
fun `getCipher should return the correct response`() = runTest {
fun `getCipher with success should return the correct response`() = runTest {
server.enqueue(MockResponse().setBody(CREATE_RESTORE_UPDATE_CIPHER_SUCCESS_JSON))
val result = ciphersService.getCipher(cipherId = "mockId-1")
assertEquals(
createMockCipher(number = 1),
GetCipherResponse.Success(cipher = createMockCipher(number = 1)),
result.getOrThrow(),
)
}
@Test
fun `getSend with 404 should return the NotFound response`() = runTest {
server.enqueue(MockResponse().setResponseCode(404))
val result = ciphersService.getCipher("mockId-1")
assertInstanceOf<GetCipherResponse.NotFound>(result.getOrThrow())
}
@Test
fun `getCipherAttachment should return the correct response`() = runTest {
server.enqueue(MockResponse().setBody(GET_CIPHER_ATTACHMENT_SUCCESS_JSON))

View File

@@ -6,6 +6,7 @@ import com.bitwarden.network.api.SendsApi
import com.bitwarden.network.base.BaseServiceTest
import com.bitwarden.network.model.CreateFileSendResponse
import com.bitwarden.network.model.CreateSendJsonResponse
import com.bitwarden.network.model.GetSendResponse
import com.bitwarden.network.model.SendTypeJson
import com.bitwarden.network.model.UpdateSendResponseJson
import com.bitwarden.network.model.createMockFileSendResponseJson
@@ -21,6 +22,7 @@ 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 org.junit.jupiter.api.assertInstanceOf
import retrofit2.create
import java.io.File
import java.time.Clock
@@ -180,11 +182,18 @@ class SendsServiceTest : BaseServiceTest() {
}
@Test
fun `getSend should return the correct response`() = runTest {
fun `getSend with success should return the success response`() = runTest {
val response = createMockSend(number = 1)
server.enqueue(MockResponse().setBody(CREATE_UPDATE_SEND_SUCCESS_JSON))
val result = sendsService.getSend("mockId-1")
assertEquals(response, result.getOrThrow())
assertEquals(GetSendResponse.Success(send = response), result.getOrThrow())
}
@Test
fun `getSend with 404 should return the NotFound response`() = runTest {
server.enqueue(MockResponse().setResponseCode(404))
val result = sendsService.getSend("mockId-1")
assertInstanceOf<GetSendResponse.NotFound>(result.getOrThrow())
}
private fun setupMockUri(