mirror of
https://github.com/bitwarden/android.git
synced 2026-04-27 19:38:42 -05:00
[PM-19948] Migrate ServerConfigRepository to data module (#5002)
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
package com.bitwarden.data.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?
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.bitwarden.data.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].
|
||||
*/
|
||||
internal 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.bitwarden.data.repository.di
|
||||
|
||||
import com.bitwarden.data.datasource.disk.ConfigDiskSource
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.data.repository.ServerConfigRepository
|
||||
import com.bitwarden.data.repository.ServerConfigRepositoryImpl
|
||||
import com.bitwarden.network.service.ConfigService
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides repositories in the data module.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object RepositoryModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideServerConfigRepository(
|
||||
configDiskSource: ConfigDiskSource,
|
||||
configService: ConfigService,
|
||||
clock: Clock,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): ServerConfigRepository =
|
||||
ServerConfigRepositoryImpl(
|
||||
configDiskSource = configDiskSource,
|
||||
configService = configService,
|
||||
clock = clock,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package com.bitwarden.data.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),
|
||||
),
|
||||
)
|
||||
Reference in New Issue
Block a user