[PM-19948] Migrate ServerConfigRepository to data module (#5002)

This commit is contained in:
Patrick Honkonen
2025-04-07 17:58:39 -04:00
committed by GitHub
parent acf222d151
commit c5e216783e
23 changed files with 56 additions and 308 deletions

View File

@@ -3,10 +3,10 @@ package com.bitwarden.authenticator
import android.content.Intent
import android.os.Parcelable
import androidx.lifecycle.viewModelScope
import com.bitwarden.authenticator.data.platform.repository.ServerConfigRepository
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.ui.platform.base.BaseViewModel
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.data.repository.ServerConfigRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

View File

@@ -1,8 +1,8 @@
package com.bitwarden.authenticator.data.platform.manager
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
import com.bitwarden.authenticator.data.platform.repository.ServerConfigRepository
import com.bitwarden.data.datasource.disk.model.ServerConfig
import com.bitwarden.data.repository.ServerConfigRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

View File

@@ -19,10 +19,10 @@ import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClip
import com.bitwarden.authenticator.data.platform.manager.imports.ImportManager
import com.bitwarden.authenticator.data.platform.manager.imports.ImportManagerImpl
import com.bitwarden.authenticator.data.platform.repository.DebugMenuRepository
import com.bitwarden.authenticator.data.platform.repository.ServerConfigRepository
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.data.manager.DispatcherManagerImpl
import com.bitwarden.data.repository.ServerConfigRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn

View File

@@ -5,6 +5,7 @@ import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagOver
import com.bitwarden.authenticator.data.platform.manager.getFlagValueOrDefault
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.data.repository.ServerConfigRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onSubscription

View File

@@ -1,21 +0,0 @@
package com.bitwarden.authenticator.data.platform.repository
import com.bitwarden.data.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?
}

View File

@@ -1,65 +0,0 @@
package com.bitwarden.authenticator.data.platform.repository
import com.bitwarden.data.datasource.disk.ConfigDiskSource
import com.bitwarden.data.datasource.disk.model.ServerConfig
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.network.service.ConfigService
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
}
}

View File

@@ -7,18 +7,14 @@ import com.bitwarden.authenticator.data.platform.datasource.disk.SettingsDiskSou
import com.bitwarden.authenticator.data.platform.manager.BiometricsEncryptionManager
import com.bitwarden.authenticator.data.platform.repository.DebugMenuRepository
import com.bitwarden.authenticator.data.platform.repository.DebugMenuRepositoryImpl
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 com.bitwarden.data.datasource.disk.ConfigDiskSource
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.network.service.ConfigService
import com.bitwarden.data.repository.ServerConfigRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import java.time.Clock
import javax.inject.Singleton
/**
@@ -45,21 +41,6 @@ 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 provideDebugMenuRepository(

View File

@@ -4,6 +4,7 @@ import app.cash.turbine.test
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
import com.bitwarden.data.datasource.disk.model.ServerConfig
import com.bitwarden.data.repository.ServerConfigRepository
import com.bitwarden.network.model.ConfigResponseJson
import io.mockk.every
import io.mockk.just

View File

@@ -1,169 +0,0 @@
package com.bitwarden.authenticator.data.platform.repository
import app.cash.turbine.test
import com.bitwarden.core.data.util.asSuccess
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
import com.bitwarden.data.datasource.disk.model.ServerConfig
import com.bitwarden.data.datasource.disk.util.FakeConfigDiskSource
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.network.model.ConfigResponseJson
import com.bitwarden.network.model.ConfigResponseJson.EnvironmentJson
import com.bitwarden.network.model.ConfigResponseJson.ServerJson
import com.bitwarden.network.service.ConfigService
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),
),
)

View File

@@ -1,7 +1,7 @@
package com.bitwarden.authenticator.data.platform.repository.util
import com.bitwarden.authenticator.data.platform.repository.ServerConfigRepository
import com.bitwarden.data.datasource.disk.model.ServerConfig
import com.bitwarden.data.repository.ServerConfigRepository
import com.bitwarden.network.model.ConfigResponseJson
import com.bitwarden.network.model.ConfigResponseJson.EnvironmentJson
import com.bitwarden.network.model.ConfigResponseJson.ServerJson