Merge pull request #5909 from element-hq/feature/bma/qrCodeLogin
Link new device using QrCode - First version
This commit is contained in:
commit
3ea10c2c62
180 changed files with 5052 additions and 144 deletions
|
|
@ -27,6 +27,8 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
|
||||
import io.element.android.libraries.matrix.api.createroom.RoomPreset
|
||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler
|
||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler
|
||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
|
|
@ -47,6 +49,9 @@ import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
|
|||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService
|
||||
import io.element.android.libraries.matrix.impl.exception.mapClientException
|
||||
import io.element.android.libraries.matrix.impl.linknewdevice.RustLinkDesktopHandler
|
||||
import io.element.android.libraries.matrix.impl.linknewdevice.RustLinkMobileHandler
|
||||
import io.element.android.libraries.matrix.impl.linknewdevice.RustQrCodeDataParser
|
||||
import io.element.android.libraries.matrix.impl.mapper.map
|
||||
import io.element.android.libraries.matrix.impl.media.RustMediaLoader
|
||||
import io.element.android.libraries.matrix.impl.media.RustMediaPreviewService
|
||||
|
|
@ -739,6 +744,35 @@ class RustMatrixClient(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun canLinkNewDevice(): Result<Boolean> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerClient.isLoginWithQrCodeSupported()
|
||||
}
|
||||
}
|
||||
|
||||
override fun createLinkMobileHandler(): Result<LinkMobileHandler> {
|
||||
return runCatchingExceptions {
|
||||
val handler = innerClient.newGrantLoginWithQrCodeHandler()
|
||||
RustLinkMobileHandler(
|
||||
inner = handler,
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
sessionDispatcher = sessionDispatcher,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createLinkDesktopHandler(): Result<LinkDesktopHandler> {
|
||||
return runCatchingExceptions {
|
||||
val handler = innerClient.newGrantLoginWithQrCodeHandler()
|
||||
RustLinkDesktopHandler(
|
||||
inner = handler,
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
sessionDispatcher = sessionDispatcher,
|
||||
qrCodeDataParser = RustQrCodeDataParser(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun markRoomAsFullyRead(roomId: RoomId, eventId: EventId): Result<Unit> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
val room = innerClient.getRoom(roomId.value) ?: error("Could not fetch associated room")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2025 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.linknewdevice
|
||||
|
||||
import io.element.android.libraries.matrix.api.linknewdevice.ErrorType
|
||||
import org.matrix.rustcomponents.sdk.HumanQrGrantLoginException
|
||||
|
||||
internal fun HumanQrGrantLoginException.map() = when (this) {
|
||||
is HumanQrGrantLoginException.DeviceIdAlreadyInUse -> ErrorType.DeviceIdAlreadyInUse(message.orEmpty())
|
||||
is HumanQrGrantLoginException.InvalidCheckCode -> ErrorType.InvalidCheckCode(message.orEmpty())
|
||||
is HumanQrGrantLoginException.MissingSecretsBackup -> ErrorType.MissingSecretsBackup(message.orEmpty())
|
||||
is HumanQrGrantLoginException.NotFound -> ErrorType.NotFound(message.orEmpty())
|
||||
is HumanQrGrantLoginException.UnableToCreateDevice -> ErrorType.UnableToCreateDevice(message.orEmpty())
|
||||
is HumanQrGrantLoginException.Unknown -> ErrorType.Unknown(message.orEmpty())
|
||||
is HumanQrGrantLoginException.UnsupportedProtocol -> ErrorType.UnsupportedProtocol(message.orEmpty())
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright (c) 2025 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.linknewdevice
|
||||
|
||||
import org.matrix.rustcomponents.sdk.QrCodeData
|
||||
|
||||
interface QrCodeDataParser {
|
||||
fun parse(data: ByteArray): QrCodeData
|
||||
}
|
||||
|
||||
class RustQrCodeDataParser : QrCodeDataParser {
|
||||
override fun parse(data: ByteArray): QrCodeData {
|
||||
return QrCodeData.fromBytes(data)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2025 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.linknewdevice
|
||||
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.linknewdevice.CheckCodeSender
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.CheckCodeSender as FfiCheckCodeSender
|
||||
|
||||
class RustCheckCodeSender(
|
||||
private val inner: FfiCheckCodeSender,
|
||||
private val sessionDispatcher: CoroutineDispatcher,
|
||||
) : CheckCodeSender {
|
||||
override suspend fun validate(code: UByte): Boolean = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
// TODO https://github.com/matrix-org/matrix-rust-sdk/pull/5957
|
||||
// inner.validate(code)
|
||||
true
|
||||
}.getOrNull() ?: true
|
||||
}
|
||||
|
||||
override suspend fun send(code: UByte): Result<Unit> = withContext(sessionDispatcher) {
|
||||
runCatchingExceptions {
|
||||
inner.send(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright (c) 2025 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.linknewdevice
|
||||
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler
|
||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopStep
|
||||
import io.element.android.libraries.matrix.api.logs.LoggerTags
|
||||
import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.GrantLoginWithQrCodeHandler
|
||||
import org.matrix.rustcomponents.sdk.GrantQrLoginProgress
|
||||
import org.matrix.rustcomponents.sdk.GrantQrLoginProgressListener
|
||||
import org.matrix.rustcomponents.sdk.HumanQrGrantLoginException
|
||||
import org.matrix.rustcomponents.sdk.QrCodeDecodeException
|
||||
import timber.log.Timber
|
||||
|
||||
private val tag = LoggerTag("RustLinkDesktopHandler", LoggerTags.linkNewDevice)
|
||||
|
||||
class RustLinkDesktopHandler(
|
||||
private val inner: GrantLoginWithQrCodeHandler,
|
||||
private val sessionCoroutineScope: CoroutineScope,
|
||||
private val sessionDispatcher: CoroutineDispatcher,
|
||||
private val qrCodeDataParser: QrCodeDataParser,
|
||||
) : LinkDesktopHandler {
|
||||
private val _linkDesktopStep = MutableStateFlow<LinkDesktopStep>(LinkDesktopStep.Uninitialized)
|
||||
override val linkDesktopStep: StateFlow<LinkDesktopStep> = _linkDesktopStep.asStateFlow()
|
||||
|
||||
override suspend fun handleScannedQrCode(data: ByteArray) = withContext(sessionDispatcher) {
|
||||
Timber.tag(tag.value).d("Emit Uninitialized")
|
||||
_linkDesktopStep.emit(LinkDesktopStep.Uninitialized)
|
||||
try {
|
||||
val qrCodeData = qrCodeDataParser.parse(data)
|
||||
inner.scan(
|
||||
qrCodeData = qrCodeData,
|
||||
progressListener = object : GrantQrLoginProgressListener {
|
||||
override fun onUpdate(state: GrantQrLoginProgress) {
|
||||
sessionCoroutineScope.launch {
|
||||
val mappedState = state.map()
|
||||
Timber.tag(tag.value).d("Emit ${mappedState::class.java.simpleName}")
|
||||
_linkDesktopStep.emit(mappedState)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e: QrCodeDecodeException) {
|
||||
Timber.tag(tag.value).w(e, "Invalid QR code scanned")
|
||||
_linkDesktopStep.emit(
|
||||
LinkDesktopStep.InvalidQrCode(
|
||||
error = QrErrorMapper.map(e)
|
||||
)
|
||||
)
|
||||
} catch (e: HumanQrGrantLoginException) {
|
||||
Timber.tag(tag.value).w(e, "Error during QR login grant")
|
||||
_linkDesktopStep.emit(LinkDesktopStep.Error(e.map()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun GrantQrLoginProgress.map() = when (this) {
|
||||
GrantQrLoginProgress.Done -> LinkDesktopStep.Done
|
||||
GrantQrLoginProgress.Starting -> LinkDesktopStep.Starting
|
||||
GrantQrLoginProgress.SyncingSecrets -> LinkDesktopStep.SyncingSecrets
|
||||
is GrantQrLoginProgress.WaitingForAuth -> LinkDesktopStep.WaitingForAuth(
|
||||
verificationUri = verificationUri,
|
||||
)
|
||||
is GrantQrLoginProgress.EstablishingSecureChannel -> LinkDesktopStep.EstablishingSecureChannel(
|
||||
checkCode = checkCode,
|
||||
checkCodeString = checkCodeString,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright (c) 2025 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.linknewdevice
|
||||
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler
|
||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep
|
||||
import io.element.android.libraries.matrix.api.logs.LoggerTags
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.GrantGeneratedQrLoginProgress
|
||||
import org.matrix.rustcomponents.sdk.GrantGeneratedQrLoginProgressListener
|
||||
import org.matrix.rustcomponents.sdk.GrantLoginWithQrCodeHandler
|
||||
import org.matrix.rustcomponents.sdk.HumanQrGrantLoginException
|
||||
import timber.log.Timber
|
||||
|
||||
private val tag = LoggerTag("RustLinkMobileHandler", LoggerTags.linkNewDevice)
|
||||
|
||||
class RustLinkMobileHandler(
|
||||
private val inner: GrantLoginWithQrCodeHandler,
|
||||
private val sessionCoroutineScope: CoroutineScope,
|
||||
private val sessionDispatcher: CoroutineDispatcher,
|
||||
) : LinkMobileHandler {
|
||||
private val _linkMobileStep = MutableStateFlow<LinkMobileStep>(LinkMobileStep.Uninitialized)
|
||||
override val linkMobileStep: Flow<LinkMobileStep> = _linkMobileStep.asStateFlow()
|
||||
|
||||
override suspend fun start() = withContext(sessionDispatcher) {
|
||||
Timber.tag(tag.value).d("Emit Uninitialized")
|
||||
_linkMobileStep.emit(LinkMobileStep.Uninitialized)
|
||||
try {
|
||||
inner.generate(
|
||||
progressListener = object : GrantGeneratedQrLoginProgressListener {
|
||||
override fun onUpdate(state: GrantGeneratedQrLoginProgress) {
|
||||
sessionCoroutineScope.launch {
|
||||
val mappedState = state.map()
|
||||
Timber.tag(tag.value).d("Emit ${mappedState::class.java.simpleName}")
|
||||
_linkMobileStep.emit(mappedState)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e: HumanQrGrantLoginException) {
|
||||
Timber.tag(tag.value).w(e, "Error during QR login grant")
|
||||
_linkMobileStep.emit(LinkMobileStep.Error(e.map()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun GrantGeneratedQrLoginProgress.map(): LinkMobileStep {
|
||||
return when (this) {
|
||||
GrantGeneratedQrLoginProgress.Done -> LinkMobileStep.Done
|
||||
is GrantGeneratedQrLoginProgress.QrReady -> {
|
||||
LinkMobileStep.QrReady(String(qrCode.toBytes(), Charsets.ISO_8859_1))
|
||||
}
|
||||
is GrantGeneratedQrLoginProgress.QrScanned -> LinkMobileStep.QrScanned(
|
||||
RustCheckCodeSender(
|
||||
inner = checkCodeSender,
|
||||
sessionDispatcher = sessionDispatcher,
|
||||
)
|
||||
)
|
||||
GrantGeneratedQrLoginProgress.Starting -> LinkMobileStep.Starting
|
||||
GrantGeneratedQrLoginProgress.SyncingSecrets -> LinkMobileStep.SyncingSecrets
|
||||
is GrantGeneratedQrLoginProgress.WaitingForAuth -> LinkMobileStep.WaitingForAuth(verificationUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright (c) 2025 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 io.element.android.tests.testutils.lambda.lambdaError
|
||||
import org.matrix.rustcomponents.sdk.CheckCodeSender
|
||||
import org.matrix.rustcomponents.sdk.NoHandle
|
||||
|
||||
class FakeFfiCheckCodeSender(
|
||||
private val sendResult: (UByte) -> Unit = { _ -> lambdaError() }
|
||||
) : CheckCodeSender(NoHandle) {
|
||||
override suspend fun send(code: UByte) {
|
||||
sendResult(code)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2025 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.GrantGeneratedQrLoginProgress
|
||||
import org.matrix.rustcomponents.sdk.GrantGeneratedQrLoginProgressListener
|
||||
import org.matrix.rustcomponents.sdk.GrantLoginWithQrCodeHandler
|
||||
import org.matrix.rustcomponents.sdk.GrantQrLoginProgress
|
||||
import org.matrix.rustcomponents.sdk.GrantQrLoginProgressListener
|
||||
import org.matrix.rustcomponents.sdk.NoHandle
|
||||
import org.matrix.rustcomponents.sdk.QrCodeData
|
||||
|
||||
class FakeFfiGrantLoginWithQrCodeHandler(
|
||||
private val generateResult: () -> Unit = {},
|
||||
private val scanResult: (QrCodeData) -> Unit = {},
|
||||
) : GrantLoginWithQrCodeHandler(NoHandle) {
|
||||
private var generateProgressListener: GrantGeneratedQrLoginProgressListener? = null
|
||||
private var scanProgressListener: GrantQrLoginProgressListener? = null
|
||||
override suspend fun generate(progressListener: GrantGeneratedQrLoginProgressListener) {
|
||||
generateProgressListener = progressListener
|
||||
generateResult()
|
||||
}
|
||||
|
||||
fun emitGenerateProgress(progress: GrantGeneratedQrLoginProgress) {
|
||||
generateProgressListener?.onUpdate(progress)
|
||||
}
|
||||
|
||||
override suspend fun scan(qrCodeData: QrCodeData, progressListener: GrantQrLoginProgressListener) {
|
||||
scanProgressListener = progressListener
|
||||
scanResult(qrCodeData)
|
||||
}
|
||||
|
||||
fun emitScanProgress(progress: GrantQrLoginProgress) {
|
||||
scanProgressListener?.onUpdate(progress)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,8 +14,13 @@ import org.matrix.rustcomponents.sdk.QrCodeData
|
|||
|
||||
class FakeFfiQrCodeData(
|
||||
private val serverNameResult: () -> String? = { lambdaError() },
|
||||
private val toBytesResult: () -> ByteArray = { lambdaError() },
|
||||
) : QrCodeData(NoHandle) {
|
||||
override fun serverName(): String? {
|
||||
return serverNameResult()
|
||||
}
|
||||
|
||||
override fun toBytes(): ByteArray {
|
||||
return toBytesResult()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright (c) 2025 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.linknewdevice
|
||||
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiQrCodeData
|
||||
import org.matrix.rustcomponents.sdk.QrCodeData
|
||||
|
||||
class FakeQrCodeDataParser : QrCodeDataParser {
|
||||
override fun parse(data: ByteArray): QrCodeData {
|
||||
return FakeFfiQrCodeData()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2025 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.linknewdevice
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiCheckCodeSender
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class RustCheckCodeSenderTest {
|
||||
@Test
|
||||
fun `send invokes the Ffi object`() = runTest {
|
||||
val sendResult = lambdaRecorder<UByte, Unit> { }
|
||||
val sut = RustCheckCodeSender(
|
||||
inner = FakeFfiCheckCodeSender(
|
||||
sendResult = sendResult,
|
||||
),
|
||||
sessionDispatcher = StandardTestDispatcher(testScheduler),
|
||||
)
|
||||
sut.send(1.toUByte())
|
||||
sendResult.assertions().isCalledOnce().with(value(1.toUByte()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validate always returns true for now`() = runTest {
|
||||
val sut = RustCheckCodeSender(
|
||||
inner = FakeFfiCheckCodeSender(),
|
||||
sessionDispatcher = StandardTestDispatcher(testScheduler),
|
||||
)
|
||||
val result = sut.validate(1.toUByte())
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright (c) 2025 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.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.libraries.matrix.impl.linknewdevice
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.linknewdevice.ErrorType
|
||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopStep
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiGrantLoginWithQrCodeHandler
|
||||
import io.element.android.libraries.matrix.test.QR_CODE_DATA
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.GrantQrLoginProgress
|
||||
import org.matrix.rustcomponents.sdk.HumanQrGrantLoginException
|
||||
import org.matrix.rustcomponents.sdk.QrCodeDecodeException
|
||||
|
||||
class RustLinkDesktopHandlerTest {
|
||||
@Test
|
||||
fun `handleScannedQrCode function works as expected`() = runTest {
|
||||
val handler = FakeFfiGrantLoginWithQrCodeHandler()
|
||||
val sut = createRustLinkDesktopHandler(
|
||||
handler,
|
||||
)
|
||||
sut.linkDesktopStep.test {
|
||||
val initialItem = awaitItem()
|
||||
assertThat(initialItem).isEqualTo(LinkDesktopStep.Uninitialized)
|
||||
backgroundScope.launch {
|
||||
sut.handleScannedQrCode(QR_CODE_DATA)
|
||||
}
|
||||
runCurrent()
|
||||
// progress from the handler is mapped and emitted
|
||||
listOf(
|
||||
GrantQrLoginProgress.Starting to LinkDesktopStep.Starting,
|
||||
GrantQrLoginProgress.SyncingSecrets to LinkDesktopStep.SyncingSecrets,
|
||||
GrantQrLoginProgress.WaitingForAuth("aVerificationUri")
|
||||
to LinkDesktopStep.WaitingForAuth("aVerificationUri"),
|
||||
GrantQrLoginProgress.EstablishingSecureChannel(1.toUByte(), "1")
|
||||
to LinkDesktopStep.EstablishingSecureChannel(1.toUByte(), "1"),
|
||||
GrantQrLoginProgress.Done to LinkDesktopStep.Done,
|
||||
).forEach { (progress, expectedStep) ->
|
||||
handler.emitScanProgress(progress)
|
||||
assertThat(awaitItem()).isEqualTo(expectedStep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when handleScannedQrCode throws QrCodeDecodeException, the handler emits error step`() = runTest {
|
||||
val handler = FakeFfiGrantLoginWithQrCodeHandler(
|
||||
scanResult = { throw QrCodeDecodeException.Crypto("Scan failed") }
|
||||
)
|
||||
val sut = createRustLinkDesktopHandler(
|
||||
handler,
|
||||
)
|
||||
sut.linkDesktopStep.test {
|
||||
val initialItem = awaitItem()
|
||||
assertThat(initialItem).isEqualTo(LinkDesktopStep.Uninitialized)
|
||||
backgroundScope.launch {
|
||||
sut.handleScannedQrCode(QR_CODE_DATA)
|
||||
}
|
||||
runCurrent()
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState).isInstanceOf(LinkDesktopStep.InvalidQrCode::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when handleScannedQrCode throws HumanQrGrantLoginException, the handler emits error step`() = runTest {
|
||||
val handler = FakeFfiGrantLoginWithQrCodeHandler(
|
||||
scanResult = { throw HumanQrGrantLoginException.InvalidCheckCode("Invalid check code") }
|
||||
)
|
||||
val sut = createRustLinkDesktopHandler(
|
||||
handler,
|
||||
)
|
||||
sut.linkDesktopStep.test {
|
||||
val initialItem = awaitItem()
|
||||
assertThat(initialItem).isEqualTo(LinkDesktopStep.Uninitialized)
|
||||
backgroundScope.launch {
|
||||
sut.handleScannedQrCode(QR_CODE_DATA)
|
||||
}
|
||||
runCurrent()
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState).isInstanceOf(LinkDesktopStep.Error::class.java)
|
||||
val errorType = (errorState as LinkDesktopStep.Error).errorType
|
||||
assertThat(errorType).isInstanceOf(ErrorType.InvalidCheckCode::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createRustLinkDesktopHandler(
|
||||
handler: FakeFfiGrantLoginWithQrCodeHandler = FakeFfiGrantLoginWithQrCodeHandler(),
|
||||
) = RustLinkDesktopHandler(
|
||||
inner = handler,
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
sessionDispatcher = StandardTestDispatcher(testScheduler),
|
||||
qrCodeDataParser = FakeQrCodeDataParser(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright (c) 2025 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.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.libraries.matrix.impl.linknewdevice
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.linknewdevice.ErrorType
|
||||
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiCheckCodeSender
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiGrantLoginWithQrCodeHandler
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiQrCodeData
|
||||
import io.element.android.libraries.matrix.test.QR_CODE_DATA_RECIPROCATE
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.GrantGeneratedQrLoginProgress
|
||||
import org.matrix.rustcomponents.sdk.HumanQrGrantLoginException
|
||||
|
||||
class RustLinkMobileHandlerTest {
|
||||
@Test
|
||||
fun `start function works as expected`() = runTest {
|
||||
val handler = FakeFfiGrantLoginWithQrCodeHandler()
|
||||
val sut = createRustLinkMobileHandler(
|
||||
handler,
|
||||
)
|
||||
sut.linkMobileStep.test {
|
||||
val initialItem = awaitItem()
|
||||
assertThat(initialItem).isEqualTo(LinkMobileStep.Uninitialized)
|
||||
backgroundScope.launch {
|
||||
sut.start()
|
||||
}
|
||||
runCurrent()
|
||||
// progress from the handler is mapped and emitted
|
||||
listOf(
|
||||
GrantGeneratedQrLoginProgress.Starting to LinkMobileStep.Starting::class.java,
|
||||
GrantGeneratedQrLoginProgress.SyncingSecrets to LinkMobileStep.SyncingSecrets::class.java,
|
||||
GrantGeneratedQrLoginProgress.WaitingForAuth("aVerificationUri")
|
||||
to LinkMobileStep.WaitingForAuth::class.java,
|
||||
GrantGeneratedQrLoginProgress.QrScanned(FakeFfiCheckCodeSender())
|
||||
to LinkMobileStep.QrScanned::class.java,
|
||||
GrantGeneratedQrLoginProgress.QrReady(FakeFfiQrCodeData(toBytesResult = { QR_CODE_DATA_RECIPROCATE }))
|
||||
to LinkMobileStep.QrReady::class.java,
|
||||
GrantGeneratedQrLoginProgress.Done to LinkMobileStep.Done::class.java,
|
||||
).forEach { (progress, expectedStepClass) ->
|
||||
handler.emitGenerateProgress(progress)
|
||||
assertThat(awaitItem()).isInstanceOf(expectedStepClass)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when start throws HumanQrGrantLoginException, the handler emits error step`() = runTest {
|
||||
val handler = FakeFfiGrantLoginWithQrCodeHandler(
|
||||
generateResult = { throw HumanQrGrantLoginException.NotFound("Timeout") }
|
||||
)
|
||||
val sut = createRustLinkMobileHandler(
|
||||
handler,
|
||||
)
|
||||
sut.linkMobileStep.test {
|
||||
val initialItem = awaitItem()
|
||||
assertThat(initialItem).isEqualTo(LinkMobileStep.Uninitialized)
|
||||
backgroundScope.launch {
|
||||
sut.start()
|
||||
}
|
||||
runCurrent()
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState).isInstanceOf(LinkMobileStep.Error::class.java)
|
||||
val errorType = (errorState as LinkMobileStep.Error).errorType
|
||||
assertThat(errorType).isInstanceOf(ErrorType.NotFound::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createRustLinkMobileHandler(
|
||||
handler: FakeFfiGrantLoginWithQrCodeHandler = FakeFfiGrantLoginWithQrCodeHandler(),
|
||||
) = RustLinkMobileHandler(
|
||||
inner = handler,
|
||||
sessionCoroutineScope = backgroundScope,
|
||||
sessionDispatcher = StandardTestDispatcher(testScheduler),
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue