diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index 19f075b5b5..b26f967b4c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -57,7 +57,6 @@ class PinUnlockPresenter @Inject constructor( var showSignOutPrompt by rememberSaveable { mutableStateOf(false) } - val signOutAction = remember { mutableStateOf>(Async.Uninitialized) } @@ -92,8 +91,10 @@ class PinUnlockPresenter @Inject constructor( PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false PinUnlockEvents.SignOut -> { - showSignOutPrompt = false - coroutineScope.signOut(signOutAction) + if (showSignOutPrompt) { + showSignOutPrompt = false + coroutineScope.signOut(signOutAction) + } } PinUnlockEvents.OnUseBiometric -> { //TODO diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt new file mode 100644 index 0000000000..a2e2dacf97 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.lockscreen.impl.pin + +import io.element.android.features.lockscreen.impl.pin.storage.InMemoryPinCodeStore +import io.element.android.libraries.cryptography.impl.AESEncryptionDecryptionService +import io.element.android.libraries.cryptography.test.SimpleSecretKeyProvider + +internal fun createPinCodeManager(): PinCodeManager { + val pinCodeStore = InMemoryPinCodeStore() + val secretKeyProvider = SimpleSecretKeyProvider() + val encryptionDecryptionService = AESEncryptionDecryptionService() + return DefaultPinCodeManager(secretKeyProvider, encryptionDecryptionService, pinCodeStore) +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index ff797b52f4..0cc38dd355 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -20,12 +20,15 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.lockscreen.impl.pin.PinCodeManager +import io.element.android.features.lockscreen.impl.pin.createPinCodeManager import io.element.android.features.lockscreen.impl.pin.model.assertEmpty import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.setup.validation.PinValidator import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.awaitLastSequentialItem +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.test.runTest import org.junit.Test @@ -38,8 +41,13 @@ class SetupPinPresenterTest { @Test fun `present - complete flow`() = runTest { - - val presenter = createSetupPinPresenter() + val pinCodeCreated = CompletableDeferred() + val callback = object : PinCodeManager.Callback { + override fun onPinCodeCreated() { + pinCodeCreated.complete(Unit) + } + } + val presenter = createSetupPinPresenter(callback) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -96,10 +104,13 @@ class SetupPinPresenterTest { state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertText(completePin) } + pinCodeCreated.await() } } - private fun createSetupPinPresenter(): SetupPinPresenter { - return SetupPinPresenter(PinValidator(setOf(blacklistedPin)), aBuildMeta()) + private fun createSetupPinPresenter(callback: PinCodeManager.Callback): SetupPinPresenter { + val pinCodeManager = createPinCodeManager() + pinCodeManager.addCallback(callback) + return SetupPinPresenter(PinValidator(setOf(blacklistedPin)), aBuildMeta(), pinCodeManager) } } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt index 391a51e692..16e44b2af0 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt @@ -20,13 +20,16 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.lockscreen.impl.pin.model.assertEmpty +import io.element.android.features.lockscreen.impl.pin.PinCodeManager +import io.element.android.features.lockscreen.impl.pin.createPinCodeManager +import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.assertText -import io.element.android.features.lockscreen.impl.DefaultLockScreenService import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.tests.testutils.awaitLastSequentialItem +import io.element.android.tests.testutils.consumeItemsUntilPredicate +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -37,16 +40,27 @@ class PinUnlockPresenterTest { private val completePin = "1235" @Test - fun `present - complete flow`() = runTest { - val presenter = createPinUnlockPresenter(this) + fun `present - success verify flow`() = runTest { + val pinCodeVerified = CompletableDeferred() + val callback = object : PinCodeManager.Callback { + override fun onPinCodeCreated() { + pinCodeVerified.complete(Unit) + } + } + val presenter = createPinUnlockPresenter(this, callback) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { awaitItem().also { state -> - state.pinEntry.assertEmpty() + assertThat(state.pinEntry).isInstanceOf(Async.Uninitialized::class.java) assertThat(state.showWrongPinTitle).isFalse() assertThat(state.showSignOutPrompt).isFalse() - assertThat(state.remainingAttempts).isEqualTo(3) + assertThat(state.signOutAction).isInstanceOf(Async.Uninitialized::class.java) + assertThat(state.remainingAttempts).isInstanceOf(Async.Uninitialized::class.java) + } + consumeItemsUntilPredicate { + it.pinEntry is Async.Success && it.remainingAttempts is Async.Success + }.last().also { state -> state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2'))) } @@ -55,9 +69,55 @@ class PinUnlockPresenterTest { state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Back)) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Empty)) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('5'))) } awaitLastSequentialItem().also { state -> - state.pinEntry.assertText(halfCompletePin) + state.pinEntry.assertText(completePin) + } + pinCodeVerified.await() + } + } + + @Test + fun `present - failure verify flow`() = runTest { + val pinCodeVerified = CompletableDeferred() + val callback = object : PinCodeManager.Callback { + override fun onPinCodeCreated() { + pinCodeVerified.complete(Unit) + } + } + val presenter = createPinUnlockPresenter(this, callback) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = consumeItemsUntilPredicate { + it.pinEntry is Async.Success && it.remainingAttempts is Async.Success + }.last() + val numberOfAttempts = initialState.remainingAttempts.dataOrNull() ?: 0 + repeat(numberOfAttempts) { + initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) + initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2'))) + initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) + initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('4'))) + } + awaitLastSequentialItem().also { state -> + assertThat(state.remainingAttempts.dataOrNull()).isEqualTo(0) + assertThat(state.showSignOutPrompt).isEqualTo(true) + assertThat(state.isSignOutPromptCancellable).isEqualTo(false) + } + } + } + + @Test + fun `present - forgot pin flow`() = runTest { + val presenter = createPinUnlockPresenter(this) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + consumeItemsUntilPredicate { + it.pinEntry is Async.Success && it.remainingAttempts is Async.Success + }.last().also { state -> state.eventSink(PinUnlockEvents.OnForgetPin) } awaitLastSequentialItem().also { state -> @@ -67,22 +127,33 @@ class PinUnlockPresenterTest { } awaitLastSequentialItem().also { state -> assertThat(state.showSignOutPrompt).isEqualTo(false) - state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) - state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('5'))) + state.eventSink(PinUnlockEvents.OnForgetPin) } awaitLastSequentialItem().also { state -> - state.pinEntry.assertText(completePin) + assertThat(state.showSignOutPrompt).isEqualTo(true) + state.eventSink(PinUnlockEvents.SignOut) + } + consumeItemsUntilPredicate { state -> + state.signOutAction is Async.Success } } } - private suspend fun createPinUnlockPresenter(scope: CoroutineScope): PinUnlockPresenter { - val featureFlagService = FakeFeatureFlagService().apply { - setFeatureEnabled(FeatureFlags.PinUnlock, true) + private fun Async.assertText(text: String) { + dataOrNull()?.assertText(text) + } + + private suspend fun createPinUnlockPresenter( + scope: CoroutineScope, + callback: PinCodeManager.Callback = object : PinCodeManager.Callback {}, + ): PinUnlockPresenter { + val pinCodeManager = createPinCodeManager().apply { + addCallback(callback) + createPinCode(completePin) } - val lockScreenStateService = DefaultLockScreenService(featureFlagService) return PinUnlockPresenter( - lockScreenStateService, + pinCodeManager, + FakeMatrixClient(), scope, ) }