Allow configuration to provide multiple account providers. (#4742)

* Allow several account provider in configuration

* Target latest private modules.

* Fix tests

* Target latest private modules.

* Trigger CI

* Fix formatting issue
This commit is contained in:
Benoit Marty 2025-05-20 08:57:01 +02:00 committed by GitHub
parent 6995e62548
commit c6f6c2cd65
13 changed files with 58 additions and 40 deletions

@ -1 +1 @@
Subproject commit c754703e720bcb20f5dfa1ea0dcd32976825f71c Subproject commit d3dffc97bf8b39386ace2db7d857bbff05c73c18

View file

@ -13,7 +13,7 @@ import io.element.android.libraries.matrix.api.core.SessionId
interface EnterpriseService { interface EnterpriseService {
val isEnterpriseBuild: Boolean val isEnterpriseBuild: Boolean
suspend fun isEnterpriseUser(sessionId: SessionId): Boolean suspend fun isEnterpriseUser(sessionId: SessionId): Boolean
fun defaultHomeserver(): String? fun defaultHomeserverList(): List<String>
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
fun semanticColorsLight(): SemanticColors fun semanticColorsLight(): SemanticColors

View file

@ -22,7 +22,7 @@ class DefaultEnterpriseService @Inject constructor() : EnterpriseService {
override suspend fun isEnterpriseUser(sessionId: SessionId) = false override suspend fun isEnterpriseUser(sessionId: SessionId) = false
override fun defaultHomeserver() = null override fun defaultHomeserverList(): List<String> = emptyList()
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
override fun semanticColorsLight(): SemanticColors = compoundColorsLight override fun semanticColorsLight(): SemanticColors = compoundColorsLight

View file

@ -21,9 +21,9 @@ class DefaultEnterpriseServiceTest {
} }
@Test @Test
fun `defaultHomeserver should return null`() { fun `defaultHomeserverList should return empty list`() {
val defaultEnterpriseService = DefaultEnterpriseService() val defaultEnterpriseService = DefaultEnterpriseService()
assertThat<String?>(defaultEnterpriseService.defaultHomeserver()).isNull() assertThat(defaultEnterpriseService.defaultHomeserverList()).isEmpty()
} }
@Test @Test

View file

@ -16,7 +16,7 @@ import io.element.android.tests.testutils.simulateLongTask
class FakeEnterpriseService( class FakeEnterpriseService(
override val isEnterpriseBuild: Boolean = false, override val isEnterpriseBuild: Boolean = false,
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() }, private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
private val defaultHomeserverResult: () -> String? = { A_FAKE_HOMESERVER }, private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() }, private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() }, private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() },
private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() }, private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() },
@ -27,8 +27,8 @@ class FakeEnterpriseService(
isEnterpriseUserResult(sessionId) isEnterpriseUserResult(sessionId)
} }
override fun defaultHomeserver(): String? { override fun defaultHomeserverList(): List<String> {
return defaultHomeserverResult() return defaultHomeserverListResult()
} }
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean = simulateLongTask { override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean = simulateLongTask {

View file

@ -20,14 +20,15 @@ import javax.inject.Inject
class AccountProviderDataSource @Inject constructor( class AccountProviderDataSource @Inject constructor(
enterpriseService: EnterpriseService, enterpriseService: EnterpriseService,
) { ) {
private val defaultAccountProvider = (enterpriseService.defaultHomeserver() ?: AuthenticationConfig.MATRIX_ORG_URL).let { url -> private val defaultAccountProvider = (enterpriseService.defaultHomeserverList().firstOrNull() ?: AuthenticationConfig.MATRIX_ORG_URL)
AccountProvider( .let { url ->
url = url, AccountProvider(
subtitle = null, url = url,
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL, subtitle = null,
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL, isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
) isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
} )
}
private val accountProvider: MutableStateFlow<AccountProvider> = MutableStateFlow( private val accountProvider: MutableStateFlow<AccountProvider> = MutableStateFlow(
defaultAccountProvider defaultAccountProvider

View file

@ -8,29 +8,39 @@
package io.element.android.features.login.impl.screens.changeaccountprovider package io.element.android.features.login.impl.screens.changeaccountprovider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import io.element.android.appconfig.AuthenticationConfig import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.features.login.impl.changeserver.ChangeServerState
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.uri.ensureProtocol
import javax.inject.Inject import javax.inject.Inject
class ChangeAccountProviderPresenter @Inject constructor( class ChangeAccountProviderPresenter @Inject constructor(
private val changeServerPresenter: Presenter<ChangeServerState>, private val changeServerPresenter: Presenter<ChangeServerState>,
private val enterpriseService: EnterpriseService,
) : Presenter<ChangeAccountProviderState> { ) : Presenter<ChangeAccountProviderState> {
@Composable @Composable
override fun present(): ChangeAccountProviderState { override fun present(): ChangeAccountProviderState {
val staticAccountProviderList = remember {
enterpriseService.defaultHomeserverList()
.map { it.ensureProtocol() }
.ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
.map { url ->
AccountProvider(
url = url,
subtitle = null,
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
isValid = true,
)
}
}
val changeServerState = changeServerPresenter.present() val changeServerState = changeServerPresenter.present()
return ChangeAccountProviderState( return ChangeAccountProviderState(
// Just matrix.org by default for now accountProviders = staticAccountProviderList,
accountProviders = listOf(
AccountProvider(
url = AuthenticationConfig.MATRIX_ORG_URL,
subtitle = null,
isPublic = true,
isMatrixOrg = true,
isValid = true,
)
),
changeServerState = changeServerState, changeServerState = changeServerState,
) )
} }

View file

@ -11,7 +11,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.features.login.impl.changeserver.ChangeServerState
// Do not use default value, so no member get forgotten in the presenters. // Do not use default value, so no member get forgotten in the presenters.
data class ChangeAccountProviderState constructor( data class ChangeAccountProviderState(
val accountProviders: List<AccountProvider>, val accountProviders: List<AccountProvider>,
val changeServerState: ChangeServerState, val changeServerState: ChangeServerState,
) )

View file

@ -27,11 +27,11 @@ class AccountProviderDataSourceTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState).isEqualTo( assertThat(initialState).isEqualTo(
AccountProvider( AccountProvider(
url = FakeEnterpriseService.A_FAKE_HOMESERVER, url = AuthenticationConfig.MATRIX_ORG_URL,
title = FakeEnterpriseService.A_FAKE_HOMESERVER, title = "matrix.org",
subtitle = null, subtitle = null,
isPublic = false, isPublic = true,
isMatrixOrg = false, isMatrixOrg = true,
isValid = false, isValid = false,
) )
) )
@ -40,9 +40,11 @@ class AccountProviderDataSourceTest {
@Test @Test
fun `present - initial state - matrix org`() = runTest { fun `present - initial state - matrix org`() = runTest {
val sut = AccountProviderDataSource(FakeEnterpriseService( val sut = AccountProviderDataSource(
defaultHomeserverResult = { AuthenticationConfig.MATRIX_ORG_URL } FakeEnterpriseService(
)) defaultHomeserverListResult = { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
)
)
sut.flow.test { sut.flow.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState).isEqualTo( assertThat(initialState).isEqualTo(
@ -63,7 +65,7 @@ class AccountProviderDataSourceTest {
val sut = AccountProviderDataSource(FakeEnterpriseService()) val sut = AccountProviderDataSource(FakeEnterpriseService())
sut.flow.test { sut.flow.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.url).isEqualTo(FakeEnterpriseService.A_FAKE_HOMESERVER) assertThat(initialState.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL)
sut.userSelection(AccountProvider(url = "https://example.com")) sut.userSelection(AccountProvider(url = "https://example.com"))
val changedState = awaitItem() val changedState = awaitItem()
assertThat(changedState).isEqualTo( assertThat(changedState).isEqualTo(
@ -78,7 +80,7 @@ class AccountProviderDataSourceTest {
) )
sut.reset() sut.reset()
val resetState = awaitItem() val resetState = awaitItem()
assertThat(resetState.url).isEqualTo(FakeEnterpriseService.A_FAKE_HOMESERVER) assertThat(resetState.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL)
} }
} }
} }

View file

@ -11,6 +11,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProvider
import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.features.login.impl.changeserver.aChangeServerState
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
@ -25,7 +26,8 @@ class ChangeAccountProviderPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = ChangeAccountProviderPresenter( val presenter = ChangeAccountProviderPresenter(
changeServerPresenter = { aChangeServerState() } changeServerPresenter = { aChangeServerState() },
enterpriseService = FakeEnterpriseService(),
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()

View file

@ -11,6 +11,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
@ -44,7 +45,7 @@ class ConfirmAccountProviderPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.isAccountCreation).isFalse() assertThat(initialState.isAccountCreation).isFalse()
assertThat(initialState.submitEnabled).isTrue() assertThat(initialState.submitEnabled).isTrue()
assertThat(initialState.accountProvider.url).isEqualTo(FakeEnterpriseService.A_FAKE_HOMESERVER) assertThat(initialState.accountProvider.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL)
assertThat(initialState.loginFlow).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.loginFlow).isEqualTo(AsyncData.Uninitialized)
} }
} }

View file

@ -8,6 +8,7 @@
package io.element.android.features.login.impl.screens.createaccount package io.element.android.features.login.impl.screens.createaccount
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.libraries.matrix.api.auth.external.ExternalSession import io.element.android.libraries.matrix.api.auth.external.ExternalSession
@ -60,7 +61,7 @@ class DefaultMessageParserTest {
// missing homeServer // missing homeServer
assertThat(sut.parse(validMessage.replace(""""home_server": "home_server",""", ""))).isEqualTo( assertThat(sut.parse(validMessage.replace(""""home_server": "home_server",""", ""))).isEqualTo(
anExternalSession( anExternalSession(
homeserverUrl = FakeEnterpriseService.A_FAKE_HOMESERVER, homeserverUrl = AuthenticationConfig.MATRIX_ORG_URL,
) )
) )
} }

View file

@ -8,6 +8,7 @@
package io.element.android.features.login.impl.screens.loginpassword package io.element.android.features.login.impl.screens.loginpassword
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
@ -33,7 +34,7 @@ class LoginPasswordPresenterTest {
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
createLoginPasswordPresenter().test { createLoginPasswordPresenter().test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.accountProvider.url).isEqualTo(FakeEnterpriseService.A_FAKE_HOMESERVER) assertThat(initialState.accountProvider.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL)
assertThat(initialState.formState).isEqualTo(LoginFormState.Default) assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
assertThat(initialState.loginAction).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.loginAction).isEqualTo(AsyncData.Uninitialized)
assertThat(initialState.submitEnabled).isFalse() assertThat(initialState.submitEnabled).isFalse()