Ensure a SpecialCircumstance is scoped to a single overall graph (#712)

This commit is contained in:
Brian Yencho 2024-01-22 16:14:06 -06:00 committed by Álison Fernandes
parent 2f918650a1
commit be7ccd3195
18 changed files with 252 additions and 134 deletions

View file

@ -3,8 +3,8 @@ package com.x8bit.bitwarden
import android.content.Intent
import android.os.Parcelable
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
@ -21,7 +21,7 @@ import javax.inject.Inject
*/
@HiltViewModel
class MainViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val specialCircumstanceManager: SpecialCircumstanceManager,
private val intentManager: IntentManager,
settingsRepository: SettingsRepository,
) : BaseViewModel<MainState, Unit, MainAction>(
@ -69,8 +69,8 @@ class MainViewModel @Inject constructor(
val shareData = intentManager.getShareDataFromIntent(intent)
when {
shareData != null -> {
authRepository.specialCircumstance =
UserState.SpecialCircumstance.ShareNewSend(
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ShareNewSend(
data = shareData,
// Allow users back into the already-running app when completing the
// Send task when this is not the first intent.

View file

@ -39,15 +39,6 @@ interface AuthRepository : AuthenticatorProvider {
*/
var rememberedEmailAddress: String?
/**
* Any special account circumstances that may be relevant (ex: pending multi-user account
* additions).
*
* This allows a direct view into and modification of [UserState.specialCircumstance].
* Note that this call has no effect when there is no [UserState] information available.
*/
var specialCircumstance: UserState.SpecialCircumstance?
/**
* Tracks whether there is an additional account that is pending login/registration in order to
* have multiple accounts available.

View file

@ -74,8 +74,6 @@ class AuthRepositoryImpl(
private val elapsedRealtimeMillisProvider: () -> Long = { SystemClock.elapsedRealtime() },
) : AuthRepository {
private val mutableHasPendingAccountAdditionStateFlow = MutableStateFlow<Boolean>(false)
private val mutableSpecialCircumstanceStateFlow =
MutableStateFlow<UserState.SpecialCircumstance?>(null)
/**
* A scope intended for use when simply collecting multiple flows in order to combine them. The
@ -109,20 +107,17 @@ class AuthRepositoryImpl(
authDiskSource.userOrganizationsListFlow,
vaultRepository.vaultStateFlow,
mutableHasPendingAccountAdditionStateFlow,
mutableSpecialCircumstanceStateFlow,
) {
userStateJson,
userOrganizationsList,
vaultState,
hasPendingAccountAddition,
specialCircumstance,
->
userStateJson
?.toUserState(
vaultState = vaultState,
userOrganizationsList = userOrganizationsList,
hasPendingAccountAddition = hasPendingAccountAddition,
specialCircumstance = specialCircumstance,
vaultUnlockTypeProvider = ::getVaultUnlockType,
)
}
@ -135,7 +130,6 @@ class AuthRepositoryImpl(
vaultState = vaultRepository.vaultStateFlow.value,
userOrganizationsList = authDiskSource.userOrganizationsList,
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
specialCircumstance = mutableSpecialCircumstanceStateFlow.value,
vaultUnlockTypeProvider = ::getVaultUnlockType,
),
)
@ -147,9 +141,6 @@ class AuthRepositoryImpl(
override var rememberedEmailAddress: String? by authDiskSource::rememberedEmailAddress
override var specialCircumstance: UserState.SpecialCircumstance?
by mutableSpecialCircumstanceStateFlow::value
override var hasPendingAccountAddition: Boolean
by mutableHasPendingAccountAdditionStateFlow::value

View file

@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.auth.repository.model
import com.x8bit.bitwarden.data.auth.repository.model.UserState.Account
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
/**
* Represents the overall "user state" of the current active user as well as any users that may be
@ -19,7 +18,6 @@ data class UserState(
val activeUserId: String,
val accounts: List<Account>,
val hasPendingAccountAddition: Boolean = false,
val specialCircumstance: SpecialCircumstance? = null,
) {
init {
require(accounts.any { it.userId == activeUserId })
@ -58,17 +56,4 @@ data class UserState(
val organizations: List<Organization>,
val vaultUnlockType: VaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
)
/**
* Represents a special account-related circumstance.
*/
sealed class SpecialCircumstance {
/**
* The app was launched in order to create/share a new Send using the given [data].
*/
data class ShareNewSend(
val data: IntentManager.ShareData,
val shouldFinishWhenComplete: Boolean,
) : SpecialCircumstance()
}
}

View file

@ -48,7 +48,6 @@ fun UserStateJson.toUserState(
vaultState: VaultState,
userOrganizationsList: List<UserOrganizations>,
hasPendingAccountAddition: Boolean,
specialCircumstance: UserState.SpecialCircumstance?,
vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType,
): UserState =
UserState(
@ -79,5 +78,4 @@ fun UserStateJson.toUserState(
)
},
hasPendingAccountAddition = hasPendingAccountAddition,
specialCircumstance = specialCircumstance,
)

View file

@ -0,0 +1,24 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.MainActivity
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import kotlinx.coroutines.flow.StateFlow
/**
* Tracks any [SpecialCircumstance] that may be present.
*
* Note that this will be scoped to the current "retained Activity": if there are multiple tasks
* that each have a [MainActivity], they can each have a separate [SpecialCircumstance] associated
* with them.
*/
interface SpecialCircumstanceManager {
/**
* Gets the current [SpecialCircumstance] if any.
*/
var specialCircumstance: SpecialCircumstance?
/**
* Emits updates that track changes to [specialCircumstance].
*/
val specialCircumstanceStateFlow: StateFlow<SpecialCircumstance?>
}

View file

@ -0,0 +1,21 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* Primary implementation of [SpecialCircumstanceManager].
*/
class SpecialCircumstanceManagerImpl : SpecialCircumstanceManager {
private val mutableSpecialCircumstanceFlow = MutableStateFlow<SpecialCircumstance?>(null)
override var specialCircumstance: SpecialCircumstance?
get() = mutableSpecialCircumstanceFlow.value
set(value) {
mutableSpecialCircumstanceFlow.value = value
}
override val specialCircumstanceStateFlow: StateFlow<SpecialCircumstance?>
get() = mutableSpecialCircumstanceFlow
}

View file

@ -0,0 +1,25 @@
package com.x8bit.bitwarden.data.platform.manager.di
import com.x8bit.bitwarden.MainActivity
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityRetainedComponent
import dagger.hilt.android.scopes.ActivityRetainedScoped
/**
* Provides managers in the platform package that must be scoped to a retained Activity. These are
* for dependencies that must operate independently in different application tasks that contain
* unique [MainActivity] instances.
*/
@Module
@InstallIn(ActivityRetainedComponent::class)
class ActivityPlatformManagerModule {
@Provides
@ActivityRetainedScoped
fun provideActivityScopedSpecialCircumstanceRepository(): SpecialCircumstanceManager =
SpecialCircumstanceManagerImpl()
}

View file

@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.platform.manager.model
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
/**
* Represents a special circumstance the app may be in. These circumstances could require some kind
* of navigation that is counter to what otherwise may happen based on the state of the app.
*/
sealed class SpecialCircumstance {
/**
* The app was launched in order to create/share a new Send using the given [data].
*/
data class ShareNewSend(
val data: IntentManager.ShareData,
val shouldFinishWhenComplete: Boolean,
) : SpecialCircumstance()
}

View file

@ -4,29 +4,40 @@ import android.os.Parcelable
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
private const val KEY_NAV_DESTINATION = "nav_state"
/**
* Manages root level navigation state of the application.
*/
@HiltViewModel
class RootNavViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val specialCircumstanceManager: SpecialCircumstanceManager,
) : BaseViewModel<RootNavState, Unit, RootNavAction>(
initialState = RootNavState.Splash,
) {
init {
authRepository
.userStateFlow
.onEach { sendAction(RootNavAction.Internal.UserStateUpdateReceive(it)) }
combine(
authRepository
.userStateFlow,
specialCircumstanceManager
.specialCircumstanceStateFlow,
) { userState, specialCircumstance ->
RootNavAction.Internal.UserStateUpdateReceive(
userState = userState,
specialCircumstance = specialCircumstance,
)
}
.onEach(::handleAction)
.launchIn(viewModelScope)
}
@ -45,16 +56,15 @@ class RootNavViewModel @Inject constructor(
action: RootNavAction.Internal.UserStateUpdateReceive,
) {
val userState = action.userState
val specialCircumstance = action.specialCircumstance
val updatedRootNavState = when {
userState == null ||
!userState.activeAccount.isLoggedIn ||
userState.hasPendingAccountAddition -> RootNavState.Auth
userState.activeAccount.isVaultUnlocked -> {
when (userState.specialCircumstance) {
is UserState.SpecialCircumstance.ShareNewSend -> {
RootNavState.VaultUnlockedForNewSend
}
when (specialCircumstance) {
is SpecialCircumstance.ShareNewSend -> RootNavState.VaultUnlockedForNewSend
null,
-> {
@ -126,6 +136,9 @@ sealed class RootNavAction {
/**
* User state in the repository layer changed.
*/
data class UserStateUpdateReceive(val userState: UserState?) : RootNavAction()
data class UserStateUpdateReceive(
val userState: UserState?,
val specialCircumstance: SpecialCircumstance?,
) : RootNavAction()
}
}

View file

@ -8,6 +8,7 @@ import com.bitwarden.core.SendView
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.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
@ -53,7 +54,7 @@ private const val MAX_FILE_SIZE_BYTES: Long = 100 * 1024 * 1024
/**
* View model for the new send screen.
*/
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LongParameterList")
@HiltViewModel
class AddSendViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
@ -61,12 +62,13 @@ class AddSendViewModel @Inject constructor(
private val clock: Clock,
private val clipboardManager: BitwardenClipboardManager,
private val environmentRepo: EnvironmentRepository,
private val specialCircumstanceManager: SpecialCircumstanceManager,
private val vaultRepo: VaultRepository,
) : BaseViewModel<AddSendState, AddSendEvent, AddSendAction>(
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE] ?: run {
// Check to see if we are navigating here from an external source
val specialCircumstance = authRepo.specialCircumstance
val specialCircumstance = specialCircumstanceManager.specialCircumstance
val shareSendType = specialCircumstance.toSendType()
val sendAddType = AddSendArgs(savedStateHandle).sendAddType
AddSendState(
@ -596,7 +598,7 @@ class AddSendViewModel @Inject constructor(
}
private fun navigateBack() {
authRepo.specialCircumstance = null
specialCircumstanceManager.specialCircumstance = null
sendEvent(
event = if (state.shouldFinishOnComplete) {
AddSendEvent.ExitApp

View file

@ -1,16 +1,16 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addsend.util
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendState
/**
* Determines the initial [AddSendState.ViewState.Content.SendType] based on the data in the
* [UserState.SpecialCircumstance].
* [SpecialCircumstance].
*/
fun UserState.SpecialCircumstance?.toSendType(): AddSendState.ViewState.Content.SendType? =
fun SpecialCircumstance?.toSendType(): AddSendState.ViewState.Content.SendType? =
when (this) {
is UserState.SpecialCircumstance.ShareNewSend -> {
is SpecialCircumstance.ShareNewSend -> {
when (data) {
is IntentManager.ShareData.FileSend -> AddSendState.ViewState.Content.SendType.File(
uri = data.fileData.uri,
@ -30,11 +30,11 @@ fun UserState.SpecialCircumstance?.toSendType(): AddSendState.ViewState.Content.
}
/**
* Determines the initial send name based on the data in the [UserState.SpecialCircumstance].
* Determines the initial send name based on the data in the [SpecialCircumstance].
*/
fun UserState.SpecialCircumstance?.toSendName(): String? =
fun SpecialCircumstance?.toSendName(): String? =
when (this) {
is UserState.SpecialCircumstance.ShareNewSend -> {
is SpecialCircumstance.ShareNewSend -> {
when (data) {
is IntentManager.ShareData.FileSend -> data.fileData.fileName
is IntentManager.ShareData.TextSend -> data.subject
@ -45,11 +45,10 @@ fun UserState.SpecialCircumstance?.toSendName(): String? =
}
/**
* Determines if the [UserState.SpecialCircumstance] requires the app to be closed after completing
* the send.
* Determines if the [SpecialCircumstance] requires the app to be closed after completing the send.
*/
fun UserState.SpecialCircumstance?.shouldFinishOnComplete(): Boolean =
fun SpecialCircumstance?.shouldFinishOnComplete(): Boolean =
when (this) {
is UserState.SpecialCircumstance.ShareNewSend -> shouldFinishWhenComplete
is SpecialCircumstance.ShareNewSend -> shouldFinishWhenComplete
else -> false
}

View file

@ -4,15 +4,15 @@ import android.content.Intent
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
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 io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.jupiter.api.Assertions.assertEquals
@ -25,13 +25,12 @@ class MainViewModelTest : BaseViewModelTest() {
val authRepository = mockk<AuthRepository> {
every { userStateFlow } returns mutableUserStateFlow
every { activeUserId } returns USER_ID
every { specialCircumstance } returns null
every { specialCircumstance = any() } just runs
}
private val settingsRepository = mockk<SettingsRepository> {
every { appTheme } returns AppTheme.DEFAULT
every { appThemeStateFlow } returns mutableAppThemeFlow
}
private val specialCircumstanceManager = SpecialCircumstanceManagerImpl()
private val intentManager: IntentManager = mockk {
every { getShareDataFromIntent(any()) } returns null
}
@ -78,12 +77,13 @@ class MainViewModelTest : BaseViewModelTest() {
intent = mockIntent,
),
)
verify {
authRepository.specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
assertEquals(
SpecialCircumstance.ShareNewSend(
data = shareData,
shouldFinishWhenComplete = true,
)
}
),
specialCircumstanceManager.specialCircumstance,
)
}
@Suppress("MaxLineLength")
@ -100,16 +100,17 @@ class MainViewModelTest : BaseViewModelTest() {
intent = mockIntent,
),
)
verify {
authRepository.specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
assertEquals(
SpecialCircumstance.ShareNewSend(
data = shareData,
shouldFinishWhenComplete = false,
)
}
),
specialCircumstanceManager.specialCircumstance,
)
}
private fun createViewModel() = MainViewModel(
authRepository = authRepository,
specialCircumstanceManager = specialCircumstanceManager,
settingsRepository = settingsRepository,
intentManager = intentManager,
)

View file

@ -35,7 +35,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.toOrganizations
@ -219,7 +218,6 @@ class AuthRepositoryTest {
vaultState = VAULT_STATE,
userOrganizationsList = emptyList(),
hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
),
repository.userStateFlow.value,
@ -241,7 +239,6 @@ class AuthRepositoryTest {
vaultState = VAULT_STATE,
userOrganizationsList = emptyList(),
hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
),
repository.userStateFlow.value,
@ -257,7 +254,6 @@ class AuthRepositoryTest {
vaultState = emptyVaultState,
userOrganizationsList = emptyList(),
hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
),
repository.userStateFlow.value,
@ -282,7 +278,6 @@ class AuthRepositoryTest {
vaultState = emptyVaultState,
userOrganizationsList = USER_ORGANIZATIONS,
hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
),
repository.userStateFlow.value,
@ -304,35 +299,6 @@ class AuthRepositoryTest {
assertNull(repository.rememberedEmailAddress)
}
@Test
fun `specialCircumstance update should trigger a change in UserState`() {
// Populate the initial UserState
assertNull(repository.specialCircumstance)
val initialUserState = SINGLE_USER_STATE_1.toUserState(
vaultState = VAULT_STATE,
userOrganizationsList = emptyList(),
hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
)
mutableVaultStateFlow.value = VAULT_STATE
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
assertEquals(
initialUserState,
repository.userStateFlow.value,
)
val mockSpecialCircumstance: UserState.SpecialCircumstance = mockk()
repository.specialCircumstance = mockSpecialCircumstance
assertEquals(
initialUserState.copy(
specialCircumstance = mockSpecialCircumstance,
),
repository.userStateFlow.value,
)
}
@Test
fun `delete account fails if not logged in`() = runTest {
val masterPassword = "hello world"
@ -591,7 +557,6 @@ class AuthRepositoryTest {
SINGLE_USER_STATE_1,
fakeAuthDiskSource.userState,
)
assertNull(repository.specialCircumstance)
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
verify { vaultRepository.clearUnlockedData() }
}
@ -1089,7 +1054,6 @@ class AuthRepositoryTest {
vaultState = VAULT_STATE,
userOrganizationsList = emptyList(),
hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
)
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
@ -1120,7 +1084,6 @@ class AuthRepositoryTest {
vaultState = VAULT_STATE,
userOrganizationsList = emptyList(),
hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
)
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
@ -1149,7 +1112,6 @@ class AuthRepositoryTest {
vaultState = VAULT_STATE,
userOrganizationsList = emptyList(),
hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
)
fakeAuthDiskSource.userState = MULTI_USER_STATE

View file

@ -155,7 +155,6 @@ class UserStateJsonExtensionsTest {
),
),
hasPendingAccountAddition = false,
specialCircumstance = null,
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
),
)
@ -187,7 +186,6 @@ class UserStateJsonExtensionsTest {
),
),
hasPendingAccountAddition = true,
specialCircumstance = MOCK_SPECIAL_CIRCUMSTANCE,
),
UserStateJson(
activeUserId = "activeUserId",
@ -227,11 +225,8 @@ class UserStateJsonExtensionsTest {
),
),
hasPendingAccountAddition = true,
specialCircumstance = MOCK_SPECIAL_CIRCUMSTANCE,
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
),
)
}
}
private val MOCK_SPECIAL_CIRCUMSTANCE: UserState.SpecialCircumstance = mockk()

View file

@ -2,6 +2,8 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import io.mockk.every
@ -19,6 +21,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
every { userStateFlow } returns mutableUserStateFlow
every { updateLastActiveTime() } just runs
}
private val specialCircumstanceManager = SpecialCircumstanceManagerImpl()
@Test
fun `when there are no accounts the nav state should be Auth`() {
@ -27,6 +30,57 @@ class RootNavViewModelTest : BaseViewModelTest() {
assertEquals(RootNavState.Auth, viewModel.stateFlow.value)
}
@Suppress("MaxLineLength")
@Test
fun `when the active user is not logged in the nav state should be Auth`() {
mutableUserStateFlow.tryEmit(
UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "name",
email = "email",
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isLoggedIn = false,
isVaultUnlocked = true,
organizations = emptyList(),
),
),
),
)
val viewModel = createViewModel()
assertEquals(RootNavState.Auth, viewModel.stateFlow.value)
}
@Suppress("MaxLineLength")
@Test
fun `when the active user but there are pending account additions the nav state should be Auth`() {
mutableUserStateFlow.tryEmit(
UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "name",
email = "email",
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = emptyList(),
),
),
hasPendingAccountAddition = true,
),
)
val viewModel = createViewModel()
assertEquals(RootNavState.Auth, viewModel.stateFlow.value)
}
@Test
fun `when the active user has an unlocked vault the nav state should be VaultUnlocked`() {
mutableUserStateFlow.tryEmit(
@ -54,6 +108,39 @@ class RootNavViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `when the active user has an unlocked vault but the is a ShareNewSend special circumstance the nav state should be VaultUnlockedForNewSend`() {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ShareNewSend(
data = mockk(),
shouldFinishWhenComplete = true,
)
mutableUserStateFlow.tryEmit(
UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "name",
email = "email",
avatarColorHex = "avatarColorHex",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = emptyList(),
),
),
),
)
val viewModel = createViewModel()
assertEquals(
RootNavState.VaultUnlockedForNewSend,
viewModel.stateFlow.value,
)
}
@Test
fun `when the active user has a locked vault the nav state should be VaultLocked`() {
mutableUserStateFlow.tryEmit(
@ -88,5 +175,6 @@ class RootNavViewModelTest : BaseViewModelTest() {
private fun createViewModel(): RootNavViewModel =
RootNavViewModel(
authRepository = authRepository,
specialCircumstanceManager = specialCircumstanceManager,
)
}

View file

@ -7,6 +7,7 @@ import com.bitwarden.core.SendView
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.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
@ -56,13 +57,15 @@ class AddSendViewModelTest : BaseViewModelTest() {
}
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
private val authRepository: AuthRepository = mockk {
every { specialCircumstance } returns null
every { specialCircumstance = null } just runs
every { userStateFlow } returns mutableUserStateFlow
}
private val environmentRepository: EnvironmentRepository = mockk {
every { environment } returns Environment.Us
}
private val specialCircumstanceManager: SpecialCircumstanceManager = mockk {
every { specialCircumstance } returns null
every { specialCircumstance = any() } just runs
}
private val mutableSendDataStateFlow = MutableStateFlow<DataState<SendView>>(DataState.Loading)
private val vaultRepository: VaultRepository = mockk {
every { getSendStateFlow(any()) } returns mutableSendDataStateFlow
@ -170,7 +173,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
assertEquals(initialState, viewModel.stateFlow.value)
coVerify(exactly = 1) {
vaultRepository.createSend(sendView = mockSendView, fileUri = null)
authRepository.specialCircumstance = null
specialCircumstanceManager.specialCircumstance = null
clipboardManager.setText(sendUrl)
}
}
@ -205,7 +208,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
assertEquals(initialState, viewModel.stateFlow.value)
coVerify(exactly = 1) {
vaultRepository.createSend(sendView = mockSendView, fileUri = null)
authRepository.specialCircumstance = null
specialCircumstanceManager.specialCircumstance = null
clipboardManager.setText(sendUrl)
}
}
@ -931,6 +934,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
private fun createViewModel(
state: AddSendState? = null,
addSendType: AddSendType = AddSendType.AddItem,
activityToken: String? = null,
): AddSendViewModel = AddSendViewModel(
savedStateHandle = SavedStateHandle().apply {
set("state", state?.copy(addSendType = addSendType))
@ -942,9 +946,11 @@ class AddSendViewModelTest : BaseViewModelTest() {
},
)
set("edit_send_id", (addSendType as? AddSendType.EditItem)?.sendItemId)
set("activityToken", activityToken)
},
authRepo = authRepository,
environmentRepo = environmentRepository,
specialCircumstanceManager = specialCircumstanceManager,
clock = clock,
clipboardManager = clipboardManager,
vaultRepo = vaultRepository,

View file

@ -1,7 +1,7 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addsend.util
import android.net.Uri
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendState
import io.mockk.mockk
@ -20,7 +20,7 @@ class SpecialCircumstanceExtensionsTest {
input = text,
isHideByDefaultChecked = false,
)
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
val specialCircumstance = SpecialCircumstance.ShareNewSend(
data = IntentManager.ShareData.TextSend(
subject = "",
text = text,
@ -44,7 +44,7 @@ class SpecialCircumstanceExtensionsTest {
sizeBytes = sizeBytes,
displaySize = null,
)
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
val specialCircumstance = SpecialCircumstance.ShareNewSend(
data = IntentManager.ShareData.FileSend(
fileData = IntentManager.FileData(
fileName = fileName,
@ -62,14 +62,14 @@ class SpecialCircumstanceExtensionsTest {
@Test
fun `toSendType with null SpecialCircumstance should return null`() {
val specialCircumstance: UserState.SpecialCircumstance? = null
val specialCircumstance: SpecialCircumstance? = null
assertNull(specialCircumstance.toSendType())
}
@Test
fun `toSendName with TextSend should return subject`() {
val subject = "Subject"
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
val specialCircumstance = SpecialCircumstance.ShareNewSend(
data = IntentManager.ShareData.TextSend(
subject = subject,
text = "",
@ -85,7 +85,7 @@ class SpecialCircumstanceExtensionsTest {
@Test
fun `toSendName with FileSend should return file name`() {
val fileName = "File Name"
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
val specialCircumstance = SpecialCircumstance.ShareNewSend(
data = IntentManager.ShareData.FileSend(
fileData = IntentManager.FileData(
fileName = fileName,
@ -103,14 +103,14 @@ class SpecialCircumstanceExtensionsTest {
@Test
fun `toSendName with null SpecialCircumstance should return null`() {
val specialCircumstance: UserState.SpecialCircumstance? = null
val specialCircumstance: SpecialCircumstance? = null
assertNull(specialCircumstance.toSendName())
}
@Suppress("MaxLineLength")
@Test
fun `shouldFinishOnComplete with ShareNewSend shouldFinishWhenComplete true should return true`() {
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
val specialCircumstance = SpecialCircumstance.ShareNewSend(
data = mockk(),
shouldFinishWhenComplete = true,
)
@ -120,7 +120,7 @@ class SpecialCircumstanceExtensionsTest {
@Suppress("MaxLineLength")
@Test
fun `shouldFinishOnComplete with ShareNewSend shouldFinishWhenComplete false should return false`() {
val specialCircumstance = UserState.SpecialCircumstance.ShareNewSend(
val specialCircumstance = SpecialCircumstance.ShareNewSend(
data = mockk(),
shouldFinishWhenComplete = false,
)
@ -129,7 +129,7 @@ class SpecialCircumstanceExtensionsTest {
@Test
fun `shouldFinishOnComplete with null SpecialCircumstance should return false`() {
val specialCircumstance: UserState.SpecialCircumstance? = null
val specialCircumstance: SpecialCircumstance? = null
assertFalse(specialCircumstance.shouldFinishOnComplete())
}
}