mirror of
https://github.com/bitwarden/android.git
synced 2026-04-29 04:18:52 -05:00
[PM-20192] Migrate CiphersService to network module (#5052)
This commit is contained in:
@@ -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?,
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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"
|
||||
}
|
||||
"""
|
||||
Reference in New Issue
Block a user