mirror of
https://github.com/bitwarden/android.git
synced 2026-03-09 03:33:36 -05:00
[PM-32802] fix: 400 error when archiving/unarchiving org-owned ciphers (#6592)
This commit is contained in:
@@ -8,10 +8,12 @@ import com.bitwarden.core.data.util.asSuccess
|
|||||||
import com.bitwarden.core.data.util.flatMap
|
import com.bitwarden.core.data.util.flatMap
|
||||||
import com.bitwarden.data.manager.file.FileManager
|
import com.bitwarden.data.manager.file.FileManager
|
||||||
import com.bitwarden.data.manager.model.DownloadResult
|
import com.bitwarden.data.manager.model.DownloadResult
|
||||||
|
import com.bitwarden.network.model.ArchiveCipherResponseJson
|
||||||
import com.bitwarden.network.model.AttachmentJsonResponse
|
import com.bitwarden.network.model.AttachmentJsonResponse
|
||||||
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
|
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
|
||||||
import com.bitwarden.network.model.CreateCipherResponseJson
|
import com.bitwarden.network.model.CreateCipherResponseJson
|
||||||
import com.bitwarden.network.model.ShareCipherJsonRequest
|
import com.bitwarden.network.model.ShareCipherJsonRequest
|
||||||
|
import com.bitwarden.network.model.UnarchiveCipherResponseJson
|
||||||
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
|
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
|
||||||
import com.bitwarden.network.model.UpdateCipherResponseJson
|
import com.bitwarden.network.model.UpdateCipherResponseJson
|
||||||
import com.bitwarden.network.service.CiphersService
|
import com.bitwarden.network.service.CiphersService
|
||||||
@@ -168,33 +170,29 @@ class CipherManagerImpl(
|
|||||||
cipherView: CipherView,
|
cipherView: CipherView,
|
||||||
): ArchiveCipherResult {
|
): ArchiveCipherResult {
|
||||||
val userId = activeUserId ?: return ArchiveCipherResult.Error(NoActiveUserException())
|
val userId = activeUserId ?: return ArchiveCipherResult.Error(NoActiveUserException())
|
||||||
return cipherView
|
return ciphersService
|
||||||
.encryptCipherAndCheckForMigration(userId = userId, cipherId = cipherId)
|
.archiveCipher(cipherId = cipherId)
|
||||||
.flatMap { encryptionContext ->
|
.flatMap { response ->
|
||||||
ciphersService
|
when (response) {
|
||||||
.archiveCipher(cipherId = cipherId)
|
is ArchiveCipherResponseJson.Invalid -> {
|
||||||
.flatMap {
|
IllegalStateException(response.firstValidationErrorMessage)
|
||||||
vaultSdkSource.decryptCipher(
|
.asFailure()
|
||||||
userId = userId,
|
|
||||||
cipher = encryptionContext.cipher,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.flatMap {
|
is ArchiveCipherResponseJson.Success -> {
|
||||||
vaultSdkSource.encryptCipher(
|
vaultDiskSource.saveCipher(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
cipherView = it.copy(archivedDate = clock.instant()),
|
cipher = response.cipher.copy(
|
||||||
)
|
collectionIds = cipherView.collectionIds,
|
||||||
}
|
),
|
||||||
.onSuccess {
|
)
|
||||||
vaultDiskSource.saveCipher(
|
settingsDiskSource.storeIntroducingArchiveActionCardDismissed(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
cipher = it.toEncryptedNetworkCipherResponse(),
|
isDismissed = true,
|
||||||
)
|
)
|
||||||
settingsDiskSource.storeIntroducingArchiveActionCardDismissed(
|
response.asSuccess()
|
||||||
userId = userId,
|
}
|
||||||
isDismissed = true,
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.fold(
|
.fold(
|
||||||
onSuccess = { ArchiveCipherResult.Success },
|
onSuccess = { ArchiveCipherResult.Success },
|
||||||
@@ -207,29 +205,25 @@ class CipherManagerImpl(
|
|||||||
cipherView: CipherView,
|
cipherView: CipherView,
|
||||||
): UnarchiveCipherResult {
|
): UnarchiveCipherResult {
|
||||||
val userId = activeUserId ?: return UnarchiveCipherResult.Error(NoActiveUserException())
|
val userId = activeUserId ?: return UnarchiveCipherResult.Error(NoActiveUserException())
|
||||||
return cipherView
|
return ciphersService
|
||||||
.encryptCipherAndCheckForMigration(userId = userId, cipherId = cipherId)
|
.unarchiveCipher(cipherId = cipherId)
|
||||||
.flatMap { encryptionContext ->
|
.flatMap { response ->
|
||||||
ciphersService
|
when (response) {
|
||||||
.unarchiveCipher(cipherId = cipherId)
|
is UnarchiveCipherResponseJson.Invalid -> {
|
||||||
.flatMap {
|
IllegalStateException(response.firstValidationErrorMessage)
|
||||||
vaultSdkSource.decryptCipher(
|
.asFailure()
|
||||||
userId = userId,
|
|
||||||
cipher = encryptionContext.cipher,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.flatMap {
|
is UnarchiveCipherResponseJson.Success -> {
|
||||||
vaultSdkSource.encryptCipher(
|
vaultDiskSource.saveCipher(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
cipherView = it.copy(archivedDate = null),
|
cipher = response.cipher.copy(
|
||||||
)
|
collectionIds = cipherView.collectionIds,
|
||||||
}
|
),
|
||||||
.onSuccess {
|
)
|
||||||
vaultDiskSource.saveCipher(
|
response.asSuccess()
|
||||||
userId = userId,
|
}
|
||||||
cipher = it.toEncryptedNetworkCipherResponse(),
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.fold(
|
.fold(
|
||||||
onSuccess = { UnarchiveCipherResult.Success },
|
onSuccess = { UnarchiveCipherResult.Success },
|
||||||
@@ -255,6 +249,9 @@ class CipherManagerImpl(
|
|||||||
): DeleteCipherResult {
|
): DeleteCipherResult {
|
||||||
val userId = activeUserId
|
val userId = activeUserId
|
||||||
?: return DeleteCipherResult.Error(error = NoActiveUserException())
|
?: 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
|
return cipherView
|
||||||
.encryptCipherAndCheckForMigration(userId = userId, cipherId = cipherId)
|
.encryptCipherAndCheckForMigration(userId = userId, cipherId = cipherId)
|
||||||
.flatMap { encryptionContext ->
|
.flatMap { encryptionContext ->
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import com.bitwarden.core.data.util.asFailure
|
|||||||
import com.bitwarden.core.data.util.asSuccess
|
import com.bitwarden.core.data.util.asSuccess
|
||||||
import com.bitwarden.data.manager.file.FileManager
|
import com.bitwarden.data.manager.file.FileManager
|
||||||
import com.bitwarden.data.manager.model.DownloadResult
|
import com.bitwarden.data.manager.model.DownloadResult
|
||||||
|
import com.bitwarden.network.model.ArchiveCipherResponseJson
|
||||||
import com.bitwarden.network.model.AttachmentJsonRequest
|
import com.bitwarden.network.model.AttachmentJsonRequest
|
||||||
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
|
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
|
||||||
import com.bitwarden.network.model.CreateCipherResponseJson
|
import com.bitwarden.network.model.CreateCipherResponseJson
|
||||||
import com.bitwarden.network.model.ShareCipherJsonRequest
|
import com.bitwarden.network.model.ShareCipherJsonRequest
|
||||||
import com.bitwarden.network.model.SyncResponseJson
|
import com.bitwarden.network.model.SyncResponseJson
|
||||||
|
import com.bitwarden.network.model.UnarchiveCipherResponseJson
|
||||||
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
|
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
|
||||||
import com.bitwarden.network.model.UpdateCipherResponseJson
|
import com.bitwarden.network.model.UpdateCipherResponseJson
|
||||||
import com.bitwarden.network.model.createMockAttachment
|
import com.bitwarden.network.model.createMockAttachment
|
||||||
@@ -74,6 +76,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
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.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
@@ -711,60 +714,63 @@ class CipherManagerTest {
|
|||||||
fun `archiveCipher with ciphersService archiveCipher failure should return ArchiveCipherResult Error`() =
|
fun `archiveCipher with ciphersService archiveCipher failure should return ArchiveCipherResult Error`() =
|
||||||
runTest {
|
runTest {
|
||||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
val userId = MOCK_USER_STATE.activeUserId
|
|
||||||
val cipherId = "mockId-1"
|
val cipherId = "mockId-1"
|
||||||
val cipherView = createMockCipherView(number = 1)
|
|
||||||
val encryptionContext = createMockEncryptionContext(number = 1)
|
|
||||||
val error = Throwable("Fail")
|
val error = Throwable("Fail")
|
||||||
coEvery {
|
|
||||||
vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView)
|
|
||||||
} returns encryptionContext.asSuccess()
|
|
||||||
coEvery {
|
coEvery {
|
||||||
ciphersService.archiveCipher(cipherId = cipherId)
|
ciphersService.archiveCipher(cipherId = cipherId)
|
||||||
} returns error.asFailure()
|
} returns error.asFailure()
|
||||||
|
|
||||||
val result = cipherManager.archiveCipher(
|
val result = cipherManager.archiveCipher(
|
||||||
cipherId = cipherId,
|
cipherId = cipherId,
|
||||||
cipherView = cipherView,
|
cipherView = createMockCipherView(number = 1),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(ArchiveCipherResult.Error(error = error), result)
|
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")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `archiveCipher with ciphersService archiveCipher success should return ArchiveCipherResult success`() =
|
fun `archiveCipher with ciphersService archiveCipher success should return ArchiveCipherResult success`() =
|
||||||
runTest {
|
runTest {
|
||||||
val fixedInstant = Instant.parse("2023-10-27T12:00:00Z")
|
|
||||||
val userId = "mockId-1"
|
val userId = "mockId-1"
|
||||||
val cipherId = "mockId-1"
|
val cipherId = "mockId-1"
|
||||||
val encryptionContext = createMockEncryptionContext(
|
val cipher = createMockCipher(number = 1)
|
||||||
number = 1,
|
|
||||||
cipher = createMockSdkCipher(number = 1, clock = clock),
|
|
||||||
)
|
|
||||||
val cipherView = createMockCipherView(number = 1)
|
val cipherView = createMockCipherView(number = 1)
|
||||||
fakeSettingsDiskSource.storeIntroducingArchiveActionCardDismissed(
|
fakeSettingsDiskSource.storeIntroducingArchiveActionCardDismissed(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
isDismissed = null,
|
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
|
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 {
|
coEvery {
|
||||||
vaultDiskSource.saveCipher(
|
vaultDiskSource.saveCipher(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
cipher = encryptionContext.toEncryptedNetworkCipherResponse(),
|
cipher = cipher.copy(
|
||||||
|
collectionIds = cipherView.collectionIds,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
} just runs
|
} just runs
|
||||||
|
|
||||||
@@ -780,70 +786,6 @@ class CipherManagerTest {
|
|||||||
assertEquals(ArchiveCipherResult.Success, result)
|
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
|
@Test
|
||||||
fun `unarchiveCipher with no active user should return UnarchiveCipherResult Error`() =
|
fun `unarchiveCipher with no active user should return UnarchiveCipherResult Error`() =
|
||||||
runTest {
|
runTest {
|
||||||
@@ -862,55 +804,59 @@ class CipherManagerTest {
|
|||||||
fun `unarchiveCipher with ciphersService unarchiveCipher failure should return UnarchiveCipherResult Error`() =
|
fun `unarchiveCipher with ciphersService unarchiveCipher failure should return UnarchiveCipherResult Error`() =
|
||||||
runTest {
|
runTest {
|
||||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
val userId = MOCK_USER_STATE.activeUserId
|
|
||||||
val cipherId = "mockId-1"
|
val cipherId = "mockId-1"
|
||||||
val cipherView = createMockCipherView(number = 1)
|
|
||||||
val encryptionContext = createMockEncryptionContext(number = 1)
|
|
||||||
val error = Throwable("Fail")
|
val error = Throwable("Fail")
|
||||||
coEvery {
|
|
||||||
vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView)
|
|
||||||
} returns encryptionContext.asSuccess()
|
|
||||||
coEvery {
|
coEvery {
|
||||||
ciphersService.unarchiveCipher(cipherId = cipherId)
|
ciphersService.unarchiveCipher(cipherId = cipherId)
|
||||||
} returns error.asFailure()
|
} returns error.asFailure()
|
||||||
|
|
||||||
val result = cipherManager.unarchiveCipher(
|
val result = cipherManager.unarchiveCipher(
|
||||||
cipherId = cipherId,
|
cipherId = cipherId,
|
||||||
cipherView = cipherView,
|
cipherView = createMockCipherView(number = 1),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(UnarchiveCipherResult.Error(error = error), result)
|
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")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `unarchiveCipher with ciphersService unarchiveCipher success should return UnarchiveCipherResult success`() =
|
fun `unarchiveCipher with ciphersService unarchiveCipher success should return UnarchiveCipherResult success`() =
|
||||||
runTest {
|
runTest {
|
||||||
val userId = "mockId-1"
|
val userId = "mockId-1"
|
||||||
val cipherId = "mockId-1"
|
val cipherId = "mockId-1"
|
||||||
val encryptionContext = createMockEncryptionContext(
|
val cipher = createMockCipher(number = 1)
|
||||||
number = 1,
|
|
||||||
cipher = createMockSdkCipher(number = 1, clock = clock),
|
|
||||||
)
|
|
||||||
val cipherView = createMockCipherView(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
|
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 {
|
coEvery {
|
||||||
vaultDiskSource.saveCipher(
|
vaultDiskSource.saveCipher(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
cipher = encryptionContext.toEncryptedNetworkCipherResponse(),
|
cipher = cipher.copy(
|
||||||
|
collectionIds = cipherView.collectionIds,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
} just runs
|
} just runs
|
||||||
|
|
||||||
@@ -922,69 +868,6 @@ class CipherManagerTest {
|
|||||||
assertEquals(UnarchiveCipherResult.Success, result)
|
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
|
@Test
|
||||||
fun `hardDeleteCipher with no active user should return DeleteCipherResult Error`() = runTest {
|
fun `hardDeleteCipher with no active user should return DeleteCipherResult Error`() = runTest {
|
||||||
fakeAuthDiskSource.userState = null
|
fakeAuthDiskSource.userState = null
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ internal interface CiphersApi {
|
|||||||
@PUT("ciphers/{cipherId}/archive")
|
@PUT("ciphers/{cipherId}/archive")
|
||||||
suspend fun archiveCipher(
|
suspend fun archiveCipher(
|
||||||
@Path("cipherId") cipherId: String,
|
@Path("cipherId") cipherId: String,
|
||||||
): NetworkResult<Unit>
|
): NetworkResult<SyncResponseJson.Cipher>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unarchive a cipher.
|
* Unarchive a cipher.
|
||||||
@@ -40,7 +40,7 @@ internal interface CiphersApi {
|
|||||||
@PUT("ciphers/{cipherId}/unarchive")
|
@PUT("ciphers/{cipherId}/unarchive")
|
||||||
suspend fun unarchiveCipher(
|
suspend fun unarchiveCipher(
|
||||||
@Path("cipherId") cipherId: String,
|
@Path("cipherId") cipherId: String,
|
||||||
): NetworkResult<Unit>
|
): NetworkResult<SyncResponseJson.Cipher>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a cipher.
|
* Create a cipher.
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.bitwarden.network.service
|
package com.bitwarden.network.service
|
||||||
|
|
||||||
|
import com.bitwarden.network.model.ArchiveCipherResponseJson
|
||||||
import com.bitwarden.network.model.AttachmentInfo
|
import com.bitwarden.network.model.AttachmentInfo
|
||||||
import com.bitwarden.network.model.AttachmentJsonRequest
|
import com.bitwarden.network.model.AttachmentJsonRequest
|
||||||
import com.bitwarden.network.model.AttachmentJsonResponse
|
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.ImportCiphersResponseJson
|
||||||
import com.bitwarden.network.model.ShareCipherJsonRequest
|
import com.bitwarden.network.model.ShareCipherJsonRequest
|
||||||
import com.bitwarden.network.model.SyncResponseJson
|
import com.bitwarden.network.model.SyncResponseJson
|
||||||
|
import com.bitwarden.network.model.UnarchiveCipherResponseJson
|
||||||
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
|
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
|
||||||
import com.bitwarden.network.model.UpdateCipherResponseJson
|
import com.bitwarden.network.model.UpdateCipherResponseJson
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -24,12 +26,12 @@ interface CiphersService {
|
|||||||
/**
|
/**
|
||||||
* Attempt to archive a cipher.
|
* Attempt to archive a cipher.
|
||||||
*/
|
*/
|
||||||
suspend fun archiveCipher(cipherId: String): Result<Unit>
|
suspend fun archiveCipher(cipherId: String): Result<ArchiveCipherResponseJson>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to unarchive a cipher.
|
* Attempt to unarchive a cipher.
|
||||||
*/
|
*/
|
||||||
suspend fun unarchiveCipher(cipherId: String): Result<Unit>
|
suspend fun unarchiveCipher(cipherId: String): Result<UnarchiveCipherResponseJson>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to create a cipher.
|
* Attempt to create a cipher.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.bitwarden.network.service
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.bitwarden.network.api.AzureApi
|
import com.bitwarden.network.api.AzureApi
|
||||||
import com.bitwarden.network.api.CiphersApi
|
import com.bitwarden.network.api.CiphersApi
|
||||||
|
import com.bitwarden.network.model.ArchiveCipherResponseJson
|
||||||
import com.bitwarden.network.model.AttachmentInfo
|
import com.bitwarden.network.model.AttachmentInfo
|
||||||
import com.bitwarden.network.model.AttachmentJsonRequest
|
import com.bitwarden.network.model.AttachmentJsonRequest
|
||||||
import com.bitwarden.network.model.AttachmentJsonResponse
|
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.ImportCiphersResponseJson
|
||||||
import com.bitwarden.network.model.ShareCipherJsonRequest
|
import com.bitwarden.network.model.ShareCipherJsonRequest
|
||||||
import com.bitwarden.network.model.SyncResponseJson
|
import com.bitwarden.network.model.SyncResponseJson
|
||||||
|
import com.bitwarden.network.model.UnarchiveCipherResponseJson
|
||||||
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
|
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
|
||||||
import com.bitwarden.network.model.UpdateCipherResponseJson
|
import com.bitwarden.network.model.UpdateCipherResponseJson
|
||||||
import com.bitwarden.network.model.toBitwardenError
|
import com.bitwarden.network.model.toBitwardenError
|
||||||
@@ -40,15 +42,37 @@ internal class CiphersServiceImpl(
|
|||||||
) : CiphersService {
|
) : CiphersService {
|
||||||
override suspend fun archiveCipher(
|
override suspend fun archiveCipher(
|
||||||
cipherId: String,
|
cipherId: String,
|
||||||
): Result<Unit> = ciphersApi
|
): Result<ArchiveCipherResponseJson> =
|
||||||
.archiveCipher(cipherId = cipherId)
|
ciphersApi
|
||||||
.toResult()
|
.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(
|
override suspend fun unarchiveCipher(
|
||||||
cipherId: String,
|
cipherId: String,
|
||||||
): Result<Unit> = ciphersApi
|
): Result<UnarchiveCipherResponseJson> =
|
||||||
.unarchiveCipher(cipherId = cipherId)
|
ciphersApi
|
||||||
.toResult()
|
.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(
|
override suspend fun createCipher(
|
||||||
body: CipherJsonRequest,
|
body: CipherJsonRequest,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.net.Uri
|
|||||||
import com.bitwarden.network.api.AzureApi
|
import com.bitwarden.network.api.AzureApi
|
||||||
import com.bitwarden.network.api.CiphersApi
|
import com.bitwarden.network.api.CiphersApi
|
||||||
import com.bitwarden.network.base.BaseServiceTest
|
import com.bitwarden.network.base.BaseServiceTest
|
||||||
|
import com.bitwarden.network.model.ArchiveCipherResponseJson
|
||||||
import com.bitwarden.network.model.AttachmentJsonResponse
|
import com.bitwarden.network.model.AttachmentJsonResponse
|
||||||
import com.bitwarden.network.model.BulkShareCiphersJsonRequest
|
import com.bitwarden.network.model.BulkShareCiphersJsonRequest
|
||||||
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
|
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.ImportCiphersJsonRequest
|
||||||
import com.bitwarden.network.model.ImportCiphersResponseJson
|
import com.bitwarden.network.model.ImportCiphersResponseJson
|
||||||
import com.bitwarden.network.model.ShareCipherJsonRequest
|
import com.bitwarden.network.model.ShareCipherJsonRequest
|
||||||
|
import com.bitwarden.network.model.UnarchiveCipherResponseJson
|
||||||
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
|
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
|
||||||
import com.bitwarden.network.model.UpdateCipherResponseJson
|
import com.bitwarden.network.model.UpdateCipherResponseJson
|
||||||
import com.bitwarden.network.model.createMockAttachment
|
import com.bitwarden.network.model.createMockAttachment
|
||||||
@@ -65,20 +67,71 @@ class CiphersServiceTest : BaseServiceTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `archiveCipher should execute the archiveCipher API`() = runTest {
|
fun `archiveCipher with success response should return a Success with the correct cipher`() =
|
||||||
server.enqueue(MockResponse().setResponseCode(200))
|
runTest {
|
||||||
val cipherId = "cipherId"
|
server.enqueue(
|
||||||
val result = ciphersService.archiveCipher(cipherId = cipherId)
|
MockResponse().setBody(CREATE_RESTORE_UPDATE_CIPHER_SUCCESS_JSON),
|
||||||
assertEquals(Unit, result.getOrThrow())
|
)
|
||||||
}
|
val result = ciphersService.archiveCipher(cipherId = "cipherId")
|
||||||
|
assertEquals(
|
||||||
|
ArchiveCipherResponseJson.Success(
|
||||||
|
cipher = createMockCipher(number = 1),
|
||||||
|
),
|
||||||
|
result.getOrThrow(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `unarchiveCipher should execute the unarchiveCipher API`() = runTest {
|
fun `archiveCipher with an invalid response should return an Invalid with the correct data`() =
|
||||||
server.enqueue(MockResponse().setResponseCode(200))
|
runTest {
|
||||||
val cipherId = "cipherId"
|
server.enqueue(
|
||||||
val result = ciphersService.unarchiveCipher(cipherId = cipherId)
|
MockResponse()
|
||||||
assertEquals(Unit, result.getOrThrow())
|
.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
|
@Test
|
||||||
fun `createCipher should return the correct response`() = runTest {
|
fun `createCipher should return the correct response`() = runTest {
|
||||||
|
|||||||
Reference in New Issue
Block a user