Merge branch 'develop' into feature/ons/toggle_ip_address_visibility

# Conflicts:
#	vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
This commit is contained in:
Onuray Sahin 2022-11-09 18:09:22 +03:00
commit b81fc4f8f1
27 changed files with 170 additions and 395 deletions

View file

@ -60,8 +60,8 @@ jobs:
headers: '{"GraphQL-Features": "projects_next_graphql"}' headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: | query: |
mutation add_to_project($projectid:ID!, $contentid:ID!) { mutation add_to_project($projectid:ID!, $contentid:ID!) {
addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
projectNextItem { item {
id id
} }
} }
@ -129,8 +129,8 @@ jobs:
headers: '{"GraphQL-Features": "projects_next_graphql"}' headers: '{"GraphQL-Features": "projects_next_graphql"}'
query: | query: |
mutation add_to_project($projectid:ID!, $contentid:ID!) { mutation add_to_project($projectid:ID!, $contentid:ID!) {
addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
projectNextItem { item {
id id
} }
} }

View file

@ -1,3 +1,10 @@
Changes in Element v1.5.7 (2022-11-07)
======================================
Bugfixes 🐛
----------
- Fix regression when syncing with homeserver < 1.4. ([#7534](https://github.com/vector-im/element-android/issues/7534))
Changes in Element v1.5.6 (2022-11-02) Changes in Element v1.5.6 (2022-11-02)
====================================== ======================================

1
changelog.d/7501.bugfix Normal file
View file

@ -0,0 +1 @@
Fix duplicated mention pills in some cases

1
changelog.d/7509.bugfix Normal file
View file

@ -0,0 +1 @@
When joining a room, the message composer is displayed once the room is loaded.

View file

@ -26,7 +26,7 @@ def jjwt = "0.11.5"
// Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert // Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert
// the whole commit which set version 0.16.0-SNAPSHOT // the whole commit which set version 0.16.0-SNAPSHOT
def vanniktechEmoji = "0.16.0-SNAPSHOT" def vanniktechEmoji = "0.16.0-SNAPSHOT"
def sentry = "6.6.0" def sentry = "6.7.0"
def fragment = "1.5.4" def fragment = "1.5.4"
// Testing // Testing
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819

View file

@ -0,0 +1,2 @@
Main changes in this version: new UI for selecting an attachment.
Full changelog: https://github.com/vector-im/element-android/releases

View file

@ -67,7 +67,9 @@ internal object FilterFactory {
} }
private fun createElementTimelineFilter(): RoomEventFilter? { private fun createElementTimelineFilter(): RoomEventFilter? {
return RoomEventFilter(enableUnreadThreadNotifications = true) // we need to check if homeserver supports thread notifications before setting this param
// return RoomEventFilter(enableUnreadThreadNotifications = true)
return null
} }
private fun createElementStateFilter(): RoomEventFilter { private fun createElementStateFilter(): RoomEventFilter {

View file

@ -132,7 +132,7 @@ dependencies {
implementation libs.androidx.biometric implementation libs.androidx.biometric
api "org.threeten:threetenbp:1.4.0:no-tzdb" api "org.threeten:threetenbp:1.4.0:no-tzdb"
api "com.gabrielittner.threetenbp:lazythreetenbp:0.11.0" api "com.gabrielittner.threetenbp:lazythreetenbp:0.12.0"
implementation libs.squareup.moshi implementation libs.squareup.moshi
kapt libs.squareup.moshiKotlin kapt libs.squareup.moshiKotlin

View file

@ -24,26 +24,6 @@ package im.vector.app.features.analytics.plan
* definition. These properties must all be device independent. * definition. These properties must all be device independent.
*/ */
data class UserProperties( data class UserProperties(
/**
* Whether the user has the favourites space enabled.
*/
val webMetaSpaceFavouritesEnabled: Boolean? = null,
/**
* Whether the user has the home space set to all rooms.
*/
val webMetaSpaceHomeAllRooms: Boolean? = null,
/**
* Whether the user has the home space enabled.
*/
val webMetaSpaceHomeEnabled: Boolean? = null,
/**
* Whether the user has the other rooms space enabled.
*/
val webMetaSpaceOrphansEnabled: Boolean? = null,
/**
* Whether the user has the people space enabled.
*/
val webMetaSpacePeopleEnabled: Boolean? = null,
/** /**
* The active filter in the All Chats screen. * The active filter in the All Chats screen.
*/ */
@ -109,11 +89,6 @@ data class UserProperties(
fun getProperties(): Map<String, Any>? { fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply { return mutableMapOf<String, Any>().apply {
webMetaSpaceFavouritesEnabled?.let { put("WebMetaSpaceFavouritesEnabled", it) }
webMetaSpaceHomeAllRooms?.let { put("WebMetaSpaceHomeAllRooms", it) }
webMetaSpaceHomeEnabled?.let { put("WebMetaSpaceHomeEnabled", it) }
webMetaSpaceOrphansEnabled?.let { put("WebMetaSpaceOrphansEnabled", it) }
webMetaSpacePeopleEnabled?.let { put("WebMetaSpacePeopleEnabled", it) }
allChatsActiveFilter?.let { put("allChatsActiveFilter", it.name) } allChatsActiveFilter?.let { put("allChatsActiveFilter", it.name) }
ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) } ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) }
numFavouriteRooms?.let { put("numFavouriteRooms", it) } numFavouriteRooms?.let { put("numFavouriteRooms", it) }

View file

@ -1169,6 +1169,9 @@ class TimelineFragment :
lazyLoadedViews.inviteView(false)?.isVisible = false lazyLoadedViews.inviteView(false)?.isVisible = false
if (mainState.tombstoneEvent == null) { if (mainState.tombstoneEvent == null) {
views.composerContainer.isInvisible = !messageComposerState.isComposerVisible
views.voiceMessageRecorderContainer.isVisible = messageComposerState.isVoiceMessageRecorderVisible
when (messageComposerState.canSendMessage) { when (messageComposerState.canSendMessage) {
CanSendStatus.Allowed -> { CanSendStatus.Allowed -> {
NotificationAreaView.State.Hidden NotificationAreaView.State.Hidden
@ -1224,6 +1227,7 @@ class TimelineFragment :
private fun FragmentTimelineBinding.hideComposerViews() { private fun FragmentTimelineBinding.hideComposerViews() {
composerContainer.isVisible = false composerContainer.isVisible = false
voiceMessageRecorderContainer.isVisible = false
} }
private fun renderTypingMessageNotification(roomSummary: RoomSummary?, state: RoomDetailViewState) { private fun renderTypingMessageNotification(roomSummary: RoomSummary?, state: RoomDetailViewState) {

View file

@ -83,6 +83,20 @@ class PillsPostProcessor @AssistedInject constructor(
val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach
val startSpan = renderedText.getSpanStart(linkSpan) val startSpan = renderedText.getSpanStart(linkSpan)
val endSpan = renderedText.getSpanEnd(linkSpan) val endSpan = renderedText.getSpanEnd(linkSpan)
// GlideImagesPlugin causes duplicated pills if we have a nested spans in the pill span,
// such as images or italic text.
// Accordingly, it's better to remove all spans that are contained in this span before rendering.
renderedText.getSpans(startSpan, endSpan, Any::class.java).forEach remove@{
if (it !is LinkSpan) {
// Make sure to only remove spans that are contained in this link, and not are bigger than this link, e.g. like reply-blocks
val start = renderedText.getSpanStart(it)
if (start < startSpan) return@remove
val end = renderedText.getSpanEnd(it)
if (end > endSpan) return@remove
renderedText.removeSpan(it)
}
}
addPillSpan(renderedText, pillSpan, startSpan, endSpan) addPillSpan(renderedText, pillSpan, startSpan, endSpan)
} }
} }

View file

@ -31,20 +31,16 @@ import im.vector.app.features.auth.PendingAuthHandler
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
import timber.log.Timber import timber.log.Timber
import kotlin.coroutines.Continuation
class DevicesViewModel @AssistedInject constructor( class DevicesViewModel @AssistedInject constructor(
@Assisted initialState: DevicesViewState, @Assisted initialState: DevicesViewState,
@ -166,16 +162,14 @@ class DevicesViewModel @AssistedInject constructor(
if (deviceIds.isEmpty()) { if (deviceIds.isEmpty()) {
return@launch return@launch
} }
val signoutResult = signout(deviceIds) val result = signout(deviceIds)
setLoading(false) setLoading(false)
if (signoutResult.isSuccess) { val error = result.exceptionOrNull()
if (error == null) {
onSignoutSuccess() onSignoutSuccess()
} else { } else {
when (val failure = signoutResult.exceptionOrNull()) { onSignoutFailure(error)
null -> onSignoutSuccess()
else -> onSignoutFailure(failure)
}
} }
} }
} }
@ -187,16 +181,9 @@ class DevicesViewModel @AssistedInject constructor(
.orEmpty() .orEmpty()
} }
private suspend fun signout(deviceIds: List<String>) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor { private suspend fun signout(deviceIds: List<String>) = signoutSessionsUseCase.execute(deviceIds, this::onReAuthNeeded)
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) {
is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result)
is SignoutSessionResult.Completed -> Unit
}
}
})
private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) {
Timber.d("onReAuthNeeded") Timber.d("onReAuthNeeded")
pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation

View file

@ -33,24 +33,18 @@ import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
import timber.log.Timber import timber.log.Timber
import kotlin.coroutines.Continuation
class OtherSessionsViewModel @AssistedInject constructor( class OtherSessionsViewModel @AssistedInject constructor(
@Assisted private val initialState: OtherSessionsViewState, @Assisted private val initialState: OtherSessionsViewState,
activeSessionHolder: ActiveSessionHolder, activeSessionHolder: ActiveSessionHolder,
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
private val signoutSessionsUseCase: SignoutSessionsUseCase, private val signoutSessionsUseCase: SignoutSessionsUseCase,
private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
private val pendingAuthHandler: PendingAuthHandler, private val pendingAuthHandler: PendingAuthHandler,
refreshDevicesUseCase: RefreshDevicesUseCase, refreshDevicesUseCase: RefreshDevicesUseCase,
@DefaultPreferences @DefaultPreferences
@ -193,16 +187,14 @@ class OtherSessionsViewModel @AssistedInject constructor(
if (deviceIds.isEmpty()) { if (deviceIds.isEmpty()) {
return@launch return@launch
} }
val signoutResult = signout(deviceIds) val result = signout(deviceIds)
setLoading(false) setLoading(false)
if (signoutResult.isSuccess) { val error = result.exceptionOrNull()
if (error == null) {
onSignoutSuccess() onSignoutSuccess()
} else { } else {
when (val failure = signoutResult.exceptionOrNull()) { onSignoutFailure(error)
null -> onSignoutSuccess()
else -> onSignoutFailure(failure)
}
} }
} }
} }
@ -215,16 +207,9 @@ class OtherSessionsViewModel @AssistedInject constructor(
}.mapNotNull { it.deviceInfo.deviceId } }.mapNotNull { it.deviceInfo.deviceId }
} }
private suspend fun signout(deviceIds: List<String>) = signoutSessionsUseCase.execute(deviceIds, object : UserInteractiveAuthInterceptor { private suspend fun signout(deviceIds: List<String>) = signoutSessionsUseCase.execute(deviceIds, this::onReAuthNeeded)
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) {
is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result)
is SignoutSessionResult.Completed -> Unit
}
}
})
private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) {
Timber.d("onReAuthNeeded") Timber.d("onReAuthNeeded")
pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation

View file

@ -34,28 +34,24 @@ import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel
import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase
import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
import timber.log.Timber import timber.log.Timber
import kotlin.coroutines.Continuation
class SessionOverviewViewModel @AssistedInject constructor( class SessionOverviewViewModel @AssistedInject constructor(
@Assisted val initialState: SessionOverviewViewState, @Assisted val initialState: SessionOverviewViewState,
private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase, private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase,
private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
private val signoutSessionUseCase: SignoutSessionUseCase, private val signoutSessionsUseCase: SignoutSessionsUseCase,
private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
private val pendingAuthHandler: PendingAuthHandler, private val pendingAuthHandler: PendingAuthHandler,
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
@ -174,30 +170,21 @@ class SessionOverviewViewModel @AssistedInject constructor(
private fun handleSignoutOtherSession(deviceId: String) { private fun handleSignoutOtherSession(deviceId: String) {
viewModelScope.launch { viewModelScope.launch {
setLoading(true) setLoading(true)
val signoutResult = signout(deviceId) val result = signout(deviceId)
setLoading(false) setLoading(false)
if (signoutResult.isSuccess) { val error = result.exceptionOrNull()
if (error == null) {
onSignoutSuccess() onSignoutSuccess()
} else { } else {
when (val failure = signoutResult.exceptionOrNull()) { onSignoutFailure(error)
null -> onSignoutSuccess()
else -> onSignoutFailure(failure)
}
} }
} }
} }
private suspend fun signout(deviceId: String) = signoutSessionUseCase.execute(deviceId, object : UserInteractiveAuthInterceptor { private suspend fun signout(deviceId: String) = signoutSessionsUseCase.execute(listOf(deviceId), this::onReAuthNeeded)
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
when (val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)) {
is SignoutSessionResult.ReAuthNeeded -> onReAuthNeeded(result)
is SignoutSessionResult.Completed -> Unit
}
}
})
private fun onReAuthNeeded(reAuthNeeded: SignoutSessionResult.ReAuthNeeded) { private fun onReAuthNeeded(reAuthNeeded: SignoutSessionsReAuthNeeded) {
Timber.d("onReAuthNeeded") Timber.d("onReAuthNeeded")
pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) pendingAuthHandler.pendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation pendingAuthHandler.uiaContinuation = reAuthNeeded.uiaContinuation

View file

@ -37,17 +37,16 @@ class InterceptSignoutFlowResponseUseCase @Inject constructor(
flowResponse: RegistrationFlowResponse, flowResponse: RegistrationFlowResponse,
errCode: String?, errCode: String?,
promise: Continuation<UIABaseAuth> promise: Continuation<UIABaseAuth>
): SignoutSessionResult { ): SignoutSessionsReAuthNeeded? {
return if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errCode == null) { return if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errCode == null) {
UserPasswordAuth( UserPasswordAuth(
session = null, session = null,
user = activeSessionHolder.getActiveSession().myUserId, user = activeSessionHolder.getActiveSession().myUserId,
password = reAuthHelper.data password = reAuthHelper.data
).let { promise.resume(it) } ).let { promise.resume(it) }
null
SignoutSessionResult.Completed
} else { } else {
SignoutSessionResult.ReAuthNeeded( SignoutSessionsReAuthNeeded(
pendingAuth = DefaultBaseAuth(session = flowResponse.session), pendingAuth = DefaultBaseAuth(session = flowResponse.session),
uiaContinuation = promise, uiaContinuation = promise,
flowResponse = flowResponse, flowResponse = flowResponse,

View file

@ -1,42 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.signout
import im.vector.app.core.di.ActiveSessionHolder
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.util.awaitCallback
import javax.inject.Inject
/**
* Use case to signout a single session.
*/
class SignoutSessionUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
) {
suspend fun execute(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result<Unit> {
return deleteDevice(deviceId, userInteractiveAuthInterceptor)
}
private suspend fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching {
awaitCallback { matrixCallback ->
activeSessionHolder.getActiveSession()
.cryptoService()
.deleteDevice(deviceId, userInteractiveAuthInterceptor, matrixCallback)
}
}
}

View file

@ -20,13 +20,9 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
sealed class SignoutSessionResult { data class SignoutSessionsReAuthNeeded(
data class ReAuthNeeded( val pendingAuth: UIABaseAuth,
val pendingAuth: UIABaseAuth, val uiaContinuation: Continuation<UIABaseAuth>,
val uiaContinuation: Continuation<UIABaseAuth>, val flowResponse: RegistrationFlowResponse,
val flowResponse: RegistrationFlowResponse, val errCode: String?
val errCode: String? )
) : SignoutSessionResult()
object Completed : SignoutSessionResult()
}

View file

@ -16,27 +16,42 @@
package im.vector.app.features.settings.devices.v2.signout package im.vector.app.features.settings.devices.v2.signout
import androidx.annotation.Size
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.util.awaitCallback import org.matrix.android.sdk.api.util.awaitCallback
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.Continuation
/**
* Use case to signout several sessions.
*/
class SignoutSessionsUseCase @Inject constructor( class SignoutSessionsUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
) { ) {
suspend fun execute(deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor): Result<Unit> { suspend fun execute(
return deleteDevices(deviceIds, userInteractiveAuthInterceptor) @Size(min = 1) deviceIds: List<String>,
onReAuthNeeded: (SignoutSessionsReAuthNeeded) -> Unit,
): Result<Unit> = runCatching {
Timber.d("start execute with ${deviceIds.size} deviceIds")
val authInterceptor = object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
val result = interceptSignoutFlowResponseUseCase.execute(flowResponse, errCode, promise)
result?.let(onReAuthNeeded)
}
}
deleteDevices(deviceIds, authInterceptor)
Timber.d("end execute")
} }
private suspend fun deleteDevices(deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) = runCatching { private suspend fun deleteDevices(deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) =
awaitCallback { matrixCallback -> awaitCallback { matrixCallback ->
activeSessionHolder.getActiveSession() activeSessionHolder.getActiveSession()
.cryptoService() .cryptoService()
.deleteDevices(deviceIds, userInteractiveAuthInterceptor, matrixCallback) .deleteDevices(deviceIds, userInteractiveAuthInterceptor, matrixCallback)
} }
}
} }

View file

@ -232,7 +232,7 @@ class DevicesViewModelTest {
// Given // Given
val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_CURRENT_DEVICE_ID) val expectedViewState = givenInitialViewState(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_CURRENT_DEVICE_ID)
// signout all devices except the current device // signout all devices except the current device
fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1), fakeInterceptSignoutFlowResponseUseCase) fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1))
// When // When
val viewModel = createViewModel() val viewModel = createViewModel()
@ -279,7 +279,7 @@ class DevicesViewModelTest {
@Test @Test
fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() { fun `given reAuth is needed during multiSignout when handling multiSignout action then requestReAuth is sent and pending auth is stored`() {
// Given // Given
val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2))
val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
val expectedReAuthEvent = DevicesViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode) val expectedReAuthEvent = DevicesViewEvent.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)

