[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

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

View File

@@ -5,6 +5,7 @@ import android.content.Context
import androidx.core.content.getSystemService
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.data.manager.DispatcherManagerImpl
import com.bitwarden.data.repository.ServerConfigRepository
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
@@ -67,7 +68,6 @@ import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessorI
import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository
import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager

View File

@@ -1,10 +1,10 @@
package com.x8bit.bitwarden.data.platform.manager.network
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.data.repository.ServerConfigRepository
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.platform.repository
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.data.repository.ServerConfigRepository
import com.x8bit.bitwarden.BuildConfig
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus

View File

@@ -1,21 +0,0 @@
package com.x8bit.bitwarden.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.x8bit.bitwarden.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

@@ -1,9 +1,8 @@
package com.x8bit.bitwarden.data.platform.repository.di
import android.view.autofill.AutofillManager
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 com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
@@ -18,8 +17,6 @@ import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepositoryImpl
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.disk.VaultDiskSource
@@ -29,7 +26,6 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import java.time.Clock
import javax.inject.Singleton
/**
@@ -55,21 +51,6 @@ object PlatformRepositoryModule {
vaultSdkSource = vaultSdkSource,
)
@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 provideEnvironmentRepository(

View File

@@ -2,11 +2,11 @@ package com.x8bit.bitwarden.data.platform.manager.network
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.data.repository.ServerConfigRepository
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.util.advanceTimeByAndRunCurrent
import io.mockk.coEvery

View File

@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.repository
import app.cash.turbine.test
import com.bitwarden.data.datasource.disk.model.ServerConfig
import com.bitwarden.data.repository.ServerConfigRepository
import com.bitwarden.network.model.ConfigResponseJson
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus

View File

@@ -1,169 +0,0 @@
package com.x8bit.bitwarden.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,10 +1,10 @@
package com.x8bit.bitwarden.data.platform.repository.util
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
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.serialization.json.JsonPrimitive