PM-11616: Manage totp logic in AutofillTotpManager (#3856)

This commit is contained in:
David Perez 2024-09-03 15:54:37 -05:00 committed by GitHub
parent 2597c44117
commit 36f13e44a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 263 additions and 477 deletions

View file

@ -13,6 +13,8 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManagerImpl
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
import com.x8bit.bitwarden.data.autofill.parser.AutofillParserImpl
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
@ -32,6 +34,7 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import java.time.Clock
import javax.inject.Singleton
/**
@ -56,21 +59,15 @@ object AutofillModule {
@Provides
fun provideAutofillCompletionManager(
autofillParser: AutofillParser,
authRepository: AuthRepository,
clipboardManager: BitwardenClipboardManager,
dispatcherManager: DispatcherManager,
settingsRepository: SettingsRepository,
vaultRepository: VaultRepository,
organizationEventManager: OrganizationEventManager,
totpManager: AutofillTotpManager,
): AutofillCompletionManager =
AutofillCompletionManagerImpl(
authRepository = authRepository,
autofillParser = autofillParser,
clipboardManager = clipboardManager,
dispatcherManager = dispatcherManager,
settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
organizationEventManager = organizationEventManager,
totpManager = totpManager,
)
@Singleton
@ -82,6 +79,25 @@ object AutofillModule {
settingsRepository = settingsRepository,
)
@Singleton
@Provides
fun providesAutofillTotpManager(
@ApplicationContext context: Context,
clock: Clock,
clipboardManager: BitwardenClipboardManager,
authRepository: AuthRepository,
settingsRepository: SettingsRepository,
vaultRepository: VaultRepository,
): AutofillTotpManager =
AutofillTotpManagerImpl(
context = context,
clock = clock,
clipboardManager = clipboardManager,
authRepository = authRepository,
settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
)
@Singleton
@Provides
fun providesAutofillCipherProvider(

View file

@ -2,11 +2,7 @@ package com.x8bit.bitwarden.data.autofill.manager
import android.app.Activity
import android.content.Intent
import android.widget.Toast
import com.bitwarden.core.DateTime
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilder
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilderImpl
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
@ -16,13 +12,9 @@ import com.x8bit.bitwarden.data.autofill.util.createAutofillSelectionResultInten
import com.x8bit.bitwarden.data.autofill.util.getAutofillAssistStructureOrNull
import com.x8bit.bitwarden.data.autofill.util.toAutofillAppInfo
import com.x8bit.bitwarden.data.autofill.util.toAutofillCipherProvider
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -31,15 +23,12 @@ import kotlinx.coroutines.launch
*/
@Suppress("LongParameterList")
class AutofillCompletionManagerImpl(
private val authRepository: AuthRepository,
private val autofillParser: AutofillParser,
private val clipboardManager: BitwardenClipboardManager,
dispatcherManager: DispatcherManager,
private val filledDataBuilderProvider: (CipherView) -> FilledDataBuilder =
{ createSingleItemFilledDataBuilder(cipherView = it) },
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
private val organizationEventManager: OrganizationEventManager,
private val totpManager: AutofillTotpManager,
) : AutofillCompletionManager {
private val mainScope = CoroutineScope(dispatcherManager.main)
@ -82,10 +71,7 @@ class AutofillCompletionManagerImpl(
activity.cancelAndFinish()
return@launch
}
tryCopyTotpToClipboard(
activity = activity,
cipherView = cipherView,
)
totpManager.tryCopyTotpToClipboard(cipherView = cipherView)
val resultIntent = createAutofillSelectionResultIntent(dataset)
activity.setResultAndFinish(resultIntent = resultIntent)
cipherView.id?.let {
@ -95,40 +81,6 @@ class AutofillCompletionManagerImpl(
}
}
}
/**
* Attempt to copy the totp code to clipboard. If it succeeds show a toast.
*
* @param activity An activity for launching a toast.
* @param cipherView The [CipherView] for which to generate a TOTP code.
*/
private suspend fun tryCopyTotpToClipboard(
activity: Activity,
cipherView: CipherView,
) {
val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true
val totpAvailableViaPremiumOrOrganization = isPremium || cipherView.organizationUseTotp
val totpCode = cipherView.login?.totp
val isTotpDisabled = settingsRepository.isAutoCopyTotpDisabled
if (!isTotpDisabled && totpAvailableViaPremiumOrOrganization && totpCode != null) {
val totpResult = vaultRepository.generateTotp(
time = DateTime.now(),
totpCode = totpCode,
)
if (totpResult is GenerateTotpResult.Success) {
clipboardManager.setText(totpResult.code)
Toast
.makeText(
activity.applicationContext,
R.string.verification_code_totp,
Toast.LENGTH_LONG,
)
.show()
}
}
}
}
private fun createSingleItemFilledDataBuilder(

View file

@ -0,0 +1,13 @@
package com.x8bit.bitwarden.data.autofill.manager
import com.bitwarden.vault.CipherView
/**
* Manages copying the totp code to the clipboard for autofill.
*/
interface AutofillTotpManager {
/**
* Attempt to copy the totp code to clipboard. If it succeeds show a toast.
*/
suspend fun tryCopyTotpToClipboard(cipherView: CipherView)
}

View file

@ -0,0 +1,47 @@
package com.x8bit.bitwarden.data.autofill.manager
import android.content.Context
import android.widget.Toast
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import java.time.Clock
/**
* Default implementation of the [AutofillTotpManager].
*/
class AutofillTotpManagerImpl(
private val context: Context,
private val clock: Clock,
private val clipboardManager: BitwardenClipboardManager,
private val authRepository: AuthRepository,
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
) : AutofillTotpManager {
override suspend fun tryCopyTotpToClipboard(cipherView: CipherView) {
if (settingsRepository.isAutoCopyTotpDisabled) return
val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true
if (!isPremium && !cipherView.organizationUseTotp) return
val totpCode = cipherView.login?.totp ?: return
val totpResult = vaultRepository.generateTotp(
time = clock.instant(),
totpCode = totpCode,
)
if (totpResult is GenerateTotpResult.Success) {
clipboardManager.setText(text = totpResult.code)
Toast
.makeText(
context.applicationContext,
R.string.verification_code_totp,
Toast.LENGTH_LONG,
)
.show()
}
}
}

View file

@ -5,11 +5,7 @@ import android.app.assist.AssistStructure
import android.content.Context
import android.content.Intent
import android.service.autofill.Dataset
import android.widget.Toast
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilder
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
@ -21,12 +17,8 @@ import com.x8bit.bitwarden.data.autofill.util.createAutofillSelectionResultInten
import com.x8bit.bitwarden.data.autofill.util.getAutofillAssistStructureOrNull
import com.x8bit.bitwarden.data.autofill.util.toAutofillAppInfo
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
@ -36,7 +28,6 @@ import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@ -50,44 +41,32 @@ class AutofillCompletionManagerTest {
every { setResult(any(), any()) } just runs
}
private val assistStructure: AssistStructure = mockk()
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
private val authRepository: AuthRepository = mockk {
every { userStateFlow } returns mutableUserStateFlow
}
private val autofillAppInfo: AutofillAppInfo = mockk()
private val autofillParser: AutofillParser = mockk()
private val cipherView: CipherView = mockk {
every { id } returns "cipherId"
}
private val clipboardManager: BitwardenClipboardManager = mockk {
every { setText(any<String>()) } just runs
}
private val dataset: Dataset = mockk()
private val dispatcherManager = FakeDispatcherManager()
private val fillableRequest: AutofillRequest.Fillable = mockk()
private val filledDataBuilder: FilledDataBuilder = mockk()
private val filledPartition: FilledPartition = mockk()
private val mockIntent: Intent = mockk()
private val settingsRepository: SettingsRepository = mockk()
private val resultIntent: Intent = mockk()
private val toast: Toast = mockk {
every { show() } just runs
}
private val vaultRepository: VaultRepository = mockk()
private val organizationEventManager = mockk<OrganizationEventManager> {
every { trackEvent(event = any()) } just runs
}
private val totpManager: AutofillTotpManager = mockk {
coEvery { tryCopyTotpToClipboard(cipherView = cipherView) } just runs
}
private val autofillCompletionManager: AutofillCompletionManager =
AutofillCompletionManagerImpl(
authRepository = authRepository,
autofillParser = autofillParser,
clipboardManager = clipboardManager,
dispatcherManager = dispatcherManager,
filledDataBuilderProvider = { filledDataBuilder },
settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
organizationEventManager = organizationEventManager,
totpManager = totpManager,
)
@BeforeEach
@ -97,7 +76,6 @@ class AutofillCompletionManagerTest {
mockkStatic(Activity::toAutofillAppInfo)
mockkStatic(FilledPartition::buildDataset)
mockkStatic(Intent::getAutofillAssistStructureOrNull)
mockkStatic(Toast::class)
every { activity.toAutofillAppInfo() } returns autofillAppInfo
}
@ -108,10 +86,8 @@ class AutofillCompletionManagerTest {
unmockkStatic(Activity::toAutofillAppInfo)
unmockkStatic(FilledPartition::buildDataset)
unmockkStatic(Intent::getAutofillAssistStructureOrNull)
unmockkStatic(Toast::class)
}
@Suppress("MaxLineLength")
@Test
fun `completeAutofill when there is no Intent present should cancel and finish the Activity`() {
every { activity.intent } returns null
@ -124,8 +100,6 @@ class AutofillCompletionManagerTest {
verify {
activity.setResult(Activity.RESULT_CANCELED)
activity.finish()
}
verify {
activity.intent
}
}
@ -144,8 +118,6 @@ class AutofillCompletionManagerTest {
verify {
activity.setResult(Activity.RESULT_CANCELED)
activity.finish()
}
verify {
activity.intent
mockIntent.getAutofillAssistStructureOrNull()
}
@ -171,8 +143,6 @@ class AutofillCompletionManagerTest {
verify {
activity.setResult(Activity.RESULT_CANCELED)
activity.finish()
}
verify {
activity.intent
mockIntent.getAutofillAssistStructureOrNull()
autofillParser.parse(
@ -208,8 +178,6 @@ class AutofillCompletionManagerTest {
verify {
activity.setResult(Activity.RESULT_CANCELED)
activity.finish()
}
verify {
activity.intent
mockIntent.getAutofillAssistStructureOrNull()
autofillParser.parse(
@ -224,182 +192,7 @@ class AutofillCompletionManagerTest {
@Suppress("MaxLineLength")
@Test
fun `completeAutofill when filled partition, premium active user, a totp code, and totp generated successfully should build a dataset, place it in a result Intent, copy totp, and finish the Activity`() {
val filledData: FilledData = mockk {
every { filledPartitions } returns listOf(filledPartition)
}
val generateTotpResult = GenerateTotpResult.Success(
code = TOTP_RESULT_VALUE,
periodSeconds = 100,
)
every { activity.intent } returns mockIntent
every { mockIntent.getAutofillAssistStructureOrNull() } returns assistStructure
every {
autofillParser.parse(
autofillAppInfo = autofillAppInfo,
assistStructure = assistStructure,
)
} returns fillableRequest
every { cipherView.login?.totp } returns TOTP_CODE
coEvery {
filledDataBuilder.build(autofillRequest = fillableRequest)
} returns filledData
every {
filledPartition.buildDataset(
authIntentSender = null,
autofillAppInfo = autofillAppInfo,
)
} returns dataset
every { settingsRepository.isAutoCopyTotpDisabled } returns false
every { createAutofillSelectionResultIntent(dataset = dataset) } returns resultIntent
coEvery {
vaultRepository.generateTotp(
time = any(),
totpCode = TOTP_CODE,
)
} returns generateTotpResult
every {
Toast.makeText(
context,
R.string.verification_code_totp,
Toast.LENGTH_LONG,
)
} returns toast
mutableUserStateFlow.value = mockk {
every { activeAccount.isPremium } returns true
}
autofillCompletionManager.completeAutofill(
activity = activity,
cipherView = cipherView,
)
verify {
activity.setResult(Activity.RESULT_OK, resultIntent)
activity.finish()
activity.intent
clipboardManager.setText(any<String>())
mockIntent.getAutofillAssistStructureOrNull()
autofillParser.parse(
autofillAppInfo = autofillAppInfo,
assistStructure = assistStructure,
)
filledPartition.buildDataset(
authIntentSender = null,
autofillAppInfo = autofillAppInfo,
)
settingsRepository.isAutoCopyTotpDisabled
createAutofillSelectionResultIntent(dataset = dataset)
Toast.makeText(
context,
R.string.verification_code_totp,
Toast.LENGTH_LONG,
)
toast.show()
organizationEventManager.trackEvent(
event = OrganizationEvent.CipherClientAutoFilled(cipherId = "cipherId"),
)
}
coVerify {
filledDataBuilder.build(autofillRequest = fillableRequest)
vaultRepository.generateTotp(
time = any(),
totpCode = TOTP_CODE,
)
}
}
@Suppress("MaxLineLength")
@Test
fun `completeAutofill when filled partition, organization uses totp, a totp code, and totp generated successfully should build a dataset, place it in a result Intent, copy totp, and finish the Activity`() {
val filledData: FilledData = mockk {
every { filledPartitions } returns listOf(filledPartition)
}
val generateTotpResult = GenerateTotpResult.Success(
code = TOTP_RESULT_VALUE,
periodSeconds = 100,
)
every { activity.intent } returns mockIntent
every { mockIntent.getAutofillAssistStructureOrNull() } returns assistStructure
every {
autofillParser.parse(
autofillAppInfo = autofillAppInfo,
assistStructure = assistStructure,
)
} returns fillableRequest
every { cipherView.login?.totp } returns TOTP_CODE
every { cipherView.organizationUseTotp } returns true
mutableUserStateFlow.value = mockk {
every { activeAccount.isPremium } returns false
}
coEvery {
filledDataBuilder.build(autofillRequest = fillableRequest)
} returns filledData
every {
filledPartition.buildDataset(
authIntentSender = null,
autofillAppInfo = autofillAppInfo,
)
} returns dataset
every { settingsRepository.isAutoCopyTotpDisabled } returns false
every { createAutofillSelectionResultIntent(dataset = dataset) } returns resultIntent
coEvery {
vaultRepository.generateTotp(
time = any(),
totpCode = TOTP_CODE,
)
} returns generateTotpResult
every {
Toast.makeText(
context,
R.string.verification_code_totp,
Toast.LENGTH_LONG,
)
} returns toast
autofillCompletionManager.completeAutofill(
activity = activity,
cipherView = cipherView,
)
verify {
activity.setResult(Activity.RESULT_OK, resultIntent)
activity.finish()
activity.intent
clipboardManager.setText(any<String>())
mockIntent.getAutofillAssistStructureOrNull()
autofillParser.parse(
autofillAppInfo = autofillAppInfo,
assistStructure = assistStructure,
)
filledPartition.buildDataset(
authIntentSender = null,
autofillAppInfo = autofillAppInfo,
)
settingsRepository.isAutoCopyTotpDisabled
createAutofillSelectionResultIntent(dataset = dataset)
Toast.makeText(
context,
R.string.verification_code_totp,
Toast.LENGTH_LONG,
)
toast.show()
organizationEventManager.trackEvent(
event = OrganizationEvent.CipherClientAutoFilled(cipherId = "cipherId"),
)
}
coVerify {
filledDataBuilder.build(autofillRequest = fillableRequest)
vaultRepository.generateTotp(
time = any(),
totpCode = TOTP_CODE,
)
}
}
@Suppress("MaxLineLength")
@Test
fun `completeAutofill when filled partition, premium active user, a totp code, and totp generated unsuccessfully should build a dataset, place it in a result Intent, and finish the Activity`() {
fun `completeAutofill when filled partition and totp generated successfully should attempt to copy totp to clipboard, build a dataset, place it in a result Intent and finish the Activity`() {
val filledData: FilledData = mockk {
every { filledPartitions } returns listOf(filledPartition)
}
@ -411,7 +204,6 @@ class AutofillCompletionManagerTest {
assistStructure = assistStructure,
)
} returns fillableRequest
every { cipherView.login?.totp } returns TOTP_CODE
coEvery {
filledDataBuilder.build(autofillRequest = fillableRequest)
} returns filledData
@ -421,17 +213,7 @@ class AutofillCompletionManagerTest {
autofillAppInfo = autofillAppInfo,
)
} returns dataset
every { settingsRepository.isAutoCopyTotpDisabled } returns false
every { createAutofillSelectionResultIntent(dataset = dataset) } returns resultIntent
coEvery {
vaultRepository.generateTotp(
time = any(),
totpCode = TOTP_CODE,
)
} returns GenerateTotpResult.Error
mutableUserStateFlow.value = mockk {
every { activeAccount.isPremium } returns true
}
autofillCompletionManager.completeAutofill(
activity = activity,
@ -441,8 +223,6 @@ class AutofillCompletionManagerTest {
verify {
activity.setResult(Activity.RESULT_OK, resultIntent)
activity.finish()
}
verify {
activity.intent
mockIntent.getAutofillAssistStructureOrNull()
autofillParser.parse(
@ -453,197 +233,6 @@ class AutofillCompletionManagerTest {
authIntentSender = null,
autofillAppInfo = autofillAppInfo,
)
settingsRepository.isAutoCopyTotpDisabled
createAutofillSelectionResultIntent(dataset = dataset)
organizationEventManager.trackEvent(
event = OrganizationEvent.CipherClientAutoFilled(cipherId = "cipherId"),
)
}
coVerify {
filledDataBuilder.build(autofillRequest = fillableRequest)
vaultRepository.generateTotp(
time = any(),
totpCode = TOTP_CODE,
)
}
}
@Suppress("MaxLineLength")
@Test
fun `completeAutofill when filled partition, premium active user, and no totp code should build a dataset, place it in a result Intent, and finish the Activity`() {
val filledData: FilledData = mockk {
every { filledPartitions } returns listOf(filledPartition)
}
every { activity.intent } returns mockIntent
every { mockIntent.getAutofillAssistStructureOrNull() } returns assistStructure
every {
autofillParser.parse(
autofillAppInfo = autofillAppInfo,
assistStructure = assistStructure,
)
} returns fillableRequest
every { cipherView.login?.totp } returns null
coEvery {
filledDataBuilder.build(autofillRequest = fillableRequest)
} returns filledData
every {
filledPartition.buildDataset(
authIntentSender = null,
autofillAppInfo = autofillAppInfo,
)
} returns dataset
every { settingsRepository.isAutoCopyTotpDisabled } returns false
every { createAutofillSelectionResultIntent(dataset = dataset) } returns resultIntent
mutableUserStateFlow.value = mockk {
every { activeAccount.isPremium } returns true
}
autofillCompletionManager.completeAutofill(
activity = activity,
cipherView = cipherView,
)
verify {
activity.setResult(Activity.RESULT_OK, resultIntent)
activity.finish()
}
verify {
settingsRepository.isAutoCopyTotpDisabled
activity.intent
mockIntent.getAutofillAssistStructureOrNull()
autofillParser.parse(
autofillAppInfo = autofillAppInfo,
assistStructure = assistStructure,
)
filledPartition.buildDataset(
authIntentSender = null,
autofillAppInfo = autofillAppInfo,
)
settingsRepository.isAutoCopyTotpDisabled
createAutofillSelectionResultIntent(dataset = dataset)
organizationEventManager.trackEvent(
event = OrganizationEvent.CipherClientAutoFilled(cipherId = "cipherId"),
)
}
coVerify {
filledDataBuilder.build(autofillRequest = fillableRequest)
}
}
@Suppress("MaxLineLength")
@Test
fun `completeAutofill when filled partition, no premium active user, organization does not use totp, and totp code should build a dataset, place it in a result Intent, and finish the Activity`() {
val filledData: FilledData = mockk {
every { filledPartitions } returns listOf(filledPartition)
}
every { activity.intent } returns mockIntent
every { mockIntent.getAutofillAssistStructureOrNull() } returns assistStructure
every {
autofillParser.parse(
autofillAppInfo = autofillAppInfo,
assistStructure = assistStructure,
)
} returns fillableRequest
every { cipherView.login?.totp } returns TOTP_CODE
every { cipherView.organizationUseTotp } returns false
coEvery {
filledDataBuilder.build(autofillRequest = fillableRequest)
} returns filledData
every {
filledPartition.buildDataset(
authIntentSender = null,
autofillAppInfo = autofillAppInfo,
)
} returns dataset
every { settingsRepository.isAutoCopyTotpDisabled } returns false
every { createAutofillSelectionResultIntent(dataset = dataset) } returns resultIntent
mutableUserStateFlow.value = mockk {
every { activeAccount.isPremium } returns false
}
autofillCompletionManager.completeAutofill(
activity = activity,
cipherView = cipherView,
)
verify {
activity.setResult(Activity.RESULT_OK, resultIntent)
activity.finish()
}
verify {
activity.intent
mockIntent.getAutofillAssistStructureOrNull()
autofillParser.parse(
autofillAppInfo = autofillAppInfo,
assistStructure = assistStructure,
)
filledPartition.buildDataset(
authIntentSender = null,
autofillAppInfo = autofillAppInfo,
)
cipherView.organizationUseTotp
settingsRepository.isAutoCopyTotpDisabled
createAutofillSelectionResultIntent(dataset = dataset)
organizationEventManager.trackEvent(
event = OrganizationEvent.CipherClientAutoFilled(cipherId = "cipherId"),
)
}
coVerify {
filledDataBuilder.build(autofillRequest = fillableRequest)
}
}
@Suppress("MaxLineLength")
@Test
fun `completeAutofill when filled partition and totp copy disabled should build a dataset, place it in a result Intent, and finish the Activity`() {
val filledData: FilledData = mockk {
every { filledPartitions } returns listOf(filledPartition)
}
every { activity.intent } returns mockIntent
every { mockIntent.getAutofillAssistStructureOrNull() } returns assistStructure
every {
autofillParser.parse(
autofillAppInfo = autofillAppInfo,
assistStructure = assistStructure,
)
} returns fillableRequest
every { cipherView.login?.totp } returns TOTP_CODE
coEvery {
filledDataBuilder.build(autofillRequest = fillableRequest)
} returns filledData
every {
filledPartition.buildDataset(
authIntentSender = null,
autofillAppInfo = autofillAppInfo,
)
} returns dataset
every { settingsRepository.isAutoCopyTotpDisabled } returns true
every { createAutofillSelectionResultIntent(dataset = dataset) } returns resultIntent
mutableUserStateFlow.value = mockk {
every { activeAccount.isPremium } returns true
}
autofillCompletionManager.completeAutofill(
activity = activity,
cipherView = cipherView,
)
verify {
activity.setResult(Activity.RESULT_OK, resultIntent)
activity.finish()
}
verify {
activity.intent
mockIntent.getAutofillAssistStructureOrNull()
autofillParser.parse(
autofillAppInfo = autofillAppInfo,
assistStructure = assistStructure,
)
filledPartition.buildDataset(
authIntentSender = null,
autofillAppInfo = autofillAppInfo,
)
settingsRepository.isAutoCopyTotpDisabled
createAutofillSelectionResultIntent(dataset = dataset)
organizationEventManager.trackEvent(
event = OrganizationEvent.CipherClientAutoFilled(cipherId = "cipherId"),
@ -651,9 +240,7 @@ class AutofillCompletionManagerTest {
}
coVerify {
filledDataBuilder.build(autofillRequest = fillableRequest)
totpManager.tryCopyTotpToClipboard(cipherView = cipherView)
}
}
}
private const val TOTP_CODE: String = "TOTP_CODE"
private const val TOTP_RESULT_VALUE: String = "TOTP_RESULT_VALUE"

View file

@ -0,0 +1,171 @@
package com.x8bit.bitwarden.data.autofill.manager
import android.content.Context
import android.widget.Toast
import com.bitwarden.vault.CipherView
import com.bitwarden.vault.LoginView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import io.mockk.coEvery
import io.mockk.coVerify
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.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
class AutofillTotpManagerTest {
private val context: Context = mockk {
every { applicationContext } returns this
}
private val toast: Toast = mockk {
every { show() } just runs
}
private val loginView: LoginView = mockk()
private val cipherView: CipherView = mockk {
every { id } returns "cipherId"
every { login } returns loginView
}
private val mutableUserStateFlow = MutableStateFlow<UserState?>(value = null)
private val authRepository: AuthRepository = mockk {
every { userStateFlow } returns mutableUserStateFlow
}
private val clipboardManager: BitwardenClipboardManager = mockk {
every { setText(any<String>()) } just runs
}
private val settingsRepository: SettingsRepository = mockk()
private val vaultRepository: VaultRepository = mockk()
private val autofillTotpManager: AutofillTotpManager = AutofillTotpManagerImpl(
context = context,
clock = FIXED_CLOCK,
clipboardManager = clipboardManager,
authRepository = authRepository,
settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
)
@BeforeEach
fun setup() {
mockkStatic(Toast::class)
every {
Toast.makeText(context, R.string.verification_code_totp, Toast.LENGTH_LONG)
} returns toast
}
@AfterEach
fun tearDown() {
unmockkStatic(Toast::class)
}
@Test
fun `tryCopyTotpToClipboard when isAutoCopyTotpDisabled is true should do nothing`() = runTest {
every { settingsRepository.isAutoCopyTotpDisabled } returns true
autofillTotpManager.tryCopyTotpToClipboard(cipherView = cipherView)
verify(exactly = 1) {
settingsRepository.isAutoCopyTotpDisabled
}
verify(exactly = 0) {
clipboardManager.setText(any<String>())
toast.show()
}
}
@Suppress("MaxLineLength")
@Test
fun `tryCopyTotpToClipboard when isAutoCopyTotpDisabled is false, user not premium and the organization does not use totp should do nothing`() =
runTest {
every { settingsRepository.isAutoCopyTotpDisabled } returns false
every { cipherView.organizationUseTotp } returns false
mutableUserStateFlow.value = mockk {
every { activeAccount.isPremium } returns false
}
autofillTotpManager.tryCopyTotpToClipboard(cipherView = cipherView)
verify(exactly = 0) {
clipboardManager.setText(any<String>())
toast.show()
}
verify(exactly = 1) {
settingsRepository.isAutoCopyTotpDisabled
}
}
@Suppress("MaxLineLength")
@Test
fun `tryCopyTotpToClipboard when isAutoCopyTotpDisabled is false, has premium and does not have totp should do nothing`() =
runTest {
every { settingsRepository.isAutoCopyTotpDisabled } returns false
every { cipherView.organizationUseTotp } returns true
mutableUserStateFlow.value = mockk {
every { activeAccount.isPremium } returns true
}
every { loginView.totp } returns null
autofillTotpManager.tryCopyTotpToClipboard(cipherView = cipherView)
verify(exactly = 0) {
clipboardManager.setText(any<String>())
toast.show()
}
verify(exactly = 1) {
settingsRepository.isAutoCopyTotpDisabled
}
}
@Suppress("MaxLineLength")
@Test
fun `tryCopyTotpToClipboard when isAutoCopyTotpDisabled is false, has premium and has totp should set the clipboard and toast`() =
runTest {
val generateTotpResult = GenerateTotpResult.Success(
code = TOTP_RESULT_VALUE,
periodSeconds = 100,
)
every { settingsRepository.isAutoCopyTotpDisabled } returns false
every { cipherView.organizationUseTotp } returns true
mutableUserStateFlow.value = mockk {
every { activeAccount.isPremium } returns true
}
every { loginView.totp } returns TOTP_CODE
coEvery {
vaultRepository.generateTotp(time = FIXED_CLOCK.instant(), totpCode = TOTP_CODE)
} returns generateTotpResult
autofillTotpManager.tryCopyTotpToClipboard(cipherView = cipherView)
verify(exactly = 1) {
clipboardManager.setText(text = TOTP_RESULT_VALUE)
settingsRepository.isAutoCopyTotpDisabled
toast.show()
}
coVerify(exactly = 1) {
vaultRepository.generateTotp(time = FIXED_CLOCK.instant(), totpCode = TOTP_CODE)
}
}
}
private val FIXED_CLOCK: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
private const val TOTP_CODE: String = "TOTP_CODE"
private const val TOTP_RESULT_VALUE: String = "TOTP_RESULT_VALUE"