Add unit test on RustLinkDesktopHandler

Add unit test on RustLinkMobileHandler
Add unit test on DefaultLinkNewDeviceEntryPoint
This commit is contained in:
Benoit Marty 2025-12-16 18:04:26 +01:00 committed by Benoit Marty
parent 86cafb7c05
commit ec10a0bf87
10 changed files with 348 additions and 2 deletions

View file

@ -0,0 +1,48 @@
/*
* 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.features.linknewdevice.impl
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.google.common.truth.Truth.assertThat
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class DefaultLinkNewDeviceEntryPointTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Test
fun `test node creation`() = runTest {
val entryPoint = DefaultLinkNewDeviceEntryPoint()
val client = FakeMatrixClient()
val parentNode = TestParentNode.create { buildContext, plugins ->
LinkNewDeviceFlowNode(
buildContext = buildContext,
plugins = plugins,
sessionCoroutineScope = backgroundScope,
linkNewMobileHandler = LinkNewMobileHandler(client),
linkNewDesktopHandler = LinkNewDesktopHandler(client),
)
}
val callback: LinkNewDeviceEntryPoint.Callback = object : LinkNewDeviceEntryPoint.Callback {
override fun onDone() = lambdaError()
}
val result = entryPoint.createNode(parentNode, BuildContext.root(null), callback)
assertThat(result).isInstanceOf(LinkNewDeviceFlowNode::class.java)
}
}

View file

@ -49,6 +49,7 @@ 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
@ -754,6 +755,7 @@ class RustMatrixClient(
inner = handler,
sessionCoroutineScope = sessionCoroutineScope,
sessionDispatcher = sessionDispatcher,
qrCodeDataParser = RustQrCodeDataParser(),
)
}
}

View file

@ -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)
}
}

View file

@ -23,7 +23,6 @@ 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.QrCodeData
import org.matrix.rustcomponents.sdk.QrCodeDecodeException
import timber.log.Timber
@ -33,6 +32,7 @@ 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()
@ -41,7 +41,7 @@ class RustLinkDesktopHandler(
Timber.tag(tag.value).d("Emit Uninitialized")
_linkDesktopStep.emit(LinkDesktopStep.Uninitialized)
try {
val qrCodeData = QrCodeData.fromBytes(data)
val qrCodeData = qrCodeDataParser.parse(data)
inner.scan(
qrCodeData = qrCodeData,
progressListener = object : GrantQrLoginProgressListener {

View file

@ -0,0 +1,13 @@
/*
* 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.CheckCodeSender
import org.matrix.rustcomponents.sdk.NoHandle
class FakeFfiCheckCodeSender : CheckCodeSender(NoHandle)

View file

@ -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)
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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(),
)
}

View file

@ -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),
)
}