Adding the Folder Api and Service (#809)

This commit is contained in:
Oleg Semenenko 2024-01-27 11:56:22 -06:00 committed by Álison Fernandes
parent ab5a35b914
commit e38dea7ab7
7 changed files with 263 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.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<SyncResponseJson.Folder>
/**
* Updates a folder.
*/
@PUT("folders/{folderId}")
suspend fun updateFolder(
@Path("folderId") folderId: String,
@Body body: FolderJsonRequest,
): Result<SyncResponseJson.Folder>
/**
* Deletes a folder.
*/
@DELETE("folders/{folderId}")
suspend fun deleteFolder(@Path("folderId") folderId: 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.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(

View file

@ -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?,
)

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 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<String, List<String>>?,
) : UpdateFolderResponseJson()
}

View file

@ -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<SyncResponseJson.Folder>
/**
* Attempt to update a folder.
*/
suspend fun updateFolder(
folderId: String,
body: FolderJsonRequest,
): Result<UpdateFolderResponseJson>
/**
* Attempt to hard delete a folder.
*/
suspend fun deleteFolder(folderId: String): Result<Unit>
}

View file

@ -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<SyncResponseJson.Folder> =
foldersApi.createFolder(body = body)
override suspend fun updateFolder(
folderId: String,
body: FolderJsonRequest,
): Result<UpdateFolderResponseJson> =
foldersApi
.updateFolder(
folderId = folderId,
body = body,
)
.map { UpdateFolderResponseJson.Success(folder = it) }
.recoverCatching { throwable ->
throwable
.toBitwardenError()
.parseErrorBodyOrNull<UpdateFolderResponseJson.Invalid>(
code = 400,
json = json,
)
?: throw throwable
}
override suspend fun deleteFolder(folderId: String): Result<Unit> =
foldersApi.deleteFolder(folderId = folderId)
}

View file

@ -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
}
"""