Merge pull request #5692 from element-hq/feature/bma/loginFlow

Improve account provider selection during the login flow
This commit is contained in:
Benoit Marty 2025-11-07 16:48:03 +01:00 committed by GitHub
commit 3e567d9b5a
36 changed files with 366 additions and 254 deletions

View file

@ -60,11 +60,12 @@ class ChangeServerPresenter(
title = data.title, title = data.title,
accountProviderUrl = data.url, accountProviderUrl = data.url,
) )
authenticationService.setHomeserver(data.url).map { val details = authenticationService.setHomeserver(data.url).getOrThrow()
authenticationService.getHomeserverDetails().value!! if (!details.isSupported) {
// Valid, remember user choice throw ChangeServerError.UnsupportedServer
accountProviderDataSource.userSelection(data) }
}.getOrThrow() // Homeserver is valid, remember user choice
accountProviderDataSource.userSelection(data)
}.runCatchingUpdatingState(changeServerAction, errorTransform = ChangeServerError::from) }.runCatchingUpdatingState(changeServerAction, errorTransform = ChangeServerError::from)
} }
} }

View file

@ -10,13 +10,12 @@ package io.element.android.features.login.impl.changeserver
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.features.login.impl.error.ChangeServerError
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.ui.strings.CommonStrings
open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerState> { open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerState> {
override val values: Sequence<ChangeServerState> override val values: Sequence<ChangeServerState>
get() = sequenceOf( get() = sequenceOf(
aChangeServerState(), aChangeServerState(),
aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.Error(CommonStrings.error_unknown))), aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.Error(null))),
aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.SlidingSyncAlert)), aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.SlidingSyncAlert)),
aChangeServerState( aChangeServerState(
changeServerAction = AsyncData.Failure( changeServerAction = AsyncData.Failure(
@ -34,6 +33,11 @@ open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerStat
), ),
) )
), ),
aChangeServerState(
changeServerAction = AsyncData.Failure(
ChangeServerError.UnsupportedServer
)
),
) )
} }

View file

