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:
Jorge Martin Espinosa 2025-11-04 15:43:00 +01:00 committed by GitHub
parent 0061b625f1
commit 015b497d5a
27 changed files with 195 additions and 341 deletions

View file

@ -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>
}

View file

@ -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()
}
}
}

View file

@ -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)
}
}
)
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -8,6 +8,5 @@
package io.element.android.libraries.wellknown.api
interface SessionWellknownRetriever {
suspend fun getWellKnown(): WellknownRetrieverResult<WellKnown>
suspend fun getElementWellKnown(): WellknownRetrieverResult<ElementWellKnown>
}

View file

@ -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?
)

View file

@ -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>
}

View file

@ -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

View file

@ -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 ->

View file

@ -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,
)

View file

@ -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>> {

View file

@ -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()
}

View file

@ -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)
}