From e88a5fc8587b9e73840540fa2f75167c463daa43 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 15:45:11 +0200 Subject: [PATCH] Pin create: add test for presenter --- build.gradle.kts | 1 - features/lockscreen/impl/build.gradle.kts | 1 + .../impl/create/CreatePinPresenter.kt | 6 +- .../impl/create/CreatePinStateProvider.kt | 4 +- .../lockscreen/impl/create/CreatePinView.kt | 8 +- .../create/validation/CreatePinFailure.kt | 4 +- .../impl/create/validation/PinValidator.kt | 10 +- .../impl/create/CreatePinPresenterTest.kt | 113 ++++++++++++++++++ .../android/tests/testutils/ReceiveTurbine.kt | 10 ++ 9 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 487776d948..e14ad71981 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -253,7 +253,6 @@ koverMerged { // Temporary until we have actually something to test. excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter" excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter$*" - excludes += "io.element.android.features.lockscreen.impl.create.CreatePinPresenter" } bound { minValue = 85 diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index af63538db5..028d8bee3c 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) testImplementation(projects.libraries.cryptography.test) testImplementation(projects.libraries.cryptography.impl) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index 525b80314b..e72e636ed4 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -57,7 +57,7 @@ class CreatePinPresenter @Inject constructor( if (confirmPinEntry == choosePinEntry) { //TODO save in db and navigate to next screen } else { - createPinFailure = CreatePinFailure.ConfirmationPinNotMatching + createPinFailure = CreatePinFailure.PinsDontMatch } } } else { @@ -74,11 +74,11 @@ class CreatePinPresenter @Inject constructor( } CreatePinEvents.ClearFailure -> { when (createPinFailure) { - is CreatePinFailure.ConfirmationPinNotMatching -> { + is CreatePinFailure.PinsDontMatch -> { choosePinEntry = PinEntry.empty(PIN_SIZE) confirmPinEntry = PinEntry.empty(PIN_SIZE) } - is CreatePinFailure.ChosenPinBlacklisted -> { + is CreatePinFailure.PinBlacklisted -> { choosePinEntry = PinEntry.empty(PIN_SIZE) } null -> Unit diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt index 40287622fd..543360f91e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt @@ -35,11 +35,11 @@ open class CreatePinStateProvider : PreviewParameterProvider { choosePinEntry = PinEntry.empty(4).fillWith("1789"), confirmPinEntry = PinEntry.empty(4).fillWith("1788"), isConfirmationStep = true, - creationFailure = CreatePinFailure.ConfirmationPinNotMatching + creationFailure = CreatePinFailure.PinsDontMatch ), aCreatePinState( choosePinEntry = PinEntry.empty(4).fillWith("1111"), - creationFailure = CreatePinFailure.ChosenPinBlacklisted + creationFailure = CreatePinFailure.PinBlacklisted ), ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index fdce08c229..915bd2b4b0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -125,16 +125,16 @@ private fun CreatePinContent( @Composable private fun CreatePinFailure.content(): String { return when (this) { - CreatePinFailure.ChosenPinBlacklisted -> "You cannot choose this as your PIN code for security reasons" - CreatePinFailure.ConfirmationPinNotMatching -> "Please enter the same PIN twice" + CreatePinFailure.PinBlacklisted -> "You cannot choose this as your PIN code for security reasons" + CreatePinFailure.PinsDontMatch -> "Please enter the same PIN twice" } } @Composable private fun CreatePinFailure.title(): String { return when (this) { - CreatePinFailure.ChosenPinBlacklisted -> "Choose a different PIN" - CreatePinFailure.ConfirmationPinNotMatching -> "PINs don't match" + CreatePinFailure.PinBlacklisted -> "Choose a different PIN" + CreatePinFailure.PinsDontMatch -> "PINs don't match" } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt index 96c0de0056..8c0cb78921 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt @@ -17,6 +17,6 @@ package io.element.android.features.lockscreen.impl.create.validation sealed interface CreatePinFailure { - data object ChosenPinBlacklisted : CreatePinFailure - data object ConfirmationPinNotMatching : CreatePinFailure + data object PinBlacklisted : CreatePinFailure + data object PinsDontMatch : CreatePinFailure } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt index 8c1854ecee..7353ec47d0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt @@ -16,13 +16,17 @@ package io.element.android.features.lockscreen.impl.create.validation +import androidx.annotation.VisibleForTesting import io.element.android.features.lockscreen.impl.create.model.PinEntry import javax.inject.Inject -private val BLACKLIST = listOf("0000", "1234") - class PinValidator @Inject constructor() { + companion object { + @VisibleForTesting + val BLACKLIST = listOf("0000", "1234") + } + sealed interface Result { data object Valid : Result data class Invalid(val failure: CreatePinFailure) : Result @@ -32,7 +36,7 @@ class PinValidator @Inject constructor() { val pinAsText = pinEntry.toText() val isBlacklisted = BLACKLIST.any { it == pinAsText } return if (isBlacklisted) { - Result.Invalid(CreatePinFailure.ChosenPinBlacklisted) + Result.Invalid(CreatePinFailure.PinBlacklisted) } else { Result.Valid } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt new file mode 100644 index 0000000000..9c86039fe1 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt @@ -0,0 +1,113 @@ +/* + * 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.create + +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.create.model.PinDigit +import io.element.android.features.lockscreen.impl.create.model.PinEntry +import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure +import io.element.android.features.lockscreen.impl.create.validation.PinValidator +import io.element.android.tests.testutils.awaitLastSequentialItem +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class CreatePinPresenterTest { + + private val blacklistedPin = PinValidator.BLACKLIST.first() + private val halfCompletePin = "12" + private val completePin = "1235" + private val mismatchedPin = "1236" + + @Test + fun `present - complete flow`() = runTest { + + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().also { state -> + state.choosePinEntry.assertEmpty() + state.confirmPinEntry.assertEmpty() + assertThat(state.createPinFailure).isNull() + assertThat(state.isConfirmationStep).isFalse() + state.eventSink(CreatePinEvents.OnPinEntryChanged(halfCompletePin)) + } + awaitItem().also { state -> + state.choosePinEntry.assertText(halfCompletePin) + state.confirmPinEntry.assertEmpty() + assertThat(state.createPinFailure).isNull() + assertThat(state.isConfirmationStep).isFalse() + state.eventSink(CreatePinEvents.OnPinEntryChanged(blacklistedPin)) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertText(blacklistedPin) + assertThat(state.createPinFailure).isEqualTo(CreatePinFailure.PinBlacklisted) + state.eventSink(CreatePinEvents.ClearFailure) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertEmpty() + assertThat(state.createPinFailure).isNull() + state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertText(completePin) + state.confirmPinEntry.assertEmpty() + assertThat(state.isConfirmationStep).isTrue() + state.eventSink(CreatePinEvents.OnPinEntryChanged(mismatchedPin)) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertText(completePin) + state.confirmPinEntry.assertText(mismatchedPin) + assertThat(state.createPinFailure).isEqualTo(CreatePinFailure.PinsDontMatch) + state.eventSink(CreatePinEvents.ClearFailure) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertEmpty() + state.confirmPinEntry.assertEmpty() + assertThat(state.isConfirmationStep).isFalse() + assertThat(state.createPinFailure).isNull() + state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertText(completePin) + state.confirmPinEntry.assertEmpty() + assertThat(state.isConfirmationStep).isTrue() + state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + } + awaitItem().also { state -> + state.choosePinEntry.assertText(completePin) + state.confirmPinEntry.assertText(completePin) + } + } + } + + private fun PinEntry.assertText(text: String) { + assertThat(toText()).isEqualTo(text) + } + + private fun PinEntry.assertEmpty() { + val isEmpty = digits.all { it is PinDigit.Empty } + assertThat(isEmpty).isTrue() + } + + private fun createPresenter(): CreatePinPresenter { + return CreatePinPresenter(PinValidator()) + } +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt index 06b6b3d3ea..3e47dd63ce 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt @@ -32,6 +32,16 @@ suspend fun ReceiveTurbine.consumeItemsUntilTimeout(timeout: Durati return consumeItemsUntilPredicate(timeout) { false } } +/** + * Consume all items which are emitted sequentially. + * Use the smallest timeout possible internally to avoid wasting time. + * Same as calling skipItems(x) and then awaitItem() but without assumption on the number of items. + * @return the last item emitted. + */ +suspend fun ReceiveTurbine.awaitLastSequentialItem(): T { + return consumeItemsUntilTimeout(1.milliseconds).last() +} + /** * Consume items until predicate is true, or timeout is reached waiting for an event, or we receive terminal event. * The timeout is applied for each event.