Add a cache for Element .well-known file.
This commit is contained in:
parent
c86532778c
commit
ca42feab87
3 changed files with 173 additions and 11 deletions
|
|
@ -33,9 +33,13 @@ dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.network)
|
implementation(projects.libraries.network)
|
||||||
|
implementation(projects.libraries.cachestore.api)
|
||||||
|
implementation(projects.services.toolbox.api)
|
||||||
|
|
||||||
testCommonDependencies(libs)
|
testCommonDependencies(libs)
|
||||||
testImplementation(libs.coroutines.core)
|
testImplementation(libs.coroutines.core)
|
||||||
|
testImplementation(projects.libraries.cachestore.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.libraries.wellknown.test)
|
||||||
testImplementation(projects.services.toolbox.test)
|
testImplementation(projects.services.toolbox.test)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,29 +10,70 @@ package io.element.android.libraries.wellknown.impl
|
||||||
|
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
import io.element.android.libraries.androidutils.json.JsonProvider
|
import io.element.android.libraries.androidutils.json.JsonProvider
|
||||||
|
import io.element.android.libraries.cachestore.api.CacheData
|
||||||
|
import io.element.android.libraries.cachestore.api.CacheStore
|
||||||
import io.element.android.libraries.core.extensions.mapCatchingExceptions
|
import io.element.android.libraries.core.extensions.mapCatchingExceptions
|
||||||
import io.element.android.libraries.di.SessionScope
|
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.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.exception.ClientException
|
import io.element.android.libraries.matrix.api.exception.ClientException
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import io.element.android.libraries.wellknown.api.SessionWellknownRetriever
|
import io.element.android.libraries.wellknown.api.SessionWellknownRetriever
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
class DefaultSessionWellknownRetriever(
|
class DefaultSessionWellknownRetriever(
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val json: JsonProvider,
|
private val json: JsonProvider,
|
||||||
|
private val cacheStore: CacheStore,
|
||||||
|
private val systemClock: SystemClock,
|
||||||
|
@SessionCoroutineScope
|
||||||
|
private val sessionCoroutineScope: CoroutineScope,
|
||||||
) : SessionWellknownRetriever {
|
) : SessionWellknownRetriever {
|
||||||
private val domain by lazy { matrixClient.userIdServerName() }
|
private val domain by lazy { matrixClient.userIdServerName() }
|
||||||
|
|
||||||
override suspend fun getElementWellKnown(): WellknownRetrieverResult<ElementWellKnown> {
|
override suspend fun getElementWellKnown(): WellknownRetrieverResult<ElementWellKnown> {
|
||||||
val url = "https://$domain/.well-known/element/element.json"
|
val url = "https://$domain/.well-known/element/element.json"
|
||||||
|
val cacheData = cacheStore.getData(url)
|
||||||
|
if (cacheData != null) {
|
||||||
|
Timber.d("Element .well-known data retrieved from cache for $domain")
|
||||||
|
// If the cache is outdated, trigger a refresh in background but still return the cached value
|
||||||
|
if (systemClock.epochMillis() > cacheData.updatedAt + CACHE_VALIDITY_MILLIS) {
|
||||||
|
sessionCoroutineScope.launch {
|
||||||
|
fetchElementWellKnown(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val parsed = json().decodeFromString<InternalElementWellKnown>(cacheData.value).map()
|
||||||
|
return WellknownRetrieverResult.Success(parsed)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Failed to parse cached Element .well-known data for $domain, deleting cache")
|
||||||
|
cacheStore.deleteData(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchElementWellKnown(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchElementWellKnown(url: String): WellknownRetrieverResult<ElementWellKnown> {
|
||||||
return matrixClient
|
return matrixClient
|
||||||
.getUrl(url)
|
.getUrl(url)
|
||||||
.mapCatchingExceptions {
|
.mapCatchingExceptions {
|
||||||
val data = String(it)
|
val data = String(it)
|
||||||
json().decodeFromString<InternalElementWellKnown>(data).map()
|
val parsed = json().decodeFromString<InternalElementWellKnown>(data).map()
|
||||||
|
// Also store in cache, if valid
|
||||||
|
cacheStore.storeData(
|
||||||
|
key = url,
|
||||||
|
data = CacheData(
|
||||||
|
value = data,
|
||||||
|
updatedAt = systemClock.epochMillis(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parsed
|
||||||
}
|
}
|
||||||
.toWellknownRetrieverResult()
|
.toWellknownRetrieverResult()
|
||||||
}
|
}
|
||||||
|
|
@ -51,4 +92,9 @@ class DefaultSessionWellknownRetriever(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// 1 day
|
||||||
|
private const val CACHE_VALIDITY_MILLIS = 1 * 24 * 60 * 60 * 1000L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,30 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
package io.element.android.libraries.wellknown.impl
|
package io.element.android.libraries.wellknown.impl
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.androidutils.json.DefaultJsonProvider
|
||||||
|
import io.element.android.libraries.androidutils.json.JsonProvider
|
||||||
|
import io.element.android.libraries.cachestore.api.CacheData
|
||||||
|
import io.element.android.libraries.cachestore.api.CacheStore
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.sessionstorage.test.InMemoryCacheStore
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
|
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
|
||||||
|
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
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 kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -49,14 +63,7 @@ class DefaultSessionWellknownRetrieverTest {
|
||||||
val sut = createDefaultSessionWellknownRetriever(
|
val sut = createDefaultSessionWellknownRetriever(
|
||||||
getUrlLambda = {
|
getUrlLambda = {
|
||||||
Result.success(
|
Result.success(
|
||||||
"""{
|
WELLKNOWN_CONTENT.toByteArray()
|
||||||
"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"
|
|
||||||
}""".trimIndent().toByteArray()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -127,13 +134,118 @@ class DefaultSessionWellknownRetrieverTest {
|
||||||
assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java)
|
assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDefaultSessionWellknownRetriever(
|
@Test
|
||||||
|
fun `get element wellknown hitting cache`() = runTest {
|
||||||
|
val sut = createDefaultSessionWellknownRetriever(
|
||||||
|
getUrlLambda = { lambdaError() },
|
||||||
|
cacheStore = InMemoryCacheStore(
|
||||||
|
initialData = mapOf(
|
||||||
|
WELLKNOWN_URL to CacheData(
|
||||||
|
value = WELLKNOWN_CONTENT,
|
||||||
|
updatedAt = A_FAKE_TIMESTAMP,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get element wellknown hitting cache containing invalid json`() = runTest {
|
||||||
|
val cacheStore = InMemoryCacheStore(
|
||||||
|
initialData = mapOf(
|
||||||
|
WELLKNOWN_URL to CacheData(
|
||||||
|
value = WELLKNOWN_CONTENT,
|
||||||
|
updatedAt = A_FAKE_TIMESTAMP,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val sut = createDefaultSessionWellknownRetriever(
|
||||||
|
getUrlLambda = {
|
||||||
|
Result.success("{}".toByteArray())
|
||||||
|
},
|
||||||
|
cacheStore = cacheStore,
|
||||||
|
jsonProvider = JsonProvider { throw Exception("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.dataMap).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get element wellknown hitting outdated cache`() = runTest {
|
||||||
|
val sut = createDefaultSessionWellknownRetriever(
|
||||||
|
getUrlLambda = {
|
||||||
|
Result.success("{}".toByteArray())
|
||||||
|
},
|
||||||
|
cacheStore = InMemoryCacheStore(
|
||||||
|
initialData = mapOf(
|
||||||
|
WELLKNOWN_URL to CacheData(
|
||||||
|
value = WELLKNOWN_CONTENT,
|
||||||
|
updatedAt = 0L,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 3 days later, so the cache is outdated
|
||||||
|
systemClock = FakeSystemClock(3 * 24 * 60 * 60 * 1000L)
|
||||||
|
)
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// Next call returns the updated value
|
||||||
|
runCurrent()
|
||||||
|
assertThat(sut.getElementWellKnown()).isEqualTo(
|
||||||
|
WellknownRetrieverResult.Success(
|
||||||
|
anElementWellKnown()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createDefaultSessionWellknownRetriever(
|
||||||
getUrlLambda: (String) -> Result<ByteArray>,
|
getUrlLambda: (String) -> Result<ByteArray>,
|
||||||
|
jsonProvider: JsonProvider = DefaultJsonProvider(),
|
||||||
|
cacheStore: CacheStore = InMemoryCacheStore(),
|
||||||
|
systemClock: SystemClock = FakeSystemClock(),
|
||||||
) = DefaultSessionWellknownRetriever(
|
) = DefaultSessionWellknownRetriever(
|
||||||
matrixClient = FakeMatrixClient(
|
matrixClient = FakeMatrixClient(
|
||||||
userIdServerNameLambda = { "user.domain.org" },
|
userIdServerNameLambda = { "user.domain.org" },
|
||||||
getUrlLambda = getUrlLambda,
|
getUrlLambda = getUrlLambda,
|
||||||
),
|
),
|
||||||
json = DefaultJsonProvider(),
|
json = jsonProvider,
|
||||||
|
cacheStore = cacheStore,
|
||||||
|
systemClock = systemClock,
|
||||||
|
sessionCoroutineScope = 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"
|
||||||
|
}"""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue