diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt index 3165736f8..5940d2872 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt @@ -1,16 +1,15 @@ package com.x8bit.bitwarden.data.auth.datasource.disk import android.content.SharedPreferences -import android.content.SharedPreferences.OnSharedPreferenceChangeListener -import androidx.core.content.edit import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson +import com.x8bit.bitwarden.data.platform.datasource.disk.BaseDiskSource +import com.x8bit.bitwarden.data.platform.datasource.disk.BaseDiskSource.Companion.BASE_KEY import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.onSubscription import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -private const val BASE_KEY = "bwPreferencesStorage" private const val REMEMBERED_EMAIL_ADDRESS_KEY = "$BASE_KEY:rememberedEmail" private const val STATE_KEY = "$BASE_KEY:state" @@ -18,9 +17,10 @@ private const val STATE_KEY = "$BASE_KEY:state" * Primary implementation of [AuthDiskSource]. */ class AuthDiskSourceImpl( - private val sharedPreferences: SharedPreferences, + sharedPreferences: SharedPreferences, private val json: Json, -) : AuthDiskSource { +) : BaseDiskSource(sharedPreferences = sharedPreferences), + AuthDiskSource { override var rememberedEmailAddress: String? get() = getString(key = REMEMBERED_EMAIL_ADDRESS_KEY) set(value) { @@ -48,25 +48,12 @@ class AuthDiskSourceImpl( extraBufferCapacity = Int.MAX_VALUE, ) - private val onSharedPreferenceChangeListener = - OnSharedPreferenceChangeListener { _, key -> - when (key) { - STATE_KEY -> mutableUserStateFlow.tryEmit(userState) - } + override fun onSharedPreferenceChanged( + sharedPreferences: SharedPreferences?, + key: String?, + ) { + when (key) { + STATE_KEY -> mutableUserStateFlow.tryEmit(userState) } - - init { - sharedPreferences - .registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener) } - - private fun getString( - key: String, - default: String? = null, - ): String? = sharedPreferences.getString(key, default) - - private fun putString( - key: String, - value: String?, - ): Unit = sharedPreferences.edit { putString(key, value) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/BaseDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/BaseDiskSource.kt new file mode 100644 index 000000000..d001daa55 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/BaseDiskSource.kt @@ -0,0 +1,33 @@ +package com.x8bit.bitwarden.data.platform.datasource.disk + +import android.content.SharedPreferences +import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import androidx.core.content.edit + +/** + * Base class for simplifying interactions with [SharedPreferences]. + */ +abstract class BaseDiskSource( + private val sharedPreferences: SharedPreferences, +) : OnSharedPreferenceChangeListener { + + init { + @Suppress("LeakingThis") + sharedPreferences + .registerOnSharedPreferenceChangeListener(this) + } + + protected fun getString( + key: String, + default: String? = null, + ): String? = sharedPreferences.getString(key, default) + + protected fun putString( + key: String, + value: String?, + ): Unit = sharedPreferences.edit { putString(key, value) } + + companion object { + const val BASE_KEY: String = "bwPreferencesStorage" + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSource.kt new file mode 100644 index 000000000..a81dcea68 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSource.kt @@ -0,0 +1,20 @@ +package com.x8bit.bitwarden.data.platform.datasource.disk + +import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson +import kotlinx.coroutines.flow.Flow + +/** + * Primary access point for general environment-related disk information. + */ +interface EnvironmentDiskSource { + /** + * The currently persisted [EnvironmentUrlDataJson] (or `null` if not set). + */ + var preAuthEnvironmentUrlData: EnvironmentUrlDataJson? + + /** + * Emits updates that track [preAuthEnvironmentUrlData]. This will replay the last known value, + * if any. + */ + val preAuthEnvironmentUrlDataFlow: Flow +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSourceImpl.kt new file mode 100644 index 000000000..3d1221b92 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSourceImpl.kt @@ -0,0 +1,48 @@ +package com.x8bit.bitwarden.data.platform.datasource.disk + +import android.content.SharedPreferences +import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson +import com.x8bit.bitwarden.data.platform.datasource.disk.BaseDiskSource.Companion.BASE_KEY +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.onSubscription +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +private const val PRE_AUTH_URLS_KEY = "$BASE_KEY:preAuthEnvironmentUrls" + +/** + * Primary implementation of [EnvironmentDiskSource]. + */ +class EnvironmentDiskSourceImpl( + sharedPreferences: SharedPreferences, + private val json: Json, +) : BaseDiskSource(sharedPreferences = sharedPreferences), + EnvironmentDiskSource { + override var preAuthEnvironmentUrlData: EnvironmentUrlDataJson? + get() = getString(key = PRE_AUTH_URLS_KEY)?.let { json.decodeFromString(it) } + set(value) { + putString( + key = PRE_AUTH_URLS_KEY, + value = value?.let { json.encodeToString(value) }, + ) + } + + override val preAuthEnvironmentUrlDataFlow: Flow + get() = mutableEnvironmentUrlDataFlow + .onSubscription { emit(preAuthEnvironmentUrlData) } + + private val mutableEnvironmentUrlDataFlow = MutableSharedFlow( + replay = 1, + extraBufferCapacity = Int.MAX_VALUE, + ) + + override fun onSharedPreferenceChanged( + sharedPreferences: SharedPreferences?, + key: String?, + ) { + when (key) { + PRE_AUTH_URLS_KEY -> mutableEnvironmentUrlDataFlow.tryEmit(preAuthEnvironmentUrlData) + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/di/DiskModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/di/DiskModule.kt new file mode 100644 index 000000000..95edda210 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/di/DiskModule.kt @@ -0,0 +1,30 @@ +package com.x8bit.bitwarden.data.platform.datasource.disk.di + +import android.content.SharedPreferences +import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource +import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSourceImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json +import javax.inject.Singleton + +/** + * Provides persistence-related dependencies in the platform package. + */ +@Module +@InstallIn(SingletonComponent::class) +object DiskModule { + + @Provides + @Singleton + fun provideEnvironmentDiskSource( + sharedPreferences: SharedPreferences, + json: Json, + ): EnvironmentDiskSource = + EnvironmentDiskSourceImpl( + sharedPreferences = sharedPreferences, + json = json, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/EnvironmentRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/EnvironmentRepository.kt new file mode 100644 index 000000000..3cc5b06d1 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/EnvironmentRepository.kt @@ -0,0 +1,19 @@ +package com.x8bit.bitwarden.data.platform.repository + +import com.x8bit.bitwarden.data.platform.repository.model.Environment +import kotlinx.coroutines.flow.StateFlow + +/** + * Provides an API for observing and modifying environment state. + */ +interface EnvironmentRepository { + /** + * The currently set environment. + */ + var environment: Environment + + /** + * Emits updates that track [environment]. + */ + val environmentStateFlow: StateFlow +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/EnvironmentRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/EnvironmentRepositoryImpl.kt new file mode 100644 index 000000000..9db7c1768 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/EnvironmentRepositoryImpl.kt @@ -0,0 +1,48 @@ +package com.x8bit.bitwarden.data.platform.repository + +import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson +import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource +import com.x8bit.bitwarden.data.platform.repository.model.Environment +import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrls +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** + * Primary implementation of [EnvironmentRepository]. + */ +class EnvironmentRepositoryImpl( + private val environmentDiskSource: EnvironmentDiskSource, + private val dispatcher: CoroutineDispatcher, +) : EnvironmentRepository { + + private val scope = CoroutineScope(dispatcher) + + override var environment: Environment + get() = environmentDiskSource + .preAuthEnvironmentUrlData + .toEnvironmentUrlsOrDefault() + set(value) { + environmentDiskSource.preAuthEnvironmentUrlData = value.environmentUrlData + } + + override val environmentStateFlow: StateFlow + get() = environmentDiskSource + .preAuthEnvironmentUrlDataFlow + .map { it.toEnvironmentUrlsOrDefault() } + .stateIn( + scope = scope, + started = SharingStarted.Lazily, + initialValue = Environment.Us, + ) +} + +/** + * Converts a nullable [EnvironmentUrlDataJson] to an [Environment], where `null` values default to + * the US environment. + */ +private fun EnvironmentUrlDataJson?.toEnvironmentUrlsOrDefault(): Environment = + this?.toEnvironmentUrls() ?: Environment.Us diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/NetworkConfigRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/NetworkConfigRepositoryImpl.kt index c6ab1c582..398760e62 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/NetworkConfigRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/NetworkConfigRepositoryImpl.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.onEach class NetworkConfigRepositoryImpl( private val authRepository: AuthRepository, private val authTokenInterceptor: AuthTokenInterceptor, + private val environmentRepository: EnvironmentRepository, dispatcher: CoroutineDispatcher, ) : NetworkConfigRepository { @@ -30,5 +31,12 @@ class NetworkConfigRepositoryImpl( } } .launchIn(scope) + + environmentRepository + .environmentStateFlow + .onEach { environment -> + // TODO: Update base URL interceptors (BIT-725) + } + .launchIn(scope) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/RepositoryModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/RepositoryModule.kt index 3b2a781ba..aa4034202 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/RepositoryModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/RepositoryModule.kt @@ -1,7 +1,10 @@ package com.x8bit.bitwarden.data.platform.repository.di import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor +import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository +import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl import com.x8bit.bitwarden.data.platform.repository.NetworkConfigRepository import com.x8bit.bitwarden.data.platform.repository.NetworkConfigRepositoryImpl import dagger.Module @@ -18,15 +21,27 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object RepositoryModule { + @Provides + @Singleton + fun provideEnvironmentRepository( + environmentDiskSource: EnvironmentDiskSource, + ): EnvironmentRepository = + EnvironmentRepositoryImpl( + environmentDiskSource = environmentDiskSource, + dispatcher = Dispatchers.IO, + ) + @Provides @Singleton fun provideNetworkConfigRepository( authRepository: AuthRepository, authTokenInterceptor: AuthTokenInterceptor, + environmentRepository: EnvironmentRepository, ): NetworkConfigRepository = NetworkConfigRepositoryImpl( authRepository = authRepository, authTokenInterceptor = authTokenInterceptor, + environmentRepository = environmentRepository, dispatcher = Dispatchers.IO, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/Environment.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/Environment.kt new file mode 100644 index 000000000..b49ed5057 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/Environment.kt @@ -0,0 +1,70 @@ +package com.x8bit.bitwarden.data.platform.repository.model + +import android.os.Parcelable +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson +import com.x8bit.bitwarden.ui.platform.base.util.Text +import com.x8bit.bitwarden.ui.platform.base.util.asText +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue + +/** + * A higher-level wrapper around [EnvironmentUrlDataJson] that provides type-safety, enumerability, + * and human-readable labels. + */ +sealed class Environment : Parcelable { + /** + * The [Type] of the environment. + */ + abstract val type: Type + + /** + * The raw [environmentUrlData] that contains specific base URLs for each relevant domain. + */ + abstract val environmentUrlData: EnvironmentUrlDataJson + + /** + * Helper for a returning a human-readable label from a [Type]. + */ + val label: Text get() = type.label + + /** + * The default US environment. + */ + @Parcelize + data object Us : Environment() { + override val type: Type get() = Type.US + override val environmentUrlData: EnvironmentUrlDataJson + get() = EnvironmentUrlDataJson.DEFAULT_US + } + + /** + * The default EU environment. + */ + @Parcelize + data object Eu : Environment() { + override val type: Type get() = Type.EU + override val environmentUrlData: EnvironmentUrlDataJson + get() = EnvironmentUrlDataJson.DEFAULT_EU + } + + /** + * A custom self-hosted environment with a fully configurable [environmentUrlData]. + */ + @Parcelize + data class SelfHosted( + override val environmentUrlData: @RawValue EnvironmentUrlDataJson, + ) : Environment() { + override val type: Type get() = Type.SELF_HOSTED + } + + /** + * A summary of the various types that can be enumerated over and which contains a + * human-readable [label]. + */ + enum class Type(val label: Text) { + US(label = "bitwarden.com".asText()), + EU(label = "bitwarden.eu".asText()), + SELF_HOSTED(label = R.string.self_hosted.asText()), + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/util/EnvironmentExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/util/EnvironmentExtensions.kt new file mode 100644 index 000000000..ca33f84fb --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/util/EnvironmentExtensions.kt @@ -0,0 +1,14 @@ +package com.x8bit.bitwarden.data.platform.repository.util + +import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson +import com.x8bit.bitwarden.data.platform.repository.model.Environment + +/** + * Converts a raw [EnvironmentUrlDataJson] to an externally-consumable [Environment]. + */ +fun EnvironmentUrlDataJson.toEnvironmentUrls(): Environment = + when (this) { + Environment.Us.environmentUrlData -> Environment.Us + Environment.Eu.environmentUrlData -> Environment.Eu + else -> Environment.SelfHosted(environmentUrlData = this) + } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSourceTest.kt new file mode 100644 index 000000000..a709785f3 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/EnvironmentDiskSourceTest.kt @@ -0,0 +1,86 @@ +package com.x8bit.bitwarden.data.platform.datasource.disk + +import app.cash.turbine.test +import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson +import com.x8bit.bitwarden.data.platform.base.FakeSharedPreferences +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test + +class EnvironmentDiskSourceTest { + private val fakeSharedPreferences = FakeSharedPreferences() + + @OptIn(ExperimentalSerializationApi::class) + private val json = Json { + ignoreUnknownKeys = true + explicitNulls = false + } + + private val environmentDiskSource = EnvironmentDiskSourceImpl( + sharedPreferences = fakeSharedPreferences, + json = json, + ) + + @Test + fun `preAuthEnvironmentUrlData should pull from and update SharedPreferences`() { + val environmentKey = "bwPreferencesStorage:preAuthEnvironmentUrls" + + // Shared preferences and the repository start with the same value. + assertNull(environmentDiskSource.preAuthEnvironmentUrlData) + assertNull(fakeSharedPreferences.getString(environmentKey, null)) + + // Updating the repository updates shared preferences + environmentDiskSource.preAuthEnvironmentUrlData = ENVIRONMENT_URL_DATA + assertEquals( + json.parseToJsonElement( + ENVIRONMENT_URL_DATA_JSON, + ), + json.parseToJsonElement( + fakeSharedPreferences.getString(environmentKey, null)!!, + ), + ) + + // Update SharedPreferences updates the repository + fakeSharedPreferences.edit().putString(environmentKey, null).apply() + assertNull(environmentDiskSource.preAuthEnvironmentUrlData) + } + + @Test + fun `preAuthEnvironmentUrlDataFlow should react to changes in preAuthEnvironmentUrlData`() = + runTest { + environmentDiskSource.preAuthEnvironmentUrlDataFlow.test { + // The initial values of the Flow and the property are in sync + assertNull(environmentDiskSource.preAuthEnvironmentUrlData) + assertNull(awaitItem()) + + // Updating the repository updates shared preferences + environmentDiskSource.preAuthEnvironmentUrlData = ENVIRONMENT_URL_DATA + assertEquals(ENVIRONMENT_URL_DATA, awaitItem()) + } + } +} + +private const val ENVIRONMENT_URL_DATA_JSON = """ + { + "base": "base", + "api": "api", + "identity": "identity", + "icon": "icon", + "notifications": "notifications", + "webVault": "webVault", + "events": "events" + } +""" + +private val ENVIRONMENT_URL_DATA = EnvironmentUrlDataJson( + base = "base", + api = "api", + identity = "identity", + icon = "icon", + notifications = "notifications", + webVault = "webVault", + events = "events", +) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/EnvironmentRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/EnvironmentRepositoryTest.kt new file mode 100644 index 000000000..e6744d33b --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/EnvironmentRepositoryTest.kt @@ -0,0 +1,123 @@ +package com.x8bit.bitwarden.data.platform.repository + +import app.cash.turbine.test +import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson +import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource +import com.x8bit.bitwarden.data.platform.repository.model.Environment +import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrls +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.onSubscription +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class EnvironmentRepositoryTest { + private val fakeEnvironmentDiskSource = FakeEnvironmentDiskSource() + + @OptIn(ExperimentalCoroutinesApi::class) + private val repository = EnvironmentRepositoryImpl( + environmentDiskSource = fakeEnvironmentDiskSource, + dispatcher = UnconfinedTestDispatcher(), + ) + + @BeforeEach + fun setUp() { + mockkStatic(ENVIRONMENT_EXTENSIONS_PATH) + } + + @AfterEach + fun tearDown() { + unmockkStatic(ENVIRONMENT_EXTENSIONS_PATH) + } + + @Test + fun `environment should pull from and update EnvironmentDiskSource`() { + val environmentUrlDataJson = mockk() + val environment = mockk() { + every { environmentUrlData } returns environmentUrlDataJson + } + every { environmentUrlDataJson.toEnvironmentUrls() } returns environment + + // The repository exposes a non-null default value when the disk source is empty + assertNull(fakeEnvironmentDiskSource.preAuthEnvironmentUrlData) + assertEquals( + Environment.Us, + repository.environment, + ) + + // Updating the repository updates the disk source + repository.environment = environment + assertEquals( + environmentUrlDataJson, + fakeEnvironmentDiskSource.preAuthEnvironmentUrlData, + ) + + // Updating the disk source updates the repository + fakeEnvironmentDiskSource.preAuthEnvironmentUrlData = null + assertEquals( + Environment.Us, + repository.environment, + ) + fakeEnvironmentDiskSource.preAuthEnvironmentUrlData = environmentUrlDataJson + assertEquals( + environment, + repository.environment, + ) + } + + @Test + fun `environmentStateFow should react to changes in environment`() = runTest { + val environmentUrlDataJson = mockk() + val environment = mockk() { + every { environmentUrlData } returns environmentUrlDataJson + } + every { environmentUrlDataJson.toEnvironmentUrls() } returns environment + + repository.environmentStateFlow.test { + // The initial values of the Flow and the property are in sync + assertEquals( + Environment.Us, + repository.environment, + ) + assertEquals( + Environment.Us, + awaitItem(), + ) + + // Updating the property causes a flow emissions + repository.environment = environment + assertEquals(environment, awaitItem()) + } + } +} + +private const val ENVIRONMENT_EXTENSIONS_PATH = + "com.x8bit.bitwarden.data.platform.repository.util.EnvironmentExtensionsKt" + +private class FakeEnvironmentDiskSource : EnvironmentDiskSource { + override var preAuthEnvironmentUrlData: EnvironmentUrlDataJson? = null + set(value) { + field = value + mutablePreAuthEnvironmentUrlDataFlow.tryEmit(value) + } + + override val preAuthEnvironmentUrlDataFlow: Flow + get() = mutablePreAuthEnvironmentUrlDataFlow + .onSubscription { emit(preAuthEnvironmentUrlData) } + + private val mutablePreAuthEnvironmentUrlDataFlow = + MutableSharedFlow( + replay = 1, + extraBufferCapacity = Int.MAX_VALUE, + ) +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/NetworkConfigRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/NetworkConfigRepositoryTest.kt index c48ecd06d..eb7335e39 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/NetworkConfigRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/NetworkConfigRepositoryTest.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.repository import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.AuthState import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor +import com.x8bit.bitwarden.data.platform.repository.model.Environment import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -16,11 +17,16 @@ import org.junit.jupiter.api.Test @OptIn(ExperimentalCoroutinesApi::class) class NetworkConfigRepositoryTest { private val mutableAuthStateFlow = MutableStateFlow(AuthState.Uninitialized) + private val mutableEnvironmentStateFlow = MutableStateFlow(Environment.Us) private val authRepository: AuthRepository = mockk() { every { authStateFlow } returns mutableAuthStateFlow } + private val environmentRepository: EnvironmentRepository = mockk { + every { environmentStateFlow } returns mutableEnvironmentStateFlow + } + private val authTokenInterceptor = AuthTokenInterceptor() private lateinit var networkConfigRepository: NetworkConfigRepository @@ -30,6 +36,7 @@ class NetworkConfigRepositoryTest { networkConfigRepository = NetworkConfigRepositoryImpl( authRepository = authRepository, authTokenInterceptor = authTokenInterceptor, + environmentRepository = environmentRepository, dispatcher = UnconfinedTestDispatcher(), ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/util/EnvironmentExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/util/EnvironmentExtensionsTest.kt new file mode 100644 index 000000000..5b0babbe8 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/util/EnvironmentExtensionsTest.kt @@ -0,0 +1,43 @@ +package com.x8bit.bitwarden.data.platform.repository.util + +import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson +import com.x8bit.bitwarden.data.platform.repository.model.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class EnvironmentExtensionsTest { + @Test + fun `toEnvironmentUrls should correctly convert US urls to the expected type`() { + assertEquals( + Environment.Us, + EnvironmentUrlDataJson.DEFAULT_US.toEnvironmentUrls(), + ) + } + + @Test + fun `toEnvironmentUrls should correctly convert EU urls to the expected type`() { + assertEquals( + Environment.Eu, + EnvironmentUrlDataJson.DEFAULT_EU.toEnvironmentUrls(), + ) + } + + @Test + fun `toEnvironmentUrls should correctly convert custom urls to the expected type`() { + val environmentUrlData = EnvironmentUrlDataJson( + base = "base", + api = "api", + identity = "identity", + icon = "icon", + notifications = "notifications", + webVault = "webVault", + events = "events", + ) + assertEquals( + Environment.SelfHosted( + environmentUrlData = environmentUrlData, + ), + environmentUrlData.toEnvironmentUrls(), + ) + } +}