diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/SendsApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/SendsApi.kt new file mode 100644 index 0000000000..ea558cb95a --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/SendsApi.kt @@ -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 + + /** + * Updates a send. + */ + @PUT("sends/{sendId}") + suspend fun updateSend( + @Path("sendId") sendId: String, + @Body body: SendJsonRequest, + ): Result + + /** + * Deletes a send. + */ + @DELETE("sends/{sendId}") + suspend fun deleteSend(@Path("sendId") sendId: String): Result +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/di/VaultNetworkModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/di/VaultNetworkModule.kt index beffb98028..e601747214 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/di/VaultNetworkModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/di/VaultNetworkModule.kt @@ -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( diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendJsonRequest.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendJsonRequest.kt new file mode 100644 index 0000000000..c3ce4da392 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendJsonRequest.kt @@ -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?, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/UpdateSendResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/UpdateSendResponseJson.kt new file mode 100644 index 0000000000..e30bcd8b54 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/UpdateSendResponseJson.kt @@ -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>?, + ) : UpdateSendResponseJson() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsService.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsService.kt new file mode 100644 index 0000000000..906c74d047 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsService.kt @@ -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 + + /** + * Attempt to update a send. + */ + suspend fun updateSend( + sendId: String, + body: SendJsonRequest, + ): Result + + /** + * Attempt to delete a cipher. + */ + suspend fun deleteSend( + sendId: String, + ): Result +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceImpl.kt new file mode 100644 index 0000000000..e2601ddf97 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceImpl.kt @@ -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 = + sendsApi.createSend(body = body) + + override suspend fun updateSend( + sendId: String, + body: SendJsonRequest, + ): Result = + sendsApi + .updateSend( + sendId = sendId, + body = body, + ) + .map { UpdateSendResponseJson.Success(send = it) } + .recoverCatching { throwable -> + throwable + .toBitwardenError() + .parseErrorBodyOrNull( + code = 400, + json = json, + ) + ?: throw throwable + } + + override suspend fun deleteSend(sendId: String): Result = + sendsApi.deleteSend(sendId = sendId) +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendJsonRequestUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendJsonRequestUtil.kt new file mode 100644 index 0000000000..bc741a50da --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SendJsonRequestUtil.kt @@ -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, + ) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceTest.kt new file mode 100644 index 0000000000..e8efe9079b --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceTest.kt @@ -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 +} +"""