From 97cfc7dde47ec3fd00e1cde434ebe707048550a4 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 20 Oct 2022 09:37:12 +0200 Subject: [PATCH 01/25] Adding changelog entry --- changelog.d/7418.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7418.feature diff --git a/changelog.d/7418.feature b/changelog.d/7418.feature new file mode 100644 index 00000000000..b68ef700da8 --- /dev/null +++ b/changelog.d/7418.feature @@ -0,0 +1 @@ +[Session manager] Multi-session signout From f45cc715d16f1ae1b70953e561b47ebb0c4e2c79 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 20 Oct 2022 10:13:34 +0200 Subject: [PATCH 02/25] Adding new menu entry for multi signout --- .../src/main/res/values/strings.xml | 2 ++ .../v2/othersessions/OtherSessionsFragment.kt | 20 +++++++++++++++++++ .../src/main/res/menu/menu_other_sessions.xml | 5 +++++ 3 files changed, 27 insertions(+) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 450eb648499..da62e4c300b 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3345,6 +3345,8 @@ No inactive sessions found. Clear Filter Select sessions + Sign out of these sessions + Sign out Sign out of this session Session details Application, device, and activity information. diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 4f1c8353f5e..7737caa6893 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -25,6 +25,7 @@ import android.view.ViewGroup import androidx.activity.OnBackPressedCallback import androidx.activity.addCallback import androidx.annotation.StringRes +import androidx.core.text.toSpannable import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.args @@ -32,12 +33,14 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.extensions.orEmpty import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK 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.StringProvider +import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.databinding.FragmentOtherSessionsBinding import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet @@ -77,9 +80,26 @@ class OtherSessionsFragment : menu.findItem(R.id.otherSessionsSelectAll).isVisible = isSelectModeEnabled menu.findItem(R.id.otherSessionsDeselectAll).isVisible = isSelectModeEnabled menu.findItem(R.id.otherSessionsSelect).isVisible = !isSelectModeEnabled && state.devices()?.isNotEmpty().orFalse() + val multiSignoutItem = menu.findItem(R.id.otherSessionsMultiSignout) + multiSignoutItem.title = if (isSelectModeEnabled) { + getString(R.string.device_manager_other_sessions_multi_signout_selection).uppercase() + } else { + getString(R.string.device_manager_other_sessions_multi_signout_all) + } + val showAsActionFlag = if (isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_ALWAYS else MenuItem.SHOW_AS_ACTION_NEVER + multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT) + changeTextColorOfDestructiveAction(multiSignoutItem) } } + private fun changeTextColorOfDestructiveAction(menuItem: MenuItem) { + val titleColor = colorProvider.getColorFromAttribute(R.attr.colorError) + val currentTitle = menuItem.title.orEmpty().toString() + menuItem.title = currentTitle + .toSpannable() + .colorizeMatchingText(currentTitle, titleColor) + } + override fun handleMenuItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.otherSessionsSelect -> { diff --git a/vector/src/main/res/menu/menu_other_sessions.xml b/vector/src/main/res/menu/menu_other_sessions.xml index 8339286fe79..d4a75bd0df5 100644 --- a/vector/src/main/res/menu/menu_other_sessions.xml +++ b/vector/src/main/res/menu/menu_other_sessions.xml @@ -9,6 +9,11 @@ android:title="@string/device_manager_other_sessions_select" app:showAsAction="withText|never" /> + + Date: Thu, 20 Oct 2022 16:22:29 +0200 Subject: [PATCH 03/25] Adding overflow menu capability in sessions list header view --- .../stylable_sessions_list_header_view.xml | 1 + .../vector/app/core/extensions/MenuItemExt.kt | 29 +++++++++++++++++++ .../v2/VectorSettingsDevicesFragment.kt | 7 +++++ .../devices/v2/list/SessionsListHeaderView.kt | 16 ++++++++++ .../v2/othersessions/OtherSessionsFragment.kt | 9 ++---- .../res/layout/fragment_settings_devices.xml | 3 +- .../res/layout/view_sessions_list_header.xml | 11 ++++++- .../res/menu/menu_other_sessions_header.xml | 12 ++++++++ 8 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/extensions/MenuItemExt.kt create mode 100644 vector/src/main/res/menu/menu_other_sessions_header.xml diff --git a/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml b/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml index 098ec263fc0..c1a51000b72 100644 --- a/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml +++ b/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml @@ -5,6 +5,7 @@ + diff --git a/vector/src/main/java/im/vector/app/core/extensions/MenuItemExt.kt b/vector/src/main/java/im/vector/app/core/extensions/MenuItemExt.kt new file mode 100644 index 00000000000..7d62a0c357b --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/MenuItemExt.kt @@ -0,0 +1,29 @@ +/* + * 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.core.extensions + +import android.view.MenuItem +import androidx.annotation.ColorInt +import androidx.core.text.toSpannable +import im.vector.app.core.utils.colorizeMatchingText + +fun MenuItem.setTextColor(@ColorInt color: Int) { + val currentTitle = title.orEmpty().toString() + title = currentTitle + .toSpannable() + .colorizeMatchingText(currentTitle, color) +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index 1c348af4f95..d192eef7781 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -30,6 +30,7 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.dialogs.ManuallyVerifyDialog +import im.vector.app.core.extensions.setTextColor import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider @@ -91,6 +92,7 @@ class VectorSettingsDevicesFragment : super.onViewCreated(view, savedInstanceState) initWaitingView() + initOtherSessionsHeaderView() initOtherSessionsView() initSecurityRecommendationsView() initQrLoginView() @@ -131,6 +133,11 @@ class VectorSettingsDevicesFragment : views.waitingView.waitingStatusText.isVisible = true } + private fun initOtherSessionsHeaderView() { + val color = colorProvider.getColorFromAttribute(R.attr.colorError) + views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout).setTextColor(color) + } + private fun initOtherSessionsView() { views.deviceListOtherSessions.callback = this } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt index 0660e7d642d..51408931c7e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt @@ -20,6 +20,9 @@ import android.content.Context import android.content.res.TypedArray import android.util.AttributeSet import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import androidx.appcompat.view.menu.MenuBuilder import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.res.use import androidx.core.view.isVisible @@ -39,6 +42,7 @@ class SessionsListHeaderView @JvmOverloads constructor( this ) + val menu: Menu = binding.sessionsListHeaderMenu.menu var onLearnMoreClickListener: (() -> Unit)? = null init { @@ -50,6 +54,7 @@ class SessionsListHeaderView @JvmOverloads constructor( ).use { setTitle(it) setDescription(it) + setMenu(it) } } @@ -90,4 +95,15 @@ class SessionsListHeaderView @JvmOverloads constructor( onLearnMoreClickListener?.invoke() } } + + private fun setMenu(typedArray: TypedArray) { + val menuResId = typedArray.getResourceId(R.styleable.SessionsListHeaderView_sessionsListHeaderMenu, -1) + if (menuResId == -1) { + binding.sessionsListHeaderMenu.isVisible = false + } else { + binding.sessionsListHeaderMenu.showOverflowMenu() + val menuBuilder = binding.sessionsListHeaderMenu.menu as? MenuBuilder + menuBuilder?.let { MenuInflater(context).inflate(menuResId, it) } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 7737caa6893..2bed0c943bc 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -25,7 +25,6 @@ import android.view.ViewGroup import androidx.activity.OnBackPressedCallback import androidx.activity.addCallback import androidx.annotation.StringRes -import androidx.core.text.toSpannable import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.args @@ -33,14 +32,13 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R -import im.vector.app.core.extensions.orEmpty +import im.vector.app.core.extensions.setTextColor import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK 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.StringProvider -import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.databinding.FragmentOtherSessionsBinding import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet @@ -94,10 +92,7 @@ class OtherSessionsFragment : private fun changeTextColorOfDestructiveAction(menuItem: MenuItem) { val titleColor = colorProvider.getColorFromAttribute(R.attr.colorError) - val currentTitle = menuItem.title.orEmpty().toString() - menuItem.title = currentTitle - .toSpannable() - .colorizeMatchingText(currentTitle, titleColor) + menuItem.setTextColor(titleColor) } override fun handleMenuItemSelected(item: MenuItem): Boolean { diff --git a/vector/src/main/res/layout/fragment_settings_devices.xml b/vector/src/main/res/layout/fragment_settings_devices.xml index 38137b2029d..81347748874 100644 --- a/vector/src/main/res/layout/fragment_settings_devices.xml +++ b/vector/src/main/res/layout/fragment_settings_devices.xml @@ -98,6 +98,7 @@ app:layout_constraintTop_toBottomOf="@id/deviceListDividerCurrentSession" app:sessionsListHeaderDescription="@string/device_manager_sessions_other_description" app:sessionsListHeaderHasLearnMoreLink="false" + app:sessionsListHeaderMenu="@menu/menu_other_sessions_header" app:sessionsListHeaderTitle="@string/device_manager_sessions_other_title" /> diff --git a/vector/src/main/res/layout/view_sessions_list_header.xml b/vector/src/main/res/layout/view_sessions_list_header.xml index 6139ff4815e..9f581a1d03f 100644 --- a/vector/src/main/res/layout/view_sessions_list_header.xml +++ b/vector/src/main/res/layout/view_sessions_list_header.xml @@ -13,7 +13,7 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/layout_horizontal_margin" android:layout_marginTop="20dp" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/sessionsListHeaderMenu" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="Other sessions" /> @@ -29,4 +29,13 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/sessions_list_header_title" tools:text="For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore. Learn More." /> + + diff --git a/vector/src/main/res/menu/menu_other_sessions_header.xml b/vector/src/main/res/menu/menu_other_sessions_header.xml new file mode 100644 index 00000000000..4ab0b7465cd --- /dev/null +++ b/vector/src/main/res/menu/menu_other_sessions_header.xml @@ -0,0 +1,12 @@ + + + + + + From ae4a7283581bacb5b8c9b860d65505adad956538 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 20 Oct 2022 16:45:31 +0200 Subject: [PATCH 04/25] Handling press on multi signout action in other sessions list screen --- .../v2/othersessions/OtherSessionsAction.kt | 1 + .../v2/othersessions/OtherSessionsFragment.kt | 31 +++++++++++++------ .../othersessions/OtherSessionsViewModel.kt | 6 ++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt index 1978708ebfb..33bc8b3f4f9 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt @@ -26,4 +26,5 @@ sealed class OtherSessionsAction : VectorViewModelAction { data class ToggleSelectionForDevice(val deviceId: String) : OtherSessionsAction() object SelectAll : OtherSessionsAction() object DeselectAll : OtherSessionsAction() + object MultiSignout : OtherSessionsAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 2bed0c943bc..8059a75c120 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -78,18 +78,27 @@ class OtherSessionsFragment : menu.findItem(R.id.otherSessionsSelectAll).isVisible = isSelectModeEnabled menu.findItem(R.id.otherSessionsDeselectAll).isVisible = isSelectModeEnabled menu.findItem(R.id.otherSessionsSelect).isVisible = !isSelectModeEnabled && state.devices()?.isNotEmpty().orFalse() - val multiSignoutItem = menu.findItem(R.id.otherSessionsMultiSignout) - multiSignoutItem.title = if (isSelectModeEnabled) { - getString(R.string.device_manager_other_sessions_multi_signout_selection).uppercase() - } else { - getString(R.string.device_manager_other_sessions_multi_signout_all) - } - val showAsActionFlag = if (isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_ALWAYS else MenuItem.SHOW_AS_ACTION_NEVER - multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT) - changeTextColorOfDestructiveAction(multiSignoutItem) + updateMultiSignoutMenuItem(menu, state) } } + private fun updateMultiSignoutMenuItem(menu: Menu, viewState: OtherSessionsViewState) { + val multiSignoutItem = menu.findItem(R.id.otherSessionsMultiSignout) + multiSignoutItem.title = if (viewState.isSelectModeEnabled) { + getString(R.string.device_manager_other_sessions_multi_signout_selection).uppercase() + } else { + getString(R.string.device_manager_other_sessions_multi_signout_all) + } + multiSignoutItem.isVisible = if (viewState.isSelectModeEnabled) { + viewState.devices.invoke()?.any { it.isSelected }.orFalse() + } else { + viewState.devices.invoke()?.isNotEmpty().orFalse() + } + val showAsActionFlag = if (viewState.isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_ALWAYS else MenuItem.SHOW_AS_ACTION_NEVER + multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT) + changeTextColorOfDestructiveAction(multiSignoutItem) + } + private fun changeTextColorOfDestructiveAction(menuItem: MenuItem) { val titleColor = colorProvider.getColorFromAttribute(R.attr.colorError) menuItem.setTextColor(titleColor) @@ -109,6 +118,10 @@ class OtherSessionsFragment : viewModel.handle(OtherSessionsAction.DeselectAll) true } + R.id.otherSessionsMultiSignout -> { + viewModel.handle(OtherSessionsAction.MultiSignout) + true + } else -> false } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index 2cd0c6af667..cac5ce7d3b7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -65,6 +65,7 @@ class OtherSessionsViewModel @AssistedInject constructor( } } + // TODO update unit tests override fun handle(action: OtherSessionsAction) { when (action) { is OtherSessionsAction.FilterDevices -> handleFilterDevices(action) @@ -73,6 +74,7 @@ class OtherSessionsViewModel @AssistedInject constructor( is OtherSessionsAction.ToggleSelectionForDevice -> handleToggleSelectionForDevice(action.deviceId) OtherSessionsAction.DeselectAll -> handleDeselectAll() OtherSessionsAction.SelectAll -> handleSelectAll() + OtherSessionsAction.MultiSignout -> handleMultiSignout() } } @@ -142,4 +144,8 @@ class OtherSessionsViewModel @AssistedInject constructor( ) } } + + private fun handleMultiSignout() { + // TODO call multi signout use case with all or only selected devices depending on the ViewState + } } From 810c93cef9f440efb4c88fa7956b14a1e9f46e1d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 20 Oct 2022 17:41:47 +0200 Subject: [PATCH 05/25] Handling press on multi signout action from header menu in other sessions section --- .../features/settings/devices/v2/DevicesAction.kt | 1 + .../features/settings/devices/v2/DevicesViewModel.kt | 6 ++++++ .../devices/v2/VectorSettingsDevicesFragment.kt | 12 ++++++++++++ .../devices/v2/list/SessionsListHeaderView.kt | 5 +++++ 4 files changed, 24 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt index c7437db44c7..9ecb72a25cd 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt @@ -22,4 +22,5 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo sealed class DevicesAction : VectorViewModelAction { object VerifyCurrentSession : DevicesAction() data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() + object MultiSignoutOtherSessions : DevicesAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index a5405756ebf..8f12bf28b60 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -95,10 +95,12 @@ class DevicesViewModel @AssistedInject constructor( } } + // TODO update unit tests override fun handle(action: DevicesAction) { when (action) { is DevicesAction.VerifyCurrentSession -> handleVerifyCurrentSessionAction() is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction() + DevicesAction.MultiSignoutOtherSessions -> handleMultiSignoutOtherSessions() } } @@ -116,4 +118,8 @@ class DevicesViewModel @AssistedInject constructor( private fun handleMarkAsManuallyVerifiedAction() { // TODO implement when needed } + + private fun handleMultiSignoutOtherSessions() { + // TODO call multi signout use case with all other devices than the current one + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index d192eef7781..f3de06a3247 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -19,9 +19,11 @@ package im.vector.app.features.settings.devices.v2 import android.content.Context import android.os.Bundle import android.view.LayoutInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.ActionMenuView.OnMenuItemClickListener import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel @@ -48,6 +50,7 @@ import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INAC import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState +import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsAction import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject @@ -136,6 +139,15 @@ class VectorSettingsDevicesFragment : private fun initOtherSessionsHeaderView() { val color = colorProvider.getColorFromAttribute(R.attr.colorError) views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout).setTextColor(color) + views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem -> + when(menuItem.itemId) { + R.id.otherSessionsHeaderMultiSignout -> { + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + true + } + else -> false + } + } } private fun initOtherSessionsView() { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt index 51408931c7e..f74d88790c1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt @@ -23,6 +23,7 @@ import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.widget.ActionMenuView.OnMenuItemClickListener import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.res.use import androidx.core.view.isVisible @@ -106,4 +107,8 @@ class SessionsListHeaderView @JvmOverloads constructor( menuBuilder?.let { MenuInflater(context).inflate(menuResId, it) } } } + + fun setOnMenuItemClickListener(listener: OnMenuItemClickListener) { + binding.sessionsListHeaderMenu.setOnMenuItemClickListener(listener) + } } From 7e836c0e97d4551d61a33a1e50efab4526c7bae3 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 24 Oct 2022 13:59:09 +0200 Subject: [PATCH 06/25] Updating the action title to include sessions number --- library/ui-strings/src/main/res/values/strings.xml | 4 ++++ .../devices/v2/VectorSettingsDevicesFragment.kt | 12 ++++++------ .../v2/othersessions/OtherSessionsFragment.kt | 3 ++- vector/src/main/res/menu/menu_other_sessions.xml | 2 +- .../src/main/res/menu/menu_other_sessions_header.xml | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index da62e4c300b..e772748a412 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3347,6 +3347,10 @@ Select sessions Sign out of these sessions Sign out + + Sign out of %1$d session + Sign out of %1$d sessions + Sign out of this session Session details Application, device, and activity information. diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index f3de06a3247..e9778e13688 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -19,11 +19,9 @@ package im.vector.app.features.settings.devices.v2 import android.content.Context import android.os.Bundle import android.view.LayoutInflater -import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.ActionMenuView.OnMenuItemClickListener import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel @@ -50,7 +48,6 @@ import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INAC import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState -import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsAction import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject @@ -137,10 +134,8 @@ class VectorSettingsDevicesFragment : } private fun initOtherSessionsHeaderView() { - val color = colorProvider.getColorFromAttribute(R.attr.colorError) - views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout).setTextColor(color) views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem -> - when(menuItem.itemId) { + when (menuItem.itemId) { R.id.otherSessionsHeaderMultiSignout -> { viewModel.handle(DevicesAction.MultiSignoutOtherSessions) true @@ -290,6 +285,11 @@ class VectorSettingsDevicesFragment : hideOtherSessionsView() } else { views.deviceListHeaderOtherSessions.isVisible = true + val color = colorProvider.getColorFromAttribute(R.attr.colorError) + val multiSignoutItem = views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout) + val nbDevices = otherDevices.size + multiSignoutItem.title = stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices) + multiSignoutItem.setTextColor(color) views.deviceListOtherSessions.isVisible = true views.deviceListOtherSessions.render( devices = otherDevices.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER), diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 8059a75c120..0429c3bbb32 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -87,7 +87,8 @@ class OtherSessionsFragment : multiSignoutItem.title = if (viewState.isSelectModeEnabled) { getString(R.string.device_manager_other_sessions_multi_signout_selection).uppercase() } else { - getString(R.string.device_manager_other_sessions_multi_signout_all) + val nbDevices = viewState.devices()?.size ?: 0 + stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices) } multiSignoutItem.isVisible = if (viewState.isSelectModeEnabled) { viewState.devices.invoke()?.any { it.isSelected }.orFalse() diff --git a/vector/src/main/res/menu/menu_other_sessions.xml b/vector/src/main/res/menu/menu_other_sessions.xml index d4a75bd0df5..7893575dded 100644 --- a/vector/src/main/res/menu/menu_other_sessions.xml +++ b/vector/src/main/res/menu/menu_other_sessions.xml @@ -11,7 +11,7 @@ From bb262f0c4164af94234d2f4157c9e0e99177b57c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 24 Oct 2022 16:12:32 +0200 Subject: [PATCH 07/25] Adding new "delete_devices" request API --- .../sdk/api/session/crypto/CryptoService.kt | 3 ++ .../internal/crypto/DefaultCryptoService.kt | 8 +++- .../sdk/internal/crypto/api/CryptoApi.kt | 12 ++++++ .../crypto/model/rest/DeleteDeviceParams.kt | 5 ++- .../crypto/model/rest/DeleteDevicesParams.kt | 37 +++++++++++++++++++ .../internal/crypto/tasks/DeleteDeviceTask.kt | 21 ++++++++++- .../sdk/internal/network/NetworkConstants.kt | 1 + 7 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index d2aa8020e82..971d04261eb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.crypto import android.content.Context +import androidx.annotation.Size import androidx.lifecycle.LiveData import androidx.paging.PagedList import org.matrix.android.sdk.api.MatrixCallback @@ -55,6 +56,8 @@ interface CryptoService { fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) + fun deleteDevices(@Size(min = 1) deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) + fun getCryptoVersion(context: Context, longFormat: Boolean): String fun isCryptoEnabled(): Boolean diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 9c3e0ba1c58..032d6494215 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -241,9 +241,15 @@ internal class DefaultCryptoService @Inject constructor( .executeBy(taskExecutor) } + // TODO add unit test override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { + deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor, callback) + } + + // TODO add unit test + override fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { deleteDeviceTask - .configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) { + .configureWith(DeleteDeviceTask.Params(deviceIds, userInteractiveAuthInterceptor, null)) { this.executionThread = TaskThread.CRYPTO this.callback = callback } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt index d5a8bdfd7cb..cfe4681bfd5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.api import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams +import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse @@ -136,6 +137,17 @@ internal interface CryptoApi { @Body params: DeleteDeviceParams ) + /** + * Deletes the given devices, and invalidates any access token associated with them. + * Doc: https://spec.matrix.org/v1.4/client-server-api/#post_matrixclientv3delete_devices + * + * @param params the deletion parameters + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_V3 + "delete_devices") + suspend fun deleteDevices( + @Body params: DeleteDevicesParams + ) + /** * Update the device information. * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-devices-deviceid diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt index c26c6107c4e..24dccc4d904 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt @@ -23,6 +23,9 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) internal data class DeleteDeviceParams( + /** + * Additional authentication information for the user-interactive authentication API. + */ @Json(name = "auth") - val auth: Map? = null + val auth: Map? = null, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt new file mode 100644 index 00000000000..19b33b2a691 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class provides the parameter to delete several devices. + */ +@JsonClass(generateAdapter = true) +internal data class DeleteDevicesParams( + /** + * Additional authentication information for the user-interactive authentication API. + */ + @Json(name = "auth") + val auth: Map? = null, + + /** + * Required: The list of device IDs to delete. + */ + @Json(name = "devices") + val deviceIds: List, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index 0a77d33accc..fc6bc9b1bcd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.uia.UiaResult import org.matrix.android.sdk.internal.auth.registration.handleUIA import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams +import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task @@ -30,21 +31,37 @@ import javax.inject.Inject internal interface DeleteDeviceTask : Task { data class Params( - val deviceId: String, + val deviceIds: List, val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?, val userAuthParam: UIABaseAuth? ) } +// TODO add unit tests internal class DefaultDeleteDeviceTask @Inject constructor( private val cryptoApi: CryptoApi, private val globalErrorReceiver: GlobalErrorReceiver ) : DeleteDeviceTask { override suspend fun execute(params: DeleteDeviceTask.Params) { + require(params.deviceIds.isNotEmpty()) + try { executeRequest(globalErrorReceiver) { - cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap())) + val userAuthParam = params.userAuthParam?.asMap() + if (params.deviceIds.size == 1) { + cryptoApi.deleteDevice( + deviceId = params.deviceIds.first(), + DeleteDeviceParams(auth = userAuthParam) + ) + } else { + cryptoApi.deleteDevices( + DeleteDevicesParams( + auth = userAuthParam, + deviceIds = params.deviceIds + ) + ) + } } } catch (throwable: Throwable) { if (params.userInteractiveAuthInterceptor == null || diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt index 5aec7db66cc..4bfda0bf3cd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt @@ -22,6 +22,7 @@ internal object NetworkConstants { const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/" const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/" const val URI_API_PREFIX_PATH_V1 = "$URI_API_PREFIX_PATH/v1/" + const val URI_API_PREFIX_PATH_V3 = "$URI_API_PREFIX_PATH/v3/" const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/" // Media From 1bda54323a586818ae155d68cefd08947e5f80bb Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 24 Oct 2022 16:51:22 +0200 Subject: [PATCH 08/25] Calling signout multi sessions use case in other sessions screen --- .../v2/othersessions/OtherSessionsAction.kt | 6 ++ .../v2/othersessions/OtherSessionsFragment.kt | 51 +++++++++- .../othersessions/OtherSessionsViewEvents.kt | 10 +- .../othersessions/OtherSessionsViewModel.kt | 99 ++++++++++++++++++- .../othersessions/OtherSessionsViewState.kt | 1 + .../v2/signout/SignoutSessionUseCase.kt | 3 + .../v2/signout/SignoutSessionsUseCase.kt | 43 ++++++++ 7 files changed, 207 insertions(+), 6 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt index 33bc8b3f4f9..24d2a08bdcc 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt @@ -20,6 +20,12 @@ import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType sealed class OtherSessionsAction : VectorViewModelAction { + // ReAuth + object SsoAuthDone : OtherSessionsAction() + data class PasswordAuthDone(val password: String) : OtherSessionsAction() + object ReAuthCancelled : OtherSessionsAction() + + // Others data class FilterDevices(val filterType: DeviceManagerFilterType) : OtherSessionsAction() data class EnableSelectMode(val deviceId: String?) : OtherSessionsAction() object DisableSelectMode : OtherSessionsAction() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 0429c3bbb32..ca9334ad08e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2.othersessions +import android.app.Activity import android.os.Bundle import android.view.LayoutInflater import android.view.Menu @@ -32,6 +33,7 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.setTextColor import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK @@ -40,6 +42,7 @@ import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.databinding.FragmentOtherSessionsBinding +import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType @@ -47,6 +50,7 @@ import im.vector.app.features.settings.devices.v2.list.OtherSessionsView import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet import im.vector.app.features.themes.ThemeUtils +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.extensions.orFalse import javax.inject.Inject @@ -158,8 +162,9 @@ class OtherSessionsFragment : private fun observeViewEvents() { viewModel.observeViewEvents { when (it) { - is OtherSessionsViewEvents.Loading -> showLoading(it.message) - is OtherSessionsViewEvents.Failure -> showFailure(it.throwable) + is OtherSessionsViewEvents.SignoutError -> showFailure(it.error) + is OtherSessionsViewEvents.RequestReAuth -> askForReAuthentication(it) + OtherSessionsViewEvents.SignoutSuccess -> enableSelectMode(false) } } } @@ -191,6 +196,7 @@ class OtherSessionsFragment : } override fun invalidate() = withState(viewModel) { state -> + updateLoading(state.isLoading) if (state.devices is Success) { val devices = state.devices.invoke() renderDevices(devices, state.currentFilter) @@ -198,6 +204,14 @@ class OtherSessionsFragment : } } + private fun updateLoading(isLoading: Boolean) { + if (isLoading) { + showLoading(null) + } else { + dismissLoadingDialog() + } + } + private fun updateToolbar(devices: List, isSelectModeEnabled: Boolean) { invalidateOptionsMenu() val title = if (isSelectModeEnabled) { @@ -312,4 +326,37 @@ class OtherSessionsFragment : override fun onViewAllOtherSessionsClicked() { // NOOP. We don't have this button in this screen } + + private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { + LoginFlowTypes.SSO -> { + viewModel.handle(OtherSessionsAction.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(OtherSessionsAction.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(OtherSessionsAction.ReAuthCancelled) + } + } + } else { + viewModel.handle(OtherSessionsAction.ReAuthCancelled) + } + } + + /** + * Launch the re auth activity to get credentials. + */ + private fun askForReAuthentication(reAuthReq: OtherSessionsViewEvents.RequestReAuth) { + ReAuthActivity.newIntent( + requireContext(), + reAuthReq.registrationFlowResponse, + reAuthReq.lastErrorCode, + getString(R.string.devices_delete_dialog_title) + ).let { intent -> + reAuthActivityResultLauncher.launch(intent) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt index 95f9c72b335..55753e35be3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt @@ -17,8 +17,14 @@ package im.vector.app.features.settings.devices.v2.othersessions import im.vector.app.core.platform.VectorViewEvents +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse sealed class OtherSessionsViewEvents : VectorViewEvents { - data class Loading(val message: CharSequence? = null) : OtherSessionsViewEvents() - data class Failure(val throwable: Throwable) : OtherSessionsViewEvents() + data class RequestReAuth( + val registrationFlowResponse: RegistrationFlowResponse, + val lastErrorCode: String? + ) : OtherSessionsViewEvents() + + object SignoutSuccess : OtherSessionsViewEvents() + data class SignoutError(val error: Throwable) : OtherSessionsViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index cac5ce7d3b7..052ec7025d3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -21,19 +21,38 @@ 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.resources.StringProvider +import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase 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.filter.DeviceManagerFilterType +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.SignoutSessionsUseCase import kotlinx.coroutines.Job +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.failure.Failure +import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import timber.log.Timber +import javax.net.ssl.HttpsURLConnection +import kotlin.coroutines.Continuation class OtherSessionsViewModel @AssistedInject constructor( @Assisted private val initialState: OtherSessionsViewState, activeSessionHolder: ActiveSessionHolder, + private val stringProvider: StringProvider, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, + private val signoutSessionsUseCase: SignoutSessionsUseCase, + private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase ) : VectorSessionsListViewModel( initialState, activeSessionHolder, refreshDevicesUseCase @@ -68,6 +87,9 @@ class OtherSessionsViewModel @AssistedInject constructor( // TODO update unit tests override fun handle(action: OtherSessionsAction) { when (action) { + is OtherSessionsAction.PasswordAuthDone -> handlePasswordAuthDone(action) + OtherSessionsAction.ReAuthCancelled -> handleReAuthCancelled() + OtherSessionsAction.SsoAuthDone -> handleSsoAuthDone() is OtherSessionsAction.FilterDevices -> handleFilterDevices(action) OtherSessionsAction.DisableSelectMode -> handleDisableSelectMode() is OtherSessionsAction.EnableSelectMode -> handleEnableSelectMode(action.deviceId) @@ -145,7 +167,80 @@ class OtherSessionsViewModel @AssistedInject constructor( } } - private fun handleMultiSignout() { - // TODO call multi signout use case with all or only selected devices depending on the ViewState + private fun handleMultiSignout() = withState { state -> + viewModelScope.launch { + setLoading(true) + val deviceIds = getDeviceIdsToSignout(state) + if (deviceIds.isEmpty()) { + return@launch + } + val signoutResult = signout(deviceIds) + setLoading(false) + + if (signoutResult.isSuccess) { + onSignoutSuccess() + } else { + when (val failure = signoutResult.exceptionOrNull()) { + null -> onSignoutSuccess() + else -> onSignoutFailure(failure) + } + } + } + } + + private fun getDeviceIdsToSignout(state: OtherSessionsViewState): List { + return if (state.isSelectModeEnabled) { + state.devices()?.filter { it.isSelected }.orEmpty() + } else { + state.devices().orEmpty() + }.mapNotNull { it.deviceInfo.deviceId } + } + + private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + 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(OtherSessionsViewEvents.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)) + } + + private fun setLoading(isLoading: Boolean) { + setState { copy(isLoading = isLoading) } + } + + private fun onSignoutSuccess() { + Timber.d("signout success") + refreshDeviceList() + _viewEvents.post(OtherSessionsViewEvents.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(OtherSessionsViewEvents.SignoutError(Exception(failureMessage))) + } + + private fun handleSsoAuthDone() { + pendingAuthHandler.ssoAuthDone() + } + + private fun handlePasswordAuthDone(action: OtherSessionsAction.PasswordAuthDone) { + pendingAuthHandler.passwordAuthDone(action.password) + } + + private fun handleReAuthCancelled() { + pendingAuthHandler.reAuthCancelled() } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt index 0db3c8cd0e5..c0b50fded8c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt @@ -27,6 +27,7 @@ data class OtherSessionsViewState( val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS, val excludeCurrentDevice: Boolean = false, val isSelectModeEnabled: Boolean = false, + val isLoading: Boolean = false, ) : MavericksState { constructor(args: OtherSessionsArgs) : this(excludeCurrentDevice = args.excludeCurrentDevice) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt index 60ca8e91c61..bc6cff0d433 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt @@ -21,6 +21,9 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.util.awaitCallback import javax.inject.Inject +/** + * Use case to signout a single session. + */ class SignoutSessionUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, ) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt new file mode 100644 index 00000000000..82b03247c4a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt @@ -0,0 +1,43 @@ +/* + * 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 + +/** + * Use case to signout several sessions. + */ +class SignoutSessionsUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + // TODO add unit tests + suspend fun execute(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result { + return deleteDevices(deviceIds, userInteractiveAuthInterceptor) + } + + private suspend fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching { + awaitCallback { matrixCallback -> + activeSessionHolder.getActiveSession() + .cryptoService() + .deleteDevices(deviceIds, userInteractiveAuthInterceptor, matrixCallback) + } + } +} From 0f8e5919daeed2d2642215a99361f8dd63b0f07c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 24 Oct 2022 17:52:13 +0200 Subject: [PATCH 09/25] Calling signout multi sessions use case in main screen for other sessions --- .../settings/devices/v2/DevicesAction.kt | 6 ++ .../settings/devices/v2/DevicesViewEvent.kt | 12 ++- .../settings/devices/v2/DevicesViewModel.kt | 97 ++++++++++++++++++- .../v2/VectorSettingsDevicesFragment.kt | 45 ++++++++- 4 files changed, 150 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt index 9ecb72a25cd..21cbb86e942 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt @@ -20,6 +20,12 @@ import im.vector.app.core.platform.VectorViewModelAction import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo sealed class DevicesAction : VectorViewModelAction { + // ReAuth + object SsoAuthDone : DevicesAction() + data class PasswordAuthDone(val password: String) : DevicesAction() + object ReAuthCancelled : DevicesAction() + + // Others object VerifyCurrentSession : DevicesAction() data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() object MultiSignoutOtherSessions : DevicesAction() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt index c78c20f792f..770ffc25133 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt @@ -17,17 +17,21 @@ package im.vector.app.features.settings.devices.v2 import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsViewEvents import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo sealed class DevicesViewEvent : VectorViewEvents { - data class Loading(val message: CharSequence? = null) : DevicesViewEvent() - data class Failure(val throwable: Throwable) : DevicesViewEvent() - data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DevicesViewEvent() - data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvent() + data class RequestReAuth( + val registrationFlowResponse: RegistrationFlowResponse, + val lastErrorCode: String? + ) : DevicesViewEvent() + data class ShowVerifyDevice(val userId: String, val transactionId: String?) : DevicesViewEvent() object SelfVerification : DevicesViewEvent() data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvent() object PromptResetSecrets : DevicesViewEvent() + object SignoutSuccess : DevicesViewEvent() + data class SignoutError(val error: Throwable) : DevicesViewEvent() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index 8f12bf28b60..abe0e2719ff 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -21,24 +21,42 @@ 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.resources.StringProvider +import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +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.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import kotlinx.coroutines.flow.launchIn 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.uia.DefaultBaseAuth +import timber.log.Timber +import javax.net.ssl.HttpsURLConnection +import kotlin.coroutines.Continuation class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, activeSessionHolder: ActiveSessionHolder, + private val stringProvider: StringProvider, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, + private val signoutSessionsUseCase: SignoutSessionsUseCase, + private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase, ) : VectorSessionsListViewModel(initialState, activeSessionHolder, refreshDevicesUseCase) { @@ -98,6 +116,9 @@ class DevicesViewModel @AssistedInject constructor( // TODO update unit tests override fun handle(action: DevicesAction) { when (action) { + is DevicesAction.PasswordAuthDone -> handlePasswordAuthDone(action) + DevicesAction.ReAuthCancelled -> handleReAuthCancelled() + DevicesAction.SsoAuthDone -> handleSsoAuthDone() is DevicesAction.VerifyCurrentSession -> handleVerifyCurrentSessionAction() is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction() DevicesAction.MultiSignoutOtherSessions -> handleMultiSignoutOtherSessions() @@ -119,7 +140,79 @@ class DevicesViewModel @AssistedInject constructor( // TODO implement when needed } - private fun handleMultiSignoutOtherSessions() { - // TODO call multi signout use case with all other devices than the current one + private fun handleMultiSignoutOtherSessions() = withState { state -> + viewModelScope.launch { + setLoading(true) + val deviceIds = getDeviceIdsOfOtherSessions(state) + if (deviceIds.isEmpty()) { + return@launch + } + val signoutResult = signout(deviceIds) + setLoading(false) + + if (signoutResult.isSuccess) { + onSignoutSuccess() + } else { + when (val failure = signoutResult.exceptionOrNull()) { + null -> onSignoutSuccess() + else -> onSignoutFailure(failure) + } + } + } + } + + private fun getDeviceIdsOfOtherSessions(state: DevicesViewState): List { + val currentDeviceId = state.currentSessionCrossSigningInfo.deviceId + return state.devices() + ?.mapNotNull { fullInfo -> fullInfo.deviceInfo.deviceId.takeUnless { it == currentDeviceId } } + .orEmpty() + } + + private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + 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(DevicesViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)) + } + + private fun setLoading(isLoading: Boolean) { + setState { copy(isLoading = isLoading) } + } + + private fun onSignoutSuccess() { + Timber.d("signout success") + refreshDeviceList() + _viewEvents.post(DevicesViewEvent.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(DevicesViewEvent.SignoutError(Exception(failureMessage))) + } + + private fun handleSsoAuthDone() { + pendingAuthHandler.ssoAuthDone() + } + + private fun handlePasswordAuthDone(action: DevicesAction.PasswordAuthDone) { + pendingAuthHandler.passwordAuthDone(action.password) + } + + private fun handleReAuthCancelled() { + pendingAuthHandler.reAuthCancelled() } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index e9778e13688..4f507b2a3dd 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2 +import android.app.Activity import android.content.Context import android.os.Bundle import android.view.LayoutInflater @@ -30,6 +31,7 @@ import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.dialogs.ManuallyVerifyDialog +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.setTextColor import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider @@ -37,6 +39,7 @@ import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.StringProvider import im.vector.app.databinding.FragmentSettingsDevicesBinding import im.vector.app.features.VectorFeatures +import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.login.qr.QrCodeLoginArgs @@ -48,6 +51,7 @@ import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INAC import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject @@ -102,10 +106,7 @@ class VectorSettingsDevicesFragment : private fun observeViewEvents() { viewModel.observeViewEvents { when (it) { - is DevicesViewEvent.Loading -> showLoading(it.message) - is DevicesViewEvent.Failure -> showFailure(it.throwable) - is DevicesViewEvent.RequestReAuth -> Unit // TODO. Next PR - is DevicesViewEvent.PromptRenameDevice -> Unit // TODO. Next PR + is DevicesViewEvent.RequestReAuth -> askForReAuthentication(it) is DevicesViewEvent.ShowVerifyDevice -> { VerificationBottomSheet.withArgs( roomId = null, @@ -124,6 +125,8 @@ class VectorSettingsDevicesFragment : is DevicesViewEvent.PromptResetSecrets -> { navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET) } + is DevicesViewEvent.SignoutError -> showFailure(it.error) + is DevicesViewEvent.SignoutSuccess -> Unit // do nothing } } } @@ -137,6 +140,7 @@ class VectorSettingsDevicesFragment : views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.otherSessionsHeaderMultiSignout -> { + // TODO ask for confirmation viewModel.handle(DevicesAction.MultiSignoutOtherSessions) true } @@ -366,4 +370,37 @@ class VectorSettingsDevicesFragment : excludeCurrentDevice = true ) } + + private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { + LoginFlowTypes.SSO -> { + viewModel.handle(DevicesAction.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(DevicesAction.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(DevicesAction.ReAuthCancelled) + } + } + } else { + viewModel.handle(DevicesAction.ReAuthCancelled) + } + } + + /** + * Launch the re auth activity to get credentials. + */ + private fun askForReAuthentication(reAuthReq: DevicesViewEvent.RequestReAuth) { + ReAuthActivity.newIntent( + requireContext(), + reAuthReq.registrationFlowResponse, + reAuthReq.lastErrorCode, + getString(R.string.devices_delete_dialog_title) + ).let { intent -> + reAuthActivityResultLauncher.launch(intent) + } + } } From 727c7462df2d910ecfdb911b749345365edf8658 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 10:17:23 +0200 Subject: [PATCH 10/25] Adding confirmation dialog before signout process --- .../v2/VectorSettingsDevicesFragment.kt | 17 ++++++++- .../v2/othersessions/OtherSessionsFragment.kt | 16 +++++++- .../v2/overview/SessionOverviewFragment.kt | 12 ++---- .../BuildConfirmSignoutDialogUseCase.kt | 37 +++++++++++++++++++ 4 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index 4f507b2a3dd..98c7016d299 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -51,6 +51,7 @@ import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INAC import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState +import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject @@ -75,6 +76,8 @@ class VectorSettingsDevicesFragment : @Inject lateinit var stringProvider: StringProvider + @Inject lateinit var buildConfirmSignoutDialogUseCase: BuildConfirmSignoutDialogUseCase + private val viewModel: DevicesViewModel by fragmentViewModel() override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSettingsDevicesBinding { @@ -140,8 +143,7 @@ class VectorSettingsDevicesFragment : views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.otherSessionsHeaderMultiSignout -> { - // TODO ask for confirmation - viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + confirmMultiSignoutOtherSessions() true } else -> false @@ -149,6 +151,17 @@ class VectorSettingsDevicesFragment : } } + private fun confirmMultiSignoutOtherSessions() { + activity?.let { + buildConfirmSignoutDialogUseCase.execute(it, this::multiSignoutOtherSessions) + .show() + } + } + + private fun multiSignoutOtherSessions() { + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + } + private fun initOtherSessionsView() { views.deviceListOtherSessions.callback = this } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index ca9334ad08e..d2bb1d443b0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -49,6 +49,7 @@ import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.list.OtherSessionsView import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet +import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase import im.vector.app.features.themes.ThemeUtils import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.extensions.orFalse @@ -70,6 +71,8 @@ class OtherSessionsFragment : @Inject lateinit var viewNavigator: OtherSessionsViewNavigator + @Inject lateinit var buildConfirmSignoutDialogUseCase: BuildConfirmSignoutDialogUseCase + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentOtherSessionsBinding { return FragmentOtherSessionsBinding.inflate(layoutInflater, container, false) } @@ -124,13 +127,24 @@ class OtherSessionsFragment : true } R.id.otherSessionsMultiSignout -> { - viewModel.handle(OtherSessionsAction.MultiSignout) + confirmMultiSignout() true } else -> false } } + private fun confirmMultiSignout() { + activity?.let { + buildConfirmSignoutDialogUseCase.execute(it, this::multiSignout) + .show() + } + } + + private fun multiSignout() { + viewModel.handle(OtherSessionsAction.MultiSignout) + } + private fun enableSelectMode(isEnabled: Boolean, deviceId: String? = null) { val action = if (isEnabled) OtherSessionsAction.EnableSelectMode(deviceId) else OtherSessionsAction.DisableSelectMode viewModel.handle(action) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index 620372f810d..e149023f226 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -29,7 +29,6 @@ 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 @@ -45,6 +44,7 @@ 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.more.SessionLearnMoreBottomSheet import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus +import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase 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 @@ -69,6 +69,8 @@ class SessionOverviewFragment : @Inject lateinit var stringProvider: StringProvider + @Inject lateinit var buildConfirmSignoutDialogUseCase: BuildConfirmSignoutDialogUseCase + private val viewModel: SessionOverviewViewModel by fragmentViewModel() override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSessionOverviewBinding { @@ -134,13 +136,7 @@ class SessionOverviewFragment : 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) + buildConfirmSignoutDialogUseCase.execute(it, this::signoutSession) .show() } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt new file mode 100644 index 00000000000..9959bd18287 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt @@ -0,0 +1,37 @@ +/* + * 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 android.content.Context +import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R +import javax.inject.Inject + +class BuildConfirmSignoutDialogUseCase @Inject constructor() { + + fun execute(context: Context, onConfirm: () -> Unit): AlertDialog { + return MaterialAlertDialogBuilder(context) + .setTitle(R.string.action_sign_out) + .setMessage(R.string.action_sign_out_confirmation_simple) + .setPositiveButton(R.string.action_sign_out) { _, _ -> + onConfirm() + } + .setNegativeButton(R.string.action_cancel, null) + .create() + } +} From a968ac08c363779d7cba8f3c91060e03ca21b53c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 14:20:38 +0200 Subject: [PATCH 11/25] Adding unit tests for signout sessions use case --- .../v2/signout/SignoutSessionsUseCase.kt | 1 - .../devices/v2/DevicesViewModelTest.kt | 28 +++++-- .../OtherSessionsViewModelTest.kt | 27 ++++-- .../v2/signout/SignoutSessionsUseCaseTest.kt | 83 +++++++++++++++++++ .../app/test/fakes/FakeCryptoService.kt | 14 ++++ 5 files changed, 137 insertions(+), 16 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt index 82b03247c4a..b4fc78043e0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt @@ -28,7 +28,6 @@ class SignoutSessionsUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, ) { - // TODO add unit tests suspend fun execute(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result { return deleteDevices(deviceIds, userInteractiveAuthInterceptor) } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index c5edfb868db..bf06dd73296 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -22,10 +22,14 @@ import com.airbnb.mvrx.test.MavericksTestRule import im.vector.app.core.session.clientinfo.MatrixClientInfoContent import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo import im.vector.app.features.settings.devices.v2.list.DeviceType +import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase 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 @@ -53,21 +57,29 @@ class DevicesViewModelTest { val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher) private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeStringProvider = FakeStringProvider() private val getCurrentSessionCrossSigningInfoUseCase = mockk() private val getDeviceFullInfoListUseCase = mockk() - private val refreshDevicesUseCase = mockk(relaxUnitFun = true) private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() + private val fakeSignoutSessionsUseCase = mockk() + private val fakeInterceptSignoutFlowResponseUseCase = mockk() + private val fakePendingAuthHandler = FakePendingAuthHandler() + private val refreshDevicesUseCase = mockk(relaxUnitFun = true) private fun createViewModel(): DevicesViewModel { return DevicesViewModel( - DevicesViewState(), - fakeActiveSessionHolder.instance, - getCurrentSessionCrossSigningInfoUseCase, - getDeviceFullInfoListUseCase, - refreshDevicesOnCryptoDevicesChangeUseCase, - checkIfCurrentSessionCanBeVerifiedUseCase, - refreshDevicesUseCase, + initialState = DevicesViewState(), + activeSessionHolder = fakeActiveSessionHolder.instance, + stringProvider = fakeStringProvider.instance, + getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase, + getDeviceFullInfoListUseCase = getDeviceFullInfoListUseCase, + refreshDevicesOnCryptoDevicesChangeUseCase = refreshDevicesOnCryptoDevicesChangeUseCase, + checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, + signoutSessionsUseCase = fakeSignoutSessionsUseCase, + interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, + pendingAuthHandler = fakePendingAuthHandler.instance, + refreshDevicesUseCase = refreshDevicesUseCase, ) } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index e7b8eeee9bd..7cf624e5698 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -23,7 +23,11 @@ import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase 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.fixtures.aDeviceFullInfo import im.vector.app.test.test @@ -54,15 +58,24 @@ class OtherSessionsViewModelTest { ) private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeStringProvider = FakeStringProvider() private val fakeGetDeviceFullInfoListUseCase = mockk() private val fakeRefreshDevicesUseCaseUseCase = mockk() - - private fun createViewModel(args: OtherSessionsArgs = defaultArgs) = OtherSessionsViewModel( - initialState = OtherSessionsViewState(args), - activeSessionHolder = fakeActiveSessionHolder.instance, - getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase, - refreshDevicesUseCase = fakeRefreshDevicesUseCaseUseCase, - ) + private val fakeSignoutSessionsUseCase = mockk() + private val fakeInterceptSignoutFlowResponseUseCase = mockk() + private val fakePendingAuthHandler = FakePendingAuthHandler() + + private fun createViewModel(args: OtherSessionsArgs = defaultArgs) = + OtherSessionsViewModel( + initialState = OtherSessionsViewState(args), + stringProvider = fakeStringProvider.instance, + activeSessionHolder = fakeActiveSessionHolder.instance, + getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase, + signoutSessionsUseCase = fakeSignoutSessionsUseCase, + interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, + pendingAuthHandler = fakePendingAuthHandler.instance, + refreshDevicesUseCase = fakeRefreshDevicesUseCaseUseCase, + ) @Before fun setup() { diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt new file mode 100644 index 00000000000..208ce8b3349 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt @@ -0,0 +1,83 @@ +/* + * 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_1 = "device-id-1" +private const val A_DEVICE_ID_2 = "device-id-2" + +class SignoutSessionsUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val signoutSessionsUseCase = SignoutSessionsUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance + ) + + @Test + fun `given a list of device ids when signing out with success then success result is returned`() = runTest { + // Given + val interceptor = givenAuthInterceptor() + val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .givenDeleteDevicesSucceeds(deviceIds) + + // When + val result = signoutSessionsUseCase.execute(deviceIds, interceptor) + + // Then + result.isSuccess shouldBe true + every { + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .deleteDevices(deviceIds, interceptor, any()) + } + } + + @Test + fun `given a list of device ids when signing out with error then failure result is returned`() = runTest { + // Given + val interceptor = givenAuthInterceptor() + val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) + val error = mockk() + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .givenDeleteDevicesFailsWithError(deviceIds, error) + + // When + val result = signoutSessionsUseCase.execute(deviceIds, interceptor) + + // Then + result.isFailure shouldBe true + every { + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .deleteDevices(deviceIds, interceptor, any()) + } + } + + private fun givenAuthInterceptor() = mockk() +} + diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt index e96a58faa0e..5f34c45fa7a 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt @@ -84,5 +84,19 @@ class FakeCryptoService( } } + fun givenDeleteDevicesSucceeds(deviceIds: List) { + val matrixCallback = slot>() + every { deleteDevices(deviceIds, any(), capture(matrixCallback)) } answers { + thirdArg>().onSuccess(Unit) + } + } + + fun givenDeleteDevicesFailsWithError(deviceIds: List, error: Exception) { + val matrixCallback = slot>() + every { deleteDevices(deviceIds, any(), capture(matrixCallback)) } answers { + thirdArg>().onFailure(error) + } + } + override fun getMyDevice() = cryptoDeviceInfo } From 5bcf2ac51ea4ce40b43700e0f03d662985872b5f Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 15:37:13 +0200 Subject: [PATCH 12/25] Adding unit tests for other sessions list view model --- .../othersessions/OtherSessionsViewModel.kt | 1 - .../OtherSessionsViewModelTest.kt | 295 +++++++++++++++++- .../overview/SessionOverviewViewModelTest.kt | 109 +++---- .../test/fakes/FakeSignoutSessionUseCase.kt | 77 +++++ .../test/fakes/FakeSignoutSessionsUseCase.kt | 77 +++++ 5 files changed, 479 insertions(+), 80 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index 052ec7025d3..a26187b7972 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -84,7 +84,6 @@ class OtherSessionsViewModel @AssistedInject constructor( } } - // TODO update unit tests override fun handle(action: OtherSessionsAction) { when (action) { is OtherSessionsAction.PasswordAuthDone -> handlePasswordAuthDone(action) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index 7cf624e5698..28b97ed70d4 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -19,32 +19,43 @@ package im.vector.app.features.settings.devices.v2.othersessions import android.os.SystemClock import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule +import im.vector.app.R import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler +import im.vector.app.test.fakes.FakeSignoutSessionsUseCase import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.fixtures.aDeviceFullInfo import im.vector.app.test.test import im.vector.app.test.testDispatcher import io.mockk.every +import io.mockk.justRun import io.mockk.mockk import io.mockk.mockkStatic 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.failure.Failure +import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import javax.net.ssl.HttpsURLConnection private const val A_TITLE_RES_ID = 1 -private const val A_DEVICE_ID = "device-id" +private const val A_DEVICE_ID_1 = "device-id-1" +private const val A_DEVICE_ID_2 = "device-id-2" +private const val A_PASSWORD = "password" +private const val AUTH_ERROR_MESSAGE = "auth-error-message" +private const val AN_ERROR_MESSAGE = "error-message" class OtherSessionsViewModelTest { @@ -60,18 +71,18 @@ class OtherSessionsViewModelTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakeStringProvider = FakeStringProvider() private val fakeGetDeviceFullInfoListUseCase = mockk() - private val fakeRefreshDevicesUseCaseUseCase = mockk() - private val fakeSignoutSessionsUseCase = mockk() + private val fakeRefreshDevicesUseCaseUseCase = mockk(relaxed = true) + private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() - private fun createViewModel(args: OtherSessionsArgs = defaultArgs) = + private fun createViewModel(viewState: OtherSessionsViewState = OtherSessionsViewState(defaultArgs)) = OtherSessionsViewModel( - initialState = OtherSessionsViewState(args), + initialState = viewState, stringProvider = fakeStringProvider.instance, activeSessionHolder = fakeActiveSessionHolder.instance, getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase, - signoutSessionsUseCase = fakeSignoutSessionsUseCase, + signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = fakeRefreshDevicesUseCaseUseCase, @@ -101,6 +112,39 @@ class OtherSessionsViewModelTest { unmockkAll() } + @Test + fun `given the viewModel when initializing it then verification listener is added`() { + // Given + val fakeVerificationService = givenVerificationService() + val devices = mockk>() + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + + // When + val viewModel = createViewModel() + + // Then + verify { + fakeVerificationService.addListener(viewModel) + } + } + + @Test + fun `given the viewModel when clearing it then verification listener is removed`() { + // Given + val fakeVerificationService = givenVerificationService() + val devices = mockk>() + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + + // When + val viewModel = createViewModel() + viewModel.onCleared() + + // Then + verify { + fakeVerificationService.removeListener(viewModel) + } + } + @Test fun `given the viewModel has been initialized then viewState is updated with devices list`() { // Given @@ -156,7 +200,7 @@ class OtherSessionsViewModelTest { @Test fun `given enable select mode action when handling the action then viewState is updated with correct info`() { // Given - val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID, isSelected = false) + val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) val devices: List = listOf(deviceFullInfo) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val expectedState = OtherSessionsViewState( @@ -169,7 +213,7 @@ class OtherSessionsViewModelTest { // When val viewModel = createViewModel() val viewModelTest = viewModel.test() - viewModel.handle(OtherSessionsAction.EnableSelectMode(A_DEVICE_ID)) + viewModel.handle(OtherSessionsAction.EnableSelectMode(A_DEVICE_ID_1)) // Then viewModelTest @@ -180,8 +224,8 @@ class OtherSessionsViewModelTest { @Test fun `given disable select mode action when handling the action then viewState is updated with correct info`() { // Given - val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true) - val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = true) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val expectedState = OtherSessionsViewState( @@ -205,7 +249,7 @@ class OtherSessionsViewModelTest { @Test fun `given toggle selection for device action when handling the action then viewState is updated with correct info`() { // Given - val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID, isSelected = false) + val deviceFullInfo = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) val devices: List = listOf(deviceFullInfo) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val expectedState = OtherSessionsViewState( @@ -218,7 +262,7 @@ class OtherSessionsViewModelTest { // When val viewModel = createViewModel() val viewModelTest = viewModel.test() - viewModel.handle(OtherSessionsAction.ToggleSelectionForDevice(A_DEVICE_ID)) + viewModel.handle(OtherSessionsAction.ToggleSelectionForDevice(A_DEVICE_ID_1)) // Then viewModelTest @@ -229,8 +273,8 @@ class OtherSessionsViewModelTest { @Test fun `given select all action when handling the action then viewState is updated with correct info`() { // Given - val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID, isSelected = false) - val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val expectedState = OtherSessionsViewState( @@ -254,8 +298,8 @@ class OtherSessionsViewModelTest { @Test fun `given deselect all action when handling the action then viewState is updated with correct info`() { // Given - val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID, isSelected = false) - val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val expectedState = OtherSessionsViewState( @@ -276,6 +320,223 @@ class OtherSessionsViewModelTest { .finish() } + @Test + fun `given no reAuth is needed and in selectMode when handling multiSignout action then signout process is performed`() { + // Given + val isSelectModeEnabled = true + val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + // signout only selected devices + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val expectedViewState = OtherSessionsViewState( + devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), + currentFilter = defaultArgs.defaultFilter, + excludeCurrentDevice = defaultArgs.excludeCurrentDevice, + isSelectModeEnabled = isSelectModeEnabled, + ) + + // When + val viewModel = createViewModel(OtherSessionsViewState(defaultArgs).copy(isSelectModeEnabled = isSelectModeEnabled)) + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.MultiSignout) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is OtherSessionsViewEvents.SignoutSuccess } + .finish() + verify { + fakeRefreshDevicesUseCaseUseCase.execute() + } + } + + @Test + fun `given no reAuth is needed and NOT in selectMode when handling multiSignout action then signout process is performed`() { + // Given + val isSelectModeEnabled = false + val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + // signout all devices + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val expectedViewState = OtherSessionsViewState( + devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), + currentFilter = defaultArgs.defaultFilter, + excludeCurrentDevice = defaultArgs.excludeCurrentDevice, + isSelectModeEnabled = isSelectModeEnabled, + ) + + // When + val viewModel = createViewModel(OtherSessionsViewState(defaultArgs).copy(isSelectModeEnabled = isSelectModeEnabled)) + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.MultiSignout) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is OtherSessionsViewEvents.SignoutSuccess } + .finish() + verify { + fakeRefreshDevicesUseCaseUseCase.execute() + } + } + + @Test + fun `given server error during multiSignout when handling multiSignout action then signout process is performed`() { + // Given + val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) + fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), serverError) + val expectedViewState = OtherSessionsViewState( + devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), + currentFilter = defaultArgs.defaultFilter, + excludeCurrentDevice = defaultArgs.excludeCurrentDevice, + ) + fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.MultiSignout) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error.message == AUTH_ERROR_MESSAGE } + .finish() + } + + @Test + fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() { + // Given + val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + val error = Exception() + fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error) + val expectedViewState = OtherSessionsViewState( + devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), + currentFilter = defaultArgs.defaultFilter, + excludeCurrentDevice = defaultArgs.excludeCurrentDevice, + ) + fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.MultiSignout) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error.message == AN_ERROR_MESSAGE } + .finish() + } + + @Test + fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() { + // Given + val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) + val expectedReAuthEvent = OtherSessionsViewEvents.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.MultiSignout) + + // 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 devices = mockk>() + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + justRun { fakePendingAuthHandler.instance.ssoAuthDone() } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.SsoAuthDone) + + // 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 devices = mockk>() + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + justRun { fakePendingAuthHandler.instance.passwordAuthDone(any()) } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.PasswordAuthDone(A_PASSWORD)) + + // 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 devices = mockk>() + givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) + justRun { fakePendingAuthHandler.instance.reAuthCancelled() } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(OtherSessionsAction.ReAuthCancelled) + + // Then + viewModelTest.finish() + verifyAll { + fakePendingAuthHandler.instance.reAuthCancelled() + } + } + private fun givenGetDeviceFullInfoListReturns( filterType: DeviceManagerFilterType, devices: List, diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index c0ba6ce28bd..289279b8f6e 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -26,11 +26,10 @@ import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus 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.FakeSignoutSessionUseCase import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase import im.vector.app.test.fakes.FakeVerificationService @@ -43,7 +42,6 @@ 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 @@ -53,14 +51,10 @@ 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" @@ -83,10 +77,10 @@ class SessionOverviewViewModelTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakeStringProvider = FakeStringProvider() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() - private val signoutSessionUseCase = mockk() + private val fakeSignoutSessionUseCase = FakeSignoutSessionUseCase() private val interceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() - private val refreshDevicesUseCase = mockk() + private val refreshDevicesUseCase = mockk(relaxed = true) private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase() private val fakeGetNotificationsStatusUseCase = mockk() private val notificationsStatus = NotificationsStatus.ENABLED @@ -96,7 +90,7 @@ class SessionOverviewViewModelTest { stringProvider = fakeStringProvider.instance, getDeviceFullInfoUseCase = getDeviceFullInfoUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, - signoutSessionUseCase = signoutSessionUseCase, + signoutSessionUseCase = fakeSignoutSessionUseCase.instance, interceptSignoutFlowResponseUseCase = interceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, activeSessionHolder = fakeActiveSessionHolder.instance, @@ -115,11 +109,50 @@ class SessionOverviewViewModelTest { every { fakeGetNotificationsStatusUseCase.execute(A_SESSION_ID_1) } returns flowOf(notificationsStatus) } + private fun givenVerificationService(): FakeVerificationService { + val fakeVerificationService = fakeActiveSessionHolder + .fakeSession + .fakeCryptoService + .fakeVerificationService + fakeVerificationService.givenAddListenerSucceeds() + fakeVerificationService.givenRemoveListenerSucceeds() + return fakeVerificationService + } + @After fun tearDown() { unmockkAll() } + @Test + fun `given the viewModel when initializing it then verification listener is added`() { + // Given + val fakeVerificationService = givenVerificationService() + + // When + val viewModel = createViewModel() + + // Then + verify { + fakeVerificationService.addListener(viewModel) + } + } + + @Test + fun `given the viewModel when clearing it then verification listener is removed`() { + // Given + val fakeVerificationService = givenVerificationService() + + // When + val viewModel = createViewModel() + viewModel.onCleared() + + // Then + verify { + fakeVerificationService.removeListener(viewModel) + } + } + @Test fun `given the viewModel has been initialized then pushers are refreshed`() { createViewModel() @@ -223,8 +256,7 @@ class SessionOverviewViewModelTest { val deviceFullInfo = mockk() 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 + fakeSignoutSessionUseCase.givenSignoutSuccess(A_SESSION_ID_1, interceptSignoutFlowResponseUseCase) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedViewState = SessionOverviewViewState( @@ -261,7 +293,7 @@ class SessionOverviewViewModelTest { 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) + fakeSignoutSessionUseCase.givenSignoutError(A_SESSION_ID_1, serverError) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedViewState = SessionOverviewViewState( @@ -296,7 +328,7 @@ class SessionOverviewViewModelTest { every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) val error = Exception() - givenSignoutError(A_SESSION_ID_1, error) + fakeSignoutSessionUseCase.givenSignoutError(A_SESSION_ID_1, error) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedViewState = SessionOverviewViewState( @@ -330,7 +362,7 @@ class SessionOverviewViewModelTest { val deviceFullInfo = mockk() every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) - val reAuthNeeded = givenSignoutReAuthNeeded(A_SESSION_ID_1) + val reAuthNeeded = fakeSignoutSessionUseCase.givenSignoutReAuthNeeded(A_SESSION_ID_1, interceptSignoutFlowResponseUseCase) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) @@ -415,53 +447,6 @@ class SessionOverviewViewModelTest { } } - private fun givenSignoutSuccess(deviceId: String) { - val interceptor = slot() - val flowResponse = mockk() - val errorCode = "errorCode" - val promise = mockk>() - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed - coEvery { signoutSessionUseCase.execute(deviceId, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) - Result.success(Unit) - } - } - - private fun givenSignoutReAuthNeeded(deviceId: String): SignoutSessionResult.ReAuthNeeded { - val interceptor = slot() - val flowResponse = mockk() - every { flowResponse.session } returns A_SESSION_ID_1 - val errorCode = "errorCode" - val promise = mockk>() - 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().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() diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt new file mode 100644 index 00000000000..8a6b101ff6d --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt @@ -0,0 +1,77 @@ +/* + * 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.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 io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +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 kotlin.coroutines.Continuation + +class FakeSignoutSessionUseCase { + + val instance = mockk() + + fun givenSignoutSuccess( + deviceId: String, + interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + ) { + val interceptor = slot() + val flowResponse = mockk() + val errorCode = "errorCode" + val promise = mockk>() + every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed + coEvery { instance.execute(deviceId, capture(interceptor)) } coAnswers { + secondArg().performStage(flowResponse, errorCode, promise) + Result.success(Unit) + } + } + + fun givenSignoutReAuthNeeded( + deviceId: String, + interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + ): SignoutSessionResult.ReAuthNeeded { + val interceptor = slot() + val flowResponse = mockk() + every { flowResponse.session } returns "a-session-id" + val errorCode = "errorCode" + val promise = mockk>() + val reAuthNeeded = SignoutSessionResult.ReAuthNeeded( + pendingAuth = mockk(), + uiaContinuation = promise, + flowResponse = flowResponse, + errCode = errorCode, + ) + every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded + coEvery { instance.execute(deviceId, capture(interceptor)) } coAnswers { + secondArg().performStage(flowResponse, errorCode, promise) + Result.success(Unit) + } + + return reAuthNeeded + } + + fun givenSignoutError(deviceId: String, error: Throwable) { + coEvery { instance.execute(deviceId, any()) } returns Result.failure(error) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt new file mode 100644 index 00000000000..04d05b1d8a4 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt @@ -0,0 +1,77 @@ +/* + * 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.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +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 kotlin.coroutines.Continuation + +class FakeSignoutSessionsUseCase { + + val instance = mockk() + + fun givenSignoutSuccess( + deviceIds: List, + interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + ) { + val interceptor = slot() + val flowResponse = mockk() + val errorCode = "errorCode" + val promise = mockk>() + every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed + coEvery { instance.execute(deviceIds, capture(interceptor)) } coAnswers { + secondArg().performStage(flowResponse, errorCode, promise) + Result.success(Unit) + } + } + + fun givenSignoutReAuthNeeded( + deviceIds: List, + interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, + ): SignoutSessionResult.ReAuthNeeded { + val interceptor = slot() + val flowResponse = mockk() + every { flowResponse.session } returns "a-session-id" + val errorCode = "errorCode" + val promise = mockk>() + val reAuthNeeded = SignoutSessionResult.ReAuthNeeded( + pendingAuth = mockk(), + uiaContinuation = promise, + flowResponse = flowResponse, + errCode = errorCode, + ) + every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded + coEvery { instance.execute(deviceIds, capture(interceptor)) } coAnswers { + secondArg().performStage(flowResponse, errorCode, promise) + Result.success(Unit) + } + + return reAuthNeeded + } + + fun givenSignoutError(deviceIds: List, error: Throwable) { + coEvery { instance.execute(deviceIds, any()) } returns Result.failure(error) + } +} From 880ee4058c2fe50f6fa10edff53c6a892cb88ef9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 16:14:32 +0200 Subject: [PATCH 13/25] Adding unit tests about reAuth actions for devices view model --- .../settings/devices/v2/DevicesViewModel.kt | 1 - .../devices/v2/DevicesViewModelTest.kt | 214 ++++++++++++++---- 2 files changed, 170 insertions(+), 45 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index abe0e2719ff..fe4d0dc838d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -113,7 +113,6 @@ class DevicesViewModel @AssistedInject constructor( } } - // TODO update unit tests override fun handle(action: DevicesAction) { when (action) { is DevicesAction.PasswordAuthDone -> handlePasswordAuthDone(action) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index bf06dd73296..71e9d609b76 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -19,16 +19,17 @@ package im.vector.app.features.settings.devices.v2 import android.os.SystemClock import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule +import im.vector.app.R import im.vector.app.core.session.clientinfo.MatrixClientInfoContent import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo import im.vector.app.features.settings.devices.v2.list.DeviceType import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase -import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler +import im.vector.app.test.fakes.FakeSignoutSessionsUseCase import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test @@ -36,20 +37,32 @@ 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.justRun import io.mockk.mockk import io.mockk.mockkStatic -import io.mockk.runs 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.failure.Failure import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth +import javax.net.ssl.HttpsURLConnection + +private const val A_CURRENT_DEVICE_ID = "current-device-id" +private const val A_DEVICE_ID_1 = "device-id-1" +private const val A_DEVICE_ID_2 = "device-id-2" +private const val A_PASSWORD = "password" +private const val AUTH_ERROR_MESSAGE = "auth-error-message" +private const val AN_ERROR_MESSAGE = "error-message" class DevicesViewModelTest { @@ -60,9 +73,9 @@ class DevicesViewModelTest { private val fakeStringProvider = FakeStringProvider() private val getCurrentSessionCrossSigningInfoUseCase = mockk() private val getDeviceFullInfoListUseCase = mockk() - private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk() + private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk(relaxed = true) private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() - private val fakeSignoutSessionsUseCase = mockk() + private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() private val refreshDevicesUseCase = mockk(relaxUnitFun = true) @@ -76,7 +89,7 @@ class DevicesViewModelTest { getDeviceFullInfoListUseCase = getDeviceFullInfoListUseCase, refreshDevicesOnCryptoDevicesChangeUseCase = refreshDevicesOnCryptoDevicesChangeUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, - signoutSessionsUseCase = fakeSignoutSessionsUseCase, + signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = refreshDevicesUseCase, @@ -88,6 +101,20 @@ class DevicesViewModelTest { // Needed for internal usage of Flow.throttleFirst() inside the ViewModel mockkStatic(SystemClock::class) every { SystemClock.elapsedRealtime() } returns 1234 + + givenVerificationService() + givenCurrentSessionCrossSigningInfo() + givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) + } + + private fun givenVerificationService(): FakeVerificationService { + val fakeVerificationService = fakeActiveSessionHolder + .fakeSession + .fakeCryptoService + .fakeVerificationService + fakeVerificationService.givenAddListenerSucceeds() + fakeVerificationService.givenRemoveListenerSucceeds() + return fakeVerificationService } @After @@ -99,9 +126,6 @@ class DevicesViewModelTest { fun `given the viewModel when initializing it then verification listener is added`() { // Given val fakeVerificationService = givenVerificationService() - givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() // When val viewModel = createViewModel() @@ -116,9 +140,6 @@ class DevicesViewModelTest { fun `given the viewModel when clearing it then verification listener is removed`() { // Given val fakeVerificationService = givenVerificationService() - givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() // When val viewModel = createViewModel() @@ -133,10 +154,7 @@ class DevicesViewModelTest { @Test fun `given the viewModel when initializing it then view state is updated with current session cross signing info`() { // Given - givenVerificationService() val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() // When val viewModelTest = createViewModel().test() @@ -149,10 +167,7 @@ class DevicesViewModelTest { @Test fun `given the viewModel when initializing it then view state is updated with current device full info list`() { // Given - givenVerificationService() - givenCurrentSessionCrossSigningInfo() - val deviceFullInfoList = givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() + val deviceFullInfoList = givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) // When val viewModelTest = createViewModel().test() @@ -168,10 +183,6 @@ class DevicesViewModelTest { @Test fun `given the viewModel when initializing it then devices are refreshed on crypto devices change`() { // Given - givenVerificationService() - givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() // When createViewModel() @@ -183,10 +194,6 @@ class DevicesViewModelTest { @Test fun `given current session can be verified when handling verify current session action then self verification event is posted`() { // Given - givenVerificationService() - givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() val verifyCurrentSessionAction = DevicesAction.VerifyCurrentSession coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns true @@ -207,10 +214,6 @@ class DevicesViewModelTest { @Test fun `given current session cannot be verified when handling verify current session action then reset secrets event is posted`() { // Given - givenVerificationService() - givenCurrentSessionCrossSigningInfo() - givenDeviceFullInfoList() - givenRefreshDevicesOnCryptoDevicesChange() val verifyCurrentSessionAction = DevicesAction.VerifyCurrentSession coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns false @@ -228,18 +231,128 @@ class DevicesViewModelTest { } } - private fun givenVerificationService(): FakeVerificationService { - val fakeVerificationService = fakeActiveSessionHolder - .fakeSession - .fakeCryptoService - .fakeVerificationService - fakeVerificationService.givenAddListenerSucceeds() - fakeVerificationService.givenRemoveListenerSucceeds() - return fakeVerificationService + @Test + fun `given server error during multiSignout when handling multiSignout other sessions action then signout process is performed`() { + // Given + val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) + fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), serverError) + val expectedViewState = givenInitialViewState() + fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is DevicesViewEvent.SignoutError && it.error.message == AUTH_ERROR_MESSAGE } + .finish() + } + + @Test + fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() { + // Given + val error = Exception() + fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error) + val expectedViewState = givenInitialViewState() + fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is DevicesViewEvent.SignoutError && it.error.message == AN_ERROR_MESSAGE } + .finish() + } + + @Test + fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() { + // Given + val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) + val expectedReAuthEvent = DevicesViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + + // 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 + justRun { fakePendingAuthHandler.instance.ssoAuthDone() } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.SsoAuthDone) + + // 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 + justRun { fakePendingAuthHandler.instance.passwordAuthDone(any()) } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.PasswordAuthDone(A_PASSWORD)) + + // 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 + justRun { fakePendingAuthHandler.instance.reAuthCancelled() } + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.ReAuthCancelled) + + // Then + viewModelTest.finish() + verifyAll { + fakePendingAuthHandler.instance.reAuthCancelled() + } } private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo { val currentSessionCrossSigningInfo = mockk() + every { currentSessionCrossSigningInfo.deviceId } returns A_CURRENT_DEVICE_ID every { getCurrentSessionCrossSigningInfoUseCase.execute() } returns flowOf(currentSessionCrossSigningInfo) return currentSessionCrossSigningInfo } @@ -247,14 +360,19 @@ class DevicesViewModelTest { /** * Generate mocked deviceFullInfo list with 1 unverified and inactive + 1 verified and active. */ - private fun givenDeviceFullInfoList(): List { + private fun givenDeviceFullInfoList(deviceId1: String, deviceId2: String): List { val verifiedCryptoDeviceInfo = mockk() every { verifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true) val unverifiedCryptoDeviceInfo = mockk() every { unverifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) + val deviceInfo1 = mockk() + every { deviceInfo1.deviceId } returns deviceId1 + val deviceInfo2 = mockk() + every { deviceInfo2.deviceId } returns deviceId2 + val deviceFullInfo1 = DeviceFullInfo( - deviceInfo = mockk(), + deviceInfo = deviceInfo1, cryptoDeviceInfo = verifiedCryptoDeviceInfo, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, isInactive = false, @@ -263,7 +381,7 @@ class DevicesViewModelTest { matrixClientInfo = MatrixClientInfoContent(), ) val deviceFullInfo2 = DeviceFullInfo( - deviceInfo = mockk(), + deviceInfo = deviceInfo2, cryptoDeviceInfo = unverifiedCryptoDeviceInfo, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, isInactive = true, @@ -277,7 +395,15 @@ class DevicesViewModelTest { return deviceFullInfoList } - private fun givenRefreshDevicesOnCryptoDevicesChange() { - coEvery { refreshDevicesOnCryptoDevicesChangeUseCase.execute() } just runs + private fun givenInitialViewState(): DevicesViewState { + val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo() + val deviceFullInfoList = givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) + return DevicesViewState( + currentSessionCrossSigningInfo = currentSessionCrossSigningInfo, + devices = Success(deviceFullInfoList), + unverifiedSessionsCount = 1, + inactiveSessionsCount = 1, + isLoading = false, + ) } } From a3df90ae3ec70e3e5427b16382855099d3db95b2 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 17:03:13 +0200 Subject: [PATCH 14/25] Adding unit tests about multi signout action for devices view model --- .../devices/v2/DevicesViewModelTest.kt | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 71e9d609b76..7ece9cf8778 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -78,7 +78,7 @@ class DevicesViewModelTest { private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() - private val refreshDevicesUseCase = mockk(relaxUnitFun = true) + private val fakeRefreshDevicesUseCase = mockk(relaxUnitFun = true) private fun createViewModel(): DevicesViewModel { return DevicesViewModel( @@ -92,7 +92,7 @@ class DevicesViewModelTest { signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, - refreshDevicesUseCase = refreshDevicesUseCase, + refreshDevicesUseCase = fakeRefreshDevicesUseCase, ) } @@ -231,12 +231,38 @@ class DevicesViewModelTest { } } + @Test + fun `given no reAuth is needed when handling multiSignout other sessions action then signout process is performed`() { + // Given + val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_CURRENT_DEVICE_ID) + // signout all devices except the current device + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1), fakeInterceptSignoutFlowResponseUseCase) + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(DevicesAction.MultiSignoutOtherSessions) + + // Then + viewModelTest + .assertStatesChanges( + expectedViewState, + { copy(isLoading = true) }, + { copy(isLoading = false) } + ) + .assertEvent { it is DevicesViewEvent.SignoutSuccess } + .finish() + verify { + fakeRefreshDevicesUseCase.execute() + } + } + @Test fun `given server error during multiSignout when handling multiSignout other sessions action then signout process is performed`() { // Given val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), serverError) - val expectedViewState = givenInitialViewState() + val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) // When @@ -260,7 +286,7 @@ class DevicesViewModelTest { // Given val error = Exception() fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error) - val expectedViewState = givenInitialViewState() + val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) // When @@ -395,9 +421,9 @@ class DevicesViewModelTest { return deviceFullInfoList } - private fun givenInitialViewState(): DevicesViewState { + private fun givenInitialViewState(deviceId1: String, deviceId2: String): DevicesViewState { val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo() - val deviceFullInfoList = givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) + val deviceFullInfoList = givenDeviceFullInfoList(deviceId1, deviceId2) return DevicesViewState( currentSessionCrossSigningInfo = currentSessionCrossSigningInfo, devices = Success(deviceFullInfoList), From e0d511a4880fb0a4c4aa305b02b60782dea2af99 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 17:05:01 +0200 Subject: [PATCH 15/25] Fixing a name of a mocked component --- .../v2/othersessions/OtherSessionsViewModelTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index 28b97ed70d4..f282e5ca829 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -71,7 +71,7 @@ class OtherSessionsViewModelTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakeStringProvider = FakeStringProvider() private val fakeGetDeviceFullInfoListUseCase = mockk() - private val fakeRefreshDevicesUseCaseUseCase = mockk(relaxed = true) + private val fakeRefreshDevicesUseCase = mockk(relaxed = true) private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() @@ -85,7 +85,7 @@ class OtherSessionsViewModelTest { signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, - refreshDevicesUseCase = fakeRefreshDevicesUseCaseUseCase, + refreshDevicesUseCase = fakeRefreshDevicesUseCase, ) @Before @@ -352,7 +352,7 @@ class OtherSessionsViewModelTest { .assertEvent { it is OtherSessionsViewEvents.SignoutSuccess } .finish() verify { - fakeRefreshDevicesUseCaseUseCase.execute() + fakeRefreshDevicesUseCase.execute() } } @@ -388,7 +388,7 @@ class OtherSessionsViewModelTest { .assertEvent { it is OtherSessionsViewEvents.SignoutSuccess } .finish() verify { - fakeRefreshDevicesUseCaseUseCase.execute() + fakeRefreshDevicesUseCase.execute() } } From 4b0b335a687e8ea9abe0ce5b2ae219020c03a39d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 17:06:32 +0200 Subject: [PATCH 16/25] Fixing code quality issues --- .../vector/app/features/settings/devices/v2/DevicesViewEvent.kt | 2 -- .../settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt | 1 - 2 files changed, 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt index 770ffc25133..9f5257693ef 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewEvent.kt @@ -17,10 +17,8 @@ package im.vector.app.features.settings.devices.v2 import im.vector.app.core.platform.VectorViewEvents -import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsViewEvents import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo sealed class DevicesViewEvent : VectorViewEvents { data class RequestReAuth( diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt index 208ce8b3349..08a9fa625be 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt @@ -80,4 +80,3 @@ class SignoutSessionsUseCaseTest { private fun givenAuthInterceptor() = mockk() } - From 76e2b6b39f5300944107e62ac3eb5cf27fe54005 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 25 Oct 2022 17:09:51 +0200 Subject: [PATCH 17/25] Removing some TODOs --- .../matrix/android/sdk/internal/crypto/DefaultCryptoService.kt | 2 -- .../android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt | 1 - 2 files changed, 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 032d6494215..7862da1c171 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -241,12 +241,10 @@ internal class DefaultCryptoService @Inject constructor( .executeBy(taskExecutor) } - // TODO add unit test override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor, callback) } - // TODO add unit test override fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { deleteDeviceTask .configureWith(DeleteDeviceTask.Params(deviceIds, userInteractiveAuthInterceptor, null)) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index fc6bc9b1bcd..12b3fbd624c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -37,7 +37,6 @@ internal interface DeleteDeviceTask : Task { ) } -// TODO add unit tests internal class DefaultDeleteDeviceTask @Inject constructor( private val cryptoApi: CryptoApi, private val globalErrorReceiver: GlobalErrorReceiver From db42d1c01cd2b98c0027cc8c315a4d7f1bf0bad3 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 12:31:57 +0200 Subject: [PATCH 18/25] Fix post rebase unit tests --- .../OtherSessionsViewModelTest.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index f282e5ca829..f899e3c657a 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -324,8 +324,8 @@ class OtherSessionsViewModelTest { fun `given no reAuth is needed and in selectMode when handling multiSignout action then signout process is performed`() { // Given val isSelectModeEnabled = true - val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) // signout only selected devices @@ -360,8 +360,8 @@ class OtherSessionsViewModelTest { fun `given no reAuth is needed and NOT in selectMode when handling multiSignout action then signout process is performed`() { // Given val isSelectModeEnabled = false - val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) // signout all devices @@ -395,8 +395,8 @@ class OtherSessionsViewModelTest { @Test fun `given server error during multiSignout when handling multiSignout action then signout process is performed`() { // Given - val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) @@ -427,8 +427,8 @@ class OtherSessionsViewModelTest { @Test fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() { // Given - val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val error = Exception() @@ -459,8 +459,8 @@ class OtherSessionsViewModelTest { @Test fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() { // Given - val deviceFullInfo1 = givenDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = givenDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) + val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) + val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) From ef5aaf752554385428a0b9f0c88e52fd19a1a6dc Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 15:10:31 +0200 Subject: [PATCH 19/25] Fix forbidden usage of AlertDialog --- .../BuildConfirmSignoutDialogUseCase.kt | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt index 9959bd18287..4edfc2febe1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/BuildConfirmSignoutDialogUseCase.kt @@ -17,21 +17,19 @@ package im.vector.app.features.settings.devices.v2.signout import android.content.Context -import androidx.appcompat.app.AlertDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import javax.inject.Inject class BuildConfirmSignoutDialogUseCase @Inject constructor() { - fun execute(context: Context, onConfirm: () -> Unit): AlertDialog { - return MaterialAlertDialogBuilder(context) - .setTitle(R.string.action_sign_out) - .setMessage(R.string.action_sign_out_confirmation_simple) - .setPositiveButton(R.string.action_sign_out) { _, _ -> - onConfirm() - } - .setNegativeButton(R.string.action_cancel, null) - .create() - } + fun execute(context: Context, onConfirm: () -> Unit) = + MaterialAlertDialogBuilder(context) + .setTitle(R.string.action_sign_out) + .setMessage(R.string.action_sign_out_confirmation_simple) + .setPositiveButton(R.string.action_sign_out) { _, _ -> + onConfirm() + } + .setNegativeButton(R.string.action_cancel, null) + .create() } From d2d9da3ef73584e9367ad443dd20851b52e6ed1c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 15:17:37 +0200 Subject: [PATCH 20/25] Exclude the current session from other sessions and security recommendation screens --- .../settings/devices/v2/VectorSettingsDevicesFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index 98c7016d299..3a3c3463fbd 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -173,7 +173,7 @@ class VectorSettingsDevicesFragment : requireActivity(), R.string.device_manager_header_section_security_recommendations_title, DeviceManagerFilterType.UNVERIFIED, - excludeCurrentDevice = false + excludeCurrentDevice = true ) } } @@ -183,7 +183,7 @@ class VectorSettingsDevicesFragment : requireActivity(), R.string.device_manager_header_section_security_recommendations_title, DeviceManagerFilterType.INACTIVE, - excludeCurrentDevice = false + excludeCurrentDevice = true ) } } From 3c7ba85c2604f886def970c5aa6f13656cabe7b1 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 16:03:22 +0200 Subject: [PATCH 21/25] Removing unused string --- library/ui-strings/src/main/res/values/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index e772748a412..cd7cb3f4775 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3345,7 +3345,6 @@ No inactive sessions found. Clear Filter Select sessions - Sign out of these sessions Sign out Sign out of %1$d session From 5515cd379f5f0a76f3efa8e0ccdaeeb864dd01f1 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 26 Oct 2022 16:04:51 +0200 Subject: [PATCH 22/25] Use SHOW_AS_ACTION_IF_ROOM tag --- .../settings/devices/v2/othersessions/OtherSessionsFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index d2bb1d443b0..487531646a5 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -102,7 +102,7 @@ class OtherSessionsFragment : } else { viewState.devices.invoke()?.isNotEmpty().orFalse() } - val showAsActionFlag = if (viewState.isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_ALWAYS else MenuItem.SHOW_AS_ACTION_NEVER + val showAsActionFlag = if (viewState.isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_IF_ROOM else MenuItem.SHOW_AS_ACTION_NEVER multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT) changeTextColorOfDestructiveAction(multiSignoutItem) } From 1d2b8e76a289fcbc3996d16379136521566221e1 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 7 Nov 2022 11:13:23 +0100 Subject: [PATCH 23/25] Adding min size annotation to task params --- .../android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index 12b3fbd624c..549122447e9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto.tasks +import androidx.annotation.Size import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.session.uia.UiaResult @@ -31,7 +32,7 @@ import javax.inject.Inject internal interface DeleteDeviceTask : Task { data class Params( - val deviceIds: List, + @Size(min = 1) val deviceIds: List, val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?, val userAuthParam: UIABaseAuth? ) From 45050e821648b83e62a549abb182ce4ac981429f Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 7 Nov 2022 11:34:04 +0100 Subject: [PATCH 24/25] Removing error formatting from ViewModel --- .../settings/devices/v2/DevicesViewModel.kt | 12 +---- .../othersessions/OtherSessionsViewModel.kt | 12 +---- .../v2/overview/SessionOverviewViewModel.kt | 12 +---- .../devices/v2/DevicesViewModelTest.kt | 35 +------------- .../OtherSessionsViewModelTest.kt | 43 +---------------- .../overview/SessionOverviewViewModelTest.kt | 46 +------------------ 6 files changed, 6 insertions(+), 154 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index fe4d0dc838d..c714645b9a8 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -21,11 +21,9 @@ 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.resources.StringProvider import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase @@ -40,16 +38,13 @@ 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.uia.DefaultBaseAuth import timber.log.Timber -import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, activeSessionHolder: ActiveSessionHolder, - private val stringProvider: StringProvider, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase, @@ -195,12 +190,7 @@ class DevicesViewModel @AssistedInject constructor( 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(DevicesViewEvent.SignoutError(Exception(failureMessage))) + _viewEvents.post(DevicesViewEvent.SignoutError(failure)) } private fun handleSsoAuthDone() { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index a26187b7972..c33490400be 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -21,11 +21,9 @@ 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.resources.StringProvider import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase @@ -39,16 +37,13 @@ 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.failure.Failure import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import timber.log.Timber -import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation class OtherSessionsViewModel @AssistedInject constructor( @Assisted private val initialState: OtherSessionsViewState, activeSessionHolder: ActiveSessionHolder, - private val stringProvider: StringProvider, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val signoutSessionsUseCase: SignoutSessionsUseCase, private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, @@ -223,12 +218,7 @@ class OtherSessionsViewModel @AssistedInject constructor( 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(OtherSessionsViewEvents.SignoutError(Exception(failureMessage))) + _viewEvents.post(OtherSessionsViewEvents.SignoutError(failure)) } private fun handleSsoAuthDone() { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index e6aa7c2747b..59eeaaadb4b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -21,11 +21,9 @@ 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.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 @@ -44,16 +42,13 @@ 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 stringProvider: StringProvider, private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, private val signoutSessionUseCase: SignoutSessionUseCase, @@ -196,12 +191,7 @@ class SessionOverviewViewModel @AssistedInject constructor( 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))) + _viewEvents.post(SessionOverviewViewEvent.SignoutError(failure)) } private fun handleSsoAuthDone() { diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 7ece9cf8778..852fc64fd5c 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -19,7 +19,6 @@ package im.vector.app.features.settings.devices.v2 import android.os.SystemClock import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule -import im.vector.app.R import im.vector.app.core.session.clientinfo.MatrixClientInfoContent import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo import im.vector.app.features.settings.devices.v2.list.DeviceType @@ -30,7 +29,6 @@ import im.vector.app.features.settings.devices.v2.verification.GetCurrentSession import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionsUseCase -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 @@ -49,20 +47,16 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth -import javax.net.ssl.HttpsURLConnection private const val A_CURRENT_DEVICE_ID = "current-device-id" private const val A_DEVICE_ID_1 = "device-id-1" private const val A_DEVICE_ID_2 = "device-id-2" private const val A_PASSWORD = "password" -private const val AUTH_ERROR_MESSAGE = "auth-error-message" -private const val AN_ERROR_MESSAGE = "error-message" class DevicesViewModelTest { @@ -70,7 +64,6 @@ class DevicesViewModelTest { val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher) private val fakeActiveSessionHolder = FakeActiveSessionHolder() - private val fakeStringProvider = FakeStringProvider() private val getCurrentSessionCrossSigningInfoUseCase = mockk() private val getDeviceFullInfoListUseCase = mockk() private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk(relaxed = true) @@ -84,7 +77,6 @@ class DevicesViewModelTest { return DevicesViewModel( initialState = DevicesViewState(), activeSessionHolder = fakeActiveSessionHolder.instance, - stringProvider = fakeStringProvider.instance, getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase, getDeviceFullInfoListUseCase = getDeviceFullInfoListUseCase, refreshDevicesOnCryptoDevicesChangeUseCase = refreshDevicesOnCryptoDevicesChangeUseCase, @@ -257,37 +249,12 @@ class DevicesViewModelTest { } } - @Test - fun `given server error during multiSignout when handling multiSignout other sessions action then signout process is performed`() { - // Given - val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) - fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), serverError) - val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) - fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) - - // When - val viewModel = createViewModel() - val viewModelTest = viewModel.test() - viewModel.handle(DevicesAction.MultiSignoutOtherSessions) - - // Then - viewModelTest - .assertStatesChanges( - expectedViewState, - { copy(isLoading = true) }, - { copy(isLoading = false) } - ) - .assertEvent { it is DevicesViewEvent.SignoutError && it.error.message == AUTH_ERROR_MESSAGE } - .finish() - } - @Test fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() { // Given val error = Exception() fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), error) val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) - fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) // When val viewModel = createViewModel() @@ -301,7 +268,7 @@ class DevicesViewModelTest { { copy(isLoading = true) }, { copy(isLoading = false) } ) - .assertEvent { it is DevicesViewEvent.SignoutError && it.error.message == AN_ERROR_MESSAGE } + .assertEvent { it is DevicesViewEvent.SignoutError && it.error == error } .finish() } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index f899e3c657a..e01d6e058ca 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -19,7 +19,6 @@ package im.vector.app.features.settings.devices.v2.othersessions import android.os.SystemClock import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule -import im.vector.app.R import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase @@ -28,7 +27,6 @@ import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowRe import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionsUseCase -import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.fixtures.aDeviceFullInfo import im.vector.app.test.test @@ -46,16 +44,12 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth -import javax.net.ssl.HttpsURLConnection private const val A_TITLE_RES_ID = 1 private const val A_DEVICE_ID_1 = "device-id-1" private const val A_DEVICE_ID_2 = "device-id-2" private const val A_PASSWORD = "password" -private const val AUTH_ERROR_MESSAGE = "auth-error-message" -private const val AN_ERROR_MESSAGE = "error-message" class OtherSessionsViewModelTest { @@ -69,7 +63,6 @@ class OtherSessionsViewModelTest { ) private val fakeActiveSessionHolder = FakeActiveSessionHolder() - private val fakeStringProvider = FakeStringProvider() private val fakeGetDeviceFullInfoListUseCase = mockk() private val fakeRefreshDevicesUseCase = mockk(relaxed = true) private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() @@ -79,7 +72,6 @@ class OtherSessionsViewModelTest { private fun createViewModel(viewState: OtherSessionsViewState = OtherSessionsViewState(defaultArgs)) = OtherSessionsViewModel( initialState = viewState, - stringProvider = fakeStringProvider.instance, activeSessionHolder = fakeActiveSessionHolder.instance, getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase, signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, @@ -392,38 +384,6 @@ class OtherSessionsViewModelTest { } } - @Test - fun `given server error during multiSignout when handling multiSignout action then signout process is performed`() { - // Given - val deviceFullInfo1 = aDeviceFullInfo(A_DEVICE_ID_1, isSelected = false) - val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) - val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) - givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) - val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) - fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), serverError) - val expectedViewState = OtherSessionsViewState( - devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), - currentFilter = defaultArgs.defaultFilter, - excludeCurrentDevice = defaultArgs.excludeCurrentDevice, - ) - fakeStringProvider.given(R.string.authentication_error, AUTH_ERROR_MESSAGE) - - // When - val viewModel = createViewModel() - val viewModelTest = viewModel.test() - viewModel.handle(OtherSessionsAction.MultiSignout) - - // Then - viewModelTest - .assertStatesChanges( - expectedViewState, - { copy(isLoading = true) }, - { copy(isLoading = false) } - ) - .assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error.message == AUTH_ERROR_MESSAGE } - .finish() - } - @Test fun `given unexpected error during multiSignout when handling multiSignout action then signout process is performed`() { // Given @@ -438,7 +398,6 @@ class OtherSessionsViewModelTest { currentFilter = defaultArgs.defaultFilter, excludeCurrentDevice = defaultArgs.excludeCurrentDevice, ) - fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) // When val viewModel = createViewModel() @@ -452,7 +411,7 @@ class OtherSessionsViewModelTest { { copy(isLoading = true) }, { copy(isLoading = false) } ) - .assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error.message == AN_ERROR_MESSAGE } + .assertEvent { it is OtherSessionsViewEvents.SignoutError && it.error == error } .finish() } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 289279b8f6e..b2ab939bd1a 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -20,7 +20,6 @@ import android.os.SystemClock import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule -import im.vector.app.R import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase @@ -30,7 +29,6 @@ import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSes import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionUseCase -import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test @@ -51,15 +49,11 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test -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 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 { @@ -75,7 +69,6 @@ class SessionOverviewViewModelTest { ) private val getDeviceFullInfoUseCase = mockk(relaxed = true) private val fakeActiveSessionHolder = FakeActiveSessionHolder() - private val fakeStringProvider = FakeStringProvider() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() private val fakeSignoutSessionUseCase = FakeSignoutSessionUseCase() private val interceptSignoutFlowResponseUseCase = mockk() @@ -87,7 +80,6 @@ class SessionOverviewViewModelTest { private fun createViewModel() = SessionOverviewViewModel( initialState = SessionOverviewViewState(args), - stringProvider = fakeStringProvider.instance, getDeviceFullInfoUseCase = getDeviceFullInfoUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, signoutSessionUseCase = fakeSignoutSessionUseCase.instance, @@ -286,41 +278,6 @@ class SessionOverviewViewModelTest { } } - @Test - fun `given another session and server error during signout when handling signout action then signout process is performed`() { - // Given - val deviceFullInfo = mockk() - every { deviceFullInfo.isCurrentDevice } returns false - every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) - val serverError = Failure.OtherServerError(errorBody = "", httpCode = HttpsURLConnection.HTTP_UNAUTHORIZED) - fakeSignoutSessionUseCase.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, - notificationsStatus = notificationsStatus, - ) - 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 @@ -338,7 +295,6 @@ class SessionOverviewViewModelTest { isLoading = false, notificationsStatus = notificationsStatus, ) - fakeStringProvider.given(R.string.matrix_error, AN_ERROR_MESSAGE) // When val viewModel = createViewModel() @@ -352,7 +308,7 @@ class SessionOverviewViewModelTest { { copy(isLoading = true) }, { copy(isLoading = false) } ) - .assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error.message == AN_ERROR_MESSAGE } + .assertEvent { it is SessionOverviewViewEvent.SignoutError && it.error == error } .finish() } From 6d2620815cc3923759b3d17690aaaf15eb1f26d6 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 7 Nov 2022 16:52:41 +0100 Subject: [PATCH 25/25] Moving UI auth interceptor into use case --- .../settings/devices/v2/DevicesViewModel.kt | 27 ++----- .../othersessions/OtherSessionsViewModel.kt | 29 ++----- .../v2/overview/SessionOverviewViewModel.kt | 31 +++----- .../InterceptSignoutFlowResponseUseCase.kt | 7 +- .../v2/signout/SignoutSessionUseCase.kt | 42 ---------- ...sult.kt => SignoutSessionsReAuthNeeded.kt} | 16 ++-- .../v2/signout/SignoutSessionsUseCase.kt | 37 ++++++--- .../devices/v2/DevicesViewModelTest.kt | 4 +- .../OtherSessionsViewModelTest.kt | 9 +-- .../overview/SessionOverviewViewModelTest.kt | 12 +-- ...InterceptSignoutFlowResponseUseCaseTest.kt | 12 +-- .../v2/signout/SignoutSessionUseCaseTest.kt | 79 ------------------- .../v2/signout/SignoutSessionsUseCaseTest.kt | 51 +++++++++--- .../app/test/fakes/FakeCryptoService.kt | 22 ++---- .../test/fakes/FakeSignoutSessionUseCase.kt | 77 ------------------ .../test/fakes/FakeSignoutSessionsUseCase.kt | 38 ++------- 16 files changed, 131 insertions(+), 362 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt rename vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/{SignoutSessionResult.kt => SignoutSessionsReAuthNeeded.kt} (71%) delete mode 100644 vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCaseTest.kt delete mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index c714645b9a8..cd97795b69a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -27,20 +27,16 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType 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.SignoutSessionsReAuthNeeded import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import kotlinx.coroutines.flow.launchIn 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.session.uia.DefaultBaseAuth import timber.log.Timber -import kotlin.coroutines.Continuation class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, @@ -141,16 +137,14 @@ class DevicesViewModel @AssistedInject constructor( if (deviceIds.isEmpty()) { return@launch } - val signoutResult = signout(deviceIds) + val result = signout(deviceIds) setLoading(false) - if (signoutResult.isSuccess) { + val error = result.exceptionOrNull() + if (error == null) { onSignoutSuccess() } else { - when (val failure = signoutResult.exceptionOrNull()) { - null -> onSignoutSuccess() - else -> onSignoutFailure(failure) - } + onSignoutFailure(error) } } } @@ -162,16 +156,9 @@ class DevicesViewModel @AssistedInject constructor( .orEmpty() } - private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) { - is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result) - is SignoutSessionResult.Completed -> Unit - } - } - }) + private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, this::onReAuthNeeded) - private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { + private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) { Timber.d("onReAuthNeeded") pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt index c33490400be..9b4c26ee4ff 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -29,24 +29,18 @@ import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase 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.filter.DeviceManagerFilterType -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.SignoutSessionsReAuthNeeded import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import kotlinx.coroutines.Job 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.session.uia.DefaultBaseAuth import timber.log.Timber -import kotlin.coroutines.Continuation class OtherSessionsViewModel @AssistedInject constructor( @Assisted private val initialState: OtherSessionsViewState, activeSessionHolder: ActiveSessionHolder, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val signoutSessionsUseCase: SignoutSessionsUseCase, - private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase ) : VectorSessionsListViewModel( @@ -168,16 +162,14 @@ class OtherSessionsViewModel @AssistedInject constructor( if (deviceIds.isEmpty()) { return@launch } - val signoutResult = signout(deviceIds) + val result = signout(deviceIds) setLoading(false) - if (signoutResult.isSuccess) { + val error = result.exceptionOrNull() + if (error == null) { onSignoutSuccess() } else { - when (val failure = signoutResult.exceptionOrNull()) { - null -> onSignoutSuccess() - else -> onSignoutFailure(failure) - } + onSignoutFailure(error) } } } @@ -190,16 +182,9 @@ class OtherSessionsViewModel @AssistedInject constructor( }.mapNotNull { it.deviceInfo.deviceId } } - private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) { - is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result) - is SignoutSessionResult.Completed -> Unit - } - } - }) + private suspend fun signout(deviceIds: List) = signoutSessionsUseCase.execute(deviceIds, this::onReAuthNeeded) - private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { + private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) { Timber.d("onReAuthNeeded") pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 59eeaaadb4b..9c4ece7e02f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -30,28 +30,24 @@ import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase 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.signout.SignoutSessionsReAuthNeeded +import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase 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.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import timber.log.Timber -import kotlin.coroutines.Continuation class SessionOverviewViewModel @AssistedInject constructor( @Assisted val initialState: SessionOverviewViewState, private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, - private val signoutSessionUseCase: SignoutSessionUseCase, + private val signoutSessionsUseCase: SignoutSessionsUseCase, private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, private val pendingAuthHandler: PendingAuthHandler, private val activeSessionHolder: ActiveSessionHolder, @@ -149,30 +145,21 @@ class SessionOverviewViewModel @AssistedInject constructor( private fun handleSignoutOtherSession(deviceId: String) { viewModelScope.launch { setLoading(true) - val signoutResult = signout(deviceId) + val result = signout(deviceId) setLoading(false) - if (signoutResult.isSuccess) { + val error = result.exceptionOrNull() + if (error == null) { onSignoutSuccess() } else { - when (val failure = signoutResult.exceptionOrNull()) { - null -> onSignoutSuccess() - else -> onSignoutFailure(failure) - } + onSignoutFailure(error) } } } - private suspend fun signout(deviceId: String) = signoutSessionUseCase.execute(deviceId, object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) { - is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result) - is SignoutSessionResult.Completed -> Unit - } - } - }) + private suspend fun signout(deviceId: String) = signoutSessionsUseCase.execute(listOf(deviceId), this::onReAuthNeeded) - private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { + private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) { Timber.d("onReAuthNeeded") pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt index 43169952723..42ebd7782e3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCase.kt @@ -37,17 +37,16 @@ class InterceptSignoutFlowResponseUseCase @Inject constructor( flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation - ): SignoutSessionResult { + ): SignoutSessionsReAuthNeeded? { 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 + null } else { - SignoutSessionResult.ReAuthNeeded( + SignoutSessionsReAuthNeeded( pendingAuth = DefaultBaseAuth(session = flowResponse.session), uiaContinuation = promise, flowResponse = flowResponse, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt deleted file mode 100644 index bc6cff0d433..00000000000 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCase.kt +++ /dev/null @@ -1,42 +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.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 - -/** - * Use case to signout a single session. - */ -class SignoutSessionUseCase @Inject constructor( - private val activeSessionHolder: ActiveSessionHolder, -) { - - suspend fun execute(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result { - return deleteDevice(deviceId, userInteractiveAuthInterceptor) - } - - private suspend fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching { - awaitCallback { matrixCallback -> - activeSessionHolder.getActiveSession() - .cryptoService() - .deleteDevice(deviceId, userInteractiveAuthInterceptor, matrixCallback) - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsReAuthNeeded.kt similarity index 71% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsReAuthNeeded.kt index fa1fb31b660..56e3d176867 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionResult.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsReAuthNeeded.kt @@ -20,13 +20,9 @@ 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, - val flowResponse: RegistrationFlowResponse, - val errCode: String? - ) : SignoutSessionResult() - - object Completed : SignoutSessionResult() -} +data class SignoutSessionsReAuthNeeded( + val pendingAuth: UIABaseAuth, + val uiaContinuation: Continuation, + val flowResponse: RegistrationFlowResponse, + val errCode: String? +) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt index b4fc78043e0..1cf713a711b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCase.kt @@ -16,27 +16,42 @@ package im.vector.app.features.settings.devices.v2.signout +import androidx.annotation.Size import im.vector.app.core.di.ActiveSessionHolder +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.util.awaitCallback +import timber.log.Timber import javax.inject.Inject +import kotlin.coroutines.Continuation -/** - * Use case to signout several sessions. - */ class SignoutSessionsUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, + private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, ) { - suspend fun execute(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result { - return deleteDevices(deviceIds, userInteractiveAuthInterceptor) - } + suspend fun execute( + @Size(min = 1) deviceIds: List, + onReAuthNeeded: (SignoutSessionsReAuthNeeded) -> Unit, + ): Result = runCatching { + Timber.d("start execute with ${deviceIds.size} deviceIds") - private suspend fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching { - awaitCallback { matrixCallback -> - activeSessionHolder.getActiveSession() - .cryptoService() - .deleteDevices(deviceIds, userInteractiveAuthInterceptor, matrixCallback) + val authInterceptor = object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise) + result?.let(onReAuthNeeded) + } } + + deleteDevices(deviceIds, authInterceptor) + Timber.d("end execute") } + + private suspend fun deleteDevices(deviceIds: List, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = + awaitCallback { matrixCallback -> + activeSessionHolder.getActiveSession() + .cryptoService() + .deleteDevices(deviceIds, userInteractiveAuthInterceptor, matrixCallback) + } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 852fc64fd5c..65da1a9385d 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -228,7 +228,7 @@ class DevicesViewModelTest { // Given val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_CURRENT_DEVICE_ID) // signout all devices except the current device - fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1), fakeInterceptSignoutFlowResponseUseCase) + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1)) // When val viewModel = createViewModel() @@ -275,7 +275,7 @@ class DevicesViewModelTest { @Test fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() { // Given - val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)) val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) val expectedReAuthEvent = DevicesViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index e01d6e058ca..1e8c511c429 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -23,7 +23,6 @@ import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType -import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionsUseCase @@ -66,7 +65,6 @@ class OtherSessionsViewModelTest { private val fakeGetDeviceFullInfoListUseCase = mockk() private val fakeRefreshDevicesUseCase = mockk(relaxed = true) private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() - private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() private fun createViewModel(viewState: OtherSessionsViewState = OtherSessionsViewState(defaultArgs)) = @@ -75,7 +73,6 @@ class OtherSessionsViewModelTest { activeSessionHolder = fakeActiveSessionHolder.instance, getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase, signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, - interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = fakeRefreshDevicesUseCase, ) @@ -321,7 +318,7 @@ class OtherSessionsViewModelTest { val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) // signout only selected devices - fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_2)) val expectedViewState = OtherSessionsViewState( devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), currentFilter = defaultArgs.defaultFilter, @@ -357,7 +354,7 @@ class OtherSessionsViewModelTest { val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) // signout all devices - fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)) val expectedViewState = OtherSessionsViewState( devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), currentFilter = defaultArgs.defaultFilter, @@ -422,7 +419,7 @@ class OtherSessionsViewModelTest { val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val devices: List = listOf(deviceFullInfo1, deviceFullInfo2) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) - val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) + val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)) val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) val expectedReAuthEvent = OtherSessionsViewEvents.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index b2ab939bd1a..f26c818e1dc 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -28,7 +28,7 @@ import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowRe 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.FakeSignoutSessionUseCase +import im.vector.app.test.fakes.FakeSignoutSessionsUseCase import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test @@ -70,7 +70,7 @@ class SessionOverviewViewModelTest { private val getDeviceFullInfoUseCase = mockk(relaxed = true) private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() - private val fakeSignoutSessionUseCase = FakeSignoutSessionUseCase() + private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val interceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() private val refreshDevicesUseCase = mockk(relaxed = true) @@ -82,7 +82,7 @@ class SessionOverviewViewModelTest { initialState = SessionOverviewViewState(args), getDeviceFullInfoUseCase = getDeviceFullInfoUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, - signoutSessionUseCase = fakeSignoutSessionUseCase.instance, + signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, interceptSignoutFlowResponseUseCase = interceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, activeSessionHolder = fakeActiveSessionHolder.instance, @@ -248,7 +248,7 @@ class SessionOverviewViewModelTest { val deviceFullInfo = mockk() every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) - fakeSignoutSessionUseCase.givenSignoutSuccess(A_SESSION_ID_1, interceptSignoutFlowResponseUseCase) + fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_SESSION_ID_1)) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedViewState = SessionOverviewViewState( @@ -285,7 +285,7 @@ class SessionOverviewViewModelTest { every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) val error = Exception() - fakeSignoutSessionUseCase.givenSignoutError(A_SESSION_ID_1, error) + fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_SESSION_ID_1), error) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedViewState = SessionOverviewViewState( @@ -318,7 +318,7 @@ class SessionOverviewViewModelTest { val deviceFullInfo = mockk() every { deviceFullInfo.isCurrentDevice } returns false every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) - val reAuthNeeded = fakeSignoutSessionUseCase.givenSignoutReAuthNeeded(A_SESSION_ID_1, interceptSignoutFlowResponseUseCase) + val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_SESSION_ID_1)) val signoutAction = SessionOverviewAction.SignoutOtherSession givenCurrentSessionIsTrusted() val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCaseTest.kt index 35551ba36e6..cd0575f2a0f 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/InterceptSignoutFlowResponseUseCaseTest.kt @@ -24,8 +24,8 @@ import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.runs import io.mockk.unmockkAll +import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeEqualTo -import org.amshove.kluent.shouldBeInstanceOf import org.junit.After import org.junit.Before import org.junit.Test @@ -63,7 +63,7 @@ class InterceptSignoutFlowResponseUseCaseTest { } @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`() { + fun `given no error and a stored password and a next stage as password when intercepting then promise is resumed and null is returned`() { // Given val registrationFlowResponse = givenNextUncompletedStage(LoginFlowTypes.PASSWORD, A_SESSION_ID) fakeReAuthHelper.givenStoredPassword(A_PASSWORD) @@ -84,7 +84,7 @@ class InterceptSignoutFlowResponseUseCaseTest { ) // Then - result shouldBeInstanceOf (SignoutSessionResult.Completed::class) + result shouldBe null every { promise.resume(expectedAuth) } @@ -97,7 +97,7 @@ class InterceptSignoutFlowResponseUseCaseTest { fakeReAuthHelper.givenStoredPassword(A_PASSWORD) val errorCode = AN_ERROR_CODE val promise = mockk>() - val expectedResult = SignoutSessionResult.ReAuthNeeded( + val expectedResult = SignoutSessionsReAuthNeeded( pendingAuth = DefaultBaseAuth(session = A_SESSION_ID), uiaContinuation = promise, flowResponse = registrationFlowResponse, @@ -122,7 +122,7 @@ class InterceptSignoutFlowResponseUseCaseTest { fakeReAuthHelper.givenStoredPassword(A_PASSWORD) val errorCode: String? = null val promise = mockk>() - val expectedResult = SignoutSessionResult.ReAuthNeeded( + val expectedResult = SignoutSessionsReAuthNeeded( pendingAuth = DefaultBaseAuth(session = A_SESSION_ID), uiaContinuation = promise, flowResponse = registrationFlowResponse, @@ -147,7 +147,7 @@ class InterceptSignoutFlowResponseUseCaseTest { fakeReAuthHelper.givenStoredPassword(null) val errorCode: String? = null val promise = mockk>() - val expectedResult = SignoutSessionResult.ReAuthNeeded( + val expectedResult = SignoutSessionsReAuthNeeded( pendingAuth = DefaultBaseAuth(session = A_SESSION_ID), uiaContinuation = promise, flowResponse = registrationFlowResponse, diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCaseTest.kt deleted file mode 100644 index 5af91c16ce4..00000000000 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionUseCaseTest.kt +++ /dev/null @@ -1,79 +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.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() - 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() -} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt index 08a9fa625be..70d2b4b039d 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/signout/SignoutSessionsUseCaseTest.kt @@ -19,10 +19,10 @@ 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 io.mockk.verify 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_1 = "device-id-1" private const val A_DEVICE_ID_2 = "device-id-2" @@ -30,36 +30,38 @@ private const val A_DEVICE_ID_2 = "device-id-2" class SignoutSessionsUseCaseTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val signoutSessionsUseCase = SignoutSessionsUseCase( - activeSessionHolder = fakeActiveSessionHolder.instance + activeSessionHolder = fakeActiveSessionHolder.instance, + interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, ) @Test fun `given a list of device ids when signing out with success then success result is returned`() = runTest { // Given - val interceptor = givenAuthInterceptor() + val callback = givenOnReAuthCallback() val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) fakeActiveSessionHolder.fakeSession .fakeCryptoService .givenDeleteDevicesSucceeds(deviceIds) // When - val result = signoutSessionsUseCase.execute(deviceIds, interceptor) + val result = signoutSessionsUseCase.execute(deviceIds, callback) // Then result.isSuccess shouldBe true - every { + verify { fakeActiveSessionHolder.fakeSession .fakeCryptoService - .deleteDevices(deviceIds, interceptor, any()) + .deleteDevices(deviceIds, any(), any()) } } @Test fun `given a list of device ids when signing out with error then failure result is returned`() = runTest { // Given - val interceptor = givenAuthInterceptor() + val interceptor = givenOnReAuthCallback() val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) val error = mockk() fakeActiveSessionHolder.fakeSession @@ -71,12 +73,41 @@ class SignoutSessionsUseCaseTest { // Then result.isFailure shouldBe true - every { + verify { + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .deleteDevices(deviceIds, any(), any()) + } + } + + @Test + fun `given a list of device ids when signing out with reAuth needed then callback is called`() = runTest { + // Given + val callback = givenOnReAuthCallback() + val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) + fakeActiveSessionHolder.fakeSession + .fakeCryptoService + .givenDeleteDevicesNeedsUIAuth(deviceIds) + val reAuthNeeded = SignoutSessionsReAuthNeeded( + pendingAuth = mockk(), + uiaContinuation = mockk(), + flowResponse = mockk(), + errCode = "errorCode" + ) + every { fakeInterceptSignoutFlowResponseUseCase.execute(any(), any(), any()) } returns reAuthNeeded + + // When + val result = signoutSessionsUseCase.execute(deviceIds, callback) + + // Then + result.isSuccess shouldBe true + verify { fakeActiveSessionHolder.fakeSession .fakeCryptoService - .deleteDevices(deviceIds, interceptor, any()) + .deleteDevices(deviceIds, any(), any()) + callback(reAuthNeeded) } } - private fun givenAuthInterceptor() = mockk() + private fun givenOnReAuthCallback(): (SignoutSessionsReAuthNeeded) -> Unit = {} } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt index 5f34c45fa7a..b23f018cf58 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt @@ -22,6 +22,7 @@ import io.mockk.every import io.mockk.mockk import io.mockk.slot import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo @@ -70,30 +71,21 @@ class FakeCryptoService( } } - fun givenDeleteDeviceSucceeds(deviceId: String) { - val matrixCallback = slot>() - every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers { + fun givenDeleteDevicesSucceeds(deviceIds: List) { + every { deleteDevices(deviceIds, any(), any()) } answers { thirdArg>().onSuccess(Unit) } } - fun givenDeleteDeviceFailsWithError(deviceId: String, error: Exception) { - val matrixCallback = slot>() - every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers { - thirdArg>().onFailure(error) - } - } - - fun givenDeleteDevicesSucceeds(deviceIds: List) { - val matrixCallback = slot>() - every { deleteDevices(deviceIds, any(), capture(matrixCallback)) } answers { + fun givenDeleteDevicesNeedsUIAuth(deviceIds: List) { + every { deleteDevices(deviceIds, any(), any()) } answers { + secondArg().performStage(mockk(), "", mockk()) thirdArg>().onSuccess(Unit) } } fun givenDeleteDevicesFailsWithError(deviceIds: List, error: Exception) { - val matrixCallback = slot>() - every { deleteDevices(deviceIds, any(), capture(matrixCallback)) } answers { + every { deleteDevices(deviceIds, any(), any()) } answers { thirdArg>().onFailure(error) } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt deleted file mode 100644 index 8a6b101ff6d..00000000000 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionUseCase.kt +++ /dev/null @@ -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.test.fakes - -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 io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -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 kotlin.coroutines.Continuation - -class FakeSignoutSessionUseCase { - - val instance = mockk() - - fun givenSignoutSuccess( - deviceId: String, - interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, - ) { - val interceptor = slot() - val flowResponse = mockk() - val errorCode = "errorCode" - val promise = mockk>() - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed - coEvery { instance.execute(deviceId, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) - Result.success(Unit) - } - } - - fun givenSignoutReAuthNeeded( - deviceId: String, - interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, - ): SignoutSessionResult.ReAuthNeeded { - val interceptor = slot() - val flowResponse = mockk() - every { flowResponse.session } returns "a-session-id" - val errorCode = "errorCode" - val promise = mockk>() - val reAuthNeeded = SignoutSessionResult.ReAuthNeeded( - pendingAuth = mockk(), - uiaContinuation = promise, - flowResponse = flowResponse, - errCode = errorCode, - ) - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded - coEvery { instance.execute(deviceId, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) - Result.success(Unit) - } - - return reAuthNeeded - } - - fun givenSignoutError(deviceId: String, error: Throwable) { - coEvery { instance.execute(deviceId, any()) } returns Result.failure(error) - } -} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt index 04d05b1d8a4..9eb36764750 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSignoutSessionsUseCase.kt @@ -16,55 +16,33 @@ package im.vector.app.test.fakes -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.SignoutSessionsReAuthNeeded import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import io.mockk.coEvery import io.mockk.every import io.mockk.mockk -import io.mockk.slot -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 kotlin.coroutines.Continuation class FakeSignoutSessionsUseCase { val instance = mockk() - fun givenSignoutSuccess( - deviceIds: List, - interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, - ) { - val interceptor = slot() - val flowResponse = mockk() - val errorCode = "errorCode" - val promise = mockk>() - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed - coEvery { instance.execute(deviceIds, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) - Result.success(Unit) - } + fun givenSignoutSuccess(deviceIds: List) { + coEvery { instance.execute(deviceIds, any()) } returns Result.success(Unit) } - fun givenSignoutReAuthNeeded( - deviceIds: List, - interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, - ): SignoutSessionResult.ReAuthNeeded { - val interceptor = slot() + fun givenSignoutReAuthNeeded(deviceIds: List): SignoutSessionsReAuthNeeded { val flowResponse = mockk() every { flowResponse.session } returns "a-session-id" val errorCode = "errorCode" - val promise = mockk>() - val reAuthNeeded = SignoutSessionResult.ReAuthNeeded( + val reAuthNeeded = SignoutSessionsReAuthNeeded( pendingAuth = mockk(), - uiaContinuation = promise, + uiaContinuation = mockk(), flowResponse = flowResponse, errCode = errorCode, ) - every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded - coEvery { instance.execute(deviceIds, capture(interceptor)) } coAnswers { - secondArg().performStage(flowResponse, errorCode, promise) + coEvery { instance.execute(deviceIds, any()) } coAnswers { + secondArg<(SignoutSessionsReAuthNeeded) -> Unit>().invoke(reAuthNeeded) Result.success(Unit) }