mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
PM-11154: Create basic Remove Master Password UI (#3782)
This commit is contained in:
parent
88b674f54c
commit
eac5516a94
20 changed files with 618 additions and 2 deletions
|
@ -1,12 +1,18 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
|
||||
/**
|
||||
* Represents an organization a user may be a member of.
|
||||
*
|
||||
* @property id The ID of the organization.
|
||||
* @property name The name of the organization (if applicable).
|
||||
* @property shouldUseKeyConnector Indicates that the organization uses a key connector.
|
||||
* @property role The user's role in the organization.
|
||||
*/
|
||||
data class Organization(
|
||||
val id: String,
|
||||
val name: String?,
|
||||
val shouldUseKeyConnector: Boolean,
|
||||
val role: OrganizationType,
|
||||
)
|
||||
|
|
|
@ -14,6 +14,8 @@ fun SyncResponseJson.Profile.Organization.toOrganization(): Organization =
|
|||
Organization(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
shouldUseKeyConnector = this.shouldUseKeyConnector,
|
||||
role = this.type,
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
@ -246,6 +246,9 @@ data class SyncResponseJson(
|
|||
@SerialName("usePolicies")
|
||||
val shouldUsePolicies: Boolean,
|
||||
|
||||
@SerialName("useKeyConnector")
|
||||
val shouldUseKeyConnector: Boolean,
|
||||
|
||||
@SerialName("keyConnectorUrl")
|
||||
val keyConnectorUrl: String?,
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.removepassword
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
|
||||
/**
|
||||
* The route for navigating to the [RemovePasswordScreen].
|
||||
*/
|
||||
const val REMOVE_PASSWORD_ROUTE: String = "remove_password"
|
||||
|
||||
/**
|
||||
* Add the Remove Password screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.removePasswordDestination() {
|
||||
composable(
|
||||
route = REMOVE_PASSWORD_ROUTE,
|
||||
) {
|
||||
RemovePasswordScreen()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the Remove Password screen.
|
||||
*/
|
||||
fun NavController.navigateToRemovePassword(
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
this.navigate(REMOVE_PASSWORD_ROUTE, navOptions)
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.removepassword
|
||||
|
||||
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.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* The top level composable for the Remove Password screen.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RemovePasswordScreen(
|
||||
viewModel: RemovePasswordViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
RemovePasswordDialogs(
|
||||
dialogState = state.dialogState,
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(RemovePasswordAction.DialogDismiss) }
|
||||
},
|
||||
)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.remove_master_password),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = null,
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
RemovePasswordScreenContent(
|
||||
state = state,
|
||||
onContinueClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(RemovePasswordAction.ContinueClick) }
|
||||
},
|
||||
onInputChanged = remember(viewModel) {
|
||||
{ viewModel.trySendAction(RemovePasswordAction.InputChanged(it)) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RemovePasswordScreenContent(
|
||||
state: RemovePasswordState,
|
||||
onContinueClick: () -> Unit,
|
||||
onInputChanged: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = state.description(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.master_password),
|
||||
value = state.input,
|
||||
onValueChange = onInputChanged,
|
||||
showPasswordTestTag = "PasswordVisibilityToggle",
|
||||
modifier = Modifier
|
||||
.testTag(tag = "MasterPasswordEntry")
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
autoFocus = true,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.continue_text),
|
||||
onClick = onContinueClick,
|
||||
isEnabled = state.input.isNotEmpty(),
|
||||
modifier = Modifier
|
||||
.testTag(tag = "ContinueButton")
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RemovePasswordDialogs(
|
||||
dialogState: RemovePasswordState.DialogState?,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
when (dialogState) {
|
||||
is RemovePasswordState.DialogState.Error -> {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = dialogState.title,
|
||||
message = dialogState.message,
|
||||
),
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun RemovePasswordScreen_preview() {
|
||||
BitwardenTheme {
|
||||
RemovePasswordScreenContent(
|
||||
state = RemovePasswordState(
|
||||
input = "",
|
||||
description = "Organization is using SSO with a self-hosted key server.".asText(),
|
||||
dialogState = null,
|
||||
),
|
||||
onContinueClick = { },
|
||||
onInputChanged = { },
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.removepassword
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
||||
/**
|
||||
* Manages application state for the Set Password screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class RemovePasswordViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<RemovePasswordState, Unit, RemovePasswordAction>(
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
val orgName = authRepository.userStateFlow.value
|
||||
?.activeAccount
|
||||
?.organizations
|
||||
?.firstOrNull { it.shouldUseKeyConnector }
|
||||
?.name
|
||||
.orEmpty()
|
||||
RemovePasswordState(
|
||||
input = "",
|
||||
description = R.string
|
||||
.organization_is_using_sso_with_a_self_hosted_key_server
|
||||
.asText(orgName),
|
||||
dialogState = null,
|
||||
)
|
||||
},
|
||||
) {
|
||||
override fun handleAction(action: RemovePasswordAction) {
|
||||
when (action) {
|
||||
RemovePasswordAction.ContinueClick -> handleContinueClick()
|
||||
is RemovePasswordAction.InputChanged -> handleInputChanged(action)
|
||||
RemovePasswordAction.DialogDismiss -> handleDialogDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleContinueClick() {
|
||||
// TODO: Process removing the password (PM-11155)
|
||||
}
|
||||
|
||||
private fun handleInputChanged(action: RemovePasswordAction.InputChanged) {
|
||||
mutableStateFlow.update { it.copy(input = action.input) }
|
||||
}
|
||||
|
||||
private fun handleDialogDismiss() {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models state of the Remove Password screen.
|
||||
*/
|
||||
@Parcelize
|
||||
data class RemovePasswordState(
|
||||
val input: String,
|
||||
val description: Text,
|
||||
val dialogState: DialogState?,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Represents the current state of any dialogs on the screen.
|
||||
*/
|
||||
sealed class DialogState : Parcelable {
|
||||
/**
|
||||
* Represents an error dialog with the given [message] and optional [title]. If no title
|
||||
* is specified a default will be provided.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Error(
|
||||
val title: Text? = null,
|
||||
val message: Text,
|
||||
) : DialogState()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models actions for the Remove Password screen.
|
||||
*/
|
||||
sealed class RemovePasswordAction {
|
||||
/**
|
||||
* Indicates that the user has clicked the continue button
|
||||
*/
|
||||
data object ContinueClick : RemovePasswordAction()
|
||||
|
||||
/**
|
||||
* The user has modified the input.
|
||||
*/
|
||||
data class InputChanged(
|
||||
val input: String,
|
||||
) : RemovePasswordAction()
|
||||
|
||||
/**
|
||||
* Indicates that the dialog has been dismissed.
|
||||
*/
|
||||
data object DialogDismiss : RemovePasswordAction()
|
||||
}
|
|
@ -615,6 +615,7 @@ Scanning will happen automatically.</string>
|
|||
<string name="updating_password">Updating password</string>
|
||||
<string name="update_password_error">Currently unable to update password</string>
|
||||
<string name="remove_master_password">Remove master password</string>
|
||||
<string name="organization_is_using_sso_with_a_self_hosted_key_server">%1$s is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.</string>
|
||||
<string name="remove_master_password_warning">%1$s is using SSO with customer-managed encryption. Continuing will remove your master password from your account and require SSO to login.</string>
|
||||
<string name="remove_master_password_warning2">If you do not want to remove your master password, you may leave this organization.</string>
|
||||
<string name="leave_organization">Leave organization</string>
|
||||
|
|
|
@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserSwitchingData
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
@ -187,6 +188,8 @@ class AuthDiskSourceExtensionsTest {
|
|||
Organization(
|
||||
id = "mockId-1",
|
||||
name = "mockName-1",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -196,6 +199,8 @@ class AuthDiskSourceExtensionsTest {
|
|||
Organization(
|
||||
id = "mockId-2",
|
||||
name = "mockName-2",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -205,6 +210,8 @@ class AuthDiskSourceExtensionsTest {
|
|||
Organization(
|
||||
id = "mockId-3",
|
||||
name = "mockName-3",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -241,6 +248,8 @@ class AuthDiskSourceExtensionsTest {
|
|||
Organization(
|
||||
id = "mockId-1",
|
||||
name = "mockName-1",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -269,6 +278,8 @@ class AuthDiskSourceExtensionsTest {
|
|||
Organization(
|
||||
id = "mockId-1",
|
||||
name = "mockName-1",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -278,6 +289,8 @@ class AuthDiskSourceExtensionsTest {
|
|||
Organization(
|
||||
id = "mockId-2",
|
||||
name = "mockName-2",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.repository.util
|
|||
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy
|
||||
|
@ -19,6 +20,8 @@ class SyncResponseJsonExtensionsTest {
|
|||
Organization(
|
||||
id = "mockId-1",
|
||||
name = "mockName-1",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
createMockOrganization(number = 1).toOrganization(),
|
||||
)
|
||||
|
@ -31,15 +34,19 @@ class SyncResponseJsonExtensionsTest {
|
|||
Organization(
|
||||
id = "mockId-1",
|
||||
name = "mockName-1",
|
||||
shouldUseKeyConnector = true,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
Organization(
|
||||
id = "mockId-2",
|
||||
name = "mockName-2",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.USER,
|
||||
),
|
||||
),
|
||||
listOf(
|
||||
createMockOrganization(number = 1),
|
||||
createMockOrganization(number = 2),
|
||||
createMockOrganization(number = 1).copy(shouldUseKeyConnector = true),
|
||||
createMockOrganization(number = 2).copy(type = OrganizationType.USER),
|
||||
)
|
||||
.toOrganizations(),
|
||||
)
|
||||
|
|
|
@ -15,6 +15,7 @@ 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.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
@ -229,6 +230,8 @@ class UserStateJsonExtensionsTest {
|
|||
Organization(
|
||||
id = "organizationId",
|
||||
name = "organizationName",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = false,
|
||||
|
@ -287,6 +290,8 @@ class UserStateJsonExtensionsTest {
|
|||
Organization(
|
||||
id = "organizationId",
|
||||
name = "organizationName",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -320,6 +325,8 @@ class UserStateJsonExtensionsTest {
|
|||
Organization(
|
||||
id = "organizationId",
|
||||
name = "organizationName",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = true,
|
||||
|
@ -374,6 +381,8 @@ class UserStateJsonExtensionsTest {
|
|||
Organization(
|
||||
id = "organizationId",
|
||||
name = "organizationName",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -408,6 +417,8 @@ class UserStateJsonExtensionsTest {
|
|||
Organization(
|
||||
id = "organizationId",
|
||||
name = "organizationName",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = false,
|
||||
|
@ -470,6 +481,8 @@ class UserStateJsonExtensionsTest {
|
|||
Organization(
|
||||
id = "organizationId",
|
||||
name = "organizationName",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -37,6 +37,7 @@ fun createMockOrganization(
|
|||
): SyncResponseJson.Profile.Organization =
|
||||
SyncResponseJson.Profile.Organization(
|
||||
shouldUsePolicies = shouldUsePolicies,
|
||||
shouldUseKeyConnector = false,
|
||||
keyConnectorUrl = "mockKeyConnectorUrl-$number",
|
||||
type = OrganizationType.ADMIN,
|
||||
seats = 1,
|
||||
|
|
|
@ -58,6 +58,7 @@ private const val SYNC_SUCCESS_JSON = """
|
|||
"organizations": [
|
||||
{
|
||||
"usePolicies": false,
|
||||
"useKeyConnector": false,
|
||||
"keyConnectorUrl": "mockKeyConnectorUrl-1",
|
||||
"type": 1,
|
||||
"seats": 1,
|
||||
|
@ -132,6 +133,7 @@ private const val SYNC_SUCCESS_JSON = """
|
|||
"providerOrganizations": [
|
||||
{
|
||||
"usePolicies": false,
|
||||
"useKeyConnector": false,
|
||||
"keyConnectorUrl": "mockKeyConnectorUrl-1",
|
||||
"type": 1,
|
||||
"seats": 1,
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.removepassword
|
||||
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.isDisplayed
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class RemovePasswordScreenTest : BaseComposeTest() {
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
val viewModel = mockk<RemovePasswordViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns bufferedMutableSharedFlow()
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
composeTestRule.setContent {
|
||||
RemovePasswordScreen(
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dialog should update according to state`() {
|
||||
val errorTitle = "message title"
|
||||
val errorMessage = "Error message"
|
||||
composeTestRule.assertNoDialogExists()
|
||||
composeTestRule.onNodeWithText(text = errorTitle).assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText(text = errorMessage).assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = RemovePasswordState.DialogState.Error(
|
||||
title = errorTitle.asText(),
|
||||
message = errorMessage.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = errorTitle)
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.isDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText(text = errorMessage)
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.isDisplayed()
|
||||
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
|
||||
composeTestRule.onNode(isDialog()).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `description should update according to state`() {
|
||||
val description = "description"
|
||||
composeTestRule.onNodeWithText(text = description).assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update { it.copy(description = description.asText()) }
|
||||
|
||||
composeTestRule.onNodeWithText(text = description).isDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `continue button should update according to state`() {
|
||||
composeTestRule.onNodeWithText(text = "Continue").performScrollTo().assertIsNotEnabled()
|
||||
mutableStateFlow.update { it.copy(input = "a") }
|
||||
composeTestRule.onNodeWithText(text = "Continue").performScrollTo().assertIsEnabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `continue button click should emit ContinueClick`() {
|
||||
mutableStateFlow.update { it.copy(input = "a") }
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Continue")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(RemovePasswordAction.ContinueClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE = RemovePasswordState(
|
||||
input = "",
|
||||
dialogState = null,
|
||||
description = "My org".asText(),
|
||||
)
|
|
@ -0,0 +1,106 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.removepassword
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class RemovePasswordViewModelTest : BaseViewModelTest() {
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
|
||||
private val authRepository: AuthRepository = mockk {
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueClick calls does nothing`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(RemovePasswordAction.ContinueClick)
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `InputChanged updates the state`() {
|
||||
val input = "123"
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(RemovePasswordAction.InputChanged(input = input))
|
||||
assertEquals(DEFAULT_STATE.copy(input = input), viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DialogDismiss calls clears the dialog state`() = runTest {
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
dialogState = RemovePasswordState.DialogState.Error(
|
||||
title = "title".asText(),
|
||||
message = "message".asText(),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel(initialState)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(initialState, awaitItem())
|
||||
viewModel.trySendAction(RemovePasswordAction.DialogDismiss)
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
state: RemovePasswordState? = null,
|
||||
): RemovePasswordViewModel =
|
||||
RemovePasswordViewModel(
|
||||
authRepository = authRepository,
|
||||
savedStateHandle = SavedStateHandle(mapOf("state" to state)),
|
||||
)
|
||||
}
|
||||
|
||||
private const val ORGANIZATION_NAME: String = "My org"
|
||||
private val DEFAULT_STATE = RemovePasswordState(
|
||||
input = "",
|
||||
dialogState = null,
|
||||
description = R.string
|
||||
.organization_is_using_sso_with_a_self_hosted_key_server
|
||||
.asText(ORGANIZATION_NAME),
|
||||
)
|
||||
|
||||
private const val USER_ID: String = "user_id"
|
||||
private val DEFAULT_ACCOUNT = UserState.Account(
|
||||
userId = USER_ID,
|
||||
name = "Active User",
|
||||
email = "active@bitwarden.com",
|
||||
environment = Environment.Us,
|
||||
avatarColorHex = "#aa00aa",
|
||||
isPremium = true,
|
||||
isLoggedIn = true,
|
||||
isVaultUnlocked = true,
|
||||
needsPasswordReset = false,
|
||||
isBiometricsEnabled = false,
|
||||
organizations = listOf(
|
||||
Organization(
|
||||
id = "orgId",
|
||||
name = ORGANIZATION_NAME,
|
||||
shouldUseKeyConnector = true,
|
||||
role = OrganizationType.USER,
|
||||
),
|
||||
),
|
||||
needsMasterPassword = false,
|
||||
trustedDevice = null,
|
||||
hasMasterPassword = true,
|
||||
)
|
||||
|
||||
private val DEFAULT_USER_STATE = UserState(
|
||||
activeUserId = USER_ID,
|
||||
accounts = listOf(DEFAULT_ACCOUNT),
|
||||
)
|
|
@ -37,6 +37,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
|||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.util.FakeGeneratorRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
|
@ -3878,6 +3879,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
Organization(
|
||||
id = "organizationId",
|
||||
name = "organizationName",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = true,
|
||||
|
|
|
@ -17,6 +17,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
|
||||
|
@ -434,6 +435,8 @@ class CipherViewExtensionsTest {
|
|||
Organization(
|
||||
id = "mockOrganizationId-1",
|
||||
name = "organizationName",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
isBiometricsEnabled = true,
|
||||
|
|
|
@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
|
@ -490,14 +491,20 @@ private val DEFAULT_USER_STATE = UserState(
|
|||
Organization(
|
||||
id = "mockOrganizationId-1",
|
||||
name = "mockOrganizationName-1",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
Organization(
|
||||
id = "mockOrganizationId-2",
|
||||
name = "mockOrganizationName-2",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
Organization(
|
||||
id = "mockOrganizationId-3",
|
||||
name = "mockOrganizationName-3",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.x8bit.bitwarden.R
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
|
@ -102,14 +103,20 @@ private fun createMockUserState(hasOrganizations: Boolean = true): UserState =
|
|||
Organization(
|
||||
id = "mockOrganizationId-1",
|
||||
name = "mockOrganizationName-1",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
Organization(
|
||||
id = "mockOrganizationId-2",
|
||||
name = "mockOrganizationName-2",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
Organization(
|
||||
id = "mockOrganizationId-3",
|
||||
name = "mockOrganizationName-3",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
|
|
|
@ -15,6 +15,7 @@ import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
|||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
|
@ -187,6 +188,8 @@ class VaultViewModelTest : BaseViewModelTest() {
|
|||
Organization(
|
||||
id = "organiationId",
|
||||
name = "Test Organization",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
|
@ -267,6 +270,8 @@ class VaultViewModelTest : BaseViewModelTest() {
|
|||
Organization(
|
||||
id = "organizationId",
|
||||
name = "Test Organization",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
|
@ -471,6 +476,8 @@ class VaultViewModelTest : BaseViewModelTest() {
|
|||
Organization(
|
||||
id = "testOrganizationId",
|
||||
name = "Test Organization",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJso
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
|
@ -76,6 +77,8 @@ class UserStateExtensionsTest {
|
|||
Organization(
|
||||
id = "organizationId",
|
||||
name = "organizationName",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
|
@ -97,6 +100,8 @@ class UserStateExtensionsTest {
|
|||
Organization(
|
||||
id = "organizationId",
|
||||
name = "organizationName",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
|
@ -122,6 +127,8 @@ class UserStateExtensionsTest {
|
|||
Organization(
|
||||
id = "organizationId",
|
||||
name = "organizationName",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
|
@ -147,6 +154,8 @@ class UserStateExtensionsTest {
|
|||
Organization(
|
||||
id = "organizationId",
|
||||
name = "organizationName",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
|
@ -187,6 +196,8 @@ class UserStateExtensionsTest {
|
|||
Organization(
|
||||
id = "organizationId",
|
||||
name = "organizationName",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
|
@ -225,6 +236,8 @@ class UserStateExtensionsTest {
|
|||
Organization(
|
||||
id = "organizationId",
|
||||
name = "organizationName",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
|
@ -267,6 +280,8 @@ class UserStateExtensionsTest {
|
|||
Organization(
|
||||
id = "organizationId",
|
||||
name = "organizationName",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
|
@ -336,10 +351,14 @@ class UserStateExtensionsTest {
|
|||
Organization(
|
||||
id = "organizationId-B",
|
||||
name = "Organization B",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
Organization(
|
||||
id = "organizationId-A",
|
||||
name = "Organization A",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
|
@ -385,10 +404,14 @@ class UserStateExtensionsTest {
|
|||
Organization(
|
||||
id = "organizationId-B",
|
||||
name = "Organization B",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
Organization(
|
||||
id = "organizationId-A",
|
||||
name = "Organization A",
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
trustedDevice = null,
|
||||
|
|
Loading…
Reference in a new issue