package com.x8bit.bitwarden import android.content.Intent import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.bitwarden.core.CipherView import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic 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.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class MainViewModelTest : BaseViewModelTest() { private val autofillSelectionManager: AutofillSelectionManager = AutofillSelectionManagerImpl() private val mutableAppThemeFlow = MutableStateFlow(AppTheme.DEFAULT) private val mutableScreenCaptureAllowedFlow = MutableStateFlow(true) private val settingsRepository = mockk { every { appTheme } returns AppTheme.DEFAULT every { appThemeStateFlow } returns mutableAppThemeFlow every { isScreenCaptureAllowedStateFlow } returns mutableScreenCaptureAllowedFlow } private val specialCircumstanceManager = SpecialCircumstanceManagerImpl() private val intentManager: IntentManager = mockk { every { getShareDataFromIntent(any()) } returns null } private val savedStateHandle = SavedStateHandle() @BeforeEach fun setup() { mockkStatic( Intent::getPasswordlessRequestDataIntentOrNull, Intent::getAutofillSaveItemOrNull, Intent::getAutofillSelectionDataOrNull, ) mockkStatic( Intent::isMyVaultShortcut, Intent::isPasswordGeneratorShortcut, ) } @AfterEach fun tearDown() { unmockkStatic( Intent::getPasswordlessRequestDataIntentOrNull, Intent::getAutofillSaveItemOrNull, Intent::getAutofillSelectionDataOrNull, ) unmockkStatic( Intent::isMyVaultShortcut, Intent::isPasswordGeneratorShortcut, ) } @Suppress("MaxLineLength") @Test fun `initialization should set a saved SpecialCircumstance to the SpecialCircumstanceManager if present`() { assertNull(specialCircumstanceManager.specialCircumstance) val specialCircumstance = mockk() createViewModel( initialSpecialCircumstance = specialCircumstance, ) assertEquals( specialCircumstance, specialCircumstanceManager.specialCircumstance, ) } @Test fun `autofill selection updates should emit CompleteAutofill events`() = runTest { val viewModel = createViewModel() val cipherView = mockk() viewModel.eventFlow.test { // Ignore initial screen capture event awaitItem() autofillSelectionManager.emitAutofillSelection(cipherView = cipherView) assertEquals( MainEvent.CompleteAutofill(cipherView = cipherView), awaitItem(), ) } } @Test fun `SpecialCircumstance updates should update the SavedStateHandle`() { createViewModel() assertNull(savedStateHandle[SPECIAL_CIRCUMSTANCE_KEY]) val specialCircumstance = mockk() specialCircumstanceManager.specialCircumstance = specialCircumstance assertEquals( specialCircumstance, savedStateHandle[SPECIAL_CIRCUMSTANCE_KEY], ) } @Test fun `on AppThemeChanged should update state`() { val viewModel = createViewModel() assertEquals( MainState( theme = AppTheme.DEFAULT, ), viewModel.stateFlow.value, ) viewModel.trySendAction( MainAction.Internal.ThemeUpdate( theme = AppTheme.DARK, ), ) assertEquals( MainState( theme = AppTheme.DARK, ), viewModel.stateFlow.value, ) verify { settingsRepository.appTheme settingsRepository.appThemeStateFlow } } @Suppress("MaxLineLength") @Test fun `on ReceiveFirstIntent with share data should set the special circumstance to ShareNewSend`() { val viewModel = createViewModel() val mockIntent = mockk() val shareData = mockk() every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getAutofillSelectionDataOrNull() } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false viewModel.trySendAction( MainAction.ReceiveFirstIntent( intent = mockIntent, ), ) assertEquals( SpecialCircumstance.ShareNewSend( data = shareData, shouldFinishWhenComplete = true, ), specialCircumstanceManager.specialCircumstance, ) } @Suppress("MaxLineLength") @Test fun `on ReceiveFirstIntent with autofill data should set the special circumstance to AutofillSelection`() { val viewModel = createViewModel() val mockIntent = mockk() val autofillSelectionData = mockk() every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getAutofillSelectionDataOrNull() } returns autofillSelectionData every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false viewModel.trySendAction( MainAction.ReceiveFirstIntent( intent = mockIntent, ), ) assertEquals( SpecialCircumstance.AutofillSelection( autofillSelectionData = autofillSelectionData, shouldFinishWhenComplete = true, ), specialCircumstanceManager.specialCircumstance, ) } @Suppress("MaxLineLength") @Test fun `on ReceiveFirstIntent with an autofill save item should set the special circumstance to AutofillSave`() { val viewModel = createViewModel() val mockIntent = mockk() val autofillSaveItem = mockk() every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns autofillSaveItem every { mockIntent.getAutofillSelectionDataOrNull() } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false viewModel.trySendAction( MainAction.ReceiveFirstIntent( intent = mockIntent, ), ) assertEquals( SpecialCircumstance.AutofillSave( autofillSaveItem = autofillSaveItem, ), specialCircumstanceManager.specialCircumstance, ) } @Suppress("MaxLineLength") @Test fun `on ReceiveFirstIntent with a passwordless request data should set the special circumstance to PasswordlessRequest`() { val viewModel = createViewModel() val mockIntent = mockk() val passwordlessRequestData = mockk() every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns passwordlessRequestData every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getAutofillSelectionDataOrNull() } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false viewModel.trySendAction( MainAction.ReceiveFirstIntent( intent = mockIntent, ), ) assertEquals( SpecialCircumstance.PasswordlessRequest( passwordlessRequestData = passwordlessRequestData, shouldFinishWhenComplete = true, ), specialCircumstanceManager.specialCircumstance, ) } @Suppress("MaxLineLength") @Test fun `on ReceiveNewIntent with share data should set the special circumstance to ShareNewSend`() { val viewModel = createViewModel() val mockIntent = mockk() val shareData = mockk() every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getAutofillSelectionDataOrNull() } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false viewModel.trySendAction( MainAction.ReceiveNewIntent( intent = mockIntent, ), ) assertEquals( SpecialCircumstance.ShareNewSend( data = shareData, shouldFinishWhenComplete = false, ), specialCircumstanceManager.specialCircumstance, ) } @Suppress("MaxLineLength") @Test fun `on ReceiveNewIntent with autofill data should set the special circumstance to AutofillSelection`() { val viewModel = createViewModel() val mockIntent = mockk() val autofillSelectionData = mockk() every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getAutofillSelectionDataOrNull() } returns autofillSelectionData every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false viewModel.trySendAction( MainAction.ReceiveNewIntent( intent = mockIntent, ), ) assertEquals( SpecialCircumstance.AutofillSelection( autofillSelectionData = autofillSelectionData, shouldFinishWhenComplete = false, ), specialCircumstanceManager.specialCircumstance, ) } @Suppress("MaxLineLength") @Test fun `on ReceiveNewIntent with an autofill save item should set the special circumstance to AutofillSave`() { val viewModel = createViewModel() val mockIntent = mockk() val autofillSaveItem = mockk() every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns autofillSaveItem every { mockIntent.getAutofillSelectionDataOrNull() } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false viewModel.trySendAction( MainAction.ReceiveNewIntent( intent = mockIntent, ), ) assertEquals( SpecialCircumstance.AutofillSave( autofillSaveItem = autofillSaveItem, ), specialCircumstanceManager.specialCircumstance, ) } @Suppress("MaxLineLength") @Test fun `on ReceiveNewIntent with a passwordless auth request data should set the special circumstance to PasswordlessRequest`() { val viewModel = createViewModel() val mockIntent = mockk() val passwordlessRequestData = mockk() every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns passwordlessRequestData every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getAutofillSelectionDataOrNull() } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false viewModel.trySendAction( MainAction.ReceiveNewIntent( intent = mockIntent, ), ) assertEquals( SpecialCircumstance.PasswordlessRequest( passwordlessRequestData = passwordlessRequestData, shouldFinishWhenComplete = false, ), specialCircumstanceManager.specialCircumstance, ) } @Suppress("MaxLineLength") @Test fun `on ReceiveNewIntent with a Vault deeplink data should set the special circumstance to VaultShortcut`() { val viewModel = createViewModel() val mockIntent = mockk { every { getPasswordlessRequestDataIntentOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null every { isMyVaultShortcut } returns true every { isPasswordGeneratorShortcut } returns false } every { intentManager.getShareDataFromIntent(mockIntent) } returns null viewModel.trySendAction( MainAction.ReceiveNewIntent( intent = mockIntent, ), ) assertEquals( SpecialCircumstance.VaultShortcut, specialCircumstanceManager.specialCircumstance, ) } @Suppress("MaxLineLength") @Test fun `on ReceiveNewIntent with a password generator deeplink data should set the special circumstance to GeneratorShortcut`() { val viewModel = createViewModel() val mockIntent = mockk { every { getPasswordlessRequestDataIntentOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null every { isMyVaultShortcut } returns false every { isPasswordGeneratorShortcut } returns true } every { intentManager.getShareDataFromIntent(mockIntent) } returns null viewModel.trySendAction( MainAction.ReceiveNewIntent( intent = mockIntent, ), ) assertEquals( SpecialCircumstance.GeneratorShortcut, specialCircumstanceManager.specialCircumstance, ) } @Suppress("MaxLineLength") @Test fun `changes in the allowed screen capture value should result in emissions of ScreenCaptureSettingChange `() = runTest { val viewModel = createViewModel() viewModel.eventFlow.test { assertEquals( MainEvent.ScreenCaptureSettingChange(isAllowed = true), awaitItem(), ) mutableScreenCaptureAllowedFlow.value = false assertEquals( MainEvent.ScreenCaptureSettingChange(isAllowed = false), awaitItem(), ) } } private fun createViewModel( initialSpecialCircumstance: SpecialCircumstance? = null, ) = MainViewModel( autofillSelectionManager = autofillSelectionManager, specialCircumstanceManager = specialCircumstanceManager, settingsRepository = settingsRepository, intentManager = intentManager, savedStateHandle = savedStateHandle.apply { set(SPECIAL_CIRCUMSTANCE_KEY, initialSpecialCircumstance) }, ) } private const val SPECIAL_CIRCUMSTANCE_KEY: String = "special-circumstance"