mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-999: Add UI for Vault Unlock screen (#243)
This commit is contained in:
parent
2f525513a2
commit
00e249dab2
8 changed files with 892 additions and 22 deletions
|
@ -12,19 +12,18 @@ import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||||
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||||
|
import com.x8bit.bitwarden.ui.platform.util.labelOrBaseUrlHost
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.net.URI
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val KEY_STATE = "state"
|
private const val KEY_STATE = "state"
|
||||||
|
@ -282,23 +281,3 @@ sealed class LoginAction {
|
||||||
) : Internal()
|
) : Internal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the [Environment.label] for non-custom values. Otherwise returns the host of the
|
|
||||||
* custom base URL.
|
|
||||||
*/
|
|
||||||
private val Environment.labelOrBaseUrlHost: Text
|
|
||||||
get() = when (this) {
|
|
||||||
is Environment.Us -> this.label
|
|
||||||
is Environment.Eu -> this.label
|
|
||||||
is Environment.SelfHosted -> {
|
|
||||||
// Grab the domain
|
|
||||||
// Ex:
|
|
||||||
// - "https://www.abc.com/path-1/path-1" -> "www.abc.com"
|
|
||||||
URI
|
|
||||||
.create(this.environmentUrlData.base)
|
|
||||||
.host
|
|
||||||
.orEmpty()
|
|
||||||
.asText()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.vaultunlock
|
||||||
|
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import com.x8bit.bitwarden.ui.platform.theme.TransitionProviders
|
||||||
|
|
||||||
|
private const val VAULT_UNLOCK: String = "vault_unlock"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the Vault Unlock screen.
|
||||||
|
*/
|
||||||
|
fun NavController.navigateToVaultUnlock(
|
||||||
|
navOptions: NavOptions? = null,
|
||||||
|
) {
|
||||||
|
navigate(VAULT_UNLOCK, navOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the Vault Unlock screen to the nav graph.
|
||||||
|
*/
|
||||||
|
fun NavGraphBuilder.vaultUnlockDestinations() {
|
||||||
|
composable(
|
||||||
|
route = VAULT_UNLOCK,
|
||||||
|
enterTransition = TransitionProviders.Enter.slideUp,
|
||||||
|
exitTransition = TransitionProviders.Exit.slideDown,
|
||||||
|
popEnterTransition = TransitionProviders.Enter.slideUp,
|
||||||
|
popExitTransition = TransitionProviders.Exit.slideDown,
|
||||||
|
) {
|
||||||
|
VaultUnlockScreen()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.vaultunlock
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountActionItem
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledButton
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top level composable for the Vault Unlock screen.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
@Composable
|
||||||
|
fun VaultUnlockScreen(
|
||||||
|
viewModel: VaultUnlockViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
|
val context = LocalContext.current
|
||||||
|
val resources = context.resources
|
||||||
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
|
when (event) {
|
||||||
|
is VaultUnlockEvent.ShowToast -> {
|
||||||
|
Toast.makeText(context, event.text(resources), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
VaultUnlockEvent.NavigateToLoginScreen -> {
|
||||||
|
// TODO: Handle adding accounts (BIT-853)
|
||||||
|
Toast.makeText(context, "Not yet implemented.", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var accountMenuVisible by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(
|
||||||
|
state = rememberTopAppBarState(),
|
||||||
|
canScroll = { !accountMenuVisible },
|
||||||
|
)
|
||||||
|
|
||||||
|
when (val dialog = state.dialog) {
|
||||||
|
is VaultUnlockState.VaultUnlockDialog.Error -> BitwardenBasicDialog(
|
||||||
|
visibilityState = BasicDialogState.Shown(
|
||||||
|
title = R.string.an_error_has_occurred.asText(),
|
||||||
|
message = dialog.message,
|
||||||
|
),
|
||||||
|
onDismissRequest = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(VaultUnlockAction.DismissDialog) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
VaultUnlockState.VaultUnlockDialog.Loading -> BitwardenLoadingDialog(
|
||||||
|
visibilityState = LoadingDialogState.Shown(R.string.loading.asText()),
|
||||||
|
)
|
||||||
|
|
||||||
|
null -> Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
BitwardenScaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
BitwardenTopAppBar(
|
||||||
|
title = stringResource(id = R.string.verify_master_password),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigationIcon = null,
|
||||||
|
actions = {
|
||||||
|
BitwardenAccountActionItem(
|
||||||
|
initials = state.initials,
|
||||||
|
color = state.avatarColor,
|
||||||
|
onClick = { accountMenuVisible = !accountMenuVisible },
|
||||||
|
)
|
||||||
|
BitwardenOverflowActionItem()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { innerPadding ->
|
||||||
|
Box {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
) {
|
||||||
|
BitwardenPasswordField(
|
||||||
|
label = stringResource(id = R.string.master_password),
|
||||||
|
value = state.passwordInput,
|
||||||
|
onValueChange = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(VaultUnlockAction.PasswordInputChanged(it)) }
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.vault_locked_master_password),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = R.string.logged_in_as_on,
|
||||||
|
state.email,
|
||||||
|
state.environmentUrl(),
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
BitwardenFilledButton(
|
||||||
|
label = stringResource(id = R.string.unlock),
|
||||||
|
onClick = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(VaultUnlockAction.UnlockClick) }
|
||||||
|
},
|
||||||
|
isEnabled = state.passwordInput.isNotEmpty(),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||||
|
}
|
||||||
|
|
||||||
|
BitwardenAccountSwitcher(
|
||||||
|
isVisible = accountMenuVisible,
|
||||||
|
accountSummaries = state.accountSummaries.toImmutableList(),
|
||||||
|
onAccountSummaryClick = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(VaultUnlockAction.SwitchAccountClick(it)) }
|
||||||
|
},
|
||||||
|
onAddAccountClick = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(VaultUnlockAction.AddAccountClick) }
|
||||||
|
},
|
||||||
|
onDismissRequest = { accountMenuVisible = false },
|
||||||
|
topAppBarScrollBehavior = scrollBehavior,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,236 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.vaultunlock
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.AccountSummary
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.hexToColor
|
||||||
|
import com.x8bit.bitwarden.ui.platform.util.labelOrBaseUrlHost
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val KEY_STATE = "state"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages application state for the initial vault unlock screen.
|
||||||
|
*/
|
||||||
|
@HiltViewModel
|
||||||
|
class VaultUnlockViewModel @Inject constructor(
|
||||||
|
private val savedStateHandle: SavedStateHandle,
|
||||||
|
private val vaultRepo: VaultRepository,
|
||||||
|
environmentRepo: EnvironmentRepository,
|
||||||
|
) : BaseViewModel<VaultUnlockState, VaultUnlockEvent, VaultUnlockAction>(
|
||||||
|
initialState = savedStateHandle[KEY_STATE] ?: VaultUnlockState(
|
||||||
|
accountSummaries = emptyList(),
|
||||||
|
avatarColorString = "0000FF",
|
||||||
|
initials = "BW",
|
||||||
|
email = "bit@bitwarden.com",
|
||||||
|
dialog = null,
|
||||||
|
environmentUrl = environmentRepo.environment.labelOrBaseUrlHost,
|
||||||
|
passwordInput = "",
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
stateFlow
|
||||||
|
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
environmentRepo
|
||||||
|
.environmentStateFlow
|
||||||
|
.onEach { environment ->
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(environmentUrl = environment.labelOrBaseUrlHost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleAction(action: VaultUnlockAction) {
|
||||||
|
when (action) {
|
||||||
|
VaultUnlockAction.AddAccountClick -> handleAddAccountClick()
|
||||||
|
VaultUnlockAction.DismissDialog -> handleDismissDialog()
|
||||||
|
is VaultUnlockAction.PasswordInputChanged -> handlePasswordInputChanged(action)
|
||||||
|
is VaultUnlockAction.SwitchAccountClick -> handleSwitchAccountClick(action)
|
||||||
|
VaultUnlockAction.UnlockClick -> handleUnlockClick()
|
||||||
|
is VaultUnlockAction.Internal.ReceiveVaultUnlockResult -> {
|
||||||
|
handleReceiveVaultUnlockResult(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleAddAccountClick() {
|
||||||
|
sendEvent(VaultUnlockEvent.NavigateToLoginScreen)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDismissDialog() {
|
||||||
|
mutableStateFlow.update { it.copy(dialog = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePasswordInputChanged(action: VaultUnlockAction.PasswordInputChanged) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(passwordInput = action.passwordInput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSwitchAccountClick(action: VaultUnlockAction.SwitchAccountClick) {
|
||||||
|
// TODO: Handle switching accounts (BIT-853)
|
||||||
|
sendEvent(VaultUnlockEvent.ShowToast("Not yet implemented.".asText()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUnlockClick() {
|
||||||
|
mutableStateFlow.update { it.copy(dialog = VaultUnlockState.VaultUnlockDialog.Loading) }
|
||||||
|
viewModelScope.launch {
|
||||||
|
val vaultUnlockResult = vaultRepo.unlockVaultAndSync(
|
||||||
|
mutableStateFlow.value.passwordInput,
|
||||||
|
)
|
||||||
|
sendAction(VaultUnlockAction.Internal.ReceiveVaultUnlockResult(vaultUnlockResult))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleReceiveVaultUnlockResult(
|
||||||
|
action: VaultUnlockAction.Internal.ReceiveVaultUnlockResult,
|
||||||
|
) {
|
||||||
|
when (action.vaultUnlockResult) {
|
||||||
|
VaultUnlockResult.AuthenticationError -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialog = VaultUnlockState.VaultUnlockDialog.Error(
|
||||||
|
R.string.invalid_master_password.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VaultUnlockResult.GenericError,
|
||||||
|
VaultUnlockResult.InvalidStateError,
|
||||||
|
-> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialog = VaultUnlockState.VaultUnlockDialog.Error(
|
||||||
|
R.string.generic_error_message.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VaultUnlockResult.Success -> {
|
||||||
|
mutableStateFlow.update { it.copy(dialog = null) }
|
||||||
|
// Don't do anything, we'll navigate to the right place.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models state of the vault unlock screen.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class VaultUnlockState(
|
||||||
|
val accountSummaries: List<AccountSummary>,
|
||||||
|
private val avatarColorString: String,
|
||||||
|
val initials: String,
|
||||||
|
val email: String,
|
||||||
|
val environmentUrl: Text,
|
||||||
|
val dialog: VaultUnlockDialog?,
|
||||||
|
val passwordInput: String,
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [Color] of the avatar.
|
||||||
|
*/
|
||||||
|
val avatarColor: Color get() = avatarColorString.hexToColor()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the various dialogs the vault unlock screen can display.
|
||||||
|
*/
|
||||||
|
sealed class VaultUnlockDialog : Parcelable {
|
||||||
|
/**
|
||||||
|
* Represents an error dialog.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Error(
|
||||||
|
val message: Text,
|
||||||
|
) : VaultUnlockDialog()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a loading state dialog.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data object Loading : VaultUnlockDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models events for the vault unlock screen.
|
||||||
|
*/
|
||||||
|
sealed class VaultUnlockEvent {
|
||||||
|
/**
|
||||||
|
* Navigates to the login flow.
|
||||||
|
*/
|
||||||
|
data object NavigateToLoginScreen : VaultUnlockEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a toast to the user.
|
||||||
|
*/
|
||||||
|
data class ShowToast(
|
||||||
|
val text: Text,
|
||||||
|
) : VaultUnlockEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models actions for the vault unlock screen.
|
||||||
|
*/
|
||||||
|
sealed class VaultUnlockAction {
|
||||||
|
/**
|
||||||
|
* The user has clicked the add account button.
|
||||||
|
*/
|
||||||
|
data object AddAccountClick : VaultUnlockAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user dismissed the currently displayed dialog.
|
||||||
|
*/
|
||||||
|
data object DismissDialog : VaultUnlockAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has modified the password input.
|
||||||
|
*/
|
||||||
|
data class PasswordInputChanged(
|
||||||
|
val passwordInput: String,
|
||||||
|
) : VaultUnlockAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has clicked the an account to switch too.
|
||||||
|
*/
|
||||||
|
data class SwitchAccountClick(
|
||||||
|
val accountSummary: AccountSummary,
|
||||||
|
) : VaultUnlockAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has clicked the unlock button.
|
||||||
|
*/
|
||||||
|
data object UnlockClick : VaultUnlockAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models actions that the [VaultUnlockViewModel] itself might send.
|
||||||
|
*/
|
||||||
|
sealed class Internal : VaultUnlockAction() {
|
||||||
|
/**
|
||||||
|
* Indicates a vault unlock result has been received.
|
||||||
|
*/
|
||||||
|
data class ReceiveVaultUnlockResult(
|
||||||
|
val vaultUnlockResult: VaultUnlockResult,
|
||||||
|
) : Internal()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.util
|
||||||
|
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the [Environment.label] for non-custom values. Otherwise returns the host of the
|
||||||
|
* custom base URL.
|
||||||
|
*/
|
||||||
|
val Environment.labelOrBaseUrlHost: Text
|
||||||
|
get() = when (this) {
|
||||||
|
is Environment.Us -> this.label
|
||||||
|
is Environment.Eu -> this.label
|
||||||
|
is Environment.SelfHosted -> {
|
||||||
|
// Grab the domain
|
||||||
|
// Ex:
|
||||||
|
// - "https://www.abc.com/path-1/path-1" -> "www.abc.com"
|
||||||
|
URI
|
||||||
|
.create(this.environmentUrlData.base)
|
||||||
|
.host
|
||||||
|
.orEmpty()
|
||||||
|
.asText()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.vaultunlock
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.assertIsEnabled
|
||||||
|
import androidx.compose.ui.test.assertIsNotEnabled
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.compose.ui.test.performScrollTo
|
||||||
|
import androidx.compose.ui.test.performTextInput
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.AccountSummary
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class VaultUnlockScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
|
private val mutableEventFlow = MutableSharedFlow<VaultUnlockEvent>(
|
||||||
|
extraBufferCapacity = Int.MAX_VALUE,
|
||||||
|
)
|
||||||
|
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||||
|
private val viewModel = mockk<VaultUnlockViewModel>(relaxed = true) {
|
||||||
|
every { eventFlow } returns mutableEventFlow
|
||||||
|
every { stateFlow } returns mutableStateFlow
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
VaultUnlockScreen(
|
||||||
|
viewModel = viewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `account icon click should show the account switcher`() {
|
||||||
|
composeTestRule.onNodeWithText("active@bitwarden.com").assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText("locked@bitwarden.com").assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText("Add account").assertDoesNotExist()
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText("AU").performClick()
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText("active@bitwarden.com").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithText("locked@bitwarden.com").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithText("Add account").assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `account click in the account switcher should send AccountSwitchClick and close switcher`() {
|
||||||
|
// Open the Account Switcher
|
||||||
|
composeTestRule.onNodeWithText("AU").performClick()
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText("locked@bitwarden.com").performClick()
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(VaultUnlockAction.SwitchAccountClick(LOCKED_ACCOUNT_SUMMARY))
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithText("locked@bitwarden.com").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `add account click in the account switcher should send AddAccountClick and close switcher`() {
|
||||||
|
// Open the Account Switcher
|
||||||
|
composeTestRule.onNodeWithText("AU").performClick()
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText("Add account").performClick()
|
||||||
|
verify { viewModel.trySendAction(VaultUnlockAction.AddAccountClick) }
|
||||||
|
composeTestRule.onNodeWithText("Add account").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `email state change should update logged in as text`() {
|
||||||
|
val newEmail = "david@bitwarden.com"
|
||||||
|
val textBeforeUpdate = "Logged in as ${DEFAULT_STATE.email} on $DEFAULT_ENVIRONMENT_URL."
|
||||||
|
val textAfterUpdate = "Logged in as $newEmail on $DEFAULT_ENVIRONMENT_URL."
|
||||||
|
composeTestRule.onNodeWithText(textBeforeUpdate).assertExists()
|
||||||
|
composeTestRule.onNodeWithText(textAfterUpdate).assertDoesNotExist()
|
||||||
|
mutableStateFlow.update { it.copy(email = newEmail) }
|
||||||
|
composeTestRule.onNodeWithText(textBeforeUpdate).assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText(textAfterUpdate).assertExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `environment url state change should update logged in as text`() {
|
||||||
|
val newEnvironmentUrl = "eu.bitwarden.com"
|
||||||
|
val textBeforeUpdate = "Logged in as ${DEFAULT_STATE.email} on $DEFAULT_ENVIRONMENT_URL."
|
||||||
|
val textAfterUpdate = "Logged in as ${DEFAULT_STATE.email} on $newEnvironmentUrl."
|
||||||
|
composeTestRule.onNodeWithText(textBeforeUpdate).assertExists()
|
||||||
|
composeTestRule.onNodeWithText(textAfterUpdate).assertDoesNotExist()
|
||||||
|
mutableStateFlow.update { it.copy(environmentUrl = newEnvironmentUrl.asText()) }
|
||||||
|
composeTestRule.onNodeWithText(textBeforeUpdate).assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText(textAfterUpdate).assertExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `password input state change should update unlock button enabled`() {
|
||||||
|
composeTestRule.onNodeWithText("Unlock").performScrollTo().assertIsNotEnabled()
|
||||||
|
mutableStateFlow.update { it.copy(passwordInput = "a") }
|
||||||
|
composeTestRule.onNodeWithText("Unlock").performScrollTo().assertIsEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unlock click should send UnlockClick action`() {
|
||||||
|
mutableStateFlow.update { it.copy(passwordInput = "abdc1234") }
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Unlock")
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
verify { viewModel.trySendAction(VaultUnlockAction.UnlockClick) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `master password change should send PasswordInputChanged action`() {
|
||||||
|
val input = "abcd1234"
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Master password")
|
||||||
|
.performScrollTo()
|
||||||
|
.performTextInput(input)
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(VaultUnlockAction.PasswordInputChanged(input))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val DEFAULT_ENVIRONMENT_URL: String = "vault.bitwarden.com"
|
||||||
|
|
||||||
|
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
|
||||||
|
userId = "activeUserId",
|
||||||
|
name = "Active User",
|
||||||
|
email = "active@bitwarden.com",
|
||||||
|
avatarColorHex = "#aa00aa",
|
||||||
|
status = AccountSummary.Status.ACTIVE,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val LOCKED_ACCOUNT_SUMMARY = AccountSummary(
|
||||||
|
userId = "lockedUserId",
|
||||||
|
name = "Locked User",
|
||||||
|
email = "locked@bitwarden.com",
|
||||||
|
avatarColorHex = "#00aaaa",
|
||||||
|
status = AccountSummary.Status.LOCKED,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val DEFAULT_STATE: VaultUnlockState = VaultUnlockState(
|
||||||
|
accountSummaries = persistentListOf(
|
||||||
|
ACTIVE_ACCOUNT_SUMMARY,
|
||||||
|
LOCKED_ACCOUNT_SUMMARY,
|
||||||
|
),
|
||||||
|
avatarColorString = "0000FF",
|
||||||
|
dialog = null,
|
||||||
|
email = "bit@bitwarden.com",
|
||||||
|
environmentUrl = DEFAULT_ENVIRONMENT_URL.asText(),
|
||||||
|
initials = "AU",
|
||||||
|
passwordInput = "",
|
||||||
|
)
|
|
@ -0,0 +1,207 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.vaultunlock
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.AccountSummary
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class VaultUnlockViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
|
private val environmentRepository = FakeEnvironmentRepository()
|
||||||
|
private val vaultRepository = mockk<VaultRepository>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state should be correct when not set`() {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state should be correct when set`() {
|
||||||
|
val state = DEFAULT_STATE.copy(
|
||||||
|
initials = "WB",
|
||||||
|
avatarColorString = "00FF00",
|
||||||
|
)
|
||||||
|
val viewModel = createViewModel(state = state)
|
||||||
|
assertEquals(state, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `environment url should update when environment repo emits an update`() {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||||
|
environmentRepository.environment = Environment.SelfHosted(
|
||||||
|
environmentUrlData = EnvironmentUrlDataJson(base = "https://vault.bitwarden.eu"),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(environmentUrl = "vault.bitwarden.eu".asText()),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on AddAccountClick should emit NavigateToLoginScreen`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(VaultUnlockAction.AddAccountClick)
|
||||||
|
assertEquals(VaultUnlockEvent.NavigateToLoginScreen, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on DismissDialog should clear the dialog state`() = runTest {
|
||||||
|
val initialState = DEFAULT_STATE.copy(dialog = VaultUnlockState.VaultUnlockDialog.Loading)
|
||||||
|
val viewModel = createViewModel(state = initialState)
|
||||||
|
viewModel.trySendAction(VaultUnlockAction.DismissDialog)
|
||||||
|
assertEquals(
|
||||||
|
initialState.copy(dialog = null),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on PasswordInputChanged should update the password input state`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val password = "abcd1234"
|
||||||
|
viewModel.trySendAction(VaultUnlockAction.PasswordInputChanged(passwordInput = password))
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(passwordInput = password),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on SwitchAccountClick should emit ShowToast`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
val accountSummary = mockk<AccountSummary> {
|
||||||
|
every { status } returns AccountSummary.Status.ACTIVE
|
||||||
|
}
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(VaultUnlockAction.SwitchAccountClick(accountSummary))
|
||||||
|
assertEquals(VaultUnlockEvent.ShowToast("Not yet implemented.".asText()), awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on UnlockClick should display error dialog on AuthenticationError`() = runTest {
|
||||||
|
val password = "abcd1234"
|
||||||
|
val initialState = DEFAULT_STATE.copy(passwordInput = password)
|
||||||
|
val viewModel = createViewModel(state = initialState)
|
||||||
|
coEvery {
|
||||||
|
vaultRepository.unlockVaultAndSync(password)
|
||||||
|
} returns VaultUnlockResult.AuthenticationError
|
||||||
|
|
||||||
|
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
|
||||||
|
assertEquals(
|
||||||
|
initialState.copy(
|
||||||
|
dialog = VaultUnlockState.VaultUnlockDialog.Error(
|
||||||
|
R.string.invalid_master_password.asText(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
coVerify {
|
||||||
|
vaultRepository.unlockVaultAndSync(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on UnlockClick should display error dialog on GenericError`() = runTest {
|
||||||
|
val password = "abcd1234"
|
||||||
|
val initialState = DEFAULT_STATE.copy(passwordInput = password)
|
||||||
|
val viewModel = createViewModel(state = initialState)
|
||||||
|
coEvery {
|
||||||
|
vaultRepository.unlockVaultAndSync(password)
|
||||||
|
} returns VaultUnlockResult.GenericError
|
||||||
|
|
||||||
|
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
|
||||||
|
assertEquals(
|
||||||
|
initialState.copy(
|
||||||
|
dialog = VaultUnlockState.VaultUnlockDialog.Error(
|
||||||
|
R.string.generic_error_message.asText(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
coVerify {
|
||||||
|
vaultRepository.unlockVaultAndSync(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on UnlockClick should display error dialog on InvalidStateError`() = runTest {
|
||||||
|
val password = "abcd1234"
|
||||||
|
val initialState = DEFAULT_STATE.copy(passwordInput = password)
|
||||||
|
val viewModel = createViewModel(state = initialState)
|
||||||
|
coEvery {
|
||||||
|
vaultRepository.unlockVaultAndSync(password)
|
||||||
|
} returns VaultUnlockResult.InvalidStateError
|
||||||
|
|
||||||
|
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
|
||||||
|
assertEquals(
|
||||||
|
initialState.copy(
|
||||||
|
dialog = VaultUnlockState.VaultUnlockDialog.Error(
|
||||||
|
R.string.generic_error_message.asText(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
coVerify {
|
||||||
|
vaultRepository.unlockVaultAndSync(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on UnlockClick should display clear dialog on success`() = runTest {
|
||||||
|
val password = "abcd1234"
|
||||||
|
val initialState = DEFAULT_STATE.copy(passwordInput = password)
|
||||||
|
val viewModel = createViewModel(state = initialState)
|
||||||
|
coEvery {
|
||||||
|
vaultRepository.unlockVaultAndSync(password)
|
||||||
|
} returns VaultUnlockResult.Success
|
||||||
|
|
||||||
|
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
|
||||||
|
assertEquals(
|
||||||
|
initialState.copy(dialog = null),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
coVerify {
|
||||||
|
vaultRepository.unlockVaultAndSync(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createViewModel(
|
||||||
|
state: VaultUnlockState? = DEFAULT_STATE,
|
||||||
|
environmentRepo: EnvironmentRepository = environmentRepository,
|
||||||
|
vaultRepo: VaultRepository = vaultRepository,
|
||||||
|
): VaultUnlockViewModel = VaultUnlockViewModel(
|
||||||
|
savedStateHandle = SavedStateHandle().apply { set("state", state) },
|
||||||
|
vaultRepo = vaultRepo,
|
||||||
|
environmentRepo = environmentRepo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DEFAULT_STATE: VaultUnlockState = VaultUnlockState(
|
||||||
|
accountSummaries = emptyList(),
|
||||||
|
avatarColorString = "0000FF",
|
||||||
|
email = "bit@bitwarden.com",
|
||||||
|
initials = "BW",
|
||||||
|
dialog = null,
|
||||||
|
environmentUrl = Environment.Us.label,
|
||||||
|
passwordInput = "",
|
||||||
|
)
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.util
|
||||||
|
|
||||||
|
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class EnvironmentExtensionsTest {
|
||||||
|
@Test
|
||||||
|
fun `labelOrBaseUrlHost should correctly convert US environment to the correct label`() {
|
||||||
|
val environment = Environment.Us
|
||||||
|
assertEquals(
|
||||||
|
environment.label,
|
||||||
|
environment.labelOrBaseUrlHost,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `labelOrBaseUrlHost should correctly convert EU environment to the correct label`() {
|
||||||
|
val environment = Environment.Eu
|
||||||
|
assertEquals(
|
||||||
|
environment.label,
|
||||||
|
environment.labelOrBaseUrlHost,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `labelOrBaseUrlHost should correctly convert self hosted environment to the correct label`() {
|
||||||
|
val environment = Environment.SelfHosted(
|
||||||
|
environmentUrlData = EnvironmentUrlDataJson(base = "https://vault.bitwarden.com"),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"vault.bitwarden.com".asText(),
|
||||||
|
environment.labelOrBaseUrlHost,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue