diff --git a/changelog.d/7114.wip b/changelog.d/7114.wip new file mode 100644 index 0000000000..79ad705132 --- /dev/null +++ b/changelog.d/7114.wip @@ -0,0 +1 @@ +[Device management] Verify current session diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt index 5b19b7a8d2..135c684e76 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -85,8 +85,7 @@ class VectorSettingsDevicesFragment : ).show(childFragmentManager, "REQPOP") } is DevicesViewEvents.SelfVerification -> { - VerificationBottomSheet.forSelfVerification(it.session) - .show(childFragmentManager, "REQPOP") + navigator.requestSelfSessionVerification(requireActivity()) } is DevicesViewEvents.ShowManuallyVerify -> { ManuallyVerifyDialog.show(requireActivity(), it.cryptoDeviceInfo) { 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 8c7718bfcf..c7437db44c 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,5 +20,6 @@ import im.vector.app.core.platform.VectorViewModelAction import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo sealed class DevicesAction : VectorViewModelAction { + object VerifyCurrentSession : DevicesAction() data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : 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 e83004843d..c78c20f792 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 @@ -18,7 +18,6 @@ package im.vector.app.features.settings.devices.v2 import im.vector.app.core.platform.VectorViewEvents import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse -import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo @@ -28,7 +27,7 @@ sealed class DevicesViewEvent : VectorViewEvents { data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DevicesViewEvent() data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvent() data class ShowVerifyDevice(val userId: String, val transactionId: String?) : DevicesViewEvent() - data class SelfVerification(val session: Session) : DevicesViewEvent() + object SelfVerification : DevicesViewEvent() data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvent() object PromptResetSecrets : 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 9c1b70a7e2..96d169bf02 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 @@ -25,6 +25,7 @@ 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.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -36,6 +37,7 @@ class DevicesViewModel @AssistedInject constructor( private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase, + private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, refreshDevicesUseCase: RefreshDevicesUseCase, ) : VectorSessionsListViewModel(initialState, activeSessionHolder, refreshDevicesUseCase) { @@ -94,10 +96,22 @@ class DevicesViewModel @AssistedInject constructor( override fun handle(action: DevicesAction) { when (action) { + is DevicesAction.VerifyCurrentSession -> handleVerifyCurrentSessionAction() is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction() } } + private fun handleVerifyCurrentSessionAction() { + viewModelScope.launch { + val currentSessionCanBeVerified = checkIfCurrentSessionCanBeVerifiedUseCase.execute() + if (currentSessionCanBeVerified) { + _viewEvents.post(DevicesViewEvent.SelfVerification) + } else { + _viewEvents.post(DevicesViewEvent.PromptResetSecrets) + } + } + } + private fun handleMarkAsManuallyVerifiedAction() { // TODO implement when needed } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/IsCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/IsCurrentSessionUseCase.kt new file mode 100644 index 0000000000..885908120a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/IsCurrentSessionUseCase.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2 + +import im.vector.app.core.di.ActiveSessionHolder +import javax.inject.Inject + +class IsCurrentSessionUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + fun execute(deviceId: String): Boolean { + val currentDeviceId = activeSessionHolder.getSafeActiveSession()?.sessionParams?.deviceId.orEmpty() + return deviceId.isNotEmpty() && deviceId == currentDeviceId + } +} 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 1e91384c3a..7cfa218bf0 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 @@ -101,8 +101,7 @@ class VectorSettingsDevicesFragment : ).show(childFragmentManager, "REQPOP") } is DevicesViewEvent.SelfVerification -> { - VerificationBottomSheet.forSelfVerification(it.session) - .show(childFragmentManager, "REQPOP") + navigator.requestSelfSessionVerification(requireActivity()) } is DevicesViewEvent.ShowManuallyVerify -> { ManuallyVerifyDialog.show(requireActivity(), it.cryptoDeviceInfo) { @@ -227,6 +226,9 @@ class VectorSettingsDevicesFragment : views.deviceListCurrentSession.viewDetailsButton.debouncedClicks { currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) } } + views.deviceListCurrentSession.viewVerifyButton.debouncedClicks { + viewModel.handle(DevicesAction.VerifyCurrentSession) + } } ?: run { hideCurrentSessionView() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt index 555d216dfc..38bc0cb5d8 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt @@ -49,6 +49,7 @@ class SessionInfoView @JvmOverloads constructor( } val viewDetailsButton = views.sessionInfoViewDetailsButton + val viewVerifyButton = views.sessionInfoVerifySessionButton fun render( sessionInfoViewState: SessionInfoViewState, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt index c028c08ec4..1118c5dc5b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt @@ -18,4 +18,6 @@ package im.vector.app.features.settings.devices.v2.overview import im.vector.app.core.platform.VectorViewModelAction -sealed class SessionOverviewAction : VectorViewModelAction +sealed class SessionOverviewAction : VectorViewModelAction { + object VerifySession : SessionOverviewAction() +} 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 3fea7a9316..4c83408fe7 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 @@ -34,6 +34,7 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.databinding.FragmentSessionOverviewBinding +import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState import javax.inject.Inject @@ -61,7 +62,9 @@ class SessionOverviewFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + observeViewEvents() initSessionInfoView() + initVerifyButton() } private fun initSessionInfoView() { @@ -70,6 +73,25 @@ class SessionOverviewFragment : } } + private fun initVerifyButton() { + views.sessionOverviewInfo.viewVerifyButton.debouncedClicks { + viewModel.handle(SessionOverviewAction.VerifySession) + } + } + + private fun observeViewEvents() { + viewModel.observeViewEvents { + when (it) { + is SessionOverviewViewEvent.SelfVerification -> { + navigator.requestSelfSessionVerification(requireActivity()) + } + is SessionOverviewViewEvent.PromptResetSecrets -> { + navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET) + } + } + } + } + override fun onDestroyView() { cleanUpSessionInfoView() super.onDestroyView() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewEvent.kt new file mode 100644 index 0000000000..83d438ecb4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewEvent.kt @@ -0,0 +1,24 @@ +/* + * 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.overview + +import im.vector.app.core.platform.VectorViewEvents + +sealed class SessionOverviewViewEvent : VectorViewEvents { + object SelfVerification : SessionOverviewViewEvent() + object PromptResetSecrets : SessionOverviewViewEvent() +} 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 bdcdc40c56..bcf7542783 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 @@ -23,17 +23,19 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory -import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.settings.devices.v2.IsCurrentSessionUseCase +import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import org.matrix.android.sdk.api.session.Session +import kotlinx.coroutines.launch class SessionOverviewViewModel @AssistedInject constructor( @Assisted val initialState: SessionOverviewViewState, - session: Session, + private val isCurrentSessionUseCase: IsCurrentSessionUseCase, private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase, -) : VectorViewModel(initialState) { + private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, +) : VectorViewModel(initialState) { companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() @@ -43,14 +45,16 @@ class SessionOverviewViewModel @AssistedInject constructor( } init { - val currentDeviceId = session.sessionParams.deviceId.orEmpty() setState { - copy(isCurrentSession = deviceId.isNotEmpty() && deviceId == currentDeviceId) + copy(isCurrentSession = isCurrentSession(deviceId)) } - observeSessionInfo(initialState.deviceId) } + private fun isCurrentSession(deviceId: String): Boolean { + return isCurrentSessionUseCase.execute(deviceId) + } + private fun observeSessionInfo(deviceId: String) { getDeviceFullInfoUseCase.execute(deviceId) .onEach { setState { copy(deviceInfo = Success(it)) } } @@ -58,6 +62,25 @@ class SessionOverviewViewModel @AssistedInject constructor( } override fun handle(action: SessionOverviewAction) { - TODO("Implement when adding the first action") + when (action) { + is SessionOverviewAction.VerifySession -> handleVerifySessionAction() + } + } + + private fun handleVerifySessionAction() = withState { viewState -> + if (isCurrentSession(viewState.deviceId)) { + handleVerifyCurrentSession() + } + } + + private fun handleVerifyCurrentSession() { + viewModelScope.launch { + val currentSessionCanBeVerified = checkIfCurrentSessionCanBeVerifiedUseCase.execute() + if (currentSessionCanBeVerified) { + _viewEvents.post(SessionOverviewViewEvent.SelfVerification) + } else { + _viewEvents.post(SessionOverviewViewEvent.PromptResetSecrets) + } + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/CheckIfCurrentSessionCanBeVerifiedUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/CheckIfCurrentSessionCanBeVerifiedUseCase.kt new file mode 100644 index 0000000000..3fdef1a98f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/verification/CheckIfCurrentSessionCanBeVerifiedUseCase.kt @@ -0,0 +1,47 @@ +/* + * 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.verification + +import im.vector.app.core.di.ActiveSessionHolder +import kotlinx.coroutines.flow.firstOrNull +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.flow.flow +import timber.log.Timber +import javax.inject.Inject + +class CheckIfCurrentSessionCanBeVerifiedUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + suspend fun execute(): Boolean { + val session = activeSessionHolder.getSafeActiveSession() + val cryptoSessionsCount = session?.flow() + ?.liveUserCryptoDevices(session.myUserId) + ?.firstOrNull() + ?.size + ?: 0 + val hasOtherSessions = cryptoSessionsCount > 1 + + val isRecoverySetup = session + ?.sharedSecretStorageService() + ?.isRecoverySetup() + .orFalse() + + Timber.d("hasOtherSessions=$hasOtherSessions (otherSessionsCount=$cryptoSessionsCount), isRecoverySetup=$isRecoverySetup") + return hasOtherSessions || isRecoverySetup + } +} 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 351d6b8eb0..0e04c2ab51 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 @@ -18,6 +18,7 @@ package im.vector.app.features.settings.devices.v2 import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MvRxTestRule +import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test @@ -45,6 +46,7 @@ class DevicesViewModelTest { private val getDeviceFullInfoListUseCase = mockk() private val refreshDevicesUseCase = mockk() private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk() + private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() private fun createViewModel(): DevicesViewModel { return DevicesViewModel( @@ -53,6 +55,7 @@ class DevicesViewModelTest { getCurrentSessionCrossSigningInfoUseCase, getDeviceFullInfoListUseCase, refreshDevicesOnCryptoDevicesChangeUseCase, + checkIfCurrentSessionCanBeVerifiedUseCase, refreshDevicesUseCase, ) } @@ -142,6 +145,54 @@ class DevicesViewModelTest { coVerify { refreshDevicesOnCryptoDevicesChangeUseCase.execute() } } + @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 + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(verifyCurrentSessionAction) + + // Then + viewModelTest + .assertEvent { it is DevicesViewEvent.SelfVerification } + .finish() + coVerify { + checkIfCurrentSessionCanBeVerifiedUseCase.execute() + } + } + + @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 + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(verifyCurrentSessionAction) + + // Then + viewModelTest + .assertEvent { it is DevicesViewEvent.PromptResetSecrets } + .finish() + coVerify { + checkIfCurrentSessionCanBeVerifiedUseCase.execute() + } + } + private fun givenVerificationService(): FakeVerificationService { val fakeVerificationService = fakeActiveSessionHolder .fakeSession diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/IsCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/IsCurrentSessionUseCaseTest.kt new file mode 100644 index 0000000000..25cd150b21 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/IsCurrentSessionUseCaseTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devices.v2 + +import im.vector.app.test.fakes.FakeActiveSessionHolder +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.amshove.kluent.shouldBe +import org.junit.Test +import org.matrix.android.sdk.api.auth.data.SessionParams + +private const val A_SESSION_ID_1 = "session-id-1" +private const val A_SESSION_ID_2 = "session-id-2" + +class IsCurrentSessionUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val isCurrentSessionUseCase = IsCurrentSessionUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + ) + + @Test + fun `given the session id of the current session when checking if id is current session then result is true`() { + // Given + val sessionParams = givenIdForCurrentSession(A_SESSION_ID_1) + + // When + val result = isCurrentSessionUseCase.execute(A_SESSION_ID_1) + + // Then + result shouldBe true + verify { sessionParams.deviceId } + } + + @Test + fun `given a session id different from the current session id when checking if id is current session then result is false`() { + // Given + val sessionParams = givenIdForCurrentSession(A_SESSION_ID_1) + + // When + val result = isCurrentSessionUseCase.execute(A_SESSION_ID_2) + + // Then + result shouldBe false + verify { sessionParams.deviceId } + } + + @Test + fun `given no current active session when checking if id is current session then result is false`() { + // Given + fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null) + + // When + val result = isCurrentSessionUseCase.execute(A_SESSION_ID_1) + + // Then + result shouldBe false + } + + private fun givenIdForCurrentSession(deviceId: String): SessionParams { + val sessionParams = mockk() + every { sessionParams.deviceId } returns deviceId + fakeActiveSessionHolder.fakeSession.givenSessionParams(sessionParams) + return sessionParams + } +} 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 8d4e49ef85..71978129d3 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 @@ -19,16 +19,18 @@ package im.vector.app.features.settings.devices.v2.overview import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MvRxTestRule import im.vector.app.features.settings.devices.v2.DeviceFullInfo -import im.vector.app.test.fakes.FakeSession +import im.vector.app.features.settings.devices.v2.IsCurrentSessionUseCase +import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.test.test import im.vector.app.test.testDispatcher +import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.flow.flowOf import org.junit.Rule import org.junit.Test -import org.matrix.android.sdk.api.auth.data.SessionParams private const val A_SESSION_ID = "session-id" @@ -40,24 +42,27 @@ class SessionOverviewViewModelTest { private val args = SessionOverviewArgs( deviceId = A_SESSION_ID ) - private val fakeSession = FakeSession() + private val isCurrentSessionUseCase = mockk() private val getDeviceFullInfoUseCase = mockk() + private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() private fun createViewModel() = SessionOverviewViewModel( initialState = SessionOverviewViewState(args), - session = fakeSession, - getDeviceFullInfoUseCase = getDeviceFullInfoUseCase + isCurrentSessionUseCase = isCurrentSessionUseCase, + getDeviceFullInfoUseCase = getDeviceFullInfoUseCase, + checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, ) @Test fun `given the viewModel has been initialized then viewState is updated with session info`() { // Given - val sessionParams = givenIdForSession(A_SESSION_ID) val deviceFullInfo = mockk() every { getDeviceFullInfoUseCase.execute(A_SESSION_ID) } returns flowOf(deviceFullInfo) + val isCurrentSession = true + every { isCurrentSessionUseCase.execute(any()) } returns isCurrentSession val expectedState = SessionOverviewViewState( deviceId = A_SESSION_ID, - isCurrentSession = true, + isCurrentSession = isCurrentSession, deviceInfo = Success(deviceFullInfo) ) @@ -68,14 +73,55 @@ class SessionOverviewViewModelTest { viewModel.test() .assertLatestState { state -> state == expectedState } .finish() - verify { sessionParams.deviceId } - verify { getDeviceFullInfoUseCase.execute(A_SESSION_ID) } + verify { + isCurrentSessionUseCase.execute(A_SESSION_ID) + getDeviceFullInfoUseCase.execute(A_SESSION_ID) + } } - private fun givenIdForSession(deviceId: String): SessionParams { - val sessionParams = mockk() - every { sessionParams.deviceId } returns deviceId - fakeSession.givenSessionParams(sessionParams) - return sessionParams + @Test + fun `given current session can be verified when handling verify current session action then self verification event is posted`() { + // Given + val deviceFullInfo = mockk() + every { getDeviceFullInfoUseCase.execute(A_SESSION_ID) } returns flowOf(deviceFullInfo) + every { isCurrentSessionUseCase.execute(any()) } returns true + val verifySessionAction = SessionOverviewAction.VerifySession + coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns true + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(verifySessionAction) + + // Then + viewModelTest + .assertEvent { it is SessionOverviewViewEvent.SelfVerification } + .finish() + coVerify { + checkIfCurrentSessionCanBeVerifiedUseCase.execute() + } + } + + @Test + fun `given current session cannot be verified when handling verify current session action then reset secrets event is posted`() { + // Given + val deviceFullInfo = mockk() + every { getDeviceFullInfoUseCase.execute(A_SESSION_ID) } returns flowOf(deviceFullInfo) + every { isCurrentSessionUseCase.execute(any()) } returns true + val verifySessionAction = SessionOverviewAction.VerifySession + coEvery { checkIfCurrentSessionCanBeVerifiedUseCase.execute() } returns false + + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + viewModel.handle(verifySessionAction) + + // Then + viewModelTest + .assertEvent { it is SessionOverviewViewEvent.PromptResetSecrets } + .finish() + coVerify { + checkIfCurrentSessionCanBeVerifiedUseCase.execute() + } } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/verification/CheckIfCurrentSessionCanBeVerifiedUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/verification/CheckIfCurrentSessionCanBeVerifiedUseCaseTest.kt new file mode 100644 index 0000000000..22bc0edae1 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/verification/CheckIfCurrentSessionCanBeVerifiedUseCaseTest.kt @@ -0,0 +1,124 @@ +/* + * 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.verification + +import im.vector.app.test.fakes.FakeActiveSessionHolder +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.flow.FlowSession +import org.matrix.android.sdk.flow.flow + +class CheckIfCurrentSessionCanBeVerifiedUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val checkIfCurrentSessionCanBeVerifiedUseCase = CheckIfCurrentSessionCanBeVerifiedUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance + ) + + @Before + fun setUp() { + mockkStatic("org.matrix.android.sdk.flow.FlowSessionKt") + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given there are other sessions when checking if session can be verified then result is true`() = runTest { + // Given + val device1 = givenACryptoDevice() + val device2 = givenACryptoDevice() + val devices = listOf(device1, device2) + val fakeSession = fakeActiveSessionHolder.fakeSession + val flowSession = mockk() + every { fakeSession.flow() } returns flowSession + every { flowSession.liveUserCryptoDevices(any()) } returns flowOf(devices) + + fakeSession.fakeSharedSecretStorageService.givenIsRecoverySetupReturns(false) + + // When + val result = checkIfCurrentSessionCanBeVerifiedUseCase.execute() + + // Then + result shouldBeEqualTo true + verify { + flowSession.liveUserCryptoDevices(fakeSession.myUserId) + fakeSession.fakeSharedSecretStorageService.isRecoverySetup() + } + } + + @Test + fun `given recovery is setup when checking if session can be verified then result is true`() = runTest { + // Given + val device1 = givenACryptoDevice() + val devices = listOf(device1) + val fakeSession = fakeActiveSessionHolder.fakeSession + val flowSession = mockk() + every { fakeSession.flow() } returns flowSession + every { flowSession.liveUserCryptoDevices(any()) } returns flowOf(devices) + + fakeSession.fakeSharedSecretStorageService.givenIsRecoverySetupReturns(true) + + // When + val result = checkIfCurrentSessionCanBeVerifiedUseCase.execute() + + // Then + result shouldBeEqualTo true + verify { + flowSession.liveUserCryptoDevices(fakeSession.myUserId) + fakeSession.fakeSharedSecretStorageService.isRecoverySetup() + } + } + + @Test + fun `given recovery is not setup and there are no other sessions when checking if session can be verified then result is false`() = runTest { + // Given + val device1 = givenACryptoDevice() + val devices = listOf(device1) + val fakeSession = fakeActiveSessionHolder.fakeSession + val flowSession = mockk() + every { fakeSession.flow() } returns flowSession + every { flowSession.liveUserCryptoDevices(any()) } returns flowOf(devices) + + fakeSession.fakeSharedSecretStorageService.givenIsRecoverySetupReturns(false) + + // When + val result = checkIfCurrentSessionCanBeVerifiedUseCase.execute() + + // Then + result shouldBeEqualTo false + verify { + flowSession.liveUserCryptoDevices(fakeSession.myUserId) + fakeSession.fakeSharedSecretStorageService.isRecoverySetup() + } + } + + private fun givenACryptoDevice(): CryptoDeviceInfo = mockk() +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt index 6ec9a4b593..1dc36de709 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt @@ -16,6 +16,8 @@ package im.vector.app.test.fakes +import io.mockk.every +import io.mockk.mockk import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.securestorage.IntegrityResult import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult @@ -26,7 +28,7 @@ import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageServi import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec -class FakeSharedSecretStorageService : SharedSecretStorageService { +class FakeSharedSecretStorageService : SharedSecretStorageService by mockk() { var integrityResult: IntegrityResult = IntegrityResult.Error(SharedSecretStorageError.OtherError(IllegalStateException())) var _defaultKey: KeyInfoResult = KeyInfoResult.Error(SharedSecretStorageError.OtherError(IllegalStateException())) @@ -76,4 +78,8 @@ class FakeSharedSecretStorageService : SharedSecretStorageService { override suspend fun requestSecret(name: String, myOtherDeviceId: String) { TODO("Not yet implemented") } + + fun givenIsRecoverySetupReturns(isRecoverySetup: Boolean) { + every { isRecoverySetup() } returns isRecoverySetup + } }