mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-1069 Adding error handling for scanning (#549)
This commit is contained in:
parent
d95e5df2a7
commit
b8d397f71f
9 changed files with 343 additions and 35 deletions
|
@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
|
|||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
@ -68,7 +69,7 @@ interface VaultRepository {
|
|||
/**
|
||||
* Flow that represents the totp code.
|
||||
*/
|
||||
val totpCodeFlow: Flow<String>
|
||||
val totpCodeFlow: Flow<TotpCodeResult>
|
||||
|
||||
/**
|
||||
* Clear any previously unlocked, in-memory data (vault, send, etc).
|
||||
|
@ -108,9 +109,9 @@ interface VaultRepository {
|
|||
fun lockVaultIfNecessary(userId: String)
|
||||
|
||||
/**
|
||||
* Emits the totp code flow to listeners.
|
||||
* Emits the totp code result flow to listeners.
|
||||
*/
|
||||
fun emitTotpCode(totpCode: String)
|
||||
fun emitTotpCodeResult(totpCodeResult: TotpCodeResult)
|
||||
|
||||
/**
|
||||
* Attempt to unlock the vault and sync the vault data for the currently active user.
|
||||
|
|
|
@ -37,6 +37,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
|
|||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkSend
|
||||
|
@ -93,7 +94,7 @@ class VaultRepositoryImpl(
|
|||
|
||||
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
|
||||
|
||||
private val mutableTotpCodeFlow = bufferedMutableSharedFlow<String>()
|
||||
private val mutableTotpCodeResultFlow = bufferedMutableSharedFlow<TotpCodeResult>()
|
||||
|
||||
private val mutableVaultStateStateFlow =
|
||||
MutableStateFlow(
|
||||
|
@ -138,8 +139,8 @@ class VaultRepositoryImpl(
|
|||
initialValue = DataState.Loading,
|
||||
)
|
||||
|
||||
override val totpCodeFlow: Flow<String>
|
||||
get() = mutableTotpCodeFlow.asSharedFlow()
|
||||
override val totpCodeFlow: Flow<TotpCodeResult>
|
||||
get() = mutableTotpCodeResultFlow.asSharedFlow()
|
||||
|
||||
override val ciphersStateFlow: StateFlow<DataState<List<CipherView>>>
|
||||
get() = mutableCiphersStateFlow.asStateFlow()
|
||||
|
@ -285,8 +286,8 @@ class VaultRepositoryImpl(
|
|||
setVaultToLocked(userId = userId)
|
||||
}
|
||||
|
||||
override fun emitTotpCode(totpCode: String) {
|
||||
mutableTotpCodeFlow.tryEmit(totpCode)
|
||||
override fun emitTotpCodeResult(totpCodeResult: TotpCodeResult) {
|
||||
mutableTotpCodeResultFlow.tryEmit(totpCodeResult)
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package com.x8bit.bitwarden.data.vault.repository.model
|
||||
|
||||
/**
|
||||
* Models result of the user adding a totp code.
|
||||
*/
|
||||
sealed class TotpCodeResult {
|
||||
|
||||
/**
|
||||
* Code has been successfully added.
|
||||
*/
|
||||
data class Success(val code: String) : TotpCodeResult()
|
||||
|
||||
/**
|
||||
* There was an error scanning the code.
|
||||
*/
|
||||
data object CodeScanningError : TotpCodeResult()
|
||||
}
|
|
@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
|||
import com.x8bit.bitwarden.data.platform.repository.util.takeUntilLoaded
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
|
@ -89,7 +90,7 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
|
||||
vaultRepository
|
||||
.totpCodeFlow
|
||||
.map { VaultAddEditAction.Internal.TotpCodeReceive(totpCode = it) }
|
||||
.map { VaultAddEditAction.Internal.TotpCodeReceive(totpResult = it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
@ -843,15 +844,29 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleVaultTotpCodeReceive(action: VaultAddEditAction.Internal.TotpCodeReceive) {
|
||||
updateLoginContent { loginType ->
|
||||
loginType.copy(totp = action.totpCode)
|
||||
}
|
||||
when (action.totpResult) {
|
||||
is TotpCodeResult.Success -> {
|
||||
sendEvent(
|
||||
event = VaultAddEditEvent.ShowToast(
|
||||
message = R.string.authenticator_key_added.asText(),
|
||||
),
|
||||
)
|
||||
|
||||
sendEvent(
|
||||
event = VaultAddEditEvent.ShowToast(
|
||||
message = R.string.authenticator_key_added.asText(),
|
||||
),
|
||||
)
|
||||
updateLoginContent { loginType ->
|
||||
loginType.copy(totp = action.totpResult.code)
|
||||
}
|
||||
}
|
||||
|
||||
TotpCodeResult.CodeScanningError -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultAddEditState.DialogState.Error(
|
||||
R.string.authenticator_key_read_error.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Internal Type Handlers
|
||||
|
@ -1612,9 +1627,9 @@ sealed class VaultAddEditAction {
|
|||
sealed class Internal : VaultAddEditAction() {
|
||||
|
||||
/**
|
||||
* Indicates that the vault totp code has been received.
|
||||
* Indicates that the vault totp code result has been received.
|
||||
*/
|
||||
data class TotpCodeReceive(val totpCode: String) : Internal()
|
||||
data class TotpCodeReceive(val totpResult: TotpCodeResult) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the vault item data has been received.
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.vault.feature.manualcodeentry
|
|||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
@ -45,7 +46,7 @@ class ManualCodeEntryViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleCodeSubmit() {
|
||||
vaultRepository.emitTotpCode(state.code)
|
||||
vaultRepository.emitTotpCodeResult(TotpCodeResult.Success(state.code))
|
||||
sendEvent(ManualCodeEntryEvent.NavigateBack)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.qrcodescan
|
||||
|
||||
import android.net.Uri
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val ALGORITHM = "algorithm"
|
||||
private const val DIGITS = "digits"
|
||||
private const val PERIOD = "period"
|
||||
private const val SECRET = "secret"
|
||||
private const val TOTP_CODE_PREFIX = "otpauth://totp"
|
||||
|
||||
/**
|
||||
* Handles [QrCodeScanAction],
|
||||
* and launches [QrCodeScanEvent] for the [QrCodeScanScreen].
|
||||
|
@ -37,8 +45,31 @@ class QrCodeScanViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
// For more information: https://bitwarden.com/help/authenticator-keys/#support-for-more-parameters
|
||||
private fun handleQrCodeScanReceive(action: QrCodeScanAction.QrCodeScanReceive) {
|
||||
vaultRepository.emitTotpCode(action.qrCode)
|
||||
var result: TotpCodeResult = TotpCodeResult.Success(action.qrCode)
|
||||
val scannedCode = action.qrCode
|
||||
|
||||
if (scannedCode.isBlank() || !scannedCode.startsWith(TOTP_CODE_PREFIX)) {
|
||||
vaultRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError)
|
||||
sendEvent(QrCodeScanEvent.NavigateBack)
|
||||
return
|
||||
}
|
||||
|
||||
val scannedCodeUri = Uri.parse(scannedCode)
|
||||
val secretValue = scannedCodeUri.getQueryParameter(SECRET)
|
||||
if (secretValue == null || !secretValue.isBase32()) {
|
||||
vaultRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError)
|
||||
sendEvent(QrCodeScanEvent.NavigateBack)
|
||||
return
|
||||
}
|
||||
|
||||
val values = scannedCodeUri.queryParameterNames
|
||||
if (!areParametersValid(scannedCode, values)) {
|
||||
result = TotpCodeResult.CodeScanningError
|
||||
}
|
||||
|
||||
vaultRepository.emitTotpCodeResult(result)
|
||||
sendEvent(QrCodeScanEvent.NavigateBack)
|
||||
}
|
||||
|
||||
|
@ -47,6 +78,40 @@ class QrCodeScanViewModel @Inject constructor(
|
|||
QrCodeScanEvent.NavigateToManualCodeEntry,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("NestedBlockDepth", "ReturnCount", "MagicNumber")
|
||||
private fun areParametersValid(scannedCode: String, parameters: Set<String>): Boolean {
|
||||
parameters.forEach { parameter ->
|
||||
Uri.parse(scannedCode).getQueryParameter(parameter)?.let { value ->
|
||||
when (parameter) {
|
||||
DIGITS -> {
|
||||
val digit = value.toInt()
|
||||
if (digit > 10 || digit < 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
PERIOD -> {
|
||||
val period = value.toInt()
|
||||
if (period < 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
ALGORITHM -> {
|
||||
val lowercaseAlgo = value.lowercase()
|
||||
if (lowercaseAlgo != "sha1" &&
|
||||
lowercaseAlgo != "sha256" &&
|
||||
lowercaseAlgo != "sha512"
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,3 +160,11 @@ sealed class QrCodeScanAction {
|
|||
*/
|
||||
data object CameraSetupErrorReceive : QrCodeScanAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string is using base32 digits.
|
||||
*/
|
||||
private fun String.isBase32(): Boolean {
|
||||
val regex = ("^[A-Z2-7]+=*$").toRegex()
|
||||
return regex.matches(this)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
|||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
|
@ -50,7 +51,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
vaultAddEditType = VaultAddEditType.AddItem,
|
||||
)
|
||||
|
||||
private val totpTestCodeFlow: MutableSharedFlow<String> = bufferedMutableSharedFlow()
|
||||
private val totpTestCodeFlow: MutableSharedFlow<TotpCodeResult> = bufferedMutableSharedFlow()
|
||||
|
||||
private val mutableVaultItemFlow = MutableStateFlow<DataState<CipherView?>>(DataState.Loading)
|
||||
|
||||
|
@ -593,13 +594,13 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `TotpCodeReceive should update totp code in state`() = runTest {
|
||||
val viewModel = createAddVaultItemViewModel()
|
||||
val testKey = "TestKey"
|
||||
val result = TotpCodeResult.Success("TestKey")
|
||||
|
||||
val expectedState = loginInitialState.copy(
|
||||
viewState = VaultAddEditState.ViewState.Content(
|
||||
common = createCommonContentViewState(),
|
||||
type = createLoginTypeContentViewState(
|
||||
totpCode = testKey,
|
||||
totpCode = "TestKey",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -607,7 +608,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(
|
||||
VaultAddEditAction.Internal.TotpCodeReceive(
|
||||
testKey,
|
||||
result,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle
|
|||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
|
@ -17,10 +18,10 @@ import org.junit.jupiter.api.Test
|
|||
|
||||
class ManualCodeEntryViewModelTests : BaseViewModelTest() {
|
||||
|
||||
private val totpTestCodeFlow: Flow<String> = bufferedMutableSharedFlow()
|
||||
private val totpTestCodeFlow: Flow<TotpCodeResult> = bufferedMutableSharedFlow()
|
||||
private val vaultRepository: VaultRepository = mockk {
|
||||
every { totpCodeFlow } returns totpTestCodeFlow
|
||||
every { emitTotpCode(any()) } just runs
|
||||
every { emitTotpCodeResult(any()) } just runs
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -41,7 +42,9 @@ class ManualCodeEntryViewModelTests : BaseViewModelTest() {
|
|||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(ManualCodeEntryAction.CodeSubmit)
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCode("TestCode") }
|
||||
verify(exactly = 1) {
|
||||
vaultRepository.emitTotpCodeResult(TotpCodeResult.Success("TestCode"))
|
||||
}
|
||||
assertEquals(ManualCodeEntryEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,42 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.qrcodescan
|
||||
|
||||
import android.net.Uri
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class QrCodeScanViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val totpTestCodeFlow: Flow<String> = bufferedMutableSharedFlow()
|
||||
private val totpTestCodeFlow: Flow<TotpCodeResult> = bufferedMutableSharedFlow()
|
||||
private val vaultRepository: VaultRepository = mockk {
|
||||
every { totpCodeFlow } returns totpTestCodeFlow
|
||||
every { emitTotpCode(any()) } just runs
|
||||
every { emitTotpCodeResult(any()) } just runs
|
||||
}
|
||||
private val uriMock = mockk<Uri>()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(Uri::class)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(Uri::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -59,20 +76,199 @@ class QrCodeScanViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit new code and NavigateBack`() = runTest {
|
||||
fun `QrCodeScan should emit new code and NavigateBack with a valid code with all values`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
|
||||
val validCode =
|
||||
"otpauth://totp/Test:me?secret=JBSWY3DPEHPK3PXP&algorithm=sha256&digits=8&period=60"
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.Success(validCode)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(QrCodeScanAction.QrCodeScanReceive(validCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit new code and NavigateBack without optional values`() = runTest {
|
||||
setupMockUri(
|
||||
queryParameterNames = setOf(SECRET),
|
||||
)
|
||||
|
||||
val validCode =
|
||||
"otpauth://totp/Test:me?secret=JBSWY3DPEHPK3PXP"
|
||||
val viewModel = createViewModel()
|
||||
val code = "NewCode"
|
||||
val result = TotpCodeResult.Success(validCode)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(QrCodeScanAction.QrCodeScanReceive(code))
|
||||
viewModel.actionChannel.trySend(QrCodeScanAction.QrCodeScanReceive(validCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCode(code) }
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
fun createViewModel(): QrCodeScanViewModel =
|
||||
@Test
|
||||
fun `QrCodeScan should emit failure result and NavigateBack with invalid algorithm`() =
|
||||
runTest {
|
||||
setupMockUri(algorithm = "SHA-224")
|
||||
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.CodeScanningError
|
||||
val invalidCode =
|
||||
"otpauth://totp/Test:me?secret=JBSWY3DPEHPK3PXP&algorithm=sha224"
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(QrCodeScanAction.QrCodeScanReceive(invalidCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit failure result and NavigateBack with invalid digits`() = runTest {
|
||||
setupMockUri(digits = "11")
|
||||
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.CodeScanningError
|
||||
val invalidCode =
|
||||
"otpauth://totp/Test:me?secret=JBSWY3DPEHPK3PXP&digits=11"
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(QrCodeScanAction.QrCodeScanReceive(invalidCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit failure result and NavigateBack with invalid period`() = runTest {
|
||||
setupMockUri(period = "0")
|
||||
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.CodeScanningError
|
||||
val invalidCode =
|
||||
"otpauth://totp/Test:me?secret=JBSWY3DPEHPK3PXP&period=0"
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(QrCodeScanAction.QrCodeScanReceive(invalidCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit failure result without correct prefix`() = runTest {
|
||||
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.CodeScanningError
|
||||
val invalidCode =
|
||||
"nototpauth://totp/Test:me?secret=JBSWY3DPEHPK3PXP"
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(QrCodeScanAction.QrCodeScanReceive(invalidCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit failure result with non base32 secret`() = runTest {
|
||||
setupMockUri(secret = "JBSWY3DPEHPK3PXP1")
|
||||
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.CodeScanningError
|
||||
val invalidCode =
|
||||
"otpauth://totp/Test:me?secret=JBSWY3DPEHPK3PXP1"
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(QrCodeScanAction.QrCodeScanReceive(invalidCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit failure result and NavigateBack without Secret`() = runTest {
|
||||
setupMockUri(secret = null)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.CodeScanningError
|
||||
val invalidCode = "otpauth://totp/Test:me"
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(QrCodeScanAction.QrCodeScanReceive(invalidCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit failure result and NavigateBack if secret is empty`() = runTest {
|
||||
setupMockUri(secret = "")
|
||||
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.CodeScanningError
|
||||
val invalidCode = "otpauth://totp/Test:me?secret= "
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(QrCodeScanAction.QrCodeScanReceive(invalidCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit failure result and NavigateBack if code is empty`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.CodeScanningError
|
||||
val invalidCode = ""
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(QrCodeScanAction.QrCodeScanReceive(invalidCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMockUri(
|
||||
secret: String? = "JBSWY3DPEHPK3PXP",
|
||||
algorithm: String = "SHA256",
|
||||
digits: String = "8",
|
||||
period: String = "60",
|
||||
queryParameterNames: Set<String> = setOf(
|
||||
ALGORITHM, PERIOD, DIGITS, SECRET,
|
||||
),
|
||||
) {
|
||||
every { Uri.parse(any()) } returns uriMock
|
||||
every { uriMock.getQueryParameter(SECRET) } returns secret
|
||||
every { uriMock.getQueryParameter(ALGORITHM) } returns algorithm
|
||||
every { uriMock.getQueryParameter(DIGITS) } returns digits
|
||||
every { uriMock.getQueryParameter(PERIOD) } returns period
|
||||
every { uriMock.queryParameterNames } returns queryParameterNames
|
||||
}
|
||||
|
||||
private fun createViewModel(): QrCodeScanViewModel =
|
||||
QrCodeScanViewModel(
|
||||
vaultRepository = vaultRepository,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val ALGORITHM = "algorithm"
|
||||
private const val DIGITS = "digits"
|
||||
private const val PERIOD = "period"
|
||||
private const val SECRET = "secret"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue