diff --git a/app/src/androidTest/kotlin/com/wire/android/ui/debug/DebugScreenComposeTest.kt b/app/src/androidTest/kotlin/com/wire/android/ui/debug/DebugScreenComposeTest.kt index be76cdd94eb..761b5d161d9 100644 --- a/app/src/androidTest/kotlin/com/wire/android/ui/debug/DebugScreenComposeTest.kt +++ b/app/src/androidTest/kotlin/com/wire/android/ui/debug/DebugScreenComposeTest.kt @@ -41,8 +41,8 @@ class DebugScreenComposeTest { onDeleteLogs = {}, onDatabaseLoggerEnabledChanged = {}, onShowFeatureFlags = {}, + onShowCryptoStats = {}, onFlushLogs = { CompletableDeferred(Unit) }, - onShowCryptoStats = {} ) } } diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt index 1469f1473c9..c60703ddad7 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt @@ -23,10 +23,12 @@ import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.debug.BreakSessionUseCase import com.wire.kalium.logic.feature.debug.DebugScope +import com.wire.kalium.logic.feature.debug.GetDebugE2EICertificateExpirationUseCase import com.wire.kalium.logic.feature.debug.GetFeatureConfigUseCase import com.wire.kalium.logic.feature.debug.GetConversationCryptoStatsUseCase import com.wire.kalium.logic.feature.debug.GetConversationEpochFromCCUseCase import com.wire.kalium.logic.feature.debug.RepairFaultyRemovalKeysUseCase +import com.wire.kalium.logic.feature.debug.SetDebugE2EICertificateExpirationUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -35,6 +37,7 @@ import dagger.hilt.android.scopes.ViewModelScoped @Module @InstallIn(ViewModelComponent::class) +@Suppress("TooManyFunctions") class DebugModule { @ViewModelScoped @@ -81,6 +84,16 @@ class DebugModule { @Provides fun provideFeatureConfigUseCase(debugScope: DebugScope): GetFeatureConfigUseCase = debugScope.getFeatureConfig + @ViewModelScoped + @Provides + fun provideGetDebugE2EICertificateExpirationUseCase(debugScope: DebugScope): GetDebugE2EICertificateExpirationUseCase = + debugScope.getDebugE2EICertificateExpiration + + @ViewModelScoped + @Provides + fun provideSetDebugE2EICertificateExpirationUseCase(debugScope: DebugScope): SetDebugE2EICertificateExpirationUseCase = + debugScope.setDebugE2EICertificateExpiration + @ViewModelScoped @Provides fun provideGetConversationEpochFromCCUseCase(debugScope: DebugScope): GetConversationEpochFromCCUseCase = diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index e8739b95654..2bd547acbbb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -55,6 +55,7 @@ import androidx.navigation.NavController import androidx.navigation.compose.currentBackStackEntryAsState import com.ramcosta.composedestinations.generated.app.destinations.E2EIEnrollmentScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.E2EiCertificateDetailsScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.DebugScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.HomeScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.LogManagementScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.LoginScreenDestination @@ -385,7 +386,7 @@ class WireActivity : BaseActivity() { shakeDetector.observeShakes() .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED) .collectLatest { - handleLogManagementShake(currentNavigator) + handleShakeShortcut(currentNavigator) } } } @@ -695,12 +696,22 @@ class WireActivity : BaseActivity() { } } - private fun handleLogManagementShake(navigator: Navigator) { + private fun handleShakeShortcut(navigator: Navigator) { runOnUiThread { val currentRoute = navigator.navController.currentDestination?.route?.getBaseRoute() - val targetRoute = LogManagementScreenDestination.baseRoute + val shouldOpenDebugTools = BuildConfig.PRIVATE_BUILD && BuildConfig.DEBUG_SCREEN_ENABLED + val targetRoute = if (shouldOpenDebugTools) { + DebugScreenDestination.baseRoute + } else { + LogManagementScreenDestination.baseRoute + } if (currentRoute == targetRoute) return@runOnUiThread - navigator.navigate(NavigationCommand(LogManagementScreenDestination, BackStackMode.UPDATE_EXISTED)) + val target = if (shouldOpenDebugTools) { + DebugScreenDestination + } else { + LogManagementScreenDestination + } + navigator.navigate(NavigationCommand(target, BackStackMode.UPDATE_EXISTED)) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt index 26ddfc8134e..ddbe202fc30 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt @@ -18,11 +18,21 @@ package com.wire.android.ui.debug import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.background +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.wire.android.BuildConfig @@ -35,6 +45,9 @@ import com.wire.android.ui.common.WireDialogButtonProperties import com.wire.android.ui.common.WireDialogButtonType import com.wire.android.ui.common.button.WirePrimaryButton import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.textfield.maxLengthDigits +import com.wire.android.ui.common.textfield.WireTextField +import com.wire.android.ui.common.textfield.WireTextFieldState import com.wire.android.ui.common.rowitem.RowItemTemplate import com.wire.android.ui.common.rowitem.SectionHeader import com.wire.android.ui.common.snackbar.LocalSnackbarHostState @@ -46,6 +59,7 @@ import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography import com.wire.android.util.ui.PreviewMultipleThemes +import com.wire.kalium.logic.feature.debug.MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS import com.wire.kalium.logic.feature.e2ei.usecase.FinalizeEnrollmentResult @Composable @@ -68,6 +82,7 @@ fun DebugDataOptions( onForceUpdateApiVersions = viewModel::forceUpdateApiVersions, onDisableEventProcessingChange = viewModel::disableEventProcessing, enrollE2EICertificate = viewModel::enrollE2EICertificate, + e2eiCertificateExpirationInputState = viewModel.e2eiCertificateExpirationInputState, handleE2EIEnrollmentResult = viewModel::handleE2EIEnrollmentResult, dismissCertificateDialog = viewModel::dismissCertificateDialog, checkCrlRevocationList = viewModel::checkCrlRevocationList, @@ -91,6 +106,7 @@ fun DebugDataOptionsContent( onRestartSlowSyncForRecovery: () -> Unit, onForceUpdateApiVersions: () -> Unit, enrollE2EICertificate: () -> Unit, + e2eiCertificateExpirationInputState: TextFieldState, handleE2EIEnrollmentResult: (FinalizeEnrollmentResult) -> Unit, dismissCertificateDialog: () -> Unit, checkCrlRevocationList: () -> Unit, @@ -193,9 +209,10 @@ fun DebugDataOptionsContent( trailingIcon = R.drawable.ic_arrow_right, ) - if (BuildConfig.DEBUG) { - GetE2EICertificateSwitch( - enrollE2EI = enrollE2EICertificate + if (BuildConfig.PRIVATE_BUILD && BuildConfig.DEBUG_SCREEN_ENABLED) { + E2EICertificateEnrollmentSection( + expirationInputState = e2eiCertificateExpirationInputState, + enrollE2EI = enrollE2EICertificate, ) if (state.showCertificate) { @@ -246,31 +263,64 @@ fun DebugDataOptionsContent( } @Composable -private fun GetE2EICertificateSwitch( +private fun E2EICertificateEnrollmentSection( + expirationInputState: TextFieldState, enrollE2EI: () -> Unit ) { + val minExpirationMinutes = MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS / 60 + val enteredMinutes = expirationInputState.text.toString().toLongOrNull() + val isInputBelowMinimum = enteredMinutes?.let { it < minExpirationMinutes } == true + Column { SectionHeader(stringResource(R.string.debug_settings_e2ei_enrollment_title)) - RowItemTemplate( - modifier = Modifier.wrapContentWidth(), - title = { - Text( - style = MaterialTheme.wireTypography.body01, - color = MaterialTheme.wireColorScheme.onBackground, - text = stringResource(R.string.label_get_e2ei_cetificate), - modifier = Modifier.padding(start = dimensions().spacing8x) + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.wireColorScheme.surface) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(dimensions().spacing8x), + verticalAlignment = Alignment.CenterVertically + ) { + WireTextField( + modifier = Modifier + .width(dimensions().spacing156x), + textState = expirationInputState, + labelText = stringResource(R.string.debug_settings_e2ei_expiration_time), + inputTransformation = InputTransformation.maxLengthDigits(maxLength = 6), + state = if (isInputBelowMinimum) WireTextFieldState.Error() else WireTextFieldState.Default, + trailingIcon = { + Text( + text = stringResource(R.string.debug_settings_e2ei_expiration_minutes_suffix), + modifier = Modifier.padding(horizontal = dimensions().spacing8x) + ) + } ) - }, - actions = { + Spacer(modifier = Modifier.weight(1f)) WirePrimaryButton( - onClick = { - enrollE2EI() - }, - text = stringResource(R.string.label_get_e2ei_cetificate), - fillMaxWidth = false + onClick = enrollE2EI, + text = stringResource(R.string.debug_settings_e2ei_enroll_button), + fillMaxWidth = false, + minSize = MaterialTheme.wireDimensions.buttonMediumMinSize, + minClickableSize = MaterialTheme.wireDimensions.buttonMinClickableSize ) } - ) + if (isInputBelowMinimum) { + Text( + text = stringResource(R.string.debug_settings_e2ei_expiration_min_error, minExpirationMinutes), + style = MaterialTheme.wireTypography.label04, + color = MaterialTheme.wireColorScheme.error, + modifier = Modifier.padding( + start = dimensions().spacing8x, + end = dimensions().spacing8x, + bottom = dimensions().spacing8x + ) + ) + } + HorizontalDivider(modifier = Modifier.height(dimensions().spacing1x)) + } } } @@ -354,6 +404,7 @@ fun PreviewOtherDebugOptions() = WireTheme { onDisableEventProcessingChange = {}, onRestartSlowSyncForRecovery = {}, enrollE2EICertificate = {}, + e2eiCertificateExpirationInputState = TextFieldState("6"), handleE2EIEnrollmentResult = {}, dismissCertificateDialog = {}, checkCrlRevocationList = {}, diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt index 86057cd198f..c32d10f8792 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt @@ -25,6 +25,7 @@ data class DebugDataOptionsState( val certificate: String = "null", val showCertificate: Boolean = false, val startGettingE2EICertificate: Boolean = false, + val e2eiCertificateExpirationSeconds: Long = 360L, val analyticsTrackingId: String = "null", val isFederationEnabled: Boolean = false, val currentApiVersion: String = "null", diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt index 256762e58c4..6d7842c07c2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt @@ -18,6 +18,8 @@ package com.wire.android.ui.debug import android.content.Context +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -41,6 +43,9 @@ import com.wire.kalium.logic.feature.debug.RepairFaultyRemovalKeysUseCase import com.wire.kalium.logic.feature.debug.RepairResult import com.wire.kalium.logic.feature.debug.StartUsingAsyncNotificationsResult import com.wire.kalium.logic.feature.debug.StartUsingAsyncNotificationsUseCase +import com.wire.kalium.logic.feature.debug.GetDebugE2EICertificateExpirationUseCase +import com.wire.kalium.logic.feature.debug.MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS +import com.wire.kalium.logic.feature.debug.SetDebugE2EICertificateExpirationUseCase import com.wire.kalium.logic.feature.debug.TargetedRepairParam import com.wire.kalium.logic.feature.e2ei.CheckCrlRevocationListUseCase import com.wire.kalium.logic.feature.e2ei.usecase.FinalizeEnrollmentResult @@ -58,18 +63,26 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject +import kotlin.time.Duration.Companion.days +@Suppress("TooManyFunctions") @ViewModelScopedPreview interface DebugDataOptionsViewModel { val infoMessage: SharedFlow get() = MutableSharedFlow() val state: DebugDataOptionsState get() = DebugDataOptionsState() + val e2eiCertificateExpirationInputState: TextFieldState get() = TextFieldState("6") fun currentAccount(): UserId = UserId("value", "domain") fun checkCrlRevocationList() {} fun restartSlowSyncForRecovery() {} fun enrollE2EICertificate() {} + fun updateE2EICertificateExpiration(seconds: Long) {} + fun updateE2EICertificateExpirationInput(minutes: String) {} fun handleE2EIEnrollmentResult(result: FinalizeEnrollmentResult) {} fun dismissCertificateDialog() {} fun forceUpdateApiVersions() {} @@ -98,11 +111,21 @@ class DebugDataOptionsViewModelImpl private val observeAsyncNotificationsEnabled: ObserveIsConsumableNotificationsEnabledUseCase, private val startUsingAsyncNotifications: StartUsingAsyncNotificationsUseCase, private val repairFaultyRemovalKeys: RepairFaultyRemovalKeysUseCase, + private val getDebugE2EICertificateExpiration: GetDebugE2EICertificateExpirationUseCase, + private val setDebugE2EICertificateExpiration: SetDebugE2EICertificateExpirationUseCase, ) : ViewModel(), DebugDataOptionsViewModel { + private companion object { + val DEFAULT_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS = 90.days.inWholeSeconds + const val SECONDS_PER_MINUTE = 60L + const val MINUTES_ROUNDING_OFFSET_SECONDS = SECONDS_PER_MINUTE - 1 + val MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_MINUTES = + MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS / SECONDS_PER_MINUTE + } override var state by mutableStateOf( DebugDataOptionsState() ) + override val e2eiCertificateExpirationInputState = TextFieldState("6") private val _infoMessage = MutableSharedFlow() override val infoMessage = _infoMessage.asSharedFlow() @@ -110,10 +133,12 @@ class DebugDataOptionsViewModelImpl init { observeAsyncNotificationsEnabledData() observeMlsMetadata() + observeE2EICertificateExpirationInput() setGitHashAndDeviceId() setAnalyticsTrackingId() setServerConfigData() setDefaultProtocol() + loadDebugE2EICertificateExpiration() } private fun observeAsyncNotificationsEnabledData() { @@ -187,9 +212,24 @@ class DebugDataOptionsViewModelImpl } override fun enrollE2EICertificate() { + val normalizedMinutes = e2eiCertificateExpirationInputState.text.toString() + .toLongOrNull() + ?.coerceAtLeast(MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_MINUTES) + ?: MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_MINUTES + setE2EICertificateExpiration(normalizedMinutes * SECONDS_PER_MINUTE) state = state.copy(startGettingE2EICertificate = true) } + override fun updateE2EICertificateExpiration(seconds: Long) { + setE2EICertificateExpiration(seconds) + } + + override fun updateE2EICertificateExpirationInput(minutes: String) { + if (e2eiCertificateExpirationInputState.text.toString() != minutes) { + e2eiCertificateExpirationInputState.setTextAndPlaceCursorAtEnd(minutes) + } + } + override fun handleE2EIEnrollmentResult(result: FinalizeEnrollmentResult) { state = when (result) { is FinalizeEnrollmentResult.Failure.OAuthError -> { @@ -332,6 +372,53 @@ class DebugDataOptionsViewModelImpl } } } + + private fun loadDebugE2EICertificateExpiration() { + viewModelScope.launch { + val currentExpiration = getDebugE2EICertificateExpiration() + if (currentExpiration == DEFAULT_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS) { + // For debug UX we default to the minimum test-friendly value instead of 90 days. + setE2EICertificateExpiration(MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS) + } else { + val minutes = (currentExpiration + MINUTES_ROUNDING_OFFSET_SECONDS) / SECONDS_PER_MINUTE + e2eiCertificateExpirationInputState.setTextAndPlaceCursorAtEnd(minutes.toString()) + state = state.copy( + e2eiCertificateExpirationSeconds = currentExpiration + ) + } + } + } + + private fun observeE2EICertificateExpirationInput() { + viewModelScope.launch { + androidx.compose.runtime.snapshotFlow { e2eiCertificateExpirationInputState.text.toString() } + .drop(1) + .distinctUntilChanged() + .collectLatest { minutesText -> + val minutes = minutesText.toLongOrNull() ?: return@collectLatest + if (minutes >= MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_MINUTES) { + applyE2EICertificateExpiration(minutes * SECONDS_PER_MINUTE) + } + } + } + } + + private fun setE2EICertificateExpiration(seconds: Long) { + val expiration = seconds.coerceAtLeast(MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS) + val minutes = (expiration + MINUTES_ROUNDING_OFFSET_SECONDS) / SECONDS_PER_MINUTE + e2eiCertificateExpirationInputState.setTextAndPlaceCursorAtEnd(minutes.toString()) + applyE2EICertificateExpiration(expiration) + } + + private fun applyE2EICertificateExpiration(seconds: Long) { + val expiration = seconds.coerceAtLeast(MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS) + state = state.copy( + e2eiCertificateExpirationSeconds = expiration + ) + viewModelScope.launch { + setDebugE2EICertificateExpiration(expiration) + } + } //endregion } //endregion diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 715f68b1087..56778ca1861 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1825,7 +1825,12 @@ In group conversations, the group admin can overwrite this setting. Register FCM push token Debug Settings API VERSIONING - E2EI Manual Enrollment + E2EI Enrollment + Expiration time + s + min + Minimum is %1$d min + Enroll Force API versioning update ⚠️ Break Session Update diff --git a/app/src/test/kotlin/com/wire/android/ui/settings/debug/DebugDataOptionsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/settings/debug/DebugDataOptionsViewModelTest.kt index ad533d966a3..53c6be6c51c 100644 --- a/app/src/test/kotlin/com/wire/android/ui/settings/debug/DebugDataOptionsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/settings/debug/DebugDataOptionsViewModelTest.kt @@ -40,6 +40,9 @@ import com.wire.kalium.logic.feature.debug.ObserveIsConsumableNotificationsEnabl import com.wire.kalium.logic.feature.debug.RepairFaultyRemovalKeysUseCase import com.wire.kalium.logic.feature.debug.StartUsingAsyncNotificationsResult import com.wire.kalium.logic.feature.debug.StartUsingAsyncNotificationsUseCase +import com.wire.kalium.logic.feature.debug.GetDebugE2EICertificateExpirationUseCase +import com.wire.kalium.logic.feature.debug.MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS +import com.wire.kalium.logic.feature.debug.SetDebugE2EICertificateExpirationUseCase import com.wire.kalium.logic.feature.e2ei.CheckCrlRevocationListUseCase import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountResult import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountUseCase @@ -67,6 +70,7 @@ import kotlinx.coroutines.test.setMain import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import kotlin.time.Duration.Companion.days @ExtendWith(ScopedArgsTestExtension::class) @ExtendWith(CoroutineTestExtension::class) @@ -242,6 +246,47 @@ class DebugDataOptionsViewModelTest { assertEquals(true, viewModel.state.isAsyncNotificationsEnabled) coVerify(exactly = 0) { arrangement.startUsingAsyncNotifications() } } + + @Test + fun `given e2ei expiration is loaded, view state should contain loaded value`() = runTest { + val (_, viewModel) = DebugDataOptionsHiltArrangement() + .withDebugE2EICertificateExpiration(999) + .arrange() + + assertEquals(999, viewModel.state.e2eiCertificateExpirationSeconds) + } + + @Test + fun `given default e2ei expiration is loaded, then minimum debug value is applied`() = runTest { + val (arrangement, viewModel) = DebugDataOptionsHiltArrangement() + .withDebugE2EICertificateExpiration(90.days.inWholeSeconds) + .arrange() + + assertEquals(MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS, viewModel.state.e2eiCertificateExpirationSeconds) + coVerify(exactly = 1) { arrangement.setDebugE2EICertificateExpiration(MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS) } + } + + @Test + fun `given expiration below minimum, when updating e2ei expiration, then minimum value is used`() = runTest { + val (arrangement, viewModel) = DebugDataOptionsHiltArrangement().arrange() + + viewModel.updateE2EICertificateExpiration(120) + + assertEquals(MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS, viewModel.state.e2eiCertificateExpirationSeconds) + coVerify(exactly = 1) { arrangement.setDebugE2EICertificateExpiration(MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS) } + } + + @Test + fun `given valid expiration value, when updating e2ei expiration, then value is updated and use case is called`() = runTest { + val (arrangement, viewModel) = DebugDataOptionsHiltArrangement() + .withDebugE2EICertificateExpiration(360) + .arrange() + + viewModel.updateE2EICertificateExpiration(420) + + coVerify(exactly = 1) { arrangement.setDebugE2EICertificateExpiration(420) } + assertEquals(420, viewModel.state.e2eiCertificateExpirationSeconds) + } } internal class DebugDataOptionsHiltArrangement { @@ -284,6 +329,12 @@ internal class DebugDataOptionsHiltArrangement { @MockK lateinit var repairFaultyRemovalKeysUseCase: RepairFaultyRemovalKeysUseCase + @MockK + lateinit var getDebugE2EICertificateExpiration: GetDebugE2EICertificateExpirationUseCase + + @MockK + lateinit var setDebugE2EICertificateExpiration: SetDebugE2EICertificateExpirationUseCase + private val viewModel by lazy { DebugDataOptionsViewModelImpl( context = context, @@ -299,7 +350,9 @@ internal class DebugDataOptionsHiltArrangement { getDefaultProtocolUseCase = getDefaultProtocolUseCase, startUsingAsyncNotifications = startUsingAsyncNotifications, observeAsyncNotificationsEnabled = observeIsConsumableNotificationsEnabled, - repairFaultyRemovalKeys = repairFaultyRemovalKeysUseCase + repairFaultyRemovalKeys = repairFaultyRemovalKeysUseCase, + getDebugE2EICertificateExpiration = getDebugE2EICertificateExpiration, + setDebugE2EICertificateExpiration = setDebugE2EICertificateExpiration ) } @@ -338,9 +391,15 @@ internal class DebugDataOptionsHiltArrangement { } returns SupportedProtocol.PROTEUS withObserveIsConsumableNotificationsEnabled(false) + coEvery { getDebugE2EICertificateExpiration() } returns 360 + coEvery { setDebugE2EICertificateExpiration(any()) } returns Unit } } + fun withDebugE2EICertificateExpiration(expiration: Long) = apply { + coEvery { getDebugE2EICertificateExpiration() } returns expiration + } + suspend fun withObserveIsConsumableNotificationsEnabled(isEnabled: Boolean = false) = apply { coEvery { observeIsConsumableNotificationsEnabled()