Compare commits

..

2 Commits

Author SHA1 Message Date
David Perez
f43d2fad4c Create a Overlay Navigation Screen 2026-06-09 17:27:17 -05:00
David Perez
2c7eef2b8c PM-38637: Bug: Add additional CRL distrobution server to clear-text permitted list (#7045) 2026-06-09 20:36:41 +00:00
13 changed files with 377 additions and 203 deletions

View File

@@ -18,6 +18,7 @@
<!-- CRL Distribution Servers -->
<domain includeSubdomains="true">c.lencr.org</domain>
<domain includeSubdomains="true">c.pki.goog</domain>
<domain includeSubdomains="true">crls.certainly.com</domain>
<!-- OCSP Responder Servers -->
<domain includeSubdomains="true">o.pki.goog</domain>

View File

@@ -36,17 +36,11 @@ import com.x8bit.bitwarden.data.platform.manager.util.ObserveScreenDataEffect
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.components.util.rememberBitwardenNavController
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
import com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure.accessibilityDisclosureDestination
import com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure.navigateToAccessibilityDisclosure
import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.cookieAcquisitionDestination
import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.navigateToCookieAcquisition
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.debugMenuDestination
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
import com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.localNetworkAccessDestination
import com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.navigateToLocalNetworkAccess
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavigationRoute
import com.x8bit.bitwarden.ui.platform.feature.rootnav.rootNavDestination
import com.x8bit.bitwarden.ui.platform.feature.overlaynav.OverlayNavRoute
import com.x8bit.bitwarden.ui.platform.feature.overlaynav.overlayNavDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
import com.x8bit.bitwarden.ui.platform.model.AuthTabLaunchers
import com.x8bit.bitwarden.ui.platform.util.appLanguage
@@ -112,7 +106,6 @@ class MainActivity : AppCompatActivity() {
)
}
@Suppress("LongMethod")
override fun onCreate(savedInstanceState: Bundle?) {
intent = intent.validate()
var shouldShowSplashScreen = true
@@ -148,31 +141,17 @@ class MainActivity : AppCompatActivity() {
) {
NavHost(
navController = navController,
startDestination = RootNavigationRoute,
startDestination = OverlayNavRoute,
modifier = Modifier
.background(color = BitwardenTheme.colorScheme.background.primary),
) {
// Root navigation, debug menu, and cookie acquisition exist at
// this top level. They can appear on top of the rest of the app
// without interacting with the state-based navigation used by
// RootNavScreen.
rootNavDestination { shouldShowSplashScreen = false }
// The OverlayNav and Debug destinations are the only UIs that can be
// displayed here, everything else should be inside the OverlayNav.
overlayNavDestination { shouldShowSplashScreen = false }
debugMenuDestination(
onNavigateBack = { navController.popBackStack() },
onSplashScreenRemoved = { shouldShowSplashScreen = false },
)
cookieAcquisitionDestination(
onDismiss = { navController.popBackStack() },
onSplashScreenRemoved = { shouldShowSplashScreen = false },
)
localNetworkAccessDestination(
onDismiss = { navController.popBackStack() },
onSplashScreenRemoved = { shouldShowSplashScreen = false },
)
accessibilityDisclosureDestination(
onDismiss = { navController.popBackStack() },
onSplashScreenRemoved = { shouldShowSplashScreen = false },
)
}
}
}
@@ -207,7 +186,7 @@ class MainActivity : AppCompatActivity() {
locales.get(0)?.appLanguage
}
} else {
// For older versions, use what ever language is available from the repository.
// For older versions, use whatever language is available from the repository.
settingsRepository.appLanguage
}
@@ -256,15 +235,6 @@ class MainActivity : AppCompatActivity() {
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
MainEvent.Recreate -> handleRecreate()
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
MainEvent.NavigateToCookieAcquisition -> navController.navigateToCookieAcquisition()
MainEvent.NavigateToLocalNetworkAccess -> {
navController.navigateToLocalNetworkAccess()
}
MainEvent.NavigateToAccessibilityDisclosure -> {
navController.navigateToAccessibilityDisclosure()
}
is MainEvent.UpdateAppLocale -> {
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.forLanguageTags(event.localeName),

View File

@@ -31,13 +31,11 @@ import com.x8bit.bitwarden.data.billing.util.getPremiumCheckoutCallbackResult
import com.x8bit.bitwarden.data.credentials.manager.CredentialProviderRequestManager
import com.x8bit.bitwarden.data.credentials.manager.model.CredentialProviderRequest
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
import com.x8bit.bitwarden.data.platform.manager.CookieAcquisitionRequestManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.manager.network.NetworkPermissionManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticator
@@ -57,7 +55,6 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@@ -81,8 +78,6 @@ private const val ANIMATION_DEBOUNCE_DELAY_MS = 500L
class MainViewModel @Inject constructor(
accessibilitySelectionManager: AccessibilitySelectionManager,
autofillSelectionManager: AutofillSelectionManager,
cookieAcquisitionRequestManager: CookieAcquisitionRequestManager,
networkPermissionManager: NetworkPermissionManager,
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
private val specialCircumstanceManager: SpecialCircumstanceManager,
private val garbageCollectionManager: GarbageCollectionManager,
@@ -152,12 +147,6 @@ class MainViewModel @Inject constructor(
.onEach(::trySendAction)
.launchIn(viewModelScope)
settingsRepository
.hasShownAccessibilityDisclaimerFlow
.map { MainAction.Internal.HasShownAccessibilityDisclaimerUpdate(it) }
.onEach(::trySendAction)
.launchIn(viewModelScope)
merge(
authRepository
.userStateFlow
@@ -177,20 +166,6 @@ class MainViewModel @Inject constructor(
.onEach(::sendAction)
.launchIn(viewModelScope)
networkPermissionManager
.isLocalNetworkAccessRequiredStateFlow
.filter { it }
.map { MainAction.Internal.LocalNetworkAccessRequired }
.onEach(::sendAction)
.launchIn(viewModelScope)
cookieAcquisitionRequestManager
.cookieAcquisitionRequestFlow
.filterNotNull()
.map { MainAction.Internal.CookieAcquisitionReady }
.onEach(::sendAction)
.launchIn(viewModelScope)
// On app launch, mark all active users as having previously logged in.
// This covers any users who are active prior to this value being recorded.
viewModelScope.launch {
@@ -239,20 +214,7 @@ class MainViewModel @Inject constructor(
is MainAction.Internal.ScreenCaptureUpdate -> handleScreenCaptureUpdate(action)
is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
is MainAction.Internal.DynamicColorsUpdate -> handleDynamicColorsUpdate(action)
is MainAction.Internal.CookieAcquisitionReady -> handleCookieAcquisitionReady()
is MainAction.Internal.LocalNetworkAccessRequired -> handleLocalNetworkAccessRequired()
is MainAction.Internal.ResizeHasBeenRequested -> handleResizeHasBeenRequested()
is MainAction.Internal.HasShownAccessibilityDisclaimerUpdate -> {
handleHasShownAccessibilityDisclaimerUpdate(action)
}
}
}
private fun handleHasShownAccessibilityDisclaimerUpdate(
action: MainAction.Internal.HasShownAccessibilityDisclaimerUpdate,
) {
if (!action.hasBeenShown) {
sendEvent(MainEvent.NavigateToAccessibilityDisclosure)
}
}
@@ -333,14 +295,6 @@ class MainViewModel @Inject constructor(
mutableStateFlow.update { it.copy(isDynamicColorsEnabled = action.isDynamicColorsEnabled) }
}
private fun handleCookieAcquisitionReady() {
sendEvent(MainEvent.NavigateToCookieAcquisition)
}
private fun handleLocalNetworkAccessRequired() {
sendEvent(MainEvent.NavigateToLocalNetworkAccess)
}
private fun handleResizeHasBeenRequested() {
mutableStateFlow.update { it.copy(hasResizeBeenRequested = true) }
}
@@ -695,26 +649,10 @@ sealed class MainAction {
val isDynamicColorsEnabled: Boolean,
) : Internal()
/**
* Indicates that the cookie acquisition conditions are met and navigation
* should proceed.
*/
data object CookieAcquisitionReady : Internal()
/**
* Indicates that the local network access is required.
*/
data object LocalNetworkAccessRequired : Internal()
/**
* Indicates that resize has been requested on the Activity
*/
data object ResizeHasBeenRequested : Internal()
/**
* Indicates that the accessibility disclaimer has been displayed.
*/
data class HasShownAccessibilityDisclaimerUpdate(val hasBeenShown: Boolean) : Internal()
}
}
@@ -744,21 +682,6 @@ sealed class MainEvent {
*/
data object NavigateToDebugMenu : MainEvent()
/**
* Navigate to the cookie acquisition screen.
*/
data object NavigateToCookieAcquisition : MainEvent()
/**
* Navigate to the local network access screen.
*/
data object NavigateToLocalNetworkAccess : MainEvent()
/**
* Navigate to the accessibility disclosure screen.
*/
data object NavigateToAccessibilityDisclosure : MainEvent()
/**
* Indicates that the app language has been updated.
*/

View File

@@ -829,7 +829,7 @@ fun Cipher.toFailureCipherListView(): CipherListView =
folderId = folderId,
collectionIds = collectionIds,
key = key,
name = name.orEmpty(),
name = name,
subtitle = "",
type = when (type) {
CipherType.LOGIN -> CipherListViewType.Login(

View File

@@ -2,7 +2,7 @@ package com.x8bit.bitwarden.ui.platform.feature.debugmenu
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import kotlinx.serialization.Serializable
/**
@@ -27,7 +27,7 @@ fun NavGraphBuilder.debugMenuDestination(
onNavigateBack: () -> Unit,
onSplashScreenRemoved: () -> Unit,
) {
composableWithPushTransitions<DebugRoute> {
composableWithSlideTransitions<DebugRoute> {
DebugMenuScreen(onNavigateBack = onNavigateBack)
// If we are displaying the debug screen, then we can just hide the splash screen.
onSplashScreenRemoved()

View File

@@ -0,0 +1,22 @@
package com.x8bit.bitwarden.ui.platform.feature.overlaynav
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import kotlinx.serialization.Serializable
/**
* The type-safe route for the overlay navigation screen.
*/
@Serializable
data object OverlayNavRoute
/**
* Add the overlay navigation screen to the nav graph.
*/
fun NavGraphBuilder.overlayNavDestination(
onSplashScreenRemoved: () -> Unit,
) {
composable<OverlayNavRoute> {
OverlayNavScreen(onSplashScreenRemoved = onSplashScreenRemoved)
}
}

View File

@@ -0,0 +1,76 @@
package com.x8bit.bitwarden.ui.platform.feature.overlaynav
import androidx.compose.runtime.Composable
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.components.util.rememberBitwardenNavController
import com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure.accessibilityDisclosureDestination
import com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure.navigateToAccessibilityDisclosure
import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.cookieAcquisitionDestination
import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.navigateToCookieAcquisition
import com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.localNetworkAccessDestination
import com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.navigateToLocalNetworkAccess
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavigationRoute
import com.x8bit.bitwarden.ui.platform.feature.rootnav.rootNavDestination
/**
* Controls the overlay [NavHost] for the app including the [rootNavDestination] and any screen
* that can appear on top of it without affecting its state.
*/
@Composable
fun OverlayNavScreen(
viewModel: OverlayNavViewModel = hiltViewModel(),
navController: NavHostController = rememberBitwardenNavController(name = "OverlayNavScreen"),
onSplashScreenRemoved: () -> Unit,
) {
OverlayNavEventsEffect(
viewModel = viewModel,
navController = navController,
)
NavHost(
navController = navController,
startDestination = RootNavigationRoute,
) {
// This is the overlay level of navigation that sits above the root nav. These screens
// can appear on top of the rest of the app without interacting with the state-based
// navigation used by RootNavScreen (which also exists here).
rootNavDestination(onSplashScreenRemoved = onSplashScreenRemoved)
cookieAcquisitionDestination(
onDismiss = { navController.popBackStack() },
onSplashScreenRemoved = onSplashScreenRemoved,
)
localNetworkAccessDestination(
onDismiss = { navController.popBackStack() },
onSplashScreenRemoved = onSplashScreenRemoved,
)
accessibilityDisclosureDestination(
onDismiss = { navController.popBackStack() },
onSplashScreenRemoved = onSplashScreenRemoved,
)
}
}
@Composable
private fun OverlayNavEventsEffect(
viewModel: OverlayNavViewModel,
navController: NavController,
) {
EventsEffect(viewModel = viewModel) { event ->
when (event) {
OverlayNavEvent.NavigateToCookieAcquisition -> {
navController.navigateToCookieAcquisition()
}
OverlayNavEvent.NavigateToLocalNetworkAccess -> {
navController.navigateToLocalNetworkAccess()
}
OverlayNavEvent.NavigateToAccessibilityDisclosure -> {
navController.navigateToAccessibilityDisclosure()
}
}
}
}

View File

@@ -0,0 +1,127 @@
package com.x8bit.bitwarden.ui.platform.feature.overlaynav
import androidx.lifecycle.viewModelScope
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.base.DeferredBackgroundEvent
import com.x8bit.bitwarden.data.platform.manager.CookieAcquisitionRequestManager
import com.x8bit.bitwarden.data.platform.manager.network.NetworkPermissionManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
/**
* Manages the overlay navigation, hosting the root-navigation and any screen that can overlay it.
*/
@HiltViewModel
class OverlayNavViewModel @Inject constructor(
cookieAcquisitionRequestManager: CookieAcquisitionRequestManager,
networkPermissionManager: NetworkPermissionManager,
settingsRepository: SettingsRepository,
) : BaseViewModel<Unit, OverlayNavEvent, OverlayNavAction>(initialState = Unit) {
init {
settingsRepository
.hasShownAccessibilityDisclaimerFlow
.filterNot { it }
.map { OverlayNavAction.Internal.AccessibilityDisclosureRequired }
.onEach(::trySendAction)
.launchIn(viewModelScope)
networkPermissionManager
.isLocalNetworkAccessRequiredStateFlow
.filter { it }
.map { OverlayNavAction.Internal.LocalNetworkAccessRequired }
.onEach(::sendAction)
.launchIn(viewModelScope)
cookieAcquisitionRequestManager
.cookieAcquisitionRequestFlow
.filterNotNull()
.map { OverlayNavAction.Internal.CookieAcquisitionReady }
.onEach(::sendAction)
.launchIn(viewModelScope)
}
override fun handleAction(action: OverlayNavAction) {
when (action) {
is OverlayNavAction.Internal -> handleInternal(action)
}
}
private fun handleInternal(action: OverlayNavAction.Internal) {
when (action) {
OverlayNavAction.Internal.AccessibilityDisclosureRequired -> {
handleAccessibilityDisclosureRequired()
}
OverlayNavAction.Internal.CookieAcquisitionReady -> handleCookieAcquisitionReady()
OverlayNavAction.Internal.LocalNetworkAccessRequired -> {
handleLocalNetworkAccessRequired()
}
}
}
private fun handleAccessibilityDisclosureRequired() {
sendEvent(OverlayNavEvent.NavigateToAccessibilityDisclosure)
}
private fun handleCookieAcquisitionReady() {
sendEvent(OverlayNavEvent.NavigateToCookieAcquisition)
}
private fun handleLocalNetworkAccessRequired() {
sendEvent(OverlayNavEvent.NavigateToLocalNetworkAccess)
}
}
/**
* Models events for the overlay navigation screen.
*/
sealed class OverlayNavEvent {
/**
* Navigate to the cookie acquisition screen.
*/
data object NavigateToCookieAcquisition : OverlayNavEvent(), DeferredBackgroundEvent
/**
* Navigate to the local network access screen.
*/
data object NavigateToLocalNetworkAccess : OverlayNavEvent(), DeferredBackgroundEvent
/**
* Navigate to the accessibility disclosure screen.
*/
data object NavigateToAccessibilityDisclosure : OverlayNavEvent(), DeferredBackgroundEvent
}
/**
* Models actions for the overlay navigation screen.
*/
sealed class OverlayNavAction {
/**
* Internal ViewModel actions.
*/
sealed class Internal : OverlayNavAction() {
/**
* Indicates that the cookie acquisition conditions are met and navigation
* should proceed.
*/
data object CookieAcquisitionReady : Internal()
/**
* Indicates that the local network access is required.
*/
data object LocalNetworkAccessRequired : Internal()
/**
* Indicates that the accessibility disclosure needs to be displayed.
*/
data object AccessibilityDisclosureRequired : Internal()
}
}

View File

@@ -18,6 +18,7 @@
<!-- CRL Distribution Servers -->
<domain includeSubdomains="true">c.lencr.org</domain>
<domain includeSubdomains="true">c.pki.goog</domain>
<domain includeSubdomains="true">crls.certainly.com</domain>
<!-- OCSP Responder Servers -->
<domain includeSubdomains="true">o.pki.goog</domain>

View File

@@ -55,17 +55,14 @@ import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionReques
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
import com.x8bit.bitwarden.data.credentials.model.ProviderGetPasswordCredentialRequest
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
import com.x8bit.bitwarden.data.platform.manager.CookieAcquisitionRequestManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
import com.x8bit.bitwarden.data.platform.manager.model.CookieAcquisitionRequest
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.manager.network.NetworkPermissionManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticator
@@ -114,7 +111,6 @@ class MainViewModelTest : BaseViewModelTest() {
private val mutableAppLanguageFlow = MutableStateFlow(AppLanguage.DEFAULT)
private val mutableScreenCaptureAllowedFlow = MutableStateFlow(true)
private val mutableIsDynamicColorsEnabledFlow = MutableStateFlow(false)
private val mutableHasShownAccessibilityDisclaimerFlow = MutableStateFlow(true)
private val settingsRepository = mockk<SettingsRepository> {
every { appTheme } returns AppTheme.DEFAULT
every { appThemeStateFlow } returns mutableAppThemeFlow
@@ -125,10 +121,6 @@ class MainViewModelTest : BaseViewModelTest() {
every { appLanguage = any() } just runs
every { isDynamicColorsEnabled } returns false
every { isDynamicColorsEnabledFlow } returns mutableIsDynamicColorsEnabledFlow
every {
hasShownAccessibilityDisclaimerFlow
} returns mutableHasShownAccessibilityDisclaimerFlow
every { accessibilityDisclaimerHasBeenShown() } just runs
}
private val authRepository = mockk<AuthRepository> {
every { activeUserId } returns DEFAULT_USER_STATE.activeUserId
@@ -183,17 +175,6 @@ class MainViewModelTest : BaseViewModelTest() {
every { show(message = any(), duration = any()) } just runs
every { show(messageId = any(), duration = any()) } just runs
}
private val mutableCookieAcquisitionRequestFlow =
MutableStateFlow<CookieAcquisitionRequest?>(null)
private val cookieAcquisitionRequestManager: CookieAcquisitionRequestManager = mockk {
every { cookieAcquisitionRequestFlow } returns mutableCookieAcquisitionRequestFlow
}
private val mutableIsLocalNetworkAccessRequiredStateFlow = MutableStateFlow(false)
private val networkPermissionManager: NetworkPermissionManager = mockk {
every {
isLocalNetworkAccessRequiredStateFlow
} returns mutableIsLocalNetworkAccessRequiredStateFlow
}
private val credentialProviderRequestManager: CredentialProviderRequestManager = mockk {
every { getPendingCredentialRequest() } returns null
}
@@ -1319,53 +1300,6 @@ class MainViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `cookie acquisition should emit NavigateToCookieAcquisition when vault unlocked with matching hostname`() =
runTest {
mutableCookieAcquisitionRequestFlow.value = CookieAcquisitionRequest(
hostname = DEFAULT_US_WEB_VAULT_URL,
)
val viewModel = createViewModel()
viewModel.eventFlow.test {
// Skip init events (appLanguage + appTheme)
skipItems(2)
mutableUserStateFlow.value = DEFAULT_USER_STATE
assertEquals(
MainEvent.NavigateToCookieAcquisition,
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `local network access required should emit NavigateToLocalNetworkAccess when stateflow emits`() =
runTest {
mutableIsLocalNetworkAccessRequiredStateFlow.value = true
val viewModel = createViewModel()
viewModel.eventFlow.test {
// Skip init events (appLanguage + appTheme)
skipItems(2)
assertEquals(MainEvent.NavigateToLocalNetworkAccess, awaitItem())
}
}
@Test
fun `cookie acquisition should not emit event when conditions are false`() =
runTest {
mutableCookieAcquisitionRequestFlow.value = null
val viewModel = createViewModel()
viewModel.eventFlow.test {
// Skip init events (appLanguage + appTheme)
skipItems(2)
mutableUserStateFlow.value = DEFAULT_USER_STATE
expectNoEvents()
}
}
@Test
fun `on handleResizeHasBeenRequested should set hasResizeBeenRequested as true`() = runTest {
val viewModel = createViewModel()
@@ -1390,31 +1324,12 @@ class MainViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `on HasShownAccessibilityDisclaimerUpdate with false should show the accessibility disclosure`() =
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
// We skip the first 2 events because they are the default appTheme and appLanguage
skipItems(2)
mutableHasShownAccessibilityDisclaimerFlow.value = false
assertEquals(MainEvent.NavigateToAccessibilityDisclosure, awaitItem())
mutableHasShownAccessibilityDisclaimerFlow.value = true
expectNoEvents()
}
}
private fun createViewModel(
initialSpecialCircumstance: SpecialCircumstance? = null,
): MainViewModel = MainViewModel(
accessibilitySelectionManager = accessibilitySelectionManager,
addTotpItemFromAuthenticatorManager = addTotpItemAuthenticatorManager,
autofillSelectionManager = autofillSelectionManager,
cookieAcquisitionRequestManager = cookieAcquisitionRequestManager,
networkPermissionManager = networkPermissionManager,
specialCircumstanceManager = specialCircumstanceManager,
garbageCollectionManager = garbageCollectionManager,
credentialProviderRequestManager = credentialProviderRequestManager,
@@ -1445,7 +1360,6 @@ private val DEFAULT_FIRST_TIME_STATE = FirstTimeState(
private const val SPECIAL_CIRCUMSTANCE_KEY: String = "special-circumstance"
private const val ACTIVE_USER_ID: String = "activeUserId"
private const val DEFAULT_US_WEB_VAULT_URL: String = "https://vault.bitwarden.com"
private val DEFAULT_ACCOUNT = UserState.Account(
userId = ACTIVE_USER_ID,
name = "Active User",

View File

@@ -0,0 +1,68 @@
package com.x8bit.bitwarden.ui.platform.feature.overlaynav
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.platform.base.createMockNavHostController
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure.AccessibilityDisclosureRoute
import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.CookieAcquisitionRoute
import com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.LocalNetworkAccessRoute
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Before
import org.junit.Test
class OverlayNavScreenTest : BitwardenComposeTest() {
private val mockNavHostController = createMockNavHostController()
private val mutableEventFlow = bufferedMutableSharedFlow<OverlayNavEvent>()
private val viewModel = mockk<OverlayNavViewModel> {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns MutableStateFlow(Unit)
}
@Before
fun setup() {
setContent {
OverlayNavScreen(
viewModel = viewModel,
navController = mockNavHostController,
onSplashScreenRemoved = {},
)
}
}
@Test
fun `on NavigateToCookieAcquisition should navigate to the cookie acquisition screen`() {
mutableEventFlow.tryEmit(OverlayNavEvent.NavigateToCookieAcquisition)
composeTestRule.runOnIdle {
verify(exactly = 1) {
mockNavHostController.navigate(route = CookieAcquisitionRoute, builder = any())
}
}
}
@Test
fun `on NavigateToLocalNetworkAccess should navigate to the local network access screen`() {
mutableEventFlow.tryEmit(OverlayNavEvent.NavigateToLocalNetworkAccess)
composeTestRule.runOnIdle {
verify(exactly = 1) {
mockNavHostController.navigate(route = LocalNetworkAccessRoute, builder = any())
}
}
}
@Suppress("MaxLineLength")
@Test
fun `on NavigateToAccessibilityDisclosure should navigate to the accessibility disclosure screen`() {
mutableEventFlow.tryEmit(OverlayNavEvent.NavigateToAccessibilityDisclosure)
composeTestRule.runOnIdle {
verify(exactly = 1) {
mockNavHostController.navigate(
route = AccessibilityDisclosureRoute,
builder = any(),
)
}
}
}
}

View File

@@ -0,0 +1,71 @@
package com.x8bit.bitwarden.ui.platform.feature.overlaynav
import app.cash.turbine.test
import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.data.platform.manager.CookieAcquisitionRequestManager
import com.x8bit.bitwarden.data.platform.manager.model.CookieAcquisitionRequest
import com.x8bit.bitwarden.data.platform.manager.network.NetworkPermissionManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class OverlayNavViewModelTest : BaseViewModelTest() {
private val mutableAccessibilityDisclaimerFlow = MutableStateFlow(true)
private val settingsRepository: SettingsRepository = mockk {
every { hasShownAccessibilityDisclaimerFlow } returns mutableAccessibilityDisclaimerFlow
}
private val mutableLocalNetworkAccessFlow = MutableStateFlow(false)
private val networkPermissionManager: NetworkPermissionManager = mockk {
every { isLocalNetworkAccessRequiredStateFlow } returns mutableLocalNetworkAccessFlow
}
private val mutableCookieAcquisitionFlow = MutableStateFlow<CookieAcquisitionRequest?>(null)
private val cookieAcquisitionRequestManager: CookieAcquisitionRequestManager = mockk {
every { cookieAcquisitionRequestFlow } returns mutableCookieAcquisitionFlow
}
@Suppress("MaxLineLength")
@Test
fun `when accessibility disclaimer flow is false should emit NavigateToAccessibilityDisclosure`() =
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
expectNoEvents()
mutableAccessibilityDisclaimerFlow.value = false
assertEquals(OverlayNavEvent.NavigateToAccessibilityDisclosure, awaitItem())
}
}
@Test
fun `when local network access flow is true should emit NavigateToLocalNetworkAccess`() =
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
expectNoEvents()
mutableLocalNetworkAccessFlow.value = true
assertEquals(OverlayNavEvent.NavigateToLocalNetworkAccess, awaitItem())
}
}
@Test
fun `when a cookie acquisition request is ready should emit NavigateToCookieAcquisition`() =
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
expectNoEvents()
mutableCookieAcquisitionFlow.value = CookieAcquisitionRequest(
hostname = "vault.bitwarden.com",
)
assertEquals(OverlayNavEvent.NavigateToCookieAcquisition, awaitItem())
}
}
private fun createViewModel(): OverlayNavViewModel = OverlayNavViewModel(
cookieAcquisitionRequestManager = cookieAcquisitionRequestManager,
networkPermissionManager = networkPermissionManager,
settingsRepository = settingsRepository,
)
}

View File

@@ -31,6 +31,7 @@ fun createMockNavHostController(): NavHostController =
every { setViewModelStore(viewModelStore = any()) } just runs
every { setLifecycleOwner(owner = any()) } just runs
every { navigate(route = any<Any>(), navOptions = any()) } just runs
every { navigate(route = any<Any>(), builder = any()) } just runs
}
/**