[BWA-86] Debug Menu #2 - config service (#274)

This commit is contained in:
André Bispo
2024-11-18 21:31:25 +00:00
committed by GitHub
parent 5b53b50b01
commit a0cc8a8a3d
20 changed files with 804 additions and 0 deletions
+2
View File
@@ -166,7 +166,9 @@ dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(libs.square.okhttp)
implementation(libs.square.okhttp.logging)
implementation(platform(libs.square.retrofit.bom))
implementation(libs.square.retrofit)
implementation(libs.square.retrofit.kotlinx.serialization)
implementation(libs.zxing.zxing.core)
// For now we are restricted to running Compose tests for debug builds only
@@ -0,0 +1,21 @@
package com.bitwarden.authenticator.data.platform.datasource.disk
import com.bitwarden.authenticator.data.platform.datasource.disk.model.ServerConfig
import kotlinx.coroutines.flow.Flow
/**
* Primary access point for server configuration-related disk information.
*/
interface ConfigDiskSource {
/**
* The currently persisted [ServerConfig] (or `null` if not set).
*/
var serverConfig: ServerConfig?
/**
* Emits updates that track [ServerConfig]. This will replay the last known value,
* if any.
*/
val serverConfigFlow: Flow<ServerConfig?>
}
@@ -0,0 +1,38 @@
package com.bitwarden.authenticator.data.platform.datasource.disk
import android.content.SharedPreferences
import com.bitwarden.authenticator.data.platform.datasource.disk.BaseDiskSource.Companion.BASE_KEY
import com.bitwarden.authenticator.data.platform.datasource.disk.model.ServerConfig
import com.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
import com.bitwarden.authenticator.data.platform.util.decodeFromStringOrNull
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
private const val SERVER_CONFIGURATIONS = "$BASE_KEY:serverConfigurations"
/**
* Primary implementation of [ConfigDiskSource].
*/
class ConfigDiskSourceImpl(
sharedPreferences: SharedPreferences,
private val json: Json,
) : BaseDiskSource(sharedPreferences = sharedPreferences),
ConfigDiskSource {
override var serverConfig: ServerConfig?
get() = getString(key = SERVER_CONFIGURATIONS)?.let { json.decodeFromStringOrNull(it) }
set(value) {
putString(
key = SERVER_CONFIGURATIONS,
value = value?.let { json.encodeToString(it) },
)
mutableServerConfigFlow.tryEmit(value)
}
override val serverConfigFlow: Flow<ServerConfig?>
get() = mutableServerConfigFlow.onSubscription { emit(serverConfig) }
private val mutableServerConfigFlow = bufferedMutableSharedFlow<ServerConfig?>(replay = 1)
}
@@ -2,6 +2,8 @@ package com.bitwarden.authenticator.data.platform.datasource.disk.di
import android.content.SharedPreferences
import com.bitwarden.authenticator.data.platform.datasource.di.UnencryptedPreferences
import com.bitwarden.authenticator.data.platform.datasource.disk.ConfigDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.ConfigDiskSourceImpl
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagDiskSourceImpl
import com.bitwarden.authenticator.data.platform.datasource.disk.SettingsDiskSource
@@ -20,6 +22,17 @@ import javax.inject.Singleton
*/
object PlatformDiskModule {
@Provides
@Singleton
fun provideConfigDiskSource(
@UnencryptedPreferences sharedPreferences: SharedPreferences,
json: Json,
): ConfigDiskSource =
ConfigDiskSourceImpl(
sharedPreferences = sharedPreferences,
json = json,
)
@Provides
@Singleton
fun provideSettingsDiskSource(
@@ -0,0 +1,22 @@
package com.bitwarden.authenticator.data.platform.datasource.disk.model
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* A higher-level wrapper around [ConfigResponseJson] that provides a timestamp
* to check if a sync is necessary
*
* @property lastSync The [Long] of the last sync.
* @property serverData The raw [ConfigResponseJson] that contains specific data of the
* server configuration
*/
@Serializable
data class ServerConfig(
@SerialName("lastSync")
val lastSync: Long,
@SerialName("serverData")
val serverData: ConfigResponseJson,
)
@@ -0,0 +1,13 @@
package com.bitwarden.authenticator.data.platform.datasource.network.api
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson
import retrofit2.http.GET
/**
* This interface defines the API service for fetching configuration data.
*/
interface ConfigApi {
@GET("config")
suspend fun getConfig(): Result<ConfigResponseJson>
}
@@ -5,6 +5,8 @@ import com.bitwarden.authenticator.data.platform.datasource.network.interceptor.
import com.bitwarden.authenticator.data.platform.datasource.network.retrofit.Retrofits
import com.bitwarden.authenticator.data.platform.datasource.network.retrofit.RetrofitsImpl
import com.bitwarden.authenticator.data.platform.datasource.network.serializer.ZonedDateTimeSerializer
import com.bitwarden.authenticator.data.platform.datasource.network.service.ConfigService
import com.bitwarden.authenticator.data.platform.datasource.network.service.ConfigServiceImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -12,6 +14,7 @@ import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
import retrofit2.create
import javax.inject.Singleton
@Module
@@ -21,6 +24,12 @@ import javax.inject.Singleton
* It initializes and configures the networking components.
*/
object PlatformNetworkModule {
@Provides
@Singleton
fun providesConfigService(
retrofits: Retrofits,
): ConfigService = ConfigServiceImpl(retrofits.unauthenticatedApiRetrofit.create())
@Provides
@Singleton
fun providesHeadersInterceptor(): HeadersInterceptor = HeadersInterceptor()
@@ -0,0 +1,82 @@
package com.bitwarden.authenticator.data.platform.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive
/**
* Represents the response model for configuration data fetched from the server.
*
* @property type The object type, typically "config".
* @property version The version of the configuration data.
* @property gitHash The Git hash associated with the configuration data.
* @property server The server information (nullable).
* @property environment The environment information containing URLs (vault, api, identity, etc.).
* @property featureStates A map containing various feature states.
*/
@Serializable
data class ConfigResponseJson(
@SerialName("object")
val type: String?,
@SerialName("version")
val version: String?,
@SerialName("gitHash")
val gitHash: String?,
@SerialName("server")
val server: ServerJson?,
@SerialName("environment")
val environment: EnvironmentJson?,
@SerialName("featureStates")
val featureStates: Map<String, JsonPrimitive>?,
) {
/**
* Represents a server in the configuration response.
*
* @param name The name of the server.
* @param url The URL of the server.
*/
@Serializable
data class ServerJson(
@SerialName("name")
val name: String?,
@SerialName("url")
val url: String?,
)
/**
* Represents the environment details in the configuration response.
*
* @param cloudRegion The cloud region associated with the environment.
* @param vaultUrl The URL of the vault service in the environment.
* @param apiUrl The URL of the API service in the environment.
* @param identityUrl The URL of the identity service in the environment.
* @param notificationsUrl The URL of the notifications service in the environment.
* @param ssoUrl The URL of the single sign-on (SSO) service in the environment.
*/
@Serializable
data class EnvironmentJson(
@SerialName("cloudRegion")
val cloudRegion: String?,
@SerialName("vault")
val vaultUrl: String?,
@SerialName("api")
val apiUrl: String?,
@SerialName("identity")
val identityUrl: String?,
@SerialName("notifications")
val notificationsUrl: String?,
@SerialName("sso")
val ssoUrl: String?,
)
}
@@ -0,0 +1,14 @@
package com.bitwarden.authenticator.data.platform.datasource.network.service
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson
/**
* Provides an API for querying for app configurations.
*/
interface ConfigService {
/**
* Fetch app configuration.
*/
suspend fun getConfig(): Result<ConfigResponseJson>
}
@@ -0,0 +1,11 @@
package com.bitwarden.authenticator.data.platform.datasource.network.service
import com.bitwarden.authenticator.data.platform.datasource.network.api.ConfigApi
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson
/**
* Default implementation of [ConfigService] for querying for app configurations.
*/
class ConfigServiceImpl(private val configApi: ConfigApi) : ConfigService {
override suspend fun getConfig(): Result<ConfigResponseJson> = configApi.getConfig()
}
@@ -0,0 +1,21 @@
package com.bitwarden.authenticator.data.platform.repository
import com.bitwarden.authenticator.data.platform.datasource.disk.model.ServerConfig
import kotlinx.coroutines.flow.StateFlow
/**
* Provides an API for observing the server config state.
*/
interface ServerConfigRepository {
/**
* Emits updates that track [ServerConfig].
*/
val serverConfigStateFlow: StateFlow<ServerConfig?>
/**
* Gets the state [ServerConfig]. If needed or forced by [forceRefresh],
* updates the values using server side data.
*/
suspend fun getServerConfig(forceRefresh: Boolean): ServerConfig?
}
@@ -0,0 +1,65 @@
package com.bitwarden.authenticator.data.platform.repository
import com.bitwarden.authenticator.data.platform.datasource.disk.ConfigDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.model.ServerConfig
import com.bitwarden.authenticator.data.platform.datasource.network.service.ConfigService
import com.bitwarden.authenticator.data.platform.manager.DispatcherManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import java.time.Clock
import java.time.Instant
/**
* Primary implementation of [ServerConfigRepositoryImpl].
*/
class ServerConfigRepositoryImpl(
private val configDiskSource: ConfigDiskSource,
private val configService: ConfigService,
private val clock: Clock,
dispatcherManager: DispatcherManager,
) : ServerConfigRepository {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
override val serverConfigStateFlow: StateFlow<ServerConfig?>
get() = configDiskSource
.serverConfigFlow
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Eagerly,
initialValue = configDiskSource.serverConfig,
)
override suspend fun getServerConfig(forceRefresh: Boolean): ServerConfig? {
val localConfig = configDiskSource.serverConfig
val needsRefresh = localConfig == null ||
Instant
.ofEpochMilli(localConfig.lastSync)
.isAfter(
clock.instant().plusSeconds(MINIMUM_CONFIG_SYNC_INTERVAL_SEC),
)
if (needsRefresh || forceRefresh) {
configService
.getConfig()
.onSuccess { configResponse ->
val serverConfig = ServerConfig(
lastSync = clock.instant().toEpochMilli(),
serverData = configResponse,
)
configDiskSource.serverConfig = serverConfig
return serverConfig
}
}
// If we are unable to retrieve a configuration from the server,
// fall back to the local configuration.
return localConfig
}
private companion object {
private const val MINIMUM_CONFIG_SYNC_INTERVAL_SEC: Long = 60 * 60
}
}
@@ -2,18 +2,23 @@ package com.bitwarden.authenticator.data.platform.repository.di
import com.bitwarden.authenticator.data.auth.datasource.disk.AuthDiskSource
import com.bitwarden.authenticator.data.authenticator.datasource.sdk.AuthenticatorSdkSource
import com.bitwarden.authenticator.data.platform.datasource.disk.ConfigDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.SettingsDiskSource
import com.bitwarden.authenticator.data.platform.datasource.network.service.ConfigService
import com.bitwarden.authenticator.data.platform.manager.BiometricsEncryptionManager
import com.bitwarden.authenticator.data.platform.manager.DispatcherManager
import com.bitwarden.authenticator.data.platform.repository.FeatureFlagRepository
import com.bitwarden.authenticator.data.platform.repository.FeatureFlagRepositoryImpl
import com.bitwarden.authenticator.data.platform.repository.ServerConfigRepository
import com.bitwarden.authenticator.data.platform.repository.ServerConfigRepositoryImpl
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.data.platform.repository.SettingsRepositoryImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import java.time.Clock
import javax.inject.Singleton
/**
@@ -40,6 +45,21 @@ object PlatformRepositoryModule {
authenticatorSdkSource = authenticatorSdkSource,
)
@Provides
@Singleton
fun provideServerConfigRepository(
configDiskSource: ConfigDiskSource,
configService: ConfigService,
clock: Clock,
dispatcherManager: DispatcherManager,
): ServerConfigRepository =
ServerConfigRepositoryImpl(
configDiskSource = configDiskSource,
configService = configService,
clock = clock,
dispatcherManager = dispatcherManager,
)
@Provides
@Singleton
fun provideFeatureFlagRepo(
@@ -0,0 +1,35 @@
package com.bitwarden.authenticator.data.platform.base
import com.bitwarden.authenticator.data.platform.datasource.network.core.ResultCallAdapterFactory
import com.bitwarden.authenticator.data.platform.datasource.network.di.PlatformNetworkModule
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.mockwebserver.MockWebServer
import org.junit.jupiter.api.AfterEach
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
/**
* Base class for service tests. Provides common mock web server and retrofit setup.
*/
abstract class BaseServiceTest {
protected val json = PlatformNetworkModule.providesJson()
protected val server = MockWebServer().apply { start() }
protected val url: HttpUrl = server.url("/")
protected val urlPrefix: String get() = "http://${server.hostName}:${server.port}"
protected val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(url.toString())
.addCallAdapterFactory(ResultCallAdapterFactory())
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
@AfterEach
fun after() {
server.shutdown()
}
}
@@ -0,0 +1,116 @@
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.data.platform.datasource.disk.model.ServerConfig
import com.bitwarden.authenticator.data.platform.datasource.network.di.PlatformNetworkModule
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson.EnvironmentJson
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson.ServerJson
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.JsonPrimitive
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
import java.time.Instant
class ConfigDiskSourceTest {
private val json = PlatformNetworkModule.providesJson()
private val fakeSharedPreferences = FakeSharedPreferences()
private val configDiskSource = ConfigDiskSourceImpl(
sharedPreferences = fakeSharedPreferences,
json = json,
)
@Test
fun `serverConfig should pull from and update SharedPreferences`() =
runTest {
val serverConfigKey = "bwPreferencesStorage:serverConfigurations"
// Shared preferences and the repository start with the same value.
assertNull(configDiskSource.serverConfig)
assertNull(fakeSharedPreferences.getString(serverConfigKey, null))
// Updating the repository updates shared preferences
configDiskSource.serverConfig = SERVER_CONFIG
assertEquals(
json.parseToJsonElement(
SERVER_CONFIG_JSON,
),
json.parseToJsonElement(
fakeSharedPreferences.getString(serverConfigKey, null)!!,
),
)
// Update SharedPreferences updates the repository
fakeSharedPreferences.edit { putString(serverConfigKey, null) }
assertNull(configDiskSource.serverConfig)
}
@Test
fun `serverConfigFlow should react to changes in serverConfig`() =
runTest {
configDiskSource.serverConfigFlow.test {
// The initial values of the Flow and the property are in sync
assertNull(configDiskSource.serverConfig)
assertNull(awaitItem())
// Updating the repository updates shared preferences
configDiskSource.serverConfig = SERVER_CONFIG
assertEquals(SERVER_CONFIG, awaitItem())
}
}
}
private const val SERVER_CONFIG_JSON = """
{
"lastSync": 1698408000000,
"serverData": {
"version": "2024.7.0",
"gitHash": "25cf6119-dirty",
"server": {
"name": "example",
"url": "https://localhost:8080"
},
"environment": {
"vault": "https://localhost:8080",
"api": "http://localhost:4000",
"identity": "http://localhost:33656",
"notifications": "http://localhost:61840",
"sso": "http://localhost:51822"
},
"featureStates": {
"duo-redirect": true,
"flexible-collections-v-1": false
}
}
}
"""
private val SERVER_CONFIG = ServerConfig(
lastSync = Instant.parse("2023-10-27T12:00:00Z").toEpochMilli(),
serverData = ConfigResponseJson(
type = null,
version = "2024.7.0",
gitHash = "25cf6119-dirty",
server = ServerJson(
name = "example",
url = "https://localhost:8080",
),
environment = EnvironmentJson(
cloudRegion = null,
vaultUrl = "https://localhost:8080",
apiUrl = "http://localhost:4000",
identityUrl = "http://localhost:33656",
notificationsUrl = "http://localhost:61840",
ssoUrl = "http://localhost:51822",
),
featureStates = mapOf(
"duo-redirect" to JsonPrimitive(true),
"flexible-collections-v-1" to JsonPrimitive(false),
),
),
)
@@ -0,0 +1,25 @@
package com.bitwarden.authenticator.data.platform.datasource.disk.util
import com.bitwarden.authenticator.data.platform.datasource.disk.ConfigDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.model.ServerConfig
import com.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onSubscription
class FakeConfigDiskSource : ConfigDiskSource {
private var serverConfigValue: ServerConfig? = null
override var serverConfig: ServerConfig?
get() = serverConfigValue
set(value) {
serverConfigValue = value
mutableServerConfigFlow.tryEmit(value)
}
override val serverConfigFlow: Flow<ServerConfig?>
get() = mutableServerConfigFlow
.onSubscription { emit(serverConfig) }
private val mutableServerConfigFlow =
bufferedMutableSharedFlow<ServerConfig?>(replay = 1)
}
@@ -0,0 +1,67 @@
package com.bitwarden.authenticator.data.platform.datasource.network.service
import com.bitwarden.authenticator.data.platform.base.BaseServiceTest
import com.bitwarden.authenticator.data.platform.datasource.network.api.ConfigApi
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson
import com.bitwarden.authenticator.data.platform.util.asSuccess
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.JsonPrimitive
import okhttp3.mockwebserver.MockResponse
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import retrofit2.create
class ConfigServiceTest : BaseServiceTest() {
private val api: ConfigApi = retrofit.create()
private val service = ConfigServiceImpl(api)
@Test
fun `getConfig should call ConfigApi`() = runTest {
server.enqueue(MockResponse().setBody(CONFIG_RESPONSE_JSON))
assertEquals(CONFIG_RESPONSE.asSuccess(), service.getConfig())
}
}
private const val CONFIG_RESPONSE_JSON = """
{
"object": "config",
"version": "1",
"gitHash": "gitHash",
"server": {
"name": "default",
"url": "url"
},
"environment": {
"cloudRegion": "US",
"vault": "vaultUrl",
"api": "apiUrl",
"identity": "identityUrl",
"notifications": "notificationsUrl",
"sso": "ssoUrl"
},
"featureStates": {
"feature one": false
}
}
"""
private val CONFIG_RESPONSE = ConfigResponseJson(
type = "config",
version = "1",
gitHash = "gitHash",
server = ConfigResponseJson.ServerJson(
name = "default",
url = "url",
),
environment = ConfigResponseJson.EnvironmentJson(
cloudRegion = "US",
vaultUrl = "vaultUrl",
apiUrl = "apiUrl",
notificationsUrl = "notificationsUrl",
identityUrl = "identityUrl",
ssoUrl = "ssoUrl",
),
featureStates = mapOf(
"feature one" to JsonPrimitive(false),
),
)
@@ -0,0 +1,169 @@
package com.bitwarden.authenticator.data.platform.repository
import app.cash.turbine.test
import com.bitwarden.authenticator.data.platform.base.FakeDispatcherManager
import com.bitwarden.authenticator.data.platform.datasource.disk.model.ServerConfig
import com.bitwarden.authenticator.data.platform.datasource.disk.util.FakeConfigDiskSource
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson.EnvironmentJson
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson.ServerJson
import com.bitwarden.authenticator.data.platform.datasource.network.service.ConfigService
import com.bitwarden.authenticator.data.platform.manager.DispatcherManager
import com.bitwarden.authenticator.data.platform.util.asSuccess
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.JsonPrimitive
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
class ServerConfigRepositoryTest {
private val fakeDispatcherManager: DispatcherManager = FakeDispatcherManager()
private val fakeConfigDiskSource = FakeConfigDiskSource()
private val configService: ConfigService = mockk {
coEvery {
getConfig()
} returns CONFIG_RESPONSE_JSON.asSuccess()
}
private val fixedClock: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
private val repository = ServerConfigRepositoryImpl(
configDiskSource = fakeConfigDiskSource,
configService = configService,
clock = fixedClock,
dispatcherManager = fakeDispatcherManager,
)
@BeforeEach
fun setUp() {
fakeConfigDiskSource.serverConfig = null
}
@Test
fun `getServerConfig should fetch a new server configuration with force refresh as true`() =
runTest {
coEvery {
configService.getConfig()
} returns CONFIG_RESPONSE_JSON.copy(version = "NEW VERSION").asSuccess()
fakeConfigDiskSource.serverConfig = SERVER_CONFIG.copy(
lastSync = fixedClock.instant().toEpochMilli(),
)
assertEquals(
fakeConfigDiskSource.serverConfig,
SERVER_CONFIG,
)
repository.getServerConfig(forceRefresh = true)
assertNotEquals(
fakeConfigDiskSource.serverConfig,
SERVER_CONFIG,
)
}
@Test
fun `getServerConfig should fetch a new server configuration if there is none in state`() =
runTest {
assertNull(
fakeConfigDiskSource.serverConfig,
)
repository.getServerConfig(forceRefresh = false)
assertEquals(
fakeConfigDiskSource.serverConfig,
SERVER_CONFIG,
)
}
@Test
fun `getServerConfig should return state server config if refresh is not necessary`() =
runTest {
val testConfig = SERVER_CONFIG.copy(
lastSync = fixedClock.instant().plusSeconds(1000L).toEpochMilli(),
serverData = CONFIG_RESPONSE_JSON.copy(
version = "new version!!",
),
)
fakeConfigDiskSource.serverConfig = testConfig
coEvery {
configService.getConfig()
} returns CONFIG_RESPONSE_JSON.asSuccess()
repository.getServerConfig(forceRefresh = false)
assertEquals(
fakeConfigDiskSource.serverConfig,
testConfig,
)
}
@Test
fun `serverConfigStateFlow should react to new server configurations`() = runTest {
repository.getServerConfig(forceRefresh = true)
repository.serverConfigStateFlow.test {
assertEquals(fakeConfigDiskSource.serverConfig, awaitItem())
}
}
}
private val SERVER_CONFIG = ServerConfig(
lastSync = Instant.parse("2023-10-27T12:00:00Z").toEpochMilli(),
serverData = ConfigResponseJson(
type = null,
version = "2024.7.0",
gitHash = "25cf6119-dirty",
server = ServerJson(
name = "example",
url = "https://localhost:8080",
),
environment = EnvironmentJson(
cloudRegion = null,
vaultUrl = "https://localhost:8080",
apiUrl = "http://localhost:4000",
identityUrl = "http://localhost:33656",
notificationsUrl = "http://localhost:61840",
ssoUrl = "http://localhost:51822",
),
featureStates = mapOf(
"duo-redirect" to JsonPrimitive(true),
"flexible-collections-v-1" to JsonPrimitive(false),
),
),
)
private val CONFIG_RESPONSE_JSON = ConfigResponseJson(
type = null,
version = "2024.7.0",
gitHash = "25cf6119-dirty",
server = ServerJson(
name = "example",
url = "https://localhost:8080",
),
environment = EnvironmentJson(
cloudRegion = null,
vaultUrl = "https://localhost:8080",
apiUrl = "http://localhost:4000",
identityUrl = "http://localhost:33656",
notificationsUrl = "http://localhost:61840",
ssoUrl = "http://localhost:51822",
),
featureStates = mapOf(
"duo-redirect" to JsonPrimitive(true),
"flexible-collections-v-1" to JsonPrimitive(false),
),
)
@@ -0,0 +1,58 @@
package com.bitwarden.authenticator.data.platform.repository.util
import com.bitwarden.authenticator.data.platform.datasource.disk.model.ServerConfig
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson.EnvironmentJson
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson.ServerJson
import com.bitwarden.authenticator.data.platform.repository.ServerConfigRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.serialization.json.JsonPrimitive
import java.time.Instant
class FakeServerConfigRepository : ServerConfigRepository {
var serverConfigValue: ServerConfig?
get() = mutableServerConfigFlow.value
set(value) {
mutableServerConfigFlow.value = value
}
private val mutableServerConfigFlow = MutableStateFlow<ServerConfig?>(SERVER_CONFIG)
override suspend fun getServerConfig(forceRefresh: Boolean): ServerConfig? {
if (forceRefresh) {
return SERVER_CONFIG
}
return serverConfigValue
}
override val serverConfigStateFlow: StateFlow<ServerConfig?>
get() = mutableServerConfigFlow
}
private val SERVER_CONFIG = ServerConfig(
lastSync = Instant.parse("2023-10-27T12:00:00Z").toEpochMilli(),
serverData = ConfigResponseJson(
type = null,
version = "2024.7.0",
gitHash = "25cf6119-dirty",
server = ServerJson(
name = "example",
url = "https://localhost:8080",
),
environment = EnvironmentJson(
cloudRegion = null,
vaultUrl = "https://localhost:8080",
apiUrl = "http://localhost:4000",
identityUrl = "http://localhost:33656",
notificationsUrl = "http://localhost:61840",
ssoUrl = "http://localhost:51822",
),
featureStates = mapOf(
"duo-redirect" to JsonPrimitive(true),
"flexible-collections-v-1" to JsonPrimitive(false),
"dummy-boolean" to JsonPrimitive(true),
),
),
)
+3
View File
@@ -49,6 +49,7 @@ ksp = "2.0.21-1.0.27"
mockk = "1.13.13"
okhttp = "4.12.0"
retrofit = "2.11.0"
retrofitBom = "2.11.0"
retrofitKotlinxSerialization = "1.0.0"
roboelectric = "4.13"
sonarqube = "5.1.0.4882"
@@ -120,6 +121,8 @@ square-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp"
square-okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
square-okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
square-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
square-retrofit-bom = { module = "com.squareup.retrofit2:retrofit-bom", version.ref = "retrofitBom" }
square-retrofit-kotlinx-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization" }
square-turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
zxing-zxing-core = { module = "com.google.zxing:core", version.ref = "zxing" }
testng = { group = "org.testng", name = "testng", version.ref = "testng" }