[PM-32802] fix: 400 error when archiving/unarchiving org-owned ciphers (#6592)

This commit is contained in:
Patrick Honkonen
2026-02-27 15:10:58 -05:00
committed by GitHub
parent e509d60af6
commit 60bc6ee0ca
8 changed files with 273 additions and 248 deletions

View File

@@ -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
return ciphersService
.archiveCipher(cipherId = cipherId)
.flatMap {
vaultSdkSource.decryptCipher(
userId = userId,
cipher = encryptionContext.cipher,
)
.flatMap { response ->
when (response) {
is ArchiveCipherResponseJson.Invalid -> {
IllegalStateException(response.firstValidationErrorMessage)
.asFailure()
}
}
.flatMap {
vaultSdkSource.encryptCipher(
userId = userId,
cipherView = it.copy(archivedDate = clock.instant()),
)
}
.onSuccess {
is ArchiveCipherResponseJson.Success -> {
vaultDiskSource.saveCipher(
userId = userId,
cipher = it.toEncryptedNetworkCipherResponse(),
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
return ciphersService
.unarchiveCipher(cipherId = cipherId)
.flatMap {
vaultSdkSource.decryptCipher(
userId = userId,
cipher = encryptionContext.cipher,
)
.flatMap { response ->
when (response) {
is UnarchiveCipherResponseJson.Invalid -> {
IllegalStateException(response.firstValidationErrorMessage)
.asFailure()
}
}
.flatMap {
vaultSdkSource.encryptCipher(
userId = userId,
cipherView = it.copy(archivedDate = null),
)
}
.onSuccess {
is UnarchiveCipherResponseJson.Success -> {
vaultDiskSource.saveCipher(
userId = userId,
cipher = it.toEncryptedNetworkCipherResponse(),
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 ->

View File

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

View File

@@ -32,7 +32,7 @@ internal interface CiphersApi {
@PUT("ciphers/{cipherId}/archive")
suspend fun archiveCipher(
@Path("cipherId") cipherId: String,
): NetworkResult<Unit>
): NetworkResult<SyncResponseJson.Cipher>
/**
* Unarchive a cipher.
@@ -40,7 +40,7 @@ internal interface CiphersApi {
@PUT("ciphers/{cipherId}/unarchive")
suspend fun unarchiveCipher(
@Path("cipherId") cipherId: String,
): NetworkResult<Unit>
): NetworkResult<SyncResponseJson.Cipher>
/**
* Create a cipher.

View File

@@ -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<String, List<String>>?,
) : ArchiveCipherResponseJson(), InvalidJsonResponse
}

View File

@@ -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<String, List<String>>?,
) : UnarchiveCipherResponseJson(), InvalidJsonResponse
}

View File

@@ -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<Unit>
suspend fun archiveCipher(cipherId: String): Result<ArchiveCipherResponseJson>
/**
* Attempt to unarchive a cipher.
*/
suspend fun unarchiveCipher(cipherId: String): Result<Unit>
suspend fun unarchiveCipher(cipherId: String): Result<UnarchiveCipherResponseJson>
/**
* Attempt to create a cipher.

View File

@@ -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<Unit> = ciphersApi
): Result<ArchiveCipherResponseJson> =
ciphersApi
.archiveCipher(cipherId = cipherId)
.toResult()
.map { ArchiveCipherResponseJson.Success(cipher = it) }
.recoverCatching { throwable ->
throwable
.toBitwardenError()
.parseErrorBodyOrNull<ArchiveCipherResponseJson.Invalid>(
code = NetworkErrorCode.BAD_REQUEST,
json = json,
)
?: throw throwable
}
override suspend fun unarchiveCipher(
cipherId: String,
): Result<Unit> = ciphersApi
): Result<UnarchiveCipherResponseJson> =
ciphersApi
.unarchiveCipher(cipherId = cipherId)
.toResult()
.map { UnarchiveCipherResponseJson.Success(cipher = it) }
.recoverCatching { throwable ->
throwable
.toBitwardenError()
.parseErrorBodyOrNull<UnarchiveCipherResponseJson.Invalid>(
code = NetworkErrorCode.BAD_REQUEST,
json = json,
)
?: throw throwable
}
override suspend fun createCipher(
body: CipherJsonRequest,

View File

@@ -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,19 +67,70 @@ 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