Move the :libraries:wellknown logic into enterprise modules (#7429)

* Extract `IsEnterpriseBuild` use case

Having it inside `EnterpriseService` made having DI cycles with the `BuildMeta` provider too easy

* Rename `EnterpriseService.defaultHomeserverList()` to `homeserverAllowList()`

This seems more aligned with the way this list works

* Move most of the logic from `:libraries:wellknown` to `:enterprise:feature:enterprise`, both `shared` and `impl` modules

Ideally I would have added everything into `impl`, but some of the classes needed to be shared between the different `:enterprise:features` modules, and those are in the `shared` module, lacking a better name/strategy

* Add `EnterpriseService.isElementProEnforced`

With this, we don't need to provide the `RemoteEnterpriseConfig` anymore to any consumers, everything goes through `EnterpriseService` or other providers

* Remove `EnterpriseService.overriddenElementWellKnown`

This leaks the remote enterprise config/element well known contents to the app in a public interface.

* Create `CustomMapTilerConfigProvider` to avoid leaking the `RemoteEnterpriseConfig` in public APIs

This commit also starts moving some of the well-known related values to the enterprise modules

* Add missing docs to `EnterpriseService` and `SessionEnterpriseService`

* Fix tests

* Fix lint issues

* Bump enterprise module
This commit is contained in:
Jorge Martin Espinosa
2026-08-17 15:21:47 +02:00
committed by GitHub
parent f94566e44f
commit 8b2c207728
72 changed files with 246 additions and 1607 deletions
@@ -1,27 +0,0 @@
/*
* 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.libraries.wellknown.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.androidutils.json.JsonProvider
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
@ContributesBinding(AppScope::class)
class DefaultElementWellKnownParser(
private val jsonProvider: JsonProvider,
) : ElementWellKnownParser {
override fun parse(json: String): Result<ElementWellKnown> {
return runCatchingExceptions {
val decoder = jsonProvider()
decoder.decodeFromString<InternalElementWellKnown>(json).map()
}
}
}
@@ -1,77 +0,0 @@
/*
* 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.libraries.wellknown.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.cachestore.api.CacheData
import io.element.android.libraries.cachestore.api.CacheStore
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
import io.element.android.libraries.wellknown.api.ElementWellknownStore
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import io.element.android.services.toolbox.api.systemclock.SystemClock
@AssistedInject
class DefaultWellKnownStore(
@Assisted
private val prefix: String?,
private val cacheStore: CacheStore,
private val elementWellKnownParser: ElementWellKnownParser,
private val systemClock: SystemClock,
) : ElementWellknownStore {
@AssistedFactory
@ContributesBinding(AppScope::class)
fun interface Factory : ElementWellknownStore.Factory {
override fun create(prefix: String?): DefaultWellKnownStore
}
override suspend fun get(domain: String): WellknownRetrieverResult<ElementWellKnown> {
return runCatchingExceptions {
val cachedData = cacheStore.getData(key(domain))
if (cachedData != null) {
val data = elementWellKnownParser.parse(cachedData.value).getOrThrow()
if (systemClock.epochMillis() > cachedData.updatedAt + CACHE_VALIDITY_MILLIS) {
WellknownRetrieverResult.Outdated(data)
} else {
WellknownRetrieverResult.Success(data)
}
} else {
WellknownRetrieverResult.NotFound
}
}.recover {
WellknownRetrieverResult.Error(it as Exception)
}.getOrThrow()
}
override suspend fun update(domain: String, wellknown: String): Result<Unit> {
return runCatchingExceptions {
cacheStore.storeData(key(domain), CacheData(wellknown, systemClock.epochMillis()))
}
}
override suspend fun delete(domain: String): Result<Unit> {
return runCatchingExceptions {
cacheStore.deleteData(key(domain))
}
}
private fun key(domain: String): String = if (prefix != null) {
"$prefix-$domain"
} else {
"https://$domain/.well-known/element/element.json"
}
companion object {
// 1 day
private const val CACHE_VALIDITY_MILLIS = 1 * 24 * 60 * 60 * 1000L
}
}
@@ -1,142 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.wellknown.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.androidutils.text.takeIfNotBlank
import io.element.android.libraries.core.extensions.mapCatchingExceptions
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
import io.element.android.libraries.wellknown.api.ElementWellknownStore
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
import io.element.android.libraries.wellknown.api.WellknownRetriever
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.net.URL
@AssistedInject
class DefaultWellknownRetriever(
private val elementWellknownStoreFactory: ElementWellknownStore.Factory,
private val enterpriseService: EnterpriseService,
private val elementWellKnownParser: ElementWellKnownParser,
@Assisted private val urlContentFetcher: UrlContentFetcher,
@AppCoroutineScope private val coroutineScope: CoroutineScope,
) : WellknownRetriever {
@ContributesBinding(AppScope::class)
@AssistedFactory
fun interface Factory : WellknownRetriever.Factory {
override fun create(urlContentFetcher: UrlContentFetcher): DefaultWellknownRetriever
}
override suspend fun getElementWellKnown(
host: String,
source: EnterpriseRemoteConfigSource
): WellknownRetrieverResult<ElementWellKnown> {
val overriddenElementWellKnown = enterpriseService.overriddenElementWellKnown()
if (overriddenElementWellKnown != null) {
return WellknownRetrieverResult.Success(overriddenElementWellKnown)
}
// The passed `host` function may be a full URL, in that case we're only interested in the actual host
val checkedHost = runCatchingExceptions { URL(host.ensureProtocol()).host.takeIfNotBlank() }
.getOrElse { return WellknownRetrieverResult.Error(IllegalArgumentException("host parameter is not valid", it)) }
?: host
// We instantiate different stores for different sources, so that we can cache them separately.
// The ESS_CONFIG source is cached with a prefix to differentiate it from the WELLKNOWN_ENDPOINT.
val prefix = when (source) {
EnterpriseRemoteConfigSource.ESS_CONFIG -> "ess_config"
EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT -> null
}
val store = elementWellknownStoreFactory.create(prefix)
return when (val cacheData = store.get(checkedHost)) {
is WellknownRetrieverResult.Success -> {
Timber.d("Using cached well-known for domain $checkedHost")
cacheData
}
is WellknownRetrieverResult.Outdated -> {
// Return the outdated data but refresh in the background
// If the cache is missing or outdated, trigger a refresh in background but still return the cached value
Timber.d("Outdated cached well-known for domain $checkedHost, returning existing value and fetching new one from network")
coroutineScope.launch { fetchElementWellKnown(checkedHost, source, store) }
cacheData
}
is WellknownRetrieverResult.NotFound -> {
// Try to fetch from the server
Timber.d("No cached well-known for domain $checkedHost, fetching from network")
fetchElementWellKnown(checkedHost, source, store)
}
is WellknownRetrieverResult.Error -> {
// Return the error
Timber.e(cacheData.exception, "Error retrieving well-known for domain $checkedHost")
cacheData.exception.toWellknownRetrieverResult()
}
}
}
private suspend fun fetchElementWellKnown(
host: String,
source: EnterpriseRemoteConfigSource,
store: ElementWellknownStore
): WellknownRetrieverResult<ElementWellKnown> {
return urlContentFetcher
.getUrl(url(host, source))
.mapCatchingExceptions {
val data = String(it)
val parsed = elementWellKnownParser.parse(data).getOrThrow()
// Also store in cache, if valid
store.update(host, data)
.onFailure { exception ->
Timber.e(exception, "Failed to parse cached Element .well-known data for $host, deleting cache")
store.delete(host)
}
parsed
}
.toWellknownRetrieverResult(host)
}
private fun <T> Result<T>.toWellknownRetrieverResult(host: String): WellknownRetrieverResult<T> = fold(
onSuccess = {
WellknownRetrieverResult.Success(it)
},
onFailure = {
Timber.e(it, "Failed to retrieve Element .well-known from $host")
// This check on message value is not ideal but this is what we got from the SDK.
it.toWellknownRetrieverResult()
}
)
private fun <T> Throwable.toWellknownRetrieverResult(): WellknownRetrieverResult<T> {
return if ((this as? ClientException.Generic)?.message?.contains("404") == true) {
WellknownRetrieverResult.NotFound
} else {
WellknownRetrieverResult.Error(this as Exception)
}
}
private fun url(host: String, source: EnterpriseRemoteConfigSource): String {
val elementWellKnownUrl = "https://$host/.well-known/element/element.json"
return when (source) {
EnterpriseRemoteConfigSource.ESS_CONFIG -> enterpriseService.essConfigEndpointUrl(host) ?: elementWellKnownUrl
EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT -> elementWellKnownUrl
}
}
}
@@ -1,71 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.wellknown.impl
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Example:
* <pre>
* {
* "registration_helper_url": "https://element.io",
* "enforce_element_pro": true,
* "rageshake_url": "https://example.org/rageshake",
* "brand_color": "#FF0000",
* "notification_sound": "ring.flac",
* "idp_app_scheme": "io.element.app",
* "custom_recovery_passphrase": {
* "min_character_count": 8
* }
* }
* </pre>
* .
*/
@Serializable
data class InternalElementWellKnown(
@SerialName("registration_helper_url")
val registrationHelperUrl: String? = null,
@SerialName("enforce_element_pro")
val enforceElementPro: Boolean? = null,
@SerialName("rageshake_url")
val rageshakeUrl: String? = null,
@SerialName("brand_color")
val brandColor: String? = null,
@SerialName("notification_sound")
val notificationSound: String? = null,
@SerialName("idp_app_scheme")
val identityProviderAppScheme: String? = null,
@SerialName("custom_recovery_passphrase")
val customRecoveryPassphrase: InternalCustomRecoveryPassphrase? = null,
@SerialName("content_scanner_url")
val contentScannerUrl: String? = null,
@SerialName("force_disable_e2ee")
val forceDisableE2EE: Boolean? = null,
@SerialName("map_tiler")
val mapTilerConfig: InternalMapTilerConfig? = null,
)
@Serializable
data class InternalCustomRecoveryPassphrase(
@SerialName("min_character_count")
val minCharacterCount: Int? = null,
)
@Serializable
data class InternalMapTilerConfig(
@SerialName("api_key")
val apiKey: String,
@SerialName("light_style_id")
val lightStyleId: String? = null,
@SerialName("dark_style_id")
val darkStyleId: String? = null,
@SerialName("base_url")
val baseUrl: String? = null,
)
@@ -1,34 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.wellknown.impl
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
* <pre>
* {
* "m.homeserver": {
* "base_url": "https://matrix.org"
* },
* "m.identity_server": {
* "base_url": "https://vector.im"
* },
* }
* </pre>
* .
*/
@Serializable
data class InternalWellKnown(
@SerialName("m.homeserver")
val homeServer: InternalWellKnownBaseConfig? = null,
@SerialName("m.identity_server")
val identityServer: InternalWellKnownBaseConfig? = null,
)
@@ -1,27 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.wellknown.impl
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
* <pre>
* {
* "base_url": "https://element.io"
* }
* </pre>
* .
*/
@Serializable
data class InternalWellKnownBaseConfig(
@SerialName("base_url")
val baseURL: String? = null
)
@@ -1,59 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.wellknown.impl
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.MapTilerConfig
import timber.log.Timber
private val loggerTag = LoggerTag("Wellknown")
/**
* Floor for the recovery passphrase minimum length. A value of 1 guarantees the derived passphrase is
* never empty (an empty passphrase derives a recoverable but effectively unprotected secret-storage
* key). Anything stronger than non-empty is left to the homeserver operator's configuration.
*/
private const val MINIMUM_PASSPHRASE_CHARACTER_COUNT = 1
fun InternalElementWellKnown.map() = ElementWellKnown(
registrationHelperUrl = registrationHelperUrl,
enforceElementPro = enforceElementPro,
rageshakeUrl = rageshakeUrl,
brandColor = brandColor,
notificationSound = notificationSound,
identityProviderAppScheme = identityProviderAppScheme,
customRecoveryPassphrase = customRecoveryPassphrase?.toPublic(),
contentScannerUrl = contentScannerUrl,
forceDisableE2EE = forceDisableE2EE,
mapTilerConfig = mapTilerConfig?.let {
MapTilerConfig(
apiKey = it.apiKey,
lightStyleId = it.lightStyleId,
darkStyleId = it.darkStyleId,
baseUrl = it.baseUrl,
)
}
)
private fun InternalCustomRecoveryPassphrase.toPublic(): CustomRecoveryPassphrase {
// Whenever the homeserver advertises the settings block we run the custom passphrase flow, flooring
// the minimum at 1 so the passphrase can never be empty even if the server omits min_character_count
// or advertises a non-positive value. The operator owns any stronger minimum.
val min = (minCharacterCount ?: MINIMUM_PASSPHRASE_CHARACTER_COUNT).coerceAtLeast(MINIMUM_PASSPHRASE_CHARACTER_COUNT)
if (min != minCharacterCount) {
Timber.tag(loggerTag.value).w(
"custom_recovery_passphrase.min_character_count was %s; flooring to %d",
minCharacterCount,
min,
)
}
return CustomRecoveryPassphrase(minCharacterCount = min)
}
@@ -1,41 +0,0 @@
/*
* 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.libraries.wellknown.impl
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
import io.element.android.libraries.wellknown.api.ElementWellknownStore
import io.element.android.libraries.wellknown.api.WellknownRetriever
import kotlinx.coroutines.CoroutineScope
@BindingContainer
@ContributesTo(SessionScope::class)
object SessionWellknownModule {
@Provides
fun provideWellKnownRetriever(
elementWellknownStoreFactory: ElementWellknownStore.Factory,
enterpriseService: EnterpriseService,
elementWellKnownParser: ElementWellKnownParser,
urlContentFetcher: UrlContentFetcher,
@SessionCoroutineScope coroutineScope: CoroutineScope,
): WellknownRetriever {
return DefaultWellknownRetriever(
elementWellknownStoreFactory = elementWellknownStoreFactory,
enterpriseService = enterpriseService,
elementWellKnownParser = elementWellKnownParser,
urlContentFetcher = urlContentFetcher,
coroutineScope = coroutineScope
)
}
}
@@ -1,19 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.wellknown.impl
import retrofit2.http.GET
internal interface WellknownAPI {
@GET(".well-known/matrix/client")
suspend fun getWellKnown(): InternalWellKnown
@GET(".well-known/element/element.json")
suspend fun getElementWellKnown(): InternalElementWellKnown
}
@@ -1,33 +0,0 @@
/*
* 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.libraries.wellknown.api
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class CustomRecoveryPassphraseTest {
@Test
fun `empty input never satisfies a positive minimum`() {
assertThat(CustomRecoveryPassphrase(minCharacterCount = 1).isSatisfiedBy("")).isFalse()
}
@Test
fun `input shorter than minimum is not satisfied`() {
assertThat(CustomRecoveryPassphrase(minCharacterCount = 8).isSatisfiedBy("abc")).isFalse()
}
@Test
fun `input exactly at minimum is satisfied`() {
assertThat(CustomRecoveryPassphrase(minCharacterCount = 8).isSatisfiedBy("abcdefgh")).isTrue()
}
@Test
fun `input longer than minimum is satisfied`() {
assertThat(CustomRecoveryPassphrase(minCharacterCount = 4).isSatisfiedBy("abcdefgh")).isTrue()
}
}
@@ -1,174 +0,0 @@
/*
* 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.libraries.wellknown.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.features.wellknown.test.anElementWellKnown
import io.element.android.libraries.androidutils.json.DefaultJsonProvider
import io.element.android.libraries.cachestore.api.CacheData
import io.element.android.libraries.sessionstorage.test.InMemoryCacheStore
import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import kotlinx.coroutines.test.runTest
import org.junit.Test
private const val A_DOMAIN = "matrix.example.com"
private const val A_CACHE_KEY = "https://$A_DOMAIN/.well-known/element/element.json"
// 1 day in millis
private const val CACHE_VALIDITY_MILLIS = 1 * 24 * 60 * 60 * 1000L
class DefaultWellKnownStoreTest {
private val jsonProvider = DefaultJsonProvider()
@Test
fun `get returns NotFound when cache is empty`() = runTest {
val sut = createDefaultWellKnownStore()
assertThat(sut.get(A_DOMAIN)).isEqualTo(WellknownRetrieverResult.NotFound)
}
@Test
fun `get returns Success when cache has fresh data`() = runTest {
val wellKnown = InternalElementWellKnown(rageshakeUrl = "https://rageshake.example.com")
val jsonString = jsonProvider().encodeToString(wellKnown)
val clock = FakeSystemClock(epochMillisResult = A_FAKE_TIMESTAMP)
val cacheStore = InMemoryCacheStore(
initialData = mapOf(A_CACHE_KEY to CacheData(jsonString, A_FAKE_TIMESTAMP))
)
val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock)
val result = sut.get(A_DOMAIN)
assertThat(result).isEqualTo(WellknownRetrieverResult.Success(wellKnown.map()))
}
@Test
fun `get returns Outdated when cached data has expired`() = runTest {
val wellKnown = InternalElementWellKnown(rageshakeUrl = "https://rageshake.example.com")
val jsonString = jsonProvider().encodeToString(wellKnown)
// Store data at timestamp 0, but clock is past the validity window
val expiredTimestamp = 0L
val nowTimestamp = expiredTimestamp + CACHE_VALIDITY_MILLIS + 1
val clock = FakeSystemClock(epochMillisResult = nowTimestamp)
val cacheStore = InMemoryCacheStore(
initialData = mapOf(A_CACHE_KEY to CacheData(jsonString, expiredTimestamp))
)
val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock)
val result = sut.get(A_DOMAIN)
assertThat(result).isEqualTo(WellknownRetrieverResult.Outdated(wellKnown.map()))
}
@Test
fun `get returns Error when cached data is invalid JSON`() = runTest {
val cacheStore = InMemoryCacheStore(
initialData = mapOf(A_CACHE_KEY to CacheData("not valid json at all", A_FAKE_TIMESTAMP))
)
val sut = createDefaultWellKnownStore(cacheStore = cacheStore)
val result = sut.get(A_DOMAIN)
assertThat(result).isInstanceOf(WellknownRetrieverResult.Error::class.java)
}
@Test
fun `update stores data and get returns Success`() = runTest {
val wellKnown = """
{
"registration_helper_url": "https://element.io",
"enforce_element_pro": true,
"rageshake_url": "https://example.org/rageshake",
"brand_color": "#FF0000",
"notification_sound": "ring.flac",
"idp_app_scheme": "io.element.app",
"custom_recovery_passphrase": {
"min_character_count": 8
}
}
""".trimIndent()
val clock = FakeSystemClock(epochMillisResult = A_FAKE_TIMESTAMP)
val cacheStore = InMemoryCacheStore()
val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock)
val updateResult = sut.update(A_DOMAIN, wellKnown)
assertThat(updateResult.isSuccess).isTrue()
val result = sut.get(A_DOMAIN)
assertThat(result).isEqualTo(WellknownRetrieverResult.Success(anElementWellKnown(
registrationHelperUrl = "https://element.io",
enforceElementPro = true,
rageshakeUrl = "https://example.org/rageshake",
brandColor = "#FF0000",
notificationSound = "ring.flac",
identityProviderAppScheme = "io.element.app",
customRecoveryPassphrase = CustomRecoveryPassphrase(8),
)))
}
@Test
fun `delete removes data and get returns NotFound`() = runTest {
val wellKnown = InternalElementWellKnown(rageshakeUrl = "https://rageshake.example.com")
val jsonString = jsonProvider().encodeToString(wellKnown)
val clock = FakeSystemClock(epochMillisResult = A_FAKE_TIMESTAMP)
val cacheStore = InMemoryCacheStore(
initialData = mapOf(A_CACHE_KEY to CacheData(jsonString, A_FAKE_TIMESTAMP))
)
val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock)
val deleteResult = sut.delete(A_DOMAIN)
assertThat(deleteResult.isSuccess).isTrue()
assertThat(sut.get(A_DOMAIN)).isEqualTo(WellknownRetrieverResult.NotFound)
}
@Test
fun `get returns data at the exact cache validity boundary as Success`() = runTest {
val wellKnown = InternalElementWellKnown(notificationSound = "ping.flac")
val jsonString = jsonProvider().encodeToString(wellKnown)
// Exactly at the boundary: epochMillis == updatedAt + CACHE_VALIDITY_MILLIS → not expired
val storedAt = 1000L
val nowAtBoundary = storedAt + CACHE_VALIDITY_MILLIS
val clock = FakeSystemClock(epochMillisResult = nowAtBoundary)
val cacheStore = InMemoryCacheStore(
initialData = mapOf(A_CACHE_KEY to CacheData(jsonString, storedAt))
)
val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock)
val result = sut.get(A_DOMAIN)
assertThat(result).isEqualTo(WellknownRetrieverResult.Success(wellKnown.map()))
}
@Test
fun `prefix is used`() = runTest {
val prefix = "test-prefix"
val clock = FakeSystemClock(epochMillisResult = A_FAKE_TIMESTAMP)
val cacheStore = InMemoryCacheStore(
initialData = mapOf("$prefix-$A_DOMAIN" to CacheData("{}", A_FAKE_TIMESTAMP))
)
val sut = createDefaultWellKnownStore(prefix = prefix, cacheStore = cacheStore, systemClock = clock)
assertThat(sut.get(A_DOMAIN)).isInstanceOf(WellknownRetrieverResult.Success::class.java)
}
private fun createDefaultWellKnownStore(
prefix: String? = null,
cacheStore: InMemoryCacheStore = InMemoryCacheStore(),
systemClock: FakeSystemClock = FakeSystemClock(),
elementWellKnownParser: ElementWellKnownParser = DefaultElementWellKnownParser(jsonProvider),
) = DefaultWellKnownStore(
prefix = prefix,
cacheStore = cacheStore,
systemClock = systemClock,
elementWellKnownParser = elementWellKnownParser,
)
}
@@ -1,241 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.libraries.wellknown.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.wellknown.test.FakeElementWellknownStore
import io.element.android.features.wellknown.test.aMapTilerConfig
import io.element.android.features.wellknown.test.anElementWellKnown
import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.exception.ClientException
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
import io.element.android.libraries.wellknown.api.ElementWellknownStore
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.net.URL
class DefaultWellknownRetrieverTest {
@Test
fun `get element wellknown calls resolver and parser`() = runTest {
val parser = lambdaRecorder<String, Result<ElementWellKnown>> { Result.success(anElementWellKnown()) }
val resolver = SpyUrlContentFetcher { Result.success(WELLKNOWN_CONTENT.toByteArray()) }
val sut = createDefaultWellknownRetriever(
elementWellKnownParser = parser,
urlResolver = resolver,
)
sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT)
resolver.assertions().isCalledOnce().with(value(WELLKNOWN_URL))
parser.assertions().isCalledOnce().with(value(WELLKNOWN_CONTENT))
}
@Test
fun `get element wellknown parser error`() = runTest {
val sut = createDefaultWellknownRetriever(
elementWellKnownParser = { Result.failure(IllegalStateException("Invalid JSON")) },
)
assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
.isInstanceOf(WellknownRetrieverResult.Error::class.java)
}
@Test
fun `get element wellknown network error`() = runTest {
val resolver = SpyUrlContentFetcher { Result.failure(IllegalStateException("Network error")) }
val sut = createDefaultWellknownRetriever(urlResolver = resolver)
assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
.isInstanceOf(WellknownRetrieverResult.Error::class.java)
}
@Test
fun `get element wellknown 404 http error counts as not found`() = runTest {
val resolver = SpyUrlContentFetcher { Result.failure(ClientException.Generic("Http error: 404", null)) }
val sut = createDefaultWellknownRetriever(urlResolver = resolver)
assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
.isInstanceOf(WellknownRetrieverResult.NotFound::class.java)
}
@Test
fun `get element wellknown will return an error if the provided host is not valid`() = runTest {
val sut = createDefaultWellknownRetriever()
assertThat(sut.getElementWellKnown("!not valid", EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
.isInstanceOf(WellknownRetrieverResult.Error::class.java)
}
@Test
fun `get element wellknown will return a cached value if present instead of fetching`() = runTest {
val host = URL(WELLKNOWN_URL).host.ensureProtocol()
val sut = createDefaultWellknownRetriever(
cacheStoreFactory = { FakeElementWellknownStore(initialData = mapOf(host to WellknownRetrieverResult.Success(expectedElementWellKnown))) }
)
assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
.isInstanceOf(WellknownRetrieverResult.Success::class.java)
}
@Test
fun `get element wellknown will return an outdated value first then fetch the current one`() = runTest {
val host = URL(WELLKNOWN_URL).host.ensureProtocol()
val resolver = SpyUrlContentFetcher { Result.success(WELLKNOWN_CONTENT.toByteArray()) }
val sut = createDefaultWellknownRetriever(
urlResolver = resolver,
cacheStoreFactory = { FakeElementWellknownStore(initialData = mapOf(host to WellknownRetrieverResult.Outdated(expectedElementWellKnown))) }
)
// The first call returns the outdated value
assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
.isInstanceOf(WellknownRetrieverResult.Outdated::class.java)
// The resolver is not called yet because the fetch is done in the background
resolver.assertions().isNeverCalled()
// We give it some time to run the background fetch
runCurrent()
// The resolver is called during the background fetch
resolver.assertions().isCalledOnce().with(value(WELLKNOWN_URL))
}
@Test
fun `get element wellknown will return an error if it happens when trying to retrieve the cached value`() = runTest {
val host = URL(WELLKNOWN_URL).host.ensureProtocol()
val sut = createDefaultWellknownRetriever(
cacheStoreFactory = { FakeElementWellknownStore(initialData = mapOf(host to WellknownRetrieverResult.Error(IllegalStateException("BOOM")))) }
)
assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
.isInstanceOf(WellknownRetrieverResult.Error::class.java)
}
@Test
fun `get element wellknown will return an error if it fails to serialize the retrieved value into the cache`() = runTest {
val sut = createDefaultWellknownRetriever(
elementWellKnownParser = { Result.failure(IllegalStateException("BOOM")) },
)
assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
.isInstanceOf(WellknownRetrieverResult.Error::class.java)
}
@Test
fun `WELLKNOWN_ENDPOINT source creates a store with no prefix`() = runTest {
val storeFactory = lambdaRecorder<String?, ElementWellknownStore> { FakeElementWellknownStore() }
val sut = createDefaultWellknownRetriever(cacheStoreFactory = storeFactory)
sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT)
storeFactory.assertions().isCalledOnce().with(value(null))
}
@Test
fun `ESS_CONFIG source creates a store with ess prefix and checks the ESS config endpoint`() = runTest {
val essConfigUrl = "https://ess-config.example.com"
val storeFactory = lambdaRecorder<String?, ElementWellknownStore> { FakeElementWellknownStore() }
val resolver = SpyUrlContentFetcher { Result.success(WELLKNOWN_CONTENT.toByteArray()) }
val sut = createDefaultWellknownRetriever(
cacheStoreFactory = storeFactory,
enterpriseService = FakeEnterpriseService(
overrideWellKnownResult = { null },
essConfigEndpointUrlResult = { essConfigUrl },
),
urlResolver = resolver,
)
sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.ESS_CONFIG)
storeFactory.assertions().isCalledOnce().with(value("ess_config"))
resolver.assertions().isCalledOnce().with(value(essConfigUrl))
}
@Test
fun `get element wellknown was overridden`() = runTest {
val wellKnown = anElementWellKnown()
val resolver = SpyUrlContentFetcher { Result.success(WELLKNOWN_CONTENT.toByteArray()) }
val sut = createDefaultWellknownRetriever(
enterpriseService = FakeEnterpriseService(
overrideWellKnownResult = { wellKnown },
),
urlResolver = resolver,
)
// The overridden value is returned
assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT)).isEqualTo(
WellknownRetrieverResult.Success(wellKnown)
)
// And the endpoint is never hit
resolver.assertions().isNeverCalled()
}
private fun TestScope.createDefaultWellknownRetriever(
cacheStoreFactory: ElementWellknownStore.Factory = { FakeElementWellknownStore() },
elementWellKnownParser: ElementWellKnownParser = { Result.success(expectedElementWellKnown) },
enterpriseService: FakeEnterpriseService = FakeEnterpriseService(overrideWellKnownResult = { null }),
urlResolver: UrlContentFetcher = SpyUrlContentFetcher { Result.success(WELLKNOWN_CONTENT.toByteArray()) },
) = DefaultWellknownRetriever(
elementWellknownStoreFactory = cacheStoreFactory,
enterpriseService = enterpriseService,
elementWellKnownParser = elementWellKnownParser,
urlContentFetcher = urlResolver,
coroutineScope = backgroundScope,
)
companion object {
private const val WELLKNOWN_URL = "https://user.domain.org/.well-known/element/element.json"
private const val WELLKNOWN_CONTENT = """{
"registration_helper_url": "a_registration_url",
"enforce_element_pro": true,
"rageshake_url": "a_rageshake_url",
"brand_color": "#FF0000",
"notification_sound": "a_notification_sound.flac",
"idp_app_scheme": "an_app_scheme",
"content_scanner_url": "https://content-scanner.example.com",
"force_disable_e2ee": false,
"map_tiler": {
"api_key": "test_api_key",
"base_url": "https://tile-server.example.com"
}
}"""
private val expectedElementWellKnown = ElementWellKnown(
registrationHelperUrl = "a_registration_url",
enforceElementPro = true,
rageshakeUrl = "a_rageshake_url",
brandColor = "#FF0000",
notificationSound = "a_notification_sound.flac",
identityProviderAppScheme = "an_app_scheme",
contentScannerUrl = "https://content-scanner.example.com",
customRecoveryPassphrase = null,
forceDisableE2EE = false,
mapTilerConfig = aMapTilerConfig(
apiKey = "test_api_key",
baseUrl = "https://tile-server.example.com",
)
)
}
}
private class SpyUrlContentFetcher(lambda: (String) -> Result<ByteArray>) : UrlContentFetcher {
private val spy = lambdaRecorder<String, Result<ByteArray>> { lambda(it) }
override suspend fun getUrl(url: String): Result<ByteArray> {
return spy(url)
}
fun assertions() = spy.assertions()
}