Merge pull request #7139 from vector-im/feature/mna/device-manager-verify-current-session

[Device management] Verify current session (PSG-722)
This commit is contained in:
Benoit Marty 2022-09-22 11:46:07 +02:00 committed by GitHub
commit 4e30bc86b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 504 additions and 30 deletions

1
changelog.d/7114.wip Normal file
View File

@ -0,0 +1 @@
[Device management] Verify current session

View File

@ -85,8 +85,7 @@ class VectorSettingsDevicesFragment :
).show(childFragmentManager, "REQPOP") ).show(childFragmentManager, "REQPOP")
} }
is DevicesViewEvents.SelfVerification -> { is DevicesViewEvents.SelfVerification -> {
VerificationBottomSheet.forSelfVerification(it.session) navigator.requestSelfSessionVerification(requireActivity())
.show(childFragmentManager, "REQPOP")
} }
is DevicesViewEvents.ShowManuallyVerify -> { is DevicesViewEvents.ShowManuallyVerify -> {
ManuallyVerifyDialog.show(requireActivity(), it.cryptoDeviceInfo) { ManuallyVerifyDialog.show(requireActivity(), it.cryptoDeviceInfo) {

View File

@ -20,5 +20,6 @@ import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
sealed class DevicesAction : VectorViewModelAction { sealed class DevicesAction : VectorViewModelAction {
object VerifyCurrentSession : DevicesAction()
data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction()
} }

View File

@ -18,7 +18,6 @@ package im.vector.app.features.settings.devices.v2
import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse 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.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo 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 RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DevicesViewEvent()
data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvent() data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvent()
data class ShowVerifyDevice(val userId: String, val transactionId: String?) : 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() data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvent()
object PromptResetSecrets : DevicesViewEvent() object PromptResetSecrets : DevicesViewEvent()
} }

View File

@ -25,6 +25,7 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType 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.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -36,6 +37,7 @@ class DevicesViewModel @AssistedInject constructor(
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase, private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase,
private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
refreshDevicesUseCase: RefreshDevicesUseCase, refreshDevicesUseCase: RefreshDevicesUseCase,
) : VectorSessionsListViewModel<DevicesViewState, DevicesAction, DevicesViewEvent>(initialState, activeSessionHolder, refreshDevicesUseCase) { ) : VectorSessionsListViewModel<DevicesViewState, DevicesAction, DevicesViewEvent>(initialState, activeSessionHolder, refreshDevicesUseCase) {
@ -94,10 +96,22 @@ class DevicesViewModel @AssistedInject constructor(
override fun handle(action: DevicesAction) { override fun handle(action: DevicesAction) {
when (action) { when (action) {
is DevicesAction.VerifyCurrentSession -> handleVerifyCurrentSessionAction()
is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction() 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() { private fun handleMarkAsManuallyVerifiedAction() {
// TODO implement when needed // TODO implement when needed
} }

View File

@ -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
}
}

View File

@ -101,8 +101,7 @@ class VectorSettingsDevicesFragment :
).show(childFragmentManager, "REQPOP") ).show(childFragmentManager, "REQPOP")
} }
is DevicesViewEvent.SelfVerification -> { is DevicesViewEvent.SelfVerification -> {
VerificationBottomSheet.forSelfVerification(it.session) navigator.requestSelfSessionVerification(requireActivity())
.show(childFragmentManager, "REQPOP")
} }
is DevicesViewEvent.ShowManuallyVerify -> { is DevicesViewEvent.ShowManuallyVerify -> {
ManuallyVerifyDialog.show(requireActivity(), it.cryptoDeviceInfo) { ManuallyVerifyDialog.show(requireActivity(), it.cryptoDeviceInfo) {
@ -227,6 +226,9 @@ class VectorSettingsDevicesFragment :
views.deviceListCurrentSession.viewDetailsButton.debouncedClicks { views.deviceListCurrentSession.viewDetailsButton.debouncedClicks {
currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) } currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) }
} }
views.deviceListCurrentSession.viewVerifyButton.debouncedClicks {
viewModel.handle(DevicesAction.VerifyCurrentSession)
}
} ?: run { } ?: run {
hideCurrentSessionView() hideCurrentSessionView()
} }

View File

