Merge pull request #7044 from vector-im/feature/mna/clean-vm-for-dm-v2

[Devices Management] Refactor some code to improve testability (PSG-701)
This commit is contained in:
Maxime NATUREL 2022-09-08 11:44:56 +02:00 committed by GitHub
commit 62dbab907d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1177 additions and 56 deletions

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

@ -0,0 +1 @@
[Devices Management] Refactor some code to improve testability

View File

@ -352,6 +352,11 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(DevicesViewModel::class) @MavericksViewModelKey(DevicesViewModel::class)
fun devicesViewModelFactory(factory: DevicesViewModel.Factory): MavericksAssistedViewModelFactory<*, *> fun devicesViewModelFactory(factory: DevicesViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(im.vector.app.features.settings.devices.v2.DevicesViewModel::class)
fun devicesViewModelV2Factory(factory: im.vector.app.features.settings.devices.v2.DevicesViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds @Binds
@IntoMap @IntoMap
@MavericksViewModelKey(KeyRequestListViewModel::class) @MavericksViewModelKey(KeyRequestListViewModel::class)

View File

@ -34,6 +34,7 @@ import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.PublishDataSource import im.vector.app.core.utils.PublishDataSource
import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.settings.devices.v2.GetEncryptionTrustLevelForDeviceUseCase
import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
import im.vector.lib.core.utils.flow.throttleFirst import im.vector.lib.core.utils.flow.throttleFirst
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers

View File

@ -17,6 +17,7 @@
package im.vector.app.features.settings.devices package im.vector.app.features.settings.devices
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.settings.devices.v2.CurrentSessionCrossSigningInfo
import javax.inject.Inject import javax.inject.Inject
class GetCurrentSessionCrossSigningInfoUseCase @Inject constructor( class GetCurrentSessionCrossSigningInfoUseCase @Inject constructor(
@ -28,7 +29,7 @@ class GetCurrentSessionCrossSigningInfoUseCase @Inject constructor(
val isCrossSigningInitialized = session.cryptoService().crossSigningService().isCrossSigningInitialized() val isCrossSigningInitialized = session.cryptoService().crossSigningService().isCrossSigningInitialized()
val isCrossSigningVerified = session.cryptoService().crossSigningService().isCrossSigningVerified() val isCrossSigningVerified = session.cryptoService().crossSigningService().isCrossSigningVerified()
return CurrentSessionCrossSigningInfo( return CurrentSessionCrossSigningInfo(
deviceId = session.sessionParams.deviceId, deviceId = session.sessionParams.deviceId.orEmpty(),
isCrossSigningInitialized = isCrossSigningInitialized, isCrossSigningInitialized = isCrossSigningInitialized,
isCrossSigningVerified = isCrossSigningVerified isCrossSigningVerified = isCrossSigningVerified
) )

View File

@ -14,13 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.settings.devices package im.vector.app.features.settings.devices.v2
/** /**
* Used to hold some info about the cross signing of the current Session. * Used to hold some info about the cross signing of the current Session.
*/ */
data class CurrentSessionCrossSigningInfo( data class CurrentSessionCrossSigningInfo(
val deviceId: String?, val deviceId: String = "",
val isCrossSigningInitialized: Boolean, val isCrossSigningInitialized: Boolean = false,
val isCrossSigningVerified: Boolean, val isCrossSigningVerified: Boolean = false,
) )

View File

@ -0,0 +1,28 @@
/*
* 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 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
data class DeviceFullInfo(
val deviceInfo: DeviceInfo,
val cryptoDeviceInfo: CryptoDeviceInfo?,
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel,
val isInactive: Boolean,
)

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
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
sealed class DevicesAction : VectorViewModelAction {
data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction()
}

View File

@ -0,0 +1,34 @@
/*
* 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.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
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 ShowVerifyDevice(val userId: String, val transactionId: String?) : DevicesViewEvent()
data class SelfVerification(val session: Session) : DevicesViewEvent()
data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvent()
object PromptResetSecrets : DevicesViewEvent()
}

View File

@ -0,0 +1,153 @@
/*
* 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 com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.Success
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.utils.PublishDataSource
import im.vector.lib.core.utils.flow.throttleFirst
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import kotlin.time.Duration.Companion.seconds
class DevicesViewModel @AssistedInject constructor(
@Assisted initialState: DevicesViewState,
private val activeSessionHolder: ActiveSessionHolder,
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
private val refreshDevicesUseCase: RefreshDevicesUseCase,
private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase,
) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvent>(initialState), VerificationService.Listener {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<DevicesViewModel, DevicesViewState> {
override fun create(initialState: DevicesViewState): DevicesViewModel
}
companion object : MavericksViewModelFactory<DevicesViewModel, DevicesViewState> by hiltMavericksViewModelFactory()
private val refreshSource = PublishDataSource<Unit>()
private val refreshThrottleDelayMs = 4.seconds.inWholeMilliseconds
init {
addVerificationListener()
observeCurrentSessionCrossSigningInfo()
observeDevices()
observeRefreshSource()
refreshDevicesOnCryptoDevicesChange()
queryRefreshDevicesList()
}
override fun onCleared() {
removeVerificationListener()
super.onCleared()
}
private fun addVerificationListener() {
activeSessionHolder.getSafeActiveSession()
?.cryptoService()
?.verificationService()
?.addListener(this)
}
private fun removeVerificationListener() {
activeSessionHolder.getSafeActiveSession()
?.cryptoService()
?.verificationService()
?.removeListener(this)
}
private fun observeCurrentSessionCrossSigningInfo() {
getCurrentSessionCrossSigningInfoUseCase.execute()
.onEach { crossSigningInfo ->
setState {
copy(currentSessionCrossSigningInfo = crossSigningInfo)
}
}
.launchIn(viewModelScope)
}
private fun observeDevices() {
getDeviceFullInfoListUseCase.execute()
.execute { async ->
if (async is Success) {
val deviceFullInfoList = async.invoke()
val unverifiedSessionsCount = deviceFullInfoList.count { !it.cryptoDeviceInfo?.isVerified.orFalse() }
val inactiveSessionsCount = deviceFullInfoList.count { it.isInactive }
copy(
devices = async,
unverifiedSessionsCount = unverifiedSessionsCount,
inactiveSessionsCount = inactiveSessionsCount,
)
} else {
copy(
devices = async
)
}
}
}
private fun refreshDevicesOnCryptoDevicesChange() {
viewModelScope.launch {
refreshDevicesOnCryptoDevicesChangeUseCase.execute()
}
}
private fun observeRefreshSource() {
refreshSource.stream()
.throttleFirst(refreshThrottleDelayMs)
.onEach { refreshDevicesUseCase.execute() }
.launchIn(viewModelScope)
}
override fun transactionUpdated(tx: VerificationTransaction) {
if (tx.state == VerificationTxState.Verified) {
queryRefreshDevicesList()
}
}
/**
* Force the refresh of the devices list.
* The devices list is the list of the devices where the user is logged in.
* It can be any mobile devices, and any browsers.
*/
private fun queryRefreshDevicesList() {
refreshSource.post(Unit)
}
override fun handle(action: DevicesAction) {
when (action) {
is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction()
}
}
private fun handleMarkAsManuallyVerifiedAction() {
// TODO implement when needed
}
}

View File

@ -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.features.settings.devices.v2
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
data class DevicesViewState(
val currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo = CurrentSessionCrossSigningInfo(),
val devices: Async<List<DeviceFullInfo>> = Uninitialized,
val unverifiedSessionsCount: Int = 0,
val inactiveSessionsCount: Int = 0,
val isLoading: Boolean = false,
) : MavericksState

View File

@ -0,0 +1,48 @@
/*
* 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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.flow.flow
import javax.inject.Inject
class GetCurrentSessionCrossSigningInfoUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
) {
fun execute(): Flow<CurrentSessionCrossSigningInfo> {
return activeSessionHolder.getSafeActiveSession()
?.let { session ->
session.flow().liveCrossSigningInfo(session.myUserId)
.map { convertToSigningInfo(session.sessionParams.deviceId.orEmpty(), it) }
} ?: emptyFlow()
}
private fun convertToSigningInfo(deviceId: String, mxCrossSigningInfo: Optional<MXCrossSigningInfo>): CurrentSessionCrossSigningInfo {
return CurrentSessionCrossSigningInfo(
deviceId = deviceId,
isCrossSigningInitialized = mxCrossSigningInfo.getOrNull() != null,
isCrossSigningVerified = mxCrossSigningInfo.getOrNull()?.isTrusted().orFalse()
)
}
}

View File

@ -0,0 +1,65 @@
/*
* 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 im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
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.flow.flow
import javax.inject.Inject
class GetDeviceFullInfoListUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase,
private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase,
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
) {
fun execute(): Flow<List<DeviceFullInfo>> {
return activeSessionHolder.getSafeActiveSession()?.let { session ->
val deviceFullInfoFlow = combine(
getCurrentSessionCrossSigningInfoUseCase.execute(),
session.flow().liveUserCryptoDevices(session.myUserId),
session.flow().liveMyDevicesInfo()
) { currentSessionCrossSigningInfo, cryptoList, infoList ->
convertToDeviceFullInfoList(currentSessionCrossSigningInfo, cryptoList, infoList)
}
deviceFullInfoFlow.distinctUntilChanged()
} ?: emptyFlow()
}
private fun convertToDeviceFullInfoList(
currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo,
cryptoList: List<CryptoDeviceInfo>,
infoList: List<DeviceInfo>,
): List<DeviceFullInfo> {
return infoList
.sortedByDescending { it.lastSeenTs }
.map { deviceInfo ->
val cryptoDeviceInfo = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }
val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo)
val isInactive = checkIfSessionIsInactiveUseCase.execute(deviceInfo.lastSeenTs ?: 0)
DeviceFullInfo(deviceInfo, cryptoDeviceInfo, roomEncryptionTrustLevel, isInactive)
}
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.settings.devices package im.vector.app.features.settings.devices.v2
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import javax.inject.Inject import javax.inject.Inject

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.settings.devices package im.vector.app.features.settings.devices.v2
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.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.settings.devices package im.vector.app.features.settings.devices.v2
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel

View File

@ -0,0 +1,49 @@
/*
* 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 kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.flow.flow
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
class RefreshDevicesOnCryptoDevicesChangeUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
) {
private val samplingPeriodMs = 5.seconds.inWholeMilliseconds
suspend fun execute() {
activeSessionHolder.getSafeActiveSession()
?.let { session ->
session.flow().liveUserCryptoDevices(session.myUserId)
.map { it.size }
.distinctUntilChanged()
.sample(samplingPeriodMs)
.onEach {
// If we have a new crypto device change, we might want to trigger refresh of device info
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
}
.collect()
}
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2
import im.vector.app.core.di.ActiveSessionHolder
import org.matrix.android.sdk.api.NoOpMatrixCallback
import javax.inject.Inject
class RefreshDevicesUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
) {
fun execute() {
activeSessionHolder.getSafeActiveSession()?.let { session ->
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
session.cryptoService().downloadKeys(listOf(session.myUserId), true, NoOpMatrixCallback())
}
}
}

View File

@ -24,8 +24,6 @@ import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
@ -39,10 +37,6 @@ import im.vector.app.core.resources.DrawableProvider
import im.vector.app.databinding.FragmentSettingsDevicesBinding import im.vector.app.databinding.FragmentSettingsDevicesBinding
import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.crypto.verification.VerificationBottomSheet
import im.vector.app.features.settings.devices.DeviceFullInfo
import im.vector.app.features.settings.devices.DevicesAction
import im.vector.app.features.settings.devices.DevicesViewEvents
import im.vector.app.features.settings.devices.DevicesViewModel
import im.vector.app.features.settings.devices.v2.list.OtherSessionsController import im.vector.app.features.settings.devices.v2.list.OtherSessionsController
import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState
@ -93,27 +87,27 @@ class VectorSettingsDevicesFragment :
private fun observeViewEvents() { private fun observeViewEvents() {
viewModel.observeViewEvents { viewModel.observeViewEvents {
when (it) { when (it) {
is DevicesViewEvents.Loading -> showLoading(it.message) is DevicesViewEvent.Loading -> showLoading(it.message)
is DevicesViewEvents.Failure -> showFailure(it.throwable) is DevicesViewEvent.Failure -> showFailure(it.throwable)
is DevicesViewEvents.RequestReAuth -> Unit // TODO. Next PR is DevicesViewEvent.RequestReAuth -> Unit // TODO. Next PR
is DevicesViewEvents.PromptRenameDevice -> Unit // TODO. Next PR is DevicesViewEvent.PromptRenameDevice -> Unit // TODO. Next PR
is DevicesViewEvents.ShowVerifyDevice -> { is DevicesViewEvent.ShowVerifyDevice -> {
VerificationBottomSheet.withArgs( VerificationBottomSheet.withArgs(
roomId = null, roomId = null,
otherUserId = it.userId, otherUserId = it.userId,
transactionId = it.transactionId transactionId = it.transactionId
).show(childFragmentManager, "REQPOP") ).show(childFragmentManager, "REQPOP")
} }
is DevicesViewEvents.SelfVerification -> { is DevicesViewEvent.SelfVerification -> {
VerificationBottomSheet.forSelfVerification(it.session) VerificationBottomSheet.forSelfVerification(it.session)
.show(childFragmentManager, "REQPOP") .show(childFragmentManager, "REQPOP")
} }
is DevicesViewEvents.ShowManuallyVerify -> { is DevicesViewEvent.ShowManuallyVerify -> {
ManuallyVerifyDialog.show(requireActivity(), it.cryptoDeviceInfo) { ManuallyVerifyDialog.show(requireActivity(), it.cryptoDeviceInfo) {
viewModel.handle(DevicesAction.MarkAsManuallyVerified(it.cryptoDeviceInfo)) viewModel.handle(DevicesAction.MarkAsManuallyVerified(it.cryptoDeviceInfo))
} }
} }
is DevicesViewEvents.PromptResetSecrets -> { is DevicesViewEvent.PromptResetSecrets -> {
navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET) navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET)
} }
} }
@ -151,10 +145,11 @@ class VectorSettingsDevicesFragment :
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
if (state.devices is Success) { if (state.devices is Success) {
val devices = state.devices() val devices = state.devices()
val currentDeviceId = state.currentSessionCrossSigningInfo.deviceId
val currentDeviceInfo = devices?.firstOrNull { val currentDeviceInfo = devices?.firstOrNull {
it.deviceInfo.deviceId == state.myDeviceId it.deviceInfo.deviceId == currentDeviceId
} }
val otherDevices = devices?.filter { it.deviceInfo.deviceId != state.myDeviceId } val otherDevices = devices?.filter { it.deviceInfo.deviceId != currentDeviceId }
renderSecurityRecommendations(state.inactiveSessionsCount, state.unverifiedSessionsCount) renderSecurityRecommendations(state.inactiveSessionsCount, state.unverifiedSessionsCount)
renderCurrentDevice(currentDeviceInfo) renderCurrentDevice(currentDeviceInfo)
@ -165,7 +160,7 @@ class VectorSettingsDevicesFragment :
hideOtherSessionsView() hideOtherSessionsView()
} }
handleRequestStatus(state.request) handleLoadingStatus(state.isLoading)
} }
private fun renderSecurityRecommendations(inactiveSessionsCount: Int, unverifiedSessionsCount: Int) { private fun renderSecurityRecommendations(inactiveSessionsCount: Int, unverifiedSessionsCount: Int) {
@ -254,10 +249,7 @@ class VectorSettingsDevicesFragment :
} }
} }
private fun handleRequestStatus(unIgnoreRequest: Async<Unit>) { private fun handleLoadingStatus(isLoading: Boolean) {
views.waitingView.root.isVisible = when (unIgnoreRequest) { views.waitingView.root.isVisible = isLoading
is Loading -> true
else -> false
}
} }
} }

View File

@ -24,7 +24,7 @@ import im.vector.app.core.epoxy.noResultItem
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.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.settings.devices.DeviceFullInfo import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import javax.inject.Inject import javax.inject.Inject
@ -60,7 +60,7 @@ class OtherSessionsController @Inject constructor(
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS, SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
formattedLastActivityDate formattedLastActivityDate
) )
} else if (device.trustLevelForShield == RoomEncryptionTrustLevel.Trusted) { } else if (device.roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Trusted) {
stringProvider.getString(R.string.device_manager_other_sessions_description_verified, formattedLastActivityDate) stringProvider.getString(R.string.device_manager_other_sessions_description_verified, formattedLastActivityDate)
} else { } else {
stringProvider.getString(R.string.device_manager_other_sessions_description_unverified, formattedLastActivityDate) stringProvider.getString(R.string.device_manager_other_sessions_description_unverified, formattedLastActivityDate)
@ -71,7 +71,7 @@ class OtherSessionsController @Inject constructor(
otherSessionItem { otherSessionItem {
id(device.deviceInfo.deviceId) id(device.deviceInfo.deviceId)
deviceType(DeviceType.UNKNOWN) // TODO. We don't have this info yet. Update accordingly. deviceType(DeviceType.UNKNOWN) // TODO. We don't have this info yet. Update accordingly.
roomEncryptionTrustLevel(device.trustLevelForShield) roomEncryptionTrustLevel(device.roomEncryptionTrustLevel)
sessionName(device.deviceInfo.displayName) sessionName(device.deviceInfo.displayName)
sessionDescription(description) sessionDescription(description)
sessionDescriptionDrawable(descriptionDrawable) sessionDescriptionDrawable(descriptionDrawable)

View File

@ -24,7 +24,7 @@ import im.vector.app.R
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.databinding.ViewOtherSessionsBinding import im.vector.app.databinding.ViewOtherSessionsBinding
import im.vector.app.features.settings.devices.DeviceFullInfo import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint

View File

@ -57,7 +57,7 @@ class SessionInfoView @JvmOverloads constructor(
) { ) {
renderDeviceInfo(sessionInfoViewState.deviceFullInfo.deviceInfo.displayName.orEmpty()) renderDeviceInfo(sessionInfoViewState.deviceFullInfo.deviceInfo.displayName.orEmpty())
renderVerificationStatus( renderVerificationStatus(
sessionInfoViewState.deviceFullInfo.trustLevelForShield, sessionInfoViewState.deviceFullInfo.roomEncryptionTrustLevel,
sessionInfoViewState.isCurrentSession, sessionInfoViewState.isCurrentSession,
sessionInfoViewState.isLearnMoreLinkVisible, sessionInfoViewState.isLearnMoreLinkVisible,
) )

View File

@ -16,7 +16,7 @@
package im.vector.app.features.settings.devices.v2.list package im.vector.app.features.settings.devices.v2.list
import im.vector.app.features.settings.devices.DeviceFullInfo import im.vector.app.features.settings.devices.v2.DeviceFullInfo
data class SessionInfoViewState( data class SessionInfoViewState(
val isCurrentSession: Boolean, val isCurrentSession: Boolean,

View File

@ -18,9 +18,9 @@ package im.vector.app.features.settings.devices.v2.overview
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.settings.devices.DeviceFullInfo import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.GetCurrentSessionCrossSigningInfoUseCase import im.vector.app.features.settings.devices.v2.GetCurrentSessionCrossSigningInfoUseCase
import im.vector.app.features.settings.devices.GetEncryptionTrustLevelForDeviceUseCase import im.vector.app.features.settings.devices.v2.GetEncryptionTrustLevelForDeviceUseCase
import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -38,11 +38,11 @@ class GetDeviceFullInfoUseCase @Inject constructor(
fun execute(deviceId: String): Flow<Optional<DeviceFullInfo>> { fun execute(deviceId: String): Flow<Optional<DeviceFullInfo>> {
return activeSessionHolder.getSafeActiveSession()?.let { session -> return activeSessionHolder.getSafeActiveSession()?.let { session ->
val currentSessionCrossSigningInfo = getCurrentSessionCrossSigningInfoUseCase.execute()
combine( combine(
getCurrentSessionCrossSigningInfoUseCase.execute(),
session.cryptoService().getMyDevicesInfoLive(deviceId).asFlow(), session.cryptoService().getMyDevicesInfoLive(deviceId).asFlow(),
session.cryptoService().getLiveCryptoDeviceInfoWithId(deviceId).asFlow() session.cryptoService().getLiveCryptoDeviceInfoWithId(deviceId).asFlow()
) { deviceInfo, cryptoDeviceInfo -> ) { currentSessionCrossSigningInfo, deviceInfo, cryptoDeviceInfo ->
val info = deviceInfo.getOrNull() val info = deviceInfo.getOrNull()
val cryptoInfo = cryptoDeviceInfo.getOrNull() val cryptoInfo = cryptoDeviceInfo.getOrNull()
val fullInfo = if (info != null && cryptoInfo != null) { val fullInfo = if (info != null && cryptoInfo != null) {
@ -51,7 +51,7 @@ class GetDeviceFullInfoUseCase @Inject constructor(
DeviceFullInfo( DeviceFullInfo(
deviceInfo = info, deviceInfo = info,
cryptoDeviceInfo = cryptoInfo, cryptoDeviceInfo = cryptoInfo,
trustLevelForShield = roomEncryptionTrustLevel, roomEncryptionTrustLevel = roomEncryptionTrustLevel,
isInactive = isInactive isInactive = isInactive
) )
} else { } else {

View File

@ -34,7 +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.settings.devices.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

View File

@ -19,7 +19,7 @@ package im.vector.app.features.settings.devices.v2.overview
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.settings.devices.DeviceFullInfo import im.vector.app.features.settings.devices.v2.DeviceFullInfo
data class SessionOverviewViewState( data class SessionOverviewViewState(
val deviceId: String, val deviceId: String,

View File

@ -16,6 +16,7 @@
package im.vector.app.features.settings.devices package im.vector.app.features.settings.devices
import im.vector.app.features.settings.devices.v2.CurrentSessionCrossSigningInfo
import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeActiveSessionHolder
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk

View File

@ -0,0 +1,191 @@
/*
* 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 com.airbnb.mvrx.Success
import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeVerificationService
import im.vector.app.test.test
import im.vector.app.test.testDispatcher
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.flowOf
import org.junit.Rule
import org.junit.Test
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
class DevicesViewModelTest {
@get:Rule
val mvRxTestRule = MvRxTestRule(testDispatcher = testDispatcher)
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val getCurrentSessionCrossSigningInfoUseCase = mockk<GetCurrentSessionCrossSigningInfoUseCase>()
private val getDeviceFullInfoListUseCase = mockk<GetDeviceFullInfoListUseCase>()
private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>()
private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk<RefreshDevicesOnCryptoDevicesChangeUseCase>()
private fun createViewModel(): DevicesViewModel {
return DevicesViewModel(
DevicesViewState(),
fakeActiveSessionHolder.instance,
getCurrentSessionCrossSigningInfoUseCase,
getDeviceFullInfoListUseCase,
refreshDevicesUseCase,
refreshDevicesOnCryptoDevicesChangeUseCase,
)
}
@Test
fun `given the viewModel when initializing it then verification listener is added`() {
// Given
val fakeVerificationService = givenVerificationService()
givenCurrentSessionCrossSigningInfo()
givenDeviceFullInfoList()
givenRefreshDevicesOnCryptoDevicesChange()
// 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()
givenCurrentSessionCrossSigningInfo()
givenDeviceFullInfoList()
givenRefreshDevicesOnCryptoDevicesChange()
// When
val viewModel = createViewModel()
viewModel.onCleared()
// Then
verify {
fakeVerificationService.removeListener(viewModel)
}
}
@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()
// Then
viewModelTest.assertLatestState { it.currentSessionCrossSigningInfo == currentSessionCrossSigningInfo }
viewModelTest.finish()
}
@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()
// When
val viewModelTest = createViewModel().test()
// Then
viewModelTest.assertLatestState {
it.devices is Success && it.devices.invoke() == deviceFullInfoList &&
it.inactiveSessionsCount == 1 && it.unverifiedSessionsCount == 1
}
viewModelTest.finish()
}
@Test
fun `given the viewModel when initializing it then devices are refreshed on crypto devices change`() {
// Given
givenVerificationService()
givenCurrentSessionCrossSigningInfo()
givenDeviceFullInfoList()
givenRefreshDevicesOnCryptoDevicesChange()
// When
createViewModel()
// Then
coVerify { refreshDevicesOnCryptoDevicesChangeUseCase.execute() }
}
private fun givenVerificationService(): FakeVerificationService {
val fakeVerificationService = fakeActiveSessionHolder
.fakeSession
.fakeCryptoService
.fakeVerificationService
every { fakeVerificationService.addListener(any()) } just runs
every { fakeVerificationService.removeListener(any()) } just runs
return fakeVerificationService
}
private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo {
val currentSessionCrossSigningInfo = mockk<CurrentSessionCrossSigningInfo>()
every { getCurrentSessionCrossSigningInfoUseCase.execute() } returns flowOf(currentSessionCrossSigningInfo)
return currentSessionCrossSigningInfo
}
/**
* Generate mocked deviceFullInfo list with 1 unverified and inactive + 1 verified and active.
*/
private fun givenDeviceFullInfoList(): List<DeviceFullInfo> {
val verifiedCryptoDeviceInfo = mockk<CryptoDeviceInfo>()
every { verifiedCryptoDeviceInfo.isVerified } returns true
val unverifiedCryptoDeviceInfo = mockk<CryptoDeviceInfo>()
every { unverifiedCryptoDeviceInfo.isVerified } returns false
val deviceFullInfo1 = DeviceFullInfo(
deviceInfo = mockk(),
cryptoDeviceInfo = verifiedCryptoDeviceInfo,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false
)
val deviceFullInfo2 = DeviceFullInfo(
deviceInfo = mockk(),
cryptoDeviceInfo = unverifiedCryptoDeviceInfo,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = true
)
val deviceFullInfoList = listOf(deviceFullInfo1, deviceFullInfo2)
val deviceFullInfoListFlow = flowOf(deviceFullInfoList)
every { getDeviceFullInfoListUseCase.execute() } returns deviceFullInfoListFlow
return deviceFullInfoList
}
private fun givenRefreshDevicesOnCryptoDevicesChange() {
coEvery { refreshDevicesOnCryptoDevicesChangeUseCase.execute() } just runs
}
}

View File

@ -0,0 +1,123 @@
/*
* 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 im.vector.app.test.fakes.FakeSession
import im.vector.app.test.test
import im.vector.app.test.testDispatcher
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.junit.After
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.util.toOptional
private const val A_DEVICE_ID = "device-id"
class GetCurrentSessionCrossSigningInfoUseCaseTest {
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val getCurrentSessionCrossSigningInfoUseCase = GetCurrentSessionCrossSigningInfoUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance
)
@Before
fun setUp() {
mockkStatic("org.matrix.android.sdk.flow.FlowSessionKt")
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given the active session and existing cross signing info when getting these info then the result is correct`() = runTest(testDispatcher) {
val fakeSession = givenSession(A_DEVICE_ID)
val fakeFlowSession = fakeSession.givenFlowSession()
val isCrossSigningVerified = true
val mxCrossSigningInfo = givenMxCrossSigningInfo(isCrossSigningVerified)
every { fakeFlowSession.liveCrossSigningInfo(any()) } returns flowOf(mxCrossSigningInfo.toOptional())
val expectedResult = CurrentSessionCrossSigningInfo(
deviceId = A_DEVICE_ID,
isCrossSigningInitialized = true,
isCrossSigningVerified = isCrossSigningVerified
)
val result = getCurrentSessionCrossSigningInfoUseCase.execute()
.test(this)
result.assertValues(listOf(expectedResult))
.finish()
verify { fakeFlowSession.liveCrossSigningInfo(fakeSession.myUserId) }
}
@Test
fun `given the active session and no existing cross signing info when getting these info then the result is correct`() = runTest(testDispatcher) {
val fakeSession = givenSession(A_DEVICE_ID)
val fakeFlowSession = fakeSession.givenFlowSession()
val mxCrossSigningInfo = null
every { fakeFlowSession.liveCrossSigningInfo(any()) } returns flowOf(mxCrossSigningInfo.toOptional())
val expectedResult = CurrentSessionCrossSigningInfo(
deviceId = A_DEVICE_ID,
isCrossSigningInitialized = false,
isCrossSigningVerified = false
)
val result = getCurrentSessionCrossSigningInfoUseCase.execute()
.test(this)
result.assertValues(listOf(expectedResult))
.finish()
verify { fakeFlowSession.liveCrossSigningInfo(fakeSession.myUserId) }
}
@Test
fun `given no active session when getting cross signing info then the result is empty`() = runTest(testDispatcher) {
fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null)
val result = getCurrentSessionCrossSigningInfoUseCase.execute()
.test(this)
result.assertNoValues()
.finish()
}
private fun givenSession(deviceId: String): FakeSession {
val sessionParams = mockk<SessionParams>()
every { sessionParams.deviceId } returns deviceId
val fakeSession = fakeActiveSessionHolder.fakeSession
fakeSession.givenSessionParams(sessionParams)
return fakeSession
}
private fun givenMxCrossSigningInfo(isTrusted: Boolean) = mockk<MXCrossSigningInfo>()
.also {
every { it.isTrusted() } returns isTrusted
}
}

View File

@ -0,0 +1,182 @@
/*
* 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.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.test
import im.vector.app.test.testDispatcher
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.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.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
private const val A_DEVICE_ID_1 = "device-id-1"
private const val A_DEVICE_ID_2 = "device-id-2"
private const val A_DEVICE_ID_3 = "device-id-3"
private const val A_TIMESTAMP_1 = 100L
private const val A_TIMESTAMP_2 = 200L
private const val A_TIMESTAMP_3 = 300L
class GetDeviceFullInfoListUseCaseTest {
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val checkIfSessionIsInactiveUseCase = mockk<CheckIfSessionIsInactiveUseCase>()
private val getEncryptionTrustLevelForDeviceUseCase = mockk<GetEncryptionTrustLevelForDeviceUseCase>()
private val getCurrentSessionCrossSigningInfoUseCase = mockk<GetCurrentSessionCrossSigningInfoUseCase>()
private val getDeviceFullInfoListUseCase = GetDeviceFullInfoListUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance,
checkIfSessionIsInactiveUseCase = checkIfSessionIsInactiveUseCase,
getEncryptionTrustLevelForDeviceUseCase = getEncryptionTrustLevelForDeviceUseCase,
getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase,
)
@Before
fun setUp() {
mockkStatic("org.matrix.android.sdk.flow.FlowSessionKt")
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given active session when getting list of device full info then the list is correct and sorted in descending order`() = runTest(testDispatcher) {
// Given
val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo()
val fakeFlowSession = fakeActiveSessionHolder.fakeSession.givenFlowSession()
val cryptoDeviceInfo1 = givenACryptoDeviceInfo(A_DEVICE_ID_1)
val cryptoDeviceInfo2 = givenACryptoDeviceInfo(A_DEVICE_ID_2)
val cryptoDeviceInfo3 = givenACryptoDeviceInfo(A_DEVICE_ID_3)
val cryptoDeviceInfoList = listOf(cryptoDeviceInfo1, cryptoDeviceInfo2, cryptoDeviceInfo3)
every { fakeFlowSession.liveUserCryptoDevices(any()) } returns flowOf(cryptoDeviceInfoList)
val deviceInfo1 = givenADevicesInfo(
deviceId = A_DEVICE_ID_1,
lastSeenTs = A_TIMESTAMP_1,
isInactive = true,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
cryptoDeviceInfo = cryptoDeviceInfo1
)
val deviceInfo2 = givenADevicesInfo(
deviceId = A_DEVICE_ID_2,
lastSeenTs = A_TIMESTAMP_2,
isInactive = false,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
cryptoDeviceInfo = cryptoDeviceInfo2
)
val deviceInfo3 = givenADevicesInfo(
deviceId = A_DEVICE_ID_3,
lastSeenTs = A_TIMESTAMP_3,
isInactive = false,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
cryptoDeviceInfo = cryptoDeviceInfo3
)
val deviceInfoList = listOf(deviceInfo1, deviceInfo2, deviceInfo3)
every { fakeFlowSession.liveMyDevicesInfo() } returns flowOf(deviceInfoList)
val expectedResult1 = DeviceFullInfo(
deviceInfo = deviceInfo1,
cryptoDeviceInfo = cryptoDeviceInfo1,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = true
)
val expectedResult2 = DeviceFullInfo(
deviceInfo = deviceInfo2,
cryptoDeviceInfo = cryptoDeviceInfo2,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false
)
val expectedResult3 = DeviceFullInfo(
deviceInfo = deviceInfo3,
cryptoDeviceInfo = cryptoDeviceInfo3,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = false
)
val expectedResult = listOf(expectedResult3, expectedResult2, expectedResult1)
// When
val result = getDeviceFullInfoListUseCase.execute()
.test(this)
// Then
result.assertValues(expectedResult)
.finish()
verify {
getCurrentSessionCrossSigningInfoUseCase.execute()
fakeFlowSession.liveUserCryptoDevices(fakeActiveSessionHolder.fakeSession.myUserId)
fakeFlowSession.liveMyDevicesInfo()
getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo1)
getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo2)
getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo3)
checkIfSessionIsInactiveUseCase.execute(A_TIMESTAMP_1)
checkIfSessionIsInactiveUseCase.execute(A_TIMESTAMP_2)
checkIfSessionIsInactiveUseCase.execute(A_TIMESTAMP_3)
}
}
@Test
fun `given no active session when getting list then the result is empty`() = runTest(testDispatcher) {
// Given
fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null)
// When
val result = getDeviceFullInfoListUseCase.execute()
.test(this)
// Then
result.assertNoValues()
.finish()
}
private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo {
val currentSessionCrossSigningInfo = mockk<CurrentSessionCrossSigningInfo>()
every { getCurrentSessionCrossSigningInfoUseCase.execute() } returns flowOf(currentSessionCrossSigningInfo)
return currentSessionCrossSigningInfo
}
private fun givenACryptoDeviceInfo(deviceId: String): CryptoDeviceInfo {
val cryptoDeviceInfo = mockk<CryptoDeviceInfo>()
every { cryptoDeviceInfo.deviceId } returns deviceId
return cryptoDeviceInfo
}
private fun givenADevicesInfo(
deviceId: String,
lastSeenTs: Long,
isInactive: Boolean,
roomEncryptionTrustLevel: RoomEncryptionTrustLevel,
cryptoDeviceInfo: CryptoDeviceInfo,
): DeviceInfo {
val deviceInfo = mockk<DeviceInfo>()
every { deviceInfo.deviceId } returns deviceId
every { deviceInfo.lastSeenTs } returns lastSeenTs
every { getEncryptionTrustLevelForDeviceUseCase.execute(any(), cryptoDeviceInfo) } returns roomEncryptionTrustLevel
every { checkIfSessionIsInactiveUseCase.execute(lastSeenTs) } returns isInactive
return deviceInfo
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.settings.devices package im.vector.app.features.settings.devices.v2
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test import org.junit.Test

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.settings.devices package im.vector.app.features.settings.devices.v2
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -90,7 +90,7 @@ class GetEncryptionTrustLevelForDeviceUseCaseTest {
} }
private fun givenCurrentSessionCrossSigningInfo( private fun givenCurrentSessionCrossSigningInfo(
deviceId: String?, deviceId: String,
isCrossSigningInitialized: Boolean, isCrossSigningInitialized: Boolean,
isCrossSigningVerified: Boolean isCrossSigningVerified: Boolean
): CurrentSessionCrossSigningInfo { ): CurrentSessionCrossSigningInfo {

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.settings.devices package im.vector.app.features.settings.devices.v2
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test import org.junit.Test

View File

@ -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.features.settings.devices.v2
import im.vector.app.test.fakes.FakeActiveSessionHolder
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
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 RefreshDevicesOnCryptoDevicesChangeUseCaseTest {
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val refreshDevicesOnCryptoDevicesChangeUseCase = RefreshDevicesOnCryptoDevicesChangeUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance
)
@Before
fun setUp() {
mockkStatic("org.matrix.android.sdk.flow.FlowSessionKt")
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given the current session when crypto devices list changes then the devices list is refreshed`() = 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)
every { fakeSession.cryptoService().fetchDevicesList(any()) } just runs
// When
refreshDevicesOnCryptoDevicesChangeUseCase.execute()
// Then
verify {
flowSession.liveUserCryptoDevices(fakeSession.myUserId)
// FIXME the following verification does not work due to the usage of Flow.sample() inside the use case implementation
// fakeSession.cryptoService().fetchDevicesList(match { it is NoOpMatrixCallback })
}
}
private fun givenACryptoDevice(): CryptoDeviceInfo = mockk()
}

View File

@ -0,0 +1,48 @@
/*
* 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.just
import io.mockk.runs
import io.mockk.verifyAll
import org.junit.Test
import org.matrix.android.sdk.api.NoOpMatrixCallback
class RefreshDevicesUseCaseTest {
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val refreshDevicesUseCase = RefreshDevicesUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance
)
@Test
fun `given current session when refreshing then devices list and keys are fetched`() {
val session = fakeActiveSessionHolder.fakeSession
every { session.cryptoService().fetchDevicesList(any()) } just runs
every { session.cryptoService().downloadKeys(any(), any(), any()) } just runs
refreshDevicesUseCase.execute()
verifyAll {
session.cryptoService().fetchDevicesList(match { it is NoOpMatrixCallback })
session.cryptoService().downloadKeys(listOf(session.myUserId), true, match { it is NoOpMatrixCallback })
}
}
}

View File

@ -18,10 +18,10 @@ package im.vector.app.features.settings.devices.v2.overview
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import im.vector.app.features.settings.devices.CurrentSessionCrossSigningInfo import im.vector.app.features.settings.devices.v2.CurrentSessionCrossSigningInfo
import im.vector.app.features.settings.devices.DeviceFullInfo import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.GetCurrentSessionCrossSigningInfoUseCase import im.vector.app.features.settings.devices.v2.GetCurrentSessionCrossSigningInfoUseCase
import im.vector.app.features.settings.devices.GetEncryptionTrustLevelForDeviceUseCase import im.vector.app.features.settings.devices.v2.GetEncryptionTrustLevelForDeviceUseCase
import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeFlowLiveDataConversions import im.vector.app.test.fakes.FakeFlowLiveDataConversions
@ -31,6 +31,7 @@ import io.mockk.mockk
import io.mockk.unmockkAll import io.mockk.unmockkAll
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.After import org.junit.After
@ -90,7 +91,7 @@ class GetDeviceFullInfoUseCaseTest {
DeviceFullInfo( DeviceFullInfo(
deviceInfo = deviceInfo, deviceInfo = deviceInfo,
cryptoDeviceInfo = cryptoDeviceInfo, cryptoDeviceInfo = cryptoDeviceInfo,
trustLevelForShield = trustLevel, roomEncryptionTrustLevel = trustLevel,
isInactive = isInactive, isInactive = isInactive,
) )
) )
@ -134,7 +135,7 @@ class GetDeviceFullInfoUseCaseTest {
isCrossSigningInitialized = true, isCrossSigningInitialized = true,
isCrossSigningVerified = false isCrossSigningVerified = false
) )
every { getCurrentSessionCrossSigningInfoUseCase.execute() } returns currentSessionCrossSigningInfo every { getCurrentSessionCrossSigningInfoUseCase.execute() } returns flowOf(currentSessionCrossSigningInfo)
return currentSessionCrossSigningInfo return currentSessionCrossSigningInfo
} }

View File

@ -18,7 +18,7 @@ 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.DeviceFullInfo import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.test import im.vector.app.test.test
import io.mockk.every import io.mockk.every

View File

@ -24,7 +24,8 @@ import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
class FakeCryptoService( class FakeCryptoService(
val fakeCrossSigningService: FakeCrossSigningService = FakeCrossSigningService() val fakeCrossSigningService: FakeCrossSigningService = FakeCrossSigningService(),
val fakeVerificationService: FakeVerificationService = FakeVerificationService(),
) : CryptoService by mockk() { ) : CryptoService by mockk() {
var roomKeysExport = ByteArray(size = 1) var roomKeysExport = ByteArray(size = 1)
@ -34,6 +35,8 @@ class FakeCryptoService(
override fun crossSigningService() = fakeCrossSigningService override fun crossSigningService() = fakeCrossSigningService
override fun verificationService() = fakeVerificationService
override suspend fun exportRoomKeys(password: String) = roomKeysExport override suspend fun exportRoomKeys(password: String) = roomKeysExport
override fun getLiveCryptoDeviceInfo() = MutableLiveData(cryptoDeviceInfos.values.toList()) override fun getLiveCryptoDeviceInfo() = MutableLiveData(cryptoDeviceInfos.values.toList())

View File

@ -32,6 +32,8 @@ import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.flow.FlowSession
import org.matrix.android.sdk.flow.flow
class FakeSession( class FakeSession(
val fakeCryptoService: FakeCryptoService = FakeCryptoService(), val fakeCryptoService: FakeCryptoService = FakeCryptoService(),
@ -76,6 +78,15 @@ class FakeSession(
every { this@FakeSession.sessionParams } returns sessionParams every { this@FakeSession.sessionParams } returns sessionParams
} }
/**
* Do not forget to call mockkStatic("org.matrix.android.sdk.flow.FlowSessionKt") in the setup method of the tests.
*/
fun givenFlowSession(): FlowSession {
val fakeFlowSession = mockk<FlowSession>()
every { flow() } returns fakeFlowSession
return fakeFlowSession
}
companion object { companion object {
fun withRoomSummary(roomSummary: RoomSummary) = FakeSession().apply { fun withRoomSummary(roomSummary: RoomSummary) = FakeSession().apply {

View File

@ -0,0 +1,22 @@
/*
* 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 io.mockk.mockk
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
class FakeVerificationService : VerificationService by mockk()