Add unit tests to mail-pin-mock

ET-6548
This commit is contained in:
Seren
2025-06-04 17:25:52 +02:00
committed by Seren Matthews
parent 2a3083b836
commit 57f2c15f47
33 changed files with 1036 additions and 188 deletions
+40
View File
@@ -1,9 +1,37 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="FunctionName" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="namePattern" value="[a-zA-Z][A-Za-z\d]*" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ObjectPropertyName" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="namePattern" value="[A-Za-z][- _A-Za-z\d]*" />
</inspection_tool>
@@ -11,6 +39,14 @@
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
@@ -35,6 +71,10 @@
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
-14
View File
@@ -165,11 +165,6 @@
android:value="androidx.startup"
tools:node="remove" />
<meta-data
android:name="ch.protonmail.android.initializer.AutoLockHandlerInitializer"
android:value="androidx.startup"
tools:node="remove" />
<meta-data
android:name="androidx.lifecycle.ProcessLifecycleInitializer"
android:value="androidx.startup" />
@@ -239,15 +234,6 @@
<receiver android:name=".mailnotifications.data.local.PushNotificationActionsBroadcastReceiver" />
<receiver
android:name=".mailsettings.presentation.settings.autolock.broadcastreceiver.TimeSetBroadcastReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.TIME_SET" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true"
@@ -60,7 +60,6 @@ class MainInitializer : Initializer<Unit> {
ThemeObserverInitializer::class.java,
NotificationInitializer::class.java,
NotificationHandlersInitializer::class.java,
AutoLockHandlerInitializer::class.java,
RustMailCommonInitializer::class.java,
ChallengeInitializer::class.java,
BackgroundExecutionInitializer::class.java,
@@ -140,13 +140,15 @@ internal fun NavGraphBuilder.addPrivacySettings(navController: NavHostController
}
}
@Suppress("ForbiddenComment")
internal fun NavGraphBuilder.addAutoLockSettings(navController: NavHostController) {
composable(route = Screen.AutoLockSettings.route) {
ProtonInvertedTheme {
AutoLockSettingsScreen(
modifier = Modifier,
actions = AutoLockSettingsScreen.Actions(
onPinScreenNavigation = {},//{ navController.navigate(Screen.AutoLockPinScreen(it)) },
// TODO ET-6548 { navController.navigate(Screen.AutoLockPinScreen(it)) },
onPinScreenNavigation = {},
onBackClick = { navController.navigateBack() },
onChangeIntervalClick = { navController.navigate(Screen.AutolockInterval.route) }
)
@@ -25,4 +25,4 @@ import uniffi.proton_mail_uniffi.MailSession
interface AppLockDataSource {
suspend fun shouldAutoLock(mailSession: MailSession): Either<DataError, Boolean>
}
}
@@ -34,5 +34,4 @@ class RustAppLockDataSource @Inject constructor() : AppLockDataSource {
is MailSessionShouldAutoLockResult.Error -> result.v1.toDataError().left()
is MailSessionShouldAutoLockResult.Ok -> result.v1.right()
}
}
}
@@ -25,21 +25,19 @@ import ch.protonmail.android.mailpinlock.model.BiometricsSystemState
import ch.protonmail.android.mailpinlock.model.Protection
import ch.protonmail.android.mailsettings.domain.model.AppSettings
fun AppSettings.toAutolock(biometricsState: BiometricsSystemState) =
Autolock(
autolockInterval = autolockInterval,
protectionType = autolockProtection,
biometricsState = biometricsState.toAutolockBiometrics(autolockProtection == Protection.Biometrics)
)
fun AppSettings.toAutolock(biometricsState: BiometricsSystemState) = Autolock(
autolockInterval = autolockInterval,
protectionType = autolockProtection,
biometricsState = biometricsState.toAutolockBiometrics(autolockProtection == Protection.Biometrics)
)
fun BiometricsSystemState.toAutolockBiometrics(enrolled: Boolean) =
when (this) {
is BiometricsSystemState.BiometricNotAvailable ->
AutoLockBiometricsState.BiometricsNotAvailable
fun BiometricsSystemState.toAutolockBiometrics(enrolled: Boolean) = when (this) {
is BiometricsSystemState.BiometricNotAvailable ->
AutoLockBiometricsState.BiometricsNotAvailable
is BiometricsSystemState.BiometricEnrolled ->
BiometricsAvailable.BiometricsEnrolled(enrolled)
is BiometricsSystemState.BiometricEnrolled ->
BiometricsAvailable.BiometricsEnrolled(enrolled)
is BiometricsSystemState.BiometricNotEnrolled ->
BiometricsAvailable.BiometricsNotEnrolled
}
is BiometricsSystemState.BiometricNotEnrolled ->
BiometricsAvailable.BiometricsNotEnrolled
}
@@ -0,0 +1,197 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.mailpinlock.data
import app.cash.turbine.test
import arrow.core.left
import arrow.core.right
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.mailpinlock.domain.BiometricsSystemStateRepository
import ch.protonmail.android.mailpinlock.model.AutoLockBiometricsState
import ch.protonmail.android.mailpinlock.model.AutoLockInterval
import ch.protonmail.android.mailpinlock.model.BiometricsSystemState
import ch.protonmail.android.mailpinlock.model.Protection
import ch.protonmail.android.mailsession.data.repository.MailSessionRepository
import ch.protonmail.android.mailsession.data.wrapper.MailSessionWrapper
import ch.protonmail.android.mailsettings.data.local.RustAppSettingsDataSource
import ch.protonmail.android.mailsettings.data.repository.AppSettingsRepository
import ch.protonmail.android.mailsettings.domain.model.AppLanguage
import ch.protonmail.android.mailsettings.domain.model.AppSettings
import ch.protonmail.android.mailsettings.domain.model.Theme
import ch.protonmail.android.mailsettings.domain.repository.AppLanguageRepository
import ch.protonmail.android.test.utils.rule.LoggingTestRule
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import uniffi.proton_mail_uniffi.AppAppearance
import uniffi.proton_mail_uniffi.AppProtection
import uniffi.proton_mail_uniffi.AutoLock
import uniffi.proton_mail_uniffi.MailSession
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class AutolockRepositoryTest {
@get:Rule
val loggingTestRule = LoggingTestRule()
private val mockMailSession = mockk<MailSession>()
private val appSettingsDataSource = mockk<RustAppSettingsDataSource> {
coEvery { this@mockk.updateAppSettings(mockMailSession, any()) } returns Unit.right()
}
private val appLockDataSource = mockk<AppLockDataSource>()
private val appLanguageRepository = mockk<AppLanguageRepository> {
every { this@mockk.observe() } returns flowOf(AppLanguage.FRENCH)
}
private val mockMailSessionWrapper = mockk<MailSessionWrapper> {
every { this@mockk.getRustMailSession() } returns mockMailSession
}
private val mailSessionRepository = mockk<MailSessionRepository> {
every { this@mockk.getMailSession() } returns mockMailSessionWrapper
}
private val expectedBiometrics = AutoLockBiometricsState.BiometricsAvailable.BiometricsEnrolled(false)
private val biometricsStateRepository = mockk<BiometricsSystemStateRepository> {
every { this@mockk.observe() } returns flowOf(BiometricsSystemState.BiometricEnrolled)
}
private val appSettingsRepository: AppSettingsRepository =
AppSettingsRepository(
mailSessionRepository = mailSessionRepository,
rustAppSettingsDataSource = appSettingsDataSource,
appLanguageRepository = appLanguageRepository
)
private var autolockRepository: AutolockRepository =
AutolockRepository(
biometricsSystemStateRepository = biometricsStateRepository,
appSettingsRepository = appSettingsRepository,
mailSessionRepository = mailSessionRepository,
appLockDataSource = appLockDataSource
)
private val mockAppSettings = uniffi.proton_mail_uniffi.AppSettings(
AppAppearance.LIGHT_MODE,
AppProtection.PIN,
AutoLock.Always,
useCombineContacts = true,
useAlternativeRouting = true
)
private val expectedAppSettings = AppSettings(
autolockInterval = AutoLockInterval.Immediately,
autolockProtection = Protection.Pin,
hasAlternativeRouting = true,
customAppLanguage = AppLanguage.FRENCH.langName,
hasDeviceContactsEnabled = true,
theme = Theme.LIGHT
)
@Test
fun `returns protection when observed`() = runTest {
// Given
coEvery {
appSettingsDataSource.getAppSettings(any())
} returns mockAppSettings.right()
// When
autolockRepository.observeAppLock().test {
// Then
assertEquals(expectedAppSettings.autolockProtection, awaitItem().protectionType)
}
}
@Test
fun `returns interval when observed`() = runTest {
// Given
coEvery {
appSettingsDataSource.getAppSettings(any())
} returns mockAppSettings.right()
// When
autolockRepository.observeAppLock().test {
// Then
assertEquals(expectedAppSettings.autolockInterval, awaitItem().autolockInterval)
}
}
@Test
fun `returns biometrics when observed`() = runTest {
// Given
coEvery { appLockDataSource.shouldAutoLock(mockMailSession) } returns false.right()
coEvery {
appSettingsDataSource.getAppSettings(any())
} returns mockAppSettings.right()
// When
autolockRepository.observeAppLock().test {
// Then
assertEquals(expectedBiometrics, awaitItem().biometricsState)
}
}
@Test
fun `when interval is updated then observer is also updated`() = runTest {
val expectedUpdatedInterval = AutoLockInterval.FifteenMinutes
val updatedAppSettings = mockAppSettings.copy(autoLock = AutoLock.Minutes(15L.toUByte()))
// Given
coEvery {
appSettingsDataSource.getAppSettings(mockMailSession)
} returns mockAppSettings.right() andThen updatedAppSettings.right()
// When
autolockRepository.observeAppLock().test {
// Then
assertEquals(expectedAppSettings.autolockInterval, awaitItem().autolockInterval)
autolockRepository.updateAutolockInterval(expectedUpdatedInterval)
assertEquals(expectedUpdatedInterval, awaitItem().autolockInterval)
}
}
@Test
fun `when shouldAutolock THEN return result`() = runTest {
coEvery { appLockDataSource.shouldAutoLock(mockMailSession) } returns true.right()
val result = autolockRepository.shouldAutolock()
assert(result.isRight())
assertTrue(result.getOrNull()!!)
}
@Test
fun `when shouldAutolock is False THEN return result`() = runTest {
coEvery { appLockDataSource.shouldAutoLock(mockMailSession) } returns false.right()
val result = autolockRepository.shouldAutolock()
assert(result.isRight())
assertFalse(result.getOrNull()!!)
}
@Test
fun `when shouldAutolock fails then return error`() = runTest {
coEvery { appLockDataSource.shouldAutoLock(mockMailSession) } returns DataError.Local.Unknown.left()
val result = autolockRepository.shouldAutolock()
assert(result.isLeft())
}
}
@@ -105,4 +105,4 @@ class BiometricsSystemStateRepositoryImplTest {
// Then
assertEquals(BiometricsSystemState.BiometricEnrolled, actual)
}
}
}
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.mailpinlock.data
import arrow.core.left
import ch.protonmail.android.mailcommon.domain.model.DataError
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
import uniffi.proton_mail_uniffi.MailSession
import uniffi.proton_mail_uniffi.MailSessionShouldAutoLockResult
import uniffi.proton_mail_uniffi.SessionErrorReason.UNKNOWN_LABEL
import uniffi.proton_mail_uniffi.UserSessionError
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class RustAppLockDataSourceTest {
private val sut = RustAppLockDataSource()
@Test
fun `when shouldAutoLock is TRUE then returns true right`() = runTest {
val mailSession = mockk<MailSession> {
coEvery { this@mockk.shouldAutoLock() } returns MailSessionShouldAutoLockResult.Ok(true)
}
val result = sut.shouldAutoLock(mailSession)
assertTrue(result.getOrNull()!!)
}
@Test
fun `when shouldAutoLock FALSE then returns false right`() = runTest {
val mailSession = mockk<MailSession> {
coEvery { this@mockk.shouldAutoLock() } returns MailSessionShouldAutoLockResult.Ok(false)
}
val result = sut.shouldAutoLock(mailSession)
assertFalse(result.getOrNull()!!)
}
@Test
fun `when shouldAutoLock and error then returns mapped error`() = runTest {
val expectedError = DataError.Local.Unknown
val mailSession = mockk<MailSession> {
coEvery { this@mockk.shouldAutoLock() } returns
MailSessionShouldAutoLockResult.Error(
UserSessionError.Reason(
UNKNOWN_LABEL
)
)
}
val result = sut.shouldAutoLock(mailSession)
assertEquals(expectedError.left(), result)
}
}
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.mailpinlock.data.mapper
import ch.protonmail.android.mailpinlock.model.AutoLockBiometricsState
import ch.protonmail.android.mailpinlock.model.AutoLockInterval
import ch.protonmail.android.mailpinlock.model.Autolock
import ch.protonmail.android.mailpinlock.model.BiometricsSystemState
import ch.protonmail.android.mailpinlock.model.Protection
import ch.protonmail.android.mailsettings.domain.model.AppSettings
import ch.protonmail.android.mailsettings.domain.model.Theme
import org.junit.Assert.assertEquals
import org.junit.Test
class AutolockMapperTest {
private val appSettings = AppSettings(
theme = Theme.DARK,
autolockInterval = AutoLockInterval.FifteenMinutes,
autolockProtection = Protection.Biometrics,
customAppLanguage = "en",
hasDeviceContactsEnabled = true,
hasAlternativeRouting = false
)
private val biometricsSystemState = BiometricsSystemState.BiometricEnrolled
private val biometricsState = AutoLockBiometricsState.BiometricsAvailable.BiometricsEnrolled(true)
private val expected = Autolock(
autolockInterval = AutoLockInterval.FifteenMinutes,
protectionType = Protection.Biometrics,
biometricsState = biometricsState
)
@Test
fun `map appSettings to autolock WHEN biometrics enrolled`() {
val actual = appSettings.toAutolock(biometricsSystemState)
assertEquals(actual.autolockInterval, expected.autolockInterval)
assertEquals(actual.protectionType, expected.protectionType)
assertEquals(actual.biometricsState, expected.biometricsState)
}
@Test
fun `map appSettings to autolock WHEN biometrics not enrolled`() {
appSettings.copy(autolockProtection = Protection.Pin)
expected.copy(biometricsState = AutoLockBiometricsState.BiometricsAvailable.BiometricsEnrolled(false))
val actual = appSettings.toAutolock(biometricsSystemState)
assertEquals(actual.autolockInterval, expected.autolockInterval)
assertEquals(actual.protectionType, expected.protectionType)
assertEquals(actual.biometricsState, expected.biometricsState)
}
}
@@ -24,4 +24,4 @@ import kotlinx.coroutines.flow.Flow
interface BiometricsSystemStateRepository {
fun getCurrentState(): BiometricsSystemState
fun observe(): Flow<BiometricsSystemState>
}
}
@@ -20,13 +20,15 @@ package ch.protonmail.android.mailpinlock.domain.usecase
import javax.inject.Inject
// TODO convert to rust
@Suppress("ForbiddenComment")
// TODO ET-6548 convert to rust
class HasValidPinValue @Inject constructor(
// private val observeAutoLockPinValue: ObserveAutoLockPinValue
// private val observeAutoLockPinValue: ObserveAutoLockPinValue
) {
// rust
suspend operator fun invoke() = false /*observeAutoLockPinValue()
private val isValid = false
suspend operator fun invoke() = isValid
/*observeAutoLockPinValue()
.first()
.getOrNull()
?.value
@@ -20,9 +20,11 @@ package ch.protonmail.android.mailpinlock.domain.usecase
import javax.inject.Inject
// TODO pin convert to rust
@Suppress("ForbiddenComment")
// TODO ET-6548 pin convert to rust
class ToggleAutoLockAttemptPendingStatus @Inject constructor() {
suspend operator fun invoke(value: Boolean) = false
val isPending = false
suspend operator fun invoke(value: Boolean) = isPending
// autoLockRepository.updateAutoLockAttemptPendingStatus(AutoLockAttemptPendingStatus(value))
}
@@ -28,4 +28,4 @@ sealed interface AutoLockBiometricsState {
data class BiometricsEnrolled(val enabled: Boolean) : BiometricsAvailable
}
}
}
@@ -19,13 +19,12 @@
package ch.protonmail.android.mailpinlock.model
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
data class Autolock(
val autolockInterval: AutoLockInterval = AutoLockInterval.Never,
val autolockInterval: AutoLockInterval = AutoLockInterval.Immediately,
val protectionType: Protection = Protection.None,
val biometricsState: AutoLockBiometricsState = AutoLockBiometricsState.BiometricsNotAvailable
) {
@@ -42,7 +41,7 @@ enum class AutoLockInterval(val duration: Duration) {
FifteenMinutes(15.minutes),
OneHour(1.hours),
OneDay(24.hours),
Never(36_000.days);
Never(Duration.INFINITE);
companion object {
@@ -22,4 +22,4 @@ sealed interface BiometricsSystemState {
object BiometricEnrolled : BiometricsSystemState
object BiometricNotEnrolled : BiometricsSystemState
object BiometricNotAvailable : BiometricsSystemState
}
}
@@ -18,7 +18,9 @@
package ch.protonmail.android.mailpinlock.domain.autolock
// TODO convert to rust ET-648
// TODO ET-6548 pin convert to rust
@Suppress("ForbiddenComment")
internal class HasValidPinValueTest {
/* private val observeValidPinValue = mockk<ObserveAutoLockPinValue>()
@@ -0,0 +1,119 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.mailpinlock.domain.autolock.usecase
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
import arrow.core.right
import ch.protonmail.android.mailcommon.domain.AppInBackgroundState
import ch.protonmail.android.mailpinlock.domain.AutolockRepository
import ch.protonmail.android.mailpinlock.domain.usecase.ShouldPresentPinInsertionScreen
import io.mockk.called
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.unmockkAll
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Test
import kotlin.test.assertTrue
internal class ShouldPresentPinInsertionScreenTest {
private val appInBackgroundState = mockk<AppInBackgroundState>()
private val autolockRepository = mockk<AutolockRepository>()
private val shouldPresentPinInsertionScreen = ShouldPresentPinInsertionScreen(
appInBackgroundState,
autolockRepository
)
@After
fun teardown() {
unmockkAll()
}
@Test
fun `should not indicate to display pin screen and do nothing when the app is in the background`() = runTest {
// Given
expectAppInBackground()
// When
val result = shouldPresentPinInsertionScreen().first()
// Then
assertFalse(result)
coVerify {
autolockRepository wasNot called
}
}
@Test
fun `should indicate to display pin screen when the app is not background AND shouldShowPin is TRUE`() = runTest {
// Given
expectAppInForeground()
coEvery { autolockRepository.shouldAutolock() } returns true.right()
// When
val result = shouldPresentPinInsertionScreen().first()
// Then
assertTrue(result)
}
@Test
fun `should NOT indicate to display pin screen when the app is not background AND shouldShowPin is FALSE`() =
runTest {
// Given
expectAppInForeground()
coEvery { autolockRepository.shouldAutolock() } returns false.right()
val result = shouldPresentPinInsertionScreen().first()
// Then
assertFalse(result)
}
private fun expectAppInForeground() {
every { appInBackgroundState.observe() } returns flowOf(false)
}
private fun expectAppInBackground() {
every { appInBackgroundState.observe() } returns flowOf(true)
}
}
@@ -19,12 +19,10 @@
package ch.protonmail.android.mailpinlock.presentation.autolock
import androidx.compose.runtime.Stable
import ch.protonmail.android.mailcommon.presentation.Effect
import ch.protonmail.android.mailcommon.presentation.model.TextUiModel
@Stable
data class AutoLockBiometricsUiModel(
val enabled: Boolean,
val biometricsEnrolled: Boolean,
val biometricsHwAvailable: Boolean,
val biometricsHwAvailable: Boolean
)
@@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("RedundantSuspendModifier", "ControlFlowWithEmptyBody")
@HiltViewModel
class AutoLockSettingsViewModel @Inject constructor(
private val autolockRepository: AutolockRepository
@@ -48,13 +49,14 @@ class AutoLockSettingsViewModel @Inject constructor(
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis), AutolockSettingsUiState.Loading)
@Suppress("ForbiddenComment")
internal fun submit(action: AutoLockSettingsViewAction) {
viewModelScope.launch {
when (action) {
is AutoLockSettingsViewAction.ToggleAutoLockPreference -> updateAutoLockEnabledValue(action.newValue)
// TODO inegrate the biometrics
//is AutoLockSettingsViewAction.ToggleAutoLockBiometricsPreference ->
// TODO ET-6548 integrate the biometrics and pin
// is AutoLockSettingsViewAction.ToggleAutoLockBiometricsPreference ->
// updateBiometricsEnabledValue(action.autoLockBiometricsUiModel)
else -> {}
}
@@ -66,7 +68,9 @@ class AutoLockSettingsViewModel @Inject constructor(
// if turn on autolock - go to pin flow to set pin
}
@Suppress("unused")
private suspend fun updateBiometricsEnabledValue(enabled: Boolean) {
// ET-6548
if (enabled) {
// both go to pin flow, need to confirm pin if disabling
// open pin verify flow
@@ -35,7 +35,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class AutolockIntervalViewModel @Inject constructor(
private val autolockRepository: AutolockRepository
@@ -50,7 +49,8 @@ class AutolockIntervalViewModel @Inject constructor(
AutolockIntervalState.Data(
autolock.autolockInterval,
AutoLockInterval.entries.sortedBy { it.duration }
.toMutableList().apply { this.removeAt(this.lastIndex) }
.toMutableList()
.apply { this.removeAt(this.lastIndex) } // we don't show never, that's autolock off
.associateWith { it.toTextUiModel() }
)
}
@@ -16,13 +16,14 @@
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("unused")
package ch.protonmail.android.mailpinlock.presentation.autolock
import ch.protonmail.android.mailcommon.presentation.Effect
import ch.protonmail.android.mailcommon.presentation.model.TextUiModel
import ch.protonmail.android.mailpinlock.presentation.R
sealed class AutolockSettingsUiState {
data class Data(val settings: AutolockSettings) : AutolockSettingsUiState()
data object Loading : AutolockSettingsUiState()
@@ -39,7 +39,6 @@ import ch.protonmail.android.design.compose.component.ProtonAppSettingsItemInver
import ch.protonmail.android.design.compose.component.ProtonAppSettingsItemNorm
import ch.protonmail.android.design.compose.component.ProtonCenteredProgress
import ch.protonmail.android.design.compose.component.ProtonMainSettingsIcon
import ch.protonmail.android.design.compose.component.ProtonSettingsDetailsAppBar
import ch.protonmail.android.design.compose.component.ProtonSettingsToggleItem
import ch.protonmail.android.design.compose.component.ProtonSettingsTopBar
import ch.protonmail.android.design.compose.component.ProtonSnackbarHostState
@@ -62,7 +61,6 @@ import ch.protonmail.android.mailpinlock.presentation.autolock.ProtectionType
import ch.protonmail.android.mailpinlock.presentation.autolock.ui.AutoLockSettingsScreen.Actions
import ch.protonmail.android.uicomponents.snackbar.DismissableSnackbarHost
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AutoLockSettingsScreen(
@@ -84,10 +82,10 @@ fun AutoLockSettingsScreen(
effects.updateError
)
ConsumableLaunchedEffect(effects.forceOpenPinCreation) {
//onPinScreenNavigation(AutoLockInsertionMode.CreatePin)
// ET-6548 onPinScreenNavigation(AutoLockInsertionMode.CreatePin)
}
ConsumableLaunchedEffect(effects.forceOpenPinCreation) {
//onPinScreenNavigation(AutoLockInsertionMode.ChangePin)
// ET-6548 onPinScreenNavigation(AutoLockInsertionMode.ChangePin)
}
when (val uiState = state) {
@@ -109,8 +107,7 @@ fun AutoLockSettingsScreen(
.padding(horizontal = ProtonDimens.Spacing.Large),
settings = uiState.settings,
submitAction = { viewModel.submit(it) },
onChangeIntervalNavigation = actions.onChangeIntervalClick,
onPinScreenNavigation = actions.onPinScreenNavigation
actions = actions
)
}
)
@@ -122,9 +119,8 @@ fun AutoLockSettingsScreen(
private fun AutolockSettingScreen(
modifier: Modifier = Modifier,
settings: AutolockSettings,
submitAction: (AutoLockSettingsViewAction) -> Unit,
onPinScreenNavigation: () -> Unit = {},
onChangeIntervalNavigation: () -> Unit = {}
actions: Actions,
submitAction: (AutoLockSettingsViewAction) -> Unit
) {
Column(modifier = modifier) {
AutolockOnOffToggle(
@@ -142,10 +138,10 @@ private fun AutolockSettingScreen(
containerColor = ProtonTheme.colors.backgroundInvertedSecondary
)
) {
ChangePinOption(onClickChangePin = onPinScreenNavigation)
ChangePinOption(onClickChangePin = actions.onPinScreenNavigation)
ChangeIntervalOption(
selectedChoice = settings.selectedUiInterval,
onClickChangeInterval = onChangeIntervalNavigation
onClickChangeInterval = actions.onChangeIntervalClick
)
if (settings.biometricsAvailable) {
BiometricsOnOffToggle(
@@ -278,7 +274,7 @@ fun PreviewAutolockSettingScreenEnabled() {
protectionType = ProtectionType.Biometrics,
biometricsAvailable = true
),
onPinScreenNavigation = {},
actions = Actions(),
submitAction = {}
)
}
@@ -293,7 +289,7 @@ fun PreviewAutolockSettingScreenDisabled() {
protectionType = ProtectionType.None,
biometricsAvailable = false
),
onPinScreenNavigation = {},
actions = Actions(),
submitAction = {}
)
}
@@ -301,8 +297,8 @@ fun PreviewAutolockSettingScreenDisabled() {
object AutoLockSettingsScreen {
data class Actions(
val onChangeIntervalClick: () -> Unit,
val onBackClick: () -> Unit,
val onPinScreenNavigation: () -> Unit = {},
val onChangeIntervalClick: () -> Unit = {},
val onBackClick: () -> Unit = {},
val onPinScreenNavigation: () -> Unit = {}
)
}
}
@@ -22,9 +22,9 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import ch.protonmail.android.mailcommon.domain.model.autolock.AutoLockPin
import ch.protonmail.android.mailpinlock.domain.usecase.ToggleAutoLockAttemptPendingStatus
import ch.protonmail.android.mailpinlock.presentation.autolock.AutoLockInsertionMode
import ch.protonmail.android.mailpinlock.presentation.pin.ui.AutoLockPinScreen
import ch.protonmail.android.mailpinlock.domain.usecase.ToggleAutoLockAttemptPendingStatus
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -32,15 +32,16 @@ import kotlinx.coroutines.launch
import me.proton.core.util.kotlin.deserialize
import javax.inject.Inject
@Suppress("unused", "UseExpressionBody", "RedundantSuspendModifier")
@HiltViewModel
class AutoLockPinViewModel @Inject constructor(
// private val observeAutoLockPin: ObserveAutoLockPinValue,
// private val toggleAutoLockEnabled: ToggleAutoLockEnabled,
// private val updateRemainingAutoLockAttempts: UpdateRemainingAutoLockAttempts,
// private val saveAutoLockPin: SaveAutoLockPin,
// private val observeAutoLockPin: ObserveAutoLockPinValue,
// private val toggleAutoLockEnabled: ToggleAutoLockEnabled,
// private val updateRemainingAutoLockAttempts: UpdateRemainingAutoLockAttempts,
// private val saveAutoLockPin: SaveAutoLockPin,
private val clearPinDataAndForceLogout: ClearPinDataAndForceLogout,
private val toggleAutoLockAttemptStatus: ToggleAutoLockAttemptPendingStatus,
// private val updateAutoLockLastForegroundMillis: UpdateLastForegroundMillis,
// private val updateAutoLockLastForegroundMillis: UpdateLastForegroundMillis,
private val reducer: AutoLockPinReducer,
savedStateHandle: SavedStateHandle
) : ViewModel() {
@@ -63,19 +64,19 @@ class AutoLockPinViewModel @Inject constructor(
}
viewModelScope.launch {
/* val remainingAttempts = getRemainingAutoLockAttempts().getOrNull()?.value?.let {
PinVerificationRemainingAttempts(it)
} ?: PinVerificationRemainingAttempts.Default
/* val remainingAttempts = getRemainingAutoLockAttempts().getOrNull()?.value?.let {
PinVerificationRemainingAttempts(it)
} ?: PinVerificationRemainingAttempts.Default
toggleAutoLockAttemptStatus(value = true)
toggleAutoLockAttemptStatus(value = true)
val currentBiometricsState = getCurrentAutoLockBiometricState()
emitNewStateFrom(AutoLockPinEvent.Data.Loaded(step, remainingAttempts, currentBiometricsState))
val currentBiometricsState = getCurrentAutoLockBiometricState()
emitNewStateFrom(AutoLockPinEvent.Data.Loaded(step, remainingAttempts, currentBiometricsState))
observeAutoLockBiometricsState()
.onEach {
emitNewStateFrom(AutoLockPinEvent.Update.BiometricStateChanged(it))
}*/
observeAutoLockBiometricsState()
.onEach {
emitNewStateFrom(AutoLockPinEvent.Update.BiometricStateChanged(it))
}*/
}
}
@@ -123,15 +124,15 @@ class AutoLockPinViewModel @Inject constructor(
val autoLockPin = AutoLockPin(insertedPin.toString())
/* saveAutoLockPin(autoLockPin).mapLeft {
Timber.e("Unable to save auto pin lock value. - $it")
return emitNewStateFrom(AutoLockPinEvent.Update.Error.UnknownError)
}
/* saveAutoLockPin(autoLockPin).mapLeft {
Timber.e("Unable to save auto pin lock value. - $it")
return emitNewStateFrom(AutoLockPinEvent.Update.Error.UnknownError)
}
toggleAutoLockEnabled(newValue = true).mapLeft {
Timber.e("Unable to enable pin lock. - $it")
return emitNewStateFrom(AutoLockPinEvent.Update.Error.UnknownError)
}*/
toggleAutoLockEnabled(newValue = true).mapLeft {
Timber.e("Unable to enable pin lock. - $it")
return emitNewStateFrom(AutoLockPinEvent.Update.Error.UnknownError)
}*/
emitNewStateFrom(AutoLockPinEvent.Update.OperationCompleted)
}
@@ -155,24 +156,24 @@ class AutoLockPinViewModel @Inject constructor(
remainingAttempts: PinVerificationRemainingAttempts,
continuation: () -> Unit
) {
/* val storedPin = observeAutoLockPin().firstOrNull()?.getOrNull()
?: return emitNewStateFrom(AutoLockPinEvent.Update.Error.UnknownError)
/* val storedPin = observeAutoLockPin().firstOrNull()?.getOrNull()
?: return emitNewStateFrom(AutoLockPinEvent.Update.Error.UnknownError)
if (!storedPin.matches(insertedPin)) {
if (remainingAttempts.value <= 1) {
clearPinDataAndForceLogout().await()
emitNewStateFrom(AutoLockPinEvent.Update.OperationAborted)
return
}
if (!storedPin.matches(insertedPin)) {
if (remainingAttempts.value <= 1) {
clearPinDataAndForceLogout().await()
emitNewStateFrom(AutoLockPinEvent.Update.OperationAborted)
return
}
val decrementedRemainingAttempts = remainingAttempts.decrement()
val decrementedRemainingAttempts = remainingAttempts.decrement()
updateRemainingAutoLockAttempts(decrementedRemainingAttempts.value).onLeft {
Timber.e("Unable to update remaining auto lock attempts. - $it")
}
updateRemainingAutoLockAttempts(decrementedRemainingAttempts.value).onLeft {
Timber.e("Unable to update remaining auto lock attempts. - $it")
}
return emitNewStateFrom(AutoLockPinEvent.Update.Error.WrongPinCode(decrementedRemainingAttempts))*/
}
return emitNewStateFrom(AutoLockPinEvent.Update.Error.WrongPinCode(decrementedRemainingAttempts))*/
}
/* updateRemainingAutoLockAttempts(PinVerificationRemainingAttempts.MaxAttempts).onLeft {
Timber.e("Unable to reset remaining auto lock attempts. - $it")
@@ -189,48 +190,48 @@ class AutoLockPinViewModel @Inject constructor(
continuation()*/
private suspend fun onBiometricAuthenticationSucceeded() {
/* updateRemainingAutoLockAttempts(PinVerificationRemainingAttempts.MaxAttempts).onLeft {
Timber.e("Unable to reset remaining auto lock attempts. - $it")
}
/* updateRemainingAutoLockAttempts(PinVerificationRemainingAttempts.MaxAttempts).onLeft {
Timber.e("Unable to reset remaining auto lock attempts. - $it")
}
toggleAutoLockAttemptStatus(value = false).onLeft {
Timber.e("Unable to reset pending lock attempt. - $it")
}
toggleAutoLockAttemptStatus(value = false).onLeft {
Timber.e("Unable to reset pending lock attempt. - $it")
}
updateAutoLockLastForegroundMillis(Long.MAX_VALUE).onLeft {
Timber.e("Unable to update last foreground millis - $it")
}*/
updateAutoLockLastForegroundMillis(Long.MAX_VALUE).onLeft {
Timber.e("Unable to update last foreground millis - $it")
}*/
emitNewStateFrom(AutoLockPinEvent.Update.VerificationCompleted)
}
private suspend fun onPerformConfirm() {
/* val state = state.value as? AutoLockPinState.DataLoaded ?: return
val currentStep = state.pinInsertionState.step
val remainingAttempts = state.pinInsertionState.remainingAttempts
val insertedPin = state.pinInsertionState.pinInsertionUiModel.currentPin
/* val state = state.value as? AutoLockPinState.DataLoaded ?: return
val currentStep = state.pinInsertionState.step
val remainingAttempts = state.pinInsertionState.remainingAttempts
val insertedPin = state.pinInsertionState.pinInsertionUiModel.currentPin
when (currentStep) {
PinInsertionStep.PinChange -> handlePinChangeConfirmation(insertedPin, remainingAttempts)
PinInsertionStep.PinInsertion -> handlePinInsertion(insertedPin)
PinInsertionStep.PinConfirmation -> handlePinConfirmed(insertedPin)
PinInsertionStep.PinVerification -> handlePinVerification(insertedPin, remainingAttempts)
}*/
when (currentStep) {
PinInsertionStep.PinChange -> handlePinChangeConfirmation(insertedPin, remainingAttempts)
PinInsertionStep.PinInsertion -> handlePinInsertion(insertedPin)
PinInsertionStep.PinConfirmation -> handlePinConfirmed(insertedPin)
PinInsertionStep.PinVerification -> handlePinVerification(insertedPin, remainingAttempts)
}*/
}
private fun onPinDigitRemoved() {
/* val state = state.value as? AutoLockPinState.DataLoaded ?: return
val currentPin =
state.pinInsertionState.pinInsertionUiModel.currentPin.takeIf { it.isNotEmpty() } ?: return
/* val state = state.value as? AutoLockPinState.DataLoaded ?: return
val currentPin =
state.pinInsertionState.pinInsertionUiModel.currentPin.takeIf { it.isNotEmpty() } ?: return
emitNewStateFrom(AutoLockPinEvent.Update.PinValueChanged(currentPin.deleteLastDigit()))*/
emitNewStateFrom(AutoLockPinEvent.Update.PinValueChanged(currentPin.deleteLastDigit()))*/
}
private fun onPinDigitAdded(action: AutoLockPinViewAction.AddPinDigit) {
/* val state = state.value as? AutoLockPinState.DataLoaded ?: return
val currentPin = state.pinInsertionState.pinInsertionUiModel.currentPin
if (currentPin.isMaxLength()) return
/* val state = state.value as? AutoLockPinState.DataLoaded ?: return
val currentPin = state.pinInsertionState.pinInsertionUiModel.currentPin
if (currentPin.isMaxLength()) return
emitNewStateFrom(AutoLockPinEvent.Update.PinValueChanged(currentPin.appendDigit(action.addition)))*/
emitNewStateFrom(AutoLockPinEvent.Update.PinValueChanged(currentPin.appendDigit(action.addition)))*/
}
private fun onSignOutRequested() {
@@ -238,15 +239,19 @@ class AutoLockPinViewModel @Inject constructor(
}
private suspend fun onSignOutConfirmed() {
/* clearPinDataAndForceLogout().await()
emitNewStateFrom(AutoLockPinEvent.Update.SignOutConfirmed)*/
/* clearPinDataAndForceLogout().await()
emitNewStateFrom(AutoLockPinEvent.Update.SignOutConfirmed)*/
}
private fun onSignOutCanceled() {
emitNewStateFrom(AutoLockPinEvent.Update.SignOutCanceled)
}
private fun emitNewStateFrom(event: AutoLockPinEvent){}/* = mutableState.update {
@Suppress("EmptyFunctionBlock")
private fun emitNewStateFrom(event: AutoLockPinEvent) {
}
/* ET-6548 = mutableState.update {
reducer.newStateFrom(it, event)
}
*/
@@ -16,23 +16,5 @@
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.initializer
package protonmail.android.mailpinlock.presentation
import android.content.Context
import androidx.startup.Initializer
import ch.protonmail.android.di.AutoLockModule
import dagger.hilt.android.EntryPointAccessors
internal class AutoLockHandlerInitializer : Initializer<Unit> {
override fun create(context: Context) {
EntryPointAccessors.fromApplication(
context.applicationContext,
AutoLockModule.EntryPointModule::class.java
).autoLockHandler().handle()
}
override fun dependencies(): List<Class<out Initializer<*>>> = listOf(
AppInBackgroundCheckerInitializer::class.java
)
}
@@ -19,21 +19,41 @@
package protonmail.android.mailpinlock.presentation.autolock
import app.cash.turbine.test
import arrow.core.right
import ch.protonmail.android.mailcommon.presentation.model.TextUiModel
import ch.protonmail.android.mailpinlock.domain.AutolockRepository
import ch.protonmail.android.mailpinlock.model.Autolock
import ch.protonmail.android.mailpinlock.presentation.R
import ch.protonmail.android.mailpinlock.presentation.autolock.AutoLockSettingsViewModel
import ch.protonmail.android.mailpinlock.presentation.autolock.AutolockSettings
import ch.protonmail.android.mailpinlock.presentation.autolock.AutolockSettingsUiState
import ch.protonmail.android.mailpinlock.presentation.autolock.ProtectionType
import io.mockk.coEvery
import io.mockk.mockk
import io.mockk.unmockkAll
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
internal class AutoLockSettingsViewModelTest {
private val autolockRepository: AutolockRepository = mockk()
private val autoLockFlow = MutableSharedFlow<Autolock>()
private val autolockRepository: AutolockRepository = mockk {
coEvery {
this@mockk.observeAppLock()
} returns autoLockFlow
coEvery {
this@mockk.updateAutolockInterval(any())
} returns Unit.right()
}
private val viewModel by lazy {
AutoLockSettingsViewModel(
@@ -53,16 +73,25 @@ internal class AutoLockSettingsViewModelTest {
@Test
fun `should return loading state when first launched`() = runTest {
// Given
// When + Then
/* viewModel.state.test {
viewModel.state.test {
val loadingState = awaitItem()
}*/
Assert.assertEquals(AutolockSettingsUiState.Loading, loadingState)
}
}
@Test
fun `should return mapped data when flow emits value`() = runTest {
viewModel.state.test {
val loadingState = awaitItem()
Assert.assertEquals(AutolockSettingsUiState.Loading, loadingState)
autoLockFlow.tryEmit(Autolock())
private companion object {
val expected = AutolockSettings(
selectedUiInterval = TextUiModel(R.string.mail_pinlock_settings_autolock_never),
protectionType = ProtectionType.None,
biometricsAvailable = false
)
AutolockSettingsUiState.Data(settings = expected)
}
}
}
@@ -0,0 +1,163 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package protonmail.android.mailpinlock.presentation.autolock
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import arrow.core.right
import ch.protonmail.android.mailcommon.presentation.Effect
import ch.protonmail.android.mailcommon.presentation.model.TextUiModel
import ch.protonmail.android.mailpinlock.domain.AutolockRepository
import ch.protonmail.android.mailpinlock.model.AutoLockInterval.FifteenMinutes
import ch.protonmail.android.mailpinlock.model.AutoLockInterval.FiveMinutes
import ch.protonmail.android.mailpinlock.model.AutoLockInterval.Immediately
import ch.protonmail.android.mailpinlock.model.AutoLockInterval.OneDay
import ch.protonmail.android.mailpinlock.model.AutoLockInterval.OneHour
import ch.protonmail.android.mailpinlock.model.Autolock
import ch.protonmail.android.mailpinlock.presentation.R
import ch.protonmail.android.mailpinlock.presentation.autolock.AutolockIntervalEffects
import ch.protonmail.android.mailpinlock.presentation.autolock.AutolockIntervalState
import ch.protonmail.android.mailpinlock.presentation.autolock.AutolockIntervalViewModel
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.Assert
import org.junit.Before
import org.junit.Test
class AutolockIntervalViewModelTest {
private val autoLockFlow = MutableSharedFlow<Autolock>()
private val autolockRepository: AutolockRepository = mockk {
coEvery {
this@mockk.observeAppLock()
} returns autoLockFlow
coEvery {
this@mockk.updateAutolockInterval(any())
} returns Unit.right()
}
private lateinit var viewModel: AutolockIntervalViewModel
@Before
fun setUp() {
Dispatchers.setMain(UnconfinedTestDispatcher())
viewModel = AutolockIntervalViewModel(
autolockRepository
)
}
@Test
fun `close effect emitted when interval is updated`() = runTest {
// Given
coEvery { autolockRepository.updateAutolockInterval(any()) } returns Unit.right()
// When
viewModel.onIntervalSelected(FifteenMinutes)
// then
Assert.assertEquals(
AutolockIntervalEffects(close = Effect.Companion.of(Unit)),
viewModel.effects.value
)
}
@Test
fun `state returns interval with 15 minutes selected when saved interval is 15 minutes`() = runTest {
viewModel.state.test {
// Given
initialStateEmitted()
// When
autoLockFlow.emit(Autolock(autolockInterval = FifteenMinutes))
// Then
Assert.assertEquals(
AutolockIntervalState.Data(
currentInterval = FifteenMinutes,
intervalsToChoices = uiIntervals
),
awaitItem()
)
}
}
@Test
fun `state is updated when repository emits an updated interval`() = runTest {
viewModel.state.test {
// Given
initialStateEmitted()
// When
autoLockFlow.emit(Autolock(autolockInterval = FifteenMinutes))
// Then
Assert.assertEquals(
AutolockIntervalState.Data(
currentInterval = FifteenMinutes,
intervalsToChoices = uiIntervals
),
awaitItem()
)
// When
autoLockFlow.emit(Autolock(autolockInterval = OneDay))
// Then
Assert.assertEquals(
AutolockIntervalState.Data(
currentInterval = OneDay,
intervalsToChoices = uiIntervals
),
awaitItem()
)
}
}
@Test
fun `updates autolockRepository on repository when intervalSelected`() = runTest {
// Given
coEvery { autolockRepository.updateAutolockInterval(any()) } returns Unit.right()
// When
viewModel.onIntervalSelected(FifteenMinutes)
// Then
coVerify { autolockRepository.updateAutolockInterval(FifteenMinutes) }
}
private suspend fun ReceiveTurbine<AutolockIntervalState>.initialStateEmitted() {
awaitItem() as AutolockIntervalState.Loading
}
companion object {
val uiIntervals = mapOf(
Immediately to TextUiModel(R.string.mail_pinlock_settings_autolock_immediately),
FiveMinutes to TextUiModel(R.string.mail_pinlock_settings_autolock_description_five_minutes),
FifteenMinutes to TextUiModel(R.string.mail_pinlock_settings_autolock_description_fifteen_minutes),
OneHour to TextUiModel(R.string.mail_pinlock_settings_autolock_description_one_hour),
OneDay to TextUiModel(R.string.mail_pinlock_settings_autolock_description_one_day)
)
}
}
@@ -0,0 +1,111 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package protonmail.android.mailpinlock.presentation.autolock.mapper
import ch.protonmail.android.mailcommon.presentation.model.TextUiModel
import ch.protonmail.android.mailpinlock.model.AutoLockInterval
import ch.protonmail.android.mailpinlock.model.Autolock
import ch.protonmail.android.mailpinlock.model.Protection
import ch.protonmail.android.mailpinlock.presentation.R
import ch.protonmail.android.mailpinlock.presentation.autolock.AutolockSettings
import ch.protonmail.android.mailpinlock.presentation.autolock.ProtectionType
import ch.protonmail.android.mailpinlock.presentation.autolock.mapper.AutolockSettingsUiMapper
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import kotlin.test.assertEquals
@RunWith(Parameterized::class)
class AutolockSettingsUIMapperTest(
@Suppress("unused") private val testName: String,
private val toMap: Autolock,
private val expectedUiModel: AutolockSettings
) {
@Test
fun `should map to ui model`() {
val uiModel = AutolockSettingsUiMapper.toUiModel(toMap)
assertEquals(expectedUiModel, uiModel)
}
companion object {
val baseAutoLock = Autolock()
val baseUiModel = AutolockSettings(
TextUiModel(R.string.mail_pinlock_settings_autolock_immediately),
protectionType = ProtectionType.None,
biometricsAvailable = false
)
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun data(): Collection<Array<Any>> = listOf(
arrayOf(
"to autolock with default values",
baseAutoLock,
baseUiModel
),
arrayOf(
"to autolock with pin",
baseAutoLock.copy(protectionType = Protection.Pin),
baseUiModel.copy(protectionType = ProtectionType.Pin)
),
arrayOf(
"to autolock with biometrics",
baseAutoLock.copy(protectionType = Protection.Biometrics),
baseUiModel.copy(protectionType = ProtectionType.Biometrics)
),
arrayOf(
"to autolock with never",
baseAutoLock.copy(autolockInterval = AutoLockInterval.Never),
baseUiModel.copy(selectedUiInterval = TextUiModel(R.string.mail_pinlock_settings_autolock_never))
),
arrayOf(
"to autolock with biometrics Immediately",
baseAutoLock.copy(autolockInterval = AutoLockInterval.Immediately),
baseUiModel.copy(selectedUiInterval = TextUiModel(R.string.mail_pinlock_settings_autolock_immediately))
),
arrayOf(
"to autolock with biometrics FifteenMinutes",
baseAutoLock.copy(autolockInterval = AutoLockInterval.FifteenMinutes),
baseUiModel.copy(
selectedUiInterval =
TextUiModel(R.string.mail_pinlock_settings_autolock_description_fifteen_minutes)
)
),
arrayOf(
"to autolock with biometrics One Hour",
baseAutoLock.copy(autolockInterval = AutoLockInterval.OneHour),
baseUiModel.copy(
selectedUiInterval =
TextUiModel(R.string.mail_pinlock_settings_autolock_description_one_hour)
)
),
arrayOf(
"to autolock with biometrics One Day",
baseAutoLock.copy(autolockInterval = AutoLockInterval.OneDay),
baseUiModel.copy(
selectedUiInterval =
TextUiModel(R.string.mail_pinlock_settings_autolock_description_one_day)
)
)
)
}
}
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package protonmail.android.mailpinlock.presentation.autolock.mapper
import ch.protonmail.android.mailpinlock.model.AutoLockBiometricsState
import ch.protonmail.android.mailpinlock.presentation.autolock.AutoLockBiometricsUiModel
import ch.protonmail.android.mailpinlock.presentation.autolock.mapper.AutoLockBiometricsUiModelMapper
import org.junit.Test
import kotlin.test.assertEquals
class BiometricsUiModelMapperTest {
val sut = AutoLockBiometricsUiModelMapper()
@Test
fun `map Biometrics Available AND Enrolled And Enabled`() {
assertEquals(
sut.toUiModel(AutoLockBiometricsState.BiometricsAvailable.BiometricsEnrolled(true)),
AutoLockBiometricsUiModel(
enabled = true,
biometricsEnrolled = true,
biometricsHwAvailable = true
)
)
}
@Test
fun `map Biometrics Available AND Enrolled And NOT Enabled`() {
assertEquals(
sut.toUiModel(AutoLockBiometricsState.BiometricsAvailable.BiometricsEnrolled(false)),
AutoLockBiometricsUiModel(
enabled = false,
biometricsEnrolled = true,
biometricsHwAvailable = true
)
)
}
@Test
fun `map Biometrics Available NOT Enrolled`() {
assertEquals(
sut.toUiModel(AutoLockBiometricsState.BiometricsAvailable.BiometricsNotEnrolled),
AutoLockBiometricsUiModel(
enabled = false,
biometricsEnrolled = false,
biometricsHwAvailable = true
)
)
}
@Test
fun `map Biometrics NOT Available`() {
assertEquals(
sut.toUiModel(AutoLockBiometricsState.BiometricsNotAvailable),
AutoLockBiometricsUiModel(
enabled = false,
biometricsEnrolled = false,
biometricsHwAvailable = false
)
)
}
}
@@ -30,6 +30,7 @@ import ch.protonmail.android.mailpinlock.presentation.pin.mapper.AutoLockSuccess
import io.mockk.mockk
// TODO currently being refactored ET-648
@Suppress("ForbiddenComment", "unused")
internal class AutoLockPinViewModelTest {
/* private val observeAutoLockBiometricsState = mockk<ObserveAutoLockBiometricsState>()
@@ -37,7 +37,6 @@ import uniffi.proton_mail_uniffi.AutoLock.Minutes
import uniffi.proton_mail_uniffi.AutoLock.Never
import uniffi.proton_mail_uniffi.AppSettingsDiff as LocalAppSettingsDiff
fun AppSettingsDiff.toAppDiff(): LocalAppSettingsDiff {
fun setTheme(theme: Theme) = themeAppearanceLookup.getOrElse(theme, {
@@ -45,12 +44,11 @@ fun AppSettingsDiff.toAppDiff(): LocalAppSettingsDiff {
defaultThemeFallback
})
fun setAutolockInteval(interval: AutoLockInterval) =
when (interval) {
AutoLockInterval.Immediately -> Always
AutoLockInterval.Never -> Never
else -> Minutes(interval.duration.inWholeMinutes.toUByte())
}
fun setAutolockInteval(interval: AutoLockInterval) = when (interval) {
AutoLockInterval.Immediately -> Always
AutoLockInterval.Never -> Never
else -> Minutes(interval.duration.inWholeMinutes.toUByte())
}
return LocalAppSettingsDiff(
autoLock = interval?.let { setAutolockInteval(it) },
@@ -73,19 +71,17 @@ private object LocalMapperThemeConstants {
fun AppAppearance.toTheme() = themeAppearanceLookup.entries.first { it.value == this }.key
fun LocalAutolock.toAutolockInterval() =
when (this) {
is Never -> AutoLockInterval.Never
is Always -> AutoLockInterval.Immediately
is Minutes -> AutoLockInterval.fromMinutes(this.v1.toLong())
}
fun LocalAutolock.toAutolockInterval() = when (this) {
is Never -> AutoLockInterval.Never
is Always -> AutoLockInterval.Immediately
is Minutes -> AutoLockInterval.fromMinutes(this.v1.toLong())
}
fun LocalProtection.toProtection() =
when (this) {
AppProtection.NONE -> Protection.None
AppProtection.BIOMETRICS -> Protection.Biometrics
AppProtection.PIN -> Protection.Pin
}
fun LocalProtection.toProtection() = when (this) {
AppProtection.NONE -> Protection.None
AppProtection.BIOMETRICS -> Protection.Biometrics
AppProtection.PIN -> Protection.Pin
}
fun LocalAppSettings.toAppSettings(customLanguage: AppLanguage? = null) = AppSettings(
autolockProtection = protection.toProtection(),
@@ -15,8 +15,8 @@
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.mailsettings.domain.model
import ch.protonmail.android.mailpinlock.model.AutoLockInterval
data class AppSettingsDiff(val theme: Theme? = null, val interval: AutoLockInterval? = null)