BIT-1069 Adding error handling for scanning (#549)

This commit is contained in:
Oleg Semenenko
2024-01-09 11:26:40 -06:00
committed by GitHub
parent 4776777759
commit f2eb46020d
9 changed files with 343 additions and 35 deletions

View File

@@ -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,
),
)

View File

@@ -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())
}
}

View File

@@ -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"
}
}