[PM-20192] Migrate CiphersService to network module (#5052)

This commit is contained in:
Patrick Honkonen
2025-04-16 11:08:17 -04:00
committed by GitHub
parent 83de8b888d
commit 2d416eade5
14 changed files with 53 additions and 69 deletions

View File

@@ -0,0 +1,14 @@
package com.bitwarden.network.model
/**
* Information about a cipher attachment to share.
*
* @property id The attachment's ID.
* @property key The attachment's encrypted key value.
* @property fileName The attachment's encrypted file name.
*/
data class AttachmentInfo(
val id: String,
val key: String,
val fileName: String?,
)

View File

@@ -0,0 +1,41 @@
package com.bitwarden.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* The response body for importing ciphers.
*/
@Serializable
sealed class ImportCiphersResponseJson {
/**
* Models a successful json response.
*/
@Serializable
object Success : ImportCiphersResponseJson()
/**
* Represents the json body of an invalid request.
*
* @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")
private val invalidMessage: String? = null,
@SerialName("Message")
private val errorMessage: String? = null,
@SerialName("validationErrors")
val validationErrors: Map<String, List<String>>?,
) : ImportCiphersResponseJson() {
/**
* A generic error message.
*/
val message: String? get() = invalidMessage ?: errorMessage
}
}

View File

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

View File

@@ -0,0 +1,128 @@
package com.bitwarden.network.service
import com.bitwarden.network.model.AttachmentInfo
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.ImportCiphersJsonRequest
import com.bitwarden.network.model.ImportCiphersResponseJson
import com.bitwarden.network.model.ShareCipherJsonRequest
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
import com.bitwarden.network.model.UpdateCipherResponseJson
import java.io.File
/**
* Provides an API for querying ciphers endpoints.
*/
@Suppress("TooManyFunctions")
interface CiphersService {
/**
* Attempt to create a cipher.
*/
suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher>
/**
* Attempt to create a cipher that belongs to an organization.
*/
suspend fun createCipherInOrganization(
body: CreateCipherInOrganizationJsonRequest,
): Result<SyncResponseJson.Cipher>
/**
* Attempt to upload an attachment file.
*/
suspend fun uploadAttachment(
attachment: AttachmentJsonResponse.Success,
encryptedFile: File,
): Result<SyncResponseJson.Cipher>
/**
* Attempt to create an attachment.
*/
suspend fun createAttachment(
cipherId: String,
body: AttachmentJsonRequest,
): Result<AttachmentJsonResponse>
/**
* Attempt to update a cipher.
*/
suspend fun updateCipher(
cipherId: String,
body: CipherJsonRequest,
): Result<UpdateCipherResponseJson>
/**
* Attempt to share a cipher.
*/
suspend fun shareCipher(
cipherId: String,
body: ShareCipherJsonRequest,
): Result<SyncResponseJson.Cipher>
/**
* Attempt to share an attachment.
*/
suspend fun shareAttachment(
cipherId: String,
attachment: AttachmentInfo,
organizationId: String,
encryptedFile: File,
): Result<Unit>
/**
* Attempt to update a cipher's collections.
*/
suspend fun updateCipherCollections(
cipherId: String,
body: UpdateCipherCollectionsJsonRequest,
): Result<Unit>
/**
* Attempt to hard delete a cipher.
*/
suspend fun hardDeleteCipher(cipherId: String): Result<Unit>
/**
* Attempt to soft delete a cipher.
*/
suspend fun softDeleteCipher(cipherId: String): Result<Unit>
/**
* Attempt to delete an attachment from a cipher.
*/
suspend fun deleteCipherAttachment(
cipherId: String,
attachmentId: String,
): Result<Unit>
/**
* Attempt to restore a cipher.
*/
suspend fun restoreCipher(cipherId: String): Result<SyncResponseJson.Cipher>
/**
* Attempt to retrieve a cipher.
*/
suspend fun getCipher(cipherId: String): Result<SyncResponseJson.Cipher>
/**
* Attempt to retrieve a cipher's attachment data.
*/
suspend fun getCipherAttachment(
cipherId: String,
attachmentId: String,
): Result<SyncResponseJson.Cipher.Attachment>
/**
* Returns a boolean indicating if the active user has unassigned ciphers.
*/
suspend fun hasUnassignedCiphers(): Result<Boolean>
/**
* Attempt to import ciphers.
*/
suspend fun importCiphers(request: ImportCiphersJsonRequest): Result<ImportCiphersResponseJson>
}

