Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add use case to approve legal hold [WPB-4393] #2239

Merged
merged 9 commits into from
Nov 22, 2023
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?
saleniuk marked this conversation as resolved.
Show resolved Hide resolved
}
}
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
Loading