diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt index 5c9e099597..a20dcfa1bd 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt @@ -8,10 +8,12 @@ import com.bitwarden.core.data.util.asSuccess import com.bitwarden.core.data.util.flatMap import com.bitwarden.data.manager.file.FileManager import com.bitwarden.data.manager.model.DownloadResult +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.ShareCipherJsonRequest +import com.bitwarden.network.model.UnarchiveCipherResponseJson import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest import com.bitwarden.network.model.UpdateCipherResponseJson import com.bitwarden.network.service.CiphersService @@ -168,33 +170,29 @@ class CipherManagerImpl( cipherView: CipherView, ): ArchiveCipherResult { val userId = activeUserId ?: return ArchiveCipherResult.Error(NoActiveUserException()) - return cipherView - .encryptCipherAndCheckForMigration(userId = userId, cipherId = cipherId) - .flatMap { encryptionContext -> - ciphersService - .archiveCipher(cipherId = cipherId) - .flatMap { - vaultSdkSource.decryptCipher( - userId = userId, - cipher = encryptionContext.cipher, - ) + return ciphersService + .archiveCipher(cipherId = cipherId) + .flatMap { response -> + when (response) { + is ArchiveCipherResponseJson.Invalid -> { + IllegalStateException(response.firstValidationErrorMessage) + .asFailure() } - } - .flatMap { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = it.copy(archivedDate = clock.instant()), - ) - } - .onSuccess { - vaultDiskSource.saveCipher( - userId = userId, - cipher = it.toEncryptedNetworkCipherResponse(), - ) - settingsDiskSource.storeIntroducingArchiveActionCardDismissed( - userId = userId, - isDismissed = true, - ) + + is ArchiveCipherResponseJson.Success -> { + vaultDiskSource.saveCipher( + userId = userId, + cipher = response.cipher.copy( + collectionIds = cipherView.collectionIds, + ), + ) + settingsDiskSource.storeIntroducingArchiveActionCardDismissed( + userId = userId, + isDismissed = true, + ) + response.asSuccess() + } + } } .fold( onSuccess = { ArchiveCipherResult.Success }, @@ -207,29 +205,25 @@ class CipherManagerImpl( cipherView: CipherView, ): UnarchiveCipherResult { val userId = activeUserId ?: return UnarchiveCipherResult.Error(NoActiveUserException()) - return cipherView - .encryptCipherAndCheckForMigration(userId = userId, cipherId = cipherId) - .flatMap { encryptionContext -> - ciphersService - .unarchiveCipher(cipherId = cipherId) - .flatMap { - vaultSdkSource.decryptCipher( - userId = userId, - cipher = encryptionContext.cipher, - ) + return ciphersService + .unarchiveCipher(cipherId = cipherId) + .flatMap { response -> + when (response) { + is UnarchiveCipherResponseJson.Invalid -> { + IllegalStateException(response.firstValidationErrorMessage) + .asFailure() } - } - .flatMap { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = it.copy(archivedDate = null), - ) - } - .onSuccess { - vaultDiskSource.saveCipher( - userId = userId, - cipher = it.toEncryptedNetworkCipherResponse(), - ) + + is UnarchiveCipherResponseJson.Success -> { + vaultDiskSource.saveCipher( + userId = userId, + cipher = response.cipher.copy( + collectionIds = cipherView.collectionIds, + ), + ) + response.asSuccess() + } + } } .fold( onSuccess = { UnarchiveCipherResult.Success }, @@ -255,6 +249,9 @@ class CipherManagerImpl( ): DeleteCipherResult { val userId = activeUserId ?: return DeleteCipherResult.Error(error = NoActiveUserException()) + // Unlike archive/unarchive, soft delete requires edit permissions, so the + // migration check is intentional here to ensure the cipher is up-to-date + // before deletion. return cipherView .encryptCipherAndCheckForMigration(userId = userId, cipherId = cipherId) .flatMap { encryptionContext -> diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt index eabb8100a4..ab06ede848 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt @@ -8,11 +8,13 @@ import com.bitwarden.core.data.util.asFailure import com.bitwarden.core.data.util.asSuccess import com.bitwarden.data.manager.file.FileManager import com.bitwarden.data.manager.model.DownloadResult +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.ShareCipherJsonRequest import com.bitwarden.network.model.SyncResponseJson +import com.bitwarden.network.model.UnarchiveCipherResponseJson import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest import com.bitwarden.network.model.UpdateCipherResponseJson import com.bitwarden.network.model.createMockAttachment @@ -74,6 +76,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach 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 @@ -711,60 +714,63 @@ class CipherManagerTest { fun `archiveCipher with ciphersService archiveCipher failure should return ArchiveCipherResult Error`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = MOCK_USER_STATE.activeUserId val cipherId = "mockId-1" - val cipherView = createMockCipherView(number = 1) - val encryptionContext = createMockEncryptionContext(number = 1) val error = Throwable("Fail") - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView) - } returns encryptionContext.asSuccess() coEvery { ciphersService.archiveCipher(cipherId = cipherId) } returns error.asFailure() val result = cipherManager.archiveCipher( cipherId = cipherId, - cipherView = cipherView, + cipherView = createMockCipherView(number = 1), ) assertEquals(ArchiveCipherResult.Error(error = error), result) } + @Suppress("MaxLineLength") + @Test + fun `archiveCipher with ciphersService archiveCipher invalid should return ArchiveCipherResult Error`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val cipherId = "mockId-1" + coEvery { + ciphersService.archiveCipher(cipherId = cipherId) + } returns ArchiveCipherResponseJson.Invalid( + message = "You do not have permission to edit this.", + validationErrors = null, + ).asSuccess() + + val result = cipherManager.archiveCipher( + cipherId = cipherId, + cipherView = createMockCipherView(number = 1), + ) + + assertTrue(result is ArchiveCipherResult.Error) + } + @Suppress("MaxLineLength") @Test fun `archiveCipher with ciphersService archiveCipher success should return ArchiveCipherResult success`() = runTest { - val fixedInstant = Instant.parse("2023-10-27T12:00:00Z") val userId = "mockId-1" val cipherId = "mockId-1" - val encryptionContext = createMockEncryptionContext( - number = 1, - cipher = createMockSdkCipher(number = 1, clock = clock), - ) + val cipher = createMockCipher(number = 1) val cipherView = createMockCipherView(number = 1) fakeSettingsDiskSource.storeIntroducingArchiveActionCardDismissed( userId = userId, isDismissed = null, ) - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView) - } returns encryptionContext.asSuccess() - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = cipherView.copy(archivedDate = fixedInstant), - ) - } returns encryptionContext.asSuccess() - coEvery { - vaultSdkSource.decryptCipher(userId = userId, cipher = encryptionContext.cipher) - } returns cipherView.asSuccess() fakeAuthDiskSource.userState = MOCK_USER_STATE - coEvery { ciphersService.archiveCipher(cipherId = cipherId) } returns Unit.asSuccess() + coEvery { + ciphersService.archiveCipher(cipherId = cipherId) + } returns ArchiveCipherResponseJson.Success(cipher = cipher).asSuccess() coEvery { vaultDiskSource.saveCipher( userId = userId, - cipher = encryptionContext.toEncryptedNetworkCipherResponse(), + cipher = cipher.copy( + collectionIds = cipherView.collectionIds, + ), ) } just runs @@ -780,70 +786,6 @@ class CipherManagerTest { assertEquals(ArchiveCipherResult.Success, result) } - @Suppress("MaxLineLength") - @Test - fun `archiveCipher with cipher migration success should return ArchiveCipherResult success`() = - runTest { - val fixedInstant = Instant.parse("2023-10-27T12:00:00Z") - val userId = "mockId-1" - val cipherId = "mockId-1" - val encryptionContext = createMockEncryptionContext( - number = 1, - cipher = createMockSdkCipher(number = 1, clock = clock), - ) - val cipherView = createMockCipherView(number = 1).copy(key = null) - val networkCipher = createMockCipher(number = 1).copy(key = null) - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView) - } returns encryptionContext.asSuccess() - coEvery { - ciphersService.updateCipher( - cipherId = cipherId, - body = encryptionContext.toEncryptedNetworkCipher(), - ) - } returns UpdateCipherResponseJson.Success(networkCipher).asSuccess() - coEvery { - vaultDiskSource.saveCipher(userId = userId, cipher = networkCipher) - } just runs - coEvery { - vaultSdkSource.decryptCipher( - userId = userId, - cipher = networkCipher.toEncryptedSdkCipher(), - ) - } returns cipherView.asSuccess() - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = cipherView.copy(archivedDate = fixedInstant), - ) - } returns encryptionContext.asSuccess() - coEvery { - vaultSdkSource.decryptCipher(userId = userId, cipher = encryptionContext.cipher) - } returns cipherView.asSuccess() - fakeAuthDiskSource.userState = MOCK_USER_STATE - coEvery { ciphersService.archiveCipher(cipherId = cipherId) } returns Unit.asSuccess() - coEvery { - vaultDiskSource.saveCipher( - userId = userId, - cipher = encryptionContext.toEncryptedNetworkCipherResponse(), - ) - } just runs - - val result = cipherManager.archiveCipher( - cipherId = cipherId, - cipherView = cipherView, - ) - - assertEquals(ArchiveCipherResult.Success, result) - coVerify(exactly = 1) { - ciphersService.updateCipher( - cipherId = cipherId, - body = encryptionContext.toEncryptedNetworkCipher(), - ) - vaultDiskSource.saveCipher(userId = userId, cipher = networkCipher) - } - } - @Test fun `unarchiveCipher with no active user should return UnarchiveCipherResult Error`() = runTest { @@ -862,55 +804,59 @@ class CipherManagerTest { fun `unarchiveCipher with ciphersService unarchiveCipher failure should return UnarchiveCipherResult Error`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = MOCK_USER_STATE.activeUserId val cipherId = "mockId-1" - val cipherView = createMockCipherView(number = 1) - val encryptionContext = createMockEncryptionContext(number = 1) val error = Throwable("Fail") - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView) - } returns encryptionContext.asSuccess() coEvery { ciphersService.unarchiveCipher(cipherId = cipherId) } returns error.asFailure() val result = cipherManager.unarchiveCipher( cipherId = cipherId, - cipherView = cipherView, + cipherView = createMockCipherView(number = 1), ) assertEquals(UnarchiveCipherResult.Error(error = error), result) } + @Suppress("MaxLineLength") + @Test + fun `unarchiveCipher with ciphersService unarchiveCipher invalid should return UnarchiveCipherResult Error`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val cipherId = "mockId-1" + coEvery { + ciphersService.unarchiveCipher(cipherId = cipherId) + } returns UnarchiveCipherResponseJson.Invalid( + message = "You do not have permission to edit this.", + validationErrors = null, + ).asSuccess() + + val result = cipherManager.unarchiveCipher( + cipherId = cipherId, + cipherView = createMockCipherView(number = 1), + ) + + assertTrue(result is UnarchiveCipherResult.Error) + } + @Suppress("MaxLineLength") @Test fun `unarchiveCipher with ciphersService unarchiveCipher success should return UnarchiveCipherResult success`() = runTest { val userId = "mockId-1" val cipherId = "mockId-1" - val encryptionContext = createMockEncryptionContext( - number = 1, - cipher = createMockSdkCipher(number = 1, clock = clock), - ) + val cipher = createMockCipher(number = 1) val cipherView = createMockCipherView(number = 1) - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView) - } returns encryptionContext.asSuccess() - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = cipherView.copy(archivedDate = null), - ) - } returns encryptionContext.asSuccess() - coEvery { - vaultSdkSource.decryptCipher(userId = userId, cipher = encryptionContext.cipher) - } returns cipherView.asSuccess() fakeAuthDiskSource.userState = MOCK_USER_STATE - coEvery { ciphersService.unarchiveCipher(cipherId = cipherId) } returns Unit.asSuccess() + coEvery { + ciphersService.unarchiveCipher(cipherId = cipherId) + } returns UnarchiveCipherResponseJson.Success(cipher = cipher).asSuccess() coEvery { vaultDiskSource.saveCipher( userId = userId, - cipher = encryptionContext.toEncryptedNetworkCipherResponse(), + cipher = cipher.copy( + collectionIds = cipherView.collectionIds, + ), ) } just runs @@ -922,69 +868,6 @@ class CipherManagerTest { assertEquals(UnarchiveCipherResult.Success, result) } - @Suppress("MaxLineLength") - @Test - fun `unarchiveCipher with cipher migration success should return UnarchiveCipherResult success`() = - runTest { - val userId = "mockId-1" - val cipherId = "mockId-1" - val encryptionContext = createMockEncryptionContext( - number = 1, - cipher = createMockSdkCipher(number = 1, clock = clock), - ) - val cipherView = createMockCipherView(number = 1).copy(key = null) - val networkCipher = createMockCipher(number = 1).copy(key = null) - coEvery { - vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView) - } returns encryptionContext.asSuccess() - coEvery { - ciphersService.updateCipher( - cipherId = cipherId, - body = encryptionContext.toEncryptedNetworkCipher(), - ) - } returns UpdateCipherResponseJson.Success(networkCipher).asSuccess() - coEvery { - vaultDiskSource.saveCipher(userId = userId, cipher = networkCipher) - } just runs - coEvery { - vaultSdkSource.decryptCipher( - userId = userId, - cipher = networkCipher.toEncryptedSdkCipher(), - ) - } returns cipherView.asSuccess() - coEvery { - vaultSdkSource.encryptCipher( - userId = userId, - cipherView = cipherView.copy(archivedDate = null), - ) - } returns encryptionContext.asSuccess() - coEvery { - vaultSdkSource.decryptCipher(userId = userId, cipher = encryptionContext.cipher) - } returns cipherView.asSuccess() - fakeAuthDiskSource.userState = MOCK_USER_STATE - coEvery { ciphersService.unarchiveCipher(cipherId = cipherId) } returns Unit.asSuccess() - coEvery { - vaultDiskSource.saveCipher( - userId = userId, - cipher = encryptionContext.toEncryptedNetworkCipherResponse(), - ) - } just runs - - val result = cipherManager.unarchiveCipher( - cipherId = cipherId, - cipherView = cipherView, - ) - - assertEquals(UnarchiveCipherResult.Success, result) - coVerify(exactly = 1) { - ciphersService.updateCipher( - cipherId = cipherId, - body = encryptionContext.toEncryptedNetworkCipher(), - ) - vaultDiskSource.saveCipher(userId = userId, cipher = networkCipher) - } - } - @Test fun `hardDeleteCipher with no active user should return DeleteCipherResult Error`() = runTest { fakeAuthDiskSource.userState = null diff --git a/network/src/main/kotlin/com/bitwarden/network/api/CiphersApi.kt b/network/src/main/kotlin/com/bitwarden/network/api/CiphersApi.kt index 80917eb756..94f7237965 100644 --- a/network/src/main/kotlin/com/bitwarden/network/api/CiphersApi.kt +++ b/network/src/main/kotlin/com/bitwarden/network/api/CiphersApi.kt @@ -32,7 +32,7 @@ internal interface CiphersApi { @PUT("ciphers/{cipherId}/archive") suspend fun archiveCipher( @Path("cipherId") cipherId: String, - ): NetworkResult + ): NetworkResult /** * Unarchive a cipher. @@ -40,7 +40,7 @@ internal interface CiphersApi { @PUT("ciphers/{cipherId}/unarchive") suspend fun unarchiveCipher( @Path("cipherId") cipherId: String, - ): NetworkResult + ): NetworkResult /** * Create a cipher. diff --git a/network/src/main/kotlin/com/bitwarden/network/model/ArchiveCipherResponseJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/ArchiveCipherResponseJson.kt new file mode 100644 index 0000000000..76b760d31d --- /dev/null +++ b/network/src/main/kotlin/com/bitwarden/network/model/ArchiveCipherResponseJson.kt @@ -0,0 +1,33 @@ +package com.bitwarden.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Models the response from the archive cipher request. + */ +sealed class ArchiveCipherResponseJson { + /** + * The request completed successfully and returned the archived [cipher]. + */ + data class Success( + val cipher: SyncResponseJson.Cipher, + ) : ArchiveCipherResponseJson() + + /** + * Represents the json body of an invalid archive request. + * + * @param message A general, user-displayable error message. + * @param validationErrors a map where each value is a list of error messages for each key. + * The values in the array should be used for display to the user, since the keys tend to come + * back as nonsense. (eg: empty string key) + */ + @Serializable + data class Invalid( + @SerialName("message") + override val message: String, + + @SerialName("validationErrors") + override val validationErrors: Map>?, + ) : ArchiveCipherResponseJson(), InvalidJsonResponse +} diff --git a/network/src/main/kotlin/com/bitwarden/network/model/UnarchiveCipherResponseJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/UnarchiveCipherResponseJson.kt new file mode 100644 index 0000000000..08199e1314 --- /dev/null +++ b/network/src/main/kotlin/com/bitwarden/network/model/UnarchiveCipherResponseJson.kt @@ -0,0 +1,33 @@ +package com.bitwarden.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Models the response from the unarchive cipher request. + */ +sealed class UnarchiveCipherResponseJson { + /** + * The request completed successfully and returned the unarchived [cipher]. + */ + data class Success( + val cipher: SyncResponseJson.Cipher, + ) : UnarchiveCipherResponseJson() + + /** + * Represents the json body of an invalid unarchive request. + * + * @param message A general, user-displayable error message. + * @param validationErrors a map where each value is a list of error messages for each key. + * The values in the array should be used for display to the user, since the keys tend to come + * back as nonsense. (eg: empty string key) + */ + @Serializable + data class Invalid( + @SerialName("message") + override val message: String, + + @SerialName("validationErrors") + override val validationErrors: Map>?, + ) : UnarchiveCipherResponseJson(), InvalidJsonResponse +} diff --git a/network/src/main/kotlin/com/bitwarden/network/service/CiphersService.kt b/network/src/main/kotlin/com/bitwarden/network/service/CiphersService.kt index 083a259c40..8dabe8a5a3 100644 --- a/network/src/main/kotlin/com/bitwarden/network/service/CiphersService.kt +++ b/network/src/main/kotlin/com/bitwarden/network/service/CiphersService.kt @@ -1,5 +1,6 @@ package com.bitwarden.network.service +import com.bitwarden.network.model.ArchiveCipherResponseJson import com.bitwarden.network.model.AttachmentInfo import com.bitwarden.network.model.AttachmentJsonRequest import com.bitwarden.network.model.AttachmentJsonResponse @@ -12,6 +13,7 @@ import com.bitwarden.network.model.ImportCiphersJsonRequest import com.bitwarden.network.model.ImportCiphersResponseJson import com.bitwarden.network.model.ShareCipherJsonRequest import com.bitwarden.network.model.SyncResponseJson +import com.bitwarden.network.model.UnarchiveCipherResponseJson import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest import com.bitwarden.network.model.UpdateCipherResponseJson import java.io.File @@ -24,12 +26,12 @@ interface CiphersService { /** * Attempt to archive a cipher. */ - suspend fun archiveCipher(cipherId: String): Result + suspend fun archiveCipher(cipherId: String): Result /** * Attempt to unarchive a cipher. */ - suspend fun unarchiveCipher(cipherId: String): Result + suspend fun unarchiveCipher(cipherId: String): Result /** * Attempt to create a cipher. diff --git a/network/src/main/kotlin/com/bitwarden/network/service/CiphersServiceImpl.kt b/network/src/main/kotlin/com/bitwarden/network/service/CiphersServiceImpl.kt index bc764a8fca..1c57429be2 100644 --- a/network/src/main/kotlin/com/bitwarden/network/service/CiphersServiceImpl.kt +++ b/network/src/main/kotlin/com/bitwarden/network/service/CiphersServiceImpl.kt @@ -3,6 +3,7 @@ package com.bitwarden.network.service import androidx.core.net.toUri import com.bitwarden.network.api.AzureApi import com.bitwarden.network.api.CiphersApi +import com.bitwarden.network.model.ArchiveCipherResponseJson import com.bitwarden.network.model.AttachmentInfo import com.bitwarden.network.model.AttachmentJsonRequest import com.bitwarden.network.model.AttachmentJsonResponse @@ -16,6 +17,7 @@ import com.bitwarden.network.model.ImportCiphersJsonRequest import com.bitwarden.network.model.ImportCiphersResponseJson import com.bitwarden.network.model.ShareCipherJsonRequest import com.bitwarden.network.model.SyncResponseJson +import com.bitwarden.network.model.UnarchiveCipherResponseJson import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest import com.bitwarden.network.model.UpdateCipherResponseJson import com.bitwarden.network.model.toBitwardenError @@ -40,15 +42,37 @@ internal class CiphersServiceImpl( ) : CiphersService { override suspend fun archiveCipher( cipherId: String, - ): Result = ciphersApi - .archiveCipher(cipherId = cipherId) - .toResult() + ): Result = + ciphersApi + .archiveCipher(cipherId = cipherId) + .toResult() + .map { ArchiveCipherResponseJson.Success(cipher = it) } + .recoverCatching { throwable -> + throwable + .toBitwardenError() + .parseErrorBodyOrNull( + code = NetworkErrorCode.BAD_REQUEST, + json = json, + ) + ?: throw throwable + } override suspend fun unarchiveCipher( cipherId: String, - ): Result = ciphersApi - .unarchiveCipher(cipherId = cipherId) - .toResult() + ): Result = + ciphersApi + .unarchiveCipher(cipherId = cipherId) + .toResult() + .map { UnarchiveCipherResponseJson.Success(cipher = it) } + .recoverCatching { throwable -> + throwable + .toBitwardenError() + .parseErrorBodyOrNull( + code = NetworkErrorCode.BAD_REQUEST, + json = json, + ) + ?: throw throwable + } override suspend fun createCipher( body: CipherJsonRequest, diff --git a/network/src/test/kotlin/com/bitwarden/network/service/CiphersServiceTest.kt b/network/src/test/kotlin/com/bitwarden/network/service/CiphersServiceTest.kt index 1b302aeac5..41353d72cb 100644 --- a/network/src/test/kotlin/com/bitwarden/network/service/CiphersServiceTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/service/CiphersServiceTest.kt @@ -4,6 +4,7 @@ import android.net.Uri import com.bitwarden.network.api.AzureApi import com.bitwarden.network.api.CiphersApi import com.bitwarden.network.base.BaseServiceTest +import com.bitwarden.network.model.ArchiveCipherResponseJson import com.bitwarden.network.model.AttachmentJsonResponse import com.bitwarden.network.model.BulkShareCiphersJsonRequest import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest @@ -12,6 +13,7 @@ import com.bitwarden.network.model.FileUploadType import com.bitwarden.network.model.ImportCiphersJsonRequest import com.bitwarden.network.model.ImportCiphersResponseJson import com.bitwarden.network.model.ShareCipherJsonRequest +import com.bitwarden.network.model.UnarchiveCipherResponseJson import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest import com.bitwarden.network.model.UpdateCipherResponseJson import com.bitwarden.network.model.createMockAttachment @@ -65,20 +67,71 @@ class CiphersServiceTest : BaseServiceTest() { } @Test - fun `archiveCipher should execute the archiveCipher API`() = runTest { - server.enqueue(MockResponse().setResponseCode(200)) - val cipherId = "cipherId" - val result = ciphersService.archiveCipher(cipherId = cipherId) - assertEquals(Unit, result.getOrThrow()) - } + fun `archiveCipher with success response should return a Success with the correct cipher`() = + runTest { + server.enqueue( + MockResponse().setBody(CREATE_RESTORE_UPDATE_CIPHER_SUCCESS_JSON), + ) + val result = ciphersService.archiveCipher(cipherId = "cipherId") + assertEquals( + ArchiveCipherResponseJson.Success( + cipher = createMockCipher(number = 1), + ), + result.getOrThrow(), + ) + } @Test - fun `unarchiveCipher should execute the unarchiveCipher API`() = runTest { - server.enqueue(MockResponse().setResponseCode(200)) - val cipherId = "cipherId" - val result = ciphersService.unarchiveCipher(cipherId = cipherId) - assertEquals(Unit, result.getOrThrow()) - } + fun `archiveCipher with an invalid response should return an Invalid with the correct data`() = + runTest { + server.enqueue( + MockResponse() + .setResponseCode(400) + .setBody(UPDATE_CIPHER_INVALID_JSON), + ) + val result = ciphersService.archiveCipher(cipherId = "cipherId") + assertEquals( + ArchiveCipherResponseJson.Invalid( + message = "You do not have permission to edit this.", + validationErrors = null, + ), + result.getOrThrow(), + ) + } + + @Test + fun `unarchiveCipher with success response should return a Success with the correct cipher`() = + runTest { + server.enqueue( + MockResponse().setBody(CREATE_RESTORE_UPDATE_CIPHER_SUCCESS_JSON), + ) + val result = ciphersService.unarchiveCipher(cipherId = "cipherId") + assertEquals( + UnarchiveCipherResponseJson.Success( + cipher = createMockCipher(number = 1), + ), + result.getOrThrow(), + ) + } + + @Suppress("MaxLineLength") + @Test + fun `unarchiveCipher with an invalid response should return an Invalid with the correct data`() = + runTest { + server.enqueue( + MockResponse() + .setResponseCode(400) + .setBody(UPDATE_CIPHER_INVALID_JSON), + ) + val result = ciphersService.unarchiveCipher(cipherId = "cipherId") + assertEquals( + UnarchiveCipherResponseJson.Invalid( + message = "You do not have permission to edit this.", + validationErrors = null, + ), + result.getOrThrow(), + ) + } @Test fun `createCipher should return the correct response`() = runTest {