PIN: fix and add tests
This commit is contained in:
parent
432e209618
commit
ed4815c40a
4 changed files with 135 additions and 24 deletions
|
|
@ -57,7 +57,6 @@ class PinUnlockPresenter @Inject constructor(
|
||||||
var showSignOutPrompt by rememberSaveable {
|
var showSignOutPrompt by rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val signOutAction = remember {
|
val signOutAction = remember {
|
||||||
mutableStateOf<Async<String?>>(Async.Uninitialized)
|
mutableStateOf<Async<String?>>(Async.Uninitialized)
|
||||||
}
|
}
|
||||||
|
|
@ -92,8 +91,10 @@ class PinUnlockPresenter @Inject constructor(
|
||||||
PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true
|
PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true
|
||||||
PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false
|
PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false
|
||||||
PinUnlockEvents.SignOut -> {
|
PinUnlockEvents.SignOut -> {
|
||||||
showSignOutPrompt = false
|
if (showSignOutPrompt) {
|
||||||
coroutineScope.signOut(signOutAction)
|
showSignOutPrompt = false
|
||||||
|
coroutineScope.signOut(signOutAction)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PinUnlockEvents.OnUseBiometric -> {
|
PinUnlockEvents.OnUseBiometric -> {
|
||||||
//TODO
|
//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.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.assertEmpty
|
||||||
import io.element.android.features.lockscreen.impl.pin.model.assertText
|
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.PinValidator
|
||||||
import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure
|
import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -38,8 +41,13 @@ class SetupPinPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - complete flow`() = runTest {
|
fun `present - complete flow`() = runTest {
|
||||||
|
val pinCodeCreated = CompletableDeferred<Unit>()
|
||||||
val presenter = createSetupPinPresenter()
|
val callback = object : PinCodeManager.Callback {
|
||||||
|
override fun onPinCodeCreated() {
|
||||||
|
pinCodeCreated.complete(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val presenter = createSetupPinPresenter(callback)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -96,10 +104,13 @@ class SetupPinPresenterTest {
|
||||||
state.choosePinEntry.assertText(completePin)
|
state.choosePinEntry.assertText(completePin)
|
||||||
state.confirmPinEntry.assertText(completePin)
|
state.confirmPinEntry.assertText(completePin)
|
||||||
}
|
}
|
||||||
|
pinCodeCreated.await()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSetupPinPresenter(): SetupPinPresenter {
|
private fun createSetupPinPresenter(callback: PinCodeManager.Callback): SetupPinPresenter {
|
||||||
return SetupPinPresenter(PinValidator(setOf(blacklistedPin)), aBuildMeta())
|
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.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.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.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -37,16 +40,27 @@ class PinUnlockPresenterTest {
|
||||||
private val completePin = "1235"
|
private val completePin = "1235"
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - complete flow`() = runTest {
|
fun `present - success verify flow`() = runTest {
|
||||||
val presenter = createPinUnlockPresenter(this)
|
val pinCodeVerified = CompletableDeferred<Unit>()
|
||||||
|
val callback = object : PinCodeManager.Callback {
|
||||||
|
override fun onPinCodeCreated() {
|
||||||
|
pinCodeVerified.complete(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val presenter = createPinUnlockPresenter(this, callback)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
state.pinEntry.assertEmpty()
|
assertThat(state.pinEntry).isInstanceOf(Async.Uninitialized::class.java)
|
||||||
assertThat(state.showWrongPinTitle).isFalse()
|
assertThat(state.showWrongPinTitle).isFalse()
|
||||||
assertThat(state.showSignOutPrompt).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('1')))
|
||||||
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2')))
|
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.Number('3')))
|
||||||
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Back))
|
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Back))
|
||||||
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Empty))
|
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Empty))
|
||||||
|
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3')))
|
||||||
|
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('5')))
|
||||||
}
|
}
|
||||||
awaitLastSequentialItem().also { state ->
|
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)
|
state.eventSink(PinUnlockEvents.OnForgetPin)
|
||||||
}
|
}
|
||||||
awaitLastSequentialItem().also { state ->
|
awaitLastSequentialItem().also { state ->
|
||||||
|
|
@ -67,22 +127,33 @@ class PinUnlockPresenterTest {
|
||||||
}
|
}
|
||||||
awaitLastSequentialItem().also { state ->
|
awaitLastSequentialItem().also { state ->
|
||||||
assertThat(state.showSignOutPrompt).isEqualTo(false)
|
assertThat(state.showSignOutPrompt).isEqualTo(false)
|
||||||
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3')))
|
state.eventSink(PinUnlockEvents.OnForgetPin)
|
||||||
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('5')))
|
|
||||||
}
|
}
|
||||||
awaitLastSequentialItem().also { state ->
|
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 {
|
private fun Async<PinEntry>.assertText(text: String) {
|
||||||
val featureFlagService = FakeFeatureFlagService().apply {
|
dataOrNull()?.assertText(text)
|
||||||
setFeatureEnabled(FeatureFlags.PinUnlock, true)
|
}
|
||||||
|
|
||||||
|
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(
|
return PinUnlockPresenter(
|
||||||
lockScreenStateService,
|
pinCodeManager,
|
||||||
|
FakeMatrixClient(),
|
||||||
scope,
|
scope,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue