mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-16 20:10:04 +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_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_no_inactive_sessions_found">No inactive sessions found.</string>
|
||||||
<string name="device_manager_other_sessions_clear_filter">Clear Filter</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_title">Session details</string>
|
||||||
<string name="device_manager_session_details_description">Application, device, and activity information.</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>
|
<string name="device_manager_session_details_session_name">Session name</string>
|
||||||
|
|
|
@ -41,6 +41,10 @@
|
||||||
<item name="lineHeight">24sp</item>
|
<item name="lineHeight">24sp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Vector.Button.Text.Destructive">
|
||||||
|
<item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayDestructive</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Vector.Button.Text.OnPrimary">
|
<style name="Widget.Vector.Button.Text.OnPrimary">
|
||||||
<item name="colorControlHighlight">?colorOnPrimary</item>
|
<item name="colorControlHighlight">?colorOnPrimary</item>
|
||||||
<item name="materialThemeOverlay">@style/VectorMaterialThemeOverlayOnPrimary</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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 {
|
sealed class SessionOverviewAction : VectorViewModelAction {
|
||||||
object VerifySession : SessionOverviewAction()
|
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
|
package im.vector.app.features.settings.devices.v2.overview
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
@ -27,16 +28,22 @@ import androidx.core.view.isVisible
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.date.VectorDateFormatter
|
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.VectorBaseFragment
|
||||||
import im.vector.app.core.platform.VectorMenuProvider
|
import im.vector.app.core.platform.VectorMenuProvider
|
||||||
import im.vector.app.core.resources.ColorProvider
|
import im.vector.app.core.resources.ColorProvider
|
||||||
import im.vector.app.core.resources.DrawableProvider
|
import im.vector.app.core.resources.DrawableProvider
|
||||||
import im.vector.app.databinding.FragmentSessionOverviewBinding
|
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.crypto.recover.SetupMode
|
||||||
import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,18 +73,7 @@ class SessionOverviewFragment :
|
||||||
observeViewEvents()
|
observeViewEvents()
|
||||||
initSessionInfoView()
|
initSessionInfoView()
|
||||||
initVerifyButton()
|
initVerifyButton()
|
||||||
}
|
initSignoutButton()
|
||||||
|
|
||||||
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 observeViewEvents() {
|
private fun observeViewEvents() {
|
||||||
|
@ -92,10 +88,60 @@ class SessionOverviewFragment :
|
||||||
is SessionOverviewViewEvent.PromptResetSecrets -> {
|
is SessionOverviewViewEvent.PromptResetSecrets -> {
|
||||||
navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET)
|
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() {
|
override fun onDestroyView() {
|
||||||
cleanUpSessionInfoView()
|
cleanUpSessionInfoView()
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
@ -122,16 +168,20 @@ class SessionOverviewFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(viewModel) { state ->
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
updateToolbar(state.isCurrentSession)
|
updateToolbar(state)
|
||||||
updateEntryDetails(state.deviceId)
|
updateEntryDetails(state.deviceId)
|
||||||
updateSessionInfo(state)
|
updateSessionInfo(state)
|
||||||
|
updateLoading(state.isLoading)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateToolbar(isCurrentSession: Boolean) {
|
private fun updateToolbar(viewState: SessionOverviewViewState) {
|
||||||
val titleResId = if (isCurrentSession) R.string.device_manager_current_session_title else R.string.device_manager_session_title
|
if (viewState.deviceInfo is Success) {
|
||||||
(activity as? AppCompatActivity)
|
val titleResId =
|
||||||
?.supportActionBar
|
if (viewState.deviceInfo.invoke().isCurrentDevice) R.string.device_manager_current_session_title else R.string.device_manager_session_title
|
||||||
?.setTitle(titleResId)
|
(activity as? AppCompatActivity)
|
||||||
|
?.supportActionBar
|
||||||
|
?.setTitle(titleResId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateEntryDetails(deviceId: String) {
|
private fun updateEntryDetails(deviceId: String) {
|
||||||
|
@ -143,10 +193,11 @@ class SessionOverviewFragment :
|
||||||
private fun updateSessionInfo(viewState: SessionOverviewViewState) {
|
private fun updateSessionInfo(viewState: SessionOverviewViewState) {
|
||||||
if (viewState.deviceInfo is Success) {
|
if (viewState.deviceInfo is Success) {
|
||||||
views.sessionOverviewInfo.isVisible = true
|
views.sessionOverviewInfo.isVisible = true
|
||||||
val isCurrentSession = viewState.isCurrentSession
|
val deviceInfo = viewState.deviceInfo.invoke()
|
||||||
|
val isCurrentSession = deviceInfo.isCurrentDevice
|
||||||
val infoViewState = SessionInfoViewState(
|
val infoViewState = SessionInfoViewState(
|
||||||
isCurrentSession = isCurrentSession,
|
isCurrentSession = isCurrentSession,
|
||||||
deviceFullInfo = viewState.deviceInfo.invoke(),
|
deviceFullInfo = deviceInfo,
|
||||||
isVerifyButtonVisible = isCurrentSession || viewState.isCurrentSessionTrusted,
|
isVerifyButtonVisible = isCurrentSession || viewState.isCurrentSessionTrusted,
|
||||||
isDetailsButtonVisible = false,
|
isDetailsButtonVisible = false,
|
||||||
isLearnMoreLinkVisible = true,
|
isLearnMoreLinkVisible = true,
|
||||||
|
@ -157,4 +208,45 @@ class SessionOverviewFragment :
|
||||||
views.sessionOverviewInfo.isVisible = false
|
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
|
package im.vector.app.features.settings.devices.v2.overview
|
||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewEvents
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
|
||||||
sealed class SessionOverviewViewEvent : VectorViewEvents {
|
sealed class SessionOverviewViewEvent : VectorViewEvents {
|
||||||
object ShowVerifyCurrentSession : SessionOverviewViewEvent()
|
object ShowVerifyCurrentSession : SessionOverviewViewEvent()
|
||||||
data class ShowVerifyOtherSession(val deviceId: String) : SessionOverviewViewEvent()
|
data class ShowVerifyOtherSession(val deviceId: String) : SessionOverviewViewEvent()
|
||||||
object PromptResetSecrets : 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.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.features.settings.devices.v2.IsCurrentSessionUseCase
|
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 im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
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.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(
|
class SessionOverviewViewModel @AssistedInject constructor(
|
||||||
@Assisted val initialState: SessionOverviewViewState,
|
@Assisted val initialState: SessionOverviewViewState,
|
||||||
private val activeSessionHolder: ActiveSessionHolder,
|
private val stringProvider: StringProvider,
|
||||||
private val isCurrentSessionUseCase: IsCurrentSessionUseCase,
|
|
||||||
private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase,
|
private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase,
|
||||||
private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
|
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()
|
companion object : MavericksViewModelFactory<SessionOverviewViewModel, SessionOverviewViewState> by hiltMavericksViewModelFactory()
|
||||||
|
|
||||||
|
@ -50,17 +71,10 @@ class SessionOverviewViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setState {
|
|
||||||
copy(isCurrentSession = isCurrentSession(deviceId))
|
|
||||||
}
|
|
||||||
observeSessionInfo(initialState.deviceId)
|
observeSessionInfo(initialState.deviceId)
|
||||||
observeCurrentSessionInfo()
|
observeCurrentSessionInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isCurrentSession(deviceId: String): Boolean {
|
|
||||||
return isCurrentSessionUseCase.execute(deviceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeSessionInfo(deviceId: String) {
|
private fun observeSessionInfo(deviceId: String) {
|
||||||
getDeviceFullInfoUseCase.execute(deviceId)
|
getDeviceFullInfoUseCase.execute(deviceId)
|
||||||
.onEach { setState { copy(deviceInfo = Success(it)) } }
|
.onEach { setState { copy(deviceInfo = Success(it)) } }
|
||||||
|
@ -83,11 +97,15 @@ class SessionOverviewViewModel @AssistedInject constructor(
|
||||||
override fun handle(action: SessionOverviewAction) {
|
override fun handle(action: SessionOverviewAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is SessionOverviewAction.VerifySession -> handleVerifySessionAction()
|
is SessionOverviewAction.VerifySession -> handleVerifySessionAction()
|
||||||
|
SessionOverviewAction.SignoutOtherSession -> handleSignoutOtherSession()
|
||||||
|
SessionOverviewAction.SsoAuthDone -> handleSsoAuthDone()
|
||||||
|
is SessionOverviewAction.PasswordAuthDone -> handlePasswordAuthDone(action)
|
||||||
|
SessionOverviewAction.ReAuthCancelled -> handleReAuthCancelled()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleVerifySessionAction() = withState { viewState ->
|
private fun handleVerifySessionAction() = withState { viewState ->
|
||||||
if (viewState.isCurrentSession) {
|
if (viewState.deviceInfo.invoke()?.isCurrentDevice.orFalse()) {
|
||||||
handleVerifyCurrentSession()
|
handleVerifyCurrentSession()
|
||||||
} else {
|
} else {
|
||||||
handleVerifyOtherSession(viewState.deviceId)
|
handleVerifyOtherSession(viewState.deviceId)
|
||||||
|
@ -108,4 +126,76 @@ class SessionOverviewViewModel @AssistedInject constructor(
|
||||||
private fun handleVerifyOtherSession(deviceId: String) {
|
private fun handleVerifyOtherSession(deviceId: String) {
|
||||||
_viewEvents.post(SessionOverviewViewEvent.ShowVerifyOtherSession(deviceId))
|
_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
|
package im.vector.app.features.settings.devices.v2.overview
|
||||||
|
|
||||||
import android.content.Context
|
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.details.SessionDetailsActivity
|
||||||
import im.vector.app.features.settings.devices.v2.rename.RenameSessionActivity
|
import im.vector.app.features.settings.devices.v2.rename.RenameSessionActivity
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -30,4 +31,8 @@ class SessionOverviewViewNavigator @Inject constructor() {
|
||||||
fun goToRenameSession(context: Context, deviceId: String) {
|
fun goToRenameSession(context: Context, deviceId: String) {
|
||||||
context.startActivity(RenameSessionActivity.newIntent(context, deviceId))
|
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(
|
data class SessionOverviewViewState(
|
||||||
val deviceId: String,
|
val deviceId: String,
|
||||||
val isCurrentSession: Boolean = false,
|
|
||||||
val isCurrentSessionTrusted: Boolean = false,
|
val isCurrentSessionTrusted: Boolean = false,
|
||||||
val deviceInfo: Async<DeviceFullInfo> = Uninitialized,
|
val deviceInfo: Async<DeviceFullInfo> = Uninitialized,
|
||||||
|
val isLoading: Boolean = false,
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
constructor(args: SessionOverviewArgs) : this(
|
constructor(args: SessionOverviewArgs) : this(
|
||||||
deviceId = args.deviceId
|
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:sessionOverviewEntryDescription="@string/device_manager_session_details_description"
|
||||||
app:sessionOverviewEntryTitle="@string/device_manager_session_details_title" />
|
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>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
|
@ -70,6 +70,7 @@ class DevicesViewModelTest {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
// Needed for internal usage of Flow<T>.throttleFirst() inside the ViewModel
|
||||||
mockkStatic(SystemClock::class)
|
mockkStatic(SystemClock::class)
|
||||||
every { SystemClock.elapsedRealtime() } returns 1234
|
every { SystemClock.elapsedRealtime() } returns 1234
|
||||||
}
|
}
|
||||||
|
@ -217,8 +218,8 @@ class DevicesViewModelTest {
|
||||||
.fakeSession
|
.fakeSession
|
||||||
.fakeCryptoService
|
.fakeCryptoService
|
||||||
.fakeVerificationService
|
.fakeVerificationService
|
||||||
every { fakeVerificationService.addListener(any()) } just runs
|
fakeVerificationService.givenAddListenerSucceeds()
|
||||||
every { fakeVerificationService.removeListener(any()) } just runs
|
fakeVerificationService.givenRemoveListenerSucceeds()
|
||||||
return fakeVerificationService
|
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
|
package im.vector.app.features.settings.devices.v2.overview
|
||||||
|
|
||||||
|
import android.os.SystemClock
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.test.MvRxTestRule
|
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.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.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
|
||||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
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.test
|
||||||
import im.vector.app.test.testDispatcher
|
import im.vector.app.test.testDispatcher
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
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.verify
|
||||||
|
import io.mockk.verifyAll
|
||||||
import kotlinx.coroutines.flow.flowOf
|
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.Rule
|
||||||
import org.junit.Test
|
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.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_1 = "session-id-1"
|
||||||
private const val A_SESSION_ID_2 = "session-id-2"
|
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 {
|
class SessionOverviewViewModelTest {
|
||||||
|
|
||||||
|
@ -46,29 +73,48 @@ class SessionOverviewViewModelTest {
|
||||||
deviceId = A_SESSION_ID_1
|
deviceId = A_SESSION_ID_1
|
||||||
)
|
)
|
||||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||||
private val isCurrentSessionUseCase = mockk<IsCurrentSessionUseCase>()
|
private val fakeStringProvider = FakeStringProvider()
|
||||||
private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>()
|
private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>()
|
||||||
private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>()
|
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(
|
private fun createViewModel() = SessionOverviewViewModel(
|
||||||
initialState = SessionOverviewViewState(args),
|
initialState = SessionOverviewViewState(args),
|
||||||
activeSessionHolder = fakeActiveSessionHolder.instance,
|
stringProvider = fakeStringProvider.instance,
|
||||||
isCurrentSessionUseCase = isCurrentSessionUseCase,
|
|
||||||
getDeviceFullInfoUseCase = getDeviceFullInfoUseCase,
|
getDeviceFullInfoUseCase = getDeviceFullInfoUseCase,
|
||||||
checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase,
|
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
|
@Test
|
||||||
fun `given the viewModel has been initialized then viewState is updated with session info and current session verification status`() {
|
fun `given the viewModel has been initialized then viewState is updated with session info and current session verification status`() {
|
||||||
// Given
|
// Given
|
||||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||||
val isCurrentSession = true
|
|
||||||
every { isCurrentSessionUseCase.execute(any()) } returns isCurrentSession
|
|
||||||
givenCurrentSessionIsTrusted()
|
givenCurrentSessionIsTrusted()
|
||||||
val expectedState = SessionOverviewViewState(
|
val expectedState = SessionOverviewViewState(
|
||||||
deviceId = A_SESSION_ID_1,
|
deviceId = A_SESSION_ID_1,
|
||||||
isCurrentSession = isCurrentSession,
|
|
||||||
deviceInfo = Success(deviceFullInfo),
|
deviceInfo = Success(deviceFullInfo),
|
||||||
isCurrentSessionTrusted = true,
|
isCurrentSessionTrusted = true,
|
||||||
)
|
)
|
||||||
|
@ -81,7 +127,6 @@ class SessionOverviewViewModelTest {
|
||||||
.assertLatestState { state -> state == expectedState }
|
.assertLatestState { state -> state == expectedState }
|
||||||
.finish()
|
.finish()
|
||||||
verify {
|
verify {
|
||||||
isCurrentSessionUseCase.execute(A_SESSION_ID_1)
|
|
||||||
getDeviceFullInfoUseCase.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`() {
|
fun `given current session can be verified when handling verify current session action then self verification event is posted`() {
|
||||||
// Given
|
// Given
|
||||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||||
|
every { deviceFullInfo.isCurrentDevice } returns true
|
||||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||||
every { isCurrentSessionUseCase.execute(any()) } returns true
|
|
||||||
val verifySessionAction = SessionOverviewAction.VerifySession
|
val verifySessionAction = SessionOverviewAction.VerifySession
|
||||||
coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns true
|
coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns true
|
||||||
givenCurrentSessionIsTrusted()
|
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`() {
|
fun `given current session cannot be verified when handling verify current session action then reset secrets event is posted`() {
|
||||||
// Given
|
// Given
|
||||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||||
|
every { deviceFullInfo.isCurrentDevice } returns true
|
||||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||||
every { isCurrentSessionUseCase.execute(any()) } returns true
|
|
||||||
val verifySessionAction = SessionOverviewAction.VerifySession
|
val verifySessionAction = SessionOverviewAction.VerifySession
|
||||||
coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns false
|
coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns false
|
||||||
givenCurrentSessionIsTrusted()
|
givenCurrentSessionIsTrusted()
|
||||||
|
@ -138,8 +183,8 @@ class SessionOverviewViewModelTest {
|
||||||
fun `given another session when handling verify session action then verify session event is posted`() {
|
fun `given another session when handling verify session action then verify session event is posted`() {
|
||||||
// Given
|
// Given
|
||||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||||
|
every { deviceFullInfo.isCurrentDevice } returns false
|
||||||
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
|
||||||
every { isCurrentSessionUseCase.execute(any()) } returns false
|
|
||||||
val verifySessionAction = SessionOverviewAction.VerifySession
|
val verifySessionAction = SessionOverviewAction.VerifySession
|
||||||
givenCurrentSessionIsTrusted()
|
givenCurrentSessionIsTrusted()
|
||||||
|
|
||||||
|
@ -154,6 +199,248 @@ class SessionOverviewViewModelTest {
|
||||||
.finish()
|
.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() {
|
private fun givenCurrentSessionIsTrusted() {
|
||||||
fakeActiveSessionHolder.fakeSession.givenSessionId(A_SESSION_ID_2)
|
fakeActiveSessionHolder.fakeSession.givenSessionId(A_SESSION_ID_2)
|
||||||
val deviceFullInfo = mockk<DeviceFullInfo>()
|
val deviceFullInfo = mockk<DeviceFullInfo>()
|
||||||
|
|
|
@ -17,12 +17,15 @@
|
||||||
package im.vector.app.features.settings.devices.v2.overview
|
package im.vector.app.features.settings.devices.v2.overview
|
||||||
|
|
||||||
import android.content.Intent
|
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.details.SessionDetailsActivity
|
||||||
import im.vector.app.features.settings.devices.v2.rename.RenameSessionActivity
|
import im.vector.app.features.settings.devices.v2.rename.RenameSessionActivity
|
||||||
import im.vector.app.test.fakes.FakeContext
|
import im.vector.app.test.fakes.FakeContext
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkObject
|
import io.mockk.mockkObject
|
||||||
|
import io.mockk.runs
|
||||||
import io.mockk.unmockkAll
|
import io.mockk.unmockkAll
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import org.junit.After
|
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 {
|
private fun givenIntentForSessionDetails(sessionId: String): Intent {
|
||||||
val intent = mockk<Intent>()
|
val intent = mockk<Intent>()
|
||||||
every { SessionDetailsActivity.newIntent(context.instance, sessionId) } returns 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)
|
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.
|
* 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 im.vector.app.features.login.ReAuthHelper
|
||||||
import javax.inject.Inject
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
class IsCurrentSessionUseCase @Inject constructor(
|
class FakeReAuthHelper {
|
||||||
private val activeSessionHolder: ActiveSessionHolder,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun execute(deviceId: String): Boolean {
|
val instance = mockk<ReAuthHelper>()
|
||||||
val currentDeviceId = activeSessionHolder.getSafeActiveSession()?.sessionParams?.deviceId.orEmpty()
|
|
||||||
return deviceId.isNotEmpty() && deviceId == currentDeviceId
|
fun givenStoredPassword(pwd: String?) {
|
||||||
|
every { instance.data } returns pwd
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -16,7 +16,19 @@
|
||||||
|
|
||||||
package im.vector.app.test.fakes
|
package im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
import io.mockk.runs
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
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…
Add table
Reference in a new issue