View File

@@ -0,0 +1,258 @@
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.AttachmentInfo
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.FileUploadType
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.UpdateCipherCollectionsJsonRequest
import com.bitwarden.network.model.UpdateCipherResponseJson
import com.bitwarden.network.model.toBitwardenError
import com.bitwarden.network.util.NetworkErrorCode
import com.bitwarden.network.util.parseErrorBodyOrNull
import com.bitwarden.network.util.toResult
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import java.time.Clock
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
@Suppress("TooManyFunctions")
class CiphersServiceImpl(
private val azureApi: AzureApi,
private val ciphersApi: CiphersApi,
private val json: Json,
private val clock: Clock,
) : CiphersService {
override suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher> =
ciphersApi
.createCipher(body = body)
.toResult()
override suspend fun createCipherInOrganization(
body: CreateCipherInOrganizationJsonRequest,
): Result<SyncResponseJson.Cipher> = ciphersApi
.createCipherInOrganization(body = body)
.toResult()
override suspend fun createAttachment(
cipherId: String,
body: AttachmentJsonRequest,
): Result<AttachmentJsonResponse> =
ciphersApi
.createAttachment(
cipherId = cipherId,
body = body,
)
.toResult()
.recoverCatching { throwable ->
throwable.toBitwardenError()
.parseErrorBodyOrNull<AttachmentJsonResponse.Invalid>(
code = NetworkErrorCode.BAD_REQUEST,
json = json,
)
?: throw throwable
}
override suspend fun uploadAttachment(
attachment: AttachmentJsonResponse.Success,
encryptedFile: File,
): Result<SyncResponseJson.Cipher> {
val cipher = attachment.cipherResponse
return when (attachment.fileUploadType) {
FileUploadType.DIRECT -> {
ciphersApi.uploadAttachment(
cipherId = requireNotNull(cipher.id),
attachmentId = attachment.attachmentId,
body = this
.createMultipartBodyBuilder(
encryptedFile = encryptedFile,
filename = cipher
.attachments
?.find { it.id == attachment.attachmentId }
?.fileName,
)
.build(),
)
}
FileUploadType.AZURE -> {
azureApi.uploadAzureBlob(
url = attachment.url,
date = DateTimeFormatter
.RFC_1123_DATE_TIME
.format(ZonedDateTime.ofInstant(clock.instant(), ZoneOffset.UTC)),
version = attachment.url.toUri().getQueryParameter("sv"),
body = encryptedFile.asRequestBody(),
)
}
}
.toResult()
.map { cipher }
}
override suspend fun updateCipher(
cipherId: String,
body: CipherJsonRequest,
): Result<UpdateCipherResponseJson> =
ciphersApi
.updateCipher(
cipherId = cipherId,
body = body,
)
.toResult()
.map { UpdateCipherResponseJson.Success(cipher = it) }
.recoverCatching { throwable ->
throwable
.toBitwardenError()
.parseErrorBodyOrNull<UpdateCipherResponseJson.Invalid>(
code = NetworkErrorCode.BAD_REQUEST,
json = json,
)
?: throw throwable
}
override suspend fun shareAttachment(
cipherId: String,
attachment: AttachmentInfo,
organizationId: String,
encryptedFile: File,
): Result<Unit> {
return ciphersApi
.shareAttachment(
cipherId = cipherId,
attachmentId = attachment.id,
organizationId = organizationId,
body = this
.createMultipartBodyBuilder(
encryptedFile = encryptedFile,
filename = attachment.fileName,
)
.addPart(
part = MultipartBody.Part.createFormData(
name = "key",
value = attachment.key,
),
)
.build(),
)
.toResult()
}
override suspend fun shareCipher(
cipherId: String,
body: ShareCipherJsonRequest,
): Result<SyncResponseJson.Cipher> =
ciphersApi
.shareCipher(
cipherId = cipherId,
body = body,
)
.toResult()
override suspend fun updateCipherCollections(
cipherId: String,
body: UpdateCipherCollectionsJsonRequest,
): Result<Unit> =
ciphersApi
.updateCipherCollections(
cipherId = cipherId,
body = body,
)
.toResult()
override suspend fun hardDeleteCipher(cipherId: String): Result<Unit> =
ciphersApi
.hardDeleteCipher(cipherId = cipherId)
.toResult()
override suspend fun softDeleteCipher(cipherId: String): Result<Unit> =
ciphersApi
.softDeleteCipher(cipherId = cipherId)
.toResult()
override suspend fun deleteCipherAttachment(
cipherId: String,
attachmentId: String,
): Result<Unit> =
ciphersApi
.deleteCipherAttachment(
cipherId = cipherId,
attachmentId = attachmentId,
)
.toResult()
override suspend fun restoreCipher(cipherId: String): Result<SyncResponseJson.Cipher> =
ciphersApi
.restoreCipher(cipherId = cipherId)
.toResult()
override suspend fun getCipher(
cipherId: String,
): Result<SyncResponseJson.Cipher> =
ciphersApi
.getCipher(cipherId = cipherId)
.toResult()
override suspend fun getCipherAttachment(
cipherId: String,
attachmentId: String,
): Result<SyncResponseJson.Cipher.Attachment> =
ciphersApi
.getCipherAttachment(
cipherId = cipherId,
attachmentId = attachmentId,
)
.toResult()
override suspend fun hasUnassignedCiphers(): Result<Boolean> =
ciphersApi
.hasUnassignedCiphers()
.toResult()
override suspend fun importCiphers(
request: ImportCiphersJsonRequest,
): Result<ImportCiphersResponseJson> =
ciphersApi
.importCiphers(body = request)
.toResult()
.map { ImportCiphersResponseJson.Success }
.recoverCatching { throwable ->
throwable
.toBitwardenError()
.parseErrorBodyOrNull<ImportCiphersResponseJson.Invalid>(
code = NetworkErrorCode.BAD_REQUEST,
json = json,
)
?: throw throwable
}
private fun createMultipartBodyBuilder(
encryptedFile: File,
filename: String?,
): MultipartBody.Builder =
MultipartBody
.Builder(boundary = "--BWMobileFormBoundary${clock.instant().toEpochMilli()}")
.setType(type = MultipartBody.FORM)
.addPart(
part = MultipartBody.Part.createFormData(
body = encryptedFile.asRequestBody(
contentType = "application/octet-stream".toMediaType(),
),
name = "data",
filename = filename,
),
)
}

View File

@@ -0,0 +1,10 @@
package com.bitwarden.network.model
/**
* Creates a mock [AttachmentInfo] with the given [number].
*/
fun createMockAttachmentInfo(number: Int = 1): AttachmentInfo = AttachmentInfo(
id = "mockId-$number",
key = "mockKey-$number",
fileName = "mockFileName-$number",
)

View File

@@ -0,0 +1,610 @@
package com.bitwarden.network.service
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.AttachmentJsonResponse
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
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.UpdateCipherCollectionsJsonRequest
import com.bitwarden.network.model.UpdateCipherResponseJson
import com.bitwarden.network.model.createMockAttachment
import com.bitwarden.network.model.createMockAttachmentInfo
import com.bitwarden.network.model.createMockAttachmentJsonRequest
import com.bitwarden.network.model.createMockAttachmentJsonResponse
import com.bitwarden.network.model.createMockAttachmentResponse
import com.bitwarden.network.model.createMockCipher
import com.bitwarden.network.model.createMockCipherJsonRequest
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import kotlinx.coroutines.test.runTest
import okhttp3.mockwebserver.MockResponse
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.create
import java.io.File
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
class CiphersServiceTest : BaseServiceTest() {
private val clock: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
private val azureApi: AzureApi = retrofit.create()
private val ciphersApi: CiphersApi = retrofit.create()
private val ciphersService: CiphersService = CiphersServiceImpl(
azureApi = azureApi,
ciphersApi = ciphersApi,
json = json,
clock = clock,
)
@BeforeEach
fun setup() {
mockkStatic(Uri::class)
}
@AfterEach
fun tearDown() {
unmockkStatic(Uri::class)
}
@Test
fun `createCipher should return the correct response`() = runTest {
server.enqueue(MockResponse().setBody(CREATE_RESTORE_UPDATE_CIPHER_SUCCESS_JSON))
val result = ciphersService.createCipher(
body = createMockCipherJsonRequest(number = 1),
)
assertEquals(
createMockCipher(number = 1),
result.getOrThrow(),
)
}
@Test
fun `createCipherInOrganization should return the correct response`() = runTest {
server.enqueue(MockResponse().setBody(CREATE_RESTORE_UPDATE_CIPHER_SUCCESS_JSON))
val result = ciphersService.createCipherInOrganization(
body = CreateCipherInOrganizationJsonRequest(
cipher = createMockCipherJsonRequest(number = 1),
collectionIds = listOf("12345"),
),
)
assertEquals(
createMockCipher(number = 1),
result.getOrThrow(),
)
}
@Test
fun `createAttachment should return the correct response`() = runTest {
server.enqueue(MockResponse().setBody(CREATE_ATTACHMENT_SUCCESS_JSON))
val result = ciphersService.createAttachment(
cipherId = "mockId-1",
body = createMockAttachmentJsonRequest(number = 1),
)
assertEquals(
createMockAttachmentJsonResponse(number = 1),
result.getOrThrow(),
)
}
@Test
fun `createAttachment with invalid response should return an Invalid with the correct data`() =
runTest {
server.enqueue(
MockResponse().setResponseCode(400).setBody(CREATE_ATTACHMENT_INVALID_JSON),
)
val result = ciphersService.createAttachment(
cipherId = "mockId-1",
body = createMockAttachmentJsonRequest(number = 1),
)
assertEquals(
AttachmentJsonResponse.Invalid(
message = "You do not have permission.",
validationErrors = null,
),
result.getOrThrow(),
)
}
@Test
fun `uploadAttachment with Azure uploadFile success should return cipher`() = runTest {
setupMockUri(url = "mockUrl-1", queryParams = mapOf("sv" to "2024-04-03"))
val mockCipher = createMockCipher(number = 1)
val encryptedFile = File.createTempFile("mockFile", "temp")
server.enqueue(MockResponse().setResponseCode(201))
val result = ciphersService.uploadAttachment(
attachment = createMockAttachmentResponse(
number = 1,
fileUploadType = FileUploadType.AZURE,
),
encryptedFile = encryptedFile,
)
assertEquals(mockCipher, result.getOrThrow())
}
@Test
fun `uploadAttachment with Direct uploadFile success should return cipher`() = runTest {
val mockCipher = createMockCipher(number = 1)
val encryptedFile = File.createTempFile("mockFile", "temp")
server.enqueue(MockResponse().setResponseCode(201))
val result = ciphersService.uploadAttachment(
attachment = createMockAttachmentResponse(
number = 1,
fileUploadType = FileUploadType.DIRECT,
),
encryptedFile = encryptedFile,
)
assertEquals(mockCipher, result.getOrThrow())
}
@Test
fun `updateCipher 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.updateCipher(
cipherId = "cipher-id-1",
body = createMockCipherJsonRequest(number = 1),
)
assertEquals(
UpdateCipherResponseJson.Success(
cipher = createMockCipher(number = 1),
),
result.getOrThrow(),
)
}
@Test
fun `updateCipher 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.updateCipher(
cipherId = "cipher-id-1",
body = createMockCipherJsonRequest(number = 1),
)
assertEquals(
UpdateCipherResponseJson.Invalid(
message = "You do not have permission to edit this.",
validationErrors = null,
),
result.getOrThrow(),
)
}
@Test
fun `hardDeleteCipher should execute the hardDeleteCipher API`() = runTest {
server.enqueue(MockResponse().setResponseCode(200))
val cipherId = "cipherId"
val result = ciphersService.hardDeleteCipher(cipherId = cipherId)
assertEquals(Unit, result.getOrThrow())
}
@Test
fun `softDeleteCipher should execute the softDeleteCipher API`() = runTest {
server.enqueue(MockResponse().setResponseCode(200))
val cipherId = "cipherId"
val result = ciphersService.softDeleteCipher(cipherId = cipherId)
assertEquals(Unit, result.getOrThrow())
}
@Test
fun `deleteCipherAttachment should execute the deleteCipherAttachment API`() = runTest {
server.enqueue(MockResponse().setResponseCode(200))
val cipherId = "cipherId"
val attachmentId = "attachmentId"
val result = ciphersService.deleteCipherAttachment(
cipherId = cipherId,
attachmentId = attachmentId,
)
assertEquals(Unit, result.getOrThrow())
}
@Test
fun `shareAttachment should execute the share attachment API`() = runTest {
server.enqueue(MockResponse().setResponseCode(200))
val cipherId = "cipherId"
val organizationId = "organizationId"
val attachment = createMockAttachmentInfo(number = 1)
val encryptedFile = File.createTempFile("mockFile", "temp")
val result = ciphersService.shareAttachment(
cipherId = cipherId,
attachment = attachment,
organizationId = organizationId,
encryptedFile = encryptedFile,
)
assertEquals(Unit, result.getOrThrow())
}
@Test
fun `shareCipher should execute the share cipher API`() = runTest {
server.enqueue(
MockResponse()
.setResponseCode(200)
.setBody(CREATE_RESTORE_UPDATE_CIPHER_SUCCESS_JSON),
)
val result = ciphersService.shareCipher(
cipherId = "mockId-1",
body = ShareCipherJsonRequest(
cipher = createMockCipherJsonRequest(number = 1),
collectionIds = listOf("mockId-1"),
),
)
assertEquals(
createMockCipher(number = 1),
result.getOrThrow(),
)
}
@Test
fun `updateCipherCollections should execute the updateCipherCollections API`() = runTest {
server.enqueue(MockResponse().setResponseCode(200))
val result = ciphersService.updateCipherCollections(
cipherId = "mockId-1",
body = UpdateCipherCollectionsJsonRequest(
collectionIds = listOf("mockId-1"),
),
)
assertEquals(
Unit,
result.getOrThrow(),
)
}
@Test
fun `restoreCipher should execute the restoreCipher API`() = runTest {
server.enqueue(MockResponse().setBody(CREATE_RESTORE_UPDATE_CIPHER_SUCCESS_JSON))
val cipherId = "cipherId"
val result = ciphersService.restoreCipher(cipherId = cipherId)
assertEquals(createMockCipher(number = 1), result.getOrThrow())
}
@Test
fun `getCipher should return the correct response`() = runTest {
server.enqueue(MockResponse().setBody(CREATE_RESTORE_UPDATE_CIPHER_SUCCESS_JSON))
val result = ciphersService.getCipher(cipherId = "mockId-1")
assertEquals(
createMockCipher(number = 1),
result.getOrThrow(),
)
}
@Test
fun `getCipherAttachment should return the correct response`() = runTest {
server.enqueue(MockResponse().setBody(GET_CIPHER_ATTACHMENT_SUCCESS_JSON))
val result = ciphersService.getCipherAttachment(
cipherId = "mockId-1",
attachmentId = "mockId-1",
)
assertEquals(
createMockAttachment(number = 1),
result.getOrThrow(),
)
}
@Test
fun `hasUnassignedCiphers should return the correct response`() = runTest {
server.enqueue(MockResponse().setBody("true"))
val result = ciphersService.hasUnassignedCiphers()
assertTrue(result.getOrThrow())
}
@Test
fun `importCiphers should return the correct response`() = runTest {
server.enqueue(MockResponse().setResponseCode(200))
val result = ciphersService.importCiphers(
request = ImportCiphersJsonRequest(
ciphers = listOf(createMockCipherJsonRequest(number = 1)),
folders = emptyList(),
folderRelationships = emptyMap(),
),
)
assertEquals(ImportCiphersResponseJson.Success, result.getOrThrow())
}
@Test
fun `importCiphers should return an error when the response is an error`() = runTest {
server.enqueue(MockResponse().setResponseCode(400))
val result = ciphersService.importCiphers(
request = ImportCiphersJsonRequest(
ciphers = listOf(createMockCipherJsonRequest(number = 1)),
folders = emptyList(),
folderRelationships = emptyMap(),
),
)
assertTrue(result.isFailure)
}
}
private fun setupMockUri(
url: String,
queryParams: Map<String, String>,
): Uri {
val mockUri = mockk<Uri> {
queryParams.forEach {
every { getQueryParameter(it.key) } returns it.value
}
}
every { Uri.parse(url) } returns mockUri
return mockUri
}
private const val CREATE_ATTACHMENT_SUCCESS_JSON = """
{
"attachmentId":"mockAttachmentId-1",
"url":"mockUrl-1",
"fileUploadType":1,
"cipherResponse":{
"notes": "mockNotes-1",
"attachments": [
{
"fileName": "mockFileName-1",
"size": 1,
"sizeName": "mockSizeName-1",
"id": "mockId-1",
"url": "mockUrl-1",
"key": "mockKey-1"
}
],
"organizationUseTotp": false,
"reprompt": 0,
"edit": false,
"passwordHistory": [
{
"password": "mockPassword-1",
"lastUsedDate": "2023-10-27T12:00:00.00Z"
}
],
"revisionDate": "2023-10-27T12:00:00.00Z",
"type": 1,
"login": {
"uris": [
{
"match": 1,
"uri": "mockUri-1",
"uriChecksum": "mockUriChecksum-1"
}
],
"totp": "mockTotp-1",
"password": "mockPassword-1",
"passwordRevisionDate": "2023-10-27T12:00:00.00Z",
"autofillOnPageLoad": false,
"uri": "mockUri-1",
"username": "mockUsername-1",
"fido2Credentials": [
{
"credentialId": "mockCredentialId-1",
"keyType": "mockKeyType-1",
"keyAlgorithm": "mockKeyAlgorithm-1",
"keyCurve": "mockKeyCurve-1",
"keyValue": "mockKeyValue-1",
"rpId": "mockRpId-1",
"rpName": "mockRpName-1",
"userHandle": "mockUserHandle-1",
"userName": "mockUserName-1",
"userDisplayName": "mockUserDisplayName-1",
"counter": "mockCounter-1",
"discoverable": "mockDiscoverable-1",
"creationDate": "2023-10-27T12:00:00.00Z"
}
]
},
"creationDate": "2023-10-27T12:00:00.00Z",
"secureNote": {
"type": 0
},
"folderId": "mockFolderId-1",
"organizationId": "mockOrganizationId-1",
"deletedDate": "2023-10-27T12:00:00.00Z",
"identity": {
"passportNumber": "mockPassportNumber-1",
"lastName": "mockLastName-1",
"address3": "mockAddress3-1",
"address2": "mockAddress2-1",
"city": "mockCity-1",
"country": "mockCountry-1",
"address1": "mockAddress1-1",
"postalCode": "mockPostalCode-1",
"title": "mockTitle-1",
"ssn": "mockSsn-1",
"firstName": "mockFirstName-1",
"phone": "mockPhone-1",
"middleName": "mockMiddleName-1",
"company": "mockCompany-1",
"licenseNumber": "mockLicenseNumber-1",
"state": "mockState-1",
"email": "mockEmail-1",
"username": "mockUsername-1"
},
"collectionIds": [
"mockCollectionId-1"
],
"name": "mockName-1",
"id": "mockId-1"
"fields": [
{
"linkedId": 100,
"name": "mockName-1",
"type": 1,
"value": "mockValue-1"
}
],
"viewPassword": false,
"favorite": false,
"card": {
"number": "mockNumber-1",
"expMonth": "mockExpMonth-1",
"code": "mockCode-1",
"expYear": "mockExpirationYear-1",
"cardholderName": "mockCardholderName-1",
"brand": "mockBrand-1"
},
"key": "mockKey-1",
"sshKey": {
"publicKey": "mockPublicKey-1",
"privateKey": "mockPrivateKey-1",
"keyFingerprint": "mockKeyFingerprint-1"
}
}
}
"""
private const val CREATE_ATTACHMENT_INVALID_JSON = """
{
"message": "You do not have permission.",
"validationErrors": null
}
"""
private const val CREATE_RESTORE_UPDATE_CIPHER_SUCCESS_JSON = """
{
"notes": "mockNotes-1",
"attachments": [
{
"fileName": "mockFileName-1",
"size": 1,
"sizeName": "mockSizeName-1",
"id": "mockId-1",
"url": "mockUrl-1",
"key": "mockKey-1"
}
],
"organizationUseTotp": false,
"reprompt": 0,
"edit": false,
"passwordHistory": [
{
"password": "mockPassword-1",
"lastUsedDate": "2023-10-27T12:00:00.00Z"
}
],
"revisionDate": "2023-10-27T12:00:00.00Z",
"type": 1,
"login": {
"uris": [
{
"match": 1,
"uri": "mockUri-1",
"uriChecksum": "mockUriChecksum-1"
}
],
"totp": "mockTotp-1",
"password": "mockPassword-1",
"passwordRevisionDate": "2023-10-27T12:00:00.00Z",
"autofillOnPageLoad": false,
"uri": "mockUri-1",
"username": "mockUsername-1",
"fido2Credentials": [
{
"credentialId": "mockCredentialId-1",
"keyType": "mockKeyType-1",
"keyAlgorithm": "mockKeyAlgorithm-1",
"keyCurve": "mockKeyCurve-1",
"keyValue": "mockKeyValue-1",
"rpId": "mockRpId-1",
"rpName": "mockRpName-1",
"userHandle": "mockUserHandle-1",
"userName": "mockUserName-1",
"userDisplayName": "mockUserDisplayName-1",
"counter": "mockCounter-1",
"discoverable": "mockDiscoverable-1",
"creationDate": "2023-10-27T12:00:00.00Z"
}
]
},
"creationDate": "2023-10-27T12:00:00.00Z",
"secureNote": {
"type": 0
},
"folderId": "mockFolderId-1",
"organizationId": "mockOrganizationId-1",
"deletedDate": "2023-10-27T12:00:00.00Z",
"identity": {
"passportNumber": "mockPassportNumber-1",
"lastName": "mockLastName-1",
"address3": "mockAddress3-1",
"address2": "mockAddress2-1",
"city": "mockCity-1",
"country": "mockCountry-1",
"address1": "mockAddress1-1",
"postalCode": "mockPostalCode-1",
"title": "mockTitle-1",
"ssn": "mockSsn-1",
"firstName": "mockFirstName-1",
"phone": "mockPhone-1",
"middleName": "mockMiddleName-1",
"company": "mockCompany-1",
"licenseNumber": "mockLicenseNumber-1",
"state": "mockState-1",
"email": "mockEmail-1",
"username": "mockUsername-1"
},
"collectionIds": [
"mockCollectionId-1"
],
"name": "mockName-1",
"id": "mockId-1"
"fields": [
{
"linkedId": 100,
"name": "mockName-1",
"type": 1,
"value": "mockValue-1"
}
],
"viewPassword": false,
"favorite": false,
"card": {
"number": "mockNumber-1",
"expMonth": "mockExpMonth-1",
"code": "mockCode-1",
"expYear": "mockExpirationYear-1",
"cardholderName": "mockCardholderName-1",
"brand": "mockBrand-1"
},
"key": "mockKey-1",
"sshKey": {
"publicKey": "mockPublicKey-1",
"privateKey": "mockPrivateKey-1",
"keyFingerprint": "mockKeyFingerprint-1"
}
}
"""
private const val UPDATE_CIPHER_INVALID_JSON = """
{
"message": "You do not have permission to edit this.",
"validationErrors": null
}
"""
private const val GET_CIPHER_ATTACHMENT_SUCCESS_JSON = """
{
"fileName": "mockFileName-1",
"size": 1,
"sizeName": "mockSizeName-1",
"id": "mockId-1",
"url": "mockUrl-1",
"key": "mockKey-1"
}
"""