PIN: fix and add tests
This commit is contained in:
@@ -57,7 +57,6 @@ class PinUnlockPresenter @Inject constructor(
|
||||
var showSignOutPrompt by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
val signOutAction = remember {
|
||||
mutableStateOf<Async<String?>>(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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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<Unit>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Unit>()
|
||||
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<Unit>()
|
||||
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<PinEntry>.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,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user