From be61b89bfb30e0db46dfd3a02051f03b4c5b0bc0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 18 Sep 2025 09:44:02 +0200 Subject: [PATCH] When logging out from Pin code screen, logout from all the sessions. --- .../impl/unlock/PinUnlockPresenter.kt | 2 +- .../features/logout/api/LogoutUseCase.kt | 10 +- features/logout/impl/build.gradle.kts | 1 + .../logout/impl/DefaultLogoutUseCase.kt | 31 +++-- .../logout/impl/DefaultLogoutUseCaseTest.kt | 120 ++++++++++++++++++ .../features/logout/test/FakeLogoutUseCase.kt | 2 +- 6 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/DefaultLogoutUseCaseTest.kt diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index cf0f864f66..fc2e61d404 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -174,7 +174,7 @@ class PinUnlockPresenter( private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch { suspend { - logoutUseCase.logout(ignoreSdkError = true) + logoutUseCase.logoutAll(ignoreSdkError = true) }.runCatchingUpdatingState(signOutAction) } } diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutUseCase.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutUseCase.kt index 6f265ec88c..3d980fe44b 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutUseCase.kt +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutUseCase.kt @@ -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) } diff --git a/features/logout/impl/build.gradle.kts b/features/logout/impl/build.gradle.kts index b999d10db2..215c273913 100644 --- a/features/logout/impl/build.gradle.kts +++ b/features/logout/impl/build.gradle.kts @@ -39,4 +39,5 @@ dependencies { testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.featureflag.test) + testImplementation(projects.libraries.sessionStorage.test) } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutUseCase.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutUseCase.kt index 06a79a8d2d..52e295ba3e 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutUseCase.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutUseCase.kt @@ -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") + } + ) + } } } diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/DefaultLogoutUseCaseTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/DefaultLogoutUseCaseTest.kt new file mode 100644 index 0000000000..a17e7285de --- /dev/null +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/DefaultLogoutUseCaseTest.kt @@ -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 { _, _ -> } + 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 { _, _ -> } + val logoutLambda2 = lambdaRecorder { _, _ -> } + 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 + } +} diff --git a/features/logout/test/src/main/kotlin/io/element/android/features/logout/test/FakeLogoutUseCase.kt b/features/logout/test/src/main/kotlin/io/element/android/features/logout/test/FakeLogoutUseCase.kt index e71266b596..dd3ded4ef9 100644 --- a/features/logout/test/src/main/kotlin/io/element/android/features/logout/test/FakeLogoutUseCase.kt +++ b/features/logout/test/src/main/kotlin/io/element/android/features/logout/test/FakeLogoutUseCase.kt @@ -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) } }