mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 08:15:02 +00:00
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:
committed by
GitHub
parent
f94566e44f
commit
8b2c207728
@@ -259,6 +259,7 @@ dependencies {
|
||||
if (isEnterpriseBuild) {
|
||||
allEnterpriseImpl(project)
|
||||
implementation(projects.appicon.enterprise)
|
||||
implementation(project(":enterprise:features:enterprise:shared"))
|
||||
} else {
|
||||
implementation(projects.features.enterprise.implFoss)
|
||||
implementation(projects.appicon.element)
|
||||
|
||||
@@ -18,7 +18,7 @@ import dev.zacsweers.metro.ContributesTo
|
||||
import dev.zacsweers.metro.Provides
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.appconfig.ApplicationConfig
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.features.enterprise.api.IsEnterpriseBuild
|
||||
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
@@ -74,17 +74,18 @@ object AppModule {
|
||||
fun providesBuildMeta(
|
||||
@ApplicationContext context: Context,
|
||||
buildType: BuildType,
|
||||
enterpriseService: EnterpriseService,
|
||||
isEnterpriseBuild: IsEnterpriseBuild,
|
||||
): BuildMeta {
|
||||
val applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name)
|
||||
val isEnterprise = isEnterpriseBuild()
|
||||
return BuildMeta(
|
||||
isDebuggable = BuildConfig.DEBUG,
|
||||
buildType = buildType,
|
||||
applicationName = applicationName,
|
||||
productionApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME,
|
||||
desktopApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME,
|
||||
productionApplicationName = if (isEnterprise) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME,
|
||||
desktopApplicationName = if (isEnterprise) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME,
|
||||
applicationId = BuildConfig.APPLICATION_ID,
|
||||
isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
|
||||
isEnterpriseBuild = isEnterprise,
|
||||
// TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
|
||||
lowPrivacyLoggingEnabled = false,
|
||||
versionName = BuildConfig.VERSION_NAME,
|
||||
|
||||
@@ -51,7 +51,7 @@ import io.element.android.compound.colors.SemanticColorsLightDark
|
||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
||||
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
||||
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
|
||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||
import io.element.android.features.ftue.api.state.FtueService
|
||||
import io.element.android.features.ftue.api.state.FtueState
|
||||
@@ -157,7 +157,7 @@ class LoggedInFlowNode(
|
||||
private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher,
|
||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
|
||||
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider,
|
||||
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
|
||||
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Placeholder,
|
||||
@@ -685,7 +685,7 @@ class LoggedInFlowNode(
|
||||
|
||||
val currentMapTilerConfig = LocalMapTilerConfig.current
|
||||
val updatedMapTilerConfig by produceState(currentMapTilerConfig) {
|
||||
value = remoteEnterpriseConfigProvider.get(matrixClient.sessionId).dataOrNull()?.mapTilerConfig ?: currentMapTilerConfig
|
||||
value = customMapTilerConfigProvider.get().getOrNull() ?: currentMapTilerConfig
|
||||
}
|
||||
|
||||
ElementThemeApp(
|
||||
|
||||
+1
-1
Submodule enterprise updated: f5f910856c...3bf0df1f25
+13
-13
@@ -12,7 +12,6 @@ import androidx.compose.ui.graphics.Color
|
||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
@@ -21,9 +20,6 @@ import kotlinx.coroutines.flow.Flow
|
||||
* On a standard build this reports permissive defaults, so callers do not need to branch on the build type themselves.
|
||||
*/
|
||||
interface EnterpriseService {
|
||||
/** Whether this build is an enterprise one; `false` for the public app. */
|
||||
val isEnterpriseBuild: Boolean
|
||||
|
||||
/**
|
||||
* Whether the given session belongs to an enterprise deployment, which can be true even on a standard build.
|
||||
*
|
||||
@@ -41,17 +37,26 @@ interface EnterpriseService {
|
||||
suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher): String
|
||||
|
||||
/**
|
||||
* The homeservers the user is allowed to sign in to; an empty list, or one containing [ANY_ACCOUNT_PROVIDER], means no restriction.
|
||||
* Returns the list of homeservers the user is allowed to sign in to.
|
||||
*
|
||||
* If the list is empty or contains the special value [ANY_ACCOUNT_PROVIDER], the user is allowed to sign in to any homeserver.
|
||||
*/
|
||||
fun defaultHomeserverList(): List<String>
|
||||
fun homeserverAllowList(): List<String>
|
||||
|
||||
/**
|
||||
* Whether the user is allowed to sign in to a given homeserver, according to [defaultHomeserverList].
|
||||
* Whether the user is allowed to sign in to a given homeserver, according to [homeserverAllowList].
|
||||
*
|
||||
* @param homeserverUrl the server the user is trying to use.
|
||||
*/
|
||||
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
|
||||
|
||||
/**
|
||||
* Whether the given homeserver enforces the use of Element Pro or a derived app.
|
||||
*
|
||||
* @param homeserverUrl the homeserver to check.
|
||||
*/
|
||||
suspend fun isElementProEnforced(homeserverUrl: String): Boolean
|
||||
|
||||
/**
|
||||
* Override the brand color.
|
||||
* @param sessionId the session to override the brand color for, or null to set the brand color to use when there is no session.
|
||||
@@ -94,11 +99,6 @@ interface EnterpriseService {
|
||||
*/
|
||||
fun getNoisyNotificationChannelId(sessionId: SessionId): String?
|
||||
|
||||
/**
|
||||
* Gets the overridden Element Well-Known data if it has been set, or null if not set.
|
||||
*/
|
||||
fun overriddenElementWellKnown(): ElementWellKnown?
|
||||
|
||||
/**
|
||||
* Gets the Element Server Suite (ESS) config endpoint URL for the given domain.
|
||||
* Returns `null` when this build does not read its configuration from an ESS deployment.
|
||||
@@ -113,7 +113,7 @@ interface EnterpriseService {
|
||||
}
|
||||
|
||||
fun EnterpriseService.canConnectToAnyHomeserver(): Boolean {
|
||||
return defaultHomeserverList().let {
|
||||
return homeserverAllowList().let {
|
||||
it.isEmpty() || it.contains(EnterpriseService.ANY_ACCOUNT_PROVIDER)
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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.enterprise.api
|
||||
|
||||
/**
|
||||
* A use case to check if this build of the app is an enterprise build.
|
||||
*/
|
||||
fun interface IsEnterpriseBuild {
|
||||
/**
|
||||
* Returns `true` if this build of the app is an enterprise build, `false` otherwise.
|
||||
*/
|
||||
operator fun invoke(): Boolean
|
||||
}
|
||||
+3
-3
@@ -14,6 +14,9 @@ package io.element.android.features.enterprise.api
|
||||
* [init] must have run before the other members return meaningful answers.
|
||||
*/
|
||||
interface SessionEnterpriseService {
|
||||
/** Loads the deployment configuration of this session; to be called once when the session starts. */
|
||||
suspend fun init()
|
||||
|
||||
/** Whether Element Call may be used on this session, which a deployment can turn off. */
|
||||
suspend fun isElementCallAvailable(): Boolean
|
||||
|
||||
@@ -26,7 +29,4 @@ interface SessionEnterpriseService {
|
||||
|
||||
/** Whether the homeserver forbids creating encrypted rooms, in which case the app must not offer the option. */
|
||||
suspend fun isEncryptionDisabledByHomeserver(): Boolean
|
||||
|
||||
/** Loads the deployment configuration of this session; to be called once when the session starts. */
|
||||
suspend fun init()
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* 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.enterprise.api.remoteconfig
|
||||
|
||||
/**
|
||||
* A provider of a runtime-fetched MapTiler configuration, which can be used to override the default MapTiler configuration.
|
||||
*/
|
||||
fun interface CustomMapTilerConfigProvider {
|
||||
suspend fun get(): Result<MapTilerConfig?>
|
||||
}
|
||||
+4
-15
@@ -1,29 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
* 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
|
||||
package io.element.android.features.enterprise.api.remoteconfig
|
||||
|
||||
data class ElementWellKnown(
|
||||
val registrationHelperUrl: String?,
|
||||
val enforceElementPro: Boolean?,
|
||||
val rageshakeUrl: String?,
|
||||
val brandColor: String?,
|
||||
val notificationSound: String?,
|
||||
val identityProviderAppScheme: String?,
|
||||
val customRecoveryPassphrase: CustomRecoveryPassphrase?,
|
||||
val contentScannerUrl: String?,
|
||||
val forceDisableE2EE: Boolean?,
|
||||
val mapTilerConfig: MapTilerConfig?,
|
||||
)
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
/**
|
||||
* A `MapTiler` configuration received from the server. This configuration is used to display maps in the app.
|
||||
*/
|
||||
@Immutable
|
||||
data class MapTilerConfig(
|
||||
/** The API key to use with MapTiler. This is always not null, but can be empty/blank. */
|
||||
val apiKey: String,
|
||||
-17
@@ -1,17 +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.features.enterprise.api.remoteconfig
|
||||
|
||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||
|
||||
/**
|
||||
* Typealias for the well-known configuration used for enterprise features.
|
||||
*
|
||||
* This will allow for easier refactoring in the future when we'll want to move as much of its logic as possible to this module.
|
||||
*/
|
||||
typealias RemoteEnterpriseConfig = ElementWellKnown
|
||||
-35
@@ -1,35 +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.features.enterprise.api.remoteconfig
|
||||
|
||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||
|
||||
/**
|
||||
* Wrapper to fetch the remote enterprise configuration for a given [SessionId], either from a local cache or from a remote source.
|
||||
*/
|
||||
interface RemoteEnterpriseConfigProvider {
|
||||
/**
|
||||
* Returns the configuration of a session, served from the cache when possible.
|
||||
* A stale entry is reported as outdated while a refresh runs in the background, so this rarely waits on the network.
|
||||
*
|
||||
* @param sessionId the session whose configuration is requested.
|
||||
*/
|
||||
suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig>
|
||||
|
||||
/**
|
||||
* Creates a provider, which needs a fetcher because the configuration is read over HTTP.
|
||||
*/
|
||||
interface Factory {
|
||||
/**
|
||||
* @param urlContentFetcher used to perform the request; a client that is not authenticated yet also works.
|
||||
*/
|
||||
fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider
|
||||
}
|
||||
}
|
||||
+7
-8
@@ -1,33 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
* 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
|
||||
package io.element.android.features.enterprise.api.remoteconfig
|
||||
|
||||
sealed interface WellknownRetrieverResult<out T> {
|
||||
sealed interface RemoteEnterpriseConfigResult<out T> {
|
||||
/**
|
||||
* Well-known data has been successfully retrieved.
|
||||
*/
|
||||
data class Success<out T>(val data: T) : WellknownRetrieverResult<T>
|
||||
data class Success<out T>(val data: T) : RemoteEnterpriseConfigResult<T>
|
||||
|
||||
/**
|
||||
* Well-known data has been retrieved from the local cache but is outdated and a manual refresh has been launched.
|
||||
*/
|
||||
data class Outdated<out T>(val data: T) : WellknownRetrieverResult<T>
|
||||
data class Outdated<out T>(val data: T) : RemoteEnterpriseConfigResult<T>
|
||||
|
||||
/**
|
||||
* Well-known data is not found (file does not exist server side, we got a 404).
|
||||
*/
|
||||
data object NotFound : WellknownRetrieverResult<Nothing>
|
||||
data object NotFound : RemoteEnterpriseConfigResult<Nothing>
|
||||
|
||||
/**
|
||||
* Any other error.
|
||||
*/
|
||||
data class Error(val exception: Exception) : WellknownRetrieverResult<Nothing>
|
||||
data class Error(val exception: Exception) : RemoteEnterpriseConfigResult<Nothing>
|
||||
|
||||
fun dataOrNull(): T? = when (this) {
|
||||
is Success<T> -> data
|
||||
+2
-6
@@ -16,18 +16,16 @@ import io.element.android.features.enterprise.api.BugReportUrl
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultEnterpriseService : EnterpriseService {
|
||||
override val isEnterpriseBuild = false
|
||||
|
||||
override suspend fun isEnterpriseUser(sessionId: SessionId) = false
|
||||
override suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher) = url
|
||||
override fun defaultHomeserverList(): List<String> = emptyList()
|
||||
override fun homeserverAllowList(): List<String> = emptyList()
|
||||
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
|
||||
override suspend fun isElementProEnforced(homeserverUrl: String): Boolean = false
|
||||
|
||||
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = Unit
|
||||
|
||||
@@ -48,7 +46,5 @@ class DefaultEnterpriseService : EnterpriseService {
|
||||
|
||||
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
|
||||
|
||||
override fun overriddenElementWellKnown(): ElementWellKnown? = null
|
||||
|
||||
override fun essConfigEndpointUrl(domain: String): String? = null
|
||||
}
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.enterprise.impl
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.features.enterprise.api.IsEnterpriseBuild
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultIsEnterpriseBuild : IsEnterpriseBuild {
|
||||
override fun invoke(): Boolean = false
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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.enterprise.impl.remoteconfig
|
||||
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
|
||||
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultCustomMapTilerConfigProvider : CustomMapTilerConfigProvider {
|
||||
override suspend fun get(): Result<MapTilerConfig?> = Result.success(null)
|
||||
}
|
||||
-30
@@ -1,30 +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.features.enterprise.impl.remoteconfig
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfig
|
||||
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultRemoteEnterpriseConfigProvider : RemoteEnterpriseConfigProvider {
|
||||
override suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig> {
|
||||
return WellknownRetrieverResult.NotFound
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class Factory : RemoteEnterpriseConfigProvider.Factory {
|
||||
override fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider {
|
||||
return DefaultRemoteEnterpriseConfigProvider()
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-8
@@ -19,15 +19,9 @@ import org.junit.Test
|
||||
|
||||
class DefaultEnterpriseServiceTest {
|
||||
@Test
|
||||
fun `isEnterpriseBuild is false`() {
|
||||
fun homeserverWhitelist() {
|
||||
val defaultEnterpriseService = DefaultEnterpriseService()
|
||||
assertThat(defaultEnterpriseService.isEnterpriseBuild).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `defaultHomeserverList should return empty list`() {
|
||||
val defaultEnterpriseService = DefaultEnterpriseService()
|
||||
assertThat(defaultEnterpriseService.defaultHomeserverList()).isEmpty()
|
||||
assertThat(defaultEnterpriseService.homeserverAllowList()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+6
-8
@@ -14,7 +14,6 @@ import io.element.android.features.enterprise.api.BugReportUrl
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -22,7 +21,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class FakeEnterpriseService(
|
||||
override val isEnterpriseBuild: Boolean = false,
|
||||
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
|
||||
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
|
||||
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
|
||||
@@ -33,8 +31,8 @@ class FakeEnterpriseService(
|
||||
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
|
||||
private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() },
|
||||
private val tweakMasUrlResult: (String, String, UrlContentFetcher) -> String = { _, _, _ -> lambdaError() },
|
||||
private val overrideWellKnownResult: () -> ElementWellKnown? = { lambdaError() },
|
||||
private val essConfigEndpointUrlResult: (String) -> String = { lambdaError() },
|
||||
private val isElementProEnforcedResult: (String) -> Boolean = { lambdaError() },
|
||||
) : EnterpriseService {
|
||||
private val brandColorState = MutableStateFlow(initialBrandColor)
|
||||
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
|
||||
@@ -47,7 +45,7 @@ class FakeEnterpriseService(
|
||||
tweakMasUrlResult(url, homeserver, urlContentFetcher)
|
||||
}
|
||||
|
||||
override fun defaultHomeserverList(): List<String> {
|
||||
override fun homeserverAllowList(): List<String> {
|
||||
return defaultHomeserverListResult()
|
||||
}
|
||||
|
||||
@@ -55,6 +53,10 @@ class FakeEnterpriseService(
|
||||
isAllowedToConnectToHomeserverResult(homeserverUrl)
|
||||
}
|
||||
|
||||
override suspend fun isElementProEnforced(homeserverUrl: String): Boolean = simulateLongTask {
|
||||
isElementProEnforcedResult(homeserverUrl)
|
||||
}
|
||||
|
||||
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = simulateLongTask {
|
||||
overrideBrandColorResult(sessionId, brandColor)
|
||||
}
|
||||
@@ -84,10 +86,6 @@ class FakeEnterpriseService(
|
||||
return getNoisyNotificationChannelIdResult(sessionId)
|
||||
}
|
||||
|
||||
override fun overriddenElementWellKnown(): ElementWellKnown? {
|
||||
return overrideWellKnownResult()
|
||||
}
|
||||
|
||||
override fun essConfigEndpointUrl(domain: String): String {
|
||||
return essConfigEndpointUrlResult(domain)
|
||||
}
|
||||
|
||||
-21
@@ -1,21 +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.features.enterprise.test.remoteconfig
|
||||
|
||||
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfig
|
||||
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||
|
||||
class FakeRemoteEnterpriseConfigProvider(
|
||||
private val getResult: (SessionId) -> WellknownRetrieverResult<RemoteEnterpriseConfig> = { WellknownRetrieverResult.NotFound }
|
||||
) : RemoteEnterpriseConfigProvider {
|
||||
override suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig> {
|
||||
return getResult(sessionId)
|
||||
}
|
||||
}
|
||||
-17
@@ -1,17 +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.features.enterprise.test.remoteconfig
|
||||
|
||||
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||
|
||||
class FakeRemoteEnterpriseConfigProviderFactory(
|
||||
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
|
||||
) : RemoteEnterpriseConfigProvider.Factory {
|
||||
override fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider = remoteEnterpriseConfigProvider
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.enterprise.test.remoteconfig
|
||||
|
||||
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||
|
||||
fun aMapTilerConfig(
|
||||
apiKey: String = "test_api_key",
|
||||
baseUrl: String? = null,
|
||||
lightStyleId: String? = null,
|
||||
darkStyleId: String? = null,
|
||||
) = MapTilerConfig(
|
||||
apiKey = apiKey,
|
||||
baseUrl = baseUrl,
|
||||
lightStyleId = lightStyleId,
|
||||
darkStyleId = darkStyleId,
|
||||
)
|
||||
@@ -71,6 +71,7 @@ dependencies {
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.wellknown.api)
|
||||
implementation(projects.features.enterprise.api)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.datetime)
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
package io.element.android.features.location.api
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||
|
||||
/**
|
||||
* Provides a [MapTilerConfig] for the MapLibre maps.
|
||||
|
||||
+1
-1
@@ -37,6 +37,7 @@ import coil3.compose.rememberAsyncImagePainter
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.SuccessResult
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||
import io.element.android.features.location.api.internal.StaticMapPlaceholder
|
||||
import io.element.android.features.location.api.internal.centerBottomEdge
|
||||
import io.element.android.features.location.api.internal.rememberStaticMapBuilder
|
||||
@@ -45,7 +46,6 @@ import io.element.android.libraries.designsystem.components.PinVariant
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||
|
||||
/**
|
||||
* Shows a static map image downloaded via a third party service's static maps API.
|
||||
|
||||
+1
-1
@@ -8,8 +8,8 @@
|
||||
|
||||
package io.element.android.features.location.api.internal
|
||||
|
||||
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||
import io.element.android.features.location.api.BuildConfig
|
||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -10,8 +10,8 @@
|
||||
|
||||
package io.element.android.features.location.api.internal
|
||||
|
||||
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||
import io.element.android.features.location.api.BuildConfig
|
||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||
|
||||
internal class MapTilerTileServerStyleUriBuilder(
|
||||
private val baseUrl: String,
|
||||
|
||||
+1
-2
@@ -10,8 +10,7 @@ package io.element.android.features.location.api.internal
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||
|
||||
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||
/**
|
||||
* Builds an URL for a 3rd party service provider static maps API.
|
||||
*/
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ package io.element.android.features.location.api.internal
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||
|
||||
/**
|
||||
* Builds a style URI for a MapLibre compatible tile server.
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
package io.element.android.features.location.api.internal
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||
import org.junit.Test
|
||||
|
||||
class MapTilerTileServerStyleUriBuilderTest {
|
||||
|
||||
+1
-1
@@ -43,6 +43,7 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.max
|
||||
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
@@ -50,7 +51,6 @@ import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||
import org.maplibre.compose.camera.CameraState
|
||||
import org.maplibre.compose.camera.rememberCameraState
|
||||
import org.maplibre.compose.map.MapOptions
|
||||
|
||||
+3
-3
@@ -22,7 +22,7 @@ import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
||||
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
|
||||
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
|
||||
import io.element.android.features.location.impl.common.LocationConstraintsCheck
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
@@ -73,7 +73,7 @@ class ShareLocationPresenter(
|
||||
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
||||
private val liveLocationStore: LiveLocationStore,
|
||||
private val userLocationStateFactory: UserLocationState.Factory,
|
||||
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider,
|
||||
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
|
||||
) : Presenter<ShareLocationState> {
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
@@ -96,7 +96,7 @@ class ShareLocationPresenter(
|
||||
val currentUser by client.userProfile.collectAsState()
|
||||
val customMapConfig by produceState(AsyncData.Loading()) {
|
||||
// Ignore errors
|
||||
value = AsyncData.Success(remoteEnterpriseConfigProvider.get(client.sessionId).dataOrNull()?.mapTilerConfig)
|
||||
value = AsyncData.Success(customMapTilerConfigProvider.get().getOrNull())
|
||||
}
|
||||
val sendLiveLocationPermissions by room.permissionsAsState(SendLiveLocationPermissions.DEFAULT) { perms ->
|
||||
perms.sendLiveLocationPermissions()
|
||||
|
||||
+1
-1
@@ -8,12 +8,12 @@
|
||||
|
||||
package io.element.android.features.location.impl.share
|
||||
|
||||
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class ShareLocationState(
|
||||
|
||||
+1
-1
@@ -9,13 +9,13 @@
|
||||
package io.element.android.features.location.impl.share
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
+5
-6
@@ -22,7 +22,7 @@ import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
||||
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.api.ShowLocationMode
|
||||
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
|
||||
@@ -47,7 +47,6 @@ import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.getBestName
|
||||
import io.element.android.libraries.matrix.api.room.joinedRoomMembers
|
||||
@@ -58,18 +57,18 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AssistedInject class ShowLocationPresenter(
|
||||
@AssistedInject
|
||||
class ShowLocationPresenter(
|
||||
@Assisted private val mode: ShowLocationMode,
|
||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||
private val locationActions: LocationActions,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val dateFormatter: DateFormatter,
|
||||
private val stringProvider: StringProvider,
|
||||
private val sessionId: SessionId,
|
||||
private val joinedRoom: JoinedRoom,
|
||||
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
||||
private val userLocationStateFactory: UserLocationState.Factory,
|
||||
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider,
|
||||
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
|
||||
) : Presenter<ShowLocationState> {
|
||||
@AssistedFactory fun interface Factory {
|
||||
fun create(mode: ShowLocationMode): ShowLocationPresenter
|
||||
@@ -89,7 +88,7 @@ import kotlinx.coroutines.launch
|
||||
|
||||
val customMapTilerConfig by produceState(AsyncData.Loading()) {
|
||||
// Ignore errors
|
||||
value = AsyncData.Success(remoteEnterpriseConfigProvider.get(sessionId).dataOrNull()?.mapTilerConfig)
|
||||
value = AsyncData.Success(customMapTilerConfigProvider.get().getOrNull())
|
||||
}
|
||||
|
||||
fun checkLocationConstraints() {
|
||||
|
||||
+1
-1
@@ -8,6 +8,7 @@
|
||||
|
||||
package io.element.android.features.location.impl.show
|
||||
|
||||
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||
import io.element.android.features.location.impl.common.ui.LocationMarkerData
|
||||
@@ -17,7 +18,6 @@ import io.element.android.libraries.designsystem.components.PinVariant
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class ShowLocationState(
|
||||
|
||||
+1
-1
@@ -9,6 +9,7 @@
|
||||
package io.element.android.features.location.impl.show
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||
@@ -18,7 +19,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
||||
|
||||
+1
-2
@@ -11,7 +11,6 @@ package io.element.android.features.location.impl.share
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.enterprise.test.remoteconfig.FakeRemoteEnterpriseConfigProvider
|
||||
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
|
||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||
@@ -59,7 +58,7 @@ class DefaultShareLocationEntryPointTest {
|
||||
sessionId = room.sessionId,
|
||||
),
|
||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||
remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
|
||||
customMapTilerConfigProvider = { Result.success(null) },
|
||||
)
|
||||
},
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
|
||||
+5
-10
@@ -19,8 +19,8 @@ import app.cash.turbine.ReceiveTurbine
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
||||
import io.element.android.features.enterprise.test.remoteconfig.FakeRemoteEnterpriseConfigProvider
|
||||
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||
import io.element.android.features.enterprise.test.remoteconfig.aMapTilerConfig
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.impl.aPermissionsState
|
||||
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
|
||||
@@ -32,8 +32,6 @@ import io.element.android.features.location.impl.common.ui.LocationConstraintsDi
|
||||
import io.element.android.features.location.impl.live.LiveLocationStore
|
||||
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
|
||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||
import io.element.android.features.wellknown.test.aMapTilerConfig
|
||||
import io.element.android.features.wellknown.test.anElementWellKnown
|
||||
import io.element.android.libraries.dateformatter.test.FakeDurationFormatter
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
@@ -55,7 +53,6 @@ import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermiss
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
|
||||
import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
@@ -92,7 +89,7 @@ class ShareLocationPresenterTest {
|
||||
liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(),
|
||||
liveLocationStore: LiveLocationStore = createLiveLocationStore(sessionId = joinedRoom.sessionId),
|
||||
client: FakeMatrixClient = fakeMatrixClient,
|
||||
remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
|
||||
customMapTilerConfigProvider: () -> Result<MapTilerConfig?> = { Result.success(null) },
|
||||
): ShareLocationPresenter = ShareLocationPresenter(
|
||||
permissionsPresenterFactory = { fakePermissionsPresenter },
|
||||
room = joinedRoom,
|
||||
@@ -106,7 +103,7 @@ class ShareLocationPresenterTest {
|
||||
liveLocationShareManager = liveLocationShareManager,
|
||||
liveLocationStore = liveLocationStore,
|
||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||
remoteEnterpriseConfigProvider = remoteEnterpriseConfigProvider,
|
||||
customMapTilerConfigProvider = customMapTilerConfigProvider,
|
||||
)
|
||||
|
||||
@Test
|
||||
@@ -134,9 +131,7 @@ class ShareLocationPresenterTest {
|
||||
client = FakeMatrixClient(
|
||||
sessionId = A_USER_ID,
|
||||
),
|
||||
remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(
|
||||
getResult = { WellknownRetrieverResult.Success(anElementWellKnown(mapTilerConfig = mapTilerConfig)) }
|
||||
)
|
||||
customMapTilerConfigProvider = { Result.success(mapTilerConfig) }
|
||||
)
|
||||
shareLocationPresenter.test {
|
||||
val state = awaitItem()
|
||||
|
||||
+1
-4
@@ -11,7 +11,6 @@ package io.element.android.features.location.impl.show
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.enterprise.test.remoteconfig.FakeRemoteEnterpriseConfigProvider
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.api.ShowLocationEntryPoint
|
||||
import io.element.android.features.location.api.ShowLocationMode
|
||||
@@ -21,7 +20,6 @@ import io.element.android.features.location.impl.common.permissions.FakePermissi
|
||||
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
@@ -51,10 +49,9 @@ class DefaultShowLocationEntryPointTest {
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
stringProvider = FakeStringProvider(),
|
||||
joinedRoom = joinedRoom,
|
||||
sessionId = A_SESSION_ID,
|
||||
liveLocationShareManager = FakeActiveLiveLocationShareManager(),
|
||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||
remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
|
||||
customMapTilerConfigProvider = { Result.success(null) },
|
||||
)
|
||||
},
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
|
||||
+6
-22
@@ -12,7 +12,8 @@ import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.enterprise.test.remoteconfig.FakeRemoteEnterpriseConfigProvider
|
||||
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
|
||||
import io.element.android.features.enterprise.test.remoteconfig.aMapTilerConfig
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.api.ShowLocationMode
|
||||
import io.element.android.features.location.impl.aPermissionsState
|
||||
@@ -23,20 +24,15 @@ import io.element.android.features.location.impl.common.permissions.PermissionsE
|
||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
|
||||
import io.element.android.features.wellknown.test.aMapTilerConfig
|
||||
import io.element.android.features.wellknown.test.anElementWellKnown
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.room.location.aLiveLocationShare
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.test
|
||||
@@ -69,9 +65,8 @@ class ShowLocationPresenterTest {
|
||||
),
|
||||
locationActions: FakeLocationActions = fakeLocationActions,
|
||||
joinedRoom: JoinedRoom = FakeJoinedRoom(),
|
||||
sessionId: SessionId = A_SESSION_ID,
|
||||
liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(),
|
||||
remoteEnterpriseConfigProvider: FakeRemoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
|
||||
customMapTilerConfigProvider: CustomMapTilerConfigProvider = { Result.success(null) },
|
||||
) = ShowLocationPresenter(
|
||||
mode = mode,
|
||||
permissionsPresenterFactory = { fakePermissionsPresenter },
|
||||
@@ -80,21 +75,15 @@ class ShowLocationPresenterTest {
|
||||
dateFormatter = fakeDateFormatter,
|
||||
stringProvider = FakeStringProvider(),
|
||||
joinedRoom = joinedRoom,
|
||||
sessionId = sessionId,
|
||||
liveLocationShareManager = liveLocationShareManager,
|
||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||
remoteEnterpriseConfigProvider = remoteEnterpriseConfigProvider,
|
||||
customMapTilerConfigProvider = customMapTilerConfigProvider,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `present - non-null customMapStyleUrl`() = runTest {
|
||||
val mapTilerConfig = aMapTilerConfig(apiKey = "A KEY")
|
||||
val shareLocationPresenter = createShowLocationPresenter(
|
||||
sessionId = A_SESSION_ID,
|
||||
remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(
|
||||
getResult = { WellknownRetrieverResult.Success(anElementWellKnown(mapTilerConfig = mapTilerConfig)) }
|
||||
)
|
||||
)
|
||||
val shareLocationPresenter = createShowLocationPresenter(customMapTilerConfigProvider = { Result.success(mapTilerConfig) })
|
||||
shareLocationPresenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.customMapTilerConfig.isLoading()).isTrue()
|
||||
@@ -131,12 +120,7 @@ class ShowLocationPresenterTest {
|
||||
fun `centers on user location`() = runTest {
|
||||
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||
|
||||
val presenter = createShowLocationPresenter(
|
||||
sessionId = A_SESSION_ID,
|
||||
remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(
|
||||
getResult = { WellknownRetrieverResult.Success(anElementWellKnown(mapTilerConfig = aMapTilerConfig())) }
|
||||
)
|
||||
)
|
||||
val presenter = createShowLocationPresenter(customMapTilerConfigProvider = { Result.success(aMapTilerConfig()) })
|
||||
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
|
||||
+9
-22
@@ -11,17 +11,14 @@ package io.element.android.features.login.impl.accesscontrol
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.features.enterprise.api.IsEnterpriseBuild
|
||||
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
|
||||
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
||||
import io.element.android.libraries.matrix.api.TemporaryMatrixClientFactory
|
||||
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultAccountProviderAccessControl(
|
||||
private val isEnterpriseBuild: IsEnterpriseBuild,
|
||||
private val enterpriseService: EnterpriseService,
|
||||
private val wellknownRetrieverFactory: WellknownRetriever.Factory,
|
||||
private val temporaryMatrixClientFactory: TemporaryMatrixClientFactory,
|
||||
) : AccountProviderAccessControl {
|
||||
override suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String) = try {
|
||||
assertIsAllowedToConnectToAccountProvider(
|
||||
@@ -38,29 +35,19 @@ class DefaultAccountProviderAccessControl(
|
||||
title: String,
|
||||
accountProviderUrl: String,
|
||||
) {
|
||||
if (enterpriseService.isEnterpriseBuild.not()) {
|
||||
if (isEnterpriseBuild().not()) {
|
||||
// Ensure that Element Pro is not required for this account provider
|
||||
val temporaryMatrixClient = temporaryMatrixClientFactory.create(accountProviderUrl).getOrThrow()
|
||||
temporaryMatrixClient.use {
|
||||
val wellknownRetriever = wellknownRetrieverFactory.create(temporaryMatrixClient)
|
||||
val wellKnown = wellknownRetriever.getElementWellKnown(
|
||||
host = accountProviderUrl,
|
||||
// It's expected this is hardcoded to the well-known endpoint
|
||||
source = EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT,
|
||||
).dataOrNull()
|
||||
|
||||
if (wellKnown?.enforceElementPro == true) {
|
||||
throw AccountProviderAccessException.NeedElementProException(
|
||||
unauthorisedAccountProviderTitle = title,
|
||||
applicationId = ELEMENT_PRO_APPLICATION_ID,
|
||||
)
|
||||
}
|
||||
if (enterpriseService.isElementProEnforced(accountProviderUrl)) {
|
||||
throw AccountProviderAccessException.NeedElementProException(
|
||||
unauthorisedAccountProviderTitle = title,
|
||||
applicationId = ELEMENT_PRO_APPLICATION_ID,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (enterpriseService.isAllowedToConnectToHomeserver(accountProviderUrl).not()) {
|
||||
throw AccountProviderAccessException.UnauthorizedAccountProviderException(
|
||||
unauthorisedAccountProviderTitle = title,
|
||||
authorisedAccountProviderTitles = enterpriseService.defaultHomeserverList(),
|
||||
authorisedAccountProviderTitles = enterpriseService.homeserverAllowList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ class AccountProviderDataSource(
|
||||
enterpriseService: EnterpriseService,
|
||||
) {
|
||||
private val defaultAccountProvider = createAccountProvider(
|
||||
url = enterpriseService.defaultHomeserverList()
|
||||
url = enterpriseService.homeserverAllowList()
|
||||
.firstOrNull { it != EnterpriseService.ANY_ACCOUNT_PROVIDER }
|
||||
?: AuthenticationConfig.MATRIX_ORG_URL
|
||||
)
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ class ChangeAccountProviderPresenter(
|
||||
@Composable
|
||||
override fun present(): ChangeAccountProviderState {
|
||||
val staticAccountProviderList = remember {
|
||||
enterpriseService.defaultHomeserverList()
|
||||
enterpriseService.homeserverAllowList()
|
||||
.filter { it != EnterpriseService.ANY_ACCOUNT_PROVIDER }
|
||||
.map { it.ensureProtocol() }
|
||||
.ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ class ChooseAccountProviderPresenter(
|
||||
|
||||
val staticAccountProviderList = remember {
|
||||
// The list cannot contains ANY_ACCOUNT_PROVIDER ("*") and cannot be empty at this point
|
||||
enterpriseService.defaultHomeserverList()
|
||||
enterpriseService.homeserverAllowList()
|
||||
.map { it.ensureProtocol() }
|
||||
.map { url ->
|
||||
AccountProvider(
|
||||
|
||||
+2
-2
@@ -62,13 +62,13 @@ class OnBoardingPresenter(
|
||||
val forcedAccountProvider = remember {
|
||||
// If defaultHomeserverList() returns a singleton list, this is the default account provider.
|
||||
// In this case, the user can sign in using this homeserver, or use QrCode login
|
||||
enterpriseService.defaultHomeserverList().singleOrNull()
|
||||
enterpriseService.homeserverAllowList().singleOrNull()
|
||||
}
|
||||
val canConnectToAnyHomeserver = remember {
|
||||
enterpriseService.canConnectToAnyHomeserver()
|
||||
}
|
||||
val mustChooseAccountProvider = remember {
|
||||
!canConnectToAnyHomeserver && enterpriseService.defaultHomeserverList().size > 1
|
||||
!canConnectToAnyHomeserver && enterpriseService.homeserverAllowList().size > 1
|
||||
}
|
||||
val linkAccountProvider by produceState<String?>(initialValue = null) {
|
||||
// Account provider from the link, if allowed by the enterprise service
|
||||
|
||||
+30
-46
@@ -11,15 +11,9 @@ package io.element.android.features.login.impl.accesscontrol
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||
import io.element.android.features.wellknown.test.FakeWellknownRetrieverFactory
|
||||
import io.element.android.features.wellknown.test.anElementWellKnown
|
||||
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER
|
||||
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
|
||||
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_URL
|
||||
import io.element.android.libraries.matrix.test.FakeTemporaryMatrixClientFactory
|
||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
@@ -30,9 +24,7 @@ class DefaultAccountProviderAccessControlTest {
|
||||
val accessControl = createDefaultAccountProviderAccessControl(
|
||||
isEnterpriseBuild = false,
|
||||
isAllowedToConnectToHomeserver = true,
|
||||
elementWellKnown = anElementWellKnown(
|
||||
enforceElementPro = true,
|
||||
),
|
||||
enterpriseService = FakeEnterpriseService(isElementProEnforcedResult = { true }),
|
||||
)
|
||||
accessControl.expectNeedElementProException()
|
||||
}
|
||||
@@ -43,9 +35,7 @@ class DefaultAccountProviderAccessControlTest {
|
||||
isEnterpriseBuild = false,
|
||||
// false here.
|
||||
isAllowedToConnectToHomeserver = false,
|
||||
elementWellKnown = anElementWellKnown(
|
||||
enforceElementPro = true,
|
||||
),
|
||||
enterpriseService = FakeEnterpriseService(isElementProEnforcedResult = { true }),
|
||||
)
|
||||
accessControl.expectNeedElementProException()
|
||||
}
|
||||
@@ -55,8 +45,9 @@ class DefaultAccountProviderAccessControlTest {
|
||||
val accessControl = createDefaultAccountProviderAccessControl(
|
||||
isEnterpriseBuild = false,
|
||||
isAllowedToConnectToHomeserver = true,
|
||||
elementWellKnown = anElementWellKnown(
|
||||
enforceElementPro = false,
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
isAllowedToConnectToHomeserverResult = { true },
|
||||
isElementProEnforcedResult = { false }
|
||||
),
|
||||
)
|
||||
accessControl.expectAllowed()
|
||||
@@ -67,19 +58,15 @@ class DefaultAccountProviderAccessControlTest {
|
||||
val accessControl = createDefaultAccountProviderAccessControl(
|
||||
isEnterpriseBuild = false,
|
||||
isAllowedToConnectToHomeserver = true,
|
||||
elementWellKnown = anElementWellKnown(
|
||||
enforceElementPro = null,
|
||||
),
|
||||
)
|
||||
accessControl.expectAllowed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `foss build should allow using account provider twith missing wellknown`() = runTest {
|
||||
fun `foss build should allow using account provider with missing wellknown`() = runTest {
|
||||
val accessControl = createDefaultAccountProviderAccessControl(
|
||||
isEnterpriseBuild = false,
|
||||
isAllowedToConnectToHomeserver = true,
|
||||
elementWellKnown = null,
|
||||
)
|
||||
accessControl.expectAllowed()
|
||||
}
|
||||
@@ -90,8 +77,10 @@ class DefaultAccountProviderAccessControlTest {
|
||||
isEnterpriseBuild = false,
|
||||
isAllowedToConnectToHomeserver = false,
|
||||
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
|
||||
elementWellKnown = anElementWellKnown(
|
||||
enforceElementPro = false,
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
isAllowedToConnectToHomeserverResult = { false },
|
||||
isElementProEnforcedResult = { false },
|
||||
defaultHomeserverListResult = { listOf(AN_ACCOUNT_PROVIDER_2) },
|
||||
),
|
||||
)
|
||||
accessControl.expectUnauthorizedAccountProviderException()
|
||||
@@ -102,8 +91,9 @@ class DefaultAccountProviderAccessControlTest {
|
||||
val accessControl = createDefaultAccountProviderAccessControl(
|
||||
isEnterpriseBuild = true,
|
||||
isAllowedToConnectToHomeserver = true,
|
||||
elementWellKnown = anElementWellKnown(
|
||||
enforceElementPro = true,
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
isAllowedToConnectToHomeserverResult = { true },
|
||||
isElementProEnforcedResult = { true },
|
||||
),
|
||||
)
|
||||
accessControl.expectAllowed()
|
||||
@@ -114,8 +104,9 @@ class DefaultAccountProviderAccessControlTest {
|
||||
val accessControl = createDefaultAccountProviderAccessControl(
|
||||
isEnterpriseBuild = true,
|
||||
isAllowedToConnectToHomeserver = true,
|
||||
elementWellKnown = anElementWellKnown(
|
||||
enforceElementPro = false,
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
isAllowedToConnectToHomeserverResult = { true },
|
||||
isElementProEnforcedResult = { false },
|
||||
),
|
||||
)
|
||||
accessControl.expectAllowed()
|
||||
@@ -127,8 +118,10 @@ class DefaultAccountProviderAccessControlTest {
|
||||
isEnterpriseBuild = true,
|
||||
isAllowedToConnectToHomeserver = false,
|
||||
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
|
||||
elementWellKnown = anElementWellKnown(
|
||||
enforceElementPro = true,
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
isAllowedToConnectToHomeserverResult = { false },
|
||||
isElementProEnforcedResult = { true },
|
||||
defaultHomeserverListResult = { listOf(AN_ACCOUNT_PROVIDER_2) },
|
||||
),
|
||||
)
|
||||
accessControl.expectUnauthorizedAccountProviderException()
|
||||
@@ -140,8 +133,10 @@ class DefaultAccountProviderAccessControlTest {
|
||||
isEnterpriseBuild = true,
|
||||
isAllowedToConnectToHomeserver = false,
|
||||
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
|
||||
elementWellKnown = anElementWellKnown(
|
||||
enforceElementPro = false,
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
isAllowedToConnectToHomeserverResult = { false },
|
||||
isElementProEnforcedResult = { false },
|
||||
defaultHomeserverListResult = { listOf(AN_ACCOUNT_PROVIDER_2) },
|
||||
),
|
||||
)
|
||||
accessControl.expectUnauthorizedAccountProviderException()
|
||||
@@ -151,25 +146,14 @@ class DefaultAccountProviderAccessControlTest {
|
||||
isEnterpriseBuild: Boolean = false,
|
||||
isAllowedToConnectToHomeserver: Boolean = false,
|
||||
allowedAccountProviders: List<String> = emptyList(),
|
||||
elementWellKnown: ElementWellKnown? = null,
|
||||
) = DefaultAccountProviderAccessControl(
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
isEnterpriseBuild = isEnterpriseBuild,
|
||||
enterpriseService: FakeEnterpriseService = FakeEnterpriseService(
|
||||
isAllowedToConnectToHomeserverResult = { isAllowedToConnectToHomeserver },
|
||||
defaultHomeserverListResult = { allowedAccountProviders },
|
||||
),
|
||||
wellknownRetrieverFactory = FakeWellknownRetrieverFactory(
|
||||
wellknownRetriever = FakeWellknownRetriever(
|
||||
getElementWellKnownResult = { _, _ ->
|
||||
if (elementWellKnown == null) {
|
||||
WellknownRetrieverResult.NotFound
|
||||
} else {
|
||||
WellknownRetrieverResult.Success(elementWellKnown)
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(),
|
||||
isElementProEnforcedResult = { false },
|
||||
)
|
||||
) = DefaultAccountProviderAccessControl(
|
||||
isEnterpriseBuild = { isEnterpriseBuild },
|
||||
enterpriseService = enterpriseService,
|
||||
)
|
||||
|
||||
private fun DefaultAccountProviderAccessControl.expectNeedElementProException() {
|
||||
|
||||
+15
-23
@@ -16,22 +16,15 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.error.ChangeServerError
|
||||
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||
import io.element.android.features.wellknown.test.FakeWellknownRetrieverFactory
|
||||
import io.element.android.features.wellknown.test.anElementWellKnown
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
import io.element.android.libraries.matrix.test.FakeTemporaryMatrixClientFactory
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
|
||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||
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 io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
@@ -64,6 +57,7 @@ class ChangeServerPresenterTest {
|
||||
authenticationService = authenticationService,
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
isAllowedToConnectToHomeserverResult = { true },
|
||||
isElementProEnforcedResult = { false },
|
||||
),
|
||||
).test {
|
||||
val initialState = awaitItem()
|
||||
@@ -86,6 +80,7 @@ class ChangeServerPresenterTest {
|
||||
createPresenter(
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
isAllowedToConnectToHomeserverResult = { true },
|
||||
isElementProEnforcedResult = { false },
|
||||
),
|
||||
authenticationService = authenticationService,
|
||||
).test {
|
||||
@@ -113,6 +108,7 @@ class ChangeServerPresenterTest {
|
||||
createPresenter(
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
isAllowedToConnectToHomeserverResult = { true },
|
||||
isElementProEnforcedResult = { false },
|
||||
),
|
||||
authenticationService = authenticationService,
|
||||
).test {
|
||||
@@ -136,6 +132,7 @@ class ChangeServerPresenterTest {
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
isAllowedToConnectToHomeserverResult = isAllowedToConnectToHomeserverResult,
|
||||
defaultHomeserverListResult = { listOf("element.io") },
|
||||
isElementProEnforcedResult = { false },
|
||||
),
|
||||
).test {
|
||||
val initialState = awaitItem()
|
||||
@@ -159,23 +156,19 @@ class ChangeServerPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `present - change server element pro required error`() = runTest {
|
||||
val getElementWellKnownResult = lambdaRecorder { homeserver: String, _: EnterpriseRemoteConfigSource ->
|
||||
assertThat(homeserver).isEqualTo(A_HOMESERVER_URL)
|
||||
WellknownRetrieverResult.Success(
|
||||
anElementWellKnown(
|
||||
enforceElementPro = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
val isEnterpriseBuildLambda = lambdaRecorder<Boolean> { false }
|
||||
createPresenter(
|
||||
wellknownRetriever = FakeWellknownRetriever(
|
||||
getElementWellKnownResult = getElementWellKnownResult,
|
||||
),
|
||||
isEnterpriseBuild = isEnterpriseBuildLambda,
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
isElementProEnforcedResult = { true },
|
||||
)
|
||||
).test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
||||
val anAccountProvider = AccountProvider(url = A_HOMESERVER_URL)
|
||||
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(anAccountProvider))
|
||||
// Skip loading state
|
||||
skipItems(1)
|
||||
val failureState = awaitItem()
|
||||
assertThat(
|
||||
(failureState.changeServerAction.errorOrNull() as ChangeServerError.NeedElementPro).unauthorisedAccountProviderTitle
|
||||
@@ -183,7 +176,7 @@ class ChangeServerPresenterTest {
|
||||
assertThat(
|
||||
(failureState.changeServerAction.errorOrNull() as ChangeServerError.NeedElementPro).applicationId
|
||||
).isEqualTo("io.element.enterprise")
|
||||
getElementWellKnownResult.assertions().isCalledOnce()
|
||||
isEnterpriseBuildLambda.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +230,7 @@ class ChangeServerPresenterTest {
|
||||
val permissionsPresenter = FakePermissionsPresenter()
|
||||
createPresenter(
|
||||
authenticationService = authenticationService,
|
||||
enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }),
|
||||
enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }, isElementProEnforcedResult = { false }),
|
||||
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(shouldPrompt = true),
|
||||
permissionsPresenter = permissionsPresenter,
|
||||
).test {
|
||||
@@ -259,16 +252,15 @@ class ChangeServerPresenterTest {
|
||||
authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
||||
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
|
||||
localNetworkPermissionAdvisor: FakeLocalNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||
permissionsPresenter: FakePermissionsPresenter = FakePermissionsPresenter(),
|
||||
isEnterpriseBuild: () -> Boolean = { false },
|
||||
) = ChangeServerPresenter(
|
||||
authenticationService = authenticationService,
|
||||
accountProviderDataSource = accountProviderDataSource,
|
||||
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
||||
enterpriseService = enterpriseService,
|
||||
wellknownRetrieverFactory = FakeWellknownRetrieverFactory(wellknownRetriever = wellknownRetriever),
|
||||
temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(),
|
||||
isEnterpriseBuild = isEnterpriseBuild,
|
||||
),
|
||||
localNetworkPermissionGate = LocalNetworkPermissionGate(
|
||||
advisor = localNetworkPermissionAdvisor,
|
||||
|
||||
+5
-7
@@ -12,13 +12,12 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.appconfig.AuthenticationConfig
|
||||
import io.element.android.appconfig.OnBoardingConfig
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.features.enterprise.api.IsEnterpriseBuild
|
||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
||||
import io.element.android.features.login.impl.login.LoginModePresenter
|
||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||
import io.element.android.features.wellknown.test.FakeWellknownRetrieverFactory
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
@@ -29,7 +28,6 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
|
||||
import io.element.android.libraries.matrix.test.A_LOGIN_HINT
|
||||
import io.element.android.libraries.matrix.test.FakeTemporaryMatrixClientFactory
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||
@@ -41,7 +39,6 @@ import io.element.android.libraries.permissions.test.FakePermissionsPresenterFac
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -186,6 +183,7 @@ class OnBoardingPresenterTest {
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) },
|
||||
isAllowedToConnectToHomeserverResult = { true },
|
||||
isElementProEnforcedResult = { false },
|
||||
),
|
||||
)
|
||||
presenter.test {
|
||||
@@ -259,6 +257,7 @@ class OnBoardingPresenterTest {
|
||||
),
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
isAllowedToConnectToHomeserverResult = { true },
|
||||
isElementProEnforcedResult = { false },
|
||||
),
|
||||
loginModePresenter = createLoginModePresenter(
|
||||
authenticationService = authenticationService,
|
||||
@@ -295,7 +294,7 @@ private fun createPresenter(
|
||||
),
|
||||
buildMeta: BuildMeta = aBuildMeta(),
|
||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
|
||||
isEnterpriseBuild: IsEnterpriseBuild = { false },
|
||||
rageshakeFeatureAvailability: () -> Flow<Boolean> = { flowOf(true) },
|
||||
loginModePresenter: LoginModePresenter = createLoginModePresenter(),
|
||||
onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null },
|
||||
@@ -307,8 +306,7 @@ private fun createPresenter(
|
||||
enterpriseService = enterpriseService,
|
||||
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
||||
enterpriseService = enterpriseService,
|
||||
wellknownRetrieverFactory = FakeWellknownRetrieverFactory(wellknownRetriever = wellknownRetriever),
|
||||
temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(),
|
||||
isEnterpriseBuild = isEnterpriseBuild,
|
||||
),
|
||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||
loginModePresenter = loginModePresenter,
|
||||
|
||||
+3
-1
@@ -30,6 +30,7 @@ import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.core.data.ByteUnit
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
@@ -50,6 +51,7 @@ class DeveloperSettingsPresenter(
|
||||
private val databaseSizesUseCase: GetDatabaseSizesUseCase,
|
||||
private val fileSizeFormatter: FileSizeFormatter,
|
||||
private val markAllRoomsAsRead: MarkAllRoomsAsRead,
|
||||
private val buildMeta: BuildMeta,
|
||||
) : Presenter<DeveloperSettingsState> {
|
||||
@Composable
|
||||
override fun present(): DeveloperSettingsState {
|
||||
@@ -117,7 +119,7 @@ class DeveloperSettingsPresenter(
|
||||
databaseSizes = databaseSizes.value,
|
||||
clearCacheAction = clearCacheAction.value,
|
||||
markAllRoomsAsReadAction = markAllRoomsAsReadAction.value,
|
||||
isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
|
||||
isEnterpriseBuild = buildMeta.isEnterpriseBuild,
|
||||
showColorPicker = showColorPicker,
|
||||
deviceId = deviceId,
|
||||
eventSink = ::handleEvent,
|
||||
|
||||
+6
-4
@@ -23,12 +23,14 @@ import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.core.data.megaBytes
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase
|
||||
import io.element.android.libraries.matrix.api.analytics.SdkStoreSizes
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.A_DEVICE_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
@@ -108,10 +110,8 @@ class DeveloperSettingsPresenterTest {
|
||||
fun `present - enterprise build can change the brand color`() = runTest {
|
||||
val overrideBrandColorResult = lambdaRecorder<SessionId?, String?, Unit> { _, _ -> }
|
||||
val presenter = createDeveloperSettingsPresenter(
|
||||
enterpriseService = FakeEnterpriseService(
|
||||
isEnterpriseBuild = true,
|
||||
overrideBrandColorResult = overrideBrandColorResult,
|
||||
)
|
||||
enterpriseService = FakeEnterpriseService(overrideBrandColorResult = overrideBrandColorResult),
|
||||
buildMeta = aBuildMeta(isEnterpriseBuild = true),
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
@@ -179,6 +179,7 @@ class DeveloperSettingsPresenterTest {
|
||||
vacuumStoresUseCase: VacuumStoresUseCase = VacuumStoresUseCase {},
|
||||
databaseSizesUseCase: GetDatabaseSizesUseCase = GetDatabaseSizesUseCase { Result.success(SdkStoreSizes(null, null, null, null)) },
|
||||
markAllRoomsAsRead: FakeMarkAllRoomsAsRead = FakeMarkAllRoomsAsRead(),
|
||||
buildMeta: BuildMeta = aBuildMeta(),
|
||||
): DeveloperSettingsPresenter {
|
||||
return DeveloperSettingsPresenter(
|
||||
appDeveloperSettingsPresenter = { anAppDeveloperSettingsState() },
|
||||
@@ -191,6 +192,7 @@ class DeveloperSettingsPresenterTest {
|
||||
databaseSizesUseCase = databaseSizesUseCase,
|
||||
fileSizeFormatter = FakeFileSizeFormatter(),
|
||||
markAllRoomsAsRead = markAllRoomsAsRead,
|
||||
buildMeta = buildMeta,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
-23
@@ -1,23 +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
|
||||
|
||||
/**
|
||||
* Server-driven settings for a user-chosen recovery passphrase, advertised under the well-known
|
||||
* `custom_recovery_passphrase` key. Today the only rule is a minimum character count; additional
|
||||
* settings can be added here as the schema grows.
|
||||
*/
|
||||
data class CustomRecoveryPassphrase(
|
||||
val minCharacterCount: Int,
|
||||
) {
|
||||
/** True when [input] meets every active rule. */
|
||||
fun isSatisfiedBy(input: String): Boolean = isSatisfiedBy(input.length)
|
||||
|
||||
/** True when an input of [length] characters meets every active rule. */
|
||||
fun isSatisfiedBy(length: Int): Boolean = length >= minCharacterCount
|
||||
}
|
||||
-20
@@ -1,20 +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
|
||||
|
||||
/**
|
||||
* Parses the raw JSON of an Element `.well-known` document.
|
||||
*/
|
||||
fun interface ElementWellKnownParser {
|
||||
/**
|
||||
* Parses the document, failing when it is not valid JSON; unknown fields are ignored so a newer server does not break an older client.
|
||||
*
|
||||
* @param json the raw document as served by the server.
|
||||
*/
|
||||
fun parse(json: String): Result<ElementWellKnown>
|
||||
}
|
||||
-47
@@ -1,47 +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
|
||||
|
||||
/**
|
||||
* On-disk cache of the Element `.well-known` documents already fetched, so the app has a configuration to work with while offline.
|
||||
*/
|
||||
interface ElementWellknownStore {
|
||||
/**
|
||||
* Reads the cached document of a domain.
|
||||
* Entries older than a day are reported as outdated rather than missing, so the caller can use them while refreshing;
|
||||
* a document that no longer parses is reported as an error.
|
||||
*
|
||||
* @param domain the server whose cached document is requested.
|
||||
*/
|
||||
suspend fun get(domain: String): WellknownRetrieverResult<ElementWellKnown>
|
||||
|
||||
/**
|
||||
* Stores the document of a domain and stamps it as fetched now, replacing any previous entry.
|
||||
*
|
||||
* @param domain the server the document belongs to.
|
||||
* @param wellknown the raw document, stored as-is rather than parsed.
|
||||
*/
|
||||
suspend fun update(domain: String, wellknown: String): Result<Unit>
|
||||
|
||||
/**
|
||||
* Removes the cached document of a domain, used when the stored value turns out to be unusable.
|
||||
*
|
||||
* @param domain the server whose entry is removed.
|
||||
*/
|
||||
suspend fun delete(domain: String): Result<Unit>
|
||||
|
||||
/**
|
||||
* Creates a store, one per configuration source so that their entries never collide.
|
||||
*/
|
||||
fun interface Factory {
|
||||
/**
|
||||
* @param prefix namespaces the cache keys, or `null` for the default well-known endpoint.
|
||||
*/
|
||||
fun create(prefix: String?): ElementWellknownStore
|
||||
}
|
||||
}
|
||||
-23
@@ -1,23 +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
|
||||
|
||||
/**
|
||||
* The source from which the enterprise remote configuration is retrieved.
|
||||
*/
|
||||
enum class EnterpriseRemoteConfigSource {
|
||||
/**
|
||||
* Retrieve the enterprise remote configuration from the Element Server Suite configuration (ESS_CONFIG) endpoint.
|
||||
*/
|
||||
ESS_CONFIG,
|
||||
|
||||
/**
|
||||
* Retrieve the enterprise remote configuration from the Element server's well-known endpoint.
|
||||
*/
|
||||
WELLKNOWN_ENDPOINT,
|
||||
}
|
||||
-39
@@ -1,39 +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.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||
|
||||
/**
|
||||
* Retrieves the Element specific `.well-known` configuration of a server, which is how a deployment advertises its own settings.
|
||||
*/
|
||||
interface WellknownRetriever {
|
||||
/**
|
||||
* Returns the configuration of a host, served from the local cache when possible.
|
||||
* A cached value that has gone stale is returned as outdated while a refresh runs in the background, so this rarely waits on the network.
|
||||
* An enterprise build with a hardcoded configuration always short-circuits and returns that instead.
|
||||
*
|
||||
* @param host the server to look up; a full URL is accepted and reduced to its host.
|
||||
* @param source which endpoint to read the configuration from, which also selects the cache it is stored in.
|
||||
*/
|
||||
suspend fun getElementWellKnown(
|
||||
host: String,
|
||||
source: EnterpriseRemoteConfigSource
|
||||
): WellknownRetrieverResult<ElementWellKnown>
|
||||
|
||||
/**
|
||||
* Creates a retriever, which needs a fetcher because the configuration is read over HTTP.
|
||||
*/
|
||||
interface Factory {
|
||||
/**
|
||||
* @param urlContentFetcher used to perform the request; a client that is not authenticated yet also works.
|
||||
*/
|
||||
fun create(urlContentFetcher: UrlContentFetcher): WellknownRetriever
|
||||
}
|
||||
}
|
||||
-27
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
-77
@@ -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
|
||||
}
|
||||
}
|
||||
-142
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
-71
@@ -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,
|
||||
)
|
||||
-34
@@ -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,
|
||||
)
|
||||
-27
@@ -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
|
||||
)
|
||||
-59
@@ -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)
|
||||
}
|
||||
-41
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
-19
@@ -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
|
||||
}
|
||||
-33
@@ -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()
|
||||
}
|
||||
}
|
||||
-174
@@ -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,
|
||||
)
|
||||
}
|
||||
-241
@@ -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()
|
||||
}
|
||||
-53
@@ -1,53 +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.features.wellknown.test
|
||||
|
||||
import io.element.android.libraries.androidutils.json.DefaultJsonProvider
|
||||
import io.element.android.libraries.androidutils.json.JsonProvider
|
||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||
import io.element.android.libraries.wellknown.api.ElementWellknownStore
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||
import io.element.android.libraries.wellknown.impl.InternalElementWellKnown
|
||||
import io.element.android.libraries.wellknown.impl.map
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.JsonDecodingException
|
||||
import java.net.URL
|
||||
|
||||
class FakeElementWellknownStore(
|
||||
initialData: Map<String, WellknownRetrieverResult<ElementWellKnown>> = emptyMap(),
|
||||
private val jsonProvider: JsonProvider = DefaultJsonProvider(),
|
||||
) : ElementWellknownStore {
|
||||
private val cachedData = mutableMapOf<String, WellknownRetrieverResult<ElementWellKnown>>()
|
||||
|
||||
init {
|
||||
initialData.forEach { (url, result) ->
|
||||
cachedData[URL(url).host] = result
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun get(domain: String): WellknownRetrieverResult<ElementWellKnown> {
|
||||
return cachedData[domain] ?: WellknownRetrieverResult.NotFound
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
override suspend fun update(domain: String, wellknown: String): Result<Unit> {
|
||||
val json = jsonProvider()
|
||||
val mapped = try {
|
||||
json.decodeFromString<InternalElementWellKnown>(wellknown).map()
|
||||
} catch (e: JsonDecodingException) {
|
||||
return Result.failure(e)
|
||||
}
|
||||
cachedData[domain] = WellknownRetrieverResult.Success(mapped)
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun delete(domain: String): Result<Unit> {
|
||||
cachedData.remove(domain)
|
||||
return Result.success(Unit)
|
||||
}
|
||||
}
|
||||
-24
@@ -1,24 +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.features.wellknown.test
|
||||
|
||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||
|
||||
class FakeWellknownRetriever(
|
||||
private val getElementWellKnownResult: (String, EnterpriseRemoteConfigSource) -> WellknownRetrieverResult<ElementWellKnown> = { _, _ ->
|
||||
WellknownRetrieverResult.NotFound
|
||||
},
|
||||
) : WellknownRetriever {
|
||||
override suspend fun getElementWellKnown(host: String, source: EnterpriseRemoteConfigSource): WellknownRetrieverResult<ElementWellKnown> {
|
||||
return getElementWellKnownResult(host, source)
|
||||
}
|
||||
}
|
||||
-17
@@ -1,17 +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.features.wellknown.test
|
||||
|
||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||
|
||||
class FakeWellknownRetrieverFactory(
|
||||
private val wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
|
||||
) : WellknownRetriever.Factory {
|
||||
override fun create(urlContentFetcher: UrlContentFetcher): WellknownRetriever = wellknownRetriever
|
||||
}
|
||||
-55
@@ -1,55 +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.features.wellknown.test
|
||||
|
||||
import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase
|
||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
||||
|
||||
fun anElementWellKnown(
|
||||
registrationHelperUrl: String? = null,
|
||||
enforceElementPro: Boolean? = null,
|
||||
rageshakeUrl: String? = null,
|
||||
brandColor: String? = null,
|
||||
notificationSound: String? = null,
|
||||
identityProviderAppScheme: String? = null,
|
||||
customRecoveryPassphrase: CustomRecoveryPassphrase? = null,
|
||||
contentScannerUrl: String? = null,
|
||||
forceDisableE2EE: Boolean? = null,
|
||||
mapTilerConfig: MapTilerConfig? = null,
|
||||
) = ElementWellKnown(
|
||||
registrationHelperUrl = registrationHelperUrl,
|
||||
enforceElementPro = enforceElementPro,
|
||||
rageshakeUrl = rageshakeUrl,
|
||||
brandColor = brandColor,
|
||||
notificationSound = notificationSound,
|
||||
identityProviderAppScheme = identityProviderAppScheme,
|
||||
customRecoveryPassphrase = customRecoveryPassphrase,
|
||||
contentScannerUrl = contentScannerUrl,
|
||||
forceDisableE2EE = forceDisableE2EE,
|
||||
mapTilerConfig = mapTilerConfig,
|
||||
)
|
||||
|
||||
fun aCustomRecoveryPassphrase(
|
||||
minCharacterCount: Int = 8,
|
||||
) = CustomRecoveryPassphrase(
|
||||
minCharacterCount = minCharacterCount,
|
||||
)
|
||||
|
||||
fun aMapTilerConfig(
|
||||
apiKey: String = "test_api_key",
|
||||
baseUrl: String? = null,
|
||||
lightStyleId: String? = null,
|
||||
darkStyleId: String? = null,
|
||||
) = MapTilerConfig(
|
||||
apiKey = apiKey,
|
||||
baseUrl = baseUrl,
|
||||
lightStyleId = lightStyleId,
|
||||
darkStyleId = darkStyleId,
|
||||
)
|
||||
+1
-3
@@ -9,7 +9,6 @@
|
||||
package io.element.android.services.analyticsproviders.posthog
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.libraries.core.extensions.isElement
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
@@ -17,10 +16,9 @@ import io.element.android.libraries.core.meta.BuildType
|
||||
@Inject
|
||||
class PosthogEndpointConfigProvider(
|
||||
private val buildMeta: BuildMeta,
|
||||
private val enterpriseService: EnterpriseService,
|
||||
) {
|
||||
fun provide(): PosthogEndpointConfig? {
|
||||
return if (enterpriseService.isEnterpriseBuild) {
|
||||
return if (buildMeta.isEnterpriseBuild) {
|
||||
PosthogEndpointConfig(
|
||||
host = BuildConfig.POSTHOG_HOST,
|
||||
apiKey = BuildConfig.POSTHOG_APIKEY,
|
||||
|
||||
Reference in New Issue
Block a user