mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 10:48:47 +03:00
BIT-1294: Add autofill cipher handling (#731)
This commit is contained in:
parent
8acb748782
commit
6a66d24dd1
4 changed files with 223 additions and 79 deletions
|
@ -1,7 +1,11 @@
|
|||
package com.x8bit.bitwarden.data.autofill.provider
|
||||
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||
import com.x8bit.bitwarden.data.platform.util.takeIfUriMatches
|
||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
|
@ -26,49 +30,58 @@ class AutofillCipherProviderImpl(
|
|||
}
|
||||
|
||||
override suspend fun getCardAutofillCiphers(): List<AutofillCipher.Card> {
|
||||
// TODO: fulfill with real ciphers (BIT-1294)
|
||||
return if (isVaultLocked()) emptyList() else cardCiphers
|
||||
val cipherViews = getUnlockedCiphersOrNull() ?: return emptyList()
|
||||
|
||||
return cipherViews
|
||||
.mapNotNull { cipherView ->
|
||||
cipherView
|
||||
.takeIf { cipherView.type == CipherType.CARD }
|
||||
?.let { nonNullCipherView ->
|
||||
AutofillCipher.Card(
|
||||
name = nonNullCipherView.name,
|
||||
subtitle = nonNullCipherView.subtitle.orEmpty(),
|
||||
cardholderName = nonNullCipherView.card?.cardholderName.orEmpty(),
|
||||
code = nonNullCipherView.card?.code.orEmpty(),
|
||||
expirationMonth = nonNullCipherView.card?.expMonth.orEmpty(),
|
||||
expirationYear = nonNullCipherView.card?.expYear.orEmpty(),
|
||||
number = nonNullCipherView.card?.number.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getLoginAutofillCiphers(
|
||||
uri: String,
|
||||
): List<AutofillCipher.Login> {
|
||||
// TODO: fulfill with real ciphers (BIT-1294)
|
||||
return if (isVaultLocked()) emptyList() else loginCiphers
|
||||
}
|
||||
}
|
||||
val cipherViews = getUnlockedCiphersOrNull() ?: return emptyList()
|
||||
|
||||
private val cardCiphers = listOf(
|
||||
AutofillCipher.Card(
|
||||
cardholderName = "John",
|
||||
code = "123",
|
||||
expirationMonth = "January",
|
||||
expirationYear = "1999",
|
||||
name = "John",
|
||||
number = "1234567890",
|
||||
subtitle = "123...",
|
||||
),
|
||||
AutofillCipher.Card(
|
||||
cardholderName = "Doe",
|
||||
code = "456",
|
||||
expirationMonth = "December",
|
||||
expirationYear = "2024",
|
||||
name = "Doe",
|
||||
number = "0987654321",
|
||||
subtitle = "098...",
|
||||
),
|
||||
)
|
||||
private val loginCiphers = listOf(
|
||||
AutofillCipher.Login(
|
||||
name = "Bitwarden1",
|
||||
password = "password123",
|
||||
subtitle = "John-Bitwarden",
|
||||
username = "John-Bitwarden",
|
||||
),
|
||||
AutofillCipher.Login(
|
||||
name = "Bitwarden2",
|
||||
password = "password123",
|
||||
subtitle = "Doe-Bitwarden",
|
||||
username = "Doe-Bitwarden",
|
||||
),
|
||||
)
|
||||
return cipherViews
|
||||
.mapNotNull { cipherView ->
|
||||
cipherView
|
||||
.takeIf { cipherView.type == CipherType.LOGIN }
|
||||
// TODO: Get global URI matching value from settings repo and
|
||||
// TODO: perform more complex URI matching here (BIT-1461).
|
||||
?.takeIfUriMatches(
|
||||
uri = uri,
|
||||
)
|
||||
?.let { nonNullCipherView ->
|
||||
AutofillCipher.Login(
|
||||
name = nonNullCipherView.name,
|
||||
password = nonNullCipherView.login?.password.orEmpty(),
|
||||
subtitle = nonNullCipherView.subtitle.orEmpty(),
|
||||
username = nonNullCipherView.login?.username.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available [CipherView]s if possible.
|
||||
*/
|
||||
private suspend fun getUnlockedCiphersOrNull(): List<CipherView>? =
|
||||
vaultRepository
|
||||
.ciphersStateFlow
|
||||
.takeUnless { isVaultLocked() }
|
||||
?.first { it.data != null }
|
||||
?.data
|
||||
}
|
||||
|
|
|
@ -68,3 +68,18 @@ private val CardView.subtitleCardNumber: String?
|
|||
*/
|
||||
private val String?.isAmEx: Boolean
|
||||
get() = this?.startsWith("34") == true || this?.startsWith("37") == true
|
||||
|
||||
/**
|
||||
* Take this [CipherView] if its uri matches [uri]. Otherwise, return null.
|
||||
*/
|
||||
fun CipherView.takeIfUriMatches(
|
||||
uri: String,
|
||||
): CipherView? =
|
||||
// TODO: Pass global URI matching value from settings (BIT-1461)
|
||||
this
|
||||
.takeIf {
|
||||
// TODO: perform comprehensive URI matching (BIT-1461)
|
||||
login
|
||||
?.uris
|
||||
?.any { it.uri == uri } == true
|
||||
}
|
||||
|
|
|
@ -1,16 +1,26 @@
|
|||
package com.x8bit.bitwarden.data.autofill.processor
|
||||
|
||||
import com.bitwarden.core.CardView
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.bitwarden.core.LoginView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||
import com.x8bit.bitwarden.data.platform.util.takeIfUriMatches
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
|
@ -18,17 +28,41 @@ import org.junit.jupiter.api.BeforeEach
|
|||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AutofillCipherProviderTest {
|
||||
|
||||
private val cardView: CardView = mockk {
|
||||
every { cardholderName } returns CARD_CARDHOLDER_NAME
|
||||
every { code } returns CARD_CODE
|
||||
every { expMonth } returns CARD_EXP_MONTH
|
||||
every { expYear } returns CARD_EXP_YEAR
|
||||
every { number } returns CARD_NUMBER
|
||||
}
|
||||
private val cardCipherView: CipherView = mockk {
|
||||
every { card } returns cardView
|
||||
every { name } returns CARD_NAME
|
||||
every { type } returns CipherType.CARD
|
||||
}
|
||||
private val loginView: LoginView = mockk {
|
||||
every { password } returns LOGIN_PASSWORD
|
||||
every { username } returns LOGIN_USERNAME
|
||||
}
|
||||
private val loginCipherView: CipherView = mockk {
|
||||
every { login } returns loginView
|
||||
every { name } returns LOGIN_NAME
|
||||
every { type } returns CipherType.LOGIN
|
||||
}
|
||||
private val authRepository: AuthRepository = mockk {
|
||||
every { activeUserId } returns ACTIVE_USER_ID
|
||||
}
|
||||
private val mutableVaultStateFlow = MutableStateFlow<VaultState>(
|
||||
private val mutableVaultStateFlow = MutableStateFlow(
|
||||
VaultState(
|
||||
unlockingVaultUserIds = emptySet(),
|
||||
unlockedVaultUserIds = emptySet(),
|
||||
),
|
||||
)
|
||||
private val mutableCiphersStateFlow = MutableStateFlow<DataState<List<CipherView>>>(
|
||||
DataState.Loading,
|
||||
)
|
||||
private val vaultRepository: VaultRepository = mockk {
|
||||
every { ciphersStateFlow } returns mutableCiphersStateFlow
|
||||
every { vaultStateFlow } returns mutableVaultStateFlow
|
||||
every { isVaultUnlocked(ACTIVE_USER_ID) } answers {
|
||||
ACTIVE_USER_ID in mutableVaultStateFlow.value.unlockedVaultUserIds
|
||||
|
@ -39,12 +73,20 @@ class AutofillCipherProviderTest {
|
|||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(CipherView::takeIfUriMatches)
|
||||
mockkStatic(CipherView::subtitle)
|
||||
autofillCipherProvider = AutofillCipherProviderImpl(
|
||||
authRepository = authRepository,
|
||||
vaultRepository = vaultRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun teardown() {
|
||||
unmockkStatic(CipherView::takeIfUriMatches)
|
||||
unmockkStatic(CipherView::subtitle)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `isVaultLocked when there is no active user should return true`() =
|
||||
|
@ -89,17 +131,28 @@ class AutofillCipherProviderTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `getCardAutofillCiphers when unlocked should return default list of card ciphers`() =
|
||||
fun `getCardAutofillCiphers when unlocked should return non-null card ciphers`() =
|
||||
runTest {
|
||||
val cipherViews = listOf(
|
||||
cardCipherView,
|
||||
loginCipherView,
|
||||
)
|
||||
mutableCiphersStateFlow.value = DataState.Loaded(
|
||||
data = cipherViews,
|
||||
)
|
||||
mutableVaultStateFlow.value = VaultState(
|
||||
unlockedVaultUserIds = setOf(ACTIVE_USER_ID),
|
||||
unlockingVaultUserIds = emptySet(),
|
||||
)
|
||||
val expected = listOf(
|
||||
CARD_AUTOFILL_CIPHER,
|
||||
)
|
||||
every { cardCipherView.subtitle } returns CARD_SUBTITLE
|
||||
|
||||
// Test & Verify
|
||||
val actual = autofillCipherProvider.getCardAutofillCiphers()
|
||||
|
||||
assertEquals(CARD_CIPHERS, actual)
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -116,19 +169,36 @@ class AutofillCipherProviderTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `getLoginAutofillCiphers when unlocked should return default list of login ciphers`() =
|
||||
fun `getLoginAutofillCiphers when unlocked should return matching login ciphers`() =
|
||||
runTest {
|
||||
val unmatchedLoginCipherView: CipherView = mockk {
|
||||
every { takeIfUriMatches(URI) } returns null
|
||||
every { type } returns CipherType.LOGIN
|
||||
}
|
||||
val cipherViews = listOf(
|
||||
cardCipherView,
|
||||
loginCipherView,
|
||||
unmatchedLoginCipherView,
|
||||
)
|
||||
mutableCiphersStateFlow.value = DataState.Loaded(
|
||||
data = cipherViews,
|
||||
)
|
||||
mutableVaultStateFlow.value = VaultState(
|
||||
unlockedVaultUserIds = setOf(ACTIVE_USER_ID),
|
||||
unlockingVaultUserIds = emptySet(),
|
||||
)
|
||||
val expected = listOf(
|
||||
LOGIN_AUTOFILL_CIPHER,
|
||||
)
|
||||
every { loginCipherView.subtitle } returns LOGIN_SUBTITLE
|
||||
every { loginCipherView.takeIfUriMatches(URI) } returns loginCipherView
|
||||
|
||||
// Test & Verify
|
||||
val actual = autofillCipherProvider.getLoginAutofillCiphers(
|
||||
uri = URI,
|
||||
)
|
||||
|
||||
assertEquals(LOGIN_CIPHERS, actual)
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -148,39 +218,34 @@ class AutofillCipherProviderTest {
|
|||
}
|
||||
|
||||
private const val ACTIVE_USER_ID = "activeUserId"
|
||||
|
||||
private val CARD_CIPHERS = listOf(
|
||||
AutofillCipher.Card(
|
||||
cardholderName = "John",
|
||||
code = "123",
|
||||
expirationMonth = "January",
|
||||
expirationYear = "1999",
|
||||
name = "John",
|
||||
number = "1234567890",
|
||||
subtitle = "123...",
|
||||
),
|
||||
AutofillCipher.Card(
|
||||
cardholderName = "Doe",
|
||||
code = "456",
|
||||
expirationMonth = "December",
|
||||
expirationYear = "2024",
|
||||
name = "Doe",
|
||||
number = "0987654321",
|
||||
subtitle = "098...",
|
||||
),
|
||||
private const val CARD_CARDHOLDER_NAME = "John Doe"
|
||||
private const val CARD_CODE = "123"
|
||||
private const val CARD_EXP_MONTH = "January"
|
||||
private const val CARD_EXP_YEAR = "2029"
|
||||
private const val CARD_NAME = "John's Card"
|
||||
private const val CARD_NUMBER = "1234567890"
|
||||
private const val CARD_SUBTITLE = "7890"
|
||||
private val CARD_AUTOFILL_CIPHER = AutofillCipher.Card(
|
||||
cardholderName = CARD_CARDHOLDER_NAME,
|
||||
code = CARD_CODE,
|
||||
expirationMonth = CARD_EXP_MONTH,
|
||||
expirationYear = CARD_EXP_YEAR,
|
||||
name = CARD_NAME,
|
||||
number = CARD_NUMBER,
|
||||
subtitle = CARD_SUBTITLE,
|
||||
)
|
||||
private val LOGIN_CIPHERS = listOf(
|
||||
AutofillCipher.Login(
|
||||
name = "Bitwarden1",
|
||||
password = "password123",
|
||||
subtitle = "John-Bitwarden",
|
||||
username = "John-Bitwarden",
|
||||
),
|
||||
AutofillCipher.Login(
|
||||
name = "Bitwarden2",
|
||||
password = "password123",
|
||||
subtitle = "Doe-Bitwarden",
|
||||
username = "Doe-Bitwarden",
|
||||
),
|
||||
private const val LOGIN_NAME = "John's Login"
|
||||
private const val LOGIN_PASSWORD = "Password123"
|
||||
private const val LOGIN_SUBTITLE = "John Doe"
|
||||
private const val LOGIN_USERNAME = "John-Bitwarden"
|
||||
private val LOGIN_AUTOFILL_CIPHER = AutofillCipher.Login(
|
||||
name = LOGIN_NAME,
|
||||
password = LOGIN_PASSWORD,
|
||||
subtitle = LOGIN_SUBTITLE,
|
||||
username = LOGIN_USERNAME,
|
||||
)
|
||||
private val CIPHERS = listOf(
|
||||
CARD_AUTOFILL_CIPHER,
|
||||
LOGIN_AUTOFILL_CIPHER,
|
||||
)
|
||||
private const val URI: String = "androidapp://com.x8bit.bitwarden"
|
||||
|
|
|
@ -4,7 +4,10 @@ import com.bitwarden.core.CardView
|
|||
import com.bitwarden.core.CipherType
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.bitwarden.core.IdentityView
|
||||
import com.bitwarden.core.LoginUriView
|
||||
import com.bitwarden.core.LoginView
|
||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||
import com.x8bit.bitwarden.data.platform.util.takeIfUriMatches
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
|
@ -329,4 +332,52 @@ class CipherViewExtensionsTest {
|
|||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `takeIfUriMatches should return the CipherView if the URI matches`() {
|
||||
// Setup
|
||||
val testUri = "com.x8bit.bitwarden"
|
||||
val loginUriView: LoginUriView = mockk {
|
||||
every { uri } returns testUri
|
||||
}
|
||||
val loginView: LoginView = mockk {
|
||||
every { uris } returns listOf(loginUriView)
|
||||
}
|
||||
val expected: CipherView = mockk {
|
||||
every { login } returns loginView
|
||||
every { type } returns CipherType.IDENTITY
|
||||
}
|
||||
|
||||
// Test
|
||||
val actual = expected.takeIfUriMatches(
|
||||
uri = testUri,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `takeIfUriMatches should return null if the URI doesn't match`() {
|
||||
// Setup
|
||||
val testUri = "com.x8bit.bitwarden"
|
||||
val loginUriView: LoginUriView = mockk {
|
||||
every { uri } returns "com.google"
|
||||
}
|
||||
val loginView: LoginView = mockk {
|
||||
every { uris } returns listOf(loginUriView)
|
||||
}
|
||||
val cipherView: CipherView = mockk {
|
||||
every { login } returns loginView
|
||||
every { type } returns CipherType.IDENTITY
|
||||
}
|
||||
|
||||
// Test
|
||||
val actual = cipherView.takeIfUriMatches(
|
||||
uri = testUri,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue