mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 11:59:12 +03:00
Merge pull request #7243 from vector-im/feature/mna/device-manager-signout-session
[Device management] Sign out a session (PSG-742)
This commit is contained in:
commit
75a381ea0d
23 changed files with 1016 additions and 135 deletions
1
changelog.d/7190.wip
Normal file
1
changelog.d/7190.wip
Normal file
|
@ -0,0 +1 @@
|
|||
[Device management] Sign out a session
|
|
@ -3290,6 +3290,7 @@
|
|||
<string name="device_manager_other_sessions_no_unverified_sessions_found">No unverified sessions found.</string>
|
||||
<string name="device_manager_other_sessions_no_inactive_sessions_found">No inactive sessions found.</string>
|
||||
<string name="device_manager_other_sessions_clear_filter">Clear Filter</string>
|
||||
<string name="device_manager_session_overview_signout">Sign out of this session</string>
|
||||
<string name="device_manager_session_details_title">Session details</string>
|
||||
<string name="device_manager_session_details_description">Application, device, and activity information.</string>
|
||||
<string name="device_manager_session_details_session_name">Session name</string>
|
||||
|
|
|
@ -41,6 +41,10 @@
|
|||
<item name="lineHeight">24sp</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Vector.Button.Text.Destructive">
|
||||
<item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayDestructive</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Vector.Button.Text.OnPrimary">
|
||||
<item name="colorControlHighlight">?colorOnPrimary</item>
|
||||
<item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayOnPrimary</item>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -20,4 +20,8 @@ import im.vector.app.core.platform.VectorViewModelAction
|
|||
|
||||
sealed class SessionOverviewAction : VectorViewModelAction {
|
||||
object VerifySession : SessionOverviewAction()
|
||||
object SignoutOtherSession : SessionOverviewAction()
|
||||
object SsoAuthDone : SessionOverviewAction()
|
||||
data class PasswordAuthDone(val password: String) : SessionOverviewAction()
|
||||
object ReAuthCancelled : SessionOverviewAction()
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.app.features.settings.devices.v2.overview
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
|
@ -27,16 +28,22 @@ import androidx.core.view.isVisible
|
|||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.date.VectorDateFormatter
|
||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.platform.VectorMenuProvider
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.resources.DrawableProvider
|
||||
import im.vector.app.databinding.FragmentSessionOverviewBinding
|
||||
import im.vector.app.features.auth.ReAuthActivity
|
||||
import im.vector.app.features.crypto.recover.SetupMode
|
||||
import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
|
||||
import im.vector.app.features.workers.signout.SignOutUiWorker
|
||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -66,18 +73,7 @@ class SessionOverviewFragment :
|
|||
observeViewEvents()
|
||||
initSessionInfoView()
|
||||
initVerifyButton()
|
||||
}
|
||||
|
||||
private fun initSessionInfoView() {
|
||||
views.sessionOverviewInfo.onLearnMoreClickListener = {
|
||||
Toast.makeText(context, "Learn more verification status", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initVerifyButton() {
|
||||
views.sessionOverviewInfo.viewVerifyButton.debouncedClicks {
|
||||
viewModel.handle(SessionOverviewAction.VerifySession)
|
||||
}
|
||||
initSignoutButton()
|
||||
}
|
||||
|
||||
private fun observeViewEvents() {
|
||||
|
@ -92,10 +88,60 @@ class SessionOverviewFragment :
|
|||
is SessionOverviewViewEvent.PromptResetSecrets -> {
|
||||
navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET)
|
||||
}
|
||||
is SessionOverviewViewEvent.RequestReAuth -> askForReAuthentication(it)
|
||||
SessionOverviewViewEvent.SignoutSuccess -> viewNavigator.goBack(requireActivity())
|
||||
is SessionOverviewViewEvent.SignoutError -> showFailure(it.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSessionInfoView() {
|
||||
views.sessionOverviewInfo.onLearnMoreClickListener = {
|
||||
Toast.makeText(context, "Learn more verification status", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initVerifyButton() {
|
||||
views.sessionOverviewInfo.viewVerifyButton.debouncedClicks {
|
||||
viewModel.handle(SessionOverviewAction.VerifySession)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSignoutButton() {
|
||||
views.sessionOverviewSignout.debouncedClicks {
|
||||
confirmSignoutSession()
|
||||
}
|
||||
}
|
||||
|
||||
private fun confirmSignoutSession() = withState(viewModel) { state ->
|
||||
if (state.deviceInfo.invoke()?.isCurrentDevice.orFalse()) {
|
||||
confirmSignoutCurrentSession()
|
||||
} else {
|
||||
confirmSignoutOtherSession()
|
||||
}
|
||||
}
|
||||
|
||||
private fun confirmSignoutCurrentSession() {
|
||||
activity?.let { SignOutUiWorker(it).perform() }
|
||||
}
|
||||
|
||||
private fun confirmSignoutOtherSession() {
|
||||
activity?.let {
|
||||
MaterialAlertDialogBuilder(it)
|
||||
.setTitle(R.string.action_sign_out)
|
||||
.setMessage(R.string.action_sign_out_confirmation_simple)
|
||||
.setPositiveButton(R.string.action_sign_out) { _, _ ->
|
||||
signoutSession()
|
||||
}
|
||||
.setNegativeButton(R.string.action_cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun signoutSession() {
|
||||
viewModel.handle(SessionOverviewAction.SignoutOtherSession)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
cleanUpSessionInfoView()
|
||||
super.onDestroyView()
|
||||
|
@ -122,16 +168,20 @@ class SessionOverviewFragment :
|
|||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
updateToolbar(state.isCurrentSession)
|
||||
updateToolbar(state)
|
||||
updateEntryDetails(state.deviceId)
|
||||
updateSessionInfo(state)
|
||||
updateLoading(state.isLoading)
|
||||
}
|
||||
|
||||
private fun updateToolbar(isCurrentSession: Boolean) {
|
||||
val titleResId = if (isCurrentSession) R.string.device_manager_current_session_title else R.string.device_manager_session_title
|
||||
(activity as? AppCompatActivity)
|
||||
?.supportActionBar
|
||||
?.setTitle(titleResId)
|
||||
private fun updateToolbar(viewState: SessionOverviewViewState) {
|
||||
if (viewState.deviceInfo is Success) {
|
||||
val titleResId =
|
||||
if (viewState.deviceInfo.invoke().isCurrentDevice) R.string.device_manager_current_session_title else R.string.device_manager_session_title
|
||||
(activity as? AppCompatActivity)
|
||||
?.supportActionBar
|
||||
?.setTitle(titleResId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateEntryDetails(deviceId: String) {
|
||||
|
@ -143,10 +193,11 @@ class SessionOverviewFragment :
|
|||
private fun updateSessionInfo(viewState: SessionOverviewViewState) {
|
||||
if (viewState.deviceInfo is Success) {
|
||||
views.sessionOverviewInfo.isVisible = true
|
||||
val isCurrentSession = viewState.isCurrentSession
|
||||
val deviceInfo = viewState.deviceInfo.invoke()
|
||||
val isCurrentSession = deviceInfo.isCurrentDevice
|
||||
val infoViewState = SessionInfoViewState(
|
||||
isCurrentSession = isCurrentSession,
|
||||
deviceFullInfo = viewState.deviceInfo.invoke(),
|
||||
deviceFullInfo = deviceInfo,
|
||||
isVerifyButtonVisible = isCurrentSession || viewState.isCurrentSessionTrusted,
|
||||
isDetailsButtonVisible = false,
|
||||
isLearnMoreLinkVisible = true,
|
||||
|
@ -157,4 +208,45 @@ class SessionOverviewFragment :
|
|||
views.sessionOverviewInfo.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLoading(isLoading: Boolean) {
|
||||
if (isLoading) {
|
||||
showLoading(null)
|
||||
} else {
|
||||
dismissLoadingDialog()
|
||||
}
|
||||
}
|
||||
|
||||
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
|
||||
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
|
||||
LoginFlowTypes.SSO -> {
|
||||
viewModel.handle(SessionOverviewAction.SsoAuthDone)
|
||||
}
|
||||
LoginFlowTypes.PASSWORD -> {
|
||||
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
|
||||
viewModel.handle(SessionOverviewAction.PasswordAuthDone(password))
|
||||
}
|
||||
else -> {
|
||||
viewModel.handle(SessionOverviewAction.ReAuthCancelled)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
viewModel.handle(SessionOverviewAction.ReAuthCancelled)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the re auth activity to get credentials.
|
||||
*/
|
||||
private fun askForReAuthentication(reAuthReq: SessionOverviewViewEvent.RequestReAuth) {
|
||||
ReAuthActivity.newIntent(
|
||||
requireContext(),
|
||||
reAuthReq.registrationFlowResponse,
|
||||
reAuthReq.lastErrorCode,
|
||||
getString(R.string.devices_delete_dialog_title)
|
||||
).let { intent ->
|
||||
reAuthActivityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,17 @@
|
|||
package im.vector.app.features.settings.devices.v2.overview
|
||||
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||
|
||||
sealed class SessionOverviewViewEvent : VectorViewEvents {
|
||||
object ShowVerifyCurrentSession : SessionOverviewViewEvent()
|
||||
data class ShowVerifyOtherSession(val deviceId: String) : SessionOverviewViewEvent()
|
||||
object PromptResetSecrets : SessionOverviewViewEvent()
|
||||
data class RequestReAuth(
|
||||
val registrationFlowResponse: RegistrationFlowResponse,
|
||||
val lastErrorCode: String?
|
||||
) : SessionOverviewViewEvent()
|
||||
|
||||
object SignoutSuccess : SessionOverviewViewEvent()
|
||||
data class SignoutError(val error: Throwable) : SessionOverviewViewEvent()
|
||||
}
|
||||
|
|
|
@ -21,26 +21,47 @@ import com.airbnb.mvrx.Success
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.features.settings.devices.v2.IsCurrentSessionUseCase
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.auth.PendingAuthHandler
|
||||
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
|
||||
import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel
|
||||
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
|
||||
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult
|
||||
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase
|
||||
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
||||
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
|
||||
import timber.log.Timber
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
import kotlin.coroutines.Continuation
|
||||
|
||||
class SessionOverviewViewModel @AssistedInject constructor(
|
||||
@Assisted val initialState: SessionOverviewViewState,
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val isCurrentSessionUseCase: IsCurrentSessionUseCase,
|
||||
private val stringProvider: StringProvider,
|
||||
private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase,
|
||||
private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
|
||||
) : VectorViewModel<SessionOverviewViewState, SessionOverviewAction, SessionOverviewViewEvent>(initialState) {
|
||||
private val signoutSessionUseCase: SignoutSessionUseCase,
|
||||
private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
|
||||
private val pendingAuthHandler: PendingAuthHandler,
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
refreshDevicesUseCase: RefreshDevicesUseCase,
|
||||
) : VectorSessionsListViewModel<SessionOverviewViewState, SessionOverviewAction, SessionOverviewViewEvent>(
|
||||
initialState, activeSessionHolder, refreshDevicesUseCase
|
||||
) {
|
||||
|
||||
companion object : MavericksViewModelFactory<SessionOverviewViewModel, SessionOverviewViewState> by hiltMavericksViewModelFactory()
|
||||
|
||||
|
@ -50,17 +71,10 @@ class SessionOverviewViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
init {
|
||||
setState {
|
||||
copy(isCurrentSession = isCurrentSession(deviceId))
|
||||
}
|
||||
observeSessionInfo(initialState.deviceId)
|
||||
observeCurrentSessionInfo()
|
||||
}
|
||||
|
||||
private fun isCurrentSession(deviceId: String): Boolean {
|
||||
return isCurrentSessionUseCase.execute(deviceId)
|
||||
}
|
||||
|
||||
private fun observeSessionInfo(deviceId: String) {
|
||||
getDeviceFullInfoUseCase.execute(deviceId)
|
||||
.onEach { setState { copy(deviceInfo = Success(it)) } }
|
||||
|
@ -83,11 +97,15 @@ class SessionOverviewViewModel @AssistedInject constructor(
|
|||
override fun handle(action: SessionOverviewAction) {
|
||||
when (action) {
|
||||
is SessionOverviewAction.VerifySession -> handleVerifySessionAction()
|
||||
SessionOverviewAction.SignoutOtherSession -> handleSignoutOtherSession()
|
||||
SessionOverviewAction.SsoAuthDone -> handleSsoAuthDone()
|
||||
is SessionOverviewAction.PasswordAuthDone -> handlePasswordAuthDone(action)
|
||||
SessionOverviewAction.ReAuthCancelled -> handleReAuthCancelled()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVerifySessionAction() = withState { viewState ->
|
||||
if (viewState.isCurrentSession) {
|
||||
if (viewState.deviceInfo.invoke()?.isCurrentDevice.orFalse()) {
|
||||
handleVerifyCurrentSession()
|
||||
} else {
|
||||
handleVerifyOtherSession(viewState.deviceId)
|
||||
|
@ -108,4 +126,76 @@ class SessionOverviewViewModel @AssistedInject constructor(
|
|||
private fun handleVerifyOtherSession(deviceId: String) {
|
||||
_viewEvents.post(SessionOverviewViewEvent.ShowVerifyOtherSession(deviceId))
|
||||
}
|
||||
|
||||
private fun handleSignoutOtherSession() = withState { state ->
|
||||
// signout process for current session is not handled here
|
||||
if (!state.deviceInfo.invoke()?.isCurrentDevice.orFalse()) {
|
||||
handleSignoutOtherSession(state.deviceId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSignoutOtherSession(deviceId: String) {
|
||||
viewModelScope.launch {
|
||||
setLoading(true)
|
||||
val signoutResult = signout(deviceId)
|
||||
setLoading(false)
|
||||
|
||||
if (signoutResult.isSuccess) {
|
||||
onSignoutSuccess()
|
||||
} else {
|
||||
when (val failure = signoutResult.exceptionOrNull()) {
|
||||
null -> onSignoutSuccess()
|
||||
else -> onSignoutFailure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun signout(deviceId: String) = signoutSessionUseCase.execute(deviceId, object : UserInteractiveAuthInterceptor {
|
||||
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||
when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) {
|
||||
is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result)
|
||||
is SignoutSessionResult.Completed -> Unit
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) {
|
||||
Timber.d("onReAuthNeeded")
|
||||
pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
|
||||
pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation
|
||||
_viewEvents.post(SessionOverviewViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode))
|
||||
}
|
||||
|
||||
private fun setLoading(isLoading: Boolean) {
|
||||
setState { copy(isLoading = isLoading) }
|
||||
}
|
||||
|
||||
private fun onSignoutSuccess() {
|
||||
Timber.d("signout success")
|
||||
refreshDeviceList()
|
||||
_viewEvents.post(SessionOverviewViewEvent.SignoutSuccess)
|
||||
}
|
||||
|
||||
private fun onSignoutFailure(failure: Throwable) {
|
||||
Timber.e("signout failure", failure)
|
||||
val failureMessage = if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) {
|
||||
stringProvider.getString(R.string.authentication_error)
|
||||
} else {
|
||||
stringProvider.getString(R.string.matrix_error)
|
||||
}
|
||||
_viewEvents.post(SessionOverviewViewEvent.SignoutError(Exception(failureMessage)))
|
||||
}
|
||||
|
||||
private fun handleSsoAuthDone() {
|
||||
pendingAuthHandler.ssoAuthDone()
|
||||
}
|
||||
|
||||
private fun handlePasswordAuthDone(action: SessionOverviewAction.PasswordAuthDone) {
|
||||
pendingAuthHandler.passwordAuthDone(action.password)
|
||||
}
|
||||
|
||||
private fun handleReAuthCancelled() {
|
||||
pendingAuthHandler.reAuthCancelled()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.app.features.settings.devices.v2.overview
|
||||
|
||||
import android.content.Context
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import im.vector.app.features.settings.devices.v2.details.SessionDetailsActivity
|
||||
import im.vector.app.features.settings.devices.v2.rename.RenameSessionActivity
|
||||
import javax.inject.Inject
|
||||
|
@ -30,4 +31,8 @@ class SessionOverviewViewNavigator @Inject constructor() {
|
|||
fun goToRenameSession(context: Context, deviceId: String) {
|
||||
context.startActivity(RenameSessionActivity.newIntent(context, deviceId))
|
||||
}
|
||||
|
||||
fun goBack(fragmentActivity: FragmentActivity) {
|
||||
fragmentActivity.finish()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,9 +23,9 @@ import im.vector.app.features.settings.devices.v2.DeviceFullInfo
|
|||
|
||||
data class SessionOverviewViewState(
|
||||
val deviceId: String,
|
||||
val isCurrentSession: Boolean = false,
|
||||
val isCurrentSessionTrusted: Boolean = false,
|
||||
val deviceInfo: Async<DeviceFullInfo> = Uninitialized,
|
||||
val isLoading: Boolean = false,
|
||||
) : MavericksState {
|
||||
constructor(args: SessionOverviewArgs) : this(
|
||||
deviceId = args.deviceId
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.devices.v2.signout
|
||||
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.features.login.ReAuthHelper
|
||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
|
||||
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class InterceptSignoutFlowResponseUseCase @Inject constructor(
|
||||
private val reAuthHelper: ReAuthHelper,
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
) {
|
||||
|
||||
fun execute(
|
||||
flowResponse: RegistrationFlowResponse,
|
||||
errCode: String?,
|
||||
promise: Continuation<UIABaseAuth>
|
||||
): SignoutSessionResult {
|
||||
return if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errCode == null) {
|
||||
UserPasswordAuth(
|
||||
session = null,
|
||||
user = activeSessionHolder.getActiveSession().myUserId,
|
||||
password = reAuthHelper.data
|
||||
).let { promise.resume(it) }
|
||||
|
||||
SignoutSessionResult.Completed
|
||||
} else {
|
||||
SignoutSessionResult.ReAuthNeeded(
|
||||
pendingAuth = DefaultBaseAuth(session = flowResponse.session),
|
||||
uiaContinuation = promise,
|
||||
flowResponse = flowResponse,
|
||||
errCode = errCode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.devices.v2.signout
|
||||
|
||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||
import kotlin.coroutines.Continuation
|
||||
|
||||
sealed class SignoutSessionResult {
|
||||
data class ReAuthNeeded(
|
||||
val pendingAuth: UIABaseAuth,
|
||||
val uiaContinuation: Continuation<UIABaseAuth>,
|
||||
val flowResponse: RegistrationFlowResponse,
|
||||
val errCode: String?
|
||||
) : SignoutSessionResult()
|
||||
|
||||
object Completed : SignoutSessionResult()
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.devices.v2.signout
|
||||
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||
import org.matrix.android.sdk.api.util.awaitCallback
|
||||
import javax.inject.Inject
|
||||
|
||||
class SignoutSessionUseCase @Inject constructor(
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
) {
|
||||
|
||||
suspend fun execute(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result<Unit> {
|
||||
return deleteDevice(deviceId, userInteractiveAuthInterceptor)
|
||||
}
|
||||
|
||||
private suspend fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching {
|
||||
awaitCallback { matrixCallback ->
|
||||
activeSessionHolder.getActiveSession()
|
||||
.cryptoService()
|
||||
.deleteDevice(deviceId, userInteractiveAuthInterceptor, matrixCallback)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,6 +31,19 @@
|
|||
app:sessionOverviewEntryDescription="@string/device_manager_session_details_description"
|
||||
app:sessionOverviewEntryTitle="@string/device_manager_session_details_title" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/sessionOverviewSignout"
|
||||
style="@style/Widget.Vector.Button.Text.Destructive"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:text="@string/device_manager_session_overview_signout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/sessionOverviewEntryDetails"
|
||||
app:layout_constraintWidth="wrap_content" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
|
|
@ -70,6 +70,7 @@ class DevicesViewModelTest {
|
|||
|
||||
@Before
|
||||
fun setup() {
|
||||
// Needed for internal usage of Flow<T>.throttleFirst() inside the ViewModel
|
||||
mockkStatic(SystemClock::class)
|
||||
every { SystemClock.elapsedRealtime() } returns 1234
|
||||
}
|
||||
|
@ -217,8 +218,8 @@ class DevicesViewModelTest {
|
|||
.fakeSession
|
||||
.fakeCryptoService
|
||||
.fakeVerificationService
|
||||
every { fakeVerificationService.addListener(any()) } just runs
|
||||
every { fakeVerificationService.removeListener(any()) } just runs
|
||||
fakeVerificationService.givenAddListenerSucceeds()
|
||||
fakeVerificationService.givenRemoveListenerSucceeds()
|
||||
return fakeVerificationService
|
||||
}
|
||||
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.devices.v2
|
||||
|
||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||
import io.mockk.verify
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||
|
||||
private const val A_SESSION_ID_1 = "session-id-1"
|
||||
private const val A_SESSION_ID_2 = "session-id-2"
|
||||
|
||||
class IsCurrentSessionUseCaseTest {
|
||||
|
||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||
|
||||
private val isCurrentSessionUseCase = IsCurrentSessionUseCase(
|
||||
activeSessionHolder = fakeActiveSessionHolder.instance,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `given the session id of the current session when checking if id is current session then result is true`() {
|
||||
// Given
|
||||
val sessionParams = givenIdForCurrentSession(A_SESSION_ID_1)
|
||||
|
||||
// When
|
||||
val result = isCurrentSessionUseCase.execute(A_SESSION_ID_1)
|
||||
|
||||
// Then
|
||||
result shouldBe true
|
||||
verify { sessionParams.deviceId }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a session id different from the current session id when checking if id is current session then result is false`() {
|
||||
// Given
|
||||
val sessionParams = givenIdForCurrentSession(A_SESSION_ID_1)
|
||||
|
||||
// When
|
||||
val result = isCurrentSessionUseCase.execute(A_SESSION_ID_2)
|
||||
|
||||
// Then
|
||||
result shouldBe false
|
||||
verify { sessionParams.deviceId }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given no current active session when checking if id is current session then result is false`() {
|
||||
// Given
|
||||
fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null)
|
||||
|
||||
// When
|
||||
val result = isCurrentSessionUseCase.execute(A_SESSION_ID_1)
|
||||
|
||||
// Then
|
||||
result shouldBe false
|
||||
}
|
||||
|
||||
private fun givenIdForCurrentSession(sessionId: String): SessionParams {
|
||||
return fakeActiveSessionHolder.fakeSession.givenSessionId(sessionId)
|
||||
}
|
||||
}
|
|
@ -16,26 +16,53 @@
|
|||
|
||||
package im.vector.app.features.settings.devices.v2.overview
|
||||
|
||||
import android.os.SystemClock
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.test.MvRxTestRule
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
|
||||
import im.vector.app.features.settings.devices.v2.IsCurrentSessionUseCase
|
||||
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
|
||||
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
|
||||
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult
|
||||
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase
|
||||
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
|
||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||
import im.vector.app.test.fakes.FakePendingAuthHandler
|
||||
import im.vector.app.test.fakes.FakeStringProvider
|
||||
import im.vector.app.test.fakes.FakeVerificationService
|
||||
import im.vector.app.test.test
|
||||
import im.vector.app.test.testDispatcher
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkAll
|
||||
import io.mockk.verify
|
||||
import io.mockk.verifyAll
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
||||
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
import kotlin.coroutines.Continuation
|
||||
|
||||
private const val A_SESSION_ID_1 = "session-id-1"
|
||||
private const val A_SESSION_ID_2 = "session-id-2"
|
||||
private const val AUTH_ERROR_MESSAGE = "auth-error-message"
|
||||
private const val AN_ERROR_MESSAGE = "error-message"
|
||||
private const val A_PASSWORD = "password"
|
||||
|
||||
class SessionOverviewViewModelTest {
|
||||
|
||||
|
@ -46,29 +73,48 @@ class SessionOverviewViewModelTest {
|
|||
deviceId = A_SESSION_ID_1
|
||||
)
|
||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||
private val isCurrentSessionUseCase = mockk<IsCurrentSessionUseCase>()
|
||||
private val fakeStringProvider = FakeStringProvider()
|
||||
private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>()
|
||||
private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>()
|
||||
private val signoutSessionUseCase = mockk<SignoutSessionUseCase>()
|
||||
private val interceptSignoutFlowResponseUseCase = mockk<InterceptSignoutFlowResponseUseCase>()
|
||||
private val fakePendingAuthHandler = FakePendingAuthHandler()
|
||||
private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>()
|
||||
|
||||
private fun createViewModel() = SessionOverviewViewModel(
|
||||
initialState = SessionOverviewViewState(args),
|
||||
activeSessionHolder = fakeActiveSessionHolder.instance,
|
||||
isCurrentSessionUseCase = isCurrentSessionUseCase,
|
||||
stringProvider = fakeStringProvider.instance,
|
||||
getDeviceFullInfoUseCase = getDeviceFullInfoUseCase,
|
||||
checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase,
|
||||
signoutSessionUseCase = signoutSessionUseCase,
|
||||
interceptSignoutFlowResponseUseCase = interceptSignoutFlowResponseUseCase,
|
||||
pendingAuthHandler = fakePendingAuthHandler.instance,
|
||||
activeSessionHolder = fakeActiveSessionHolder.instance,
|
||||
refreshDevicesUseCase = refreshDevicesUseCase,
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
// Needed for internal usage of Flow<T>.throttleFirst() inside the ViewModel
|
||||
mockkStatic(SystemClock::class)
|
||||
every { SystemClock.elapsedRealtime() } returns 1234
|
||||
|
||||
givenVerificationService()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given the viewModel has been initialized then viewState is updated with session info and current session verification status`() {
|
||||
// Given
|
||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||
val isCurrentSession = true
|
||||
every { isCurrentSessionUseCase.execute(any()) } returns isCurrentSession
|
||||
givenCurrentSessionIsTrusted()
|
||||
val expectedState = SessionOverviewViewState(
|
||||
deviceId = A_SESSION_ID_1,
|
||||
isCurrentSession = isCurrentSession,
|
||||
deviceInfo = Success(deviceFullInfo),
|
||||
isCurrentSessionTrusted = true,
|
||||
)
|
||||
|
@ -81,7 +127,6 @@ class SessionOverviewViewModelTest {
|
|||
.assertLatestState { state -> state == expectedState }
|
||||
.finish()
|
||||
verify {
|
||||
isCurrentSessionUseCase.execute(A_SESSION_ID_1)
|
||||
getDeviceFullInfoUseCase.execute(A_SESSION_ID_1)
|
||||
}
|
||||
}
|
||||
|
@ -90,8 +135,8 @@ class SessionOverviewViewModelTest {
|
|||
fun `given current session can be verified when handling verify current session action then self verification event is posted`() {
|
||||
// Given
|
||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||
every { deviceFullInfo.isCurrentDevice } returns true
|
||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||
every { isCurrentSessionUseCase.execute(any()) } returns true
|
||||
val verifySessionAction = SessionOverviewAction.VerifySession
|
||||
coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns true
|
||||
givenCurrentSessionIsTrusted()
|
||||
|
@ -114,8 +159,8 @@ class SessionOverviewViewModelTest {
|
|||
fun `given current session cannot be verified when handling verify current session action then reset secrets event is posted`() {
|
||||
// Given
|
||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||
every { deviceFullInfo.isCurrentDevice } returns true
|
||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||
every { isCurrentSessionUseCase.execute(any()) } returns true
|
||||
val verifySessionAction = SessionOverviewAction.VerifySession
|
||||
coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns false
|
||||
givenCurrentSessionIsTrusted()
|
||||
|
@ -138,8 +183,8 @@ class SessionOverviewViewModelTest {
|
|||
fun `given another session when handling verify session action then verify session event is posted`() {
|
||||
// Given
|
||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||
every { deviceFullInfo.isCurrentDevice } returns false
|
||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||
every { isCurrentSessionUseCase.execute(any()) } returns false
|
||||
val verifySessionAction = SessionOverviewAction.VerifySession
|
||||
givenCurrentSessionIsTrusted()
|
||||
|
||||
|
@ -154,6 +199,248 @@ class SessionOverviewViewModelTest {
|
|||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given another session and no reAuth is needed when handling signout action then signout process is performed`() {
|
||||
// Given
|
||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||
every { deviceFullInfo.isCurrentDevice } returns false
|
||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||
givenSignoutSuccess(A_SESSION_ID_1)
|
||||
every { refreshDevicesUseCase.execute() } just runs
|
||||
val signoutAction = SessionOverviewAction.SignoutOtherSession
|
||||
givenCurrentSessionIsTrusted()
|
||||
val expectedViewState = SessionOverviewViewState(
|
||||
deviceId = A_SESSION_ID_1,
|
||||
isCurrentSessionTrusted = true,
|
||||
deviceInfo = Success(deviceFullInfo),
|
||||
isLoading = false,
|
||||
)
|
||||
|
||||
// When
|
||||
val viewModel = createViewModel()
|
||||
val viewModelTest = viewModel.test()
|
||||
viewModel.handle(signoutAction)
|
||||
|
||||
// Then
|
||||
viewModelTest
|
||||
.assertStatesChanges(
|
||||
expectedViewState,
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false) }
|
||||
)
|
||||
.assertEvent { it is SessionOverviewViewEvent.SignoutSuccess }
|
||||
.finish()
|
||||
verify {
|
||||
refreshDevicesUseCase.execute()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given another session and server error during signout when handling signout action then signout process is performed`() {
|
||||
// Given
|
||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||
every { deviceFullInfo.isCurrentDevice } returns false
|
||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||
val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED)
|
||||
givenSignoutError(A_SESSION_ID_1, serverError)
|
||||
val signoutAction = SessionOverviewAction.SignoutOtherSession
|
||||
givenCurrentSessionIsTrusted()
|
||||
val expectedViewState = SessionOverviewViewState(
|
||||
deviceId = A_SESSION_ID_1,
|
||||
isCurrentSessionTrusted = true,
|
||||
deviceInfo = Success(deviceFullInfo),
|
||||
isLoading = false,
|
||||
)
|
||||
fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE)
|
||||
|
||||
// When
|
||||
val viewModel = createViewModel()
|
||||
val viewModelTest = viewModel.test()
|
||||
viewModel.handle(signoutAction)
|
||||
|
||||
// Then
|
||||
viewModelTest
|
||||
.assertStatesChanges(
|
||||
expectedViewState,
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false) }
|
||||
)
|
||||
.assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error.message == AUTH_ERROR_MESSAGE }
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given another session and unexpected error during signout when handling signout action then signout process is performed`() {
|
||||
// Given
|
||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||
every { deviceFullInfo.isCurrentDevice } returns false
|
||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||
val error = Exception()
|
||||
givenSignoutError(A_SESSION_ID_1, error)
|
||||
val signoutAction = SessionOverviewAction.SignoutOtherSession
|
||||
givenCurrentSessionIsTrusted()
|
||||
val expectedViewState = SessionOverviewViewState(
|
||||
deviceId = A_SESSION_ID_1,
|
||||
isCurrentSessionTrusted = true,
|
||||
deviceInfo = Success(deviceFullInfo),
|
||||
isLoading = false,
|
||||
)
|
||||
fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE)
|
||||
|
||||
// When
|
||||
val viewModel = createViewModel()
|
||||
val viewModelTest = viewModel.test()
|
||||
viewModel.handle(signoutAction)
|
||||
|
||||
// Then
|
||||
viewModelTest
|
||||
.assertStatesChanges(
|
||||
expectedViewState,
|
||||
{ copy(isLoading = true) },
|
||||
{ copy(isLoading = false) }
|
||||
)
|
||||
.assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error.message == AN_ERROR_MESSAGE }
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given another session and reAuth is needed during signout when handling signout action then requestReAuth is sent and pending auth is stored`() {
|
||||
// Given
|
||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||
every { deviceFullInfo.isCurrentDevice } returns false
|
||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||
val reAuthNeeded = givenSignoutReAuthNeeded(A_SESSION_ID_1)
|
||||
val signoutAction = SessionOverviewAction.SignoutOtherSession
|
||||
givenCurrentSessionIsTrusted()
|
||||
val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
|
||||
val expectedReAuthEvent = SessionOverviewViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)
|
||||
|
||||
// When
|
||||
val viewModel = createViewModel()
|
||||
val viewModelTest = viewModel.test()
|
||||
viewModel.handle(signoutAction)
|
||||
|
||||
// Then
|
||||
viewModelTest
|
||||
.assertEvent { it == expectedReAuthEvent }
|
||||
.finish()
|
||||
fakePendingAuthHandler.instance.pendingAuth shouldBeEqualTo expectedPendingAuth
|
||||
fakePendingAuthHandler.instance.uiaContinuation shouldBeEqualTo reAuthNeeded.uiaContinuation
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given SSO auth has been done when handling ssoAuthDone action then corresponding method of pending auth handler is called`() {
|
||||
// Given
|
||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||
every { deviceFullInfo.isCurrentDevice } returns false
|
||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||
val action = SessionOverviewAction.SsoAuthDone
|
||||
givenCurrentSessionIsTrusted()
|
||||
every { fakePendingAuthHandler.instance.ssoAuthDone() } just runs
|
||||
|
||||
// When
|
||||
val viewModel = createViewModel()
|
||||
val viewModelTest = viewModel.test()
|
||||
viewModel.handle(action)
|
||||
|
||||
// Then
|
||||
viewModelTest.finish()
|
||||
verifyAll {
|
||||
fakePendingAuthHandler.instance.ssoAuthDone()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given password auth has been done when handling passwordAuthDone action then corresponding method of pending auth handler is called`() {
|
||||
// Given
|
||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||
every { deviceFullInfo.isCurrentDevice } returns false
|
||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||
val action = SessionOverviewAction.PasswordAuthDone(password = A_PASSWORD)
|
||||
givenCurrentSessionIsTrusted()
|
||||
every { fakePendingAuthHandler.instance.passwordAuthDone(any()) } just runs
|
||||
|
||||
// When
|
||||
val viewModel = createViewModel()
|
||||
val viewModelTest = viewModel.test()
|
||||
viewModel.handle(action)
|
||||
|
||||
// Then
|
||||
viewModelTest.finish()
|
||||
verifyAll {
|
||||
fakePendingAuthHandler.instance.passwordAuthDone(A_PASSWORD)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given reAuth has been cancelled when handling reAuthCancelled action then corresponding method of pending auth handler is called`() {
|
||||
// Given
|
||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||
every { deviceFullInfo.isCurrentDevice } returns false
|
||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||
val action = SessionOverviewAction.ReAuthCancelled
|
||||
givenCurrentSessionIsTrusted()
|
||||
every { fakePendingAuthHandler.instance.reAuthCancelled() } just runs
|
||||
|
||||
// When
|
||||
val viewModel = createViewModel()
|
||||
val viewModelTest = viewModel.test()
|
||||
viewModel.handle(action)
|
||||
|
||||
// Then
|
||||
viewModelTest.finish()
|
||||
verifyAll {
|
||||
fakePendingAuthHandler.instance.reAuthCancelled()
|
||||
}
|
||||
}
|
||||
|
||||
private fun givenSignoutSuccess(deviceId: String) {
|
||||
val interceptor = slot<UserInteractiveAuthInterceptor>()
|
||||
val flowResponse = mockk<RegistrationFlowResponse>()
|
||||
val errorCode = "errorCode"
|
||||
val promise = mockk<Continuation<UIABaseAuth>>()
|
||||
every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed
|
||||
coEvery { signoutSessionUseCase.execute(deviceId, capture(interceptor)) } coAnswers {
|
||||
secondArg<UserInteractiveAuthInterceptor>().performStage(flowResponse, errorCode, promise)
|
||||
Result.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private fun givenSignoutReAuthNeeded(deviceId: String): SignoutSessionResult.ReAuthNeeded {
|
||||
val interceptor = slot<UserInteractiveAuthInterceptor>()
|
||||
val flowResponse = mockk<RegistrationFlowResponse>()
|
||||
every { flowResponse.session } returns A_SESSION_ID_1
|
||||
val errorCode = "errorCode"
|
||||
val promise = mockk<Continuation<UIABaseAuth>>()
|
||||
val reAuthNeeded = SignoutSessionResult.ReAuthNeeded(
|
||||
pendingAuth = mockk(),
|
||||
uiaContinuation = promise,
|
||||
flowResponse = flowResponse,
|
||||
errCode = errorCode,
|
||||
)
|
||||
every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded
|
||||
coEvery { signoutSessionUseCase.execute(deviceId, capture(interceptor)) } coAnswers {
|
||||
secondArg<UserInteractiveAuthInterceptor>().performStage(flowResponse, errorCode, promise)
|
||||
Result.success(Unit)
|
||||
}
|
||||
|
||||
return reAuthNeeded
|
||||
}
|
||||
|
||||
private fun givenSignoutError(deviceId: String, error: Throwable) {
|
||||
coEvery { signoutSessionUseCase.execute(deviceId, any()) } returns Result.failure(error)
|
||||
}
|
||||
|
||||
private fun givenVerificationService(): FakeVerificationService {
|
||||
val fakeVerificationService = fakeActiveSessionHolder
|
||||
.fakeSession
|
||||
.fakeCryptoService
|
||||
.fakeVerificationService
|
||||
fakeVerificationService.givenAddListenerSucceeds()
|
||||
fakeVerificationService.givenRemoveListenerSucceeds()
|
||||
return fakeVerificationService
|
||||
}
|
||||
|
||||
private fun givenCurrentSessionIsTrusted() {
|
||||
fakeActiveSessionHolder.fakeSession.givenSessionId(A_SESSION_ID_2)
|
||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||
|
|
|
@ -17,12 +17,15 @@
|
|||
package im.vector.app.features.settings.devices.v2.overview
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import im.vector.app.features.settings.devices.v2.details.SessionDetailsActivity
|
||||
import im.vector.app.features.settings.devices.v2.rename.RenameSessionActivity
|
||||
import im.vector.app.test.fakes.FakeContext
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkAll
|
||||
import io.mockk.verify
|
||||
import org.junit.After
|
||||
|
@ -77,6 +80,21 @@ class SessionOverviewViewNavigatorTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given an activity when going back then the activity is finished`() {
|
||||
// Given
|
||||
val fragmentActivity = mockk<FragmentActivity>()
|
||||
every { fragmentActivity.finish() } just runs
|
||||
|
||||
// When
|
||||
sessionOverviewViewNavigator.goBack(fragmentActivity)
|
||||
|
||||
// Then
|
||||
verify {
|
||||
fragmentActivity.finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun givenIntentForSessionDetails(sessionId: String): Intent {
|
||||
val intent = mockk<Intent>()
|
||||
every { SessionDetailsActivity.newIntent(context.instance, sessionId) } returns intent
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.devices.v2.signout
|
||||
|
||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||
import im.vector.app.test.fakes.FakeReAuthHelper
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkAll
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.amshove.kluent.shouldBeInstanceOf
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
|
||||
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
private const val A_PASSWORD = "password"
|
||||
private const val A_SESSION_ID = "session-id"
|
||||
private const val AN_ERROR_CODE = "error-code"
|
||||
|
||||
class InterceptSignoutFlowResponseUseCaseTest {
|
||||
|
||||
private val fakeReAuthHelper = FakeReAuthHelper()
|
||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||
|
||||
private val interceptSignoutFlowResponseUseCase = InterceptSignoutFlowResponseUseCase(
|
||||
reAuthHelper = fakeReAuthHelper.instance,
|
||||
activeSessionHolder = fakeActiveSessionHolder.instance,
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockkStatic("org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponseKt")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given no error and a stored password and a next stage as password when intercepting then promise is resumed and success is returned`() {
|
||||
// Given
|
||||
val registrationFlowResponse = givenNextUncompletedStage(LoginFlowTypes.PASSWORD, A_SESSION_ID)
|
||||
fakeReAuthHelper.givenStoredPassword(A_PASSWORD)
|
||||
val errorCode: String? = null
|
||||
val promise = mockk<Continuation<UIABaseAuth>>()
|
||||
every { promise.resume(any()) } just runs
|
||||
val expectedAuth = UserPasswordAuth(
|
||||
session = null,
|
||||
user = fakeActiveSessionHolder.fakeSession.myUserId,
|
||||
password = A_PASSWORD,
|
||||
)
|
||||
|
||||
// When
|
||||
val result = interceptSignoutFlowResponseUseCase.execute(
|
||||
flowResponse = registrationFlowResponse,
|
||||
errCode = errorCode,
|
||||
promise = promise,
|
||||
)
|
||||
|
||||
// Then
|
||||
result shouldBeInstanceOf (SignoutSessionResult.Completed::class)
|
||||
every {
|
||||
promise.resume(expectedAuth)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given an error when intercepting then reAuthNeeded result is returned`() {
|
||||
// Given
|
||||
val registrationFlowResponse = givenNextUncompletedStage(LoginFlowTypes.PASSWORD, A_SESSION_ID)
|
||||
fakeReAuthHelper.givenStoredPassword(A_PASSWORD)
|
||||
val errorCode = AN_ERROR_CODE
|
||||
val promise = mockk<Continuation<UIABaseAuth>>()
|
||||
val expectedResult = SignoutSessionResult.ReAuthNeeded(
|
||||
pendingAuth = DefaultBaseAuth(session = A_SESSION_ID),
|
||||
uiaContinuation = promise,
|
||||
flowResponse = registrationFlowResponse,
|
||||
errCode = errorCode
|
||||
)
|
||||
|
||||
// When
|
||||
val result = interceptSignoutFlowResponseUseCase.execute(
|
||||
flowResponse = registrationFlowResponse,
|
||||
errCode = errorCode,
|
||||
promise = promise,
|
||||
)
|
||||
|
||||
// Then
|
||||
result shouldBeEqualTo expectedResult
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given next stage is not password when intercepting then reAuthNeeded result is returned`() {
|
||||
// Given
|
||||
val registrationFlowResponse = givenNextUncompletedStage(LoginFlowTypes.SSO, A_SESSION_ID)
|
||||
fakeReAuthHelper.givenStoredPassword(A_PASSWORD)
|
||||
val errorCode: String? = null
|
||||
val promise = mockk<Continuation<UIABaseAuth>>()
|
||||
val expectedResult = SignoutSessionResult.ReAuthNeeded(
|
||||
pendingAuth = DefaultBaseAuth(session = A_SESSION_ID),
|
||||
uiaContinuation = promise,
|
||||
flowResponse = registrationFlowResponse,
|
||||
errCode = errorCode
|
||||
)
|
||||
|
||||
// When
|
||||
val result = interceptSignoutFlowResponseUseCase.execute(
|
||||
flowResponse = registrationFlowResponse,
|
||||
errCode = errorCode,
|
||||
promise = promise,
|
||||
)
|
||||
|
||||
// Then
|
||||
result shouldBeEqualTo expectedResult
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given no existing stored password when intercepting then reAuthNeeded result is returned`() {
|
||||
// Given
|
||||
val registrationFlowResponse = givenNextUncompletedStage(LoginFlowTypes.PASSWORD, A_SESSION_ID)
|
||||
fakeReAuthHelper.givenStoredPassword(null)
|
||||
val errorCode: String? = null
|
||||
val promise = mockk<Continuation<UIABaseAuth>>()
|
||||
val expectedResult = SignoutSessionResult.ReAuthNeeded(
|
||||
pendingAuth = DefaultBaseAuth(session = A_SESSION_ID),
|
||||
uiaContinuation = promise,
|
||||
flowResponse = registrationFlowResponse,
|
||||
errCode = errorCode
|
||||
)
|
||||
|
||||
// When
|
||||
val result = interceptSignoutFlowResponseUseCase.execute(
|
||||
flowResponse = registrationFlowResponse,
|
||||
errCode = errorCode,
|
||||
promise = promise,
|
||||
)
|
||||
|
||||
// Then
|
||||
result shouldBeEqualTo expectedResult
|
||||
}
|
||||
|
||||
private fun givenNextUncompletedStage(nextStage: String, sessionId: String): RegistrationFlowResponse {
|
||||
val registrationFlowResponse = mockk<RegistrationFlowResponse>()
|
||||
every { registrationFlowResponse.nextUncompletedStage() } returns nextStage
|
||||
every { registrationFlowResponse.session } returns sessionId
|
||||
return registrationFlowResponse
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.devices.v2.signout
|
||||
|
||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||
|
||||
private const val A_DEVICE_ID = "device-id"
|
||||
|
||||
class SignoutSessionUseCaseTest {
|
||||
|
||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||
|
||||
private val signoutSessionUseCase = SignoutSessionUseCase(
|
||||
activeSessionHolder = fakeActiveSessionHolder.instance
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `given a device id when signing out with success then success result is returned`() = runTest {
|
||||
// Given
|
||||
val interceptor = givenAuthInterceptor()
|
||||
fakeActiveSessionHolder.fakeSession
|
||||
.fakeCryptoService
|
||||
.givenDeleteDeviceSucceeds(A_DEVICE_ID)
|
||||
|
||||
// When
|
||||
val result = signoutSessionUseCase.execute(A_DEVICE_ID, interceptor)
|
||||
|
||||
// Then
|
||||
result.isSuccess shouldBe true
|
||||
every {
|
||||
fakeActiveSessionHolder.fakeSession
|
||||
.fakeCryptoService
|
||||
.deleteDevice(A_DEVICE_ID, interceptor, any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a device id when signing out with error then failure result is returned`() = runTest {
|
||||
// Given
|
||||
val interceptor = givenAuthInterceptor()
|
||||
val error = mockk<Exception>()
|
||||
fakeActiveSessionHolder.fakeSession
|
||||
.fakeCryptoService
|
||||
.givenDeleteDeviceFailsWithError(A_DEVICE_ID, error)
|
||||
|
||||
// When
|
||||
val result = signoutSessionUseCase.execute(A_DEVICE_ID, interceptor)
|
||||
|
||||
// Then
|
||||
result.isFailure shouldBe true
|
||||
every {
|
||||
fakeActiveSessionHolder.fakeSession
|
||||
.fakeCryptoService
|
||||
.deleteDevice(A_DEVICE_ID, interceptor, any())
|
||||
}
|
||||
}
|
||||
|
||||
private fun givenAuthInterceptor() = mockk<UserInteractiveAuthInterceptor>()
|
||||
}
|
|
@ -67,4 +67,18 @@ class FakeCryptoService(
|
|||
thirdArg<MatrixCallback<Unit>>().onFailure(error)
|
||||
}
|
||||
}
|
||||
|
||||
fun givenDeleteDeviceSucceeds(deviceId: String) {
|
||||
val matrixCallback = slot<MatrixCallback<Unit>>()
|
||||
every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers {
|
||||
thirdArg<MatrixCallback<Unit>>().onSuccess(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun givenDeleteDeviceFailsWithError(deviceId: String, error: Exception) {
|
||||
val matrixCallback = slot<MatrixCallback<Unit>>()
|
||||
every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers {
|
||||
thirdArg<MatrixCallback<Unit>>().onFailure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.test.fakes
|
||||
|
||||
import im.vector.app.features.auth.PendingAuthHandler
|
||||
import io.mockk.mockk
|
||||
import io.mockk.spyk
|
||||
|
||||
class FakePendingAuthHandler {
|
||||
|
||||
val instance = spyk(mockk<PendingAuthHandler>())
|
||||
}
|
|
@ -14,17 +14,17 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.settings.devices.v2
|
||||
package im.vector.app.test.fakes
|
||||
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import javax.inject.Inject
|
||||
import im.vector.app.features.login.ReAuthHelper
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
||||
class IsCurrentSessionUseCase @Inject constructor(
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
) {
|
||||
class FakeReAuthHelper {
|
||||
|
||||
fun execute(deviceId: String): Boolean {
|
||||
val currentDeviceId = activeSessionHolder.getSafeActiveSession()?.sessionParams?.deviceId.orEmpty()
|
||||
return deviceId.isNotEmpty() && deviceId == currentDeviceId
|
||||
val instance = mockk<ReAuthHelper>()
|
||||
|
||||
fun givenStoredPassword(pwd: String?) {
|
||||
every { instance.data } returns pwd
|
||||
}
|
||||
}
|
|
@ -16,7 +16,19 @@
|
|||
|
||||
package im.vector.app.test.fakes
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||
|
||||
class FakeVerificationService : VerificationService by mockk()
|
||||
class FakeVerificationService : VerificationService by mockk() {
|
||||
|
||||
fun givenAddListenerSucceeds() {
|
||||
every { addListener(any()) } just runs
|
||||
}
|
||||
|
||||
fun givenRemoveListenerSucceeds() {
|
||||
every { removeListener(any()) } just runs
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue