mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +00:00
Let users enter their full Matrix user ID when searching for a homeserver
The account provider search treated its whole input as a host, so a full user ID was probed as https://@alice:example.org, which never resolves, and the permissive URL fallback then offered that invalid address as a provider. Recognise a complete user ID and resolve the homeserver from its server name, preserving a port when there is one. Input that starts with @ but is not yet a complete user ID resolves nothing, so no candidate is produced while the user is still typing. Detection reuses MatrixPatterns.isUserId and UserId.domainName rather than a new pattern.
This commit is contained in:
+9
-2
@@ -14,6 +14,8 @@ import io.element.android.libraries.core.coroutine.parallelMap
|
|||||||
import io.element.android.libraries.core.uri.ensureProtocol
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
import io.element.android.libraries.core.uri.isValidUrl
|
import io.element.android.libraries.core.uri.isValidUrl
|
||||||
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
|
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
|
||||||
|
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -34,8 +36,13 @@ class HomeserverResolver(
|
|||||||
fun resolve(userInput: String): Flow<List<HomeserverData>> = flow {
|
fun resolve(userInput: String): Flow<List<HomeserverData>> = flow {
|
||||||
val flowContext = currentCoroutineContext()
|
val flowContext = currentCoroutineContext()
|
||||||
val trimmedUserInput = userInput.trim()
|
val trimmedUserInput = userInput.trim()
|
||||||
if (trimmedUserInput.length < 4) return@flow
|
val searchTerm = when {
|
||||||
val candidateBase = trimmedUserInput.ensureProtocol().removeSuffix("/")
|
MatrixPatterns.isUserId(trimmedUserInput) -> UserId(trimmedUserInput).domainName.orEmpty()
|
||||||
|
trimmedUserInput.startsWith("@") -> return@flow
|
||||||
|
else -> trimmedUserInput
|
||||||
|
}
|
||||||
|
if (searchTerm.length < 4) return@flow
|
||||||
|
val candidateBase = searchTerm.ensureProtocol().removeSuffix("/")
|
||||||
val list = getUrlCandidates(candidateBase)
|
val list = getUrlCandidates(candidateBase)
|
||||||
val currentList = Collections.synchronizedList(mutableListOf<HomeserverData>())
|
val currentList = Collections.synchronizedList(mutableListOf<HomeserverData>())
|
||||||
// Run all the requests in parallel
|
// Run all the requests in parallel
|
||||||
|
|||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.features.login.impl.resolver
|
||||||
|
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatibilityChecker
|
||||||
|
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class HomeserverResolverTest {
|
||||||
|
@Test
|
||||||
|
fun `resolve - a server name is used as is`() = runTest {
|
||||||
|
val checkResult = lambdaRecorder<String, Result<Boolean>> { Result.success(it == "https://example.org") }
|
||||||
|
val sut = createHomeserverResolver(checkResult)
|
||||||
|
sut.resolve("example.org").test {
|
||||||
|
assertThat(awaitItem()).isEqualTo(listOf(HomeserverData(homeserverUrl = "https://example.org")))
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
checkResult.assertions().isCalledExactly(1)
|
||||||
|
.withSequence(
|
||||||
|
listOf(value("https://example.org")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resolve - an url is used as is`() = runTest {
|
||||||
|
val checkResult = lambdaRecorder<String, Result<Boolean>> { Result.success(it == "https://example.org") }
|
||||||
|
val sut = createHomeserverResolver(checkResult)
|
||||||
|
sut.resolve("https://example.org").test {
|
||||||
|
assertThat(awaitItem()).isEqualTo(listOf(HomeserverData(homeserverUrl = "https://example.org")))
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
checkResult.assertions().isCalledExactly(1)
|
||||||
|
.withSequence(
|
||||||
|
listOf(value("https://example.org")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resolve - a user id resolves the server name of the user id`() = runTest {
|
||||||
|
val checkResult = lambdaRecorder<String, Result<Boolean>> { Result.success(it == "https://example.org") }
|
||||||
|
val sut = createHomeserverResolver(checkResult)
|
||||||
|
sut.resolve("@alice:example.org").test {
|
||||||
|
assertThat(awaitItem()).isEqualTo(listOf(HomeserverData(homeserverUrl = "https://example.org")))
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
checkResult.assertions().isCalledExactly(1)
|
||||||
|
.withSequence(
|
||||||
|
listOf(value("https://example.org")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resolve - a user id with a port resolves the server name of the user id`() = runTest {
|
||||||
|
val checkResult = lambdaRecorder<String, Result<Boolean>> { Result.success(it == "https://example.org:8448") }
|
||||||
|
val sut = createHomeserverResolver(checkResult)
|
||||||
|
sut.resolve("@alice:example.org:8448").test {
|
||||||
|
assertThat(awaitItem()).isEqualTo(listOf(HomeserverData(homeserverUrl = "https://example.org:8448")))
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
checkResult.assertions().isCalledExactly(1)
|
||||||
|
.withSequence(
|
||||||
|
listOf(value("https://example.org:8448")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resolve - an incomplete user id does not resolve anything`() = runTest {
|
||||||
|
val checkResult = lambdaRecorder<String, Result<Boolean>> { Result.success(true) }
|
||||||
|
val sut = createHomeserverResolver(checkResult)
|
||||||
|
sut.resolve("@alice").test {
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
sut.resolve("@alice:").test {
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
checkResult.assertions().isNeverCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resolve - a user id with a too short server name does not resolve anything`() = runTest {
|
||||||
|
val checkResult = lambdaRecorder<String, Result<Boolean>> { Result.success(true) }
|
||||||
|
val sut = createHomeserverResolver(checkResult)
|
||||||
|
sut.resolve("@alice:a.b").test {
|
||||||
|
awaitComplete()
|
||||||
|
}
|
||||||
|
checkResult.assertions().isNeverCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createHomeserverResolver(
|
||||||
|
checkResult: (String) -> Result<Boolean>,
|
||||||
|
) = HomeserverResolver(
|
||||||
|
dispatchers = testCoroutineDispatchers(),
|
||||||
|
homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult),
|
||||||
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user