@ -26,6 +26,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.LocalBuildMeta import io.element.android.libraries.designsystem.theme.LocalBuildMeta
import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun ChangeServerView( fun ChangeServerView(
@ -39,10 +40,26 @@ fun ChangeServerView(
when (state.changeServerAction) { when (state.changeServerAction) {
is AsyncData.Failure -> { is AsyncData.Failure -> {
when (val error = state.changeServerAction.error as? ChangeServerError) { when (val error = state.changeServerAction.error as? ChangeServerError) {
ChangeServerError.InvalidServer ->
ErrorDialog(
modifier = modifier,
content = stringResource(R.string.screen_change_server_error_invalid_homeserver),
onSubmit = {
eventSink.invoke(ChangeServerEvents.ClearError)
}
)
ChangeServerError.UnsupportedServer ->
ErrorDialog(
modifier = modifier,
content = stringResource(R.string.screen_login_error_unsupported_authentication),
onSubmit = {
eventSink.invoke(ChangeServerEvents.ClearError)
}
)
is ChangeServerError.Error -> { is ChangeServerError.Error -> {
ErrorDialog( ErrorDialog(
modifier = modifier, modifier = modifier,
content = error.message(), content = error.messageStr ?: stringResource(CommonStrings.error_unknown),
onSubmit = { onSubmit = {
eventSink.invoke(ChangeServerEvents.ClearError) eventSink.invoke(ChangeServerEvents.ClearError)
} }

View file

@ -7,24 +7,13 @@
package io.element.android.features.login.impl.error package io.element.android.features.login.impl.error
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.stringResource
import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
import io.element.android.libraries.matrix.api.auth.AuthenticationException import io.element.android.libraries.matrix.api.auth.AuthenticationException
import io.element.android.libraries.ui.strings.CommonStrings
sealed class ChangeServerError : Exception() { sealed class ChangeServerError : Exception() {
data class Error( data class Error(
@StringRes val messageId: Int? = null,
val messageStr: String? = null, val messageStr: String? = null,
) : ChangeServerError() { ) : ChangeServerError()
@Composable
@ReadOnlyComposable
fun message(): String = messageStr ?: stringResource(messageId ?: CommonStrings.error_unknown)
}
data class NeedElementPro( data class NeedElementPro(
val unauthorisedAccountProviderTitle: String, val unauthorisedAccountProviderTitle: String,
@ -37,11 +26,23 @@ sealed class ChangeServerError : Exception() {
) : ChangeServerError() ) : ChangeServerError()
data object SlidingSyncAlert : ChangeServerError() data object SlidingSyncAlert : ChangeServerError()
data object InvalidServer : ChangeServerError()
data object UnsupportedServer : ChangeServerError()
companion object { companion object {
fun from(error: Throwable): ChangeServerError = when (error) { fun from(error: Throwable): ChangeServerError = when (error) {
is AuthenticationException.SlidingSyncVersion -> SlidingSyncAlert is ChangeServerError -> error
is AuthenticationException.Oidc -> Error(messageStr = error.message) is AuthenticationException -> {
when (error) {
is AuthenticationException.SlidingSyncVersion -> SlidingSyncAlert
is AuthenticationException.InvalidServerName,
is AuthenticationException.ServerUnreachable -> InvalidServer
// AccountAlreadyLoggedIn error should not happen at this point
is AuthenticationException.AccountAlreadyLoggedIn -> Error(messageStr = error.message)
is AuthenticationException.Generic -> Error(messageStr = error.message)
is AuthenticationException.Oidc -> Error(messageStr = error.message)
}
}
is AccountProviderAccessException.NeedElementProException -> NeedElementPro( is AccountProviderAccessException.NeedElementProException -> NeedElementPro(
unauthorisedAccountProviderTitle = error.unauthorisedAccountProviderTitle, unauthorisedAccountProviderTitle = error.unauthorisedAccountProviderTitle,
applicationId = error.applicationId, applicationId = error.applicationId,
@ -50,7 +51,7 @@ sealed class ChangeServerError : Exception() {
unauthorisedAccountProviderTitle = error.unauthorisedAccountProviderTitle, unauthorisedAccountProviderTitle = error.unauthorisedAccountProviderTitle,
authorisedAccountProviderTitles = error.authorisedAccountProviderTitles, authorisedAccountProviderTitles = error.authorisedAccountProviderTitles,
) )
else -> Error(messageId = R.string.screen_change_server_error_invalid_homeserver) else -> Error(messageStr = error.message)
} }
} }
} }

View file

@ -8,14 +8,11 @@
package io.element.android.features.login.impl.error package io.element.android.features.login.impl.error
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.login.impl.R
class ChangeServerErrorProvider : PreviewParameterProvider<ChangeServerError> { class ChangeServerErrorProvider : PreviewParameterProvider<ChangeServerError> {
override val values: Sequence<ChangeServerError> override val values: Sequence<ChangeServerError>
get() = sequenceOf( get() = sequenceOf(
ChangeServerError.Error( ChangeServerError.InvalidServer,
messageId = R.string.screen_change_server_error_invalid_homeserver,
),
ChangeServerError.Error( ChangeServerError.Error(
messageStr = "An error description", messageStr = "An error description",
), ),
@ -28,5 +25,6 @@ class ChangeServerErrorProvider : PreviewParameterProvider<ChangeServerError> {
authorisedAccountProviderTitles = listOf("provider.org", "provider.io"), authorisedAccountProviderTitles = listOf("provider.org", "provider.io"),
), ),
ChangeServerError.SlidingSyncAlert, ChangeServerError.SlidingSyncAlert,
ChangeServerError.UnsupportedServer,
) )
} }

View file

@ -65,8 +65,7 @@ class LoginHelper(
loginHint: String?, loginHint: String?,
) = coroutineScope.launch { ) = coroutineScope.launch {
suspend { suspend {
authenticationService.setHomeserver(homeserverUrl).map { authenticationService.setHomeserver(homeserverUrl).map { matrixHomeServerDetails ->
val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!!
if (matrixHomeServerDetails.supportsOidcLogin) { if (matrixHomeServerDetails.supportsOidcLogin) {
// Retrieve the details right now // Retrieve the details right now
val oidcPrompt = if (isAccountCreation) OidcPrompt.Create else OidcPrompt.Login val oidcPrompt = if (isAccountCreation) OidcPrompt.Create else OidcPrompt.Login

View file

@ -41,9 +41,20 @@ fun LoginModeView(
when (val error = loginMode.error) { when (val error = loginMode.error) {
is ChangeServerError -> { is ChangeServerError -> {
when (error) { when (error) {
ChangeServerError.InvalidServer ->
ErrorDialog(
content = stringResource(R.string.screen_change_server_error_invalid_homeserver),
onSubmit = onClearError,
)
is ChangeServerError.UnsupportedServer -> {
ErrorDialog(
content = stringResource(R.string.screen_login_error_unsupported_authentication),
onSubmit = onClearError,
)
}
is ChangeServerError.Error -> { is ChangeServerError.Error -> {
ErrorDialog( ErrorDialog(
content = error.message(), content = error.messageStr ?: stringResource(CommonStrings.error_unknown),
onSubmit = onClearError, onSubmit = onClearError,
) )
} }
@ -91,7 +102,7 @@ fun LoginModeView(
} }
is AuthenticationException.AccountAlreadyLoggedIn -> { is AuthenticationException.AccountAlreadyLoggedIn -> {
ErrorDialog( ErrorDialog(
content = stringResource(CommonStrings.error_account_already_logged_in, error.message.orEmpty()), content = stringResource(CommonStrings.error_account_already_logged_in, error.userId),
onSubmit = onClearError, onSubmit = onClearError,
) )
} }

View file

@ -52,9 +52,10 @@ class HomeserverResolver(
} }
} }
} }
// If list is empty, and the user has entered an URL, do not block the user. // If list is empty, and candidateBase is a valid an URL, do not block the user.
if (currentList.isEmpty() && trimmedUserInput.isValidUrl()) { // A unsupported homeserver / homeserver not found error will be displayed if the user continues
emit(listOf(HomeserverData(homeserverUrl = trimmedUserInput))) if (currentList.isEmpty() && candidateBase.isValidUrl()) {
emit(listOf(HomeserverData(homeserverUrl = candidateBase)))
} }
} }

View file

@ -43,5 +43,5 @@ fun aHomeserverDataList(): List<HomeserverData> {
fun aHomeserverData( fun aHomeserverData(
homeserverUrl: String = AuthenticationConfig.MATRIX_ORG_URL, homeserverUrl: String = AuthenticationConfig.MATRIX_ORG_URL,
): HomeserverData { ): HomeserverData {
return HomeserverData(homeserverUrl = homeserverUrl,) return HomeserverData(homeserverUrl = homeserverUrl)
} }

View file

@ -18,9 +18,10 @@ import io.element.android.features.wellknown.test.FakeWellknownRetriever
import io.element.android.features.wellknown.test.anElementWellKnown import io.element.android.features.wellknown.test.anElementWellKnown
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.WellknownRetriever import io.element.android.libraries.wellknown.api.WellknownRetriever
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
@ -46,7 +47,11 @@ class ChangeServerPresenterTest {
@Test @Test
fun `present - change server ok`() = runTest { fun `present - change server ok`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
},
)
createPresenter( createPresenter(
authenticationService = authenticationService, authenticationService = authenticationService,
enterpriseService = FakeEnterpriseService( enterpriseService = FakeEnterpriseService(
@ -55,7 +60,6 @@ class ChangeServerPresenterTest {
).test { ).test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
authenticationService.givenHomeserver(A_HOMESERVER)
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL)))
val loadingState = awaitItem() val loadingState = awaitItem()
assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java) assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java)
@ -66,10 +70,16 @@ class ChangeServerPresenterTest {
@Test @Test
fun `present - change server error`() = runTest { fun `present - change server error`() = runTest {
val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = {
Result.failure(AN_EXCEPTION)
},
)
createPresenter( createPresenter(
enterpriseService = FakeEnterpriseService( enterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = { true }, isAllowedToConnectToHomeserverResult = { true },
), ),
authenticationService = authenticationService,
).test { ).test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
@ -85,6 +95,32 @@ class ChangeServerPresenterTest {
} }
} }
@Test
fun `present - change server unsupported server`() = runTest {
val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = {
Result.success(aMatrixHomeServerDetails())
},
)
createPresenter(
enterpriseService = FakeEnterpriseService(
isAllowedToConnectToHomeserverResult = { true },
),
authenticationService = authenticationService,
).test {
val initialState = awaitItem()
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL)))
val loadingState = awaitItem()
assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java)
val failureState = awaitItem()
assertThat(failureState.changeServerAction).isInstanceOf(AsyncData.Failure::class.java)
assertThat(failureState.changeServerAction.errorOrNull()).isEqualTo(
ChangeServerError.UnsupportedServer
)
}
}
@Test @Test
fun `present - change server not allowed error`() = runTest { fun `present - change server not allowed error`() = runTest {
val isAllowedToConnectToHomeserverResult = lambdaRecorder<String, Boolean> { false } val isAllowedToConnectToHomeserverResult = lambdaRecorder<String, Boolean> { false }

View file

@ -7,9 +7,6 @@
package io.element.android.features.login.impl.screens.changeaccountprovider package io.element.android.features.login.impl.screens.changeaccountprovider
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
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.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService
@ -18,6 +15,7 @@ import io.element.android.features.login.impl.changeserver.aChangeServerState
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2 import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -34,9 +32,7 @@ class ChangeAccountProviderPresenterTest {
defaultHomeserverListResult = { emptyList() } defaultHomeserverListResult = { emptyList() }
), ),
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.accountProviders).isEqualTo( assertThat(initialState.accountProviders).isEqualTo(
listOf( listOf(
@ -63,9 +59,7 @@ class ChangeAccountProviderPresenterTest {
} }
), ),
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.accountProviders).isEqualTo( assertThat(initialState.accountProviders).isEqualTo(
listOf( listOf(
@ -99,9 +93,7 @@ class ChangeAccountProviderPresenterTest {
} }
), ),
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.accountProviders).isEqualTo( assertThat(initialState.accountProviders).isEqualTo(
listOf( listOf(

View file

@ -95,7 +95,11 @@ class ChooseAccountProviderPresenterTest {
@Test @Test
fun `present - select account provider and continue - error then clear error`() = runTest { fun `present - select account provider and continue - error then clear error`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = {
Result.failure(AN_EXCEPTION)
},
)
val presenter = createPresenter( val presenter = createPresenter(
enterpriseService = FakeEnterpriseService( enterpriseService = FakeEnterpriseService(
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) },
@ -111,7 +115,6 @@ class ChooseAccountProviderPresenterTest {
} }
awaitItem().also { awaitItem().also {
assertThat(it.selectedAccountProvider).isEqualTo(accountProvider1) assertThat(it.selectedAccountProvider).isEqualTo(accountProvider1)
authenticationService.givenChangeServerError(AN_EXCEPTION)
it.eventSink(ChooseAccountProviderEvents.Continue) it.eventSink(ChooseAccountProviderEvents.Continue)
skipItems(1) // Loading skipItems(1) // Loading

View file

@ -7,9 +7,6 @@
package io.element.android.features.login.impl.screens.confirmaccountprovider package io.element.android.features.login.impl.screens.confirmaccountprovider
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
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.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService
@ -22,13 +19,13 @@ import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationR
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oidc.api.OidcAction
import io.element.android.libraries.oidc.api.OidcActionFlow import io.element.android.libraries.oidc.api.OidcActionFlow
import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -40,9 +37,7 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - initial test`() = runTest { fun `present - initial test`() = runTest {
val presenter = createConfirmAccountProviderPresenter() val presenter = createConfirmAccountProviderPresenter()
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.isAccountCreation).isFalse() assertThat(initialState.isAccountCreation).isFalse()
assertThat(initialState.submitEnabled).isTrue() assertThat(initialState.submitEnabled).isTrue()
@ -53,14 +48,15 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - continue password login`() = runTest { fun `present - continue password login`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsPasswordLogin = true))
},
)
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
) )
authenticationService.givenHomeserver(A_HOMESERVER) presenter.test {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
@ -75,14 +71,15 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - continue oidc`() = runTest { fun `present - continue oidc`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
},
)
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
) )
authenticationService.givenHomeserver(A_HOMESERVER_OIDC) presenter.test {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
@ -97,16 +94,17 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - oidc - cancel with failure`() = runTest { fun `present - oidc - cancel with failure`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
},
)
val defaultOidcActionFlow = FakeOidcActionFlow() val defaultOidcActionFlow = FakeOidcActionFlow()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
defaultOidcActionFlow = defaultOidcActionFlow, defaultOidcActionFlow = defaultOidcActionFlow,
) )
authenticationService.givenHomeserver(A_HOMESERVER_OIDC) presenter.test {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
@ -125,16 +123,17 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - oidc - cancel with success`() = runTest { fun `present - oidc - cancel with success`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
},
)
val defaultOidcActionFlow = FakeOidcActionFlow() val defaultOidcActionFlow = FakeOidcActionFlow()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
defaultOidcActionFlow = defaultOidcActionFlow, defaultOidcActionFlow = defaultOidcActionFlow,
) )
authenticationService.givenHomeserver(A_HOMESERVER_OIDC) presenter.test {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
@ -152,16 +151,17 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - oidc - cancel to unblock`() = runTest { fun `present - oidc - cancel to unblock`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
},
)
val defaultOidcActionFlow = FakeOidcActionFlow() val defaultOidcActionFlow = FakeOidcActionFlow()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
defaultOidcActionFlow = defaultOidcActionFlow, defaultOidcActionFlow = defaultOidcActionFlow,
) )
authenticationService.givenHomeserver(A_HOMESERVER_OIDC) presenter.test {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
@ -175,16 +175,17 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - oidc - success with failure`() = runTest { fun `present - oidc - success with failure`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
},
)
val defaultOidcActionFlow = FakeOidcActionFlow() val defaultOidcActionFlow = FakeOidcActionFlow()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
defaultOidcActionFlow = defaultOidcActionFlow, defaultOidcActionFlow = defaultOidcActionFlow,
) )
authenticationService.givenHomeserver(A_HOMESERVER_OIDC) presenter.test {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
@ -205,16 +206,17 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - oidc - success with success`() = runTest { fun `present - oidc - success with success`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
},
)
val defaultOidcActionFlow = FakeOidcActionFlow() val defaultOidcActionFlow = FakeOidcActionFlow()
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
defaultOidcActionFlow = defaultOidcActionFlow, defaultOidcActionFlow = defaultOidcActionFlow,
) )
authenticationService.givenHomeserver(A_HOMESERVER_OIDC) presenter.test {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
val loadingState = awaitItem() val loadingState = awaitItem()
@ -232,15 +234,16 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - submit fails`() = runTest { fun `present - submit fails`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = {
Result.failure(AN_EXCEPTION)
},
)
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
authenticationService.givenChangeServerError(RuntimeException())
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading skipItems(1) // Loading
val failureState = awaitItem() val failureState = awaitItem()
@ -251,17 +254,18 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - clear error`() = runTest { fun `present - clear error`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = {
Result.failure(AN_EXCEPTION)
},
)
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
// Submit will return an error // Submit will return an error
authenticationService.givenChangeServerError(AN_EXCEPTION)
initialState.eventSink(ConfirmAccountProviderEvents.Continue) initialState.eventSink(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading skipItems(1) // Loading
@ -279,8 +283,11 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - confirm account creation without oidc and without url generates an error`() = runTest { fun `present - confirm account creation without oidc and without url generates an error`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
authenticationService.givenHomeserver(A_HOMESERVER) setHomeserverResult = {
Result.success(aMatrixHomeServerDetails())
},
)
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
params = ConfirmAccountProviderPresenter.Params(isAccountCreation = true), params = ConfirmAccountProviderPresenter.Params(isAccountCreation = true),
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
@ -288,9 +295,7 @@ class ConfirmAccountProviderPresenterTest {
throw AccountCreationNotSupported() throw AccountCreationNotSupported()
}, },
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(ConfirmAccountProviderEvents.Continue) initialState.eventSink(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading skipItems(1) // Loading
@ -306,15 +311,16 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - confirm account creation with oidc is successful`() = runTest { fun `present - confirm account creation with oidc is successful`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
authenticationService.givenHomeserver(A_HOMESERVER_OIDC) setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
},
)
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
params = ConfirmAccountProviderPresenter.Params(isAccountCreation = true), params = ConfirmAccountProviderPresenter.Params(isAccountCreation = true),
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(ConfirmAccountProviderEvents.Continue) initialState.eventSink(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading skipItems(1) // Loading
@ -327,16 +333,17 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - confirm account creation with oidc and url continues with oidc`() = runTest { fun `present - confirm account creation with oidc and url continues with oidc`() = runTest {
val aUrl = "aUrl" val aUrl = "aUrl"
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
authenticationService.givenHomeserver(A_HOMESERVER_OIDC) setHomeserverResult = {
Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true))
},
)
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
params = ConfirmAccountProviderPresenter.Params(isAccountCreation = true), params = ConfirmAccountProviderPresenter.Params(isAccountCreation = true),
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
webClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever { aUrl }, webClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever { aUrl },
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(ConfirmAccountProviderEvents.Continue) initialState.eventSink(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading skipItems(1) // Loading
@ -349,16 +356,17 @@ class ConfirmAccountProviderPresenterTest {
@Test @Test
fun `present - confirm account creation without oidc and with url continuing with url`() = runTest { fun `present - confirm account creation without oidc and with url continuing with url`() = runTest {
val aUrl = "aUrl" val aUrl = "aUrl"
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
authenticationService.givenHomeserver(A_HOMESERVER) setHomeserverResult = {
Result.success(aMatrixHomeServerDetails())
},
)
val presenter = createConfirmAccountProviderPresenter( val presenter = createConfirmAccountProviderPresenter(
params = ConfirmAccountProviderPresenter.Params(isAccountCreation = true), params = ConfirmAccountProviderPresenter.Params(isAccountCreation = true),
matrixAuthenticationService = authenticationService, matrixAuthenticationService = authenticationService,
webClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever { aUrl }, webClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever { aUrl },
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(ConfirmAccountProviderEvents.Continue) initialState.eventSink(ConfirmAccountProviderEvents.Continue)
skipItems(1) // Loading skipItems(1) // Loading

View file

@ -7,9 +7,6 @@
package io.element.android.features.login.impl.screens.createaccount package io.element.android.features.login.impl.screens.createaccount
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
@ -26,6 +23,7 @@ import io.element.android.libraries.matrix.test.verification.FakeSessionVerifica
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -37,9 +35,7 @@ class CreateAccountPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = createPresenter() val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.url).isEqualTo("aUrl") assertThat(initialState.url).isEqualTo("aUrl")
assertThat(initialState.pageProgress).isEqualTo(0) assertThat(initialState.pageProgress).isEqualTo(0)
@ -51,9 +47,7 @@ class CreateAccountPresenterTest {
@Test @Test
fun `present - set up progress update the state`() = runTest { fun `present - set up progress update the state`() = runTest {
val presenter = createPresenter() val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(CreateAccountEvents.SetPageProgress(33)) initialState.eventSink(CreateAccountEvents.SetPageProgress(33))
assertThat(awaitItem().pageProgress).isEqualTo(33) assertThat(awaitItem().pageProgress).isEqualTo(33)
@ -65,9 +59,7 @@ class CreateAccountPresenterTest {
val presenter = createPresenter( val presenter = createPresenter(
messageParser = FakeMessageParser { error("An error") } messageParser = FakeMessageParser { error("An error") }
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(CreateAccountEvents.OnMessageReceived("")) initialState.eventSink(CreateAccountEvents.OnMessageReceived(""))
assertThat(awaitItem().createAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(awaitItem().createAction).isInstanceOf(AsyncAction.Failure::class.java)
@ -77,9 +69,7 @@ class CreateAccountPresenterTest {
@Test @Test
fun `present - receiving a message containing isTrusted is ignored`() = runTest { fun `present - receiving a message containing isTrusted is ignored`() = runTest {
val presenter = createPresenter() val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(CreateAccountEvents.OnMessageReceived("isTrusted")) initialState.eventSink(CreateAccountEvents.OnMessageReceived("isTrusted"))
} }
@ -98,9 +88,7 @@ class CreateAccountPresenterTest {
messageParser = FakeMessageParser(lambda), messageParser = FakeMessageParser(lambda),
clientProvider = clientProvider, clientProvider = clientProvider,
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(CreateAccountEvents.OnMessageReceived("aMessage")) initialState.eventSink(CreateAccountEvents.OnMessageReceived("aMessage"))
assertThat(awaitItem().createAction.isLoading()).isTrue() assertThat(awaitItem().createAction.isLoading()).isTrue()
@ -118,9 +106,7 @@ class CreateAccountPresenterTest {
), ),
messageParser = FakeMessageParser { anExternalSession() } messageParser = FakeMessageParser { anExternalSession() }
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(CreateAccountEvents.OnMessageReceived("")) initialState.eventSink(CreateAccountEvents.OnMessageReceived(""))
assertThat(awaitItem().createAction.isLoading()).isTrue() assertThat(awaitItem().createAction.isLoading()).isTrue()

View file

@ -14,11 +14,11 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_PASSWORD import io.element.android.libraries.matrix.test.A_PASSWORD
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -42,8 +42,11 @@ class LoginPasswordPresenterTest {
@Test @Test
fun `present - enter login and password`() = runTest { fun `present - enter login and password`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
authenticationService.givenHomeserver(A_HOMESERVER) setHomeserverResult = {
Result.success(aMatrixHomeServerDetails())
},
)
createLoginPasswordPresenter( createLoginPasswordPresenter(
authenticationService = authenticationService, authenticationService = authenticationService,
).test { ).test {
@ -61,8 +64,11 @@ class LoginPasswordPresenterTest {
@Test @Test
fun `present - submit`() = runTest { fun `present - submit`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
authenticationService.givenHomeserver(A_HOMESERVER) setHomeserverResult = {
Result.success(aMatrixHomeServerDetails())
},
)
createLoginPasswordPresenter( createLoginPasswordPresenter(
authenticationService = authenticationService, authenticationService = authenticationService,
).test { ).test {
@ -81,8 +87,11 @@ class LoginPasswordPresenterTest {
@Test @Test
fun `present - submit with error`() = runTest { fun `present - submit with error`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
authenticationService.givenHomeserver(A_HOMESERVER) setHomeserverResult = {
Result.success(aMatrixHomeServerDetails())
},
)
createLoginPasswordPresenter( createLoginPasswordPresenter(
authenticationService = authenticationService, authenticationService = authenticationService,
).test { ).test {
@ -102,8 +111,11 @@ class LoginPasswordPresenterTest {
@Test @Test
fun `present - clear error`() = runTest { fun `present - clear error`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
authenticationService.givenHomeserver(A_HOMESERVER) setHomeserverResult = {
Result.success(aMatrixHomeServerDetails())
},
)
createLoginPasswordPresenter( createLoginPasswordPresenter(
authenticationService = authenticationService, authenticationService = authenticationService,
).test { ).test {

View file

@ -214,7 +214,11 @@ class OnBoardingPresenterTest {
@Test @Test
fun `present - default account provider - login and clear error`() = runTest { fun `present - default account provider - login and clear error`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService(
setHomeserverResult = {
Result.failure(AN_EXCEPTION)
},
)
val presenter = createPresenter( val presenter = createPresenter(
params = OnBoardingNode.Params( params = OnBoardingNode.Params(
accountProvider = A_HOMESERVER_URL, accountProvider = A_HOMESERVER_URL,
@ -231,7 +235,6 @@ class OnBoardingPresenterTest {
skipItems(3) skipItems(3)
awaitItem().also { awaitItem().also {
assertThat(it.defaultAccountProvider).isEqualTo(A_HOMESERVER_URL) assertThat(it.defaultAccountProvider).isEqualTo(A_HOMESERVER_URL)
authenticationService.givenChangeServerError(AN_EXCEPTION)
it.eventSink(OnBoardingEvents.OnSignIn(A_HOMESERVER_URL)) it.eventSink(OnBoardingEvents.OnSignIn(A_HOMESERVER_URL))
skipItems(1) // Loading skipItems(1) // Loading

View file

@ -7,14 +7,12 @@
package io.element.android.features.login.impl.screens.qrcode.intro package io.element.android.features.login.impl.screens.qrcode.intro
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -22,9 +20,7 @@ class QrCodeIntroPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = createQrCodeIntroPresenter() val presenter = createQrCodeIntroPresenter()
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
awaitItem().run { awaitItem().run {
assertThat(appName).isEqualTo("AppName") assertThat(appName).isEqualTo("AppName")
assertThat(desktopAppName).isEqualTo("DesktopAppName") assertThat(desktopAppName).isEqualTo("DesktopAppName")
@ -39,9 +35,7 @@ class QrCodeIntroPresenterTest {
val permissionsPresenter = FakePermissionsPresenter().apply { setPermissionGranted() } val permissionsPresenter = FakePermissionsPresenter().apply { setPermissionGranted() }
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter) val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
val presenter = createQrCodeIntroPresenter(permissionsPresenterFactory = permissionsPresenterFactory) val presenter = createQrCodeIntroPresenter(permissionsPresenterFactory = permissionsPresenterFactory)
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
awaitItem().eventSink(QrCodeIntroEvents.Continue) awaitItem().eventSink(QrCodeIntroEvents.Continue)
assertThat(awaitItem().canContinue).isTrue() assertThat(awaitItem().canContinue).isTrue()
} }
@ -52,9 +46,7 @@ class QrCodeIntroPresenterTest {
val permissionsPresenter = FakePermissionsPresenter() val permissionsPresenter = FakePermissionsPresenter()
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter) val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
val presenter = createQrCodeIntroPresenter(permissionsPresenterFactory = permissionsPresenterFactory) val presenter = createQrCodeIntroPresenter(permissionsPresenterFactory = permissionsPresenterFactory)
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
awaitItem().eventSink(QrCodeIntroEvents.Continue) awaitItem().eventSink(QrCodeIntroEvents.Continue)
assertThat(awaitItem().cameraPermissionState.showDialog).isTrue() assertThat(awaitItem().cameraPermissionState.showDialog).isTrue()
} }

View file

@ -7,9 +7,6 @@
package io.element.android.features.login.impl.screens.qrcode.scan package io.element.android.features.login.impl.screens.qrcode.scan
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
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.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService
@ -34,9 +31,7 @@ class QrCodeScanPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = createQrCodeScanPresenter() val presenter = createQrCodeScanPresenter()
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
awaitItem().run { awaitItem().run {
assertThat(isScanning).isTrue() assertThat(isScanning).isTrue()
assertThat(authenticationAction.isUninitialized()).isTrue() assertThat(authenticationAction.isUninitialized()).isTrue()
@ -114,9 +109,7 @@ class QrCodeScanPresenterTest {
parseQrCodeLoginDataResult = { Result.failure(Exception("Failed to parse QR code")) } parseQrCodeLoginDataResult = { Result.failure(Exception("Failed to parse QR code")) }
) )
val presenter = createQrCodeScanPresenter(qrCodeLoginDataFactory = qrCodeLoginDataFactory) val presenter = createQrCodeScanPresenter(qrCodeLoginDataFactory = qrCodeLoginDataFactory)
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(QrCodeScanEvents.QrCodeScanned(byteArrayOf())) initialState.eventSink(QrCodeScanEvents.QrCodeScanned(byteArrayOf()))
assertThat(awaitItem().isScanning).isFalse() assertThat(awaitItem().isScanning).isFalse()
@ -140,9 +133,7 @@ class QrCodeScanPresenterTest {
} }
qrCodeLoginManager.resetAction = resetAction qrCodeLoginManager.resetAction = resetAction
val presenter = createQrCodeScanPresenter(qrCodeLoginDataFactory = qrCodeLoginDataFactory, qrCodeLoginManager = qrCodeLoginManager) val presenter = createQrCodeScanPresenter(qrCodeLoginDataFactory = qrCodeLoginDataFactory, qrCodeLoginManager = qrCodeLoginManager)
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
// Skip initial item // Skip initial item
skipItems(1) skipItems(1)

View file

@ -7,9 +7,6 @@
package io.element.android.features.login.impl.screens.searchaccountprovider package io.element.android.features.login.impl.screens.searchaccountprovider
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.features.login.impl.changeserver.aChangeServerState
import io.element.android.features.login.impl.resolver.HomeserverResolver import io.element.android.features.login.impl.resolver.HomeserverResolver
@ -18,6 +15,7 @@ import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatib
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@ -34,9 +32,7 @@ class SearchAccountProviderPresenterTest {
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker),
changeServerPresenter = { aChangeServerState() } changeServerPresenter = { aChangeServerState() }
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.userInput).isEmpty() assertThat(initialState.userInput).isEmpty()
assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
@ -50,9 +46,7 @@ class SearchAccountProviderPresenterTest {
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker),
changeServerPresenter = { aChangeServerState() } changeServerPresenter = { aChangeServerState() }
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org")) initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org"))
val withInputState = awaitItem() val withInputState = awaitItem()
@ -76,16 +70,20 @@ class SearchAccountProviderPresenterTest {
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever), homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
changeServerPresenter = { aChangeServerState() } changeServerPresenter = { aChangeServerState() }
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test")) initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
val withInputState = awaitItem() val withInputState = awaitItem()
assertThat(withInputState.userInput).isEqualTo("test") assertThat(withInputState.userInput).isEqualTo("test")
assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java) assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
assertThat(awaitItem().userInputResult).isEqualTo(AsyncData.Uninitialized) assertThat(awaitItem().userInputResult).isEqualTo(
AsyncData.Success(
listOf(
aHomeserverData(homeserverUrl = "https://test"),
)
)
)
} }
} }
@ -105,9 +103,7 @@ class SearchAccountProviderPresenterTest {
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker),
changeServerPresenter = { aChangeServerState() } changeServerPresenter = { aChangeServerState() }
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test")) initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
val withInputState = awaitItem() val withInputState = awaitItem()
@ -147,9 +143,7 @@ class SearchAccountProviderPresenterTest {
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker),
changeServerPresenter = { aChangeServerState() } changeServerPresenter = { aChangeServerState() }
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test")) initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
val withInputState = awaitItem() val withInputState = awaitItem()

View file

@ -7,10 +7,14 @@
package io.element.android.libraries.matrix.api.auth package io.element.android.libraries.matrix.api.auth
sealed class AuthenticationException(message: String) : Exception(message) { sealed class AuthenticationException(message: String?) : Exception(message) {
class AccountAlreadyLoggedIn(userId: String) : AuthenticationException(userId) data class AccountAlreadyLoggedIn(
class InvalidServerName(message: String) : AuthenticationException(message) val userId: String,
class SlidingSyncVersion(message: String) : AuthenticationException(message) ) : AuthenticationException(null)
class Oidc(message: String) : AuthenticationException(message)
class Generic(message: String) : AuthenticationException(message) class InvalidServerName(message: String?) : AuthenticationException(message)
class SlidingSyncVersion(message: String?) : AuthenticationException(message)
class ServerUnreachable(message: String?) : AuthenticationException(message)
class Oidc(message: String?) : AuthenticationException(message)
class Generic(message: String?) : AuthenticationException(message)
} }

View file

@ -13,7 +13,6 @@ import io.element.android.libraries.matrix.api.auth.external.ExternalSession
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.StateFlow
interface MatrixAuthenticationService { interface MatrixAuthenticationService {
/** /**
@ -22,8 +21,12 @@ interface MatrixAuthenticationService {
* Generally this method should not be used directly, prefer using [MatrixClientProvider.getOrRestore] instead. * Generally this method should not be used directly, prefer using [MatrixClientProvider.getOrRestore] instead.
*/ */
suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient> suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient>
fun getHomeserverDetails(): StateFlow<MatrixHomeServerDetails?>
suspend fun setHomeserver(homeserver: String): Result<Unit> /**
* Set the homeserver to use for authentication, and return its details.
*/
suspend fun setHomeserver(homeserver: String): Result<MatrixHomeServerDetails>
suspend fun login(username: String, password: String): Result<SessionId> suspend fun login(username: String, password: String): Result<SessionId>
/** /**

View file

@ -7,12 +7,10 @@
package io.element.android.libraries.matrix.api.auth package io.element.android.libraries.matrix.api.auth
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class MatrixHomeServerDetails( data class MatrixHomeServerDetails(
val url: String, val url: String,
val supportsPasswordLogin: Boolean, val supportsPasswordLogin: Boolean,
val supportsOidcLogin: Boolean, val supportsOidcLogin: Boolean,
) : Parcelable ) {
val isSupported = supportsPasswordLogin || supportsOidcLogin
}

View file

@ -0,0 +1,50 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.auth
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
import org.junit.Test
class MatrixHomeServerDetailsTest {
@Test
fun `if homeserver supports oidc, then it is supported`() {
val sut = aMatrixHomeServerDetails(
supportsOidcLogin = true,
supportsPasswordLogin = false,
)
assertThat(sut.isSupported).isTrue()
}
@Test
fun `if homeserver supports password, then it is supported`() {
val sut = aMatrixHomeServerDetails(
supportsOidcLogin = false,
supportsPasswordLogin = true,
)
assertThat(sut.isSupported).isTrue()
}
@Test
fun `if homeserver supports both, then it is supported`() {
val sut = aMatrixHomeServerDetails(
supportsOidcLogin = true,
supportsPasswordLogin = true,
)
assertThat(sut.isSupported).isTrue()
}
@Test
fun `if homeserver supports none, then it is not supported`() {
val sut = aMatrixHomeServerDetails(
supportsOidcLogin = false,
supportsPasswordLogin = false,
)
assertThat(sut.isSupported).isFalse()
}
}

View file

@ -12,7 +12,6 @@ import org.matrix.rustcomponents.sdk.ClientBuildException
import org.matrix.rustcomponents.sdk.OidcException import org.matrix.rustcomponents.sdk.OidcException
fun Throwable.mapAuthenticationException(): AuthenticationException { fun Throwable.mapAuthenticationException(): AuthenticationException {
val message = this.message ?: "Unknown error"
return when (this) { return when (this) {
is AuthenticationException -> this is AuthenticationException -> this
is ClientBuildException -> when (this) { is ClientBuildException -> when (this) {
@ -20,7 +19,7 @@ fun Throwable.mapAuthenticationException(): AuthenticationException {
is ClientBuildException.InvalidServerName -> AuthenticationException.InvalidServerName(message) is ClientBuildException.InvalidServerName -> AuthenticationException.InvalidServerName(message)
is ClientBuildException.SlidingSyncVersion -> AuthenticationException.SlidingSyncVersion(message) is ClientBuildException.SlidingSyncVersion -> AuthenticationException.SlidingSyncVersion(message)
is ClientBuildException.Sdk -> AuthenticationException.Generic(message) is ClientBuildException.Sdk -> AuthenticationException.Generic(message)
is ClientBuildException.ServerUnreachable -> AuthenticationException.Generic(message) is ClientBuildException.ServerUnreachable -> AuthenticationException.ServerUnreachable(message)
is ClientBuildException.SlidingSync -> AuthenticationException.Generic(message) is ClientBuildException.SlidingSync -> AuthenticationException.Generic(message)
is ClientBuildException.WellKnownDeserializationException -> AuthenticationException.Generic(message) is ClientBuildException.WellKnownDeserializationException -> AuthenticationException.Generic(message)
is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message) is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message)

View file

@ -36,8 +36,6 @@ import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientBuilder import org.matrix.rustcomponents.sdk.ClientBuilder
@ -67,7 +65,6 @@ class RustMatrixAuthenticationService(
// Ideally it would be possible to get the sessionPath from the Client to avoid doing this. // Ideally it would be possible to get the sessionPath from the Client to avoid doing this.
private var sessionPaths: SessionPaths? = null private var sessionPaths: SessionPaths? = null
private var currentClient: Client? = null private var currentClient: Client? = null
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
private val newMatrixClientObservers = mutableListOf<(MatrixClient) -> Unit>() private val newMatrixClientObservers = mutableListOf<(MatrixClient) -> Unit>()
override fun listenToNewMatrixClients(lambda: (MatrixClient) -> Unit) { override fun listenToNewMatrixClients(lambda: (MatrixClient) -> Unit) {
@ -111,9 +108,7 @@ class RustMatrixAuthenticationService(
return passphrase return passphrase
} }
override fun getHomeserverDetails(): StateFlow<MatrixHomeServerDetails?> = currentHomeserver override suspend fun setHomeserver(homeserver: String): Result<MatrixHomeServerDetails> =
override suspend fun setHomeserver(homeserver: String): Result<Unit> =
withContext(coroutineDispatchers.io) { withContext(coroutineDispatchers.io) {
val emptySessionPath = rotateSessionPath() val emptySessionPath = rotateSessionPath()
runCatchingExceptions { runCatchingExceptions {
@ -122,8 +117,7 @@ class RustMatrixAuthenticationService(
} }
currentClient = client currentClient = client
val homeServerDetails = client.homeserverLoginDetails().map() client.homeserverLoginDetails().map()
currentHomeserver.value = homeServerDetails.copy(url = homeserver)
}.onFailure { }.onFailure {
clear() clear()
}.mapFailure { failure -> }.mapFailure { failure ->

View file

@ -16,10 +16,10 @@ import org.matrix.rustcomponents.sdk.OidcException
class AuthenticationExceptionMappingTest { class AuthenticationExceptionMappingTest {
@Test @Test
fun `mapping an exception with no message returns 'Unknown error' message`() { fun `mapping an exception with no message returns null message`() {
val exception = Exception() val exception = Exception()
val mappedException = exception.mapAuthenticationException() val mappedException = exception.mapAuthenticationException()
assertThat(mappedException.message).isEqualTo("Unknown error") assertThat(mappedException.message).isNull()
} }
@Test @Test
@ -46,7 +46,7 @@ class AuthenticationExceptionMappingTest {
assertThat(ClientBuildException.Sdk("SDK issue").mapAuthenticationException()) assertThat(ClientBuildException.Sdk("SDK issue").mapAuthenticationException())
.isException<AuthenticationException.Generic>("SDK issue") .isException<AuthenticationException.Generic>("SDK issue")
assertThat(ClientBuildException.ServerUnreachable("Server unreachable").mapAuthenticationException()) assertThat(ClientBuildException.ServerUnreachable("Server unreachable").mapAuthenticationException())
.isException<AuthenticationException.Generic>("Server unreachable") .isException<AuthenticationException.ServerUnreachable>("Server unreachable")
assertThat(ClientBuildException.SlidingSync("Sliding Sync").mapAuthenticationException()) assertThat(ClientBuildException.SlidingSync("Sliding Sync").mapAuthenticationException())
.isException<AuthenticationException.Generic>("Sliding Sync") .isException<AuthenticationException.Generic>("Sliding Sync")
assertThat(ClientBuildException.WellKnownDeserializationException("WellKnown Deserialization").mapAuthenticationException()) assertThat(ClientBuildException.WellKnownDeserializationException("WellKnown Deserialization").mapAuthenticationException())

View file

@ -8,7 +8,6 @@
package io.element.android.libraries.matrix.test package io.element.android.libraries.matrix.test
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
@ -79,8 +78,6 @@ const val AN_ACCOUNT_PROVIDER = "matrix.org"
const val AN_ACCOUNT_PROVIDER_2 = "element.io" const val AN_ACCOUNT_PROVIDER_2 = "element.io"
const val AN_ACCOUNT_PROVIDER_3 = "other.io" const val AN_ACCOUNT_PROVIDER_3 = "other.io"
val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidcLogin = false)
val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true)
val A_ROOM_NOTIFICATION_MODE = RoomNotificationMode.MUTE val A_ROOM_NOTIFICATION_MODE = RoomNotificationMode.MUTE
const val AN_AVATAR_URL = "mxc://data" const val AN_AVATAR_URL = "mxc://data"

View file

@ -22,8 +22,6 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
val A_OIDC_DATA = OidcDetails(url = "a-url") val A_OIDC_DATA = OidcDetails(url = "a-url")
@ -31,13 +29,12 @@ class FakeMatrixAuthenticationService(
var matrixClientResult: ((SessionId) -> Result<MatrixClient>)? = null, var matrixClientResult: ((SessionId) -> Result<MatrixClient>)? = null,
var loginWithQrCodeResult: (qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) -> Result<SessionId> = var loginWithQrCodeResult: (qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) -> Result<SessionId> =
lambdaRecorder<MatrixQrCodeLoginData, (QrCodeLoginStep) -> Unit, Result<SessionId>> { _, _ -> Result.success(A_SESSION_ID) }, lambdaRecorder<MatrixQrCodeLoginData, (QrCodeLoginStep) -> Unit, Result<SessionId>> { _, _ -> Result.success(A_SESSION_ID) },
private val importCreatedSessionLambda: (ExternalSession) -> Result<SessionId> = { lambdaError() } private val importCreatedSessionLambda: (ExternalSession) -> Result<SessionId> = { lambdaError() },
private val setHomeserverResult: (String) -> Result<MatrixHomeServerDetails> = { lambdaError() },
) : MatrixAuthenticationService { ) : MatrixAuthenticationService {
private val homeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
private var oidcError: Throwable? = null private var oidcError: Throwable? = null
private var oidcCancelError: Throwable? = null private var oidcCancelError: Throwable? = null
private var loginError: Throwable? = null private var loginError: Throwable? = null
private var changeServerError: Throwable? = null
private var matrixClient: MatrixClient? = null private var matrixClient: MatrixClient? = null
private var onAuthenticationListener: ((MatrixClient) -> Unit)? = null private var onAuthenticationListener: ((MatrixClient) -> Unit)? = null
@ -53,16 +50,8 @@ class FakeMatrixAuthenticationService(
} }
} }
override fun getHomeserverDetails(): StateFlow<MatrixHomeServerDetails?> { override suspend fun setHomeserver(homeserver: String): Result<MatrixHomeServerDetails> = simulateLongTask {
return homeserver setHomeserverResult(homeserver)
}
fun givenHomeserver(homeserver: MatrixHomeServerDetails) {
this.homeserver.value = homeserver
}
override suspend fun setHomeserver(homeserver: String): Result<Unit> = simulateLongTask {
changeServerError?.let { Result.failure(it) } ?: Result.success(Unit)
} }
override suspend fun login(username: String, password: String): Result<SessionId> = simulateLongTask { override suspend fun login(username: String, password: String): Result<SessionId> = simulateLongTask {
@ -115,10 +104,6 @@ class FakeMatrixAuthenticationService(
loginError = throwable loginError = throwable
} }
fun givenChangeServerError(throwable: Throwable?) {
changeServerError = throwable
}
fun givenMatrixClient(matrixClient: MatrixClient) { fun givenMatrixClient(matrixClient: MatrixClient) {
this.matrixClient = matrixClient this.matrixClient = matrixClient
} }

View file

@ -0,0 +1,21 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.test.auth
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
fun aMatrixHomeServerDetails(
url: String = A_HOMESERVER_URL,
supportsPasswordLogin: Boolean = false,
supportsOidcLogin: Boolean = false,
) = MatrixHomeServerDetails(
url = url,
supportsPasswordLogin = supportsPasswordLogin,
supportsOidcLogin = supportsOidcLogin,
)

View file

@ -44,7 +44,7 @@ class DefaultActiveNotificationsProviderTest {
@Test @Test
fun `getMembershipNotificationsForSession returns only membership notifications for that session id`() { fun `getMembershipNotificationsForSession returns only membership notifications for that session id`() {
val activeNotifications = listOf( val activeNotifications = listOf(
aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value,), aStatusBarNotification(id = notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value),
aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value), aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value),
aStatusBarNotification( aStatusBarNotification(
id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID_2), id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID_2),

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ae6d230961018f1128ea655529a4287dbe3bbbc2762079d0c4f335313d2f2ab0
size 25166

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1502711e6305f9adf4719b28c1fe3755089ae6c42c6c66e8e5bf73ab2bbb65c1
size 23874

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4eaf3d650155e9a779cf796cef116eaba0f1d6722b229332b224475393a88178 oid sha256:ae6d230961018f1128ea655529a4287dbe3bbbc2762079d0c4f335313d2f2ab0
size 16828 size 25166

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4eaf3d650155e9a779cf796cef116eaba0f1d6722b229332b224475393a88178
size 16828

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:59f062f54833df71be9d7c4e785bb01013a10642e0d863bf7ef2abd8862b93c8 oid sha256:1502711e6305f9adf4719b28c1fe3755089ae6c42c6c66e8e5bf73ab2bbb65c1
size 15476 size 23874

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:59f062f54833df71be9d7c4e785bb01013a10642e0d863bf7ef2abd8862b93c8
size 15476