mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-18 04:50:08 +03:00
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:
parent
fcc7bbadfa
commit
aecf460c96
5 changed files with 155 additions and 101 deletions
|
@ -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")
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
Loading…
Add table
Reference in a new issue