Add underlyng SendsService to make sends API requests. (#511)

This commit is contained in:
David Perez 2024-01-06 16:25:54 -06:00 committed by Álison Fernandes
parent 3200f44611
commit 6ef7be296e
8 changed files with 353 additions and 0 deletions

View file

@ -0,0 +1,36 @@
package com.x8bit.bitwarden.data.vault.datasource.network.api
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
/**
* Defines raw calls under the /send API with authentication applied.
*/
interface SendsApi {
/**
* Create a send.
*/
@POST("sends")
suspend fun createSend(@Body body: SendJsonRequest): Result<SyncResponseJson.Send>
/**
* Updates a send.
*/
@PUT("sends/{sendId}")
suspend fun updateSend(
@Path("sendId") sendId: String,
@Body body: SendJsonRequest,
): Result<SyncResponseJson.Send>
/**
* Deletes a send.
*/
@DELETE("sends/{sendId}")
suspend fun deleteSend(@Path("sendId") sendId: String): Result<Unit>
}

View file

@ -3,6 +3,8 @@ package com.x8bit.bitwarden.data.vault.datasource.network.di
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService
import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersServiceImpl
import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsServiceImpl
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncServiceImpl
import dagger.Module
@ -30,6 +32,16 @@ object VaultNetworkModule {
json = json,
)
@Provides
@Singleton
fun provideSendsService(
retrofits: Retrofits,
json: Json,
): SendsService = SendsServiceImpl(
sendsApi = retrofits.authenticatedApiRetrofit.create(),
json = json,
)
@Provides
@Singleton
fun provideSyncService(

View file

@ -0,0 +1,63 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.ZonedDateTime
/**
* Represents a send request.
*
* @property type The type of send.
* @property name The name of the send (nullable).
* @property notes The notes of the send (nullable).
* @property key The send key.
* @property maxAccessCount The maximum number of people who can access this send (nullable).
* @property expirationDate The date in which the send will expire (nullable).
* @property deletionDate The date in which the send will be deleted.
* @property file The file associated with this send (nullable).
* @property text The text associated with this send (nullable).
* @property password The password protecting this send (nullable).
* @property isDisabled Indicate if this send is disabled.
* @property shouldHideEmail Should the email address of the sender be hidden (nullable).
*/
@Serializable
data class SendJsonRequest(
@SerialName("type")
val type: SendTypeJson,
@SerialName("name")
val name: String?,
@SerialName("notes")
val notes: String?,
@SerialName("key")
val key: String,
@SerialName("maxAccessCount")
val maxAccessCount: Int?,
@SerialName("expirationDate")
@Contextual
val expirationDate: ZonedDateTime?,
@SerialName("deletionDate")
@Contextual
val deletionDate: ZonedDateTime,
@SerialName("file")
val file: SyncResponseJson.Send.File?,
@SerialName("text")
val text: SyncResponseJson.Send.Text?,
@SerialName("password")
val password: String?,
@SerialName("disabled")
val isDisabled: Boolean,
@SerialName("hideEmail")
val shouldHideEmail: Boolean?,
)

View file

@ -0,0 +1,33 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Models the response from the update send request.
*/
sealed class UpdateSendResponseJson {
/**
* The request completed successfully and returned the updated [send].
*/
data class Success(
val send: SyncResponseJson.Send,
) : UpdateSendResponseJson()
/**
* 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>>?,
) : UpdateSendResponseJson()
}

View file

@ -0,0 +1,32 @@
package com.x8bit.bitwarden.data.vault.datasource.network.service
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateSendResponseJson
/**
* Provides an API for querying sends endpoints.
*/
interface SendsService {
/**
* Attempt to create a send.
*/
suspend fun createSend(
body: SendJsonRequest,
): Result<SyncResponseJson.Send>
/**
* Attempt to update a send.
*/
suspend fun updateSend(
sendId: String,
body: SendJsonRequest,
): Result<UpdateSendResponseJson>
/**
* Attempt to delete a cipher.
*/
suspend fun deleteSend(
sendId: String,
): Result<Unit>
}

View file

@ -0,0 +1,43 @@
package com.x8bit.bitwarden.data.vault.datasource.network.service
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
import com.x8bit.bitwarden.data.vault.datasource.network.api.SendsApi
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateSendResponseJson
import kotlinx.serialization.json.Json
/**
* Default implementation of the [SendsService].
*/
class SendsServiceImpl(
private val sendsApi: SendsApi,
private val json: Json,
) : SendsService {
override suspend fun createSend(body: SendJsonRequest): Result<SyncResponseJson.Send> =
sendsApi.createSend(body = body)
override suspend fun updateSend(
sendId: String,
body: SendJsonRequest,
): Result<UpdateSendResponseJson> =
sendsApi
.updateSend(
sendId = sendId,
body = body,
)
.map { UpdateSendResponseJson.Success(send = it) }
.recoverCatching { throwable ->
throwable
.toBitwardenError()
.parseErrorBodyOrNull<UpdateSendResponseJson.Invalid>(
code = 400,
json = json,
)
?: throw throwable
}
override suspend fun deleteSend(sendId: String): Result<Unit> =
sendsApi.deleteSend(sendId = sendId)
}

View file

@ -0,0 +1,25 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import java.time.ZonedDateTime
/**
* Create a mock [SendJsonRequest] with a given [number].
*/
fun createMockSendJsonRequest(
number: Int,
type: SendTypeJson = SendTypeJson.TEXT,
): SendJsonRequest =
SendJsonRequest(
name = "mockName-$number",
notes = "mockNotes-$number",
type = type,
key = "mockKey-$number",
maxAccessCount = 1,
expirationDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"),
deletionDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"),
file = createMockFile(number),
text = createMockText(number),
password = "mockPassword-$number",
isDisabled = false,
shouldHideEmail = false,
)

View file

@ -0,0 +1,109 @@
package com.x8bit.bitwarden.data.vault.datasource.network.service
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
import com.x8bit.bitwarden.data.vault.datasource.network.api.SendsApi
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateSendResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSend
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSendJsonRequest
import kotlinx.coroutines.test.runTest
import okhttp3.mockwebserver.MockResponse
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import retrofit2.create
class SendsServiceTest : BaseServiceTest() {
private val sendsApi: SendsApi = retrofit.create()
private val sendsService: SendsService = SendsServiceImpl(
sendsApi = sendsApi,
json = json,
)
@Test
fun `createSend should return the correct response`() = runTest {
server.enqueue(MockResponse().setBody(CREATE_UPDATE_SEND_SUCCESS_JSON))
val result = sendsService.createSend(
body = createMockSendJsonRequest(number = 1),
)
assertEquals(
createMockSend(number = 1),
result.getOrThrow(),
)
}
@Test
fun `updateSend with success response should return a Success with the correct send`() =
runTest {
server.enqueue(MockResponse().setBody(CREATE_UPDATE_SEND_SUCCESS_JSON))
val result = sendsService.updateSend(
sendId = "send-id-1",
body = createMockSendJsonRequest(number = 1),
)
assertEquals(
UpdateSendResponseJson.Success(
send = createMockSend(number = 1),
),
result.getOrThrow(),
)
}
@Test
fun `updateSend with an invalid response should return an Invalid with the correct data`() =
runTest {
server.enqueue(MockResponse().setResponseCode(400).setBody(UPDATE_SEND_INVALID_JSON))
val result = sendsService.updateSend(
sendId = "send-id-1",
body = createMockSendJsonRequest(number = 1),
)
assertEquals(
UpdateSendResponseJson.Invalid(
message = "You do not have permission to edit this.",
validationErrors = null,
),
result.getOrThrow(),
)
}
@Test
fun `deleteSend should return a Success with the correct data`() = runTest {
server.enqueue(MockResponse().setResponseCode(200))
val result = sendsService.deleteSend(sendId = "send-id-1")
assertEquals(Unit, result.getOrThrow())
}
}
private const val CREATE_UPDATE_SEND_SUCCESS_JSON = """
{
"id": "mockId-1",
"accessId": "mockAccessId-1",
"type": 1,
"name": "mockName-1",
"notes": "mockNotes-1",
"file": {
"id": "mockId-1",
"fileName": "mockFileName-1",
"size": 1,
"sizeName": "mockSizeName-1"
},
"text": {
"text": "mockText-1",
"hidden": false
},
"key": "mockKey-1",
"maxAccessCount": 1,
"accessCount": 1,
"password": "mockPassword-1",
"disabled": false,
"revisionDate": "2023-10-27T12:00:00.00Z",
"expirationDate": "2023-10-27T12:00:00.00Z",
"deletionDate": "2023-10-27T12:00:00.00Z",
"hideEmail": false
}
"""
private const val UPDATE_SEND_INVALID_JSON = """
{
"message": "You do not have permission to edit this.",
"validationErrors": null
}
"""