From ad3315196899c2a85c7ea46998f72b62612bdc6e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 6 Feb 2025 15:50:02 +0100 Subject: [PATCH 1/3] Exclude some class from State coverage check > Rule 'Check code coverage of states' violated: instructions covered percentage for class 'io.element.android.libraries.mediaviewer.impl.local.player.MediaPlayerControllerState' is 0.000000, but expected minimum is 90 instructions covered percentage for class 'io.element.android.libraries.textcomposer.components.FormattingOptionState' is 0.000000, but expected minimum is 90 --- plugins/src/main/kotlin/extension/KoverExtension.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/src/main/kotlin/extension/KoverExtension.kt b/plugins/src/main/kotlin/extension/KoverExtension.kt index 35aa9042446..c361aeba52e 100644 --- a/plugins/src/main/kotlin/extension/KoverExtension.kt +++ b/plugins/src/main/kotlin/extension/KoverExtension.kt @@ -54,7 +54,6 @@ fun Project.setupKover() { description = "Verifies the code coverage of all subprojects." val dependencies = listOf(":app:koverVerifyGplayDebug") + koverVariants.map { ":app:koverVerify${it.replaceFirstChar(Char::titlecase)}" } dependsOn(dependencies) - } // https://kotlin.github.io/kotlinx-kover/ // Run `./gradlew :app:koverHtmlReport` to get report at ./app/build/reports/kover @@ -180,7 +179,9 @@ fun Project.setupKover() { "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*", "io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*", "io.element.android.libraries.mediaviewer.impl.local.pdf.PdfViewerState", + "io.element.android.libraries.mediaviewer.impl.local.player.MediaPlayerControllerState", "io.element.android.libraries.textcomposer.model.TextEditorState", + "io.element.android.libraries.textcomposer.components.FormattingOptionState", ) includes.classes("*State") } From 6b12d459eafdf8b0b37968034877ddca52460de6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 6 Feb 2025 16:11:54 +0100 Subject: [PATCH 2/3] Add unit test for SecureBackupRootState --- .../impl/root/SecureBackupRootStateTest.kt | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateTest.kt diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateTest.kt new file mode 100644 index 00000000000..1e4f58a84a6 --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateTest.kt @@ -0,0 +1,63 @@ +/* + * 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. + */ + +package io.element.android.features.securebackup.impl.root + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.encryption.BackupState +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import org.junit.Test + +class SecureBackupRootStateTest { + @Test + fun `isKeyStorageEnabled should be true for all these backup states`() { + listOf( + BackupState.CREATING, + BackupState.ENABLING, + BackupState.RESUMING, + BackupState.DOWNLOADING, + BackupState.ENABLED, + ).forEach { backupState -> + assertThat(aSecureBackupRootState(backupState = backupState).isKeyStorageEnabled).isTrue() + } + } + + @Test + fun `isKeyStorageEnabled should be false for all these backup states`() { + listOf( + BackupState.WAITING_FOR_SYNC, + BackupState.DISABLING, + ).forEach { backupState -> + assertThat(aSecureBackupRootState(backupState = backupState).isKeyStorageEnabled).isFalse() + } + } + + @Test + fun `isKeyStorageEnabled should have value depending on doesBackupExistOnServer when state is UNKNOWN`() { + assertThat( + aSecureBackupRootState( + backupState = BackupState.UNKNOWN, + doesBackupExistOnServer = AsyncData.Success(true), + ).isKeyStorageEnabled + ).isTrue() + + listOf( + AsyncData.Uninitialized, + AsyncData.Loading(), + AsyncData.Failure(AN_EXCEPTION), + AsyncData.Success(false), + ).forEach { doesBackupExistOnServer -> + assertThat( + aSecureBackupRootState( + backupState = BackupState.UNKNOWN, + doesBackupExistOnServer = doesBackupExistOnServer, + ).isKeyStorageEnabled + ).isFalse() + } + } +} From 54af22924551907c0327d76a5abebc94ab149c9f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 6 Feb 2025 16:34:56 +0100 Subject: [PATCH 3/3] Add unit test for PinUnlockState --- .../impl/unlock/PinUnlockStateProvider.kt | 14 ++-- .../impl/unlock/PinUnlockStateTest.kt | 76 +++++++++++++++++++ 2 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateTest.kt diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index 51c05820dd8..77d20585d1b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -23,17 +23,19 @@ open class PinUnlockStateProvider : PreviewParameterProvider { aPinUnlockState(showWrongPinTitle = true), aPinUnlockState(showSignOutPrompt = true), aPinUnlockState(showBiometricUnlock = false), - aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0), + aPinUnlockState(showSignOutPrompt = true, remainingAttempts = AsyncData.Success(0)), aPinUnlockState(signOutAction = AsyncAction.Loading), - aPinUnlockState(biometricUnlockResult = BiometricAuthenticator.AuthenticationResult.Failure( - BiometricUnlockError(BiometricPrompt.ERROR_LOCKOUT, "Biometric auth disabled") - )), + aPinUnlockState( + biometricUnlockResult = BiometricAuthenticator.AuthenticationResult.Failure( + BiometricUnlockError(BiometricPrompt.ERROR_LOCKOUT, "Biometric auth disabled") + ) + ), ) } fun aPinUnlockState( pinEntry: PinEntry = PinEntry.createEmpty(4), - remainingAttempts: Int = 3, + remainingAttempts: AsyncData = AsyncData.Success(3), showWrongPinTitle: Boolean = false, showSignOutPrompt: Boolean = false, showBiometricUnlock: Boolean = true, @@ -43,7 +45,7 @@ fun aPinUnlockState( ) = PinUnlockState( pinEntry = AsyncData.Success(pinEntry), showWrongPinTitle = showWrongPinTitle, - remainingAttempts = AsyncData.Success(remainingAttempts), + remainingAttempts = remainingAttempts, showSignOutPrompt = showSignOutPrompt, showBiometricUnlock = showBiometricUnlock, signOutAction = signOutAction, diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateTest.kt new file mode 100644 index 00000000000..389ebf83c73 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateTest.kt @@ -0,0 +1,76 @@ +/* + * 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. + */ + +package io.element.android.features.lockscreen.impl.unlock + +import androidx.biometric.BiometricPrompt +import com.google.common.truth.Truth.assertThat +import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator +import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockError +import io.element.android.libraries.architecture.AsyncData +import org.junit.Test + +class PinUnlockStateTest { + @Test + fun `isSignOutPromptCancellable should have expected values`() { + assertThat(aPinUnlockState(remainingAttempts = AsyncData.Uninitialized).isSignOutPromptCancellable).isTrue() + assertThat(aPinUnlockState(remainingAttempts = AsyncData.Success(1)).isSignOutPromptCancellable).isTrue() + assertThat(aPinUnlockState(remainingAttempts = AsyncData.Success(0)).isSignOutPromptCancellable).isFalse() + } + + @Test + fun `biometricUnlockErrorMessage and showBiometricUnlockError should have expected values`() { + listOf( + null, + BiometricAuthenticator.AuthenticationResult.Failure(), + BiometricAuthenticator.AuthenticationResult.Success, + ).forEach { biometricUnlockResult -> + aPinUnlockState( + biometricUnlockResult = biometricUnlockResult, + ).let { + assertThat(it.biometricUnlockErrorMessage).isNull() + assertThat(it.showBiometricUnlockError).isFalse() + } + } + listOf( + BiometricPrompt.ERROR_HW_UNAVAILABLE, + BiometricPrompt.ERROR_UNABLE_TO_PROCESS, + BiometricPrompt.ERROR_TIMEOUT, + BiometricPrompt.ERROR_NO_SPACE, + BiometricPrompt.ERROR_CANCELED, + BiometricPrompt.ERROR_VENDOR, + BiometricPrompt.ERROR_USER_CANCELED, + BiometricPrompt.ERROR_NO_BIOMETRICS, + BiometricPrompt.ERROR_HW_NOT_PRESENT, + BiometricPrompt.ERROR_NEGATIVE_BUTTON, + BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL, + BiometricPrompt.ERROR_SECURITY_UPDATE_REQUIRED, + ).forEach { code -> + aPinUnlockState( + biometricUnlockResult = BiometricAuthenticator.AuthenticationResult.Failure( + error = BiometricUnlockError(code, "Error message") + ), + ).let { + assertThat(it.biometricUnlockErrorMessage).isNull() + assertThat(it.showBiometricUnlockError).isFalse() + } + } + listOf( + BiometricPrompt.ERROR_LOCKOUT, + BiometricPrompt.ERROR_LOCKOUT_PERMANENT, + ).forEach { code -> + aPinUnlockState( + biometricUnlockResult = BiometricAuthenticator.AuthenticationResult.Failure( + error = BiometricUnlockError(code, "Error message") + ), + ).let { + assertThat(it.biometricUnlockErrorMessage).isEqualTo("Error message") + assertThat(it.showBiometricUnlockError).isTrue() + } + } + } +}