From 646566edd884f5d119b310ef465649d958083833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bispo?= Date: Tue, 30 Jul 2024 20:23:33 +0100 Subject: [PATCH] [PM-9875] Server configurations (#3645) --- .../x8bit/bitwarden/BitwardenApplication.kt | 4 + .../datasource/disk/ConfigDiskSource.kt | 21 ++ .../datasource/disk/ConfigDiskSourceImpl.kt | 37 ++++ .../disk/EnvironmentDiskSourceImpl.kt | 2 +- .../datasource/disk/di/PlatformDiskModule.kt | 13 ++ .../datasource/disk/model/ServerConfig.kt | 18 ++ .../repository/ServerConfigRepository.kt | 21 ++ .../repository/ServerConfigRepositoryImpl.kt | 77 ++++++++ .../repository/di/PlatformRepositoryModule.kt | 22 +++ .../datasource/disk/ConfigDiskSourceTest.kt | 111 +++++++++++ .../disk/util/FakeConfigDiskSource.kt | 25 +++ .../repository/ServerConfigRepositoryTest.kt | 186 ++++++++++++++++++ 12 files changed, 536 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/ConfigDiskSource.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/ConfigDiskSourceImpl.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/model/ServerConfig.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/platform/repository/ServerConfigRepository.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/platform/repository/ServerConfigRepositoryImpl.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/ConfigDiskSourceTest.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeConfigDiskSource.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/platform/repository/ServerConfigRepositoryTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/BitwardenApplication.kt b/app/src/main/java/com/x8bit/bitwarden/BitwardenApplication.kt index efca8263e1..978a301dd5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/BitwardenApplication.kt +++ b/app/src/main/java/com/x8bit/bitwarden/BitwardenApplication.kt @@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager +import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository import dagger.hilt.android.HiltAndroidApp import javax.inject.Inject @@ -32,4 +33,7 @@ class BitwardenApplication : Application() { @Inject lateinit var restrictionManager: RestrictionManager + + @Inject + lateinit var serverConfigRepository: ServerConfigRepository } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/ConfigDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/ConfigDiskSource.kt new file mode 100644 index 0000000000..25a8a92193 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/ConfigDiskSource.kt @@ -0,0 +1,21 @@ +package com.x8bit.bitwarden.data.platform.datasource.disk + +import com.x8bit.bitwarden.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 +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/ConfigDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/ConfigDiskSourceImpl.kt new file mode 100644 index 0000000000..28fc492438 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/ConfigDiskSourceImpl.kt @@ -0,0 +1,37 @@ +package com.x8bit.bitwarden.data.platform.datasource.disk + +import android.content.SharedPreferences +import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig +import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow +import com.x8bit.bitwarden.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 = "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 + get() = mutableServerConfigFlow.onSubscription { emit(serverConfig) } + + private val mutableServerConfigFlow = bufferedMutableSharedFlow(replay = 1) +} 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 index fc4efe37a5..9ae41c8d05 100644 --- 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 @@ -24,7 +24,7 @@ class EnvironmentDiskSourceImpl( set(value) { putString( key = PRE_AUTH_URLS_KEY, - value = value?.let { json.encodeToString(value) }, + value = value?.let { json.encodeToString(it) }, ) mutableEnvironmentUrlDataFlow.tryEmit(value) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/di/PlatformDiskModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/di/PlatformDiskModule.kt index 22c8cd1765..eadbf20f6b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/di/PlatformDiskModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/di/PlatformDiskModule.kt @@ -6,6 +6,8 @@ import android.content.SharedPreferences import androidx.room.Room import com.x8bit.bitwarden.data.platform.datasource.di.EncryptedPreferences import com.x8bit.bitwarden.data.platform.datasource.di.UnencryptedPreferences +import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource +import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSourceImpl import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSourceImpl import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource @@ -51,6 +53,17 @@ object PlatformDiskModule { json = json, ) + @Provides + @Singleton + fun provideConfigDiskSource( + @UnencryptedPreferences sharedPreferences: SharedPreferences, + json: Json, + ): ConfigDiskSource = + ConfigDiskSourceImpl( + sharedPreferences = sharedPreferences, + json = json, + ) + @Provides @Singleton fun provideEventDatabase(app: Application): PlatformDatabase = diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/model/ServerConfig.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/model/ServerConfig.kt new file mode 100644 index 0000000000..bea4e3d2f6 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/model/ServerConfig.kt @@ -0,0 +1,18 @@ +package com.x8bit.bitwarden.data.platform.datasource.disk.model + +import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson +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( + val lastSync: Long, + val serverData: ConfigResponseJson, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/ServerConfigRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/ServerConfigRepository.kt new file mode 100644 index 0000000000..147bc8cb17 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/ServerConfigRepository.kt @@ -0,0 +1,21 @@ +package com.x8bit.bitwarden.data.platform.repository + +import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig +import kotlinx.coroutines.flow.StateFlow + +/** + * Provides an API for observing the server config state. + */ +interface ServerConfigRepository { + + /** + * Gets the state [ServerConfig]. If needed or forced by [forceRefresh], + * updates the values using server side data. + */ + suspend fun getServerConfig(forceRefresh: Boolean): ServerConfig? + + /** + * Emits updates that track [ServerConfig]. + */ + val serverConfigStateFlow: StateFlow +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/ServerConfigRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/ServerConfigRepositoryImpl.kt new file mode 100644 index 0000000000..661f3dd7aa --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/ServerConfigRepositoryImpl.kt @@ -0,0 +1,77 @@ +package com.x8bit.bitwarden.data.platform.repository + +import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource +import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig +import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigService +import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +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, + environmentRepository: EnvironmentRepository, + dispatcherManager: DispatcherManager, +) : ServerConfigRepository { + + private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined) + + init { + environmentRepository + .environmentStateFlow + .onEach { + getServerConfig(true) + } + .launchIn(unconfinedScope) + } + + 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 + } + + override val serverConfigStateFlow: StateFlow + get() = configDiskSource + .serverConfigFlow + .stateIn( + scope = unconfinedScope, + started = SharingStarted.Eagerly, + initialValue = configDiskSource.serverConfig, + ) + + companion object { + private const val MINIMUM_CONFIG_SYNC_INTERVAL_SEC: Long = 60 * 60 + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/PlatformRepositoryModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/PlatformRepositoryModule.kt index 5cc937b3f9..e52f75c93d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/PlatformRepositoryModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/PlatformRepositoryModule.kt @@ -3,13 +3,17 @@ package com.x8bit.bitwarden.data.platform.repository.di import android.view.autofill.AutofillManager import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager +import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource +import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigService import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl +import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository +import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepositoryImpl import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepositoryImpl import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource @@ -17,6 +21,7 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import java.time.Clock import javax.inject.Singleton /** @@ -26,6 +31,23 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object PlatformRepositoryModule { + @Provides + @Singleton + fun provideServerConfigRepository( + configDiskSource: ConfigDiskSource, + configService: ConfigService, + clock: Clock, + environmentRepository: EnvironmentRepository, + dispatcherManager: DispatcherManager, + ): ServerConfigRepository = + ServerConfigRepositoryImpl( + configDiskSource = configDiskSource, + configService = configService, + clock = clock, + environmentRepository = environmentRepository, + dispatcherManager = dispatcherManager, + ) + @Provides @Singleton fun provideEnvironmentRepository( diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/ConfigDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/ConfigDiskSourceTest.kt new file mode 100644 index 0000000000..683da403be --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/ConfigDiskSourceTest.kt @@ -0,0 +1,111 @@ +package com.x8bit.bitwarden.data.platform.datasource.disk + +import androidx.core.content.edit +import app.cash.turbine.test +import com.x8bit.bitwarden.data.platform.base.FakeSharedPreferences +import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig +import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule +import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson +import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson.EnvironmentJson +import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson.ServerJson +import kotlinx.coroutines.test.runTest +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`() { + 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 true, "flexible-collections-v-1" to false), + ), +) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeConfigDiskSource.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeConfigDiskSource.kt new file mode 100644 index 0000000000..ba71225be1 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeConfigDiskSource.kt @@ -0,0 +1,25 @@ +package com.x8bit.bitwarden.data.platform.datasource.disk.util + +import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource +import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig +import com.x8bit.bitwarden.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 + get() = mutableServerConfigFlow + .onSubscription { emit(serverConfig) } + + private val mutableServerConfigFlow = + bufferedMutableSharedFlow(replay = 1) +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/ServerConfigRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/ServerConfigRepositoryTest.kt new file mode 100644 index 0000000000..a71e6021c4 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/ServerConfigRepositoryTest.kt @@ -0,0 +1,186 @@ +package com.x8bit.bitwarden.data.platform.repository + +import app.cash.turbine.test +import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager +import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig +import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeConfigDiskSource +import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson +import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson.EnvironmentJson +import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson.ServerJson +import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigService +import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager +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.asSuccess +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +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 environmentRepository = FakeEnvironmentRepository().apply { + environment = Environment.Us + } + + 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, + environmentRepository = environmentRepository, + dispatcherManager = fakeDispatcherManager, + ) + + @BeforeEach + fun setUp() { + fakeConfigDiskSource.serverConfig = null + } + + @Test + fun `environmentRepository stateflow should trigger new server configuration`() = runTest { + assertNull( + fakeConfigDiskSource.serverConfig, + ) + + // This should trigger a new server config to be fetched + environmentRepository.environment = Environment.Eu + + repository.serverConfigStateFlow.test { + assertEquals( + SERVER_CONFIG, + awaitItem(), + ) + } + } + + @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 true, "flexible-collections-v-1" to 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 true, "flexible-collections-v-1" to false), +)