mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-752: Add Environment/EnvironmentRepository/EnvironmentDiskSource (#151)
This commit is contained in:
parent
f4dbe68527
commit
e4ab70a106
15 changed files with 575 additions and 24 deletions
|
@ -1,16 +1,15 @@
|
||||||
package com.x8bit.bitwarden.data.auth.datasource.disk
|
package com.x8bit.bitwarden.data.auth.datasource.disk
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
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.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.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.onSubscription
|
import kotlinx.coroutines.flow.onSubscription
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
private const val BASE_KEY = "bwPreferencesStorage"
|
|
||||||
private const val REMEMBERED_EMAIL_ADDRESS_KEY = "$BASE_KEY:rememberedEmail"
|
private const val REMEMBERED_EMAIL_ADDRESS_KEY = "$BASE_KEY:rememberedEmail"
|
||||||
private const val STATE_KEY = "$BASE_KEY:state"
|
private const val STATE_KEY = "$BASE_KEY:state"
|
||||||
|
|
||||||
|
@ -18,9 +17,10 @@ private const val STATE_KEY = "$BASE_KEY:state"
|
||||||
* Primary implementation of [AuthDiskSource].
|
* Primary implementation of [AuthDiskSource].
|
||||||
*/
|
*/
|
||||||
class AuthDiskSourceImpl(
|
class AuthDiskSourceImpl(
|
||||||
private val sharedPreferences: SharedPreferences,
|
sharedPreferences: SharedPreferences,
|
||||||
private val json: Json,
|
private val json: Json,
|
||||||
) : AuthDiskSource {
|
) : BaseDiskSource(sharedPreferences = sharedPreferences),
|
||||||
|
AuthDiskSource {
|
||||||
override var rememberedEmailAddress: String?
|
override var rememberedEmailAddress: String?
|
||||||
get() = getString(key = REMEMBERED_EMAIL_ADDRESS_KEY)
|
get() = getString(key = REMEMBERED_EMAIL_ADDRESS_KEY)
|
||||||
set(value) {
|
set(value) {
|
||||||
|
@ -48,25 +48,12 @@ class AuthDiskSourceImpl(
|
||||||
extraBufferCapacity = Int.MAX_VALUE,
|
extraBufferCapacity = Int.MAX_VALUE,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val onSharedPreferenceChangeListener =
|
override fun onSharedPreferenceChanged(
|
||||||
OnSharedPreferenceChangeListener { _, key ->
|
sharedPreferences: SharedPreferences?,
|
||||||
|
key: String?,
|
||||||
|
) {
|
||||||
when (key) {
|
when (key) {
|
||||||
STATE_KEY -> mutableUserStateFlow.tryEmit(userState)
|
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) }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<EnvironmentUrlDataJson?>
|
||||||
|
}
|
|
@ -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<EnvironmentUrlDataJson?>
|
||||||
|
get() = mutableEnvironmentUrlDataFlow
|
||||||
|
.onSubscription { emit(preAuthEnvironmentUrlData) }
|
||||||
|
|
||||||
|
private val mutableEnvironmentUrlDataFlow = MutableSharedFlow<EnvironmentUrlDataJson?>(
|
||||||
|
replay = 1,
|
||||||
|
extraBufferCapacity = Int.MAX_VALUE,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onSharedPreferenceChanged(
|
||||||
|
sharedPreferences: SharedPreferences?,
|
||||||
|
key: String?,
|
||||||
|
) {
|
||||||
|
when (key) {
|
||||||
|
PRE_AUTH_URLS_KEY -> mutableEnvironmentUrlDataFlow.tryEmit(preAuthEnvironmentUrlData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
|
@ -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<Environment>
|
||||||
|
}
|
|
@ -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<Environment>
|
||||||
|
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
|
|
@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.onEach
|
||||||
class NetworkConfigRepositoryImpl(
|
class NetworkConfigRepositoryImpl(
|
||||||
private val authRepository: AuthRepository,
|
private val authRepository: AuthRepository,
|
||||||
private val authTokenInterceptor: AuthTokenInterceptor,
|
private val authTokenInterceptor: AuthTokenInterceptor,
|
||||||
|
private val environmentRepository: EnvironmentRepository,
|
||||||
dispatcher: CoroutineDispatcher,
|
dispatcher: CoroutineDispatcher,
|
||||||
) : NetworkConfigRepository {
|
) : NetworkConfigRepository {
|
||||||
|
|
||||||
|
@ -30,5 +31,12 @@ class NetworkConfigRepositoryImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
|
|
||||||
|
environmentRepository
|
||||||
|
.environmentStateFlow
|
||||||
|
.onEach { environment ->
|
||||||
|
// TODO: Update base URL interceptors (BIT-725)
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package com.x8bit.bitwarden.data.platform.repository.di
|
package com.x8bit.bitwarden.data.platform.repository.di
|
||||||
|
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
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.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.NetworkConfigRepository
|
||||||
import com.x8bit.bitwarden.data.platform.repository.NetworkConfigRepositoryImpl
|
import com.x8bit.bitwarden.data.platform.repository.NetworkConfigRepositoryImpl
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
@ -18,15 +21,27 @@ import javax.inject.Singleton
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object RepositoryModule {
|
object RepositoryModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideEnvironmentRepository(
|
||||||
|
environmentDiskSource: EnvironmentDiskSource,
|
||||||
|
): EnvironmentRepository =
|
||||||
|
EnvironmentRepositoryImpl(
|
||||||
|
environmentDiskSource = environmentDiskSource,
|
||||||
|
dispatcher = Dispatchers.IO,
|
||||||
|
)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideNetworkConfigRepository(
|
fun provideNetworkConfigRepository(
|
||||||
authRepository: AuthRepository,
|
authRepository: AuthRepository,
|
||||||
authTokenInterceptor: AuthTokenInterceptor,
|
authTokenInterceptor: AuthTokenInterceptor,
|
||||||
|
environmentRepository: EnvironmentRepository,
|
||||||
): NetworkConfigRepository =
|
): NetworkConfigRepository =
|
||||||
NetworkConfigRepositoryImpl(
|
NetworkConfigRepositoryImpl(
|
||||||
authRepository = authRepository,
|
authRepository = authRepository,
|
||||||
authTokenInterceptor = authTokenInterceptor,
|
authTokenInterceptor = authTokenInterceptor,
|
||||||
|
environmentRepository = environmentRepository,
|
||||||
dispatcher = Dispatchers.IO,
|
dispatcher = Dispatchers.IO,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()),
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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",
|
||||||
|
)
|
|
@ -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<EnvironmentUrlDataJson>()
|
||||||
|
val environment = mockk<Environment>() {
|
||||||
|
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<EnvironmentUrlDataJson>()
|
||||||
|
val environment = mockk<Environment>() {
|
||||||
|
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<EnvironmentUrlDataJson?>
|
||||||
|
get() = mutablePreAuthEnvironmentUrlDataFlow
|
||||||
|
.onSubscription { emit(preAuthEnvironmentUrlData) }
|
||||||
|
|
||||||
|
private val mutablePreAuthEnvironmentUrlDataFlow =
|
||||||
|
MutableSharedFlow<EnvironmentUrlDataJson?>(
|
||||||
|
replay = 1,
|
||||||
|
extraBufferCapacity = Int.MAX_VALUE,
|
||||||
|
)
|
||||||
|
}
|
|
@ -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.AuthRepository
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
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.datasource.network.interceptor.AuthTokenInterceptor
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
@ -16,11 +17,16 @@ import org.junit.jupiter.api.Test
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class NetworkConfigRepositoryTest {
|
class NetworkConfigRepositoryTest {
|
||||||
private val mutableAuthStateFlow = MutableStateFlow<AuthState>(AuthState.Uninitialized)
|
private val mutableAuthStateFlow = MutableStateFlow<AuthState>(AuthState.Uninitialized)
|
||||||
|
private val mutableEnvironmentStateFlow = MutableStateFlow<Environment>(Environment.Us)
|
||||||
|
|
||||||
private val authRepository: AuthRepository = mockk() {
|
private val authRepository: AuthRepository = mockk() {
|
||||||
every { authStateFlow } returns mutableAuthStateFlow
|
every { authStateFlow } returns mutableAuthStateFlow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val environmentRepository: EnvironmentRepository = mockk {
|
||||||
|
every { environmentStateFlow } returns mutableEnvironmentStateFlow
|
||||||
|
}
|
||||||
|
|
||||||
private val authTokenInterceptor = AuthTokenInterceptor()
|
private val authTokenInterceptor = AuthTokenInterceptor()
|
||||||
|
|
||||||
private lateinit var networkConfigRepository: NetworkConfigRepository
|
private lateinit var networkConfigRepository: NetworkConfigRepository
|
||||||
|
@ -30,6 +36,7 @@ class NetworkConfigRepositoryTest {
|
||||||
networkConfigRepository = NetworkConfigRepositoryImpl(
|
networkConfigRepository = NetworkConfigRepositoryImpl(
|
||||||
authRepository = authRepository,
|
authRepository = authRepository,
|
||||||
authTokenInterceptor = authTokenInterceptor,
|
authTokenInterceptor = authTokenInterceptor,
|
||||||
|
environmentRepository = environmentRepository,
|
||||||
dispatcher = UnconfinedTestDispatcher(),
|
dispatcher = UnconfinedTestDispatcher(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue