diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt index 5c73d08778..ac2d0724f1 100644 --- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt @@ -31,6 +31,8 @@ import io.element.android.libraries.preferences.test.InMemorySessionPreferencesS import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -44,9 +46,9 @@ class DefaultFtueServiceTest { givenVerifiedStatus(SessionVerifiedStatus.Unknown) } val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) - val state = createState(coroutineScope, sessionVerificationService) + val service = createDefaultFtueService(coroutineScope, sessionVerificationService) - state.state.test { + service.state.test { // Verification state is unknown, we don't display the flow yet assertThat(awaitItem()).isEqualTo(FtueState.Unknown) @@ -67,7 +69,7 @@ class DefaultFtueServiceTest { val lockScreenService = FakeLockScreenService() val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) - val state = createState( + val service = createDefaultFtueService( coroutineScope = coroutineScope, sessionVerificationService = sessionVerificationService, analyticsService = analyticsService, @@ -79,9 +81,9 @@ class DefaultFtueServiceTest { analyticsService.setDidAskUserConsent() permissionStateProvider.setPermissionGranted() lockScreenService.setIsPinSetup(true) - state.updateState() + service.updateState() - assertThat(state.state.value).isEqualTo(FtueState.Complete) + assertThat(service.state.value).isEqualTo(FtueState.Complete) // Cleanup coroutineScope.cancel() @@ -97,7 +99,7 @@ class DefaultFtueServiceTest { val lockScreenService = FakeLockScreenService() val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) - val state = createState( + val service = createDefaultFtueService( coroutineScope = coroutineScope, sessionVerificationService = sessionVerificationService, analyticsService = analyticsService, @@ -107,23 +109,23 @@ class DefaultFtueServiceTest { val steps = mutableListOf() // Session verification - steps.add(state.getNextStep(steps.lastOrNull())) + steps.add(service.getNextStep(steps.lastOrNull())) sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified) // Notifications opt in - steps.add(state.getNextStep(steps.lastOrNull())) + steps.add(service.getNextStep(steps.lastOrNull())) permissionStateProvider.setPermissionGranted() // Entering PIN code - steps.add(state.getNextStep(steps.lastOrNull())) + steps.add(service.getNextStep(steps.lastOrNull())) lockScreenService.setIsPinSetup(true) // Analytics opt in - steps.add(state.getNextStep(steps.lastOrNull())) + steps.add(service.getNextStep(steps.lastOrNull())) analyticsService.setDidAskUserConsent() // Final step (null) - steps.add(state.getNextStep(steps.lastOrNull())) + steps.add(service.getNextStep(steps.lastOrNull())) assertThat(steps).containsExactly( FtueStep.SessionVerification, @@ -145,7 +147,7 @@ class DefaultFtueServiceTest { val analyticsService = FakeAnalyticsService() val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false) val lockScreenService = FakeLockScreenService() - val state = createState( + val service = createDefaultFtueService( coroutineScope = coroutineScope, sessionVerificationService = sessionVerificationService, analyticsService = analyticsService, @@ -158,10 +160,10 @@ class DefaultFtueServiceTest { permissionStateProvider.setPermissionGranted() lockScreenService.setIsPinSetup(true) - assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn) + assertThat(service.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn) analyticsService.setDidAskUserConsent() - assertThat(state.getNextStep(null)).isNull() + assertThat(service.getNextStep(null)).isNull() // Cleanup coroutineScope.cancel() @@ -174,7 +176,7 @@ class DefaultFtueServiceTest { val analyticsService = FakeAnalyticsService() val lockScreenService = FakeLockScreenService() - val state = createState( + val service = createDefaultFtueService( sdkIntVersion = Build.VERSION_CODES.M, sessionVerificationService = sessionVerificationService, coroutineScope = coroutineScope, @@ -185,16 +187,61 @@ class DefaultFtueServiceTest { sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified) lockScreenService.setIsPinSetup(true) - assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn) + assertThat(service.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn) analyticsService.setDidAskUserConsent() - assertThat(state.getNextStep(null)).isNull() + assertThat(service.getNextStep(null)).isNull() // Cleanup coroutineScope.cancel() } - private fun createState( + @Test + fun `reset do the expected actions S`() = runTest { + val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) + val resetAnalyticsLambda = lambdaRecorder { } + val analyticsService = FakeAnalyticsService( + resetLambda = resetAnalyticsLambda + ) + val resetPermissionLambda = lambdaRecorder { } + val permissionStateProvider = FakePermissionStateProvider( + resetPermissionLambda = resetPermissionLambda + ) + val service = createDefaultFtueService( + coroutineScope = coroutineScope, + sdkIntVersion = Build.VERSION_CODES.S, + permissionStateProvider = permissionStateProvider, + analyticsService = analyticsService, + ) + service.reset() + resetAnalyticsLambda.assertions().isCalledOnce() + resetPermissionLambda.assertions().isNeverCalled() + } + + @Test + fun `reset do the expected actions TIRAMISU`() = runTest { + val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) + val resetLambda = lambdaRecorder { } + val analyticsService = FakeAnalyticsService( + resetLambda = resetLambda + ) + val resetPermissionLambda = lambdaRecorder { } + val permissionStateProvider = FakePermissionStateProvider( + resetPermissionLambda = resetPermissionLambda + ) + val service = createDefaultFtueService( + coroutineScope = coroutineScope, + sdkIntVersion = Build.VERSION_CODES.TIRAMISU, + permissionStateProvider = permissionStateProvider, + analyticsService = analyticsService, + ) + service.reset() + resetLambda.assertions().isCalledOnce() + resetPermissionLambda.assertions().isCalledOnce() + .with(value("android.permission.POST_NOTIFICATIONS")) + } + + private fun createDefaultFtueService( coroutineScope: CoroutineScope, sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), analyticsService: AnalyticsService = FakeAnalyticsService(), diff --git a/features/ftue/test/build.gradle.kts b/features/ftue/test/build.gradle.kts new file mode 100644 index 0000000000..eb7a3a565c --- /dev/null +++ b/features/ftue/test/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.ftue.test" +} + +dependencies { + implementation(projects.features.ftue.api) + implementation(projects.tests.testutils) +} diff --git a/features/ftue/test/src/main/kotlin/io/element/android/features/ftue/test/FakeFtueService.kt b/features/ftue/test/src/main/kotlin/io/element/android/features/ftue/test/FakeFtueService.kt new file mode 100644 index 0000000000..4f69abd793 --- /dev/null +++ b/features/ftue/test/src/main/kotlin/io/element/android/features/ftue/test/FakeFtueService.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 + * + * 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.ftue.test + +import io.element.android.features.ftue.api.state.FtueService +import io.element.android.features.ftue.api.state.FtueState +import io.element.android.tests.testutils.lambda.lambdaError +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeFtueService( + private val resetLambda: () -> Unit = { lambdaError() }, +) : FtueService { + override val state: MutableStateFlow = MutableStateFlow(FtueState.Unknown) + + override suspend fun reset() { + resetLambda() + } + + suspend fun emitState(newState: FtueState) { + state.emit(newState) + } +} diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index 2e38ff5348..b4d2f54bab 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -89,8 +89,10 @@ dependencies { testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushstore.test) + testImplementation(projects.features.ftue.test) testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.impl) + testImplementation(projects.features.roomlist.test) testImplementation(projects.libraries.indicator.impl) testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.libraries.fullscreenintent.test) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt index 9227eef364..12e5fd40f0 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt @@ -43,7 +43,7 @@ class DefaultClearCacheUseCase @Inject constructor( @ApplicationContext private val context: Context, private val matrixClient: MatrixClient, private val coroutineDispatchers: CoroutineDispatchers, - private val defaultCacheIndexProvider: DefaultCacheService, + private val defaultCacheService: DefaultCacheService, private val okHttpClient: Provider, private val ftueService: FtueService, private val migrationScreenStore: MigrationScreenStore, @@ -65,6 +65,6 @@ class DefaultClearCacheUseCase @Inject constructor( // Clear migration screen store migrationScreenStore.reset() // Ensure the app is restarted - defaultCacheIndexProvider.onClearedCache(matrixClient.sessionId) + defaultCacheService.onClearedCache(matrixClient.sessionId) } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/DefaultClearCacheUseCaseTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/DefaultClearCacheUseCaseTest.kt new file mode 100644 index 0000000000..4b576bf3d1 --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/DefaultClearCacheUseCaseTest.kt @@ -0,0 +1,68 @@ +/* + * 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.preferences.impl.tasks + +import androidx.test.platform.app.InstrumentationRegistry +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.ftue.test.FakeFtueService +import io.element.android.features.preferences.impl.DefaultCacheService +import io.element.android.features.roomlist.impl.migration.InMemoryMigrationScreenStore +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.runTest +import okhttp3.OkHttpClient +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DefaultClearCacheUseCaseTest { + @Test + fun `execute clear cache should do all the expected tasks`() = runTest { + val clearCacheLambda = lambdaRecorder { } + val matrixClient = FakeMatrixClient( + clearCacheLambda = clearCacheLambda, + ) + val defaultCacheService = DefaultCacheService() + val resetFtueLambda = lambdaRecorder { } + val ftueService = FakeFtueService( + resetLambda = resetFtueLambda, + ) + val resetMigrationLambda = lambdaRecorder { } + val migrationScreenStore = InMemoryMigrationScreenStore( + resetLambda = resetMigrationLambda, + ) + val sut = DefaultClearCacheUseCase( + context = InstrumentationRegistry.getInstrumentation().context, + matrixClient = matrixClient, + coroutineDispatchers = testCoroutineDispatchers(), + defaultCacheService = defaultCacheService, + okHttpClient = { OkHttpClient.Builder().build() }, + ftueService = ftueService, + migrationScreenStore = migrationScreenStore + ) + defaultCacheService.clearedCacheEventFlow.test { + sut.invoke() + clearCacheLambda.assertions().isCalledOnce() + resetFtueLambda.assertions().isCalledOnce() + resetMigrationLambda.assertions().isCalledOnce() + assertThat(awaitItem()).isEqualTo(matrixClient.sessionId) + } + } +} diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index 1ee3cb2cca..07cf9b1d22 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -84,6 +84,7 @@ dependencies { testImplementation(projects.services.analytics.test) testImplementation(projects.services.toolbox.test) testImplementation(projects.features.networkmonitor.test) + testImplementation(projects.features.roomlist.test) testImplementation(projects.tests.testutils) testImplementation(projects.features.leaveroom.test) } diff --git a/features/roomlist/test/build.gradle.kts b/features/roomlist/test/build.gradle.kts new file mode 100644 index 0000000000..f3bef445d4 --- /dev/null +++ b/features/roomlist/test/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * 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. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.roomlist.test" +} + +dependencies { + implementation(projects.features.roomlist.api) + implementation(projects.libraries.matrix.api) + implementation(projects.tests.testutils) +} diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/migration/InMemoryMigrationScreenStore.kt b/features/roomlist/test/src/main/kotlin/io/element/android/features/roomlist/impl/migration/InMemoryMigrationScreenStore.kt similarity index 90% rename from features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/migration/InMemoryMigrationScreenStore.kt rename to features/roomlist/test/src/main/kotlin/io/element/android/features/roomlist/impl/migration/InMemoryMigrationScreenStore.kt index 79d0b7c628..6c6b1ce3c1 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/migration/InMemoryMigrationScreenStore.kt +++ b/features/roomlist/test/src/main/kotlin/io/element/android/features/roomlist/impl/migration/InMemoryMigrationScreenStore.kt @@ -19,7 +19,9 @@ package io.element.android.features.roomlist.impl.migration import io.element.android.features.roomlist.api.migration.MigrationScreenStore import io.element.android.libraries.matrix.api.core.SessionId -class InMemoryMigrationScreenStore : MigrationScreenStore { +class InMemoryMigrationScreenStore( + private val resetLambda: () -> Unit = { } +) : MigrationScreenStore { private val store = mutableMapOf() override fun isMigrationScreenNeeded(sessionId: SessionId): Boolean { @@ -33,5 +35,6 @@ class InMemoryMigrationScreenStore : MigrationScreenStore { override fun reset() { store.clear() + resetLambda() } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 8f17c3547d..d2eb47a585 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -48,6 +48,7 @@ import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryS import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.simulateLongTask import kotlinx.collections.immutable.ImmutableList @@ -80,6 +81,7 @@ class FakeMatrixClient( private val accountManagementUrlString: Result = Result.success(null), private val resolveRoomAliasResult: (RoomAlias) -> Result = { Result.success(ResolvedRoomAlias(A_ROOM_ID, emptyList())) }, private val getRoomPreviewFromRoomIdResult: (RoomId, List) -> Result = { _, _ -> Result.failure(AN_EXCEPTION) }, + private val clearCacheLambda: () -> Unit = { lambdaError() }, ) : MatrixClient { var setDisplayNameCalled: Boolean = false private set @@ -161,6 +163,7 @@ class FakeMatrixClient( } override suspend fun clearCache() { + clearCacheLambda() } override suspend fun logout(ignoreSdkError: Boolean): String? = simulateLongTask { diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt index 9d7d4d4e65..245cb564f5 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/FakePermissionStateProvider.kt @@ -24,6 +24,7 @@ class FakePermissionStateProvider( private var permissionGranted: Boolean = true, permissionDenied: Boolean = false, permissionAsked: Boolean = false, + private val resetPermissionLambda: (String) -> Unit = {}, ) : PermissionStateProvider { private val permissionDeniedFlow = MutableStateFlow(permissionDenied) private val permissionAskedFlow = MutableStateFlow(permissionAsked) @@ -49,5 +50,6 @@ class FakePermissionStateProvider( override suspend fun resetPermission(permission: String) { setPermissionAsked(permission, false) setPermissionDenied(permission, false) + resetPermissionLambda(permission) } } diff --git a/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt b/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt index 0be41517ed..63186609d9 100644 --- a/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt +++ b/services/analytics/test/src/main/kotlin/io/element/android/services/analytics/test/FakeAnalyticsService.kt @@ -27,7 +27,8 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeAnalyticsService( isEnabled: Boolean = false, - didAskUserConsent: Boolean = false + didAskUserConsent: Boolean = false, + private val resetLambda: () -> Unit = {}, ) : AnalyticsService { private val isEnabledFlow = MutableStateFlow(isEnabled) private val didAskUserConsentFlow = MutableStateFlow(didAskUserConsent) @@ -77,5 +78,6 @@ class FakeAnalyticsService( override suspend fun reset() { didAskUserConsentFlow.value = false + resetLambda() } }