Skip to content

Commit

Permalink
feat: add use case to approve legal hold [WPB-4393] (#2239)
Browse files Browse the repository at this point in the history
* feat: add use case to approve legal hold [WPB-4393]

* add doc comments

* trigger build

* update jvm tests

* revert jvm tests

---------

Co-authored-by: Yamil Medina <[email protected]>
  • Loading branch information
2 people authored and augustocdias committed Dec 5, 2023
1 parent abb96d1 commit a950ad7
Show file tree
Hide file tree
Showing 8 changed files with 330 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ interface TeamRepository {
suspend fun removeTeamMember(teamId: String, userId: String): Either<CoreFailure, Unit>
suspend fun updateTeam(team: Team): Either<CoreFailure, Unit>
suspend fun syncServices(teamId: TeamId): Either<CoreFailure, Unit>
suspend fun approveLegalHold(teamId: TeamId, password: String?): Either<CoreFailure, Unit>
}

@Suppress("LongParameterList")
Expand Down Expand Up @@ -172,4 +173,9 @@ internal class TeamDataSource(
serviceDAO.insertMultiple(it)
}
}

override suspend fun approveLegalHold(teamId: TeamId, password: String?): Either<CoreFailure, Unit> = wrapApiRequest {
teamsApi.approveLegalHold(teamId.value, selfUserId.value, password)
// TODO: should we update the legal hold status for the current user in the database?
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.kalium.logic.feature.legalhold

import com.wire.kalium.logic.CoreFailure
import com.wire.kalium.logic.NetworkFailure
import com.wire.kalium.logic.StorageFailure
import com.wire.kalium.logic.data.id.SelfTeamIdProvider
import com.wire.kalium.logic.data.team.TeamRepository
import com.wire.kalium.logic.functional.Either
import com.wire.kalium.logic.functional.flatMap
import com.wire.kalium.logic.functional.fold
import com.wire.kalium.network.exceptions.KaliumException
import com.wire.kalium.network.exceptions.isAccessDenied
import com.wire.kalium.network.exceptions.isBadRequest
import io.ktor.http.HttpStatusCode

/**
* Use Case that allows the user to accept a requested legal hold.
*/
interface ApproveLegalHoldUseCase {

/**
* Use case [ApproveLegalHoldUseCase] operation
*
* @param password password for the user account to confirm the action, can be empty for sso users
* @return a [ApproveLegalHoldUseCase.Result] indicating the operation result
*/
suspend operator fun invoke(password: String?): Result

sealed class Result {
data object Success : Result()
sealed class Failure : Result() {
data class GenericFailure(val coreFailure: CoreFailure) : Failure()
data object InvalidPassword : Failure()
data object PasswordRequired : Failure()
}
}
}

class ApproveLegalHoldUseCaseImpl internal constructor(
private val teamRepository: TeamRepository,
private val selfTeamIdProvider: SelfTeamIdProvider,
) : ApproveLegalHoldUseCase {
override suspend fun invoke(password: String?): ApproveLegalHoldUseCase.Result {
return selfTeamIdProvider()
.flatMap {
if (it == null) Either.Left(StorageFailure.DataNotFound)
else Either.Right(it)
}
.flatMap { teamId ->
teamRepository.approveLegalHold(teamId, password)
}
.fold({ handleError(it) }, { ApproveLegalHoldUseCase.Result.Success })
}

private fun handleError(failure: CoreFailure): ApproveLegalHoldUseCase.Result.Failure =
if (failure is NetworkFailure.ServerMiscommunication && failure.kaliumException is KaliumException.InvalidRequestError)
failure.kaliumException.let { error: KaliumException.InvalidRequestError ->
when {
error.errorResponse.code == HttpStatusCode.BadRequest.value && error.isBadRequest() ->
ApproveLegalHoldUseCase.Result.Failure.InvalidPassword
error.errorResponse.code == HttpStatusCode.Forbidden.value && error.isAccessDenied() ->
ApproveLegalHoldUseCase.Result.Failure.PasswordRequired
else -> ApproveLegalHoldUseCase.Result.Failure.GenericFailure(failure)
}
}
else ApproveLegalHoldUseCase.Result.Failure.GenericFailure(failure)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import com.wire.kalium.logic.data.conversation.ConversationRepository
import com.wire.kalium.logic.data.team.TeamRepository
import com.wire.kalium.logic.data.user.UserRepository
import com.wire.kalium.logic.data.id.SelfTeamIdProvider
import com.wire.kalium.logic.feature.legalhold.ApproveLegalHoldUseCase
import com.wire.kalium.logic.feature.legalhold.ApproveLegalHoldUseCaseImpl
import com.wire.kalium.logic.feature.user.IsSelfATeamMemberUseCase
import com.wire.kalium.logic.feature.user.IsSelfATeamMemberUseCaseImpl

Expand All @@ -45,4 +47,9 @@ class TeamScope internal constructor(
)

val isSelfATeamMember: IsSelfATeamMemberUseCase get() = IsSelfATeamMemberUseCaseImpl(selfTeamIdProvider)

val approveLegalHold: ApproveLegalHoldUseCase get() = ApproveLegalHoldUseCaseImpl(
teamRepository = teamRepository,
selfTeamIdProvider = selfTeamIdProvider,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,19 @@ class TeamRepositoryTest {
.wasInvoked(once)
}

@Test
fun givenTeamIdAndUserIdAndPassword_whenFetchingTeamMember_thenTeamMemberShouldBeSuccessful() = runTest {
// given
val (_, teamRepository) = Arrangement()
.withApiApproveLegalHoldSuccess()
.withGetUsersInfoSuccess()
.arrange()
// when
val result = teamRepository.approveLegalHold(teamId = TeamId(value = "teamId"), password = "password")
// then
result.shouldSucceed()
}

private class Arrangement {
@Mock
val teamDAO = configure(mock(classOf<TeamDAO>())) {
Expand Down Expand Up @@ -326,6 +339,13 @@ class TeamRepositoryTest {
.thenReturn(NetworkResponse.Success(value = SERVICE_DETAILS_RESPONSE, headers = mapOf(), httpCode = 200))
}

fun withApiApproveLegalHoldSuccess() = apply {
given(teamsApi)
.suspendFunction(teamsApi::approveLegalHold)
.whenInvokedWith(any(), any())
.thenReturn(NetworkResponse.Success(value = Unit, headers = mapOf(), httpCode = 200))
}

fun arrange() = this to teamRepository

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.kalium.logic.feature.legalhold

import com.wire.kalium.logic.CoreFailure
import com.wire.kalium.logic.NetworkFailure
import com.wire.kalium.logic.StorageFailure
import com.wire.kalium.logic.data.id.SelfTeamIdProvider
import com.wire.kalium.logic.data.id.TeamId
import com.wire.kalium.logic.data.team.TeamRepository
import com.wire.kalium.logic.framework.TestTeam
import com.wire.kalium.logic.functional.Either
import com.wire.kalium.logic.test_util.TestNetworkException
import com.wire.kalium.network.exceptions.KaliumException
import io.ktor.utils.io.errors.IOException
import io.mockative.Mock
import io.mockative.anything
import io.mockative.eq
import io.mockative.given
import io.mockative.mock
import io.mockative.once
import io.mockative.verify
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertIs
import kotlin.test.assertSame

class ApproveLegalHoldUseCaseTest {

@Test
fun givenApproveLegalHoldParams_whenApproving_thenTheRepositoryShouldBeCalledWithCorrectParameters() = runTest {
// given
val selfTeamId = TeamId(TestTeam.TEAM.id)
val password = "password"
val (arrangement, useCase) = Arrangement()
.withGetSelfTeamResult(Either.Right(selfTeamId))
.withApproveLegalHoldResult(Either.Right(Unit))
.arrange()
// when
useCase.invoke(password)
// then
verify(arrangement.teamRepository)
.suspendFunction(arrangement.teamRepository::approveLegalHold)
.with(eq(selfTeamId), eq(password))
.wasInvoked(once)
}

@Test
fun givenGetSelfTeamIdFails_whenApproving_thenDataNotFoundErrorShouldBeReturned() = runTest {
// given
val password = "password"
val (_, useCase) = Arrangement()
.withGetSelfTeamResult(Either.Left(StorageFailure.DataNotFound))
.arrange()
// when
val result = useCase.invoke(password)
// then
assertIs<ApproveLegalHoldUseCase.Result.Failure.GenericFailure>(result)
assertSame(StorageFailure.DataNotFound, result.coreFailure)
}

@Test
fun givenApproveFailsDueToGenericError_whenApproving_thenGenericErrorShouldBeReturned() = runTest {
// given
val selfTeamId = TeamId(TestTeam.TEAM.id)
val password = "password"
val failure = NetworkFailure.ServerMiscommunication(KaliumException.GenericError(IOException("no internet")))
val (_, useCase) = Arrangement()
.withGetSelfTeamResult(Either.Right(selfTeamId))
.withApproveLegalHoldResult(Either.Left(failure))
.arrange()
// when
val result = useCase(password)
// then
assertIs<ApproveLegalHoldUseCase.Result.Failure.GenericFailure>(result)
assertSame(failure, result.coreFailure)
}

@Test
fun givenApproveFailsDueToBadRequest_whenApproving_thenInvalidPasswordErrorShouldBeReturned() = runTest {
// given
val selfTeamId = TeamId(TestTeam.TEAM.id)
val password = "password"
val failure = NetworkFailure.ServerMiscommunication(TestNetworkException.badRequest)
val (_, useCase) = Arrangement()
.withGetSelfTeamResult(Either.Right(selfTeamId))
.withApproveLegalHoldResult(Either.Left(failure))
.arrange()
// when
val result = useCase(password)
// then
assertIs<ApproveLegalHoldUseCase.Result.Failure.InvalidPassword>(result)
}

@Test
fun givenApproveFailsDueToAccessDenied_whenDeleting_thenPasswordRequiredErrorShouldBeReturned() = runTest {
// given
val selfTeamId = TeamId(TestTeam.TEAM.id)
val failure = NetworkFailure.ServerMiscommunication(TestNetworkException.accessDenied)
val (_, useCase) = Arrangement()
.withGetSelfTeamResult(Either.Right(selfTeamId))
.withApproveLegalHoldResult(Either.Left(failure))
.arrange()
// when
val result = useCase(null)
// then
assertIs<ApproveLegalHoldUseCase.Result.Failure.PasswordRequired>(result)
}

@Test
fun givenApproveSucceeds_whenApproving_thenSuccessShouldBeReturned() = runTest {
// given
val selfTeamId = TeamId(TestTeam.TEAM.id)
val password = "password"
val (_, useCase) = Arrangement()
.withGetSelfTeamResult(Either.Right(selfTeamId))
.withApproveLegalHoldResult(Either.Right(Unit))
.arrange()
// when
val result = useCase(password)
// then
assertIs<ApproveLegalHoldUseCase.Result.Success>(result)
}

private class Arrangement {

@Mock
val teamRepository: TeamRepository = mock(TeamRepository::class)
@Mock
val selfTeamIdProvider: SelfTeamIdProvider = mock(SelfTeamIdProvider::class)

val useCase: ApproveLegalHoldUseCase by lazy { ApproveLegalHoldUseCaseImpl(teamRepository, selfTeamIdProvider) }

fun arrange() = this to useCase

fun withGetSelfTeamResult(result: Either<CoreFailure, TeamId?>) = apply {
given(selfTeamIdProvider)
.suspendFunction(selfTeamIdProvider::invoke)
.whenInvoked()
.thenReturn(result)
}

fun withApproveLegalHoldResult(result: Either<CoreFailure, Unit>) = apply {
given(teamRepository)
.suspendFunction(teamRepository::approveLegalHold)
.whenInvokedWith(anything(), anything())
.thenReturn(result)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ interface TeamsApi {
data class TeamMemberIdList(
@SerialName("user_ids") val userIds: List<NonQualifiedUserId>
)
@Serializable
data class PasswordRequest(
@SerialName("password") val password: String?
)

sealed interface GetTeamsOptionsInterface

Expand Down Expand Up @@ -89,6 +93,7 @@ interface TeamsApi {
suspend fun getTeamMember(teamId: TeamId, userId: NonQualifiedUserId): NetworkResponse<TeamMemberDTO>
suspend fun getTeamInfo(teamId: TeamId): NetworkResponse<TeamDTO>
suspend fun whiteListedServices(teamId: TeamId, size: Int = DEFAULT_SERVICES_SIZE): NetworkResponse<ServiceDetailResponse>
suspend fun approveLegalHold(teamId: TeamId, userId: NonQualifiedUserId, password: String?): NetworkResponse<Unit>

companion object {
const val DEFAULT_SERVICES_SIZE = 100 // this number is copied from the web client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package com.wire.kalium.network.api.v0.authenticated

import com.wire.kalium.network.AuthenticatedNetworkClient
import com.wire.kalium.network.api.base.authenticated.TeamsApi
import com.wire.kalium.network.api.base.authenticated.client.PasswordRequest
import com.wire.kalium.network.api.base.model.NonQualifiedConversationId
import com.wire.kalium.network.api.base.model.NonQualifiedUserId
import com.wire.kalium.network.api.base.model.ServiceDetailResponse
Expand All @@ -31,6 +32,7 @@ import io.ktor.client.request.delete
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.request.post
import io.ktor.client.request.put
import io.ktor.client.request.setBody

internal open class TeamsApiV0 internal constructor(
Expand Down Expand Up @@ -76,12 +78,21 @@ internal open class TeamsApiV0 internal constructor(
httpClient.get("$PATH_TEAMS/$teamId/$PATH_MEMBERS/$userId")
}

override suspend fun approveLegalHold(teamId: TeamId, userId: NonQualifiedUserId, password: String?): NetworkResponse<Unit> =
wrapKaliumResponse {
httpClient.put("$PATH_TEAMS/$teamId/$PATH_LEGAL_HOLD/$userId/$PATH_APPROVE") {
setBody(PasswordRequest(password))
}
}

private companion object {
const val PATH_TEAMS = "teams"
const val PATH_CONVERSATIONS = "conversations"
const val PATH_MEMBERS = "members"
const val PATH_MEMBERS_BY_IDS = "get-members-by-ids-using-post"
const val PATH_SERVICES = "services"
const val PATH_WHITELISTED = "whitelisted"
const val PATH_LEGAL_HOLD = "legalhold"
const val PATH_APPROVE = "approve"
}
}
Loading

0 comments on commit a950ad7

Please sign in to comment.