Compare commits

...

7 Commits

Author SHA1 Message Date
André Bispo
39e72a86e2 [PM-16808] Add question mark to copy (#4544) 2025-01-09 23:21:54 +00:00
André Bispo
ac80d662d5 [PM-16670] Add check for 2fa status (#4542) 2025-01-09 22:00:39 +00:00
André Bispo
08d173e8e1 [PM-16670] Force app to sync after 2FA notice (#4525) 2025-01-08 22:35:56 +00:00
André Bispo
8310a03d73 [PM-16809] Fix remind me later date (#4526) 2025-01-08 19:03:40 +00:00
aj-rosado
cabd1865f0 [PM-16695] Added "learn more" link to NewDeviceNoticeEmailAccess (#4520) 2025-01-06 18:47:16 +00:00
André Bispo
0c0509f567 [PM-15969] Users with Can Edit access cannot assign collections (#4512) 2025-01-06 14:48:34 +00:00
André Bispo
02d3c19e1c [PM-8217] Add local feature flag to ignore environment validation (#4516) 2025-01-06 12:55:34 +00:00
17 changed files with 288 additions and 115 deletions

View File

@@ -1388,7 +1388,9 @@ class AuthRepositoryImpl(
* - Cannot have two-factor authentication enabled.
*/
private fun newDeviceNoticePreConditionsValid(): Boolean {
if (environmentRepository.environment.type == Environment.Type.SELF_HOSTED) {
val checkEnvironment = !featureFlagManager.getFeatureFlag(FlagKey.IgnoreEnvironmentCheck)
val isSelfHosted = environmentRepository.environment.type == Environment.Type.SELF_HOSTED
if (checkEnvironment && isSelfHosted) {
return false
}

View File

@@ -38,6 +38,7 @@ sealed class FlagKey<out T : Any> {
AppReviewPrompt,
NewDevicePermanentDismiss,
NewDeviceTemporaryDismiss,
IgnoreEnvironmentCheck,
)
}
}
@@ -161,6 +162,15 @@ sealed class FlagKey<out T : Any> {
override val isRemotelyConfigured: Boolean = true
}
/**
* Data object holding the feature flag key to ignore an environment check.
*/
data object IgnoreEnvironmentCheck : FlagKey<Boolean>() {
override val keyName: String = "ignore-environment-check"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = false
}
//region Dummy keys for testing
/**
* Data object holding the key for a [Boolean] flag to be used in tests.

View File

@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
@@ -24,19 +25,24 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeEmailAccessAction.ContinueClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeEmailAccessAction.EmailAccessToggle
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeEmailAccessAction.LearnMoreClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeEmailAccessEvent.NavigateToTwoFactorOptions
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
@@ -47,12 +53,19 @@ fun NewDeviceNoticeEmailAccessScreen(
onNavigateBackToVault: () -> Unit,
onNavigateToTwoFactorOptions: () -> Unit,
viewModel: NewDeviceNoticeEmailAccessViewModel = hiltViewModel(),
intentManager: IntentManager = LocalIntentManager.current,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
NavigateToTwoFactorOptions -> onNavigateToTwoFactorOptions()
NewDeviceNoticeEmailAccessEvent.NavigateBackToVault -> onNavigateBackToVault()
is NewDeviceNoticeEmailAccessEvent.NavigateToLearnMore -> {
intentManager.launchUri(
"https://bitwarden.com/help/new-device-verification/"
.toUri(),
)
}
}
}
@@ -66,6 +79,7 @@ fun NewDeviceNoticeEmailAccessScreen(
}
},
onContinueClick = { viewModel.trySendAction(ContinueClick) },
onLearnMoreClick = { viewModel.trySendAction(LearnMoreClick) },
)
}
}
@@ -76,6 +90,7 @@ private fun NewDeviceNoticeEmailAccessContent(
isEmailAccessEnabled: Boolean,
onEmailAccessToggleChanged: (Boolean) -> Unit,
onContinueClick: () -> Unit,
onLearnMoreClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@@ -86,7 +101,7 @@ private fun NewDeviceNoticeEmailAccessContent(
.verticalScroll(state = rememberScrollState()),
) {
Spacer(modifier = Modifier.height(104.dp))
HeaderContent()
HeaderContent(onLearnMoreClick = onLearnMoreClick)
Spacer(modifier = Modifier.height(24.dp))
MainContent(
email = email,
@@ -110,7 +125,9 @@ private fun NewDeviceNoticeEmailAccessContent(
*/
@Suppress("MaxLineLength")
@Composable
private fun ColumnScope.HeaderContent() {
private fun ColumnScope.HeaderContent(
onLearnMoreClick: () -> Unit,
) {
Image(
painter = rememberVectorPainter(id = R.drawable.warning),
contentDescription = null,
@@ -132,6 +149,13 @@ private fun ColumnScope.HeaderContent() {
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
)
BitwardenClickableText(
label = stringResource(id = R.string.learn_more),
onClick = onLearnMoreClick,
style = BitwardenTheme.typography.labelLarge,
innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
modifier = Modifier.testTag("LearnMoreLabel"),
)
}
/**
@@ -183,6 +207,7 @@ private fun NewDeviceNoticeEmailAccessScreen_preview() {
isEmailAccessEnabled = true,
onEmailAccessToggleChanged = {},
onContinueClick = {},
onLearnMoreClick = {},
)
}
}

View File

@@ -2,20 +2,23 @@ package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL_PERMANENT
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeEmailAccessAction.ContinueClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeEmailAccessAction.EmailAccessToggle
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeEmailAccessAction.LearnMoreClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeEmailAccessEvent.NavigateToTwoFactorOptions
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import java.time.ZonedDateTime
import javax.inject.Inject
private const val KEY_STATE = "state"
@@ -26,6 +29,7 @@ private const val KEY_STATE = "state"
@HiltViewModel
class NewDeviceNoticeEmailAccessViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val vaultRepository: VaultRepository,
private val featureFlagManager: FeatureFlagManager,
savedStateHandle: SavedStateHandle,
) : BaseViewModel<
@@ -39,10 +43,20 @@ class NewDeviceNoticeEmailAccessViewModel @Inject constructor(
isEmailAccessEnabled = false,
),
) {
init {
viewModelScope.launch {
vaultRepository.syncForResult()
if (!authRepository.checkUserNeedsNewDeviceTwoFactorNotice()) {
sendEvent(NewDeviceNoticeEmailAccessEvent.NavigateBackToVault)
}
}
}
override fun handleAction(action: NewDeviceNoticeEmailAccessAction) {
when (action) {
ContinueClick -> handleContinueClick()
is EmailAccessToggle -> handleEmailAccessToggle(action)
LearnMoreClick -> handleLearnMoreClick()
}
}
@@ -58,7 +72,7 @@ class NewDeviceNoticeEmailAccessViewModel @Inject constructor(
authRepository.setNewDeviceNoticeState(
NewDeviceNoticeState(
displayStatus = displayStatus,
lastSeenDate = ZonedDateTime.now(),
lastSeenDate = null,
),
)
sendEvent(NewDeviceNoticeEmailAccessEvent.NavigateBackToVault)
@@ -72,6 +86,10 @@ class NewDeviceNoticeEmailAccessViewModel @Inject constructor(
it.copy(isEmailAccessEnabled = action.isEnabled)
}
}
private fun handleLearnMoreClick() {
sendEvent(NewDeviceNoticeEmailAccessEvent.NavigateToLearnMore)
}
}
/**
@@ -96,6 +114,11 @@ sealed class NewDeviceNoticeEmailAccessEvent {
* Navigates back.
*/
data object NavigateBackToVault : NewDeviceNoticeEmailAccessEvent()
/**
* Navigates to learn more about New Device Login Protection
*/
data object NavigateToLearnMore : NewDeviceNoticeEmailAccessEvent()
}
/**
@@ -111,4 +134,9 @@ sealed class NewDeviceNoticeEmailAccessAction {
* User tapped the email access toggle.
*/
data class EmailAccessToggle(val isEnabled: Boolean) : NewDeviceNoticeEmailAccessAction()
/**
* User tapped the learn more button.
*/
data object LearnMoreClick : NewDeviceNoticeEmailAccessAction()
}

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.auth.feature.newdevicenotice
import android.os.Parcelable
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
@@ -8,7 +9,9 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.util.baseWebVaultUrlOrDefault
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorAction.ChangeAccountEmailClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorAction.ContinueDialogClick
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorAction.DismissDialogClick
@@ -21,7 +24,10 @@ import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import java.time.Clock
import java.time.ZonedDateTime
import javax.inject.Inject
/**
@@ -32,6 +38,9 @@ class NewDeviceNoticeTwoFactorViewModel @Inject constructor(
val authRepository: AuthRepository,
val environmentRepository: EnvironmentRepository,
val featureFlagManager: FeatureFlagManager,
val settingsRepository: SettingsRepository,
val vaultRepository: VaultRepository,
private val clock: Clock,
) : BaseViewModel<
NewDeviceNoticeTwoFactorState,
NewDeviceNoticeTwoFactorEvent,
@@ -43,6 +52,15 @@ class NewDeviceNoticeTwoFactorViewModel @Inject constructor(
),
),
) {
init {
viewModelScope.launch {
vaultRepository.syncForResult()
if (!authRepository.checkUserNeedsNewDeviceTwoFactorNotice()) {
sendEvent(NewDeviceNoticeTwoFactorEvent.NavigateBackToVault)
}
}
}
private val webTwoFactorUrl: String
get() {
val baseUrl = environmentRepository
@@ -79,7 +97,7 @@ class NewDeviceNoticeTwoFactorViewModel @Inject constructor(
authRepository.setNewDeviceNoticeState(
NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_SEEN,
lastSeenDate = null,
lastSeenDate = ZonedDateTime.now(clock),
),
)
sendEvent(NewDeviceNoticeTwoFactorEvent.NavigateBackToVault)
@@ -88,6 +106,8 @@ class NewDeviceNoticeTwoFactorViewModel @Inject constructor(
private fun handleContinueDialog() {
when (state.dialogState) {
is ChangeAccountEmailDialog -> {
// when the user leaves the app set sync date to null to force a sync on next unlock
settingsRepository.vaultLastSync = null
sendEvent(
NewDeviceNoticeTwoFactorEvent.NavigateToChangeAccountEmail(url = webAccountUrl),
)
@@ -95,6 +115,8 @@ class NewDeviceNoticeTwoFactorViewModel @Inject constructor(
}
is TurnOnTwoFactorDialog -> {
// when the user leaves the app set sync date to null to force a sync on next unlock
settingsRepository.vaultLastSync = null
sendEvent(
NewDeviceNoticeTwoFactorEvent.NavigateToTurnOnTwoFactor(url = webTwoFactorUrl),
)

View File

@@ -35,6 +35,7 @@ fun <T : Any> FlagKey<T>.ListItemContent(
FlagKey.CipherKeyEncryption,
FlagKey.NewDevicePermanentDismiss,
FlagKey.NewDeviceTemporaryDismiss,
FlagKey.IgnoreEnvironmentCheck,
-> BooleanFlagItem(
label = flagKey.getDisplayLabel(),
key = flagKey as FlagKey<Boolean>,
@@ -85,4 +86,5 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
FlagKey.CipherKeyEncryption -> stringResource(R.string.cipher_key_encryption)
FlagKey.NewDevicePermanentDismiss -> stringResource(R.string.new_device_permanent_dismiss)
FlagKey.NewDeviceTemporaryDismiss -> stringResource(R.string.new_device_temporary_dismiss)
FlagKey.IgnoreEnvironmentCheck -> stringResource(R.string.ignore_environment_check)
}

View File

@@ -110,33 +110,20 @@ fun List<CollectionView>?.hasDeletePermissionInAtLeastOneCollection(
/**
* Checks if the user has permission to assign an item to a collection.
*
* Assigning to a collection is not allowed when the item is in a collection that the user does not
* have "manage" permission for and is also in a collection they cannot view the passwords in.
*
* E.g., If an item is in A collection with "view except passwords" or "edit except passwords"
* permission and in another with "manage" permission, the user **cannot** assign the item to other
* collections. Conversely, if an item is in a collection with "manage" permission and another with
* "view" or "edit" permission, the user **can** assign the item to other collections.
* Assigning to a collection is only allowed when the item is in a collection that the user does
* have "manage" or "edit" permission.
*/
fun List<CollectionView>?.canAssignToCollections(currentCollectionIds: List<String>?): Boolean {
if (this.isNullOrEmpty()) return true
if (currentCollectionIds.isNullOrEmpty()) return true
// Verify user can MANAGE at least one collection the item is in.
// Verify user can MANAGE or EDIT at least one collection the item is in.
return this
.any {
currentCollectionIds.contains(it.id) &&
it.permission == CollectionPermission.MANAGE
} &&
// Verify user does not have "edit except password" or "view except passwords"
// permission in any collection the item is not in.
this
.none {
currentCollectionIds.contains(it.id) &&
(it.permission == CollectionPermission.EDIT_EXCEPT_PASSWORD ||
it.permission == CollectionPermission.VIEW_EXCEPT_PASSWORDS)
}
(it.permission == CollectionPermission.MANAGE ||
it.permission == CollectionPermission.EDIT)
}
}
/**

View File

@@ -1089,7 +1089,7 @@ Do you want to switch to this account?</string>
<string name="copy_address">Copy address</string>
<string name="important_notice">Important notice</string>
<string name="bitwarden_will_soon_send_a_code_to_your_account_email_to_verify_logins_from_new_devices_in_february">Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025.</string>
<string name="do_you_have_reliable_access_to_your_email">Do you have reliable access to your email, <annotation emphasis="bold"><annotation arg="0">%1$s?</annotation></annotation></string>
<string name="do_you_have_reliable_access_to_your_email">Do you have reliable access to your email, <annotation emphasis="bold"><annotation arg="0">%1$s?</annotation></annotation>?</string>
<string name="yes_i_can_reliably_access_my_email">Yes, I can reliably access my email</string>
<string name="biometrics_no_longer_supported_title">Biometrics are no longer supported on this device</string>
<string name="biometrics_no_longer_supported">Youve been logged out because your devices biometrics dont meet the latest security requirements. To update settings, log in once again or contact your administrator for access.</string>

View File

@@ -24,5 +24,6 @@
<string name="cipher_key_encryption">Cipher Key Encryption</string>">
<string name="new_device_permanent_dismiss">New device notice permanent dismiss</string>">
<string name="new_device_temporary_dismiss">New device notice temporary dismiss</string>">
<string name="ignore_environment_check">Ignore environment check</string>">
<!-- /Debug Menu -->
</resources>

View File

@@ -250,7 +250,10 @@ class AuthRepositoryTest {
}
private val featureFlagManager: FeatureFlagManager = mockk(relaxed = true) {
every { getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss) } returns true
every { getFeatureFlag(FlagKey.NewDevicePermanentDismiss) } returns true
every { getFeatureFlag(FlagKey.OnboardingFlow) } returns false
every { getFeatureFlag(FlagKey.IgnoreEnvironmentCheck) } returns false
}
private val firstTimeActionManager = mockk<FirstTimeActionManager> {
@@ -6530,12 +6533,6 @@ class AuthRepositoryTest {
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice flags on, is cloud user, profile at least week old, no required sso policy, no two factor enable returns true`() =
runTest {
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
} returns true
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
} returns true
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
@@ -6571,14 +6568,47 @@ class AuthRepositoryTest {
}
@Test
fun `checkUserNeedsNewDeviceTwoFactorNotice has required SSO policy returns false`() =
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice IgnoreEnvironmentCheck flag enabled should not check for a cloud environment and return true`() =
runTest {
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
featureFlagManager.getFeatureFlag(FlagKey.IgnoreEnvironmentCheck)
} returns true
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
} returns true
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
fakeEnvironmentRepository.environment = Environment.SelfHosted(
EnvironmentUrlDataJson(base = "https://myselfhosted.environment.com"),
)
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
val shouldShowNewDeviceNotice = repository.checkUserNeedsNewDeviceTwoFactorNotice()
assertTrue(shouldShowNewDeviceNotice)
}
@Test
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice if environment is selfhosted return false`() =
runTest {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
fakeEnvironmentRepository.environment = Environment.SelfHosted(
EnvironmentUrlDataJson(base = "https://myselfhosted.environment.com"),
)
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
val shouldShowNewDeviceNotice = repository.checkUserNeedsNewDeviceTwoFactorNotice()
assertFalse(shouldShowNewDeviceNotice)
}
@Test
fun `checkUserNeedsNewDeviceTwoFactorNotice has required SSO policy returns false`() =
runTest {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf(
@@ -6599,12 +6629,6 @@ class AuthRepositoryTest {
@Test
fun `checkUserNeedsNewDeviceTwoFactorNotice with two factor enable returns false`() =
runTest {
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
} returns true
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
} returns true
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
@@ -6620,12 +6644,6 @@ class AuthRepositoryTest {
@Test
fun `checkUserNeedsNewDeviceTwoFactorNotice account less than a week old returns false`() =
runTest {
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
} returns true
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
} returns true
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
@@ -6652,12 +6670,6 @@ class AuthRepositoryTest {
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice with NewDeviceNoticeDisplayStatus CAN_ACCESS_EMAIL_PERMANENT return false`() =
runTest {
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
} returns true
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
} returns true
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
@@ -6681,12 +6693,6 @@ class AuthRepositoryTest {
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice with NewDeviceNoticeDisplayStatus HAS_NOT_SEEN return true`() =
runTest {
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
} returns true
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
} returns true
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
@@ -6710,12 +6716,6 @@ class AuthRepositoryTest {
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice with NewDeviceNoticeDisplayStatus HAS_SEEN return true if date is older than 7 days`() =
runTest {
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
} returns true
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
} returns true
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
@@ -6739,12 +6739,6 @@ class AuthRepositoryTest {
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice with NewDeviceNoticeDisplayStatus HAS_SEEN return false if date is not older than 7 days`() =
runTest {
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
} returns true
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
} returns true
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
@@ -6768,12 +6762,6 @@ class AuthRepositoryTest {
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice with NewDeviceNoticeDisplayStatus CAN_ACCESS_EMAIL return permanent flag value`() =
runTest {
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
} returns true
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
} returns true
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
@@ -6800,12 +6788,6 @@ class AuthRepositoryTest {
@Test
fun `checkUserNeedsNewDeviceTwoFactorNotice with no active user returns false`() =
runTest {
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
} returns true
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
} returns true
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
@@ -6819,12 +6801,6 @@ class AuthRepositoryTest {
@Test
fun `checkUserNeedsNewDeviceTwoFactorNotice account with null creationDate returns false`() =
runTest {
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
} returns true
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
} returns true
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()
@@ -6840,7 +6816,6 @@ class AuthRepositoryTest {
),
),
)
assertFalse(repository.checkUserNeedsNewDeviceTwoFactorNotice())
}
@@ -6848,12 +6823,6 @@ class AuthRepositoryTest {
@Suppress("MaxLineLength")
fun `checkUserNeedsNewDeviceTwoFactorNotice account with null isTwoFactorEnabled returns true`() =
runTest {
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
} returns true
every {
featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
} returns true
every {
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
} returns listOf()

View File

@@ -66,4 +66,14 @@ class FlagKeyTest {
fun `NewDeviceTemporaryDismiss is remotely configured value should be true`() {
assertTrue(FlagKey.NewDeviceTemporaryDismiss.isRemotelyConfigured)
}
@Test
fun `IgnoreEnvironmentCheck default value should be false`() {
assertFalse(FlagKey.IgnoreEnvironmentCheck.defaultValue)
}
@Test
fun `IgnoreEnvironmentCheck is remotely configured value should be false`() {
assertFalse(FlagKey.IgnoreEnvironmentCheck.isRemotelyConfigured)
}
}

View File

@@ -5,10 +5,14 @@ import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.core.net.toUri
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.flow.MutableStateFlow
@@ -27,6 +31,10 @@ class NewDeviceNoticeEmailAccessScreenTest : BaseComposeTest() {
every { eventFlow } returns mutableEventFlow
}
private val intentManager: IntentManager = mockk {
every { launchUri(any()) } just runs
}
@Before
fun setUp() {
composeTestRule.setContent {
@@ -34,6 +42,7 @@ class NewDeviceNoticeEmailAccessScreenTest : BaseComposeTest() {
onNavigateBackToVault = { onNavigateBackToVaultCalled = true },
onNavigateToTwoFactorOptions = { onNavigateToTwoFactorOptionsCalled = true },
viewModel = viewModel,
intentManager,
)
}
}
@@ -91,6 +100,14 @@ class NewDeviceNoticeEmailAccessScreenTest : BaseComposeTest() {
mutableEventFlow.tryEmit(NewDeviceNoticeEmailAccessEvent.NavigateToTwoFactorOptions)
assertTrue(onNavigateToTwoFactorOptionsCalled)
}
@Test
fun `on NavigateToLearnMore should call launchUri on IntentManager`() {
mutableEventFlow.tryEmit(NewDeviceNoticeEmailAccessEvent.NavigateToLearnMore)
verify {
intentManager.launchUri("https://bitwarden.com/help/new-device-verification/".toUri())
}
}
}
private const val EMAIL = "active@bitwarden.com"

View File

@@ -7,11 +7,13 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@@ -31,6 +33,8 @@ class NewDeviceNoticeEmailAccessViewModelTest : BaseViewModelTest() {
every { getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss) } returns true
}
private val vaultRepository = mockk<VaultRepository>(relaxed = true)
@Test
fun `initial state should be correct with email from state handle`() = runTest {
val viewModel = createViewModel()
@@ -39,6 +43,27 @@ class NewDeviceNoticeEmailAccessViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `Init should not send events if user needs new device notice`() = runTest {
every { authRepository.checkUserNeedsNewDeviceTwoFactorNotice() } returns true
val viewModel = createViewModel()
viewModel.eventFlow.test {
expectNoEvents()
}
}
@Test
fun `Init should send NavigateBackToVault if user does not need new device notice`() = runTest {
every { authRepository.checkUserNeedsNewDeviceTwoFactorNotice() } returns false
val viewModel = createViewModel()
viewModel.eventFlow.test {
assertEquals(
NewDeviceNoticeEmailAccessEvent.NavigateBackToVault,
awaitItem(),
)
}
}
@Test
fun `EmailAccessToggle should update value of isEmailAccessEnabled`() = runTest {
val viewModel = createViewModel()
@@ -61,6 +86,14 @@ class NewDeviceNoticeEmailAccessViewModelTest : BaseViewModelTest() {
NewDeviceNoticeEmailAccessEvent.NavigateBackToVault,
awaitItem(),
)
verify(exactly = 1) {
authRepository.setNewDeviceNoticeState(
NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL_PERMANENT,
lastSeenDate = null,
),
)
}
}
}
@@ -77,6 +110,14 @@ class NewDeviceNoticeEmailAccessViewModelTest : BaseViewModelTest() {
NewDeviceNoticeEmailAccessEvent.NavigateBackToVault,
awaitItem(),
)
verify(exactly = 1) {
authRepository.setNewDeviceNoticeState(
NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL,
lastSeenDate = null,
),
)
}
}
}
@@ -93,6 +134,16 @@ class NewDeviceNoticeEmailAccessViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `LearnMoreClick should emit NavigateToLearnMore`() =
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(NewDeviceNoticeEmailAccessAction.LearnMoreClick)
assertEquals(NewDeviceNoticeEmailAccessEvent.NavigateToLearnMore, awaitItem())
}
}
private fun createViewModel(
savedStateHandle: SavedStateHandle = SavedStateHandle().also {
it["email_address"] = EMAIL
@@ -100,6 +151,7 @@ class NewDeviceNoticeEmailAccessViewModelTest : BaseViewModelTest() {
): NewDeviceNoticeEmailAccessViewModel = NewDeviceNoticeEmailAccessViewModel(
authRepository = authRepository,
featureFlagManager = featureFlagManager,
vaultRepository = vaultRepository,
savedStateHandle = savedStateHandle,
)
}

View File

@@ -6,26 +6,27 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorDialogState.ChangeAccountEmailDialog
import com.x8bit.bitwarden.ui.auth.feature.newdevicenotice.NewDeviceNoticeTwoFactorDialogState.TurnOnTwoFactorDialog
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
import java.time.ZonedDateTime
class NewDeviceNoticeTwoFactorViewModelTest : BaseViewModelTest() {
private val environmentRepository = FakeEnvironmentRepository()
private val authRepository = mockk<AuthRepository> {
every { getNewDeviceNoticeState() } returns NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN,
lastSeenDate = null,
)
every { setNewDeviceNoticeState(any()) } just runs
private val authRepository = mockk<AuthRepository>(relaxed = true) {
every { checkUserNeedsNewDeviceTwoFactorNotice() } returns true
}
private val featureFlagManager = mockk<FeatureFlagManager>(relaxed = true) {
@@ -33,6 +34,10 @@ class NewDeviceNoticeTwoFactorViewModelTest : BaseViewModelTest() {
every { getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss) } returns true
}
private val settingsRepository = mockk<SettingsRepository>(relaxed = true)
private val vaultRepository = mockk<VaultRepository>(relaxed = true)
@Test
fun `initial state should be correct with NewDevicePermanentDismiss flag false`() = runTest {
val viewModel = createViewModel()
@@ -53,6 +58,27 @@ class NewDeviceNoticeTwoFactorViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `Init should not send events if user needs new device notice`() = runTest {
every { authRepository.checkUserNeedsNewDeviceTwoFactorNotice() } returns true
val viewModel = createViewModel()
viewModel.eventFlow.test {
expectNoEvents()
}
}
@Test
fun `Init should send NavigateBackToVault if user does not need new device notice`() = runTest {
every { authRepository.checkUserNeedsNewDeviceTwoFactorNotice() } returns false
val viewModel = createViewModel()
viewModel.eventFlow.test {
assertEquals(
NewDeviceNoticeTwoFactorEvent.NavigateBackToVault,
awaitItem(),
)
}
}
@Test
fun `initial state should be correct with email from state handle`() = runTest {
val viewModel = createViewModel()
@@ -109,6 +135,14 @@ class NewDeviceNoticeTwoFactorViewModelTest : BaseViewModelTest() {
NewDeviceNoticeTwoFactorEvent.NavigateBackToVault,
awaitItem(),
)
verify(exactly = 1) {
authRepository.setNewDeviceNoticeState(
NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_SEEN,
lastSeenDate = ZonedDateTime.now(FIXED_CLOCK),
),
)
}
}
}
@@ -130,6 +164,9 @@ class NewDeviceNoticeTwoFactorViewModelTest : BaseViewModelTest() {
DEFAULT_STATE,
viewModel.stateFlow.value,
)
verify(exactly = 1) {
settingsRepository.vaultLastSync = null
}
}
}
@@ -151,6 +188,9 @@ class NewDeviceNoticeTwoFactorViewModelTest : BaseViewModelTest() {
DEFAULT_STATE,
viewModel.stateFlow.value,
)
verify(exactly = 1) {
settingsRepository.vaultLastSync = null
}
}
}
@@ -172,6 +212,9 @@ class NewDeviceNoticeTwoFactorViewModelTest : BaseViewModelTest() {
authRepository = authRepository,
environmentRepository = environmentRepository,
featureFlagManager = featureFlagManager,
settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
clock = FIXED_CLOCK,
)
}
@@ -180,3 +223,8 @@ private val DEFAULT_STATE =
shouldShowRemindMeLater = true,
dialogState = null,
)
private val FIXED_CLOCK: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)

View File

@@ -119,6 +119,7 @@ private val DEFAULT_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
FlagKey.AppReviewPrompt to true,
FlagKey.NewDeviceTemporaryDismiss to true,
FlagKey.NewDevicePermanentDismiss to true,
FlagKey.IgnoreEnvironmentCheck to true,
)
private val UPDATED_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
@@ -134,6 +135,7 @@ private val UPDATED_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
FlagKey.AppReviewPrompt to false,
FlagKey.NewDeviceTemporaryDismiss to false,
FlagKey.NewDevicePermanentDismiss to false,
FlagKey.IgnoreEnvironmentCheck to false,
)
private val DEFAULT_STATE = DebugMenuState(

View File

@@ -1187,7 +1187,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
resourceManager = resourceManager,
clock = fixedClock,
canDelete = false,
canAssignToCollections = false,
canAssignToCollections = true,
)
} returns stateWithName.viewState
@@ -1215,7 +1215,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
resourceManager = resourceManager,
clock = fixedClock,
canDelete = false,
canAssignToCollections = false,
canAssignToCollections = true,
)
}
}
@@ -1385,7 +1385,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
resourceManager = resourceManager,
clock = fixedClock,
canDelete = true,
canAssignToCollections = false,
canAssignToCollections = true,
)
} returns stateWithName.viewState
@@ -1414,7 +1414,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
resourceManager = resourceManager,
clock = fixedClock,
canDelete = true,
canAssignToCollections = false,
canAssignToCollections = true,
)
}
}
@@ -1440,7 +1440,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
),
notes = "mockNotes-1",
canDelete = true,
canAssociateToCollections = false,
canAssociateToCollections = true,
),
)
@@ -1452,7 +1452,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
resourceManager = resourceManager,
clock = fixedClock,
canDelete = true,
canAssignToCollections = false,
canAssignToCollections = true,
)
} returns stateWithName.viewState
@@ -1481,7 +1481,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
resourceManager = resourceManager,
clock = fixedClock,
canDelete = true,
canAssignToCollections = false,
canAssignToCollections = true,
)
}
}

View File

@@ -155,13 +155,11 @@ class CollectionViewExtensionsTest {
@Suppress("MaxLineLength")
@Test
fun `canAssociateToCollections should return false if the user has except password permission at least one collection`() {
fun `canAssociateToCollections should return false if the user doesn't have any manage or edit permissions`() {
val collectionList: List<CollectionView> = listOf(
createEditExceptPasswordsCollectionView(number = 1),
createViewCollectionView(number = 2),
createViewExceptPasswordsCollectionView(number = 3),
createManageCollectionView(number = 4),
createEditCollectionView(number = 5),
)
val collectionIds = collectionList.mapNotNull { it.id }
assertFalse(collectionList.canAssignToCollections(collectionIds))