Use the SDK Client to check whether a homeserver is compatible (#5664)
* Use the SDK `Client` to check whether a HS is compatible * Remove usage of unused `WellKnown`, keep `ElementWellKnown` * Make `HomeServerLoginCompatibilityChecker.check` return `true/false` values to distinguish non-valid homeservers from a failed check * Use `inMemoryStore` and `serverNameOrHomeserverUrl` * Do some cleanup of `isValid` and `isWellknownValid` * Make the debounce for starting the search a bit higher, as checking for the homeservers seems more resource-intensive now
This commit is contained in:
parent
8a4d0c7bee
commit
7aa564e74d
27 changed files with 195 additions and 341 deletions
|
|
@ -13,5 +13,4 @@ data class AccountProvider(
|
|||
val subtitle: String? = null,
|
||||
val isPublic: Boolean = false,
|
||||
val isMatrixOrg: Boolean = false,
|
||||
val isValid: Boolean = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ open class AccountProviderProvider : PreviewParameterProvider<AccountProvider> {
|
|||
get() = sequenceOf(
|
||||
anAccountProvider(),
|
||||
anAccountProvider().copy(subtitle = null),
|
||||
anAccountProvider().copy(subtitle = null, title = "invalid", isValid = false),
|
||||
anAccountProvider().copy(subtitle = null, title = "invalid"),
|
||||
anAccountProvider().copy(subtitle = null, title = "Other", isPublic = false, isMatrixOrg = false),
|
||||
// Add other state here
|
||||
)
|
||||
|
|
@ -26,11 +26,9 @@ fun anAccountProvider(
|
|||
subtitle: String? = "Matrix.org is an open network for secure, decentralized communication.",
|
||||
isPublic: Boolean = true,
|
||||
isMatrixOrg: Boolean = true,
|
||||
isValid: Boolean = true,
|
||||
) = AccountProvider(
|
||||
url = url,
|
||||
subtitle = subtitle,
|
||||
isPublic = isPublic,
|
||||
isMatrixOrg = isMatrixOrg,
|
||||
isValid = isValid,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,4 @@ package io.element.android.features.login.impl.resolver
|
|||
data class HomeserverData(
|
||||
// The computed homeserver url, for which a wellknown file has been retrieved, or just a valid Url
|
||||
val homeserverUrl: String,
|
||||
// True if a wellknown file has been found and is valid. If false, it means that the [homeserverUrl] is valid
|
||||
val isWellknownValid: Boolean,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,19 +8,16 @@
|
|||
package io.element.android.features.login.impl.resolver
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.coroutine.parallelMap
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.core.uri.ensureProtocol
|
||||
import io.element.android.libraries.core.uri.isValidUrl
|
||||
import io.element.android.libraries.wellknown.api.WellKnown
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import timber.log.Timber
|
||||
import java.util.Collections
|
||||
|
||||
/**
|
||||
|
|
@ -29,7 +26,7 @@ import java.util.Collections
|
|||
@Inject
|
||||
class HomeserverResolver(
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val wellknownRetriever: WellknownRetriever,
|
||||
private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker,
|
||||
) {
|
||||
fun resolve(userInput: String): Flow<List<HomeserverData>> = flow {
|
||||
val flowContext = currentCoroutineContext()
|
||||
|
|
@ -41,20 +38,14 @@ class HomeserverResolver(
|
|||
// Run all the requests in parallel
|
||||
withContext(dispatchers.io) {
|
||||
list.parallelMap { url ->
|
||||
val wellKnown = tryOrNull {
|
||||
withTimeout(5000) {
|
||||
wellknownRetriever.getWellKnown(url)
|
||||
}
|
||||
}
|
||||
val isValid = wellKnown?.dataOrNull()?.isValid().orFalse()
|
||||
val isValid = homeServerLoginCompatibilityChecker.check(url)
|
||||
.onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") }
|
||||
.getOrNull()
|
||||
?: return@parallelMap
|
||||
|
||||
// Emit the list as soon as possible
|
||||
if (isValid) {
|
||||
// Emit the list as soon as possible
|
||||
currentList.add(
|
||||
HomeserverData(
|
||||
homeserverUrl = url,
|
||||
isWellknownValid = true,
|
||||
)
|
||||
)
|
||||
currentList.add(HomeserverData(homeserverUrl = url))
|
||||
withContext(flowContext) {
|
||||
emit(currentList.toList())
|
||||
}
|
||||
|
|
@ -63,14 +54,7 @@ class HomeserverResolver(
|
|||
}
|
||||
// If list is empty, and the user has entered an URL, do not block the user.
|
||||
if (currentList.isEmpty() && trimmedUserInput.isValidUrl()) {
|
||||
emit(
|
||||
listOf(
|
||||
HomeserverData(
|
||||
homeserverUrl = trimmedUserInput,
|
||||
isWellknownValid = false,
|
||||
)
|
||||
)
|
||||
)
|
||||
emit(listOf(HomeserverData(homeserverUrl = trimmedUserInput)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +72,3 @@ class HomeserverResolver(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun WellKnown.isValid(): Boolean {
|
||||
return homeServer?.baseURL?.isNotBlank().orFalse()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ class ChangeAccountProviderPresenter(
|
|||
subtitle = null,
|
||||
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||
isValid = true,
|
||||
)
|
||||
}
|
||||
.toImmutableList()
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@ class ChooseAccountProviderPresenter(
|
|||
subtitle = null,
|
||||
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
|
||||
isValid = true,
|
||||
)
|
||||
}
|
||||
.toImmutableList()
|
||||
|
|
|
|||
|
|
@ -57,14 +57,14 @@ class SearchAccountProviderPresenter(
|
|||
userInput = userInput,
|
||||
userInputResult = data.value,
|
||||
changeServerState = changeServerState,
|
||||
eventSink = ::handleEvents
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.onUserInput(userInput: String, data: MutableState<AsyncData<List<HomeserverData>>>) = launch {
|
||||
data.value = AsyncData.Uninitialized
|
||||
// Debounce
|
||||
delay(300)
|
||||
delay(500)
|
||||
data.value = AsyncData.Loading()
|
||||
homeserverResolver.resolve(userInput).collect {
|
||||
data.value = AsyncData.Success(it)
|
||||
|
|
|
|||
|
|
@ -34,18 +34,14 @@ fun aSearchAccountProviderState(
|
|||
|
||||
fun aHomeserverDataList(): List<HomeserverData> {
|
||||
return listOf(
|
||||
aHomeserverData(isWellknownValid = true),
|
||||
aHomeserverData(homeserverUrl = "https://no.sliding.sync", isWellknownValid = true),
|
||||
aHomeserverData(homeserverUrl = "https://invalid", isWellknownValid = false),
|
||||
aHomeserverData(homeserverUrl = AuthenticationConfig.MATRIX_ORG_URL),
|
||||
aHomeserverData(homeserverUrl = "https://no.sliding.sync"),
|
||||
aHomeserverData(homeserverUrl = "https://invalid"),
|
||||
)
|
||||
}
|
||||
|
||||
fun aHomeserverData(
|
||||
homeserverUrl: String = AuthenticationConfig.MATRIX_ORG_URL,
|
||||
isWellknownValid: Boolean = true,
|
||||
): HomeserverData {
|
||||
return HomeserverData(
|
||||
homeserverUrl = homeserverUrl,
|
||||
isWellknownValid = isWellknownValid,
|
||||
)
|
||||
return HomeserverData(homeserverUrl = homeserverUrl,)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,7 +192,6 @@ private fun HomeserverData.toAccountProvider(): AccountProvider {
|
|||
// There is no need to know for other servers right now
|
||||
isPublic = isMatrixOrg,
|
||||
isMatrixOrg = isMatrixOrg,
|
||||
isValid = isWellknownValid,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ class AccountProviderDataSourceTest {
|
|||
subtitle = null,
|
||||
isPublic = true,
|
||||
isMatrixOrg = true,
|
||||
isValid = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -55,7 +54,6 @@ class AccountProviderDataSourceTest {
|
|||
subtitle = null,
|
||||
isPublic = true,
|
||||
isMatrixOrg = true,
|
||||
isValid = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -77,7 +75,6 @@ class AccountProviderDataSourceTest {
|
|||
subtitle = null,
|
||||
isPublic = true,
|
||||
isMatrixOrg = true,
|
||||
isValid = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -98,7 +95,6 @@ class AccountProviderDataSourceTest {
|
|||
subtitle = null,
|
||||
isPublic = false,
|
||||
isMatrixOrg = false,
|
||||
isValid = false,
|
||||
)
|
||||
)
|
||||
sut.reset()
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ class ChangeAccountProviderPresenterTest {
|
|||
subtitle = null,
|
||||
isPublic = true,
|
||||
isMatrixOrg = true,
|
||||
isValid = true,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -76,7 +75,6 @@ class ChangeAccountProviderPresenterTest {
|
|||
subtitle = null,
|
||||
isPublic = true,
|
||||
isMatrixOrg = true,
|
||||
isValid = true,
|
||||
),
|
||||
AccountProvider(
|
||||
url = "https://element.io",
|
||||
|
|
@ -84,7 +82,6 @@ class ChangeAccountProviderPresenterTest {
|
|||
subtitle = null,
|
||||
isPublic = false,
|
||||
isMatrixOrg = false,
|
||||
isValid = true,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -114,7 +111,6 @@ class ChangeAccountProviderPresenterTest {
|
|||
subtitle = null,
|
||||
isPublic = true,
|
||||
isMatrixOrg = true,
|
||||
isValid = true,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -37,14 +37,12 @@ class ChooseAccountProviderPresenterTest {
|
|||
subtitle = null,
|
||||
isPublic = false,
|
||||
isMatrixOrg = false,
|
||||
isValid = true,
|
||||
)
|
||||
val accountProvider2 = AccountProvider(
|
||||
url = ACCOUNT_PROVIDER_FROM_CONFIG_2.ensureProtocol(),
|
||||
subtitle = null,
|
||||
isPublic = false,
|
||||
isMatrixOrg = false,
|
||||
isValid = true,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,12 +13,8 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
||||
import io.element.android.features.login.impl.resolver.HomeserverResolver
|
||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
import io.element.android.libraries.wellknown.api.WellKnown
|
||||
import io.element.android.libraries.wellknown.api.WellKnownBaseConfig
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||
import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatibilityChecker
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
|
|
@ -33,9 +29,9 @@ class SearchAccountProviderPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val fakeWellknownRetriever = FakeWellknownRetriever()
|
||||
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(true) })
|
||||
val presenter = SearchAccountProviderPresenter(
|
||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker),
|
||||
changeServerPresenter = { aChangeServerState() }
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
|
|
@ -47,9 +43,35 @@ class SearchAccountProviderPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - error while checking login compatibility`() = runTest {
|
||||
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.failure(IllegalStateException("Oops")) })
|
||||
val presenter = SearchAccountProviderPresenter(
|
||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker),
|
||||
changeServerPresenter = { aChangeServerState() }
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org"))
|
||||
val withInputState = awaitItem()
|
||||
assertThat(withInputState.userInput).isEqualTo("https://test.org")
|
||||
assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
|
||||
assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
|
||||
assertThat(awaitItem().userInputResult).isEqualTo(
|
||||
AsyncData.Success(
|
||||
listOf(
|
||||
aHomeserverData(homeserverUrl = "https://test.org")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - enter text no result`() = runTest {
|
||||
val fakeWellknownRetriever = FakeWellknownRetriever()
|
||||
val fakeWellknownRetriever = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(false) })
|
||||
val presenter = SearchAccountProviderPresenter(
|
||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
||||
changeServerPresenter = { aChangeServerState() }
|
||||
|
|
@ -67,48 +89,20 @@ class SearchAccountProviderPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - enter valid url no wellknown`() = runTest {
|
||||
val fakeWellknownRetriever = FakeWellknownRetriever()
|
||||
val presenter = SearchAccountProviderPresenter(
|
||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
||||
changeServerPresenter = { aChangeServerState() }
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org"))
|
||||
val withInputState = awaitItem()
|
||||
assertThat(withInputState.userInput).isEqualTo("https://test.org")
|
||||
assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
|
||||
assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
|
||||
assertThat(awaitItem().userInputResult).isEqualTo(
|
||||
AsyncData.Success(
|
||||
listOf(
|
||||
aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = false)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - enter text one result with wellknown`() = runTest {
|
||||
val getWellKnownResult = lambdaRecorder<String, WellknownRetrieverResult<WellKnown>> {
|
||||
val checkResult = lambdaRecorder<String, Result<Boolean>> {
|
||||
when (it) {
|
||||
"https://test.org" -> WellknownRetrieverResult.NotFound
|
||||
"https://test.com" -> WellknownRetrieverResult.NotFound
|
||||
"https://test.io" -> WellknownRetrieverResult.Success(aWellKnown())
|
||||
"https://test" -> WellknownRetrieverResult.NotFound
|
||||
"https://test.org" -> Result.success(false)
|
||||
"https://test.com" -> Result.success(false)
|
||||
"https://test.io" -> Result.success(true)
|
||||
"https://test" -> Result.success(false)
|
||||
else -> error("should not happen")
|
||||
}
|
||||
}
|
||||
val fakeWellknownRetriever = FakeWellknownRetriever(
|
||||
getWellKnownResult = getWellKnownResult,
|
||||
)
|
||||
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult)
|
||||
val presenter = SearchAccountProviderPresenter(
|
||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker),
|
||||
changeServerPresenter = { aChangeServerState() }
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
|
|
@ -127,7 +121,7 @@ class SearchAccountProviderPresenterTest {
|
|||
)
|
||||
)
|
||||
)
|
||||
getWellKnownResult.assertions().isCalledExactly(4)
|
||||
checkResult.assertions().isCalledExactly(4)
|
||||
.withSequence(
|
||||
listOf(value("https://test.org")),
|
||||
listOf(value("https://test.com")),
|
||||
|
|
@ -139,20 +133,18 @@ class SearchAccountProviderPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - enter text two results with wellknown`() = runTest {
|
||||
val getWellKnownResult = lambdaRecorder<String, WellknownRetrieverResult<WellKnown>> {
|
||||
val checkResult = lambdaRecorder<String, Result<Boolean>> {
|
||||
when (it) {
|
||||
"https://test.org" -> WellknownRetrieverResult.Success(aWellKnown())
|
||||
"https://test.com" -> WellknownRetrieverResult.NotFound
|
||||
"https://test.io" -> WellknownRetrieverResult.Success(aWellKnown())
|
||||
"https://test" -> WellknownRetrieverResult.NotFound
|
||||
"https://test.org" -> Result.success(true)
|
||||
"https://test.com" -> Result.success(false)
|
||||
"https://test.io" -> Result.success(true)
|
||||
"https://test" -> Result.success(false)
|
||||
else -> error("should not happen")
|
||||
}
|
||||
}
|
||||
val fakeWellknownRetriever = FakeWellknownRetriever(
|
||||
getWellKnownResult = getWellKnownResult,
|
||||
)
|
||||
val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult)
|
||||
val presenter = SearchAccountProviderPresenter(
|
||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker),
|
||||
changeServerPresenter = { aChangeServerState() }
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
|
|
@ -179,7 +171,7 @@ class SearchAccountProviderPresenterTest {
|
|||
)
|
||||
)
|
||||
)
|
||||
getWellKnownResult.assertions().isCalledExactly(4)
|
||||
checkResult.assertions().isCalledExactly(4)
|
||||
.withSequence(
|
||||
listOf(value("https://test.org")),
|
||||
listOf(value("https://test.com")),
|
||||
|
|
@ -188,15 +180,4 @@ class SearchAccountProviderPresenterTest {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun aWellKnown(): WellKnown {
|
||||
return WellKnown(
|
||||
homeServer = WellKnownBaseConfig(
|
||||
baseURL = A_HOMESERVER_URL
|
||||
),
|
||||
identityServer = WellKnownBaseConfig(
|
||||
baseURL = A_HOMESERVER_URL
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
/**
|
||||
* Checks the homeserver's compatibility with Element X.
|
||||
*/
|
||||
interface HomeServerLoginCompatibilityChecker {
|
||||
/**
|
||||
* Performs the compatibility check given the homeserver's [url].
|
||||
* @return a `true` value if the homeserver is compatible, `false` if not, or a failure result if the check unexpectedly failed.
|
||||
*/
|
||||
suspend fun check(url: String): Result<Boolean>
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.impl.auth
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
|
||||
import io.element.android.libraries.matrix.impl.ClientBuilderProvider
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class RustHomeServerLoginCompatibilityChecker(
|
||||
private val clientBuilderProvider: ClientBuilderProvider,
|
||||
) : HomeServerLoginCompatibilityChecker {
|
||||
override suspend fun check(url: String): Result<Boolean> = runCatchingExceptions {
|
||||
clientBuilderProvider.provide()
|
||||
.inMemoryStore()
|
||||
.serverNameOrHomeserverUrl(url)
|
||||
.build()
|
||||
.use {
|
||||
it.homeserverLoginDetails()
|
||||
}
|
||||
.use {
|
||||
Timber.d("Homeserver $url | OIDC: ${it.supportsOidcLogin()} | Password: ${it.supportsPasswordLogin()} | SSO: ${it.supportsSsoLogin()}")
|
||||
it.supportsOidcLogin() || it.supportsPasswordLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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.impl.auth
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.impl.FakeClientBuilderProvider
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverLoginDetails
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
@Ignore("JNA direct mapping has broken unit tests with FFI fakes")
|
||||
class RustHomeserverLoginCompatibilityCheckerTest {
|
||||
@Test
|
||||
fun `check - is valid if it supports OIDC login`() = runTest {
|
||||
val sut = createChecker { FakeFfiHomeserverLoginDetails(supportsOidcLogin = true) }
|
||||
assertThat(sut.check("https://matrix.host.org").getOrNull()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check - is valid if it supports password login`() = runTest {
|
||||
val sut = createChecker { FakeFfiHomeserverLoginDetails(supportsPasswordLogin = true) }
|
||||
assertThat(sut.check("https://matrix.host.org").getOrNull()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check - is not valid if it only supports SSO login`() = runTest {
|
||||
val sut = createChecker { FakeFfiHomeserverLoginDetails(supportsSsoLogin = true) }
|
||||
assertThat(sut.check("https://matrix.host.org").getOrNull()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check - is not valid if fetching the data fails`() = runTest {
|
||||
val sut = createChecker { error("Unexpected error!") }
|
||||
assertThat(sut.check("https://matrix.host.org").isFailure).isTrue()
|
||||
}
|
||||
|
||||
private fun createChecker(
|
||||
result: () -> FakeFfiHomeserverLoginDetails,
|
||||
) = RustHomeServerLoginCompatibilityChecker(
|
||||
clientBuilderProvider = FakeClientBuilderProvider {
|
||||
FakeFfiClientBuilder {
|
||||
FakeFfiClient(homeserverLoginDetailsResult = result)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -12,10 +12,12 @@ import org.matrix.rustcomponents.sdk.NoHandle
|
|||
|
||||
class FakeFfiHomeserverLoginDetails(
|
||||
private val url: String = "https://example.org",
|
||||
private val supportsPasswordLogin: Boolean = true,
|
||||
private val supportsOidcLogin: Boolean = false
|
||||
private val supportsPasswordLogin: Boolean = false,
|
||||
private val supportsOidcLogin: Boolean = false,
|
||||
private val supportsSsoLogin: Boolean = false,
|
||||
) : HomeserverLoginDetails(NoHandle) {
|
||||
override fun url(): String = url
|
||||
override fun supportsOidcLogin(): Boolean = supportsOidcLogin
|
||||
override fun supportsPasswordLogin(): Boolean = supportsPasswordLogin
|
||||
override fun supportsSsoLogin(): Boolean = supportsSsoLogin
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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.HomeServerLoginCompatibilityChecker
|
||||
|
||||
class FakeHomeServerLoginCompatibilityChecker(
|
||||
private val checkResult: (String) -> Result<Boolean>,
|
||||
) : HomeServerLoginCompatibilityChecker {
|
||||
override suspend fun check(url: String): Result<Boolean> {
|
||||
return checkResult(url)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,5 @@
|
|||
package io.element.android.libraries.wellknown.api
|
||||
|
||||
interface SessionWellknownRetriever {
|
||||
suspend fun getWellKnown(): WellknownRetrieverResult<WellKnown>
|
||||
suspend fun getElementWellKnown(): WellknownRetrieverResult<ElementWellKnown>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 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.wellknown.api
|
||||
|
||||
data class WellKnown(
|
||||
val homeServer: WellKnownBaseConfig?,
|
||||
val identityServer: WellKnownBaseConfig?,
|
||||
)
|
||||
|
||||
data class WellKnownBaseConfig(
|
||||
val baseURL: String?
|
||||
)
|
||||
|
|
@ -8,6 +8,5 @@
|
|||
package io.element.android.libraries.wellknown.api
|
||||
|
||||
interface WellknownRetriever {
|
||||
suspend fun getWellKnown(baseUrl: String): WellknownRetrieverResult<WellKnown>
|
||||
suspend fun getElementWellKnown(baseUrl: String): WellknownRetrieverResult<ElementWellKnown>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
|||
import io.element.android.libraries.matrix.api.exception.ClientException
|
||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||
import io.element.android.libraries.wellknown.api.SessionWellknownRetriever
|
||||
import io.element.android.libraries.wellknown.api.WellKnown
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||
import timber.log.Timber
|
||||
|
||||
|
|
@ -26,17 +25,6 @@ class DefaultSessionWellknownRetriever(
|
|||
) : SessionWellknownRetriever {
|
||||
private val domain by lazy { matrixClient.userIdServerName() }
|
||||
|
||||
override suspend fun getWellKnown(): WellknownRetrieverResult<WellKnown> {
|
||||
val url = "https://$domain/.well-known/matrix/client"
|
||||
return matrixClient
|
||||
.getUrl(url)
|
||||
.mapCatchingExceptions {
|
||||
val data = String(it)
|
||||
json().decodeFromString<InternalWellKnown>(data).map()
|
||||
}
|
||||
.toWellknownRetrieverResult()
|
||||
}
|
||||
|
||||
override suspend fun getElementWellKnown(): WellknownRetrieverResult<ElementWellKnown> {
|
||||
val url = "https://$domain/.well-known/element/element.json"
|
||||
return matrixClient
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import io.element.android.libraries.core.extensions.runCatchingExceptions
|
|||
import io.element.android.libraries.core.uri.ensureProtocol
|
||||
import io.element.android.libraries.network.RetrofitFactory
|
||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||
import io.element.android.libraries.wellknown.api.WellKnown
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||
import retrofit2.HttpException
|
||||
|
|
@ -24,27 +23,6 @@ import java.net.HttpURLConnection
|
|||
class DefaultWellknownRetriever(
|
||||
private val retrofitFactory: RetrofitFactory,
|
||||
) : WellknownRetriever {
|
||||
override suspend fun getWellKnown(baseUrl: String): WellknownRetrieverResult<WellKnown> {
|
||||
return buildWellknownApi(baseUrl)
|
||||
.map { wellknownApi ->
|
||||
try {
|
||||
val result = wellknownApi.getWellKnown().map()
|
||||
WellknownRetrieverResult.Success(result)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to retrieve well-known data for $baseUrl")
|
||||
if ((e as? HttpException)?.code() == HttpURLConnection.HTTP_NOT_FOUND) {
|
||||
WellknownRetrieverResult.NotFound
|
||||
} else {
|
||||
WellknownRetrieverResult.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { WellknownRetrieverResult.Error(it as Exception) }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getElementWellKnown(baseUrl: String): WellknownRetrieverResult<ElementWellKnown> {
|
||||
return buildWellknownApi(baseUrl)
|
||||
.map { wellknownApi ->
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@
|
|||
package io.element.android.libraries.wellknown.impl
|
||||
|
||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||
import io.element.android.libraries.wellknown.api.WellKnown
|
||||
import io.element.android.libraries.wellknown.api.WellKnownBaseConfig
|
||||
|
||||
internal fun InternalElementWellKnown.map() = ElementWellKnown(
|
||||
registrationHelperUrl = registrationHelperUrl,
|
||||
|
|
@ -17,12 +15,3 @@ internal fun InternalElementWellKnown.map() = ElementWellKnown(
|
|||
rageshakeUrl = rageshakeUrl,
|
||||
brandColor = brandColor,
|
||||
)
|
||||
|
||||
internal fun InternalWellKnown.map() = WellKnown(
|
||||
homeServer = homeServer?.map(),
|
||||
identityServer = identityServer?.map(),
|
||||
)
|
||||
|
||||
internal fun InternalWellKnownBaseConfig.map() = WellKnownBaseConfig(
|
||||
baseURL = baseURL,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ import io.element.android.libraries.androidutils.json.DefaultJsonProvider
|
|||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||
import io.element.android.libraries.wellknown.api.WellKnown
|
||||
import io.element.android.libraries.wellknown.api.WellKnownBaseConfig
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
|
|
@ -21,142 +19,6 @@ import kotlinx.coroutines.test.runTest
|
|||
import org.junit.Test
|
||||
|
||||
class DefaultSessionWellknownRetrieverTest {
|
||||
@Test
|
||||
fun `get empty wellknown`() = runTest {
|
||||
val getUrlLambda = lambdaRecorder<String, Result<ByteArray>> {
|
||||
Result.success("{}".toByteArray())
|
||||
}
|
||||
val sut = createDefaultSessionWellknownRetriever(
|
||||
getUrlLambda = getUrlLambda,
|
||||
)
|
||||
assertThat(sut.getWellKnown()).isEqualTo(
|
||||
WellknownRetrieverResult.Success(
|
||||
WellKnown(
|
||||
homeServer = null,
|
||||
identityServer = null,
|
||||
)
|
||||
)
|
||||
)
|
||||
getUrlLambda.assertions().isCalledOnce()
|
||||
.with(value("https://user.domain.org/.well-known/matrix/client"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get wellknown with full content`() = runTest {
|
||||
val sut = createDefaultSessionWellknownRetriever(
|
||||
getUrlLambda = {
|
||||
Result.success(
|
||||
"""{
|
||||
"m.homeserver": {
|
||||
"base_url": "https://example.org"
|
||||
},
|
||||
"m.identity_server": {
|
||||
"base_url": "https://identity.example.org"
|
||||
}
|
||||
}""".trimIndent().toByteArray()
|
||||
)
|
||||
}
|
||||
)
|
||||
assertThat(sut.getWellKnown()).isEqualTo(
|
||||
WellknownRetrieverResult.Success(
|
||||
WellKnown(
|
||||
homeServer = WellKnownBaseConfig(
|
||||
baseURL = "https://example.org",
|
||||
),
|
||||
identityServer = WellKnownBaseConfig(
|
||||
baseURL = "https://identity.example.org",
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get wellknown with full content empty base_url`() = runTest {
|
||||
val sut = createDefaultSessionWellknownRetriever(
|
||||
getUrlLambda = {
|
||||
Result.success(
|
||||
"""{
|
||||
"m.homeserver": {
|
||||
"base_url": "https://example.org"
|
||||
},
|
||||
"m.identity_server": {}
|
||||
}""".trimIndent().toByteArray()
|
||||
)
|
||||
}
|
||||
)
|
||||
assertThat(sut.getWellKnown()).isEqualTo(
|
||||
WellknownRetrieverResult.Success(
|
||||
WellKnown(
|
||||
homeServer = WellKnownBaseConfig(
|
||||
baseURL = "https://example.org",
|
||||
),
|
||||
identityServer = WellKnownBaseConfig(
|
||||
baseURL = null,
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get wellknown with unknown key`() = runTest {
|
||||
val sut = createDefaultSessionWellknownRetriever(
|
||||
getUrlLambda = {
|
||||
Result.success(
|
||||
"""{
|
||||
"m.homeserver": {
|
||||
"base_url": "https://example.org"
|
||||
},
|
||||
"m.identity_server": {
|
||||
"base_url": "https://identity.example.org"
|
||||
},
|
||||
"other": true
|
||||
}""".trimIndent().toByteArray()
|
||||
)
|
||||
},
|
||||
)
|
||||
assertThat(sut.getWellKnown()).isEqualTo(
|
||||
WellknownRetrieverResult.Success(
|
||||
WellKnown(
|
||||
homeServer = WellKnownBaseConfig(
|
||||
baseURL = "https://example.org",
|
||||
),
|
||||
identityServer = WellKnownBaseConfig(
|
||||
baseURL = "https://identity.example.org",
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get wellknown json error`() = runTest {
|
||||
val sut = createDefaultSessionWellknownRetriever(
|
||||
getUrlLambda = {
|
||||
Result.success(
|
||||
"""{
|
||||
"m.homeserver": {
|
||||
"base_url": "https://example.org"
|
||||
},
|
||||
error
|
||||
}""".trimIndent().toByteArray()
|
||||
)
|
||||
}
|
||||
)
|
||||
assertThat(sut.getWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get wellknown network error`() = runTest {
|
||||
val sut = createDefaultSessionWellknownRetriever(
|
||||
getUrlLambda = {
|
||||
Result.failure(AN_EXCEPTION)
|
||||
}
|
||||
)
|
||||
assertThat(sut.getWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get empty element wellknown`() = runTest {
|
||||
val getUrlLambda = lambdaRecorder<String, Result<ByteArray>> {
|
||||
|
|
|
|||
|
|
@ -9,18 +9,12 @@ package io.element.android.features.wellknown.test
|
|||
|
||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||
import io.element.android.libraries.wellknown.api.SessionWellknownRetriever
|
||||
import io.element.android.libraries.wellknown.api.WellKnown
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
|
||||
class FakeSessionWellknownRetriever(
|
||||
private val getWellKnownResult: () -> WellknownRetrieverResult<WellKnown> = { WellknownRetrieverResult.NotFound },
|
||||
private val getElementWellKnownResult: () -> WellknownRetrieverResult<ElementWellKnown> = { WellknownRetrieverResult.NotFound },
|
||||
) : SessionWellknownRetriever {
|
||||
override suspend fun getWellKnown(): WellknownRetrieverResult<WellKnown> = simulateLongTask {
|
||||
getWellKnownResult()
|
||||
}
|
||||
|
||||
override suspend fun getElementWellKnown(): WellknownRetrieverResult<ElementWellKnown> = simulateLongTask {
|
||||
getElementWellKnownResult()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,19 +8,13 @@
|
|||
package io.element.android.features.wellknown.test
|
||||
|
||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||
import io.element.android.libraries.wellknown.api.WellKnown
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
|
||||
class FakeWellknownRetriever(
|
||||
private val getWellKnownResult: (String) -> WellknownRetrieverResult<WellKnown> = { WellknownRetrieverResult.NotFound },
|
||||
private val getElementWellKnownResult: (String) -> WellknownRetrieverResult<ElementWellKnown> = { WellknownRetrieverResult.NotFound },
|
||||
) : WellknownRetriever {
|
||||
override suspend fun getWellKnown(baseUrl: String): WellknownRetrieverResult<WellKnown> = simulateLongTask {
|
||||
getWellKnownResult(baseUrl)
|
||||
}
|
||||
|
||||
override suspend fun getElementWellKnown(baseUrl: String): WellknownRetrieverResult<ElementWellKnown> = simulateLongTask {
|
||||
getElementWellKnownResult(baseUrl)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue