mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-04 20:01:55 +00:00
Merge pull request #7591 from element-hq/bma/iterateOnClientCreation
Iterate on client creation
This commit is contained in:
+1
-1
Submodule enterprise updated: bb07b4f839...f463003f68
+19
-2
@@ -13,6 +13,23 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
|||||||
/**
|
/**
|
||||||
* A hook that can be used to customize the [MatrixClientBuilder] for enterprise features.
|
* A hook that can be used to customize the [MatrixClientBuilder] for enterprise features.
|
||||||
*/
|
*/
|
||||||
fun interface ClientBuilderEnterpriseHook {
|
interface ClientBuilderEnterpriseHook {
|
||||||
suspend operator fun invoke(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder
|
/**
|
||||||
|
* Customize the [MatrixClientBuilder] for enterprise features.
|
||||||
|
* This method is invoked everytime a new [MatrixClientBuilder] is created.
|
||||||
|
*
|
||||||
|
* @param clientBuilder The [MatrixClientBuilder] to customize.
|
||||||
|
* @return The customized [MatrixClientBuilder].
|
||||||
|
*/
|
||||||
|
suspend fun beforeClientCreation(clientBuilder: MatrixClientBuilder): MatrixClientBuilder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customize the [MatrixClientBuilder] for enterprise features.
|
||||||
|
* This method is invoked as well as the other method when a new [MatrixClientBuilder] is created to build a client for a specific session.
|
||||||
|
*
|
||||||
|
* @param clientBuilder The [MatrixClientBuilder] to customize.
|
||||||
|
* @param sessionId The [SessionId] for which the [MatrixClientBuilder] is being created.
|
||||||
|
* @return The customized [MatrixClientBuilder].
|
||||||
|
*/
|
||||||
|
suspend fun beforeClientCreationWithSession(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -18,7 +18,12 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
|||||||
*/
|
*/
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultClientBuilderEnterpriseHook : ClientBuilderEnterpriseHook {
|
class DefaultClientBuilderEnterpriseHook : ClientBuilderEnterpriseHook {
|
||||||
override suspend fun invoke(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder {
|
override suspend fun beforeClientCreation(clientBuilder: MatrixClientBuilder): MatrixClientBuilder {
|
||||||
|
// No modification
|
||||||
|
return clientBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun beforeClientCreationWithSession(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder {
|
||||||
// No modification
|
// No modification
|
||||||
return clientBuilder
|
return clientBuilder
|
||||||
}
|
}
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import io.element.android.features.enterprise.api.ClientBuilderEnterpriseHook
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClientBuilder
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
||||||
|
class FakeClientBuilderEnterpriseHook(
|
||||||
|
private val beforeClientCreationResult: (MatrixClientBuilder) -> MatrixClientBuilder = { it },
|
||||||
|
private val beforeClientCreationWithSessionResult: (MatrixClientBuilder, SessionId) -> MatrixClientBuilder = { clientBuilder, _ -> clientBuilder },
|
||||||
|
) : ClientBuilderEnterpriseHook {
|
||||||
|
override suspend fun beforeClientCreation(clientBuilder: MatrixClientBuilder): MatrixClientBuilder {
|
||||||
|
return beforeClientCreationResult(clientBuilder)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun beforeClientCreationWithSession(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder {
|
||||||
|
return beforeClientCreationWithSessionResult(clientBuilder, sessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-11
@@ -35,7 +35,6 @@ import io.element.android.services.analytics.api.AnalyticsService
|
|||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.rustcomponents.sdk.Client
|
|
||||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||||
import org.matrix.rustcomponents.sdk.CrossProcessLockConfig
|
import org.matrix.rustcomponents.sdk.CrossProcessLockConfig
|
||||||
import org.matrix.rustcomponents.sdk.RequestConfig
|
import org.matrix.rustcomponents.sdk.RequestConfig
|
||||||
@@ -100,7 +99,12 @@ class RustMatrixClientFactory(
|
|||||||
)
|
)
|
||||||
.homeserverUrl(sessionData.homeserverUrl)
|
.homeserverUrl(sessionData.homeserverUrl)
|
||||||
.enableAutomaticBackPagination(featureFlagService.isFeatureEnabled(FeatureFlags.AutomaticBackPagination))
|
.enableAutomaticBackPagination(featureFlagService.isFeatureEnabled(FeatureFlags.AutomaticBackPagination))
|
||||||
.let { (clientBuilderEnterpriseHook(RustMatrixClientBuilder(it), SessionId(sessionData.userId)) as RustMatrixClientBuilder).inner }
|
.let {
|
||||||
|
(clientBuilderEnterpriseHook.beforeClientCreationWithSession(
|
||||||
|
clientBuilder = RustMatrixClientBuilder(it),
|
||||||
|
sessionId = SessionId(sessionData.userId),
|
||||||
|
) as RustMatrixClientBuilder).inner
|
||||||
|
}
|
||||||
.use { it.build() }
|
.use { it.build() }
|
||||||
|
|
||||||
client.setMediaRetentionPolicy(
|
client.setMediaRetentionPolicy(
|
||||||
@@ -118,14 +122,6 @@ class RustMatrixClientFactory(
|
|||||||
|
|
||||||
client.restoreSession(sessionData.toSession())
|
client.restoreSession(sessionData.toSession())
|
||||||
|
|
||||||
create(client, sessionData, isMessageSearchAvailable)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun create(
|
|
||||||
client: Client,
|
|
||||||
sessionData: SessionData,
|
|
||||||
isMessageSearchAvailable: Boolean,
|
|
||||||
): RustMatrixClient {
|
|
||||||
val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens()
|
val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens()
|
||||||
|
|
||||||
client.setUtdDelegate(UtdTracker(analyticsService))
|
client.setUtdDelegate(UtdTracker(analyticsService))
|
||||||
@@ -135,7 +131,7 @@ class RustMatrixClientFactory(
|
|||||||
.withOfflineMode()
|
.withOfflineMode()
|
||||||
.finish()
|
.finish()
|
||||||
|
|
||||||
return RustMatrixClient(
|
RustMatrixClient(
|
||||||
sessionPaths = sessionData.getSessionPaths(),
|
sessionPaths = sessionData.getSessionPaths(),
|
||||||
innerClient = client,
|
innerClient = client,
|
||||||
sessionStore = sessionStore,
|
sessionStore = sessionStore,
|
||||||
@@ -227,6 +223,9 @@ class RustMatrixClientFactory(
|
|||||||
// Workaround for non-nullable proxy parameter in the SDK, since each call to the ClientBuilder returns a new reference we need to keep
|
// Workaround for non-nullable proxy parameter in the SDK, since each call to the ClientBuilder returns a new reference we need to keep
|
||||||
proxyProvider.provides()?.let { proxy(it) } ?: this
|
proxyProvider.provides()?.let { proxy(it) } ?: this
|
||||||
}
|
}
|
||||||
|
.let {
|
||||||
|
(clientBuilderEnterpriseHook.beforeClientCreation(RustMatrixClientBuilder(it)) as RustMatrixClientBuilder).inner
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+45
-54
@@ -44,6 +44,7 @@ import io.element.android.libraries.matrix.impl.keys.SecretGenerator
|
|||||||
import io.element.android.libraries.matrix.impl.mapper.toSessionData
|
import io.element.android.libraries.matrix.impl.mapper.toSessionData
|
||||||
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
||||||
import io.element.android.libraries.sessionstorage.api.LoginType
|
import io.element.android.libraries.sessionstorage.api.LoginType
|
||||||
|
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
@@ -101,17 +102,7 @@ class RustMatrixAuthenticationService(
|
|||||||
runCatchingExceptions {
|
runCatchingExceptions {
|
||||||
val sessionData = sessionStore.getSession(sessionId.value)
|
val sessionData = sessionStore.getSession(sessionId.value)
|
||||||
if (sessionData != null) {
|
if (sessionData != null) {
|
||||||
if (sessionData.isTokenValid) {
|
restoreSession(sessionData)
|
||||||
// Use the sessionData.passphrase, which can be null for a previously created session
|
|
||||||
if (sessionData.passphrase == null) {
|
|
||||||
Timber.w("Restoring a session without a passphrase")
|
|
||||||
} else {
|
|
||||||
Timber.w("Restoring a session with a passphrase")
|
|
||||||
}
|
|
||||||
rustMatrixClientFactory.create(sessionData)
|
|
||||||
} else {
|
|
||||||
throw SessionRestorationException.InvalidToken()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw SessionRestorationException.MissingSession(sessionId)
|
throw SessionRestorationException.MissingSession(sessionId)
|
||||||
}
|
}
|
||||||
@@ -120,6 +111,19 @@ class RustMatrixAuthenticationService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun restoreSession(sessionData: SessionData): MatrixClient {
|
||||||
|
if (!sessionData.isTokenValid) {
|
||||||
|
throw SessionRestorationException.InvalidToken()
|
||||||
|
}
|
||||||
|
// Use the sessionData.passphrase, which can be null for a previously created session
|
||||||
|
if (sessionData.passphrase == null) {
|
||||||
|
Timber.w("Restoring a session without a passphrase")
|
||||||
|
} else {
|
||||||
|
Timber.w("Restoring a session with a passphrase")
|
||||||
|
}
|
||||||
|
return rustMatrixClientFactory.create(sessionData)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getDatabaseKey(): ClientSecret {
|
private fun getDatabaseKey(): ClientSecret {
|
||||||
Timber.d("New sessions will be encrypted with a raw key")
|
Timber.d("New sessions will be encrypted with a raw key")
|
||||||
return secretGenerator.generateKey()
|
return secretGenerator.generateKey()
|
||||||
@@ -137,7 +141,7 @@ class RustMatrixAuthenticationService(
|
|||||||
|
|
||||||
client.homeserverLoginDetails().map()
|
client.homeserverLoginDetails().map()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
clear(destroyClient = true)
|
clear()
|
||||||
}.mapFailure { failure ->
|
}.mapFailure { failure ->
|
||||||
Timber.e(failure, "Failed to set homeserver to $homeserver")
|
Timber.e(failure, "Failed to set homeserver to $homeserver")
|
||||||
failure.mapAuthenticationException()
|
failure.mapAuthenticationException()
|
||||||
@@ -165,18 +169,7 @@ class RustMatrixAuthenticationService(
|
|||||||
passphrase = pendingKey.formattedAsString(),
|
passphrase = pendingKey.formattedAsString(),
|
||||||
sessionPaths = currentSessionPaths,
|
sessionPaths = currentSessionPaths,
|
||||||
)
|
)
|
||||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable())
|
finalizeClientCreation(sessionData)
|
||||||
|
|
||||||
// Apply enterprise hooks to the newly created client as soon as possible
|
|
||||||
clientEnterpriseHook(matrixClient)
|
|
||||||
|
|
||||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
|
||||||
sessionStore.addSession(sessionData)
|
|
||||||
|
|
||||||
// Clean up the strong reference held here since it's no longer necessary
|
|
||||||
clear(destroyClient = false)
|
|
||||||
|
|
||||||
SessionId(sessionData.userId)
|
|
||||||
}.mapFailure { failure ->
|
}.mapFailure { failure ->
|
||||||
Timber.e(failure, "Failed to login")
|
Timber.e(failure, "Failed to login")
|
||||||
failure.mapAuthenticationException()
|
failure.mapAuthenticationException()
|
||||||
@@ -305,20 +298,7 @@ class RustMatrixAuthenticationService(
|
|||||||
passphrase = pendingKey.formattedAsString(),
|
passphrase = pendingKey.formattedAsString(),
|
||||||
sessionPaths = currentSessionPaths,
|
sessionPaths = currentSessionPaths,
|
||||||
)
|
)
|
||||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable())
|
finalizeClientCreation(sessionData)
|
||||||
|
|
||||||
// Apply enterprise hooks to the newly created client as soon as possible
|
|
||||||
clientEnterpriseHook(matrixClient)
|
|
||||||
|
|
||||||
matrixClient.waitForKnownVerificationState()
|
|
||||||
|
|
||||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
|
||||||
sessionStore.addSession(sessionData)
|
|
||||||
|
|
||||||
// Clean up the strong reference held here since it's no longer necessary
|
|
||||||
clear(destroyClient = false)
|
|
||||||
|
|
||||||
SessionId(sessionData.userId)
|
|
||||||
}.mapFailure { failure ->
|
}.mapFailure { failure ->
|
||||||
Timber.e(failure, "Failed to login with OAuth")
|
Timber.e(failure, "Failed to login with OAuth")
|
||||||
failure.mapAuthenticationException()
|
failure.mapAuthenticationException()
|
||||||
@@ -357,6 +337,7 @@ class RustMatrixAuthenticationService(
|
|||||||
sessionPaths = emptySessionPaths,
|
sessionPaths = emptySessionPaths,
|
||||||
qrCodeData = sdkQrCodeLoginData,
|
qrCodeData = sdkQrCodeLoginData,
|
||||||
)
|
)
|
||||||
|
currentClient = client
|
||||||
client.newLoginWithQrCodeHandler(
|
client.newLoginWithQrCodeHandler(
|
||||||
oauthConfiguration = oAuthConfiguration,
|
oauthConfiguration = oAuthConfiguration,
|
||||||
).use {
|
).use {
|
||||||
@@ -374,18 +355,7 @@ class RustMatrixAuthenticationService(
|
|||||||
passphrase = pendingKey.formattedAsString(),
|
passphrase = pendingKey.formattedAsString(),
|
||||||
sessionPaths = emptySessionPaths,
|
sessionPaths = emptySessionPaths,
|
||||||
)
|
)
|
||||||
val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable())
|
finalizeClientCreation(sessionData)
|
||||||
|
|
||||||
// Apply enterprise hooks to the newly created client as soon as possible
|
|
||||||
clientEnterpriseHook(matrixClient)
|
|
||||||
|
|
||||||
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
|
||||||
sessionStore.addSession(sessionData)
|
|
||||||
|
|
||||||
// Clean up the strong reference held here since it's no longer necessary
|
|
||||||
clear(destroyClient = false)
|
|
||||||
|
|
||||||
SessionId(sessionData.userId)
|
|
||||||
}.mapFailure {
|
}.mapFailure {
|
||||||
when (it) {
|
when (it) {
|
||||||
is QrCodeDecodeException -> QrErrorMapper.map(it)
|
is QrCodeDecodeException -> QrErrorMapper.map(it)
|
||||||
@@ -393,6 +363,8 @@ class RustMatrixAuthenticationService(
|
|||||||
else -> it
|
else -> it
|
||||||
}
|
}
|
||||||
}.onFailure { throwable ->
|
}.onFailure { throwable ->
|
||||||
|
// A QR code login always builds its own client, so it can be disposed of on failure.
|
||||||
|
clear()
|
||||||
if (throwable is CancellationException) {
|
if (throwable is CancellationException) {
|
||||||
throw throwable
|
throw throwable
|
||||||
}
|
}
|
||||||
@@ -400,6 +372,27 @@ class RustMatrixAuthenticationService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun finalizeClientCreation(sessionData: SessionData): SessionId {
|
||||||
|
// Close the client which was used to perform the login before creating the final client.
|
||||||
|
// Both use the same session paths, so their SQLite stores must never be opened at the same time.
|
||||||
|
clear()
|
||||||
|
|
||||||
|
val matrixClient = restoreSession(sessionData)
|
||||||
|
// Apply enterprise hooks to the newly created client as soon as possible
|
||||||
|
clientEnterpriseHook(matrixClient)
|
||||||
|
|
||||||
|
matrixClient.waitForKnownVerificationState()
|
||||||
|
|
||||||
|
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
|
||||||
|
sessionStore.addSession(sessionData)
|
||||||
|
|
||||||
|
// The session paths now hold the data of the account which has just been logged in, so forget
|
||||||
|
// them: they must not be deleted by the rotateSessionPath() of the next login attempt.
|
||||||
|
sessionPaths = null
|
||||||
|
|
||||||
|
return SessionId(sessionData.userId)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun makeClient(
|
private suspend fun makeClient(
|
||||||
sessionPaths: SessionPaths,
|
sessionPaths: SessionPaths,
|
||||||
config: suspend ClientBuilder.() -> ClientBuilder,
|
config: suspend ClientBuilder.() -> ClientBuilder,
|
||||||
@@ -449,10 +442,8 @@ class RustMatrixAuthenticationService(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clear(destroyClient: Boolean) {
|
private fun clear() {
|
||||||
if (destroyClient) {
|
currentClient?.close()
|
||||||
currentClient?.close()
|
|
||||||
}
|
|
||||||
currentClient = null
|
currentClient = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -9,11 +9,13 @@
|
|||||||
package io.element.android.libraries.matrix.impl
|
package io.element.android.libraries.matrix.impl
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.enterprise.test.FakeClientBuilderEnterpriseHook
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider
|
import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider
|
||||||
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
|
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory
|
||||||
import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider
|
import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider
|
||||||
|
import io.element.android.libraries.matrix.impl.storage.SqliteStoreBuilderProvider
|
||||||
import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
|
import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
@@ -51,6 +53,7 @@ fun TestScope.createRustMatrixClientFactory(
|
|||||||
),
|
),
|
||||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||||
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
|
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
|
||||||
|
sqliteStoreBuilderProvider: SqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
|
||||||
) = RustMatrixClientFactory(
|
) = RustMatrixClientFactory(
|
||||||
cacheDirectory = cacheDirectory,
|
cacheDirectory = cacheDirectory,
|
||||||
appCoroutineScope = backgroundScope,
|
appCoroutineScope = backgroundScope,
|
||||||
@@ -63,7 +66,7 @@ fun TestScope.createRustMatrixClientFactory(
|
|||||||
featureFlagService = FakeFeatureFlagService(),
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
timelineEventFilterFactory = FakeTimelineEventFilterFactory(),
|
timelineEventFilterFactory = FakeTimelineEventFilterFactory(),
|
||||||
clientBuilderProvider = clientBuilderProvider,
|
clientBuilderProvider = clientBuilderProvider,
|
||||||
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
|
sqliteStoreBuilderProvider = sqliteStoreBuilderProvider,
|
||||||
workManagerScheduler = workManagerScheduler,
|
workManagerScheduler = workManagerScheduler,
|
||||||
clientBuilderEnterpriseHook = { builder, _ -> builder },
|
clientBuilderEnterpriseHook = FakeClientBuilderEnterpriseHook(),
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-1
@@ -8,6 +8,7 @@
|
|||||||
package io.element.android.libraries.matrix.impl
|
package io.element.android.libraries.matrix.impl
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.enterprise.test.FakeClientBuilderEnterpriseHook
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider
|
import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider
|
||||||
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
||||||
@@ -77,6 +78,6 @@ class RustTemporaryMatrixClientFactoryTest {
|
|||||||
clientBuilderProvider = clientBuilderProvider,
|
clientBuilderProvider = clientBuilderProvider,
|
||||||
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
|
sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
|
||||||
workManagerScheduler = workManagerScheduler,
|
workManagerScheduler = workManagerScheduler,
|
||||||
clientBuilderEnterpriseHook = { builder, _ -> builder },
|
clientBuilderEnterpriseHook = FakeClientBuilderEnterpriseHook(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+187
-5
@@ -12,25 +12,44 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
|
import io.element.android.libraries.matrix.api.paths.SessionPaths
|
||||||
import io.element.android.libraries.matrix.impl.ClientBuilderProvider
|
import io.element.android.libraries.matrix.impl.ClientBuilderProvider
|
||||||
import io.element.android.libraries.matrix.impl.FakeClientBuilderProvider
|
import io.element.android.libraries.matrix.impl.FakeClientBuilderProvider
|
||||||
|
import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData
|
||||||
import io.element.android.libraries.matrix.impl.createRustMatrixClientFactory
|
import io.element.android.libraries.matrix.impl.createRustMatrixClientFactory
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverLoginDetails
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverLoginDetails
|
||||||
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiLoginWithQrCodeHandler
|
||||||
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiQrCodeData
|
||||||
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
||||||
|
import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilder
|
||||||
|
import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider
|
||||||
|
import io.element.android.libraries.matrix.impl.storage.SqliteStoreBuilder
|
||||||
|
import io.element.android.libraries.matrix.impl.storage.SqliteStoreBuilderProvider
|
||||||
|
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeOAuthRedirectUrlProvider
|
import io.element.android.libraries.matrix.test.auth.FakeOAuthRedirectUrlProvider
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
|
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import org.matrix.rustcomponents.sdk.Client
|
||||||
|
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||||
|
import org.matrix.rustcomponents.sdk.HumanQrLoginException
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class RustMatrixAuthenticationServiceTest {
|
class RustMatrixAuthenticationServiceTest {
|
||||||
|
@get:Rule
|
||||||
|
val temporaryFolder = TemporaryFolder()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `setHomeserver is successful`() = runTest {
|
fun `setHomeserver is successful`() = runTest {
|
||||||
val sut = createRustMatrixAuthenticationService(
|
val sut = createRustMatrixAuthenticationService(
|
||||||
@@ -74,20 +93,183 @@ class RustMatrixAuthenticationServiceTest {
|
|||||||
closeResult.assertions().isCalledOnce()
|
closeResult.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `login closes the client used to log in before building the client of the session`() = runTest {
|
||||||
|
val events = mutableListOf<String>()
|
||||||
|
val sut = createRustMatrixAuthenticationService(
|
||||||
|
clientBuilderProvider = FakeSequentialClientBuilderProvider(
|
||||||
|
{
|
||||||
|
events.add("build login client")
|
||||||
|
FakeFfiClient(
|
||||||
|
homeserverLoginDetailsResult = { FakeFfiHomeserverLoginDetails() },
|
||||||
|
loginResult = { _, _ -> },
|
||||||
|
closeResult = { events.add("close login client") },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
events.add("build session client")
|
||||||
|
FakeFfiClient(withUtdHook = {})
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue()
|
||||||
|
assertThat(sut.login("alice", "password").getOrNull()).isEqualTo(A_USER_ID)
|
||||||
|
|
||||||
|
// The two clients share the same session paths, so the login one must be closed first.
|
||||||
|
assertThat(events).containsExactly("build login client", "close login client", "build session client").inOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loginWithQrCode closes the client used to log in before building the client of the session`() = runTest {
|
||||||
|
val events = mutableListOf<String>()
|
||||||
|
val sut = createRustMatrixAuthenticationService(
|
||||||
|
clientBuilderProvider = FakeSequentialClientBuilderProvider(
|
||||||
|
{
|
||||||
|
events.add("build login client")
|
||||||
|
FakeFfiClient(
|
||||||
|
newLoginWithQrCodeHandlerResult = { FakeFfiLoginWithQrCodeHandler() },
|
||||||
|
closeResult = { events.add("close login client") },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
events.add("build session client")
|
||||||
|
FakeFfiClient(withUtdHook = {})
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = sut.loginWithQrCode(aSdkQrCodeLoginData()) {}
|
||||||
|
|
||||||
|
assertThat(result.getOrNull()).isEqualTo(A_USER_ID)
|
||||||
|
assertThat(events).containsExactly("build login client", "close login client", "build session client").inOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loginWithQrCode closes the client it created when the login fails`() = runTest {
|
||||||
|
val closeResult = lambdaRecorder<Unit> {}
|
||||||
|
val sut = createRustMatrixAuthenticationService(
|
||||||
|
clientBuilderProvider = FakeClientBuilderProvider(
|
||||||
|
provideResult = {
|
||||||
|
FakeFfiClientBuilder(
|
||||||
|
buildResult = {
|
||||||
|
FakeFfiClient(
|
||||||
|
newLoginWithQrCodeHandlerResult = {
|
||||||
|
FakeFfiLoginWithQrCodeHandler(
|
||||||
|
scanResult = { throw HumanQrLoginException.Unknown() },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
closeResult = closeResult,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(sut.loginWithQrCode(aSdkQrCodeLoginData()) {}.isFailure).isTrue()
|
||||||
|
closeResult.assertions().isCalledOnce()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aSdkQrCodeLoginData() = SdkQrCodeLoginData(
|
||||||
|
FakeFfiQrCodeData(
|
||||||
|
baseUrlResult = { A_HOMESERVER_URL },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [ClientBuilderProvider] handing out one [Client] per call, in order.
|
||||||
|
*/
|
||||||
|
private class FakeSequentialClientBuilderProvider(
|
||||||
|
private vararg val clients: () -> Client,
|
||||||
|
) : ClientBuilderProvider {
|
||||||
|
private var index = 0
|
||||||
|
override fun provide(): ClientBuilder = FakeFfiClientBuilder(buildResult = clients[index++])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a new login does not delete the session data of the account which has just been logged in`() = runTest {
|
||||||
|
val storeBuilderProvider = SessionDirectoryCreatingSqliteStoreBuilderProvider()
|
||||||
|
val sut = createRustMatrixAuthenticationService(
|
||||||
|
clientBuilderProvider = FakeSequentialClientBuilderProvider(
|
||||||
|
{ aLoginFakeFfiClient() },
|
||||||
|
{ FakeFfiClient(withUtdHook = {}) },
|
||||||
|
{ aLoginFakeFfiClient() },
|
||||||
|
),
|
||||||
|
sessionPathsFactory = SessionPathsFactory(temporaryFolder.newFolder("base"), temporaryFolder.newFolder("cache")),
|
||||||
|
sqliteStoreBuilderProvider = storeBuilderProvider,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue()
|
||||||
|
assertThat(sut.login("alice", "password").getOrNull()).isEqualTo(A_USER_ID)
|
||||||
|
val loggedInSessionPaths = storeBuilderProvider.providedSessionPaths.first()
|
||||||
|
|
||||||
|
// Adding another account rotates the session paths, which must not touch the previous account.
|
||||||
|
assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue()
|
||||||
|
|
||||||
|
assertThat(loggedInSessionPaths.fileDirectory.exists()).isTrue()
|
||||||
|
assertThat(loggedInSessionPaths.cacheDirectory.exists()).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a new login deletes the session data of a previous failed login attempt`() = runTest {
|
||||||
|
val storeBuilderProvider = SessionDirectoryCreatingSqliteStoreBuilderProvider()
|
||||||
|
val sut = createRustMatrixAuthenticationService(
|
||||||
|
clientBuilderProvider = FakeSequentialClientBuilderProvider(
|
||||||
|
{ aLoginFakeFfiClient() },
|
||||||
|
{ throw IllegalStateException("Failed to build the client of the session") },
|
||||||
|
{ aLoginFakeFfiClient() },
|
||||||
|
),
|
||||||
|
sessionPathsFactory = SessionPathsFactory(temporaryFolder.newFolder("base"), temporaryFolder.newFolder("cache")),
|
||||||
|
sqliteStoreBuilderProvider = storeBuilderProvider,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue()
|
||||||
|
assertThat(sut.login("alice", "password").isFailure).isTrue()
|
||||||
|
val abandonedSessionPaths = storeBuilderProvider.providedSessionPaths.first()
|
||||||
|
|
||||||
|
assertThat(sut.setHomeserver("matrix.org").isSuccess).isTrue()
|
||||||
|
|
||||||
|
assertThat(abandonedSessionPaths.fileDirectory.exists()).isFalse()
|
||||||
|
assertThat(abandonedSessionPaths.cacheDirectory.exists()).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aLoginFakeFfiClient() = FakeFfiClient(
|
||||||
|
homeserverLoginDetailsResult = { FakeFfiHomeserverLoginDetails() },
|
||||||
|
loginResult = { _, _ -> },
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [SqliteStoreBuilderProvider] which creates the session directories, like the SDK does when it
|
||||||
|
* opens its stores, and records them so that a test can assert on their lifecycle.
|
||||||
|
*/
|
||||||
|
private class SessionDirectoryCreatingSqliteStoreBuilderProvider : SqliteStoreBuilderProvider {
|
||||||
|
val providedSessionPaths = mutableListOf<SessionPaths>()
|
||||||
|
|
||||||
|
override fun provide(sessionPaths: SessionPaths): SqliteStoreBuilder {
|
||||||
|
sessionPaths.fileDirectory.mkdirs()
|
||||||
|
sessionPaths.cacheDirectory.mkdirs()
|
||||||
|
providedSessionPaths.add(sessionPaths)
|
||||||
|
return FakeSqliteStoreBuilder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun TestScope.createRustMatrixAuthenticationService(
|
private fun TestScope.createRustMatrixAuthenticationService(
|
||||||
sessionStore: SessionStore = InMemorySessionStore(),
|
sessionStore: SessionStore = InMemorySessionStore(updateUserProfileResult = { _, _, _ -> }),
|
||||||
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(),
|
||||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
|
sessionPathsFactory: SessionPathsFactory = SessionPathsFactory(File("/base"), File("/cache")),
|
||||||
|
sqliteStoreBuilderProvider: SqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(),
|
||||||
): RustMatrixAuthenticationService {
|
): RustMatrixAuthenticationService {
|
||||||
val baseDirectory = File("/base")
|
|
||||||
val cacheDirectory = File("/cache")
|
|
||||||
val rustMatrixClientFactory = createRustMatrixClientFactory(
|
val rustMatrixClientFactory = createRustMatrixClientFactory(
|
||||||
cacheDirectory = cacheDirectory,
|
cacheDirectory = File("/cache"),
|
||||||
sessionStore = sessionStore,
|
sessionStore = sessionStore,
|
||||||
clientBuilderProvider = clientBuilderProvider,
|
clientBuilderProvider = clientBuilderProvider,
|
||||||
|
workManagerScheduler = FakeWorkManagerScheduler(submitLambda = {}),
|
||||||
|
sqliteStoreBuilderProvider = sqliteStoreBuilderProvider,
|
||||||
)
|
)
|
||||||
return RustMatrixAuthenticationService(
|
return RustMatrixAuthenticationService(
|
||||||
sessionPathsFactory = SessionPathsFactory(baseDirectory, cacheDirectory),
|
sessionPathsFactory = sessionPathsFactory,
|
||||||
coroutineDispatchers = testCoroutineDispatchers(),
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
sessionStore = sessionStore,
|
sessionStore = sessionStore,
|
||||||
rustMatrixClientFactory = rustMatrixClientFactory,
|
rustMatrixClientFactory = rustMatrixClientFactory,
|
||||||
|
|||||||
+12
@@ -25,10 +25,12 @@ import org.matrix.rustcomponents.sdk.Encryption
|
|||||||
import org.matrix.rustcomponents.sdk.HomeserverCapabilities
|
import org.matrix.rustcomponents.sdk.HomeserverCapabilities
|
||||||
import org.matrix.rustcomponents.sdk.HomeserverLoginDetails
|
import org.matrix.rustcomponents.sdk.HomeserverLoginDetails
|
||||||
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
|
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
|
||||||
|
import org.matrix.rustcomponents.sdk.LoginWithQrCodeHandler
|
||||||
import org.matrix.rustcomponents.sdk.NoHandle
|
import org.matrix.rustcomponents.sdk.NoHandle
|
||||||
import org.matrix.rustcomponents.sdk.NotificationClient
|
import org.matrix.rustcomponents.sdk.NotificationClient
|
||||||
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
|
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
|
||||||
import org.matrix.rustcomponents.sdk.NotificationSettings
|
import org.matrix.rustcomponents.sdk.NotificationSettings
|
||||||
|
import org.matrix.rustcomponents.sdk.OAuthConfiguration
|
||||||
import org.matrix.rustcomponents.sdk.ProfileListener
|
import org.matrix.rustcomponents.sdk.ProfileListener
|
||||||
import org.matrix.rustcomponents.sdk.PusherIdentifiers
|
import org.matrix.rustcomponents.sdk.PusherIdentifiers
|
||||||
import org.matrix.rustcomponents.sdk.PusherKind
|
import org.matrix.rustcomponents.sdk.PusherKind
|
||||||
@@ -57,6 +59,8 @@ class FakeFfiClient(
|
|||||||
private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() },
|
private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() },
|
||||||
private val getProfileResult: (String) -> UserProfile = { aRustUserProfile() },
|
private val getProfileResult: (String) -> UserProfile = { aRustUserProfile() },
|
||||||
private val homeserverLoginDetailsResult: () -> HomeserverLoginDetails = { lambdaError() },
|
private val homeserverLoginDetailsResult: () -> HomeserverLoginDetails = { lambdaError() },
|
||||||
|
private val loginResult: (String, String) -> Unit = { _, _ -> lambdaError() },
|
||||||
|
private val newLoginWithQrCodeHandlerResult: () -> LoginWithQrCodeHandler = { lambdaError() },
|
||||||
private val getStoreSizesResult: () -> StoreSizes = { lambdaError() },
|
private val getStoreSizesResult: () -> StoreSizes = { lambdaError() },
|
||||||
private val createRoomResult: (CreateRoomParameters) -> String = { lambdaError() },
|
private val createRoomResult: (CreateRoomParameters) -> String = { lambdaError() },
|
||||||
private val homeserverCapabilities: HomeserverCapabilities = FakeFfiHomeserverCapabilities(),
|
private val homeserverCapabilities: HomeserverCapabilities = FakeFfiHomeserverCapabilities(),
|
||||||
@@ -122,6 +126,14 @@ class FakeFfiClient(
|
|||||||
return homeserverLoginDetailsResult()
|
return homeserverLoginDetailsResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun login(username: String, password: String, initialDeviceName: String?, deviceId: String?) {
|
||||||
|
loginResult(username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newLoginWithQrCodeHandler(oauthConfiguration: OAuthConfiguration): LoginWithQrCodeHandler {
|
||||||
|
return newLoginWithQrCodeHandlerResult()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun setMediaRetentionPolicy(policy: MediaRetentionPolicy) {}
|
override suspend fun setMediaRetentionPolicy(policy: MediaRetentionPolicy) {}
|
||||||
|
|
||||||
override suspend fun getStoreSizes(): StoreSizes {
|
override suspend fun getStoreSizes(): StoreSizes {
|
||||||
|
|||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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.matrix.impl.fixtures.fakes
|
||||||
|
|
||||||
|
import org.matrix.rustcomponents.sdk.GeneratedQrLoginProgressListener
|
||||||
|
import org.matrix.rustcomponents.sdk.LoginWithQrCodeHandler
|
||||||
|
import org.matrix.rustcomponents.sdk.NoHandle
|
||||||
|
import org.matrix.rustcomponents.sdk.QrCodeData
|
||||||
|
import org.matrix.rustcomponents.sdk.QrLoginProgress
|
||||||
|
import org.matrix.rustcomponents.sdk.QrLoginProgressListener
|
||||||
|
|
||||||
|
class FakeFfiLoginWithQrCodeHandler(
|
||||||
|
private val generateResult: suspend () -> Unit = {},
|
||||||
|
private val scanResult: suspend (QrCodeData) -> Unit = {},
|
||||||
|
) : LoginWithQrCodeHandler(NoHandle) {
|
||||||
|
private var scanProgressListener: QrLoginProgressListener? = null
|
||||||
|
|
||||||
|
override suspend fun generate(progressListener: GeneratedQrLoginProgressListener) {
|
||||||
|
generateResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun scan(qrCodeData: QrCodeData, progressListener: QrLoginProgressListener) {
|
||||||
|
scanProgressListener = progressListener
|
||||||
|
scanResult(qrCodeData)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun emitScanProgress(progress: QrLoginProgress) {
|
||||||
|
scanProgressListener?.onUpdate(progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() = Unit
|
||||||
|
}
|
||||||
+5
@@ -14,12 +14,17 @@ import org.matrix.rustcomponents.sdk.QrCodeData
|
|||||||
|
|
||||||
class FakeFfiQrCodeData(
|
class FakeFfiQrCodeData(
|
||||||
private val serverNameResult: () -> String? = { lambdaError() },
|
private val serverNameResult: () -> String? = { lambdaError() },
|
||||||
|
private val baseUrlResult: () -> String? = { lambdaError() },
|
||||||
private val toBytesResult: () -> ByteArray = { lambdaError() },
|
private val toBytesResult: () -> ByteArray = { lambdaError() },
|
||||||
) : QrCodeData(NoHandle) {
|
) : QrCodeData(NoHandle) {
|
||||||
override fun serverName(): String? {
|
override fun serverName(): String? {
|
||||||
return serverNameResult()
|
return serverNameResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun baseUrl(): String? {
|
||||||
|
return baseUrlResult()
|
||||||
|
}
|
||||||
|
|
||||||
override fun toBytes(): ByteArray {
|
override fun toBytes(): ByteArray {
|
||||||
return toBytesResult()
|
return toBytesResult()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ class KonsistClassNameTest {
|
|||||||
"FakeListenableFuture",
|
"FakeListenableFuture",
|
||||||
"FakeMediaStoreContentProvider",
|
"FakeMediaStoreContentProvider",
|
||||||
"FakeParentNode",
|
"FakeParentNode",
|
||||||
|
"FakeSequentialClientBuilderProvider",
|
||||||
)
|
)
|
||||||
.assertTrue {
|
.assertTrue {
|
||||||
val interfaceName = it.name
|
val interfaceName = it.name
|
||||||
|
|||||||
Reference in New Issue
Block a user