@ -49,6 +49,7 @@ class SessionInfoView @JvmOverloads constructor(
} }
val viewDetailsButton = views.sessionInfoViewDetailsButton val viewDetailsButton = views.sessionInfoViewDetailsButton
val viewVerifyButton = views.sessionInfoVerifySessionButton
fun render( fun render(
sessionInfoViewState: SessionInfoViewState, sessionInfoViewState: SessionInfoViewState,

View File

@ -18,4 +18,6 @@ package im.vector.app.features.settings.devices.v2.overview
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
sealed class SessionOverviewAction : VectorViewModelAction sealed class SessionOverviewAction : VectorViewModelAction {
object VerifySession : SessionOverviewAction()
}

View File

@ -34,6 +34,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.DrawableProvider
import im.vector.app.databinding.FragmentSessionOverviewBinding import im.vector.app.databinding.FragmentSessionOverviewBinding
import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
import javax.inject.Inject import javax.inject.Inject
@ -61,7 +62,9 @@ class SessionOverviewFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
observeViewEvents()
initSessionInfoView() initSessionInfoView()
initVerifyButton()
} }
private fun initSessionInfoView() { 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() { override fun onDestroyView() {
cleanUpSessionInfoView() cleanUpSessionInfoView()
super.onDestroyView() super.onDestroyView()

View File

@ -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()
}

View File

@ -23,17 +23,19 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel 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.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.session.Session import kotlinx.coroutines.launch
class SessionOverviewViewModel @AssistedInject constructor( class SessionOverviewViewModel @AssistedInject constructor(
@Assisted val initialState: SessionOverviewViewState, @Assisted val initialState: SessionOverviewViewState,
session: Session, private val isCurrentSessionUseCase: IsCurrentSessionUseCase,
private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase, private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase,
) : VectorViewModel<SessionOverviewViewState, SessionOverviewAction, EmptyViewEvents>(initialState) { private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
) : VectorViewModel<SessionOverviewViewState, SessionOverviewAction, SessionOverviewViewEvent>(initialState) {
companion object : MavericksViewModelFactory<SessionOverviewViewModel, SessionOverviewViewState> by hiltMavericksViewModelFactory() companion object : MavericksViewModelFactory<SessionOverviewViewModel, SessionOverviewViewState> by hiltMavericksViewModelFactory()
@ -43,14 +45,16 @@ class SessionOverviewViewModel @AssistedInject constructor(
} }
init { init {
val currentDeviceId = session.sessionParams.deviceId.orEmpty()
setState { setState {
copy(isCurrentSession = deviceId.isNotEmpty() && deviceId == currentDeviceId) copy(isCurrentSession = isCurrentSession(deviceId))
} }
observeSessionInfo(initialState.deviceId) observeSessionInfo(initialState.deviceId)
} }
private fun isCurrentSession(deviceId: String): Boolean {
return isCurrentSessionUseCase.execute(deviceId)
}
private fun observeSessionInfo(deviceId: String) { private fun observeSessionInfo(deviceId: String) {
getDeviceFullInfoUseCase.execute(deviceId) getDeviceFullInfoUseCase.execute(deviceId)
.onEach { setState { copy(deviceInfo = Success(it)) } } .onEach { setState { copy(deviceInfo = Success(it)) } }
@ -58,6 +62,25 @@ class SessionOverviewViewModel @AssistedInject constructor(
} }
override fun handle(action: SessionOverviewAction) { 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)
}
}
} }
} }

View File

@ -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
}
}

View File

