Improve tests for lockscreen (#6796)

* Improve tests

* Address review comments.

* Refactor pin code tests and code to improve testability.

* Fix lint issues
This commit is contained in:
Jorge Martin Espinosa 2022-08-10 14:00:36 +02:00 committed by GitHub
parent fcc7bbadfa
commit aecf460c96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 155 additions and 101 deletions

View File

@ -64,8 +64,6 @@ class LockScreenViewModel @AssistedInject constructor(
private val biometricHelper = biometricHelperFactory.create(initialState.lockScreenConfiguration) private val biometricHelper = biometricHelperFactory.create(initialState.lockScreenConfiguration)
private var firstEnteredCode: String? = null
// BiometricPrompt will automatically disable system auth after too many failed auth attempts // BiometricPrompt will automatically disable system auth after too many failed auth attempts
private var isSystemAuthTemporarilyDisabledByBiometricPrompt = false private var isSystemAuthTemporarilyDisabledByBiometricPrompt = false
@ -108,18 +106,17 @@ class LockScreenViewModel @AssistedInject constructor(
val state = awaitState() val state = awaitState()
when (state.lockScreenConfiguration.mode) { when (state.lockScreenConfiguration.mode) {
LockScreenMode.CREATE -> { LockScreenMode.CREATE -> {
if (firstEnteredCode == null && state.lockScreenConfiguration.needsNewCodeValidation) { val enteredPinCode = (state.pinCodeState as? PinCodeState.FirstCodeEntered)?.pinCode
firstEnteredCode = code if (enteredPinCode == null && state.lockScreenConfiguration.needsNewCodeValidation) {
_viewEvents.post(LockScreenViewEvent.ClearPinCode(false)) _viewEvents.post(LockScreenViewEvent.ClearPinCode(confirmationFailed = false))
emit(PinCodeState.FirstCodeEntered) emit(PinCodeState.FirstCodeEntered(code))
} else { } else {
if (!state.lockScreenConfiguration.needsNewCodeValidation || code == firstEnteredCode) { if (!state.lockScreenConfiguration.needsNewCodeValidation || code == enteredPinCode) {
pinCodeHelper.createPinCode(code) pinCodeHelper.createPinCode(code)
_viewEvents.post(LockScreenViewEvent.CodeCreationComplete) _viewEvents.post(LockScreenViewEvent.CodeCreationComplete)
emit(null) emit(null)
} else { } else {
firstEnteredCode = null _viewEvents.post(LockScreenViewEvent.ClearPinCode(confirmationFailed = true))
_viewEvents.post(LockScreenViewEvent.ClearPinCode(true))
emit(PinCodeState.Idle) emit(PinCodeState.Idle)
} }
} }
@ -137,7 +134,9 @@ class LockScreenViewModel @AssistedInject constructor(
}.catch { error -> }.catch { error ->
_viewEvents.post(LockScreenViewEvent.AuthError(AuthMethod.PIN_CODE, error)) _viewEvents.post(LockScreenViewEvent.AuthError(AuthMethod.PIN_CODE, error))
}.onEach { newPinState -> }.onEach { newPinState ->
newPinState?.let { setState { copy(pinCodeState = it) } } if (newPinState != null) {
setState { copy(pinCodeState = newPinState) }
}
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
@SuppressLint("NewApi") @SuppressLint("NewApi")

View File

@ -27,11 +27,11 @@ data class LockScreenViewState(
val isBiometricKeyInvalidated: Boolean, val isBiometricKeyInvalidated: Boolean,
) : MavericksState { ) : MavericksState {
constructor(lockScreenConfiguration: LockScreenConfiguration) : this( constructor(lockScreenConfiguration: LockScreenConfiguration) : this(
lockScreenConfiguration, false, false, PinCodeState.Idle, false lockScreenConfiguration, false, false, PinCodeState.Idle, false,
) )
} }
sealed class PinCodeState { sealed class PinCodeState {
object Idle : PinCodeState() object Idle : PinCodeState()
object FirstCodeEntered : PinCodeState() data class FirstCodeEntered(val pinCode: String) : PinCodeState()
} }

View File

@ -19,9 +19,11 @@ package im.vector.app.features.pin.lockscreen.fragment
import android.app.KeyguardManager import android.app.KeyguardManager
import android.os.Build import android.os.Build
import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyPermanentlyInvalidatedException
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.airbnb.mvrx.test.MvRxTestRule import com.airbnb.mvrx.test.MvRxTestRule
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.app.features.pin.lockscreen.biometrics.BiometricAuthError
import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
@ -42,10 +44,8 @@ import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldNotBeEqualTo import org.amshove.kluent.shouldNotBeEqualTo
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@ -76,138 +76,141 @@ class LockScreenViewModelTests {
@Test @Test
fun `init migrates old keys to new ones if needed`() { fun `init migrates old keys to new ones if needed`() {
// given
val initialState = createViewState() val initialState = createViewState()
// when
LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager) LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
// then
coVerify { keysMigrator.migrateIfNeeded() } coVerify { keysMigrator.migrateIfNeeded() }
} }
@Test @Test
fun `init updates the initial state with biometric info`() = runTest { fun `init updates the initial state with biometric info`() = runTest {
// given
every { biometricHelper.isSystemAuthEnabledAndValid } returns true every { biometricHelper.isSystemAuthEnabledAndValid } returns true
val initialState = createViewState() val initialState = createViewState()
// when
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
advanceUntilIdle() // then
val newState = viewModel.awaitState() val newState = viewModel.awaitState() // Can't use viewModel.test() here since we want to record events emitted on init
newState shouldNotBeEqualTo initialState newState shouldNotBeEqualTo initialState
} }
@Test @Test
fun `Updating the initial state with biometric info waits until device is unlocked on Android 12+`() = runTest { fun `Updating the initial state with biometric info waits until device is unlocked on Android 12+`() = runTest {
// given
val initialState = createViewState() val initialState = createViewState()
versionProvider.value = Build.VERSION_CODES.S versionProvider.value = Build.VERSION_CODES.S
// when
LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager) LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
advanceUntilIdle() // then
verify { keyguardManager.isDeviceLocked } verify { keyguardManager.isDeviceLocked }
} }
@Test @Test
fun `when ViewModel is instantiated initialState is updated with biometric info`() { fun `when ViewModel is instantiated initialState is updated with biometric info`() {
// given
givenShowBiometricPromptAutomatically()
val initialState = createViewState() val initialState = createViewState()
// This should set canUseBiometricAuth to true // when
every { biometricHelper.isSystemAuthEnabledAndValid } returns true
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val newState = withState(viewModel) { it } // then
initialState shouldNotBeEqualTo newState withState(viewModel) { newState ->
initialState shouldNotBeEqualTo newState
}
} }
@Test @Test
fun `when onPinCodeEntered is called in VERIFY mode, the code is verified and the result is emitted as a ViewEvent`() = runTest { fun `when onPinCodeEntered is called in VERIFY mode and verification is successful, code is verified and result is emitted as a ViewEvent`() = runTest {
// given
val initialState = createViewState() val initialState = createViewState()
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
coEvery { pinCodeHelper.verifyPinCode(any()) } returns true coEvery { pinCodeHelper.verifyPinCode(any()) } returns true
val test = viewModel.test()
val events = viewModel.test().viewEvents // when
events.assertNoValues()
val stateBefore = viewModel.awaitState()
viewModel.handle(LockScreenAction.PinCodeEntered("1234")) viewModel.handle(LockScreenAction.PinCodeEntered("1234"))
// then
coVerify { pinCodeHelper.verifyPinCode(any()) } coVerify { pinCodeHelper.verifyPinCode(any()) }
events.assertValues(LockScreenViewEvent.AuthSuccessful(AuthMethod.PIN_CODE)) test.assertEvents(LockScreenViewEvent.AuthSuccessful(AuthMethod.PIN_CODE))
test.assertStates(initialState)
}
@Test
fun `when onPinCodeEntered is called in VERIFY mode and verification fails, the error result is emitted as a ViewEvent`() = runTest {
// given
val initialState = createViewState()
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
coEvery { pinCodeHelper.verifyPinCode(any()) } returns false coEvery { pinCodeHelper.verifyPinCode(any()) } returns false
val test = viewModel.test()
// when
viewModel.handle(LockScreenAction.PinCodeEntered("1234")) viewModel.handle(LockScreenAction.PinCodeEntered("1234"))
events.assertValues(LockScreenViewEvent.AuthSuccessful(AuthMethod.PIN_CODE), LockScreenViewEvent.AuthFailure(AuthMethod.PIN_CODE)) // then
coVerify { pinCodeHelper.verifyPinCode(any()) }
val stateAfter = viewModel.awaitState() test.assertEvents(LockScreenViewEvent.AuthFailure(AuthMethod.PIN_CODE))
stateBefore shouldBeEqualTo stateAfter test.assertStates(initialState)
} }
@Test @Test
fun `when onPinCodeEntered is called in CREATE mode with no confirmation needed it creates the pin code`() = runTest { fun `when onPinCodeEntered is called in CREATE mode with no confirmation needed it creates the pin code`() = runTest {
// given
val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = false) val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = false)
val initialState = createViewState(lockScreenConfiguration = configuration) val initialState = createViewState(lockScreenConfiguration = configuration)
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val test = viewModel.test()
val events = viewModel.test().viewEvents // when
events.assertNoValues()
viewModel.handle(LockScreenAction.PinCodeEntered("1234")) viewModel.handle(LockScreenAction.PinCodeEntered("1234"))
// then
coVerify { pinCodeHelper.createPinCode(any()) } coVerify { pinCodeHelper.createPinCode(any()) }
test.assertEvents(LockScreenViewEvent.CodeCreationComplete)
events.assertValues(LockScreenViewEvent.CodeCreationComplete)
} }
@Test @Test
fun `when onPinCodeEntered is called twice in CREATE mode with confirmation needed it verifies and creates the pin code`() = runTest { fun `when onPinCodeEntered is called twice in CREATE mode with confirmation needed it verifies and creates the pin code`() = runTest {
// given
val pinCode = "1234"
val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = true) val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = true)
val initialState = createViewState(lockScreenConfiguration = configuration) val initialState = createViewState(lockScreenConfiguration = configuration, pinCodeState = PinCodeState.FirstCodeEntered(pinCode))
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val test = viewModel.test()
val events = viewModel.test().viewEvents // when
events.assertNoValues() viewModel.handle(LockScreenAction.PinCodeEntered(pinCode))
// then
viewModel.handle(LockScreenAction.PinCodeEntered("1234")) test.assertEvents(LockScreenViewEvent.CodeCreationComplete)
.assertLatestState { (it.pinCodeState as? PinCodeState.FirstCodeEntered)?.pinCode == pinCode }
events.assertValues(LockScreenViewEvent.ClearPinCode(false))
val pinCodeState = viewModel.awaitState().pinCodeState
pinCodeState shouldBeEqualTo PinCodeState.FirstCodeEntered
viewModel.handle(LockScreenAction.PinCodeEntered("1234"))
events.assertValues(LockScreenViewEvent.ClearPinCode(false), LockScreenViewEvent.CodeCreationComplete)
} }
@Test @Test
fun `when onPinCodeEntered is called in CREATE mode with incorrect confirmation it clears the pin code`() = runTest { fun `when onPinCodeEntered is called in CREATE mode with incorrect confirmation it clears the pin code`() = runTest {
// given
val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = true) val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = true)
val initialState = createViewState(lockScreenConfiguration = configuration) val initialState = createViewState(lockScreenConfiguration = configuration, pinCodeState = PinCodeState.FirstCodeEntered("1234"))
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val test = viewModel.test()
val events = viewModel.test().viewEvents // when
events.assertNoValues()
viewModel.handle(LockScreenAction.PinCodeEntered("1234"))
events.assertValues(LockScreenViewEvent.ClearPinCode(false))
val pinCodeState = viewModel.awaitState().pinCodeState
pinCodeState shouldBeEqualTo PinCodeState.FirstCodeEntered
viewModel.handle(LockScreenAction.PinCodeEntered("4321")) viewModel.handle(LockScreenAction.PinCodeEntered("4321"))
events.assertValues(LockScreenViewEvent.ClearPinCode(false), LockScreenViewEvent.ClearPinCode(true)) // then
val newPinCodeState = viewModel.awaitState().pinCodeState test.assertEvents(LockScreenViewEvent.ClearPinCode(true))
newPinCodeState shouldBeEqualTo PinCodeState.Idle .assertLatestState(initialState.copy(pinCodeState = PinCodeState.Idle))
} }
@Test @Test
fun `onPinCodeEntered handles exceptions`() = runTest { fun `onPinCodeEntered handles exceptions`() = runTest {
// given
val initialState = createViewState() val initialState = createViewState()
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val exception = IllegalStateException("Something went wrong") val exception = IllegalStateException("Something went wrong")
coEvery { pinCodeHelper.verifyPinCode(any()) } throws exception coEvery { pinCodeHelper.verifyPinCode(any()) } throws exception
val test = viewModel.test()
val events = viewModel.test().viewEvents // when
events.assertNoValues()
viewModel.handle(LockScreenAction.PinCodeEntered("1234")) viewModel.handle(LockScreenAction.PinCodeEntered("1234"))
// then
events.assertValues(LockScreenViewEvent.AuthError(AuthMethod.PIN_CODE, exception)) test.assertEvents(LockScreenViewEvent.AuthError(AuthMethod.PIN_CODE, exception))
} }
@Test @Test
fun `when showBiometricPrompt catches a KeyPermanentlyInvalidatedException it disables biometric authentication`() = runTest { fun `when showBiometricPrompt catches a KeyPermanentlyInvalidatedException it disables biometric authentication`() = runTest {
// given
versionProvider.value = Build.VERSION_CODES.M versionProvider.value = Build.VERSION_CODES.M
every { biometricHelper.isSystemKeyValid } returns false every { biometricHelper.isSystemKeyValid } returns false
val exception = KeyPermanentlyInvalidatedException() val exception = KeyPermanentlyInvalidatedException()
coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } throws exception coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } throws exception
@ -218,49 +221,81 @@ class LockScreenViewModelTests {
lockScreenConfiguration = configuration lockScreenConfiguration = configuration
) )
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val test = viewModel.test()
val events = viewModel.test().viewEvents // when
events.assertNoValues()
viewModel.handle(LockScreenAction.ShowBiometricPrompt(mockk())) viewModel.handle(LockScreenAction.ShowBiometricPrompt(mockk()))
// then
events.assertValues(LockScreenViewEvent.ShowBiometricKeyInvalidatedMessage) test.assertEvents(LockScreenViewEvent.ShowBiometricKeyInvalidatedMessage)
// Biometric key is invalidated so biometric auth is disabled
.assertLatestState { !it.canUseBiometricAuth }
verify { biometricHelper.disableAuthentication() } verify { biometricHelper.disableAuthentication() }
// System key was deleted, biometric auth should be disabled
every { biometricHelper.isSystemAuthEnabledAndValid } returns false
val newState = viewModel.awaitState()
newState.canUseBiometricAuth.shouldBeFalse()
} }
@Test @Test
fun `when showBiometricPrompt receives an event it propagates it as a ViewEvent`() = runTest { fun `when showBiometricPrompt receives an event it propagates it as a ViewEvent`() = runTest {
// given
val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager) val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } returns flowOf(false, true) coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } returns flowOf(false, true)
val test = viewModel.test()
val events = viewModel.test().viewEvents // when
events.assertNoValues()
viewModel.handle(LockScreenAction.ShowBiometricPrompt(mockk())) viewModel.handle(LockScreenAction.ShowBiometricPrompt(mockk()))
// then
events.assertValues(LockScreenViewEvent.AuthFailure(AuthMethod.BIOMETRICS), LockScreenViewEvent.AuthSuccessful(AuthMethod.BIOMETRICS)) test.assertEvents(LockScreenViewEvent.AuthFailure(AuthMethod.BIOMETRICS), LockScreenViewEvent.AuthSuccessful(AuthMethod.BIOMETRICS))
} }
@Test @Test
fun `showBiometricPrompt handles exceptions`() = runTest { fun `showBiometricPrompt handles exceptions`() = runTest {
// given
val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager) val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val exception = IllegalStateException("Something went wrong") val exception = IllegalStateException("Something went wrong")
coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } throws exception coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } throws exception
val test = viewModel.test()
val events = viewModel.test().viewEvents // when
events.assertNoValues()
viewModel.handle(LockScreenAction.ShowBiometricPrompt(mockk())) viewModel.handle(LockScreenAction.ShowBiometricPrompt(mockk()))
// then
events.assertValues(LockScreenViewEvent.AuthError(AuthMethod.BIOMETRICS, exception)) test.assertEvents(LockScreenViewEvent.AuthError(AuthMethod.BIOMETRICS, exception))
} }
private fun createViewState( @Test
fun `when showBiometricPrompt handles isAuthDisabledError, canUseBiometricAuth becomes false`() = runTest {
// given
val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val exception = BiometricAuthError(BiometricPrompt.ERROR_LOCKOUT_PERMANENT, "Permanent lockout")
coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } throws exception
val test = viewModel.test()
// when
viewModel.handle(LockScreenAction.ShowBiometricPrompt(mockk()))
// then
exception.isAuthDisabledError.shouldBeTrue()
test.assertEvents(LockScreenViewEvent.AuthError(AuthMethod.BIOMETRICS, exception))
.assertLatestState { !it.canUseBiometricAuth }
}
@Test
fun `when OnUIReady action is received and showBiometricPromptAutomatically is true it shows prompt`() = runTest {
// given
givenShowBiometricPromptAutomatically()
val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val test = viewModel.test()
// when
viewModel.handle(LockScreenAction.OnUIReady)
// then
test.assertEvents(LockScreenViewEvent.ShowBiometricPromptAutomatically)
}
@Test
fun `when OnUIReady action is received and isBiometricKeyInvalidated is true it shows prompt`() = runTest {
// given
givenBiometricKeyIsInvalidated()
val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val test = viewModel.test()
// when
viewModel.handle(LockScreenAction.OnUIReady)
// then
test.assertEvents(LockScreenViewEvent.ShowBiometricKeyInvalidatedMessage)
}
private fun createViewState(
lockScreenConfiguration: LockScreenConfiguration = createDefaultConfiguration(), lockScreenConfiguration: LockScreenConfiguration = createDefaultConfiguration(),
canUseBiometricAuth: Boolean = false, canUseBiometricAuth: Boolean = false,
showBiometricPromptAutomatically: Boolean = false, showBiometricPromptAutomatically: Boolean = false,
@ -286,4 +321,13 @@ class LockScreenViewModelTests {
isDeviceCredentialUnlockEnabled, isDeviceCredentialUnlockEnabled,
needsNewCodeValidation needsNewCodeValidation
).let(otherChanges) ).let(otherChanges)
private fun givenBiometricKeyIsInvalidated() {
every { biometricHelper.hasSystemKey } returns true
every { biometricHelper.isSystemKeyValid } returns false
}
private fun givenShowBiometricPromptAutomatically() {
every { biometricHelper.isSystemAuthEnabledAndValid } returns true
}
} }

View File

@ -91,6 +91,11 @@ class ViewModelTest<S, VE>(
return this return this
} }
fun assertLatestState(predicate: (S) -> Boolean): ViewModelTest<S, VE> {
states.assertLatestValue(predicate)
return this
}
fun finish() { fun finish() {
states.finish() states.finish()
viewEvents.finish() viewEvents.finish()

View File

@ -47,8 +47,14 @@ class FlowTestObserver<T>(
return this return this
} }
fun assertLatestValue(value: T) { fun assertLatestValue(predicate: (T) -> Boolean): FlowTestObserver<T> {
assertTrue(values.last() == value) assertTrue(predicate(values.last()))
return this
}
fun assertLatestValue(value: T): FlowTestObserver<T> {
assertEquals(value, values.last())
return this
} }
fun assertValues(values: List<T>): FlowTestObserver<T> { fun assertValues(values: List<T>): FlowTestObserver<T> {