View file

@ -23,7 +23,6 @@ import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakePendingAuthHandler
import im.vector.app.test.fakes.FakeSharedPreferences import im.vector.app.test.fakes.FakeSharedPreferences
@ -67,7 +66,6 @@ class OtherSessionsViewModelTest {
private val fakeGetDeviceFullInfoListUseCase = mockk<GetDeviceFullInfoListUseCase>() private val fakeGetDeviceFullInfoListUseCase = mockk<GetDeviceFullInfoListUseCase>()
private val fakeRefreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxed = true) private val fakeRefreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxed = true)
private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase()
private val fakeInterceptSignoutFlowResponseUseCase = mockk<InterceptSignoutFlowResponseUseCase>()
private val fakePendingAuthHandler = FakePendingAuthHandler() private val fakePendingAuthHandler = FakePendingAuthHandler()
private val fakeSharedPreferences = FakeSharedPreferences() private val fakeSharedPreferences = FakeSharedPreferences()
@ -77,7 +75,6 @@ class OtherSessionsViewModelTest {
activeSessionHolder = fakeActiveSessionHolder.instance, activeSessionHolder = fakeActiveSessionHolder.instance,
getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase, getDeviceFullInfoListUseCase = fakeGetDeviceFullInfoListUseCase,
signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance,
interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase,
pendingAuthHandler = fakePendingAuthHandler.instance, pendingAuthHandler = fakePendingAuthHandler.instance,
refreshDevicesUseCase = fakeRefreshDevicesUseCase, refreshDevicesUseCase = fakeRefreshDevicesUseCase,
sharedPreferences = fakeSharedPreferences, sharedPreferences = fakeSharedPreferences,
@ -325,7 +322,7 @@ class OtherSessionsViewModelTest {
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2) val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
// signout only selected devices // signout only selected devices
fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_2))
val expectedViewState = OtherSessionsViewState( val expectedViewState = OtherSessionsViewState(
devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)),
currentFilter = defaultArgs.defaultFilter, currentFilter = defaultArgs.defaultFilter,
@ -361,7 +358,7 @@ class OtherSessionsViewModelTest {
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2) val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
// signout all devices // signout all devices
fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2))
val expectedViewState = OtherSessionsViewState( val expectedViewState = OtherSessionsViewState(
devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)), devices = Success(listOf(deviceFullInfo1, deviceFullInfo2)),
currentFilter = defaultArgs.defaultFilter, currentFilter = defaultArgs.defaultFilter,
@ -426,7 +423,7 @@ class OtherSessionsViewModelTest {
val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true) val deviceFullInfo2 = aDeviceFullInfo(A_DEVICE_ID_2, isSelected = true)
val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2) val devices: List<DeviceFullInfo> = listOf(deviceFullInfo1, deviceFullInfo2)
givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices) givenGetDeviceFullInfoListReturns(filterType = defaultArgs.defaultFilter, devices)
val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2), fakeInterceptSignoutFlowResponseUseCase) val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_DEVICE_ID_1, A_DEVICE_ID_2))
val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)
val expectedReAuthEvent = OtherSessionsViewEvents.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode) val expectedReAuthEvent = OtherSessionsViewEvents.RequestReAuth(reAuthNeeded.flowResponse, reAuthNeeded.errCode)

