From affdf277f10976eb74c34177dba08d9e69526b5f Mon Sep 17 00:00:00 2001 From: MohamadJaara Date: Fri, 26 Jan 2024 11:05:50 +0100 Subject: [PATCH] refactor: implement UpdateApiVersionsUseCaseImpl to be proxy safe --- .../wire/kalium/logic/GlobalKaliumScope.kt | 15 +- .../configuration/server/ServerConfig.kt | 4 +- .../server/ServerConfigRepository.kt | 53 +-- .../logic/data/session/SessionMapper.kt | 14 +- .../logic/data/session/SessionRepository.kt | 10 +- .../wire/kalium/logic/di/MapperProvider.kt | 4 +- .../server/UpdateApiVersionsUseCase.kt | 62 +++- .../configuration/ServerConfigMapperTest.kt | 3 +- .../ServerConfigRepositoryTest.kt | 80 ----- .../logic/data/session/SessionMapperTest.kt | 12 +- .../server/UpdateApiVersionUseCaseTest.kt | 304 ++++++++++++++++-- .../com/wire/kalium/persistence/Accounts.sq | 22 ++ .../persistence/daokaliumdb/AccountsDAO.kt | 55 ++++ 13 files changed, 444 insertions(+), 194 deletions(-) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/GlobalKaliumScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/GlobalKaliumScope.kt index bd1068be432..aadfe33bba7 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/GlobalKaliumScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/GlobalKaliumScope.kt @@ -135,7 +135,20 @@ class GlobalKaliumScope internal constructor( val session: SessionScope get() = SessionScope(sessionRepository) val fetchServerConfigFromDeepLink: GetServerConfigUseCase get() = GetServerConfigUseCase(customServerConfigRepository) - val updateApiVersions: UpdateApiVersionsUseCase get() = UpdateApiVersionsUseCaseImpl() + val updateApiVersions: UpdateApiVersionsUseCase + get() = UpdateApiVersionsUseCaseImpl( + sessionRepository, + globalPreferences.authTokenStorage, + { serverConfig, proxyCredentials -> + authenticationScopeProvider.provide( + serverConfig, + proxyCredentials, + networkStateObserver, + globalDatabase, + kaliumConfigs + ).serverConfigRepository + }, + ) val storeServerConfig: StoreServerConfigUseCase get() = StoreServerConfigUseCaseImpl(customServerConfigRepository) val saveNotificationToken: SaveNotificationTokenUseCase diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfig.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfig.kt index 33210252795..393a9b4976c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfig.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfig.kt @@ -20,7 +20,6 @@ package com.wire.kalium.logic.configuration.server -import com.wire.kalium.logic.data.id.IdMapper import com.wire.kalium.logic.data.id.toModel import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.network.tools.ApiVersionDTO @@ -163,8 +162,7 @@ interface ServerConfigMapper { } class ServerConfigMapperImpl( - private val apiVersionMapper: ApiVersionMapper, - private val idMapper: IdMapper + private val apiVersionMapper: ApiVersionMapper ) : ServerConfigMapper { override fun toDTO(serverConfig: ServerConfig): ServerConfigDTO = with(serverConfig) { ServerConfigDTO( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfigRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfigRepository.kt index bc209c8c377..ffde0908a40 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfigRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfigRepository.kt @@ -37,28 +37,9 @@ import com.wire.kalium.persistence.daokaliumdb.ServerConfigurationDAO import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl import io.ktor.http.Url -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -@Suppress("TooManyFunctions") internal interface ServerConfigRepository { - - /** - * @return list of all locally stored server configurations - */ - suspend fun configList(): Either> - - /** - * @return observable list of all locally stored server configurations - */ - suspend fun configFlow(): Either>> - - /** - * delete a locally stored server configuration - */ - suspend fun deleteById(id: String): Either - suspend fun delete(serverConfig: ServerConfig): Either suspend fun getOrFetchMetadata(serverLinks: ServerConfig.Links): Either suspend fun storeConfig(links: ServerConfig.Links, metadata: ServerConfig.MetaData): Either @@ -72,20 +53,10 @@ internal interface ServerConfigRepository { */ suspend fun fetchApiVersionAndStore(links: ServerConfig.Links): Either - /** - * retrieve a config from the local DB by ID - */ - fun configById(id: String): Either - - /** - * retrieve a config from the local DB by Links - */ - suspend fun configByLinks(links: ServerConfig.Links): Either - /** * update the api version of a locally stored config */ - suspend fun updateConfigApiVersion(id: String): Either + suspend fun updateConfigApiVersion(serverConfig: ServerConfig): Either /** * Return the server links and metadata for the given userId @@ -101,15 +72,6 @@ internal class ServerConfigDataSource( private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl ) : ServerConfigRepository { - override suspend fun configList(): Either> = - wrapStorageRequest { dao.allConfig() }.map { it.map(serverConfigMapper::fromEntity) } - - override suspend fun configFlow(): Either>> = - wrapStorageRequest { dao.allConfigFlow().map { it.map(serverConfigMapper::fromEntity) } } - - override suspend fun deleteById(id: String) = wrapStorageRequest { dao.deleteById(id) } - - override suspend fun delete(serverConfig: ServerConfig) = deleteById(serverConfig.id) override suspend fun getOrFetchMetadata(serverLinks: ServerConfig.Links): Either = wrapStorageRequest { dao.configByLinks(serverConfigMapper.toEntity(serverLinks)) }.fold({ fetchApiVersionAndStore(serverLinks) @@ -164,16 +126,9 @@ internal class ServerConfigDataSource( storeConfig(links, metaData) } - override fun configById(id: String): Either = wrapStorageRequest { - dao.configById(id) - }.map { serverConfigMapper.fromEntity(it) } - - override suspend fun configByLinks(links: ServerConfig.Links): Either = - wrapStorageRequest { dao.configByLinks(serverConfigMapper.toEntity(links)) }.map { serverConfigMapper.fromEntity(it) } - - override suspend fun updateConfigApiVersion(id: String): Either = configById(id) - .flatMap { fetchMetadata(it.links) } - .flatMap { wrapStorageRequest { dao.updateApiVersion(id, it.commonApiVersion.version) } } + override suspend fun updateConfigApiVersion(serverConfig: ServerConfig): Either = + fetchMetadata(serverConfig.links) + .flatMap { wrapStorageRequest { dao.updateApiVersion(serverConfig.id, it.commonApiVersion.version) } } override suspend fun configForUser(userId: UserId): Either = wrapStorageRequest { dao.configForUser(userId.toDao()) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt index 3a73941fd10..33a49584c92 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt @@ -19,7 +19,6 @@ package com.wire.kalium.logic.data.session import com.wire.kalium.logic.data.auth.login.ProxyCredentials -import com.wire.kalium.logic.data.id.IdMapper import com.wire.kalium.logic.data.id.toApi import com.wire.kalium.logic.data.id.toDao import com.wire.kalium.logic.data.id.toModel @@ -49,18 +48,18 @@ interface SessionMapper { fun fromSsoIdEntity(ssoIdEntity: SsoIdEntity?): SsoId? fun toLogoutReason(reason: LogoutReasonEntity): LogoutReason fun fromEntityToProxyCredentialsDTO(proxyCredentialsEntity: ProxyCredentialsEntity): ProxyCredentialsDTO + fun formEntityToProxyModel(proxyCredentialsEntity: ProxyCredentialsEntity): ProxyCredentials fun fromPersistentWebSocketStatusEntity( persistentWebSocketStatusEntity: PersistentWebSocketStatusEntity ): PersistentWebSocketStatus + fun fromModelToProxyCredentialsEntity(proxyCredentialsModel: ProxyCredentials): ProxyCredentialsEntity fun fromModelToProxyCredentialsDTO(proxyCredentialsModel: ProxyCredentials): ProxyCredentialsDTO fun fromDTOToProxyCredentialsModel(proxyCredentialsDTO: ProxyCredentialsDTO?): ProxyCredentials? } @Suppress("TooManyFunctions") -internal class SessionMapperImpl( - private val idMapper: IdMapper -) : SessionMapper { +internal class SessionMapperImpl : SessionMapper { override fun toSessionDTO(authSession: AccountTokens): SessionDTO = with(authSession) { SessionDTO( @@ -137,12 +136,19 @@ internal class SessionMapperImpl( override fun fromEntityToProxyCredentialsDTO(proxyCredentialsEntity: ProxyCredentialsEntity): ProxyCredentialsDTO = ProxyCredentialsDTO(proxyCredentialsEntity.username, proxyCredentialsEntity.password) + override fun formEntityToProxyModel(proxyCredentialsEntity: ProxyCredentialsEntity): ProxyCredentials = + ProxyCredentials( + username = proxyCredentialsEntity.username, + password = proxyCredentialsEntity.password + ) + override fun fromPersistentWebSocketStatusEntity( persistentWebSocketStatusEntity: PersistentWebSocketStatusEntity ): PersistentWebSocketStatus = PersistentWebSocketStatus( persistentWebSocketStatusEntity.userIDEntity.toModel(), persistentWebSocketStatusEntity.isPersistentWebSocketEnabled ) + override fun fromModelToProxyCredentialsEntity(proxyCredentialsModel: ProxyCredentials): ProxyCredentialsEntity = ProxyCredentialsEntity(proxyCredentialsModel.username, proxyCredentialsModel.password) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionRepository.kt index ac41b468be7..de2d23f16bc 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionRepository.kt @@ -79,7 +79,7 @@ interface SessionRepository { suspend fun persistentWebSocketStatus(userId: UserId): Either suspend fun cookieLabel(userId: UserId): Either suspend fun isAccountReadOnly(userId: UserId): Either - + suspend fun validSessionsWithServerConfig(): Either> } @Suppress("TooManyFunctions", "LongParameterList") @@ -231,6 +231,14 @@ internal class SessionDataSource( } } + override suspend fun validSessionsWithServerConfig(): Either> = wrapStorageRequest { + accountsDAO.validAccountWithServerConfigId() + }.map { + it.map { (userId, serverConfig) -> + userId.toModel() to serverConfigMapper.fromEntity(serverConfig) + }.toMap() + } + internal fun ManagedByDTO.toDao() = when (this) { ManagedByDTO.WIRE -> ManagedByEntity.WIRE ManagedByDTO.SCIM -> ManagedByEntity.SCIM diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt index 07edb83f0c1..e5f908fcf71 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt @@ -101,8 +101,8 @@ import com.wire.kalium.logic.data.user.type.UserEntityTypeMapperImpl internal object MapperProvider { fun apiVersionMapper(): ApiVersionMapper = ApiVersionMapperImpl() fun idMapper(): IdMapper = IdMapperImpl() - fun serverConfigMapper(): ServerConfigMapper = ServerConfigMapperImpl(apiVersionMapper(), idMapper()) - fun sessionMapper(): SessionMapper = SessionMapperImpl(idMapper()) + fun serverConfigMapper(): ServerConfigMapper = ServerConfigMapperImpl(apiVersionMapper()) + fun sessionMapper(): SessionMapper = SessionMapperImpl() fun availabilityStatusMapper(): AvailabilityStatusMapper = AvailabilityStatusMapperImpl() fun connectionStateMapper(): ConnectionStateMapper = ConnectionStateMapperImpl() fun userMapper(): UserMapper = UserMapperImpl( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/server/UpdateApiVersionsUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/server/UpdateApiVersionsUseCase.kt index 29b36455d57..6cd77f2ad6d 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/server/UpdateApiVersionsUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/server/UpdateApiVersionsUseCase.kt @@ -18,6 +18,24 @@ package com.wire.kalium.logic.feature.server +import com.wire.kalium.logic.configuration.server.ServerConfig +import com.wire.kalium.logic.configuration.server.ServerConfigRepository +import com.wire.kalium.logic.data.auth.login.ProxyCredentials +import com.wire.kalium.logic.data.id.toDao +import com.wire.kalium.logic.data.session.SessionMapper +import com.wire.kalium.logic.data.session.SessionRepository +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.di.MapperProvider +import com.wire.kalium.logic.functional.getOrElse +import com.wire.kalium.logic.functional.map +import com.wire.kalium.logic.kaliumLogger +import com.wire.kalium.logic.wrapStorageNullableRequest +import com.wire.kalium.persistence.client.AuthTokenStorage +import io.ktor.util.collections.ConcurrentSet +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch + /** * Iterates over all locally stored server configs and update each api version */ @@ -25,14 +43,42 @@ interface UpdateApiVersionsUseCase { suspend operator fun invoke() } -class UpdateApiVersionsUseCaseImpl internal constructor() : UpdateApiVersionsUseCase { +class UpdateApiVersionsUseCaseImpl internal constructor( + private val sessionRepository: SessionRepository, + private val tokenStorage: AuthTokenStorage, + private val serverConfigRepoProvider: (serverConfig: ServerConfig, proxyCredentials: ProxyCredentials?) -> ServerConfigRepository, + private val sessionMapper: SessionMapper = MapperProvider.sessionMapper() +) : UpdateApiVersionsUseCase { override suspend operator fun invoke() { - // TODO: reimplement this in a safe way -// -// configRepository.configList().onSuccess { configList -> -// configList.forEach { -// configRepository.updateConfigApiVersion(it.id) -// } -// } + coroutineScope { + val updatedServerId = ConcurrentSet() + sessionRepository.validSessionsWithServerConfig().getOrElse { + return@coroutineScope + }.map { (userId, serverConfig) -> + launch { + if (updatedServerId.contains(serverConfig.id)) { + return@launch + } + updatedServerId.add(serverConfig.id) + updateApiForUser(userId, serverConfig) + } + }.joinAll() + } + } + + private suspend fun updateApiForUser(userId: UserId, serverConfig: ServerConfig) { + val proxyCredentials: ProxyCredentials? = if (serverConfig.links.apiProxy?.needsAuthentication == true) { + wrapStorageNullableRequest { + tokenStorage.proxyCredentials(userId.toDao()) + }.map { + it?.let { sessionMapper.formEntityToProxyModel(it) } + }.getOrElse { + kaliumLogger.d("No proxy credentials found for user ${userId.toLogString()}}") + return + } + } else { + null + } + serverConfigRepoProvider(serverConfig, proxyCredentials).updateConfigApiVersion(serverConfig) } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/configuration/ServerConfigMapperTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/configuration/ServerConfigMapperTest.kt index a4b684d4646..3651f08760a 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/configuration/ServerConfigMapperTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/configuration/ServerConfigMapperTest.kt @@ -24,7 +24,6 @@ import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.configuration.server.ServerConfigMapper import com.wire.kalium.logic.configuration.server.ServerConfigMapperImpl import com.wire.kalium.logic.configuration.server.toCommonApiVersionType -import com.wire.kalium.logic.di.MapperProvider import com.wire.kalium.logic.util.stubs.newServerConfig import com.wire.kalium.logic.util.stubs.newServerConfigDTO import com.wire.kalium.logic.util.stubs.newServerConfigEntity @@ -49,7 +48,7 @@ class ServerConfigMapperTest { @BeforeTest fun setup() { - serverConfigMapper = ServerConfigMapperImpl(versionMapper, MapperProvider.idMapper()) + serverConfigMapper = ServerConfigMapperImpl(versionMapper) given(versionMapper).invocation { toDTO(SERVER_CONFIG_TEST.metaData.commonApiVersion) }.then { ApiVersionDTO.Valid(1) } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/configuration/ServerConfigRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/configuration/ServerConfigRepositoryTest.kt index fc930482560..7d45193396d 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/configuration/ServerConfigRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/configuration/ServerConfigRepositoryTest.kt @@ -23,7 +23,6 @@ import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.configuration.server.ServerConfigDataSource import com.wire.kalium.logic.configuration.server.ServerConfigRepository import com.wire.kalium.logic.failure.ServerConfigFailure -import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.util.shouldFail import com.wire.kalium.logic.util.shouldSucceed import com.wire.kalium.logic.util.stubs.newServerConfig @@ -45,92 +44,13 @@ import io.mockative.mock import io.mockative.once import io.mockative.verify import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertIs @ExperimentalCoroutinesApi class ServerConfigRepositoryTest { - - @Test - fun givenStoredConfig_thenItCanBeRetrievedAsList() = runTest { - val (arrangement, repository) = Arrangement().withDaoEntityResponse().arrange() - val expected = listOf(newServerConfig(1), newServerConfig(2), newServerConfig(3)) - - repository.configList().shouldSucceed { assertEquals(it, expected) } - verify(arrangement.serverConfigDAO) - .suspendFunction(arrangement.serverConfigDAO::allConfig) - .wasInvoked(exactly = once) - } - - @Test - fun givenStoredConfig_thenItCanBeRetrievedAsFlow() = runTest { - val (arrangement, repository) = Arrangement().withDaoEntityFlowResponse().arrange() - val expected = listOf(newServerConfig(1), newServerConfig(2), newServerConfig(3)) - - val actual = repository.configFlow() - - assertIs>>>(actual) - assertEquals(expected.first(), actual.value.first()[0]) - - verify(arrangement.serverConfigDAO) - .suspendFunction(arrangement.serverConfigDAO::allConfigFlow) - .wasInvoked(exactly = once) - } - - @Test - fun givenStoredConfig_thenItCanBeRetrievedById() = runTest { - val (arrangement, repository) = Arrangement() - .withConfigById(newServerConfigEntity(1)) - .arrange() - val expected = newServerConfig(1) - - val actual = repository.configById(expected.id) - assertIs>(actual) - assertEquals(expected, actual.value) - - verify(arrangement.serverConfigDAO) - .function(arrangement.serverConfigDAO::configById) - .with(any()) - .wasInvoked(exactly = once) - } - - @Test - fun givenStoredConfig_thenItCanBeDeleted() = runTest { - val serverConfigId = "1" - val (arrangement, repository) = Arrangement() - .withConfigById(newServerConfigEntity(1)) - .arrange() - - val actual = repository.deleteById(serverConfigId) - - actual.shouldSucceed() - verify(arrangement.serverConfigDAO) - .suspendFunction(arrangement.serverConfigDAO::deleteById) - .with(any()) - .wasInvoked(exactly = once) - } - - @Test - fun givenStoredConfig_whenDeleting_thenItCanBeDeleted() = runTest { - val serverConfig = newServerConfig(1) - val (arrangement, repository) = Arrangement() - .withConfigById(newServerConfigEntity(1)) - .arrange() - - val actual = repository.delete(serverConfig) - - actual.shouldSucceed() - verify(arrangement.serverConfigDAO) - .suspendFunction(arrangement.serverConfigDAO::deleteById) - .with(any()) - .wasInvoked(exactly = once) - } - @Test fun givenValidCompatibleApiVersion_whenStoringConfigLocally_thenConfigIsStored() = runTest { val expected = newServerConfig(1) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/session/SessionMapperTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/session/SessionMapperTest.kt index 170294c5205..47819778e10 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/session/SessionMapperTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/session/SessionMapperTest.kt @@ -18,7 +18,6 @@ package com.wire.kalium.logic.data.session -import com.wire.kalium.logic.data.id.IdMapper import com.wire.kalium.logic.data.user.SsoId import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.auth.AccountTokens @@ -26,10 +25,6 @@ import com.wire.kalium.network.api.base.model.SessionDTO import com.wire.kalium.persistence.client.AuthTokenEntity import com.wire.kalium.persistence.dao.UserIDEntity import com.wire.kalium.persistence.model.SsoIdEntity -import io.mockative.Mock -import io.mockative.classOf -import io.mockative.given -import io.mockative.mock import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -37,14 +32,11 @@ import com.wire.kalium.network.api.base.model.UserId as UserIdDTO class SessionMapperTest { - @Mock - val idMapper = mock(classOf()) - private lateinit var sessionMapper: SessionMapper @BeforeTest fun setup() { - sessionMapper = SessionMapperImpl(idMapper) + sessionMapper = SessionMapperImpl() } @Test @@ -70,8 +62,6 @@ class SessionMapperTest { fun givenAnAuthTokens_whenMappingToPersistenceAuthTokens_thenValuesAreMappedCorrectly() { val authSession: AccountTokens = TEST_AUTH_TOKENS - given(idMapper).invocation { idMapper.toSsoIdEntity(TEST_SSO_ID) }.then { TEST_SSO_ID_ENTITY } - val expected: AuthTokenEntity = with(authSession) { AuthTokenEntity( userId = UserIDEntity(userId.value, userId.domain), diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/server/UpdateApiVersionUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/server/UpdateApiVersionUseCaseTest.kt index 60590818b2a..458be080f9b 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/server/UpdateApiVersionUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/server/UpdateApiVersionUseCaseTest.kt @@ -18,59 +18,297 @@ package com.wire.kalium.logic.feature.server +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.configuration.server.ServerConfigRepository +import com.wire.kalium.logic.data.auth.login.ProxyCredentials +import com.wire.kalium.logic.data.id.toDao +import com.wire.kalium.logic.data.session.SessionRepository +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.util.stubs.newServerConfig +import com.wire.kalium.persistence.client.AuthTokenStorage +import com.wire.kalium.persistence.client.ProxyCredentialsEntity +import com.wire.kalium.persistence.dao.UserIDEntity import io.mockative.Mock -import io.mockative.Times import io.mockative.any -import io.mockative.configure +import io.mockative.eq import io.mockative.given -import io.mockative.matchers.OneOfMatcher import io.mockative.mock +import io.mockative.once import io.mockative.verify import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest -import kotlin.test.BeforeTest -import kotlin.test.Ignore +import okio.IOException import kotlin.test.Test +import kotlin.test.assertEquals @OptIn(ExperimentalCoroutinesApi::class) -@Ignore class UpdateApiVersionUseCaseTest { + @Test + fun givenError_whenCallingValidSessionsWithServerConfig_thenDoNothingElse() { + val (arrangement, updateApiVersionsUseCase) = Arrangement() + .arrange { + withValidSessionWithServerConfig(Either.Left(StorageFailure.Generic(IOException()))) + } + + runTest { + updateApiVersionsUseCase() + advanceUntilIdle() + } + + assertEquals(0, arrangement.serverConfigProviderCalledCount) + } + + @Test + fun givenUsersWithServerConfigNoProxy_thenDoNotFetchProxyCredentials() { + val (arrangement, updateApiVersionsUseCase) = Arrangement() + .arrange { + withValidSessionWithServerConfig( + Either.Right( + mapOf( + userId1 to serverConfig1.copy( + links = serverConfig1.links.copy( + apiProxy = null + ) + ) + ) + ) + ) + withUpdateConfigApiVersion(serverConfig1, Either.Right(Unit)) + } + + runTest { + updateApiVersionsUseCase() + advanceUntilIdle() + } + + verify(arrangement.tokenStorage) + .suspendFunction(arrangement.tokenStorage::proxyCredentials) + .with(any()) + .wasNotInvoked() + + + verify(arrangement.serverConfigRepository1) + .suspendFunction(arrangement.serverConfigRepository1::updateConfigApiVersion) + .with(eq(serverConfig1)) + .wasInvoked(exactly = once) + } + + @Test + fun givenUserWithProxyButNoAuthentication_thenDoNotFetchProxyCredentials() { + val (arrangement, updateApiVersionsUseCase) = Arrangement() + .arrange { + withValidSessionWithServerConfig( + Either.Right( + mapOf( + userId1 to serverConfig1.copy( + links = serverConfig1.links.copy( + apiProxy = ServerConfig.ApiProxy( + host = "host", + port = 1234, + needsAuthentication = false + ) + ) + ) + ) + ) + ) + withUpdateConfigApiVersion(serverConfig1, Either.Right(Unit)) + } + + runTest { + updateApiVersionsUseCase() + advanceUntilIdle() + } + + verify(arrangement.tokenStorage) + .suspendFunction(arrangement.tokenStorage::proxyCredentials) + .with(any()) + .wasNotInvoked() + + + verify(arrangement.serverConfigRepository1) + .suspendFunction(arrangement.serverConfigRepository1::updateConfigApiVersion) + .with(any()) + .wasInvoked(exactly = once) + } + + @Test + fun givenUserWithProxyAndNeedAuthentication_thenFetchProxyCredentials() { + val (arrangement, updateApiVersionsUseCase) = Arrangement() + .arrange { + withValidSessionWithServerConfig( + Either.Right( + mapOf( + userId1 to serverConfig1.copy( + links = serverConfig1.links.copy( + apiProxy = ServerConfig.ApiProxy( + host = "host", + port = 1234, + needsAuthentication = true + ) + ) + ) + ) + ) + ) + withUpdateConfigApiVersion(serverConfig1, Either.Right(Unit)) + withProxyCredForUser(userId1.toDao(), ProxyCredentialsEntity("user", "pass")) + } - @Mock - internal val configRepository = configure(mock(ServerConfigRepository::class)) { stubsUnitByDefault = true } + runTest { + updateApiVersionsUseCase() + advanceUntilIdle() + } - private lateinit var updateApiVersionsUseCase: UpdateApiVersionsUseCase + verify(arrangement.tokenStorage) + .suspendFunction(arrangement.tokenStorage::proxyCredentials) + .with(any()) + .wasInvoked(exactly = once) - @BeforeTest - fun setup() { - updateApiVersionsUseCase = UpdateApiVersionsUseCaseImpl() + verify(arrangement.serverConfigRepository1) + .suspendFunction(arrangement.serverConfigRepository1::updateConfigApiVersion) + .with(any()) + .wasInvoked(exactly = once) } @Test - fun givenConfigList_whenUpdatingApiVersions_thenALLMUSTBEUPDATED() = runTest { - val configList = listOf(newServerConfig(1), newServerConfig(2), newServerConfig(3), newServerConfig(4)) - - given(configRepository) - .suspendFunction(configRepository::configList) - .whenInvoked() - .thenReturn( - Either.Right(configList) - ) - - given(configRepository) - .suspendFunction(configRepository::updateConfigApiVersion) - .whenInvokedWith(any()) - .then { Either.Right(Unit) } - - updateApiVersionsUseCase() - - verify(configRepository) - .suspendFunction(configRepository::updateConfigApiVersion) - .with(OneOfMatcher(configList.map { it.id })) - .wasInvoked(exactly = Times(configList.size)) + fun givenMultipleUsers_thenUpdateApiVersionForAll() { + val (arrangement, updateApiVersionsUseCase) = Arrangement() + .arrange { + withValidSessionWithServerConfig( + Either.Right( + mapOf( + userId1 to serverConfig1.copy( + links = serverConfig1.links.copy( + apiProxy = ServerConfig.ApiProxy( + host = "host", + port = 1234, + needsAuthentication = false + ) + ) + ), + userId2 to serverConfig2.copy( + links = serverConfig2.links.copy( + apiProxy = ServerConfig.ApiProxy( + host = "host", + port = 1234, + needsAuthentication = true + ) + ) + ) + ) + ) + ) + withUpdateConfigApiVersion(serverConfig1, Either.Right(Unit)) + withUpdateConfigApiVersion(serverConfig2, Either.Right(Unit)) + withProxyCredForUser(userId2.toDao(), ProxyCredentialsEntity("user", "pass")) + } + + runTest { + updateApiVersionsUseCase() + advanceUntilIdle() + } + + verify(arrangement.tokenStorage) + .suspendFunction(arrangement.tokenStorage::proxyCredentials) + .with(eq(userId2.toDao())) + .wasInvoked(exactly = once) + + verify(arrangement.tokenStorage) + .suspendFunction(arrangement.tokenStorage::proxyCredentials) + .with(eq(userId1.toDao())) + .wasNotInvoked() + + verify(arrangement.serverConfigRepository1) + .suspendFunction(arrangement.serverConfigRepository1::updateConfigApiVersion) + .with(any()) + .wasInvoked(exactly = once) + + verify(arrangement.serverConfigRepository2) + .suspendFunction(arrangement.serverConfigRepository2::updateConfigApiVersion) + .with(any()) + .wasInvoked(exactly = once) + + } + private companion object { + val userId1: UserId = UserId("user1", "domnaion1") + val userId2: UserId = UserId("user2", "domnaion2") + val serverConfig1 = newServerConfig(1) + val serverConfig2 = newServerConfig(2) + } + + private class Arrangement { + @Mock + val sessionRepository: SessionRepository = mock(SessionRepository::class) + + @Mock + val tokenStorage: AuthTokenStorage = mock(AuthTokenStorage::class) + + var serverConfigProviderCalledCount: Int = 0 + private set + + @Mock + val serverConfigRepository1: ServerConfigRepository = mock(ServerConfigRepository::class) + + @Mock + val serverConfigRepository2: ServerConfigRepository = mock(ServerConfigRepository::class) + + fun withProxyCredForUser( + userId: UserIDEntity, + result: ProxyCredentialsEntity? + ) { + given(tokenStorage) + .function(tokenStorage::proxyCredentials) + .whenInvokedWith(eq(userId)) + .then { result } + } + + fun withUpdateConfigApiVersion( + serverConfig: ServerConfig, + result: Either + ) { + when (serverConfig.id) { + serverConfig1.id -> given(serverConfigRepository1) + .suspendFunction(serverConfigRepository1::updateConfigApiVersion) + .whenInvokedWith(any()) + .then { result } + + serverConfig2.id -> given(serverConfigRepository2) + .suspendFunction(serverConfigRepository2::updateConfigApiVersion) + .whenInvokedWith(any()) + .then { result } + + else -> throw IllegalArgumentException("Unexpected server config: $serverConfig") + } + } + + fun withValidSessionWithServerConfig( + result: Either> + ) { + given(sessionRepository) + .suspendFunction(sessionRepository::validSessionsWithServerConfig) + .whenInvoked() + .then { result } + } + + private val updateApiVersionsUseCase = UpdateApiVersionsUseCaseImpl( + sessionRepository, + tokenStorage, + { serverConfig: ServerConfig, proxyCredentials: ProxyCredentials? -> + serverConfigProviderCalledCount++ + when (serverConfig.id) { + serverConfig1.id -> serverConfigRepository1 + serverConfig2.id -> serverConfigRepository2 + else -> throw IllegalArgumentException("Unexpected server config: $serverConfig") + } + } + ) + + fun arrange(block: Arrangement.() -> Unit) = apply(block).let { this to updateApiVersionsUseCase } } } diff --git a/persistence/src/commonMain/db_global/com/wire/kalium/persistence/Accounts.sq b/persistence/src/commonMain/db_global/com/wire/kalium/persistence/Accounts.sq index f503bf4e10f..3130400058e 100644 --- a/persistence/src/commonMain/db_global/com/wire/kalium/persistence/Accounts.sq +++ b/persistence/src/commonMain/db_global/com/wire/kalium/persistence/Accounts.sq @@ -68,3 +68,25 @@ UPDATE Accounts SET managed_by = :managedBy WHERE id = :userId; managedBy: SELECT managed_by FROM Accounts WHERE id = :userId; + +allValidAccountsWithServerConfig: +SELECT Accounts.id AS userId, + ServerConfiguration.id AS serverConfigId, + ServerConfiguration.apiBaseUrl, + ServerConfiguration.accountBaseUrl, + ServerConfiguration.webSocketBaseUrl, + ServerConfiguration.blackListUrl, + ServerConfiguration.teamsUrl, + ServerConfiguration.websiteUrl, + ServerConfiguration.isOnPremises, + ServerConfiguration.domain, + ServerConfiguration.commonApiVersion, + ServerConfiguration.federation, + ServerConfiguration.apiProxyHost, + ServerConfiguration.apiProxyPort, + ServerConfiguration.apiProxyNeedsAuthentication, + ServerConfiguration.title +FROM Accounts +INNER JOIN ServerConfiguration + ON Accounts.server_config_id = ServerConfiguration.id +WHERE Accounts.logout_reason IS NULL; diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/AccountsDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/AccountsDAO.kt index c4277bcd88c..916c55b0d1c 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/AccountsDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/AccountsDAO.kt @@ -25,6 +25,7 @@ import com.wire.kalium.persistence.dao.ManagedByEntity import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.UserIDEntity import com.wire.kalium.persistence.model.LogoutReason +import com.wire.kalium.persistence.model.ServerConfigEntity import com.wire.kalium.persistence.model.SsoIdEntity import com.wire.kalium.persistence.util.mapToList import com.wire.kalium.persistence.util.mapToOneOrNull @@ -95,6 +96,55 @@ internal object AccountMapper { } else { SsoIdEntity(scim_external_id, subject, tenant) } + + fun fromUserIDWithServerConfig( + userId: QualifiedIDEntity, + serverConfigId: String, + apiBaseUrl: String, + accountBaseUrl: String, + webSocketBaseUrl: String, + blackListUrl: String, + teamsUrl: String, + websiteUrl: String, + isOnPremises: Boolean, + domain: String?, + commonApiVersion: Int, + federation: Boolean, + apiProxyHost: String?, + apiProxyPort: Int?, + apiProxyNeedsAuthentication: Boolean?, + title: String + ): Pair { + val apiProxy: ServerConfigEntity.ApiProxy? = + if (apiProxyHost != null && apiProxyPort != null && apiProxyNeedsAuthentication != null) { + ServerConfigEntity.ApiProxy(apiProxyNeedsAuthentication, apiProxyHost, apiProxyPort) + } else { + null + } + + val serverConfig = ServerConfigEntity( + id = serverConfigId, + links = ServerConfigEntity.Links( + api = apiBaseUrl, + accounts = accountBaseUrl, + webSocket = webSocketBaseUrl, + blackList = blackListUrl, + teams = teamsUrl, + website = websiteUrl, + isOnPremises = isOnPremises, + apiProxy = apiProxy, + title = title + ), + metaData = ServerConfigEntity.MetaData( + domain = domain, + apiVersion = commonApiVersion, + federation = federation, + ) + ) + + return userId to serverConfig + } + } @Suppress("TooManyFunctions") @@ -126,6 +176,7 @@ interface AccountsDAO { fun fullAccountInfo(userIDEntity: UserIDEntity): FullAccountEntity? suspend fun getAllValidAccountPersistentWebSocketStatus(): Flow> suspend fun getAccountManagedBy(userIDEntity: UserIDEntity): ManagedByEntity? + suspend fun validAccountWithServerConfigId(): Map } @Suppress("TooManyFunctions") @@ -251,6 +302,10 @@ internal class AccountsDAOImpl internal constructor( queries.managedBy(userIDEntity).executeAsOneOrNull()?.managed_by } + override suspend fun validAccountWithServerConfigId(): Map = withContext(queriesContext) { + queries.allValidAccountsWithServerConfig(mapper = mapper::fromUserIDWithServerConfig).executeAsList().toMap() + } + override suspend fun accountInfo(userIDEntity: UserIDEntity): AccountInfoEntity? = withContext(queriesContext) { queries.accountInfo(userIDEntity, mapper = mapper::fromAccount).executeAsOneOrNull() }