mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
PM-11616: Manage totp logic in AutofillTotpManager (#3856)
This commit is contained in:
parent
2597c44117
commit
36f13e44a3
6 changed files with 263 additions and 477 deletions
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
Loading…
Reference in a new issue