mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
BIT-1729: Update generator policy UI in realtime (#990)
This commit is contained in:
parent
5a6be93644
commit
a7b58e9c19
4 changed files with 219 additions and 7 deletions
|
@ -4,12 +4,33 @@ import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
|||
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* Get a list of active policies with the data decoded to the specified type.
|
||||
*/
|
||||
inline fun <reified T : PolicyInformation> PolicyManager.getActivePolicies(): List<T> {
|
||||
val type = when (T::class.java) {
|
||||
inline fun <reified T : PolicyInformation> PolicyManager.getActivePolicies(): List<T> =
|
||||
this
|
||||
.getActivePolicies(type = getPolicyTypeJson<T>())
|
||||
.mapNotNull { it.policyInformation as? T }
|
||||
|
||||
/**
|
||||
* Gets a flow of all the active policies with the data decoded to the specified type.
|
||||
*/
|
||||
inline fun <reified T : PolicyInformation> PolicyManager.getActivePoliciesFlow(): Flow<List<T>> =
|
||||
this
|
||||
.getActivePoliciesFlow(type = getPolicyTypeJson<T>())
|
||||
.map { policies ->
|
||||
policies.mapNotNull { policy -> policy.policyInformation as? T }
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for mapping a specific [PolicyInformation] type to its [PolicyTypeJson]
|
||||
* counterpart.
|
||||
*/
|
||||
inline fun <reified T : PolicyInformation> getPolicyTypeJson(): PolicyTypeJson =
|
||||
when (T::class.java) {
|
||||
PolicyInformation.MasterPassword::class.java -> PolicyTypeJson.MASTER_PASSWORD
|
||||
PolicyInformation.PasswordGenerator::class.java -> PolicyTypeJson.PASSWORD_GENERATOR
|
||||
PolicyInformation.SendOptions::class.java -> PolicyTypeJson.SEND_OPTIONS
|
||||
|
@ -18,11 +39,7 @@ inline fun <reified T : PolicyInformation> PolicyManager.getActivePolicies(): Li
|
|||
else -> {
|
||||
throw IllegalStateException(
|
||||
"Looks like you are missing a branch in your when statement. Update " +
|
||||
"getActivePolicies() to handle all PolicyInformation implementations.",
|
||||
"getPolicyTypeJson() to handle all PolicyInformation implementations.",
|
||||
)
|
||||
}
|
||||
}
|
||||
return this
|
||||
.getActivePolicies(type = type)
|
||||
.mapNotNull { it.policyInformation as? T }
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
|||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.getActivePoliciesFlow
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedCatchAllUsernameResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedForwardedServiceUsernameResult
|
||||
|
@ -49,6 +50,7 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.util.toUsernameGeneratorRe
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -103,6 +105,11 @@ class GeneratorViewModel @Inject constructor(
|
|||
is Passcode -> loadPasscodeOptions(selectedType, usePolicyDefault = true)
|
||||
is Username -> loadUsernameOptions(selectedType)
|
||||
}
|
||||
policyManager
|
||||
.getActivePoliciesFlow<PolicyInformation.PasswordGenerator>()
|
||||
.map { GeneratorAction.Internal.PasswordGeneratorPolicyReceive(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: GeneratorAction) {
|
||||
|
@ -204,6 +211,10 @@ class GeneratorViewModel @Inject constructor(
|
|||
is GeneratorAction.Internal.UpdateGeneratedForwardedServiceUsernameResult -> {
|
||||
handleUpdateForwardedServiceGeneratedUsernameResult(action)
|
||||
}
|
||||
|
||||
is GeneratorAction.Internal.PasswordGeneratorPolicyReceive -> {
|
||||
handlePasswordGeneratorPolicyReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -637,6 +648,12 @@ class GeneratorViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handlePasswordGeneratorPolicyReceive(
|
||||
action: GeneratorAction.Internal.PasswordGeneratorPolicyReceive,
|
||||
) {
|
||||
mutableStateFlow.update { it.copy(isUnderPolicy = action.policies.any()) }
|
||||
}
|
||||
|
||||
//endregion Generated Field Handlers
|
||||
|
||||
//region Main Type Option Handlers
|
||||
|
@ -2251,6 +2268,13 @@ sealed class GeneratorAction {
|
|||
* Models actions that the [GeneratorViewModel] itself might send.
|
||||
*/
|
||||
sealed class Internal : GeneratorAction() {
|
||||
/**
|
||||
* Indicates that updated policies have been received.
|
||||
*/
|
||||
data class PasswordGeneratorPolicyReceive(
|
||||
val policies: List<PolicyInformation.PasswordGenerator>,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates a generated text update is received.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
package com.x8bit.bitwarden.data.platform.manager.util
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class PolicyManagerExtensionsTest {
|
||||
private val mutablePolicyFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Policy>>()
|
||||
private val policyManager: PolicyManager = mockk {
|
||||
every { getActivePoliciesFlow(any()) } returns mutablePolicyFlow
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getActivePolicies should filter out undesired policies`() {
|
||||
every {
|
||||
policyManager.getActivePolicies(any())
|
||||
} returns listOf(MASTER_PASSWORD_POLICY, PASSWORD_GENERATOR_POLICY)
|
||||
|
||||
val result = policyManager.getActivePolicies<PolicyInformation.MasterPassword>()
|
||||
|
||||
assertEquals(listOf(MASTER_PASSWORD_POLICY_INFO), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getActivePoliciesFlow should filter out undesired policies when policyManager emits`() =
|
||||
runTest {
|
||||
policyManager.getActivePoliciesFlow<PolicyInformation.MasterPassword>().test {
|
||||
mutablePolicyFlow.tryEmit(listOf(PASSWORD_GENERATOR_POLICY))
|
||||
assertEquals(emptyList<PolicyInformation.MasterPassword>(), awaitItem())
|
||||
mutablePolicyFlow.tryEmit(
|
||||
listOf(
|
||||
MASTER_PASSWORD_POLICY,
|
||||
PASSWORD_GENERATOR_POLICY,
|
||||
),
|
||||
)
|
||||
assertEquals(listOf(MASTER_PASSWORD_POLICY_INFO), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPolicyTypeJson with MasterPassword should map to appropriate PolicyTypeJson`() {
|
||||
assertEquals(
|
||||
PolicyTypeJson.MASTER_PASSWORD,
|
||||
getPolicyTypeJson<PolicyInformation.MasterPassword>(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPolicyTypeJson with PasswordGenerator should map to appropriate PolicyTypeJson`() {
|
||||
assertEquals(
|
||||
PolicyTypeJson.PASSWORD_GENERATOR,
|
||||
getPolicyTypeJson<PolicyInformation.PasswordGenerator>(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPolicyTypeJson with SendOptions should map to appropriate PolicyTypeJson`() {
|
||||
assertEquals(
|
||||
PolicyTypeJson.SEND_OPTIONS,
|
||||
getPolicyTypeJson<PolicyInformation.SendOptions>(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPolicyTypeJson with VaultTimeout should map to appropriate PolicyTypeJson`() {
|
||||
assertEquals(
|
||||
PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT,
|
||||
getPolicyTypeJson<PolicyInformation.VaultTimeout>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val MASTER_PASSWORD_POLICY = SyncResponseJson.Policy(
|
||||
organizationId = "organizationId",
|
||||
id = "master_password_id",
|
||||
type = PolicyTypeJson.MASTER_PASSWORD,
|
||||
isEnabled = true,
|
||||
data = JsonObject(
|
||||
mapOf(
|
||||
"minLength" to JsonPrimitive(10),
|
||||
"minComplexity" to JsonPrimitive(10),
|
||||
"requireUpper" to JsonPrimitive(false),
|
||||
"requireLower" to JsonPrimitive(true),
|
||||
"requireNumbers" to JsonNull,
|
||||
"requireSpecial" to JsonPrimitive(false),
|
||||
"enforceOnLogin" to JsonPrimitive(true),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val MASTER_PASSWORD_POLICY_INFO = PolicyInformation.MasterPassword(
|
||||
minLength = 10,
|
||||
minComplexity = 10,
|
||||
requireUpper = false,
|
||||
requireLower = true,
|
||||
requireNumbers = null,
|
||||
requireSpecial = false,
|
||||
enforceOnLogin = true,
|
||||
)
|
||||
|
||||
private val PASSWORD_GENERATOR_POLICY = SyncResponseJson.Policy(
|
||||
organizationId = "organizationId",
|
||||
id = "password_generator_id",
|
||||
type = PolicyTypeJson.PASSWORD_GENERATOR,
|
||||
isEnabled = true,
|
||||
data = JsonObject(
|
||||
mapOf(
|
||||
"defaultType" to JsonNull,
|
||||
"minLength" to JsonPrimitive(10),
|
||||
"useUpper" to JsonPrimitive(true),
|
||||
"useNumbers" to JsonPrimitive(true),
|
||||
"useSpecial" to JsonPrimitive(true),
|
||||
"minNumbers" to JsonPrimitive(3),
|
||||
"minSpecial" to JsonPrimitive(3),
|
||||
"minNumberWords" to JsonPrimitive(5),
|
||||
"capitalize" to JsonPrimitive(true),
|
||||
"includeNumber" to JsonPrimitive(true),
|
||||
"useLower" to JsonPrimitive(true),
|
||||
),
|
||||
),
|
||||
)
|
|
@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
|||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
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.model.GeneratedCatchAllUsernameResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedForwardedServiceUsernameResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassphraseResult
|
||||
|
@ -19,6 +20,7 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
|
|||
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.util.FakeGeneratorRepository
|
||||
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.network.model.createMockPolicy
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
|
@ -91,8 +93,10 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
private val mutablePolicyFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Policy>>()
|
||||
private val policyManager: PolicyManager = mockk {
|
||||
every { getActivePolicies(PolicyTypeJson.PASSWORD_GENERATOR) } returns emptyList()
|
||||
every { getActivePoliciesFlow(PolicyTypeJson.PASSWORD_GENERATOR) } returns mutablePolicyFlow
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -107,6 +111,41 @@ class GeneratorViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(initialPasscodeState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `activePolicyFlow changes should update state`() = runTest {
|
||||
val payload = mapOf(
|
||||
"defaultType" to JsonNull,
|
||||
"minLength" to JsonPrimitive(10),
|
||||
"useUpper" to JsonPrimitive(true),
|
||||
"useNumbers" to JsonPrimitive(true),
|
||||
"useSpecial" to JsonPrimitive(true),
|
||||
"minNumbers" to JsonPrimitive(3),
|
||||
"minSpecial" to JsonPrimitive(3),
|
||||
"minNumberWords" to JsonPrimitive(5),
|
||||
"capitalize" to JsonPrimitive(true),
|
||||
"includeNumber" to JsonPrimitive(true),
|
||||
"useLower" to JsonPrimitive(true),
|
||||
)
|
||||
val policies = listOf(
|
||||
SyncResponseJson.Policy(
|
||||
organizationId = "organizationId",
|
||||
id = "id",
|
||||
type = PolicyTypeJson.PASSWORD_GENERATOR,
|
||||
isEnabled = true,
|
||||
data = JsonObject(payload),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel(state = initialPasscodeState)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(initialPasscodeState, awaitItem())
|
||||
mutablePolicyFlow.tryEmit(value = policies)
|
||||
assertEquals(initialPasscodeState.copy(isUnderPolicy = true), awaitItem())
|
||||
mutablePolicyFlow.tryEmit(value = emptyList())
|
||||
assertEquals(initialPasscodeState.copy(isUnderPolicy = false), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CloseClick should emit NavigateBack event`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
|
Loading…
Add table
Reference in a new issue