BITAU-89 Have SettingsViewModel obsererve DefaultSaveOption (#265)

Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
This commit is contained in:
Andrew HaistingandPatrick Honkonen authored and GitHub committed 2024-10-31 21:33:02 +00:00
1 parent 78d7865207
commit b42ec0ae13
8 files changed
+147 -46

No files matched your search

@@ -30,6 +30,11 @@ interface SettingsDiskSource {
*/
var defaultSaveOption: DefaultSaveOption
/**
* Flow that emits changes to [defaultSaveOption]
*/
val defaultSaveOptionFlow: Flow<DefaultSaveOption>
/**
* The currently persisted biometric integrity source for the system.
*/
@@ -45,6 +45,9 @@ class SettingsDiskSourceImpl(
private val mutableIsCrashLoggingEnabledFlow =
bufferedMutableSharedFlow<Boolean?>()
private val mutableDefaultSaveOptionFlow =
bufferedMutableSharedFlow<DefaultSaveOption>()
override var appLanguage: AppLanguage?
get() = getString(key = APP_LANGUAGE_KEY)
?.let { storedValue ->
@@ -89,7 +92,11 @@ class SettingsDiskSourceImpl(
key = DEFAULT_SAVE_OPTION_KEY,
value = newValue.value,
)
mutableDefaultSaveOptionFlow.tryEmit(newValue)
}
override val defaultSaveOptionFlow: Flow<DefaultSaveOption>
get() = mutableDefaultSaveOptionFlow
.onSubscription { emit(defaultSaveOption) }
override var systemBiometricIntegritySource: String?
get() = getString(key = SYSTEM_BIOMETRIC_INTEGRITY_SOURCE_KEY)
@@ -37,6 +37,11 @@ interface SettingsRepository {
*/
var defaultSaveOption: DefaultSaveOption
/**
* Flow that emits changes to [defaultSaveOption]
*/
val defaultSaveOptionFlow: Flow<DefaultSaveOption>
/**
* Whether or not biometric unlocking is enabled for the current user.
*/
@@ -44,6 +44,9 @@ class SettingsRepositoryImpl(
override var defaultSaveOption: DefaultSaveOption by settingsDiskSource::defaultSaveOption
override val defaultSaveOptionFlow: Flow<DefaultSaveOption>
by settingsDiskSource::defaultSaveOptionFlow
override val isUnlockWithBiometricsEnabled: Boolean
get() = authDiskSource.getUserBiometricUnlockKey() != null
@@ -72,6 +72,12 @@ class SettingsViewModel @Inject constructor(
.map { SettingsAction.Internal.SharedAccountsStateUpdated(it) }
.onEach(::handleAction)
.launchIn(viewModelScope)
settingsRepository
.defaultSaveOptionFlow
.map { SettingsAction.Internal.DefaultSaveOptionUpdated(it) }
.onEach(::handleAction)
.launchIn(viewModelScope)
}
override fun handleAction(action: SettingsAction) {
@@ -103,6 +109,10 @@ class SettingsViewModel @Inject constructor(
is SettingsAction.Internal.SharedAccountsStateUpdated -> {
handleSharedAccountsStateUpdated(action)
}
is SettingsAction.Internal.DefaultSaveOptionUpdated -> {
handleDefaultSaveOptionUpdated(action)
}
}
}
@@ -175,14 +185,19 @@ class SettingsViewModel @Inject constructor(
SettingsAction.DataClick.BackupClick -> handleBackupClick()
SettingsAction.DataClick.SyncWithBitwardenClick -> handleSyncWithBitwardenClick()
is SettingsAction.DataClick.DefaultSaveOptionUpdated ->
handleDefaultSaveOptionUpdated(action)
handleDefaultSaveOptionChosen(action)
}
}
private fun handleDefaultSaveOptionUpdated(
private fun handleDefaultSaveOptionChosen(
action: SettingsAction.DataClick.DefaultSaveOptionUpdated,
) {
settingsRepository.defaultSaveOption = action.option
}
private fun handleDefaultSaveOptionUpdated(
action: SettingsAction.Internal.DefaultSaveOptionUpdated,
) {
mutableStateFlow.update {
it.copy(
defaultSaveOption = action.option,
@@ -553,5 +568,12 @@ sealed class SettingsAction(
data class SharedAccountsStateUpdated(
val state: SharedVerificationCodesState,
) : SettingsAction()
/**
* Indicates that the default save option on disk was updated.
*/
data class DefaultSaveOptionUpdated(
val option: DefaultSaveOption,
) : SettingsAction()
}
}
@@ -1,8 +1,10 @@
package com.bitwarden.authenticator.data.platform.datasource.disk
import androidx.core.content.edit
import app.cash.turbine.test
import com.bitwarden.authenticator.data.platform.base.FakeSharedPreferences
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
@@ -54,46 +56,56 @@ class SettingDiskSourceTest {
}
@Test
fun `defaultSaveOption should read and write from shared preferences`() {
fun `defaultSaveOption should read and write from shared preferences`() = runTest {
val sharedPrefsKey = "bwPreferencesStorage:defaultSaveOption"
// Verify initial value is null and disk source should default to NONE
assertNull(sharedPreferences.getString(sharedPrefsKey, null))
assertEquals(
DefaultSaveOption.NONE,
settingDiskSource.defaultSaveOption,
)
settingDiskSource.defaultSaveOptionFlow.test {
// Verify initial value is null and disk source should default to NONE
assertNull(sharedPreferences.getString(sharedPrefsKey, null))
assertEquals(
DefaultSaveOption.NONE,
settingDiskSource.defaultSaveOption,
)
assertEquals(
DefaultSaveOption.NONE,
awaitItem(),
)
// Updating the shared preferences should update disk source
sharedPreferences.edit {
putString(
sharedPrefsKey,
DefaultSaveOption.BITWARDEN_APP.value,
// Updating the shared preferences should update disk source
sharedPreferences.edit {
putString(
sharedPrefsKey,
DefaultSaveOption.BITWARDEN_APP.value,
)
}
assertEquals(
DefaultSaveOption.BITWARDEN_APP,
settingDiskSource.defaultSaveOption,
)
// Updating the disk source should update shared preferences
settingDiskSource.defaultSaveOption = DefaultSaveOption.LOCAL
assertEquals(
DefaultSaveOption.LOCAL.value,
sharedPreferences.getString(sharedPrefsKey, null),
)
assertEquals(
DefaultSaveOption.LOCAL,
awaitItem(),
)
// Incorrect value should default to DefaultSaveOption.NONE
sharedPreferences.edit {
putString(
sharedPrefsKey,
"invalid",
)
}
assertEquals(
DefaultSaveOption.NONE,
settingDiskSource.defaultSaveOption,
)
}
assertEquals(
DefaultSaveOption.BITWARDEN_APP,
settingDiskSource.defaultSaveOption,
)
// Updating the disk source should update shared preferences
settingDiskSource.defaultSaveOption = DefaultSaveOption.LOCAL
assertEquals(
DefaultSaveOption.LOCAL.value,
sharedPreferences.getString(sharedPrefsKey, null),
)
// Incorrect value should default to DefaultSaveOption.NONE
sharedPreferences.edit {
putString(
sharedPrefsKey,
"invalid",
)
}
assertEquals(
DefaultSaveOption.NONE,
settingDiskSource.defaultSaveOption,
)
}
@Test
@@ -1,5 +1,6 @@
package com.bitwarden.authenticator.data.platform.repository
import app.cash.turbine.test
import com.bitwarden.authenticator.data.auth.datasource.disk.AuthDiskSource
import com.bitwarden.authenticator.data.auth.datasource.disk.util.FakeAuthDiskSource
import com.bitwarden.authenticator.data.authenticator.datasource.sdk.AuthenticatorSdkSource
@@ -12,6 +13,8 @@ import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
@@ -93,6 +96,27 @@ class SettingsRepositoryTest {
verify { settingsDiskSource.defaultSaveOption = DefaultSaveOption.BITWARDEN_APP }
}
@Test
fun `defaultSaveOptionFlow should match SettingsDiskSource`() = runTest {
// Reading from repository should read from disk source:
val expectedOptions = listOf(
DefaultSaveOption.NONE,
DefaultSaveOption.LOCAL,
DefaultSaveOption.BITWARDEN_APP,
DefaultSaveOption.NONE,
)
every { settingsDiskSource.defaultSaveOptionFlow } returns flow {
expectedOptions.forEach { emit(it) }
}
settingsRepository.defaultSaveOptionFlow.test {
expectedOptions.forEach {
assertEquals(it, awaitItem())
}
awaitComplete()
}
}
@Test
fun `previouslySyncedBitwardenAccountIds should pull from and update SettingsDiskSource`() {
// Reading from repository should read from disk source:
@@ -1,6 +1,7 @@
package com.bitwarden.authenticator.ui.platform.feature.settings
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewmodel.compose.viewModel
import app.cash.turbine.test
import com.bitwarden.authenticator.BuildConfig
import com.bitwarden.authenticator.R
@@ -11,6 +12,7 @@ import com.bitwarden.authenticator.data.platform.manager.FeatureFlagManager
import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManager
import com.bitwarden.authenticator.data.platform.manager.model.LocalFeatureFlag
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
import com.bitwarden.authenticator.ui.platform.base.BaseViewModelTest
import com.bitwarden.authenticator.ui.platform.base.util.asText
import com.bitwarden.authenticator.ui.platform.base.util.concat
@@ -29,6 +31,7 @@ import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -46,10 +49,12 @@ class SettingsViewModelTest : BaseViewModelTest() {
private val authenticatorRepository: AuthenticatorRepository = mockk {
every { sharedCodesStateFlow } returns mutableSharedCodesFlow
}
private val mutableDefaultSaveOptionFlow = bufferedMutableSharedFlow<DefaultSaveOption>()
private val settingsRepository: SettingsRepository = mockk {
every { appLanguage } returns APP_LANGUAGE
every { appTheme } returns APP_THEME
every { defaultSaveOption } returns DEFAULT_SAVE_OPTION
every { defaultSaveOptionFlow } returns mutableDefaultSaveOptionFlow
every { isUnlockWithBiometricsEnabled } returns true
every { isCrashLoggingEnabled } returns true
}
@@ -64,6 +69,7 @@ class SettingsViewModelTest : BaseViewModelTest() {
every { MOCK_SHARED_CODES_STATE.isSyncWithBitwardenEnabled } returns false
}
@AfterEach
fun teardown() {
unmockkStatic(SharedVerificationCodesState::isSyncWithBitwardenEnabled)
}
@@ -177,22 +183,39 @@ class SettingsViewModelTest : BaseViewModelTest() {
@Test
@Suppress("MaxLineLength")
fun `on DefaultSaveOptionUpdated should update SettingsRepository and state`() {
fun `on DefaultSaveOptionUpdated should update SettingsRepository`() {
val expectedOption = DefaultSaveOption.BITWARDEN_APP
every { settingsRepository.defaultSaveOption = expectedOption } just runs
val viewModel = createViewModel()
viewModel.trySendAction(SettingsAction.DataClick.DefaultSaveOptionUpdated(expectedOption))
assertEquals(
DEFAULT_STATE.copy(
defaultSaveOption = expectedOption,
),
viewModel.stateFlow.value,
)
verify { settingsRepository.defaultSaveOption = expectedOption }
}
@Test
@Suppress("MaxLineLength")
fun `Default save option should update when repository emits`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
mutableDefaultSaveOptionFlow.emit(DefaultSaveOption.LOCAL)
assertEquals(
DEFAULT_STATE.copy(
defaultSaveOption = DefaultSaveOption.LOCAL,
),
awaitItem(),
)
mutableDefaultSaveOptionFlow.emit(DefaultSaveOption.BITWARDEN_APP)
assertEquals(
DEFAULT_STATE.copy(
defaultSaveOption = DefaultSaveOption.BITWARDEN_APP,
),
awaitItem(),
)
}
}
private fun createViewModel(
savedState: SettingsState? = DEFAULT_STATE,
) = SettingsViewModel(