From e38dea7ab7e330895c0392b27d507ba281b2705e Mon Sep 17 00:00:00 2001 From: Oleg Semenenko <146032743+oleg-livefront@users.noreply.github.com> Date: Sat, 27 Jan 2024 11:56:22 -0600 Subject: [PATCH] Adding the Folder Api and Service (#809) --- .../datasource/network/api/FoldersApi.kt | 36 +++++++ .../network/di/VaultNetworkModule.kt | 12 +++ .../network/model/FolderJsonRequest.kt | 15 +++ .../network/model/UpdateFolderResponseJson.kt | 33 +++++++ .../network/service/FolderService.kt | 28 ++++++ .../network/service/FolderServiceImpl.kt | 40 ++++++++ .../network/service/FoldersServiceTest.kt | 99 +++++++++++++++++++ 7 files changed, 263 insertions(+) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/FoldersApi.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/FolderJsonRequest.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/UpdateFolderResponseJson.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/FolderService.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/FolderServiceImpl.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/FoldersServiceTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/FoldersApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/FoldersApi.kt new file mode 100644 index 000000000..a46204737 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/FoldersApi.kt @@ -0,0 +1,36 @@ +package com.x8bit.bitwarden.data.vault.datasource.network.api + +import com.x8bit.bitwarden.data.vault.datasource.network.model.FolderJsonRequest +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 /folders API with authentication applied. + */ +interface FoldersApi { + + /** + * Create a folder. + */ + @POST("folders") + suspend fun createFolder(@Body body: FolderJsonRequest): Result + + /** + * Updates a folder. + */ + @PUT("folders/{folderId}") + suspend fun updateFolder( + @Path("folderId") folderId: String, + @Body body: FolderJsonRequest, + ): Result + + /** + * Deletes a folder. + */ + @DELETE("folders/{folderId}") + suspend fun deleteFolder(@Path("folderId") folderId: 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 8b4e3b789..395348eda 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.FolderService +import com.x8bit.bitwarden.data.vault.datasource.network.service.FolderServiceImpl 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 @@ -41,6 +43,16 @@ object VaultNetworkModule { clock = clock, ) + @Provides + @Singleton + fun providesFolderService( + retrofits: Retrofits, + json: Json, + ): FolderService = FolderServiceImpl( + foldersApi = retrofits.authenticatedApiRetrofit.create(), + json = json, + ) + @Provides @Singleton fun provideSendsService( diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/FolderJsonRequest.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/FolderJsonRequest.kt new file mode 100644 index 000000000..37a02fcb9 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/FolderJsonRequest.kt @@ -0,0 +1,15 @@ +package com.x8bit.bitwarden.data.vault.datasource.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Represents a folder request. + * + * @property name The name of the folder. + */ +@Serializable +data class FolderJsonRequest( + @SerialName("name") + val name: String?, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/UpdateFolderResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/UpdateFolderResponseJson.kt new file mode 100644 index 000000000..0833efef0 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/UpdateFolderResponseJson.kt @@ -0,0 +1,33 @@ +package com.x8bit.bitwarden.data.vault.datasource.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Models the responses from the folder update request. + */ +sealed class UpdateFolderResponseJson { + /** + * The request completed successfully and returned the updated [folder]. + */ + data class Success( + val folder: SyncResponseJson.Folder, + ) : UpdateFolderResponseJson() + + /** + * 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>?, + ) : UpdateFolderResponseJson() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/FolderService.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/FolderService.kt new file mode 100644 index 000000000..9bbb4fefa --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/FolderService.kt @@ -0,0 +1,28 @@ +package com.x8bit.bitwarden.data.vault.datasource.network.service + +import com.x8bit.bitwarden.data.vault.datasource.network.model.FolderJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateFolderResponseJson +import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson + +/** + * Provides an API for querying folder endpoints. + */ +interface FolderService { + /** + * Attempt to create a folder. + */ + suspend fun createFolder(body: FolderJsonRequest): Result + + /** + * Attempt to update a folder. + */ + suspend fun updateFolder( + folderId: String, + body: FolderJsonRequest, + ): Result + + /** + * Attempt to hard delete a folder. + */ + suspend fun deleteFolder(folderId: String): Result +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/FolderServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/FolderServiceImpl.kt new file mode 100644 index 000000000..e93c94343 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/FolderServiceImpl.kt @@ -0,0 +1,40 @@ +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.FoldersApi +import com.x8bit.bitwarden.data.vault.datasource.network.model.FolderJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateFolderResponseJson +import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson +import kotlinx.serialization.json.Json + +class FolderServiceImpl constructor( + private val foldersApi: FoldersApi, + private val json: Json, +) : FolderService { + override suspend fun createFolder(body: FolderJsonRequest): Result = + foldersApi.createFolder(body = body) + + override suspend fun updateFolder( + folderId: String, + body: FolderJsonRequest, + ): Result = + foldersApi + .updateFolder( + folderId = folderId, + body = body, + ) + .map { UpdateFolderResponseJson.Success(folder = it) } + .recoverCatching { throwable -> + throwable + .toBitwardenError() + .parseErrorBodyOrNull( + code = 400, + json = json, + ) + ?: throw throwable + } + + override suspend fun deleteFolder(folderId: String): Result = + foldersApi.deleteFolder(folderId = folderId) +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/FoldersServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/FoldersServiceTest.kt new file mode 100644 index 000000000..7ff1dd9cb --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/FoldersServiceTest.kt @@ -0,0 +1,99 @@ +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.FoldersApi +import com.x8bit.bitwarden.data.vault.datasource.network.model.FolderJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson +import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateFolderResponseJson +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 +import java.time.ZonedDateTime + +class FoldersServiceTest : BaseServiceTest() { + private val folderApi: FoldersApi = retrofit.create() + + private val folderService: FolderService = FolderServiceImpl( + foldersApi = folderApi, + json = json, + ) + + @Test + fun `createFolder should return the correct response`() = runTest { + server.enqueue(MockResponse().setBody(CREATE_UPDATE_FOLDER_SUCCESS_JSON)) + val result = folderService.createFolder( + body = FolderJsonRequest(DEFAULT_NAME), + ) + assertEquals( + DEFAULT_FOLDER, + result.getOrThrow(), + ) + } + + @Test + fun `updateFolder with success response should return a Success with the correct folder`() = + runTest { + server.enqueue(MockResponse().setBody(CREATE_UPDATE_FOLDER_SUCCESS_JSON)) + val result = folderService.updateFolder( + folderId = DEFAULT_ID, + body = FolderJsonRequest(DEFAULT_NAME), + ) + + assertEquals( + UpdateFolderResponseJson.Success(DEFAULT_FOLDER), + result.getOrThrow(), + ) + } + + @Test + fun `updateFolder with invalid response should return an Invalid with the correct data`() = + runTest { + server.enqueue(MockResponse().setResponseCode(400).setBody(UPDATE_FOLDER_INVALID_JSON)) + val result = folderService.updateFolder( + folderId = DEFAULT_ID, + body = FolderJsonRequest(DEFAULT_NAME), + ) + + assertEquals( + UpdateFolderResponseJson.Invalid( + message = "You do not have permission to edit this.", + validationErrors = null, + ), + result.getOrThrow(), + ) + } + + @Test + fun `DeleteFolder should return a Success with the correct data`() = runTest { + server.enqueue(MockResponse().setResponseCode(200)) + val result = folderService.deleteFolder(DEFAULT_ID) + assertEquals(Unit, result.getOrThrow()) + } +} + +private const val DEFAULT_ID = "FolderId" +private const val DEFAULT_NAME = "TestName" + +private val DEFAULT_FOLDER = SyncResponseJson.Folder( + id = DEFAULT_ID, + name = DEFAULT_NAME, + revisionDate = ZonedDateTime.parse("2024-01-24T22:40:17.1559611Z"), +) + +private const val CREATE_UPDATE_FOLDER_SUCCESS_JSON = """ +{ + "id":"FolderId", + "name":"TestName", + "revisionDate":"2024-01-24T22:40:17.1559611Z", + "object":"folder" +} +""" + +private const val UPDATE_FOLDER_INVALID_JSON = """ +{ + "message": "You do not have permission to edit this.", + "validationErrors": null +} +"""