diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/SessionWellknownRetriever.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/SessionWellknownRetriever.kt deleted file mode 100644 index b006508fc7..0000000000 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/SessionWellknownRetriever.kt +++ /dev/null @@ -1,13 +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 - -interface SessionWellknownRetriever { - suspend fun getElementWellKnown(): WellknownRetrieverResult -} diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt deleted file mode 100644 index 66654dd228..0000000000 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt +++ /dev/null @@ -1,109 +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.ContributesBinding -import io.element.android.features.enterprise.api.EnterpriseService -import io.element.android.libraries.core.extensions.mapCatchingExceptions -import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.di.annotations.SessionCoroutineScope -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.exception.ClientException -import io.element.android.libraries.wellknown.api.ElementWellKnown -import io.element.android.libraries.wellknown.api.ElementWellKnownParser -import io.element.android.libraries.wellknown.api.ElementWellknownStore -import io.element.android.libraries.wellknown.api.SessionWellknownRetriever -import io.element.android.libraries.wellknown.api.WellknownRetrieverResult -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import timber.log.Timber - -@ContributesBinding(SessionScope::class) -class DefaultSessionWellknownRetriever( - private val matrixClient: MatrixClient, - @SessionCoroutineScope - private val sessionCoroutineScope: CoroutineScope, - private val elementWellknownStore: ElementWellknownStore, - private val elementWellKnownParser: ElementWellKnownParser, - private val enterpriseService: EnterpriseService, -) : SessionWellknownRetriever { - private val domain by lazy { matrixClient.userIdServerName() } - - override suspend fun getElementWellKnown(): WellknownRetrieverResult { - val overriddenElementWellKnown = enterpriseService.overriddenElementWellKnown() - if (overriddenElementWellKnown != null) { - return WellknownRetrieverResult.Success(overriddenElementWellKnown) - } - - val cacheData = elementWellknownStore.get(domain) - return when (cacheData) { - is WellknownRetrieverResult.Success -> { - Timber.d("Using cached well-known for domain $domain") - 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 $domain, returning existing value and fetching new one from network") - sessionCoroutineScope.launch { - val url = "https://$domain/.well-known/element/element.json" - fetchElementWellKnown(url) - } - cacheData - } - is WellknownRetrieverResult.NotFound -> { - // Try to fetch from the server - Timber.d("No cached well-known for domain $domain, fetching from network") - val url = "https://$domain/.well-known/element/element.json" - fetchElementWellKnown(url) - } - is WellknownRetrieverResult.Error -> { - // Return the error - Timber.e(cacheData.exception, "Error retrieving well-known for domain $domain") - cacheData.exception.toWellknownRetrieverResult() - } - } - } - - private suspend fun fetchElementWellKnown(url: String): WellknownRetrieverResult { - return matrixClient - .getUrl(url) - .mapCatchingExceptions { - val data = String(it) - val parsed = elementWellKnownParser.parse(data).getOrThrow() - // Also store in cache, if valid - elementWellknownStore.update(domain, data) - .onFailure { exception -> - Timber.e(exception, "Failed to parse cached Element .well-known data for $domain, deleting cache") - elementWellknownStore.delete(domain) - } - parsed - } - .toWellknownRetrieverResult() - } - - private fun Result.toWellknownRetrieverResult(): WellknownRetrieverResult = fold( - onSuccess = { - WellknownRetrieverResult.Success(it) - }, - onFailure = { - Timber.e(it, "Failed to retrieve Element .well-known from $domain") - // This check on message value is not ideal but this is what we got from the SDK. - it.toWellknownRetrieverResult() - } - ) - - private fun Throwable.toWellknownRetrieverResult(): WellknownRetrieverResult { - return if ((this as? ClientException.Generic)?.message?.contains("404") == true) { - WellknownRetrieverResult.NotFound - } else { - WellknownRetrieverResult.Error(this as Exception) - } - } -} diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt deleted file mode 100644 index f54cf919d9..0000000000 --- a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt +++ /dev/null @@ -1,376 +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.anElementWellKnown -import io.element.android.libraries.androidutils.json.DefaultJsonProvider -import io.element.android.libraries.matrix.api.exception.ClientException -import io.element.android.libraries.matrix.test.AN_EXCEPTION -import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase -import io.element.android.libraries.wellknown.api.ElementWellKnown -import io.element.android.libraries.wellknown.api.ElementWellKnownParser -import io.element.android.libraries.wellknown.api.WellknownRetrieverResult -import io.element.android.tests.testutils.lambda.lambdaError -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 - -class DefaultSessionWellknownRetrieverTest { - @Test - fun `get empty element wellknown`() = runTest { - val getUrlLambda = lambdaRecorder> { - Result.success("{}".toByteArray()) - } - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = getUrlLambda, - ) - assertThat(sut.getElementWellKnown()).isEqualTo( - WellknownRetrieverResult.Success( - ElementWellKnown( - registrationHelperUrl = null, - enforceElementPro = null, - rageshakeUrl = null, - brandColor = null, - notificationSound = null, - identityProviderAppScheme = null, - customRecoveryPassphrase = null, - contentScannerUrl = null, - forceDisableE2EE = null, - ) - ) - ) - getUrlLambda.assertions().isCalledOnce() - .with(value("https://user.domain.org/.well-known/element/element.json")) - } - - @Test - fun `get element wellknown with full content`() = runTest { - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { - Result.success( - WELLKNOWN_CONTENT.toByteArray() - ) - } - ) - assertThat(sut.getElementWellKnown()).isEqualTo( - WellknownRetrieverResult.Success( - ElementWellKnown( - registrationHelperUrl = "a_registration_url", - enforceElementPro = true, - rageshakeUrl = "a_rageshake_url", - brandColor = "#FF0000", - notificationSound = "a_notification_sound.flac", - identityProviderAppScheme = "an_app_scheme", - customRecoveryPassphrase = null, - contentScannerUrl = "https://content-scanner.example.com", - forceDisableE2EE = false, - ) - ) - ) - } - - @Test - fun `get element wellknown with unknown key`() = runTest { - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { - Result.success( - """{ - "registration_helper_url": "a_registration_url", - "enforce_element_pro": true, - "rageshake_url": "a_rageshake_url", - // Note the trailing comma, and the comment! - "other": true, - }""".trimIndent().toByteArray() - ) - }, - ) - assertThat(sut.getElementWellKnown()).isEqualTo( - WellknownRetrieverResult.Success( - ElementWellKnown( - registrationHelperUrl = "a_registration_url", - enforceElementPro = true, - rageshakeUrl = "a_rageshake_url", - brandColor = null, - notificationSound = null, - identityProviderAppScheme = null, - contentScannerUrl = null, - customRecoveryPassphrase = null, - forceDisableE2EE = null, - ) - ) - ) - } - - @Test - fun `get element wellknown with custom recovery passphrase settings`() = runTest { - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { - Result.success( - """{ - "custom_recovery_passphrase": { - "min_character_count": 8 - } - }""".trimIndent().toByteArray() - ) - }, - ) - assertThat(sut.getElementWellKnown()).isEqualTo( - WellknownRetrieverResult.Success( - anElementWellKnown( - customRecoveryPassphrase = CustomRecoveryPassphrase(minCharacterCount = 8) - ) - ) - ) - } - - @Test - fun `get element wellknown with custom recovery passphrase settings missing min character count floors to 1`() = runTest { - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { - Result.success( - """{ - "custom_recovery_passphrase": {} - }""".trimIndent().toByteArray() - ) - }, - ) - assertThat(sut.getElementWellKnown()).isEqualTo( - WellknownRetrieverResult.Success( - anElementWellKnown( - customRecoveryPassphrase = CustomRecoveryPassphrase(minCharacterCount = 1) - ) - ) - ) - } - - @Test - fun `get element wellknown with zero min character count floors to 1`() = runTest { - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { - Result.success( - """{ - "custom_recovery_passphrase": { - "min_character_count": 0 - } - }""".trimIndent().toByteArray() - ) - }, - ) - assertThat(sut.getElementWellKnown()).isEqualTo( - WellknownRetrieverResult.Success( - anElementWellKnown( - customRecoveryPassphrase = CustomRecoveryPassphrase(minCharacterCount = 1) - ) - ) - ) - } - - @Test - fun `get element wellknown with negative min character count floors to 1`() = runTest { - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { - Result.success( - """{ - "custom_recovery_passphrase": { - "min_character_count": -5 - } - }""".trimIndent().toByteArray() - ) - }, - ) - assertThat(sut.getElementWellKnown()).isEqualTo( - WellknownRetrieverResult.Success( - anElementWellKnown( - customRecoveryPassphrase = CustomRecoveryPassphrase(minCharacterCount = 1) - ) - ) - ) - } - - @Test - fun `get element wellknown json error`() = runTest { - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { - Result.success( - """{ - "registration_helper_url" = "a_registration_url", - error - }""".trimIndent().toByteArray() - ) - } - ) - assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java) - } - - @Test - fun `get element wellknown network error`() = runTest { - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { - Result.failure(AN_EXCEPTION) - } - ) - assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java) - } - - @Test - fun `get element wellknown 404 http error counts as not found`() = runTest { - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { - Result.failure(ClientException.Generic("Not Found: 404 status code", "The file could not be found")) - } - ) - assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.NotFound::class.java) - } - - @Test - fun `get element wellknown hitting cache`() = runTest { - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { lambdaError() }, - cacheStore = FakeElementWellknownStore( - initialData = mapOf( - WELLKNOWN_URL to WellknownRetrieverResult.Success(parsedWellKnownContent()) - ) - ) - ) - assertThat(sut.getElementWellKnown()).isEqualTo( - WellknownRetrieverResult.Success( - ElementWellKnown( - registrationHelperUrl = "a_registration_url", - enforceElementPro = true, - rageshakeUrl = "a_rageshake_url", - brandColor = "#FF0000", - notificationSound = "a_notification_sound.flac", - identityProviderAppScheme = "an_app_scheme", - customRecoveryPassphrase = null, - contentScannerUrl = "https://content-scanner.example.com", - forceDisableE2EE = false, - ) - ) - ) - } - - @Test - fun `get element wellknown hitting cache containing invalid json`() = runTest { - val cacheStore = FakeElementWellknownStore( - initialData = mapOf( - WELLKNOWN_URL to WellknownRetrieverResult.Error(IllegalStateException("Invalid JSON")) - ) - ) - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { - Result.success("{}".toByteArray()) - }, - cacheStore = cacheStore, - elementWellKnownParser = { Result.failure(IllegalStateException("Failed to parse JSON")) } - ) - assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java) - // Ensure that the cache is deleted after the failure to parse it - assertThat(cacheStore.get(WELLKNOWN_URL)).isEqualTo(WellknownRetrieverResult.NotFound) - } - - @Test - fun `get element wellknown hitting outdated cache`() = runTest { - val cacheStore = FakeElementWellknownStore( - initialData = mapOf( - WELLKNOWN_URL to WellknownRetrieverResult.Outdated(parsedWellKnownContent()), - ) - ) - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { - Result.success("{}".toByteArray()) - }, - cacheStore = cacheStore, - ) - assertThat(sut.getElementWellKnown()).isEqualTo( - WellknownRetrieverResult.Outdated( - ElementWellKnown( - registrationHelperUrl = "a_registration_url", - enforceElementPro = true, - rageshakeUrl = "a_rageshake_url", - brandColor = "#FF0000", - notificationSound = "a_notification_sound.flac", - identityProviderAppScheme = "an_app_scheme", - customRecoveryPassphrase = null, - contentScannerUrl = "https://content-scanner.example.com", - forceDisableE2EE = false, - ) - ) - ) - // Next call returns the updated value - runCurrent() - assertThat(sut.getElementWellKnown()).isEqualTo( - WellknownRetrieverResult.Success( - anElementWellKnown() - ) - ) - } - - @Test - fun `get element wellknown was overridden`() = runTest { - val getLambda = lambdaRecorder> { Result.failure(IllegalStateException("BOOM")) } - val wellKnown = anElementWellKnown() - - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = getLambda, - enterpriseService = FakeEnterpriseService( - overrideWellKnownResult = { wellKnown } - ) - ) - - // The overridden value is returned - assertThat(sut.getElementWellKnown()).isEqualTo( - WellknownRetrieverResult.Success(wellKnown) - ) - - // And the endpoint is never hit - getLambda.assertions().isNeverCalled() - } - - private fun parsedWellKnownContent() = DefaultJsonProvider().invoke().decodeFromString(WELLKNOWN_CONTENT).map() - - private fun TestScope.createDefaultSessionWellknownRetriever( - cacheStore: FakeElementWellknownStore = FakeElementWellknownStore(), - getUrlLambda: (String) -> Result, - elementWellKnownParser: ElementWellKnownParser = DefaultElementWellKnownParser(DefaultJsonProvider()), - enterpriseService: FakeEnterpriseService = FakeEnterpriseService(overrideWellKnownResult = { null }), - ) = DefaultSessionWellknownRetriever( - matrixClient = FakeMatrixClient( - userIdServerNameLambda = { "user.domain.org" }, - getUrlLambda = getUrlLambda, - ), - sessionCoroutineScope = backgroundScope, - elementWellknownStore = cacheStore, - elementWellKnownParser = elementWellKnownParser, - enterpriseService = enterpriseService, - ) - - 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, - }""" - } -}