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)
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
@IntoMap
@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.features.auth.ReAuthActivity
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.lib.core.utils.flow.throttleFirst
import kotlinx.coroutines.Dispatchers

View file

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

View file

@ -14,13 +14,13 @@
* 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.
*/
data class CurrentSessionCrossSigningInfo(
val deviceId: String?,
val isCrossSigningInitialized: Boolean,
val isCrossSigningVerified: Boolean,
val deviceId: String = "",
val isCrossSigningInitialized: Boolean = false,
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.
*/
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 javax.inject.Inject

View file

@ -14,7 +14,7 @@
* 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.RoomEncryptionTrustLevel

View file

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

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.DrawableProvider
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 javax.inject.Inject
@ -60,7 +60,7 @@ class OtherSessionsController @Inject constructor(
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
formattedLastActivityDate
)
} else if (device.trustLevelForShield == RoomEncryptionTrustLevel.Trusted) {
} else if (device.roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Trusted) {
stringProvider.getString(R.string.device_manager_other_sessions_description_verified, formattedLastActivityDate)
} else {
stringProvider.getString(R.string.device_manager_other_sessions_description_unverified, formattedLastActivityDate)
@ -71,7 +71,7 @@ class OtherSessionsController @Inject constructor(
otherSessionItem {
id(device.deviceInfo.deviceId)
deviceType(DeviceType.UNKNOWN) // TODO. We don't have this info yet. Update accordingly.
roomEncryptionTrustLevel(device.trustLevelForShield)
roomEncryptionTrustLevel(device.roomEncryptionTrustLevel)
sessionName(device.deviceInfo.displayName)
sessionDescription(description)
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.configureWith
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
@AndroidEntryPoint

View file

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

View file

@ -16,7 +16,7 @@
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(
val isCurrentSession: Boolean,

View file

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

View file

@ -16,6 +16,7 @@
package im.vector.app.features.settings.devices
import im.vector.app.features.settings.devices.v2.CurrentSessionCrossSigningInfo
import im.vector.app.test.fakes.FakeActiveSessionHolder
import io.mockk.every
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.
*/
package im.vector.app.features.settings.devices
package im.vector.app.features.settings.devices.v2
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test

View file

@ -14,7 +14,7 @@
* 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.mockk
@ -90,7 +90,7 @@ class GetEncryptionTrustLevelForDeviceUseCaseTest {
}
private fun givenCurrentSessionCrossSigningInfo(
deviceId: String?,
deviceId: String,
isCrossSigningInitialized: Boolean,
isCrossSigningVerified: Boolean
): CurrentSessionCrossSigningInfo {

View file

@ -14,7 +14,7 @@
* 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.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.asFlow
import im.vector.app.features.settings.devices.CurrentSessionCrossSigningInfo
import im.vector.app.features.settings.devices.DeviceFullInfo
import im.vector.app.features.settings.devices.GetCurrentSessionCrossSigningInfoUseCase
import im.vector.app.features.settings.devices.GetEncryptionTrustLevelForDeviceUseCase
import im.vector.app.features.settings.devices.v2.CurrentSessionCrossSigningInfo
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.GetCurrentSessionCrossSigningInfoUseCase
import im.vector.app.features.settings.devices.v2.GetEncryptionTrustLevelForDeviceUseCase
import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeFlowLiveDataConversions
@ -31,6 +31,7 @@ import io.mockk.mockk
import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.After
@ -90,7 +91,7 @@ class GetDeviceFullInfoUseCaseTest {
DeviceFullInfo(
deviceInfo = deviceInfo,
cryptoDeviceInfo = cryptoDeviceInfo,
trustLevelForShield = trustLevel,
roomEncryptionTrustLevel = trustLevel,
isInactive = isInactive,
)
)
@ -134,7 +135,7 @@ class GetDeviceFullInfoUseCaseTest {
isCrossSigningInitialized = true,
isCrossSigningVerified = false
)
every { getCurrentSessionCrossSigningInfoUseCase.execute() } returns currentSessionCrossSigningInfo
every { getCurrentSessionCrossSigningInfoUseCase.execute() } returns flowOf(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.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.test
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
class FakeCryptoService(
val fakeCrossSigningService: FakeCrossSigningService = FakeCrossSigningService()
val fakeCrossSigningService: FakeCrossSigningService = FakeCrossSigningService(),
val fakeVerificationService: FakeVerificationService = FakeVerificationService(),
) : CryptoService by mockk() {
var roomKeysExport = ByteArray(size = 1)
@ -34,6 +35,8 @@ class FakeCryptoService(
override fun crossSigningService() = fakeCrossSigningService
override fun verificationService() = fakeVerificationService
override suspend fun exportRoomKeys(password: String) = roomKeysExport
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.profile.ProfileService
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(
val fakeCryptoService: FakeCryptoService = FakeCryptoService(),
@ -76,6 +78,15 @@ class FakeSession(
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 {
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()