View file

@ -29,7 +29,7 @@ import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSes
import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakePendingAuthHandler
import im.vector.app.test.fakes.FakeSharedPreferences import im.vector.app.test.fakes.FakeSharedPreferences
import im.vector.app.test.fakes.FakeSignoutSessionUseCase import im.vector.app.test.fakes.FakeSignoutSessionsUseCase
import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase
import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.fakes.FakeVerificationService
import im.vector.app.test.test import im.vector.app.test.test
@ -71,7 +71,7 @@ class SessionOverviewViewModelTest {
private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>(relaxed = true) private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>(relaxed = true)
private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>()
private val fakeSignoutSessionUseCase = FakeSignoutSessionUseCase() private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase()
private val interceptSignoutFlowResponseUseCase = mockk<InterceptSignoutFlowResponseUseCase>() private val interceptSignoutFlowResponseUseCase = mockk<InterceptSignoutFlowResponseUseCase>()
private val fakePendingAuthHandler = FakePendingAuthHandler() private val fakePendingAuthHandler = FakePendingAuthHandler()
private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxed = true) private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxed = true)
@ -84,7 +84,7 @@ class SessionOverviewViewModelTest {
initialState = SessionOverviewViewState(args), initialState = SessionOverviewViewState(args),
getDeviceFullInfoUseCase = getDeviceFullInfoUseCase, getDeviceFullInfoUseCase = getDeviceFullInfoUseCase,
checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase,
signoutSessionUseCase = fakeSignoutSessionUseCase.instance, signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance,
interceptSignoutFlowResponseUseCase = interceptSignoutFlowResponseUseCase, interceptSignoutFlowResponseUseCase = interceptSignoutFlowResponseUseCase,
pendingAuthHandler = fakePendingAuthHandler.instance, pendingAuthHandler = fakePendingAuthHandler.instance,
activeSessionHolder = fakeActiveSessionHolder.instance, activeSessionHolder = fakeActiveSessionHolder.instance,
@ -252,7 +252,7 @@ class SessionOverviewViewModelTest {
val deviceFullInfo = mockk<DeviceFullInfo>() val deviceFullInfo = mockk<DeviceFullInfo>()
every { deviceFullInfo.isCurrentDevice } returns false every { deviceFullInfo.isCurrentDevice } returns false
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
fakeSignoutSessionUseCase.givenSignoutSuccess(A_SESSION_ID_1, interceptSignoutFlowResponseUseCase) fakeSignoutSessionsUseCase.givenSignoutSuccess(listOf(A_SESSION_ID_1))
val signoutAction = SessionOverviewAction.SignoutOtherSession val signoutAction = SessionOverviewAction.SignoutOtherSession
givenCurrentSessionIsTrusted() givenCurrentSessionIsTrusted()
val expectedViewState = SessionOverviewViewState( val expectedViewState = SessionOverviewViewState(
@ -289,7 +289,7 @@ class SessionOverviewViewModelTest {
every { deviceFullInfo.isCurrentDevice } returns false every { deviceFullInfo.isCurrentDevice } returns false
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
val error = Exception() val error = Exception()
fakeSignoutSessionUseCase.givenSignoutError(A_SESSION_ID_1, error) fakeSignoutSessionsUseCase.givenSignoutError(listOf(A_SESSION_ID_1), error)
val signoutAction = SessionOverviewAction.SignoutOtherSession val signoutAction = SessionOverviewAction.SignoutOtherSession
givenCurrentSessionIsTrusted() givenCurrentSessionIsTrusted()
val expectedViewState = SessionOverviewViewState( val expectedViewState = SessionOverviewViewState(
@ -322,7 +322,7 @@ class SessionOverviewViewModelTest {
val deviceFullInfo = mockk<DeviceFullInfo>() val deviceFullInfo = mockk<DeviceFullInfo>()
every { deviceFullInfo.isCurrentDevice } returns false every { deviceFullInfo.isCurrentDevice } returns false
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo) every { getDeviceFullInfoUseCase.execute(A_SESSION_ID_1) } returns flowOf(deviceFullInfo)
val reAuthNeeded = fakeSignoutSessionUseCase.givenSignoutReAuthNeeded(A_SESSION_ID_1, interceptSignoutFlowResponseUseCase) val reAuthNeeded = fakeSignoutSessionsUseCase.givenSignoutReAuthNeeded(listOf(A_SESSION_ID_1))
val signoutAction = SessionOverviewAction.SignoutOtherSession val signoutAction = SessionOverviewAction.SignoutOtherSession
givenCurrentSessionIsTrusted() givenCurrentSessionIsTrusted()
val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session) val expectedPendingAuth = DefaultBaseAuth(session = reAuthNeeded.flowResponse.session)

View file

@ -24,8 +24,8 @@ import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.runs import io.mockk.runs
import io.mockk.unmockkAll import io.mockk.unmockkAll
import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeInstanceOf
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -63,7 +63,7 @@ class InterceptSignoutFlowResponseUseCaseTest {
} }
@Test @Test
fun `given no error and a stored password and a next stage as password when intercepting then promise is resumed and success is returned`() { fun `given no error and a stored password and a next stage as password when intercepting then promise is resumed and null is returned`() {
// Given // Given
val registrationFlowResponse = givenNextUncompletedStage(LoginFlowTypes.PASSWORD, A_SESSION_ID) val registrationFlowResponse = givenNextUncompletedStage(LoginFlowTypes.PASSWORD, A_SESSION_ID)
fakeReAuthHelper.givenStoredPassword(A_PASSWORD) fakeReAuthHelper.givenStoredPassword(A_PASSWORD)
@ -84,7 +84,7 @@ class InterceptSignoutFlowResponseUseCaseTest {
) )
// Then // Then
result shouldBeInstanceOf (SignoutSessionResult.Completed::class) result shouldBe null
every { every {
promise.resume(expectedAuth) promise.resume(expectedAuth)
} }
@ -97,7 +97,7 @@ class InterceptSignoutFlowResponseUseCaseTest {
fakeReAuthHelper.givenStoredPassword(A_PASSWORD) fakeReAuthHelper.givenStoredPassword(A_PASSWORD)
val errorCode = AN_ERROR_CODE val errorCode = AN_ERROR_CODE
val promise = mockk<Continuation<UIABaseAuth>>() val promise = mockk<Continuation<UIABaseAuth>>()
val expectedResult = SignoutSessionResult.ReAuthNeeded( val expectedResult = SignoutSessionsReAuthNeeded(
pendingAuth = DefaultBaseAuth(session = A_SESSION_ID), pendingAuth = DefaultBaseAuth(session = A_SESSION_ID),
uiaContinuation = promise, uiaContinuation = promise,
flowResponse = registrationFlowResponse, flowResponse = registrationFlowResponse,
@ -122,7 +122,7 @@ class InterceptSignoutFlowResponseUseCaseTest {
fakeReAuthHelper.givenStoredPassword(A_PASSWORD) fakeReAuthHelper.givenStoredPassword(A_PASSWORD)
val errorCode: String? = null val errorCode: String? = null
val promise = mockk<Continuation<UIABaseAuth>>() val promise = mockk<Continuation<UIABaseAuth>>()
val expectedResult = SignoutSessionResult.ReAuthNeeded( val expectedResult = SignoutSessionsReAuthNeeded(
pendingAuth = DefaultBaseAuth(session = A_SESSION_ID), pendingAuth = DefaultBaseAuth(session = A_SESSION_ID),
uiaContinuation = promise, uiaContinuation = promise,
flowResponse = registrationFlowResponse, flowResponse = registrationFlowResponse,
@ -147,7 +147,7 @@ class InterceptSignoutFlowResponseUseCaseTest {
fakeReAuthHelper.givenStoredPassword(null) fakeReAuthHelper.givenStoredPassword(null)
val errorCode: String? = null val errorCode: String? = null
val promise = mockk<Continuation<UIABaseAuth>>() val promise = mockk<Continuation<UIABaseAuth>>()
val expectedResult = SignoutSessionResult.ReAuthNeeded( val expectedResult = SignoutSessionsReAuthNeeded(
pendingAuth = DefaultBaseAuth(session = A_SESSION_ID), pendingAuth = DefaultBaseAuth(session = A_SESSION_ID),
uiaContinuation = promise, uiaContinuation = promise,
flowResponse = registrationFlowResponse, flowResponse = registrationFlowResponse,

View file

@ -1,79 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.signout
import im.vector.app.test.fakes.FakeActiveSessionHolder
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe
import org.junit.Test
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
private const val A_DEVICE_ID = "device-id"
class SignoutSessionUseCaseTest {
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val signoutSessionUseCase = SignoutSessionUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance
)
@Test
fun `given a device id when signing out with success then success result is returned`() = runTest {
// Given
val interceptor = givenAuthInterceptor()
fakeActiveSessionHolder.fakeSession
.fakeCryptoService
.givenDeleteDeviceSucceeds(A_DEVICE_ID)
// When
val result = signoutSessionUseCase.execute(A_DEVICE_ID, interceptor)
// Then
result.isSuccess shouldBe true
every {
fakeActiveSessionHolder.fakeSession
.fakeCryptoService
.deleteDevice(A_DEVICE_ID, interceptor, any())
}
}
@Test
fun `given a device id when signing out with error then failure result is returned`() = runTest {
// Given
val interceptor = givenAuthInterceptor()
val error = mockk<Exception>()
fakeActiveSessionHolder.fakeSession
.fakeCryptoService
.givenDeleteDeviceFailsWithError(A_DEVICE_ID, error)
// When
val result = signoutSessionUseCase.execute(A_DEVICE_ID, interceptor)
// Then
result.isFailure shouldBe true
every {
fakeActiveSessionHolder.fakeSession
.fakeCryptoService
.deleteDevice(A_DEVICE_ID, interceptor, any())
}
}
private fun givenAuthInterceptor() = mockk<UserInteractiveAuthInterceptor>()
}

View file

@ -19,10 +19,10 @@ package im.vector.app.features.settings.devices.v2.signout
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
import io.mockk.verify
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBe
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
private const val A_DEVICE_ID_1 = "device-id-1" private const val A_DEVICE_ID_1 = "device-id-1"
private const val A_DEVICE_ID_2 = "device-id-2" private const val A_DEVICE_ID_2 = "device-id-2"
@ -30,36 +30,38 @@ private const val A_DEVICE_ID_2 = "device-id-2"
class SignoutSessionsUseCaseTest { class SignoutSessionsUseCaseTest {
private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val fakeInterceptSignoutFlowResponseUseCase = mockk<InterceptSignoutFlowResponseUseCase>()
private val signoutSessionsUseCase = SignoutSessionsUseCase( private val signoutSessionsUseCase = SignoutSessionsUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance activeSessionHolder = fakeActiveSessionHolder.instance,
interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase,
) )
@Test @Test
fun `given a list of device ids when signing out with success then success result is returned`() = runTest { fun `given a list of device ids when signing out with success then success result is returned`() = runTest {
// Given // Given
val interceptor = givenAuthInterceptor() val callback = givenOnReAuthCallback()
val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)
fakeActiveSessionHolder.fakeSession fakeActiveSessionHolder.fakeSession
.fakeCryptoService .fakeCryptoService
.givenDeleteDevicesSucceeds(deviceIds) .givenDeleteDevicesSucceeds(deviceIds)
// When // When
val result = signoutSessionsUseCase.execute(deviceIds, interceptor) val result = signoutSessionsUseCase.execute(deviceIds, callback)
// Then // Then
result.isSuccess shouldBe true result.isSuccess shouldBe true
every { verify {
fakeActiveSessionHolder.fakeSession fakeActiveSessionHolder.fakeSession
.fakeCryptoService .fakeCryptoService
.deleteDevices(deviceIds, interceptor, any()) .deleteDevices(deviceIds, any(), any())
} }
} }
@Test @Test
fun `given a list of device ids when signing out with error then failure result is returned`() = runTest { fun `given a list of device ids when signing out with error then failure result is returned`() = runTest {
// Given // Given
val interceptor = givenAuthInterceptor() val interceptor = givenOnReAuthCallback()
val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2) val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)
val error = mockk<Exception>() val error = mockk<Exception>()
fakeActiveSessionHolder.fakeSession fakeActiveSessionHolder.fakeSession
@ -71,12 +73,41 @@ class SignoutSessionsUseCaseTest {
// Then // Then
result.isFailure shouldBe true result.isFailure shouldBe true
every { verify {
fakeActiveSessionHolder.fakeSession fakeActiveSessionHolder.fakeSession
.fakeCryptoService .fakeCryptoService
.deleteDevices(deviceIds, interceptor, any()) .deleteDevices(deviceIds, any(), any())
} }
} }
private fun givenAuthInterceptor() = mockk<UserInteractiveAuthInterceptor>() @Test
fun `given a list of device ids when signing out with reAuth needed then callback is called`() = runTest {
// Given
val callback = givenOnReAuthCallback()
val deviceIds = listOf(A_DEVICE_ID_1, A_DEVICE_ID_2)
fakeActiveSessionHolder.fakeSession
.fakeCryptoService
.givenDeleteDevicesNeedsUIAuth(deviceIds)
val reAuthNeeded = SignoutSessionsReAuthNeeded(
pendingAuth = mockk(),
uiaContinuation = mockk(),
flowResponse = mockk(),
errCode = "errorCode"
)
every { fakeInterceptSignoutFlowResponseUseCase.execute(any(), any(), any()) } returns reAuthNeeded
// When
val result = signoutSessionsUseCase.execute(deviceIds, callback)
// Then
result.isSuccess shouldBe true
verify {
fakeActiveSessionHolder.fakeSession
.fakeCryptoService
.deleteDevices(deviceIds, any(), any())
callback(reAuthNeeded)
}
}
private fun givenOnReAuthCallback(): (SignoutSessionsReAuthNeeded) -> Unit = {}
} }

View file

@ -22,6 +22,7 @@ import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.slot import io.mockk.slot
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
@ -70,30 +71,21 @@ class FakeCryptoService(
} }
} }
fun givenDeleteDeviceSucceeds(deviceId: String) { fun givenDeleteDevicesSucceeds(deviceIds: List<String>) {
val matrixCallback = slot<MatrixCallback<Unit>>() every { deleteDevices(deviceIds, any(), any()) } answers {
every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers {
thirdArg<MatrixCallback<Unit>>().onSuccess(Unit) thirdArg<MatrixCallback<Unit>>().onSuccess(Unit)
} }
} }
fun givenDeleteDeviceFailsWithError(deviceId: String, error: Exception) { fun givenDeleteDevicesNeedsUIAuth(deviceIds: List<String>) {
val matrixCallback = slot<MatrixCallback<Unit>>() every { deleteDevices(deviceIds, any(), any()) } answers {
every { deleteDevice(deviceId, any(), capture(matrixCallback)) } answers { secondArg<UserInteractiveAuthInterceptor>().performStage(mockk(), "", mockk())
thirdArg<MatrixCallback<Unit>>().onFailure(error)
}
}
fun givenDeleteDevicesSucceeds(deviceIds: List<String>) {
val matrixCallback = slot<MatrixCallback<Unit>>()
every { deleteDevices(deviceIds, any(), capture(matrixCallback)) } answers {
thirdArg<MatrixCallback<Unit>>().onSuccess(Unit) thirdArg<MatrixCallback<Unit>>().onSuccess(Unit)
} }
} }
fun givenDeleteDevicesFailsWithError(deviceIds: List<String>, error: Exception) { fun givenDeleteDevicesFailsWithError(deviceIds: List<String>, error: Exception) {
val matrixCallback = slot<MatrixCallback<Unit>>() every { deleteDevices(deviceIds, any(), any()) } answers {
every { deleteDevices(deviceIds, any(), capture(matrixCallback)) } answers {
thirdArg<MatrixCallback<Unit>>().onFailure(error) thirdArg<MatrixCallback<Unit>>().onFailure(error)
} }
} }

View file

@ -1,77 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fakes
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionUseCase
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import kotlin.coroutines.Continuation
class FakeSignoutSessionUseCase {
val instance = mockk<SignoutSessionUseCase>()
fun givenSignoutSuccess(
deviceId: String,
interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
) {
val interceptor = slot<UserInteractiveAuthInterceptor>()
val flowResponse = mockk<RegistrationFlowResponse>()
val errorCode = "errorCode"
val promise = mockk<Continuation<UIABaseAuth>>()
every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed
coEvery { instance.execute(deviceId, capture(interceptor)) } coAnswers {
secondArg<UserInteractiveAuthInterceptor>().performStage(flowResponse, errorCode, promise)
Result.success(Unit)
}
}
fun givenSignoutReAuthNeeded(
deviceId: String,
interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
): SignoutSessionResult.ReAuthNeeded {
val interceptor = slot<UserInteractiveAuthInterceptor>()
val flowResponse = mockk<RegistrationFlowResponse>()
every { flowResponse.session } returns "a-session-id"
val errorCode = "errorCode"
val promise = mockk<Continuation<UIABaseAuth>>()
val reAuthNeeded = SignoutSessionResult.ReAuthNeeded(
pendingAuth = mockk(),
uiaContinuation = promise,
flowResponse = flowResponse,
errCode = errorCode,
)
every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded
coEvery { instance.execute(deviceId, capture(interceptor)) } coAnswers {
secondArg<UserInteractiveAuthInterceptor>().performStage(flowResponse, errorCode, promise)
Result.success(Unit)
}
return reAuthNeeded
}
fun givenSignoutError(deviceId: String, error: Throwable) {
coEvery { instance.execute(deviceId, any()) } returns Result.failure(error)
}
}

View file

@ -16,55 +16,33 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionResult
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.slot
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import kotlin.coroutines.Continuation
class FakeSignoutSessionsUseCase { class FakeSignoutSessionsUseCase {
val instance = mockk<SignoutSessionsUseCase>() val instance = mockk<SignoutSessionsUseCase>()
fun givenSignoutSuccess( fun givenSignoutSuccess(deviceIds: List<String>) {
deviceIds: List<String>, coEvery { instance.execute(deviceIds, any()) } returns Result.success(Unit)
interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
) {
val interceptor = slot<UserInteractiveAuthInterceptor>()
val flowResponse = mockk<RegistrationFlowResponse>()
val errorCode = "errorCode"
val promise = mockk<Continuation<UIABaseAuth>>()
every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns SignoutSessionResult.Completed
coEvery { instance.execute(deviceIds, capture(interceptor)) } coAnswers {
secondArg<UserInteractiveAuthInterceptor>().performStage(flowResponse, errorCode, promise)
Result.success(Unit)
}
} }
fun givenSignoutReAuthNeeded( fun givenSignoutReAuthNeeded(deviceIds: List<String>): SignoutSessionsReAuthNeeded {
deviceIds: List<String>,
interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
): SignoutSessionResult.ReAuthNeeded {
val interceptor = slot<UserInteractiveAuthInterceptor>()
val flowResponse = mockk<RegistrationFlowResponse>() val flowResponse = mockk<RegistrationFlowResponse>()
every { flowResponse.session } returns "a-session-id" every { flowResponse.session } returns "a-session-id"
val errorCode = "errorCode" val errorCode = "errorCode"
val promise = mockk<Continuation<UIABaseAuth>>() val reAuthNeeded = SignoutSessionsReAuthNeeded(
val reAuthNeeded = SignoutSessionResult.ReAuthNeeded(
pendingAuth = mockk(), pendingAuth = mockk(),
uiaContinuation = promise, uiaContinuation = mockk(),
flowResponse = flowResponse, flowResponse = flowResponse,
errCode = errorCode, errCode = errorCode,
) )
every { interceptSignoutFlowResponseUseCase.execute(flowResponse, errorCode, promise) } returns reAuthNeeded coEvery { instance.execute(deviceIds, any()) } coAnswers {
coEvery { instance.execute(deviceIds, capture(interceptor)) } coAnswers { secondArg<(SignoutSessionsReAuthNeeded) -> Unit>().invoke(reAuthNeeded)
secondArg<UserInteractiveAuthInterceptor>().performStage(flowResponse, errorCode, promise)
Result.success(Unit) Result.success(Unit)
} }