Save Organizations data to disk when syncing (#429)

This commit is contained in:
Brian Yencho
2023-12-21 10:33:48 -06:00
committed by GitHub
parent feb37ccaf3
commit f634e0543f
8 changed files with 190 additions and 10 deletions

View File

@@ -10,9 +10,11 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorUserDe
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
import com.x8bit.bitwarden.data.platform.base.FakeSharedPreferences
import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.encodeToJsonElement
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
@@ -20,11 +22,7 @@ import org.junit.jupiter.api.Test
class AuthDiskSourceTest {
private val fakeSharedPreferences = FakeSharedPreferences()
@OptIn(ExperimentalSerializationApi::class)
private val json = Json {
ignoreUnknownKeys = true
explicitNulls = false
}
private val json = PlatformNetworkModule.providesJson()
private val authDiskSource = AuthDiskSourceImpl(
sharedPreferences = fakeSharedPreferences,
@@ -250,6 +248,71 @@ class AuthDiskSourceTest {
json.parseToJsonElement(requireNotNull(actual)),
)
}
@Test
fun `getOrganizations should pull from SharedPreferences`() {
val organizationsBaseKey = "bwPreferencesStorage:organizations"
val mockUserId = "mockUserId"
val mockOrganizations = listOf(
createMockOrganization(0),
createMockOrganization(1),
)
fakeSharedPreferences
.edit()
.putString(
"${organizationsBaseKey}_$mockUserId",
json.encodeToString(mockOrganizations),
)
.apply()
val actual = authDiskSource.getOrganizations(userId = mockUserId)
assertEquals(
mockOrganizations,
actual,
)
}
@Test
fun `getOrganizationsFlow should react to changes in getOrganizations`() = runTest {
val mockUserId = "mockUserId"
val mockOrganizations = listOf(
createMockOrganization(0),
createMockOrganization(1),
)
authDiskSource.getOrganizationsFlow(userId = mockUserId).test {
// The initial values of the Flow and the property are in sync
assertNull(authDiskSource.getOrganizations(userId = mockUserId))
assertNull(awaitItem())
// Updating the repository updates shared preferences
authDiskSource.storeOrganizations(
userId = mockUserId,
organizations = mockOrganizations,
)
assertEquals(mockOrganizations, awaitItem())
}
}
@Test
fun `storeOrganizations should update SharedPreferences`() {
val organizationsBaseKey = "bwPreferencesStorage:organizations"
val mockUserId = "mockUserId"
val mockOrganizations = listOf(
createMockOrganization(0),
createMockOrganization(1),
)
authDiskSource.storeOrganizations(
userId = mockUserId,
organizations = mockOrganizations,
)
val actual = fakeSharedPreferences.getString(
"${organizationsBaseKey}_$mockUserId",
null,
)
assertEquals(
json.encodeToJsonElement(mockOrganizations),
json.parseToJsonElement(requireNotNull(actual)),
)
}
}
private const val USER_STATE_JSON = """

View File

@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.disk.util
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.onSubscription
@@ -13,6 +14,8 @@ class FakeAuthDiskSource : AuthDiskSource {
override var rememberedEmailAddress: String? = null
private val mutableOrganizationsFlowMap =
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?>>()
private val mutableUserStateFlow =
MutableSharedFlow<UserStateJson?>(
replay = 1,
@@ -21,6 +24,8 @@ class FakeAuthDiskSource : AuthDiskSource {
private val storedUserKeys = mutableMapOf<String, String?>()
private val storedPrivateKeys = mutableMapOf<String, String?>()
private val storedOrganizations =
mutableMapOf<String, List<SyncResponseJson.Profile.Organization>?>()
private val storedOrganizationKeys = mutableMapOf<String, Map<String, String>?>()
override var userState: UserStateJson? = null
@@ -55,6 +60,23 @@ class FakeAuthDiskSource : AuthDiskSource {
storedOrganizationKeys[userId] = organizationKeys
}
override fun getOrganizations(
userId: String,
): List<SyncResponseJson.Profile.Organization>? = storedOrganizations[userId]
override fun getOrganizationsFlow(
userId: String,
): Flow<List<SyncResponseJson.Profile.Organization>?> =
getMutableOrganizationsFlow(userId).onSubscription { emit(getOrganizations(userId)) }
override fun storeOrganizations(
userId: String,
organizations: List<SyncResponseJson.Profile.Organization>?,
) {
storedOrganizations[userId] = organizations
getMutableOrganizationsFlow(userId = userId).tryEmit(organizations)
}
/**
* Assert that the given [userState] matches the currently tracked value.
*/
@@ -82,4 +104,28 @@ class FakeAuthDiskSource : AuthDiskSource {
fun assertOrganizationKeys(userId: String, organizationKeys: Map<String, String>?) {
assertEquals(organizationKeys, storedOrganizationKeys[userId])
}
/**
* Assert that the [organizations] were stored successfully using the [userId].
*/
fun assertOrganizations(
userId: String,
organizations: List<SyncResponseJson.Profile.Organization>?,
) {
assertEquals(organizations, storedOrganizations[userId])
}
//region Private helper functions
private fun getMutableOrganizationsFlow(
userId: String,
): MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?> =
mutableOrganizationsFlowMap.getOrPut(userId) {
MutableSharedFlow(
replay = 1,
extraBufferCapacity = Int.MAX_VALUE,
)
}
//endregion Private helper functions
}

View File

@@ -44,6 +44,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
@@ -927,8 +928,9 @@ class AuthRepositoryTest {
}
}
@Suppress("MaxLineLength")
@Test
fun `logout for single account should clear the access token and stored keys`() = runTest {
fun `logout for single account should clear the access token and profile data`() = runTest {
// First login:
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
coEvery {
@@ -973,6 +975,10 @@ class AuthRepositoryTest {
userId = USER_ID_1,
organizationKeys = ORGANIZATION_KEYS,
)
storeOrganizations(
userId = USER_ID_1,
organizations = ORGANIZATIONS,
)
}
repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
@@ -1000,6 +1006,10 @@ class AuthRepositoryTest {
userId = USER_ID_1,
organizationKeys = null,
)
fakeAuthDiskSource.assertOrganizations(
userId = USER_ID_1,
organizations = null,
)
verify { vaultRepository.deleteVaultData(userId = USER_ID_1) }
verify { vaultRepository.clearUnlockedData() }
verify { vaultRepository.lockVaultIfNecessary(userId = USER_ID_1) }
@@ -1356,6 +1366,7 @@ class AuthRepositoryTest {
private const val USER_ID_2 = "b9d32ec0-6497-4582-9798-b350f53bfa02"
private const val USER_ID_3 = "3816ef34-0747-4133-9b7a-ba35d3768a68"
private val ORGANIZATION_KEYS = mapOf("organizationId1" to "organizationKey1")
private val ORGANIZATIONS = listOf(createMockOrganization(number = 0))
private val PRE_LOGIN_SUCCESS = PreLoginResponseJson(
kdfParams = PreLoginResponseJson.KdfParams.Pbkdf2(iterations = 1u),
)

View File

@@ -25,6 +25,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCollection
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockFolder
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganizationKeys
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSyncResponse
import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService
@@ -310,6 +311,10 @@ class VaultRepositoryTest {
userId = "mockId-1",
organizationKeys = mapOf("mockId-1" to "mockKey-1"),
)
fakeAuthDiskSource.assertOrganizations(
userId = "mockId-1",
organizations = listOf(createMockOrganization(number = 1)),
)
assertEquals(
DataState.Loaded(
data = SendData(