From 1fbab32fe71578402bd2e0c24638f8f770e9de00 Mon Sep 17 00:00:00 2001 From: Brian Yencho Date: Sun, 7 Jan 2024 13:06:30 -0600 Subject: [PATCH] Add VaultTimeoutAction and handle its persistence (#520) --- .../datasource/disk/SettingsDiskSource.kt | 20 +++++ .../datasource/disk/SettingsDiskSourceImpl.kt | 32 ++++++++ .../repository/model/VaultTimeoutAction.kt | 20 +++++ .../datasource/disk/SettingsDiskSourceTest.kt | 80 +++++++++++++++++++ .../disk/util/FakeSettingsDiskSource.kt | 27 +++++++ 5 files changed, 179 insertions(+) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/VaultTimeoutAction.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt index bc6a1d0def..928de1ec54 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.platform.datasource.disk +import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction import kotlinx.coroutines.flow.Flow /** @@ -22,4 +23,23 @@ interface SettingsDiskSource { * Stores the given [vaultTimeoutInMinutes] for the given [userId]. */ fun storeVaultTimeoutInMinutes(userId: String, vaultTimeoutInMinutes: Int?) + + /** + * Gets the current [VaultTimeoutAction] for the given [userId]. + */ + fun getVaultTimeoutAction(userId: String): VaultTimeoutAction? + + /** + * Emits updates that track [getVaultTimeoutAction] for the given [userId]. This will replay + * the last known value, if any. + */ + fun getVaultTimeoutActionFlow(userId: String): Flow + + /** + * Stores the given [vaultTimeoutAction] for the given [userId]. + */ + fun storeVaultTimeoutAction( + userId: String, + vaultTimeoutAction: VaultTimeoutAction?, + ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt index f48febdb55..d4bb883857 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt @@ -2,11 +2,13 @@ package com.x8bit.bitwarden.data.platform.datasource.disk import android.content.SharedPreferences import com.x8bit.bitwarden.data.platform.datasource.disk.BaseDiskSource.Companion.BASE_KEY +import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.onSubscription +private const val VAULT_TIMEOUT_ACTION_KEY = "$BASE_KEY:vaultTimeoutAction" private const val VAULT_TIME_IN_MINUTES_KEY = "$BASE_KEY:vaultTimeout" /** @@ -16,6 +18,9 @@ class SettingsDiskSourceImpl( val sharedPreferences: SharedPreferences, ) : BaseDiskSource(sharedPreferences = sharedPreferences), SettingsDiskSource { + private val mutableVaultTimeoutActionFlowMap = + mutableMapOf>() + private val mutableVaultTimeoutInMinutesFlowMap = mutableMapOf>() @@ -37,6 +42,33 @@ class SettingsDiskSourceImpl( getMutableVaultTimeoutInMinutesFlow(userId = userId).tryEmit(vaultTimeoutInMinutes) } + override fun getVaultTimeoutAction(userId: String): VaultTimeoutAction? = + getInt(key = "${VAULT_TIMEOUT_ACTION_KEY}_$userId")?.let { storedValue -> + VaultTimeoutAction.entries.firstOrNull { storedValue == it.value } + } + + override fun getVaultTimeoutActionFlow(userId: String): Flow = + getMutableVaultTimeoutActionFlow(userId = userId) + .onSubscription { emit(getVaultTimeoutAction(userId = userId)) } + + override fun storeVaultTimeoutAction( + userId: String, + vaultTimeoutAction: VaultTimeoutAction?, + ) { + putInt( + key = "${VAULT_TIMEOUT_ACTION_KEY}_$userId", + value = vaultTimeoutAction?.value, + ) + getMutableVaultTimeoutActionFlow(userId = userId).tryEmit(vaultTimeoutAction) + } + + private fun getMutableVaultTimeoutActionFlow( + userId: String, + ): MutableSharedFlow = + mutableVaultTimeoutActionFlowMap.getOrPut(userId) { + bufferedMutableSharedFlow(replay = 1) + } + private fun getMutableVaultTimeoutInMinutesFlow( userId: String, ): MutableSharedFlow = diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/VaultTimeoutAction.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/VaultTimeoutAction.kt new file mode 100644 index 0000000000..91cbed8e1c --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/VaultTimeoutAction.kt @@ -0,0 +1,20 @@ +package com.x8bit.bitwarden.data.platform.repository.model + +/** + * Represents different type of actions that may be performed when a vault times out. + * + * The [value] is used for consistent storage purposes. + */ +enum class VaultTimeoutAction( + val value: Int, +) { + /** + * The vault should lock when it times out. + */ + LOCK(0), + + /** + * The user should be logged out when their vault times out. + */ + LOGOUT(1), +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt index b2558aa732..0ee632a927 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.datasource.disk import androidx.core.content.edit import app.cash.turbine.test import com.x8bit.bitwarden.data.platform.base.FakeSharedPreferences +import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse @@ -95,4 +96,83 @@ class SettingsDiskSourceTest { ) assertFalse(fakeSharedPreferences.contains(vaultTimeoutKey)) } + + @Test + fun `getVaultTimeoutAction when values are present should pull from SharedPreferences`() { + val vaultTimeoutActionBaseKey = "bwPreferencesStorage:vaultTimeoutAction" + val mockUserId = "mockUserId" + val vaultTimeoutAction = VaultTimeoutAction.LOCK + fakeSharedPreferences + .edit() + .putInt( + "${vaultTimeoutActionBaseKey}_$mockUserId", + vaultTimeoutAction.value, + ) + .apply() + val actual = settingsDiskSource.getVaultTimeoutAction(userId = mockUserId) + assertEquals( + vaultTimeoutAction, + actual, + ) + } + + @Test + fun `getVaultTimeoutAction when values are absent should return null`() { + val mockUserId = "mockUserId" + assertNull(settingsDiskSource.getVaultTimeoutAction(userId = mockUserId)) + } + + @Test + fun `getVaultTimeoutActionFlow should react to changes in getOrganizations`() = runTest { + val mockUserId = "mockUserId" + val vaultTimeoutAction = VaultTimeoutAction.LOCK + settingsDiskSource.getVaultTimeoutActionFlow(userId = mockUserId).test { + // The initial values of the Flow and the property are in sync + assertNull(settingsDiskSource.getVaultTimeoutAction(userId = mockUserId)) + assertNull(awaitItem()) + + // Updating the disk source updates shared preferences + settingsDiskSource.storeVaultTimeoutAction( + userId = mockUserId, + vaultTimeoutAction = vaultTimeoutAction, + ) + assertEquals(vaultTimeoutAction, awaitItem()) + } + } + + @Test + fun `storeVaultTimeoutAction for non-null values should update SharedPreferences`() { + val vaultTimeoutActionBaseKey = "bwPreferencesStorage:vaultTimeoutAction" + val mockUserId = "mockUserId" + val vaultTimeoutAction = VaultTimeoutAction.LOCK + settingsDiskSource.storeVaultTimeoutAction( + userId = mockUserId, + vaultTimeoutAction = vaultTimeoutAction, + ) + val actual = fakeSharedPreferences.getInt( + "${vaultTimeoutActionBaseKey}_$mockUserId", + 0, + ) + assertEquals( + vaultTimeoutAction.value, + actual, + ) + } + + @Test + fun `storeVaultTimeoutAction for null values should clear SharedPreferences`() { + val vaultTimeoutActionBaseKey = "bwPreferencesStorage:vaultTimeoutAction" + val mockUserId = "mockUserId" + val previousValue = VaultTimeoutAction.LOCK + val vaultTimeoutActionKey = "${vaultTimeoutActionBaseKey}_$mockUserId" + fakeSharedPreferences.edit { + putInt(vaultTimeoutActionKey, previousValue.value) + } + assertTrue(fakeSharedPreferences.contains(vaultTimeoutActionKey)) + settingsDiskSource.storeVaultTimeoutAction( + userId = mockUserId, + vaultTimeoutAction = null, + ) + assertFalse(fakeSharedPreferences.contains(vaultTimeoutActionKey)) + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt index 54d36d6de2..851d355a53 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.data.platform.datasource.disk.util import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource +import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -11,9 +12,13 @@ import kotlinx.coroutines.flow.onSubscription */ class FakeSettingsDiskSource : SettingsDiskSource { + private val mutableVaultTimeoutActionsFlowMap = + mutableMapOf>() + private val mutableVaultTimeoutInMinutesFlowMap = mutableMapOf>() + private val storedVaultTimeoutActions = mutableMapOf() private val storedVaultTimeoutInMinutes = mutableMapOf() override fun getVaultTimeoutInMinutes(userId: String): Int? = @@ -31,8 +36,30 @@ class FakeSettingsDiskSource : SettingsDiskSource { getMutableVaultTimeoutInMinutesFlow(userId = userId).tryEmit(vaultTimeoutInMinutes) } + override fun getVaultTimeoutAction(userId: String): VaultTimeoutAction? = + storedVaultTimeoutActions[userId] + + override fun getVaultTimeoutActionFlow(userId: String): Flow = + getMutableVaultTimeoutActionsFlow(userId = userId) + .onSubscription { emit(getVaultTimeoutAction(userId = userId)) } + + override fun storeVaultTimeoutAction( + userId: String, + vaultTimeoutAction: VaultTimeoutAction?, + ) { + storedVaultTimeoutActions[userId] = vaultTimeoutAction + getMutableVaultTimeoutActionsFlow(userId = userId).tryEmit(vaultTimeoutAction) + } + //region Private helper functions + private fun getMutableVaultTimeoutActionsFlow( + userId: String, + ): MutableSharedFlow = + mutableVaultTimeoutActionsFlowMap.getOrPut(userId) { + bufferedMutableSharedFlow(replay = 1) + } + private fun getMutableVaultTimeoutInMinutesFlow( userId: String, ): MutableSharedFlow =