Merge pull request #5372 from element-hq/feature/bma/fixLogoutUseCase

When logging out from Pin code screen, logout from all the sessions.
This commit is contained in:
Benoit Marty
2025-09-19 10:15:18 +02:00
committed by GitHub
6 changed files with 146 additions and 20 deletions

View File

@@ -174,7 +174,7 @@ class PinUnlockPresenter(
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncAction<Unit>>) = launch {
suspend {
logoutUseCase.logout(ignoreSdkError = true)
logoutUseCase.logoutAll(ignoreSdkError = true)
}.runCatchingUpdatingState(signOutAction)
}
}

View File

@@ -8,16 +8,12 @@
package io.element.android.features.logout.api
/**
* Used to trigger a log out of the current user from any part of the app.
* Used to trigger a log out of the current user(s) from any part of the app.
*/
interface LogoutUseCase {
/**
* Log out the current user and then perform any needed cleanup tasks.
* Log out the current user(s) and then perform any needed cleanup tasks.
* @param ignoreSdkError if true, the SDK error will be ignored and the user will be logged out anyway.
*/
suspend fun logout(ignoreSdkError: Boolean)
interface Factory {
fun create(sessionId: String): LogoutUseCase
}
suspend fun logoutAll(ignoreSdkError: Boolean)
}

View File

@@ -39,4 +39,5 @@ dependencies {
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.sessionStorage.test)
}

View File

@@ -12,22 +12,31 @@ import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.sessionstorage.api.SessionStore
import timber.log.Timber
@ContributesBinding(AppScope::class)
@Inject
class DefaultLogoutUseCase(
private val authenticationService: MatrixAuthenticationService,
private val sessionStore: SessionStore,
private val matrixClientProvider: MatrixClientProvider,
) : LogoutUseCase {
override suspend fun logout(ignoreSdkError: Boolean) {
val currentSession = authenticationService.getLatestSessionId()
if (currentSession != null) {
matrixClientProvider.getOrRestore(currentSession)
.getOrThrow()
.logout(userInitiated = true, ignoreSdkError = true)
} else {
error("No session to sign out")
}
override suspend fun logoutAll(ignoreSdkError: Boolean) {
sessionStore.getAllSessions()
.map { sessionData ->
SessionId(sessionData.userId)
}
.forEach { sessionId ->
Timber.d("Logging out sessionId: $sessionId")
matrixClientProvider.getOrRestore(sessionId).fold(
onSuccess = { client ->
client.logout(userInitiated = true, ignoreSdkError = ignoreSdkError)
},
onFailure = { error ->
Timber.e(error, "Failed to get or restore MatrixClient for sessionId: $sessionId")
}
)
}
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.logout.impl
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultLogoutUseCaseTest {
@Test
fun `test logout from one session`() = runTest {
val logoutLambda1 = lambdaRecorder<Boolean, Boolean, Unit> { _, _ -> }
val client1 = FakeMatrixClient(A_USER_ID).apply {
logoutLambda = logoutLambda1
}
val sut = DefaultLogoutUseCase(
sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(sessionId = A_USER_ID.value),
)
),
matrixClientProvider = FakeMatrixClientProvider(
getClient = { sessionId ->
when (sessionId) {
A_USER_ID -> Result.success(client1)
else -> error("Unexpected sessionId")
}
}
),
)
sut.logoutAll(ignoreSdkError = true)
logoutLambda1.assertions().isCalledOnce().with(value(true), value(true))
}
@Test
fun `test logout from several sessions`() = runTest {
val logoutLambda1 = lambdaRecorder<Boolean, Boolean, Unit> { _, _ -> }
val logoutLambda2 = lambdaRecorder<Boolean, Boolean, Unit> { _, _ -> }
val client1 = FakeMatrixClient(A_USER_ID).apply {
logoutLambda = logoutLambda1
}
val client2 = FakeMatrixClient(A_USER_ID_2).apply {
logoutLambda = logoutLambda2
}
val sut = DefaultLogoutUseCase(
sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(sessionId = A_USER_ID.value),
aSessionData(sessionId = A_USER_ID_2.value),
)
),
matrixClientProvider = FakeMatrixClientProvider(
getClient = { sessionId ->
when (sessionId) {
A_USER_ID -> Result.success(client1)
A_USER_ID_2 -> Result.success(client2)
else -> error("Unexpected sessionId")
}
}
),
)
sut.logoutAll(ignoreSdkError = true)
logoutLambda1.assertions().isCalledOnce().with(value(true), value(true))
logoutLambda2.assertions().isCalledOnce().with(value(true), value(true))
}
@Test
fun `test logout session not found is ignored`() = runTest {
val sut = DefaultLogoutUseCase(
sessionStore = InMemorySessionStore(
initialList = listOf(
aSessionData(sessionId = A_USER_ID.value),
)
),
matrixClientProvider = FakeMatrixClientProvider(
getClient = { sessionId ->
when (sessionId) {
A_USER_ID -> Result.failure(Exception("Session not found"))
else -> error("Unexpected sessionId")
}
}
),
)
sut.logoutAll(ignoreSdkError = true)
// No error
}
@Test
fun `test logout no sessions`() = runTest {
val sut = DefaultLogoutUseCase(
sessionStore = InMemorySessionStore(
initialList = emptyList()
),
matrixClientProvider = FakeMatrixClientProvider(
getClient = { sessionId ->
when (sessionId) {
else -> error("Unexpected sessionId")
}
}
),
)
sut.logoutAll(ignoreSdkError = true)
// No error
}
}

View File

@@ -14,7 +14,7 @@ import io.element.android.tests.testutils.simulateLongTask
class FakeLogoutUseCase(
var logoutLambda: (Boolean) -> Unit = { lambdaError() }
) : LogoutUseCase {
override suspend fun logout(ignoreSdkError: Boolean) = simulateLongTask {
override suspend fun logoutAll(ignoreSdkError: Boolean) = simulateLongTask {
logoutLambda(ignoreSdkError)
}
}