@ -18,6 +18,7 @@ package im.vector.app.features.settings.devices.v2
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.test.MvRxTestRule import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.fakes.FakeVerificationService
import im.vector.app.test.test import im.vector.app.test.test
@ -45,6 +46,7 @@ class DevicesViewModelTest {
private val getDeviceFullInfoListUseCase = mockk<GetDeviceFullInfoListUseCase>() private val getDeviceFullInfoListUseCase = mockk<GetDeviceFullInfoListUseCase>()
private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>() private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>()
private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk<RefreshDevicesOnCryptoDevicesChangeUseCase>() private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk<RefreshDevicesOnCryptoDevicesChangeUseCase>()
private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>()
private fun createViewModel(): DevicesViewModel { private fun createViewModel(): DevicesViewModel {
return DevicesViewModel( return DevicesViewModel(
@ -53,6 +55,7 @@ class DevicesViewModelTest {
getCurrentSessionCrossSigningInfoUseCase, getCurrentSessionCrossSigningInfoUseCase,
getDeviceFullInfoListUseCase, getDeviceFullInfoListUseCase,
refreshDevicesOnCryptoDevicesChangeUseCase, refreshDevicesOnCryptoDevicesChangeUseCase,
checkIfCurrentSessionCanBeVerifiedUseCase,
refreshDevicesUseCase, refreshDevicesUseCase,
) )
} }
@ -142,6 +145,54 @@ class DevicesViewModelTest {
coVerify { refreshDevicesOnCryptoDevicesChangeUseCase.execute() } 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 { private fun givenVerificationService(): FakeVerificationService {
val fakeVerificationService = fakeActiveSessionHolder val fakeVerificationService = fakeActiveSessionHolder
.fakeSession .fakeSession

View File

@ -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<SessionParams>()
every { sessionParams.deviceId } returns deviceId
fakeActiveSessionHolder.fakeSession.givenSessionParams(sessionParams)
return sessionParams
}
}

View File

@ -19,16 +19,18 @@ package im.vector.app.features.settings.devices.v2.overview
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.test.MvRxTestRule import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.features.settings.devices.v2.DeviceFullInfo 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.test
import im.vector.app.test.testDispatcher import im.vector.app.test.testDispatcher
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.auth.data.SessionParams
private const val A_SESSION_ID = "session-id" private const val A_SESSION_ID = "session-id"
@ -40,24 +42,27 @@ class SessionOverviewViewModelTest {
private val args = SessionOverviewArgs( private val args = SessionOverviewArgs(
deviceId = A_SESSION_ID deviceId = A_SESSION_ID
) )
private val fakeSession = FakeSession() private val isCurrentSessionUseCase = mockk<IsCurrentSessionUseCase>()
private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>() private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>()
private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>()
private fun createViewModel() = SessionOverviewViewModel( private fun createViewModel() = SessionOverviewViewModel(
initialState = SessionOverviewViewState(args), initialState = SessionOverviewViewState(args),
session = fakeSession, isCurrentSessionUseCase = isCurrentSessionUseCase,
getDeviceFullInfoUseCase = getDeviceFullInfoUseCase getDeviceFullInfoUseCase = getDeviceFullInfoUseCase,
checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase,
) )
@Test @Test
fun `given the viewModel has been initialized then viewState is updated with session info`() { fun `given the viewModel has been initialized then viewState is updated with session info`() {
// Given // Given
val sessionParams = givenIdForSession(A_SESSION_ID)
val deviceFullInfo = mockk<DeviceFullInfo>() val deviceFullInfo = mockk<DeviceFullInfo>()
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID) } returns flowOf(deviceFullInfo) every { getDeviceFullInfoUseCase.execute(A_SESSION_ID) } returns flowOf(deviceFullInfo)
val isCurrentSession = true
every { isCurrentSessionUseCase.execute(any()) } returns isCurrentSession
val expectedState = SessionOverviewViewState( val expectedState = SessionOverviewViewState(
deviceId = A_SESSION_ID, deviceId = A_SESSION_ID,
isCurrentSession = true, isCurrentSession = isCurrentSession,
deviceInfo = Success(deviceFullInfo) deviceInfo = Success(deviceFullInfo)
) )
@ -68,14 +73,55 @@ class SessionOverviewViewModelTest {
viewModel.test() viewModel.test()
.assertLatestState { state -> state == expectedState } .assertLatestState { state -> state == expectedState }
.finish() .finish()
verify { sessionParams.deviceId } verify {
verify { getDeviceFullInfoUseCase.execute(A_SESSION_ID) } isCurrentSessionUseCase.execute(A_SESSION_ID)
getDeviceFullInfoUseCase.execute(A_SESSION_ID)
}
} }
private fun givenIdForSession(deviceId: String): SessionParams { @Test
val sessionParams = mockk<SessionParams>() fun `given current session can be verified when handling verify current session action then self verification event is posted`() {
every { sessionParams.deviceId } returns deviceId // Given
fakeSession.givenSessionParams(sessionParams) val deviceFullInfo = mockk<DeviceFullInfo>()
return sessionParams 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<DeviceFullInfo>()
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()
}
} }
} }

View File

@ -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<FlowSession>()
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<FlowSession>()
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<FlowSession>()
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()
}

View File

@ -16,6 +16,8 @@
package im.vector.app.test.fakes 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.listeners.ProgressListener
import org.matrix.android.sdk.api.session.securestorage.IntegrityResult import org.matrix.android.sdk.api.session.securestorage.IntegrityResult
import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult 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.SsssKeyCreationInfo
import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec 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 integrityResult: IntegrityResult = IntegrityResult.Error(SharedSecretStorageError.OtherError(IllegalStateException()))
var _defaultKey: KeyInfoResult = KeyInfoResult.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) { override suspend fun requestSecret(name: String, myOtherDeviceId: String) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
fun givenIsRecoverySetupReturns(isRecoverySetup: Boolean) {
every { isRecoverySetup() } returns isRecoverySetup
}
} }