diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index 21f3e05421..b8dc1c5035 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation(projects.libraries.featureflag.api) implementation(projects.libraries.cryptography.api) implementation(projects.libraries.preferences.api) + implementation(projects.features.logout.api) implementation(projects.libraries.uiStrings) implementation(projects.libraries.sessionStorage.api) implementation(projects.services.appnavstate.api) @@ -59,4 +60,5 @@ dependencies { testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.services.appnavstate.test) + testImplementation(projects.features.logout.test) } 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 db56b8c17b..b563b3ac70 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 @@ -29,7 +29,7 @@ import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockMana import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel -import io.element.android.features.lockscreen.impl.unlock.signout.SignOut +import io.element.android.features.logout.api.LogoutUseCase import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState @@ -41,7 +41,7 @@ import javax.inject.Inject class PinUnlockPresenter @Inject constructor( private val pinCodeManager: PinCodeManager, private val biometricUnlockManager: BiometricUnlockManager, - private val signOut: SignOut, + private val logoutUseCase: LogoutUseCase, private val coroutineScope: CoroutineScope, private val pinUnlockHelper: PinUnlockHelper, ) : Presenter { @@ -179,7 +179,7 @@ class PinUnlockPresenter @Inject constructor( private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch { suspend { - signOut() + logoutUseCase.logout(ignoreSdkError = true) }.runCatchingUpdatingState(signOutAction) } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt index ddd62d2fb6..0f71222ebd 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt @@ -18,9 +18,9 @@ package io.element.android.features.lockscreen.impl.unlock.di import com.squareup.anvil.annotations.ContributesTo import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity -import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SessionScope -@ContributesTo(AppScope::class) +@ContributesTo(SessionScope::class) interface PinUnlockBindings { fun inject(activity: PinUnlockActivity) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/DefaultSignOut.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/DefaultSignOut.kt deleted file mode 100644 index 2c541911e6..0000000000 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/DefaultSignOut.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2024 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 io.element.android.features.lockscreen.impl.unlock.signout - -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.matrix.api.MatrixClientProvider -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import javax.inject.Inject - -@ContributesBinding(AppScope::class) -class DefaultSignOut @Inject constructor( - private val authenticationService: MatrixAuthenticationService, - private val matrixClientProvider: MatrixClientProvider, -) : SignOut { - override suspend fun invoke(): String? { - val currentSession = authenticationService.getLatestSessionId() - return if (currentSession != null) { - matrixClientProvider.getOrRestore(currentSession) - .getOrThrow() - .logout(ignoreSdkError = true) - } else { - error("No session to sign out") - } - } -} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/DefaultSignOutTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/DefaultSignOutTest.kt deleted file mode 100644 index 392693d9ef..0000000000 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/DefaultSignOutTest.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2024 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 io.element.android.features.lockscreen.impl.unlock - -import com.google.common.truth.Truth.assertThat -import io.element.android.features.lockscreen.impl.unlock.signout.DefaultSignOut -import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.matrix.test.FakeMatrixClientProvider -import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService -import io.element.android.tests.testutils.lambda.assert -import io.element.android.tests.testutils.lambda.lambdaRecorder -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class DefaultSignOutTest { - private val matrixClient = FakeMatrixClient() - private val authenticationService = FakeMatrixAuthenticationService() - private val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }) - private val sut = DefaultSignOut(authenticationService, matrixClientProvider) - - @Test - fun `when no active session then it throws`() = runTest { - authenticationService.getLatestSessionIdLambda = { null } - val result = runCatching { sut.invoke() } - assertThat(result.isFailure).isTrue() - } - - @Test - fun `with one active session and successful logout on client`() = runTest { - val logoutLambda = lambdaRecorder { _: Boolean -> null } - authenticationService.getLatestSessionIdLambda = { matrixClient.sessionId } - matrixClient.logoutLambda = logoutLambda - val result = runCatching { sut.invoke() } - assertThat(result.isSuccess).isTrue() - assert(logoutLambda).isCalledOnce() - } - - @Test - fun `with one active session and and failed logout on client`() = runTest { - val logoutLambda = lambdaRecorder { _: Boolean -> error("Failed to logout") } - authenticationService.getLatestSessionIdLambda = { matrixClient.sessionId } - matrixClient.logoutLambda = logoutLambda - val result = runCatching { sut.invoke() } - assertThat(result.isFailure).isTrue() - assert(logoutLambda).isCalledOnce() - } -} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt index 89d0e92ee2..8299b5a1ab 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt @@ -28,7 +28,7 @@ import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel -import io.element.android.features.lockscreen.impl.unlock.signout.SignOut +import io.element.android.features.logout.test.FakeLogoutUseCase import io.element.android.libraries.architecture.AsyncData import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -106,9 +106,9 @@ class PinUnlockPresenterTest { @Test fun `present - forgot pin flow`() = runTest { - val signOutLambda = lambdaRecorder { null } - val signOut = FakeSignOut(signOutLambda) - val presenter = createPinUnlockPresenter(this, signOut = signOut) + val signOutLambda = lambdaRecorder { "" } + val signOut = FakeLogoutUseCase(signOutLambda) + val presenter = createPinUnlockPresenter(this, logoutUseCase = signOut) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -135,7 +135,7 @@ class PinUnlockPresenterTest { awaitItem().also { state -> assertThat(state.signOutAction).isInstanceOf(AsyncData.Success::class.java) } - assert(signOutLambda).isCalledOnce().withNoParameter() + assert(signOutLambda).isCalledOnce() } } @@ -147,7 +147,7 @@ class PinUnlockPresenterTest { scope: CoroutineScope, biometricUnlockManager: BiometricUnlockManager = FakeBiometricUnlockManager(), callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(), - signOut: SignOut = FakeSignOut(), + logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }), ): PinUnlockPresenter { val pinCodeManager = aPinCodeManager().apply { addCallback(callback) @@ -156,7 +156,7 @@ class PinUnlockPresenterTest { return PinUnlockPresenter( pinCodeManager = pinCodeManager, biometricUnlockManager = biometricUnlockManager, - signOut = signOut, + logoutUseCase = logoutUseCase, coroutineScope = scope, pinUnlockHelper = PinUnlockHelper(biometricUnlockManager, pinCodeManager), ) 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 397f5ebed7..a06b7117b1 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 @@ -23,6 +23,11 @@ interface LogoutUseCase { /** * Log out the current user 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. + * @return the session id of the logged out user. */ - suspend fun logout(ignoreSdkError: Boolean) + suspend fun logout(ignoreSdkError: Boolean): String + + interface Factory { + fun create(sessionId: String): LogoutUseCase + } } 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 52bf0425d4..8e5f08a87a 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 @@ -17,16 +17,27 @@ package io.element.android.features.logout.impl import com.squareup.anvil.annotations.ContributesBinding +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import io.element.android.features.logout.api.LogoutUseCase -import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.api.MatrixClient -import javax.inject.Inject +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.core.SessionId -@ContributesBinding(SessionScope::class) -class DefaultLogoutUseCase @Inject constructor( - private val matrixClient: MatrixClient +class DefaultLogoutUseCase @AssistedInject constructor( + @Assisted private val sessionId: String, + private val matrixClientProvider: MatrixClientProvider, ) : LogoutUseCase { - override suspend fun logout(ignoreSdkError: Boolean) { + @ContributesBinding(AppScope::class) + @AssistedFactory + interface Factory : LogoutUseCase.Factory { + override fun create(sessionId: String): DefaultLogoutUseCase + } + + override suspend fun logout(ignoreSdkError: Boolean): String { + val matrixClient = matrixClientProvider.getOrRestore(SessionId(sessionId)).getOrThrow() matrixClient.logout(ignoreSdkError = ignoreSdkError) + return sessionId } } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/SessionLogoutModule.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/SessionLogoutModule.kt new file mode 100644 index 0000000000..ec725a34a6 --- /dev/null +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/SessionLogoutModule.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 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 + * + * https://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 io.element.android.features.logout.impl + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import io.element.android.features.logout.api.LogoutUseCase +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder + +@Module +@ContributesTo(SessionScope::class) +object SessionLogoutModule { + @Provides + fun provideLogoutUseCase( + currentSessionIdHolder: CurrentSessionIdHolder, + factory: DefaultLogoutUseCase.Factory, + ): LogoutUseCase { + return factory.create(currentSessionIdHolder.current.value) + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/SignOut.kt b/features/logout/test/build.gradle.kts similarity index 64% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/SignOut.kt rename to features/logout/test/build.gradle.kts index f4c91aece6..7be20ef279 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/SignOut.kt +++ b/features/logout/test/build.gradle.kts @@ -5,7 +5,7 @@ * 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 + * https://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, @@ -14,8 +14,16 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.unlock.signout - -interface SignOut { - suspend operator fun invoke(): String? +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.logout.test" +} + +dependencies { + implementation(libs.coroutines.core) + implementation(projects.tests.testutils) + api(projects.features.logout.api) } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/FakeSignOut.kt b/features/logout/test/src/main/kotlin/io/element/android/features/logout/test/FakeLogoutUseCase.kt similarity index 56% rename from features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/FakeSignOut.kt rename to features/logout/test/src/main/kotlin/io/element/android/features/logout/test/FakeLogoutUseCase.kt index 883a5bf97b..bbc67765d1 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/FakeSignOut.kt +++ b/features/logout/test/src/main/kotlin/io/element/android/features/logout/test/FakeLogoutUseCase.kt @@ -5,7 +5,7 @@ * 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 + * https://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, @@ -14,15 +14,15 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.unlock +package io.element.android.features.logout.test -import io.element.android.features.lockscreen.impl.unlock.signout.SignOut -import io.element.android.tests.testutils.simulateLongTask +import io.element.android.features.logout.api.LogoutUseCase +import io.element.android.tests.testutils.lambda.lambdaError -class FakeSignOut( - var lambda: () -> String? = { null } -) : SignOut { - override suspend fun invoke(): String? = simulateLongTask { - lambda() +class FakeLogoutUseCase( + var logoutLambda: (Boolean) -> String = lambdaError() +) : LogoutUseCase { + override suspend fun logout(ignoreSdkError: Boolean): String { + return logoutLambda(ignoreSdkError) } } diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index ead4515360..ec265d739e 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -90,6 +90,7 @@ dependencies { testImplementation(projects.features.ftue.test) testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.impl) + testImplementation(projects.features.logout.test) testImplementation(projects.features.roomlist.test) testImplementation(projects.libraries.indicator.impl) testImplementation(projects.libraries.pushproviders.test) diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index be3c6bbb20..9fb1c8b37f 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -21,7 +21,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.appconfig.ElementCallConfig -import io.element.android.features.logout.api.LogoutUseCase +import io.element.android.features.logout.test.FakeLogoutUseCase import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter @@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.awaitLastSequentialItem +import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -165,7 +166,8 @@ class DeveloperSettingsPresenterTest { @Test fun `present - toggling simplified sliding sync changes the preferences and logs out the user`() = runTest { - val logoutUseCase = FakeLogoutUseCase() + val logoutCallRecorder = lambdaRecorder { "" } + val logoutUseCase = FakeLogoutUseCase(logoutLambda = logoutCallRecorder) val preferences = InMemoryAppPreferencesStore() val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences, logoutUseCase = logoutUseCase) moleculeFlow(RecompositionMode.Immediate) { @@ -177,12 +179,12 @@ class DeveloperSettingsPresenterTest { initialState.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(true)) assertThat(awaitItem().isSimpleSlidingSyncEnabled).isTrue() assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue() - assertThat(logoutUseCase.logoutCallCount).isEqualTo(1) + logoutCallRecorder.assertions().isCalledOnce() initialState.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(false)) assertThat(awaitItem().isSimpleSlidingSyncEnabled).isFalse() assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse() - assertThat(logoutUseCase.logoutCallCount).isEqualTo(2) + logoutCallRecorder.assertions().isCalledExactly(times = 2) } } @@ -193,7 +195,7 @@ class DeveloperSettingsPresenterTest { rageshakePresenter: DefaultRageshakePreferencesPresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()), preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), buildMeta: BuildMeta = aBuildMeta(), - logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase() + logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }) ): DeveloperSettingsPresenter { return DeveloperSettingsPresenter( featureFlagService = featureFlagService, @@ -206,12 +208,3 @@ class DeveloperSettingsPresenterTest { ) } } - -private class FakeLogoutUseCase : LogoutUseCase { - var logoutCallCount = 0 - private set - - override suspend fun logout(ignoreSdkError: Boolean) { - logoutCallCount++ - } -}