BIT-1729: Update generator policy UI in realtime (#990)

This commit is contained in:
David Perez 2024-02-08 17:10:25 -06:00 committed by Álison Fernandes
parent 5a6be93644
commit a7b58e9c19
4 changed files with 219 additions and 7 deletions

View file

@ -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 }
}

View file

@ -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.
*/

View file

@ -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),
),
),
)

View file

@ -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()