[PM-21782] Improve create cipher error handling (#5362)

This commit is contained in:
Patrick Honkonen
2025-06-16 10:23:30 -04:00
committed by GitHub
parent 469df4495a
commit 95f146fb3e
9 changed files with 320 additions and 33 deletions

View File

@@ -0,0 +1,32 @@
package com.bitwarden.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Models the response from the create cipher request.
*/
sealed class CreateCipherResponseJson {
/**
* The request completed successfully and returned the created [cipher].
*/
data class Success(val cipher: SyncResponseJson.Cipher) : CreateCipherResponseJson()
/**
* Represents the json body of an invalid create 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")
val message: String?,
@SerialName("validationErrors")
val validationErrors: Map<String, List<String>>?,
) : CreateCipherResponseJson()
}

View File

@@ -5,6 +5,7 @@ import com.bitwarden.network.model.AttachmentJsonRequest
import com.bitwarden.network.model.AttachmentJsonResponse
import com.bitwarden.network.model.CipherJsonRequest
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
import com.bitwarden.network.model.CreateCipherResponseJson
import com.bitwarden.network.model.ImportCiphersJsonRequest
import com.bitwarden.network.model.ImportCiphersResponseJson
import com.bitwarden.network.model.ShareCipherJsonRequest
@@ -21,14 +22,14 @@ interface CiphersService {
/**
* Attempt to create a cipher.
*/
suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher>
suspend fun createCipher(body: CipherJsonRequest): Result<CreateCipherResponseJson>
/**
* Attempt to create a cipher that belongs to an organization.
*/
suspend fun createCipherInOrganization(
body: CreateCipherInOrganizationJsonRequest,
): Result<SyncResponseJson.Cipher>
): Result<CreateCipherResponseJson>
/**
* Attempt to upload an attachment file.

View File

@@ -8,6 +8,7 @@ import com.bitwarden.network.model.AttachmentJsonRequest
import com.bitwarden.network.model.AttachmentJsonResponse
import com.bitwarden.network.model.CipherJsonRequest
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
import com.bitwarden.network.model.CreateCipherResponseJson
import com.bitwarden.network.model.FileUploadType
import com.bitwarden.network.model.ImportCiphersJsonRequest
import com.bitwarden.network.model.ImportCiphersResponseJson
@@ -36,16 +37,38 @@ internal class CiphersServiceImpl(
private val json: Json,
private val clock: Clock,
) : CiphersService {
override suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher> =
override suspend fun createCipher(
body: CipherJsonRequest,
): Result<CreateCipherResponseJson> =
ciphersApi
.createCipher(body = body)
.toResult()
.map { CreateCipherResponseJson.Success(it) }
.recoverCatching { throwable ->
throwable
.toBitwardenError()
.parseErrorBodyOrNull<CreateCipherResponseJson.Invalid>(
code = NetworkErrorCode.BAD_REQUEST,
json = json,
)
?: throw throwable
}
override suspend fun createCipherInOrganization(
body: CreateCipherInOrganizationJsonRequest,
): Result<SyncResponseJson.Cipher> = ciphersApi
): Result<CreateCipherResponseJson> = ciphersApi
.createCipherInOrganization(body = body)
.toResult()
.map { CreateCipherResponseJson.Success(it) }
.recoverCatching { throwable ->
throwable
.toBitwardenError()
.parseErrorBodyOrNull<CreateCipherResponseJson.Invalid>(
code = NetworkErrorCode.BAD_REQUEST,
json = json,
)
?: throw throwable
}
override suspend fun createAttachment(
cipherId: String,

View File

@@ -6,6 +6,7 @@ import com.bitwarden.network.api.CiphersApi
import com.bitwarden.network.base.BaseServiceTest
import com.bitwarden.network.model.AttachmentJsonResponse
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
import com.bitwarden.network.model.CreateCipherResponseJson
import com.bitwarden.network.model.FileUploadType
import com.bitwarden.network.model.ImportCiphersJsonRequest
import com.bitwarden.network.model.ImportCiphersResponseJson
@@ -67,7 +68,22 @@ class CiphersServiceTest : BaseServiceTest() {
body = createMockCipherJsonRequest(number = 1),
)
assertEquals(
createMockCipher(number = 1),
CreateCipherResponseJson.Success(createMockCipher(number = 1)),
result.getOrThrow(),
)
}
@Test
fun `createCipher should return Invalid with correct data`() = runTest {
server.enqueue(MockResponse().setResponseCode(400).setBody(CREATE_CIPHER_INVALID_JSON))
val result = ciphersService.createCipher(
body = createMockCipherJsonRequest(number = 1),
)
assertEquals(
CreateCipherResponseJson.Invalid(
message = "Cipher was not encrypted for the current user. Please try again.",
validationErrors = null,
),
result.getOrThrow(),
)
}
@@ -82,11 +98,30 @@ class CiphersServiceTest : BaseServiceTest() {
),
)
assertEquals(
createMockCipher(number = 1),
CreateCipherResponseJson.Success(createMockCipher(number = 1)),
result.getOrThrow(),
)
}
@Test
fun `createCipherInOrganization should return Invalid with correct data`() =
runTest {
server.enqueue(MockResponse().setResponseCode(400).setBody(CREATE_CIPHER_INVALID_JSON))
val result = ciphersService.createCipherInOrganization(
body = CreateCipherInOrganizationJsonRequest(
cipher = createMockCipherJsonRequest(number = 1),
collectionIds = listOf("12345"),
),
)
assertEquals(
CreateCipherResponseJson.Invalid(
message = "Cipher was not encrypted for the current user. Please try again.",
validationErrors = null,
),
result.getOrThrow(),
)
}
@Test
fun `createAttachment should return the correct response`() = runTest {
server.enqueue(MockResponse().setBody(CREATE_ATTACHMENT_SUCCESS_JSON))
@@ -606,6 +641,12 @@ private const val UPDATE_CIPHER_INVALID_JSON = """
"validationErrors": null
}
"""
private const val CREATE_CIPHER_INVALID_JSON = """
{
"message": "Cipher was not encrypted for the current user. Please try again.",
"validationErrors": null
}
"""
private const val GET_CIPHER_ATTACHMENT_SUCCESS_JSON = """
{