Check homeserver when login using qr code (#4708)
* Login with Qr code: check homeserver validity * QrCode login, unauthorized homeserver: update copy. * Update screenshots * Add unit test on SdkQrCodeLoginData * Remove default param value. * Remember imageAnalysis --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
cf02ac7eeb
commit
98cfddce3f
22 changed files with 208 additions and 30 deletions
|
|
@ -21,8 +21,10 @@ open class AccountProviderProvider : PreviewParameterProvider<AccountProvider> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun anAccountProvider() = AccountProvider(
|
fun anAccountProvider(
|
||||||
url = AuthenticationConfig.MATRIX_ORG_URL,
|
url: String = AuthenticationConfig.MATRIX_ORG_URL,
|
||||||
|
) = AccountProvider(
|
||||||
|
url = url,
|
||||||
subtitle = "Matrix.org is an open network for secure, decentralized communication.",
|
subtitle = "Matrix.org is an open network for secure, decentralized communication.",
|
||||||
isPublic = true,
|
isPublic = true,
|
||||||
isMatrixOrg = true,
|
isMatrixOrg = true,
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,10 @@ class ChangeServerPresenter @Inject constructor(
|
||||||
) = launch {
|
) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
if (enterpriseService.isAllowedToConnectToHomeserver(data.url).not()) {
|
if (enterpriseService.isAllowedToConnectToHomeserver(data.url).not()) {
|
||||||
throw UnauthorizedAccountProviderException(data)
|
throw UnauthorizedAccountProviderException(
|
||||||
|
unauthorisedAccountProviderTitle = data.title,
|
||||||
|
authorisedAccountProviderTitles = listOfNotNull(enterpriseService.defaultHomeserver())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
authenticationService.setHomeserver(data.url).map {
|
authenticationService.setHomeserver(data.url).map {
|
||||||
authenticationService.getHomeserverDetails().value!!
|
authenticationService.getHomeserverDetails().value!!
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
package io.element.android.features.login.impl.changeserver
|
package io.element.android.features.login.impl.changeserver
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
|
||||||
import io.element.android.features.login.impl.error.ChangeServerError
|
import io.element.android.features.login.impl.error.ChangeServerError
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
@ -19,7 +18,14 @@ open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerStat
|
||||||
aChangeServerState(),
|
aChangeServerState(),
|
||||||
aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.Error(CommonStrings.error_unknown))),
|
aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.Error(CommonStrings.error_unknown))),
|
||||||
aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.SlidingSyncAlert)),
|
aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.SlidingSyncAlert)),
|
||||||
aChangeServerState(changeServerAction = AsyncData.Failure(ChangeServerError.UnauthorizedAccountProvider(anAccountProvider()))),
|
aChangeServerState(
|
||||||
|
changeServerAction = AsyncData.Failure(
|
||||||
|
ChangeServerError.UnauthorizedAccountProvider(
|
||||||
|
unauthorisedAccountProviderTitle = "example.com",
|
||||||
|
authorisedAccountProviderTitles = listOf("element.io", "element.org"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ fun ChangeServerView(
|
||||||
content = stringResource(
|
content = stringResource(
|
||||||
id = R.string.screen_change_server_error_unauthorized_homeserver,
|
id = R.string.screen_change_server_error_unauthorized_homeserver,
|
||||||
LocalBuildMeta.current.applicationName,
|
LocalBuildMeta.current.applicationName,
|
||||||
error.accountProvider.title,
|
error.unauthorisedAccountProviderTitle,
|
||||||
),
|
),
|
||||||
onSubmit = {
|
onSubmit = {
|
||||||
eventSink.invoke(ChangeServerEvents.ClearError)
|
eventSink.invoke(ChangeServerEvents.ClearError)
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.changeserver
|
package io.element.android.features.login.impl.changeserver
|
||||||
|
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
|
||||||
|
|
||||||
class UnauthorizedAccountProviderException(
|
class UnauthorizedAccountProviderException(
|
||||||
val accountProvider: AccountProvider,
|
val unauthorisedAccountProviderTitle: String,
|
||||||
|
val authorisedAccountProviderTitles: List<String>,
|
||||||
) : Exception()
|
) : Exception()
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
|
||||||
import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException
|
import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException
|
||||||
import io.element.android.libraries.matrix.api.auth.AuthenticationException
|
import io.element.android.libraries.matrix.api.auth.AuthenticationException
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
@ -26,7 +25,8 @@ sealed class ChangeServerError : Throwable() {
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UnauthorizedAccountProvider(
|
data class UnauthorizedAccountProvider(
|
||||||
val accountProvider: AccountProvider,
|
val unauthorisedAccountProviderTitle: String,
|
||||||
|
val authorisedAccountProviderTitles: List<String>,
|
||||||
) : ChangeServerError()
|
) : ChangeServerError()
|
||||||
|
|
||||||
data object SlidingSyncAlert : ChangeServerError()
|
data object SlidingSyncAlert : ChangeServerError()
|
||||||
|
|
@ -35,7 +35,10 @@ sealed class ChangeServerError : Throwable() {
|
||||||
fun from(error: Throwable): ChangeServerError = when (error) {
|
fun from(error: Throwable): ChangeServerError = when (error) {
|
||||||
is AuthenticationException.SlidingSyncVersion -> SlidingSyncAlert
|
is AuthenticationException.SlidingSyncVersion -> SlidingSyncAlert
|
||||||
is AuthenticationException.Oidc -> Error(messageStr = error.message)
|
is AuthenticationException.Oidc -> Error(messageStr = error.message)
|
||||||
is UnauthorizedAccountProviderException -> UnauthorizedAccountProvider(error.accountProvider)
|
is UnauthorizedAccountProviderException -> UnauthorizedAccountProvider(
|
||||||
|
unauthorisedAccountProviderTitle = error.unauthorisedAccountProviderTitle,
|
||||||
|
authorisedAccountProviderTitles = error.authorisedAccountProviderTitles,
|
||||||
|
)
|
||||||
else -> Error(messageId = R.string.screen_change_server_error_invalid_homeserver)
|
else -> Error(messageId = R.string.screen_change_server_error_invalid_homeserver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
|
import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException
|
||||||
import io.element.android.features.login.impl.qrcode.QrCodeLoginManager
|
import io.element.android.features.login.impl.qrcode.QrCodeLoginManager
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -36,6 +38,7 @@ class QrCodeScanPresenter @Inject constructor(
|
||||||
private val qrCodeLoginDataFactory: MatrixQrCodeLoginDataFactory,
|
private val qrCodeLoginDataFactory: MatrixQrCodeLoginDataFactory,
|
||||||
private val qrCodeLoginManager: QrCodeLoginManager,
|
private val qrCodeLoginManager: QrCodeLoginManager,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
private val enterpriseService: EnterpriseService,
|
||||||
) : Presenter<QrCodeScanState> {
|
) : Presenter<QrCodeScanState> {
|
||||||
private var isScanning by mutableStateOf(true)
|
private var isScanning by mutableStateOf(true)
|
||||||
|
|
||||||
|
|
@ -90,9 +93,17 @@ class QrCodeScanPresenter @Inject constructor(
|
||||||
|
|
||||||
launch(coroutineDispatchers.computation) {
|
launch(coroutineDispatchers.computation) {
|
||||||
suspend {
|
suspend {
|
||||||
qrCodeLoginDataFactory.parseQrCodeData(code).onFailure {
|
val data = qrCodeLoginDataFactory.parseQrCodeData(code).onFailure {
|
||||||
Timber.e(it, "Error parsing QR code data")
|
Timber.e(it, "Error parsing QR code data")
|
||||||
}.getOrThrow()
|
}.getOrThrow()
|
||||||
|
val serverName = data.serverName()
|
||||||
|
if (serverName != null && enterpriseService.isAllowedToConnectToHomeserver(serverName).not()) {
|
||||||
|
throw UnauthorizedAccountProviderException(
|
||||||
|
unauthorisedAccountProviderTitle = serverName,
|
||||||
|
authorisedAccountProviderTitles = listOfNotNull(enterpriseService.defaultHomeserver())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
data
|
||||||
}.runCatchingUpdatingState(codeScannedAction)
|
}.runCatchingUpdatingState(codeScannedAction)
|
||||||
}.invokeOnCompletion {
|
}.invokeOnCompletion {
|
||||||
isProcessingCode.set(false)
|
isProcessingCode.set(false)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.features.login.impl.screens.qrcode.scan
|
package io.element.android.features.login.impl.screens.qrcode.scan
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
||||||
|
|
@ -19,6 +20,15 @@ open class QrCodeScanStateProvider : PreviewParameterProvider<QrCodeScanState> {
|
||||||
aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Loading),
|
aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Loading),
|
||||||
aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Failure(Exception("Error"))),
|
aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Failure(Exception("Error"))),
|
||||||
aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Failure(QrLoginException.OtherDeviceNotSignedIn)),
|
aQrCodeScanState(isScanning = false, authenticationAction = AsyncAction.Failure(QrLoginException.OtherDeviceNotSignedIn)),
|
||||||
|
aQrCodeScanState(
|
||||||
|
isScanning = false,
|
||||||
|
authenticationAction = AsyncAction.Failure(
|
||||||
|
UnauthorizedAccountProviderException(
|
||||||
|
unauthorisedAccountProviderTitle = "example.com",
|
||||||
|
authorisedAccountProviderTitles = listOf("element.io", "element.org"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
// Add other state here
|
// Add other state here
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
|
import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
||||||
import io.element.android.libraries.designsystem.components.BigIcon
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
|
|
@ -144,6 +145,12 @@ private fun ColumnScope.Buttons(
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = when (error) {
|
text = when (error) {
|
||||||
|
is UnauthorizedAccountProviderException -> {
|
||||||
|
stringResource(
|
||||||
|
id = R.string.screen_change_server_error_unauthorized_homeserver_title,
|
||||||
|
error.unauthorisedAccountProviderTitle,
|
||||||
|
)
|
||||||
|
}
|
||||||
is QrLoginException.OtherDeviceNotSignedIn -> {
|
is QrLoginException.OtherDeviceNotSignedIn -> {
|
||||||
stringResource(R.string.screen_qr_code_login_device_not_signed_in_scan_state_subtitle)
|
stringResource(R.string.screen_qr_code_login_device_not_signed_in_scan_state_subtitle)
|
||||||
}
|
}
|
||||||
|
|
@ -156,6 +163,12 @@ private fun ColumnScope.Buttons(
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = when (error) {
|
text = when (error) {
|
||||||
|
is UnauthorizedAccountProviderException -> {
|
||||||
|
stringResource(
|
||||||
|
id = R.string.screen_change_server_error_unauthorized_homeserver_content,
|
||||||
|
error.authorisedAccountProviderTitles.joinToString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
is QrLoginException.OtherDeviceNotSignedIn -> {
|
is QrLoginException.OtherDeviceNotSignedIn -> {
|
||||||
stringResource(R.string.screen_qr_code_login_device_not_signed_in_scan_state_description)
|
stringResource(R.string.screen_qr_code_login_device_not_signed_in_scan_state_description)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@
|
||||||
%1$s"</string>
|
%1$s"</string>
|
||||||
<string name="screen_change_server_error_no_sliding_sync_message">"The selected account provider does not support sliding sync. An upgrade to the server is needed to use %1$s."</string>
|
<string name="screen_change_server_error_no_sliding_sync_message">"The selected account provider does not support sliding sync. An upgrade to the server is needed to use %1$s."</string>
|
||||||
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s is not allowed to connect to %2$s."</string>
|
<string name="screen_change_server_error_unauthorized_homeserver">"%1$s is not allowed to connect to %2$s."</string>
|
||||||
|
<string name="screen_change_server_error_unauthorized_homeserver_content">"This app has been configured to allow: %1$s."</string>
|
||||||
|
<string name="screen_change_server_error_unauthorized_homeserver_title">"Account provider %1$s not allowed."</string>
|
||||||
<string name="screen_change_server_form_header">"Homeserver URL"</string>
|
<string name="screen_change_server_form_header">"Homeserver URL"</string>
|
||||||
<string name="screen_change_server_form_notice">"Enter a domain address."</string>
|
<string name="screen_change_server_form_notice">"Enter a domain address."</string>
|
||||||
<string name="screen_change_server_subtitle">"What is the address of your server?"</string>
|
<string name="screen_change_server_subtitle">"What is the address of your server?"</string>
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ class ChangeServerPresenterTest {
|
||||||
createPresenter(
|
createPresenter(
|
||||||
enterpriseService = FakeEnterpriseService(
|
enterpriseService = FakeEnterpriseService(
|
||||||
isAllowedToConnectToHomeserverResult = isAllowedToConnectToHomeserverResult,
|
isAllowedToConnectToHomeserverResult = isAllowedToConnectToHomeserverResult,
|
||||||
|
defaultHomeserverResult = { "element.io" },
|
||||||
),
|
),
|
||||||
).test {
|
).test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -94,8 +95,11 @@ class ChangeServerPresenterTest {
|
||||||
assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java)
|
assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java)
|
||||||
val failureState = awaitItem()
|
val failureState = awaitItem()
|
||||||
assertThat(
|
assertThat(
|
||||||
(failureState.changeServerAction.errorOrNull() as ChangeServerError.UnauthorizedAccountProvider).accountProvider
|
(failureState.changeServerAction.errorOrNull() as ChangeServerError.UnauthorizedAccountProvider).unauthorisedAccountProviderTitle
|
||||||
).isEqualTo(anAccountProvider)
|
).isEqualTo(anAccountProvider.title)
|
||||||
|
assertThat(
|
||||||
|
(failureState.changeServerAction.errorOrNull() as ChangeServerError.UnauthorizedAccountProvider).authorisedAccountProviderTitles
|
||||||
|
).containsExactly("element.io")
|
||||||
isAllowedToConnectToHomeserverResult.assertions()
|
isAllowedToConnectToHomeserverResult.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value(A_HOMESERVER_URL))
|
.with(value(A_HOMESERVER_URL))
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,17 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
|
import io.element.android.features.login.impl.changeserver.UnauthorizedAccountProviderException
|
||||||
import io.element.android.features.login.impl.qrcode.FakeQrCodeLoginManager
|
import io.element.android.features.login.impl.qrcode.FakeQrCodeLoginManager
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
||||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
||||||
|
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData
|
||||||
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginDataFactory
|
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginDataFactory
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
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
|
||||||
|
|
@ -38,10 +43,22 @@ class QrCodeScanPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - scanned QR code successfully`() = runTest {
|
fun `present - scanned QR code successfully`() = runTest {
|
||||||
val presenter = createQrCodeScanPresenter()
|
val qrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory(
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
parseQrCodeLoginDataResult = {
|
||||||
presenter.present()
|
Result.success(
|
||||||
}.test {
|
FakeMatrixQrCodeLoginData(
|
||||||
|
serverNameResult = { "example.com" }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val presenter = createQrCodeScanPresenter(
|
||||||
|
qrCodeLoginDataFactory = qrCodeLoginDataFactory,
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
isAllowedToConnectToHomeserverResult = { true },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(QrCodeScanEvents.QrCodeScanned(byteArrayOf()))
|
initialState.eventSink(QrCodeScanEvents.QrCodeScanned(byteArrayOf()))
|
||||||
assertThat(awaitItem().isScanning).isFalse()
|
assertThat(awaitItem().isScanning).isFalse()
|
||||||
|
|
@ -50,6 +67,38 @@ class QrCodeScanPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - scanned QR code successfully, but homeserver not allowed`() = runTest {
|
||||||
|
val qrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory(
|
||||||
|
parseQrCodeLoginDataResult = {
|
||||||
|
Result.success(
|
||||||
|
FakeMatrixQrCodeLoginData(
|
||||||
|
serverNameResult = { "example.com" }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val presenter = createQrCodeScanPresenter(
|
||||||
|
qrCodeLoginDataFactory = qrCodeLoginDataFactory,
|
||||||
|
enterpriseService = FakeEnterpriseService(
|
||||||
|
isAllowedToConnectToHomeserverResult = { false },
|
||||||
|
defaultHomeserverResult = { "element.io" }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(QrCodeScanEvents.QrCodeScanned(byteArrayOf()))
|
||||||
|
assertThat(awaitItem().isScanning).isFalse()
|
||||||
|
assertThat(awaitItem().authenticationAction.isLoading()).isTrue()
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat((state.authenticationAction.errorOrNull() as UnauthorizedAccountProviderException).unauthorisedAccountProviderTitle)
|
||||||
|
.isEqualTo("example.com")
|
||||||
|
assertThat((state.authenticationAction.errorOrNull() as UnauthorizedAccountProviderException).authorisedAccountProviderTitles)
|
||||||
|
.containsExactly("element.io")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - scanned QR code failed and can be retried`() = runTest {
|
fun `present - scanned QR code failed and can be retried`() = runTest {
|
||||||
val qrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory(
|
val qrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory(
|
||||||
|
|
@ -103,9 +152,11 @@ class QrCodeScanPresenterTest {
|
||||||
qrCodeLoginDataFactory: FakeMatrixQrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory(),
|
qrCodeLoginDataFactory: FakeMatrixQrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory(),
|
||||||
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||||
qrCodeLoginManager: FakeQrCodeLoginManager = FakeQrCodeLoginManager(),
|
qrCodeLoginManager: FakeQrCodeLoginManager = FakeQrCodeLoginManager(),
|
||||||
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
) = QrCodeScanPresenter(
|
) = QrCodeScanPresenter(
|
||||||
qrCodeLoginDataFactory = qrCodeLoginDataFactory,
|
qrCodeLoginDataFactory = qrCodeLoginDataFactory,
|
||||||
qrCodeLoginManager = qrCodeLoginManager,
|
qrCodeLoginManager = qrCodeLoginManager,
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
|
enterpriseService = enterpriseService,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,6 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.auth.qrlogin
|
package io.element.android.libraries.matrix.api.auth.qrlogin
|
||||||
|
|
||||||
interface MatrixQrCodeLoginData
|
interface MatrixQrCodeLoginData {
|
||||||
|
fun serverName(): String?
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,8 @@ import org.matrix.rustcomponents.sdk.QrCodeData as RustQrCodeData
|
||||||
|
|
||||||
class SdkQrCodeLoginData(
|
class SdkQrCodeLoginData(
|
||||||
internal val rustQrCodeData: RustQrCodeData,
|
internal val rustQrCodeData: RustQrCodeData,
|
||||||
) : MatrixQrCodeLoginData
|
) : MatrixQrCodeLoginData {
|
||||||
|
override fun serverName(): String? {
|
||||||
|
return rustQrCodeData.serverName()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.impl.auth.qrlogin
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeQrCodeData
|
||||||
|
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SdkQrCodeLoginDataTest {
|
||||||
|
@Test
|
||||||
|
fun `getServer reads the value from the Rust side, null case`() {
|
||||||
|
val sut = SdkQrCodeLoginData(
|
||||||
|
rustQrCodeData = FakeQrCodeData(
|
||||||
|
serverNameResult = { null },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertThat(sut.serverName()).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getServer reads the value from the Rust side`() {
|
||||||
|
val sut = SdkQrCodeLoginData(
|
||||||
|
rustQrCodeData = FakeQrCodeData(
|
||||||
|
serverNameResult = { A_HOMESERVER_URL },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertThat(sut.serverName()).isEqualTo(A_HOMESERVER_URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.impl.fixtures.fakes
|
||||||
|
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
import org.matrix.rustcomponents.sdk.NoPointer
|
||||||
|
import org.matrix.rustcomponents.sdk.QrCodeData
|
||||||
|
|
||||||
|
class FakeQrCodeData(
|
||||||
|
private val serverNameResult: () -> String? = { lambdaError() },
|
||||||
|
) : QrCodeData(NoPointer) {
|
||||||
|
override fun serverName(): String? {
|
||||||
|
return serverNameResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.test.auth.qrlogin
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
|
||||||
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDataFactory
|
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDataFactory
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
|
||||||
class FakeMatrixQrCodeLoginDataFactory(
|
class FakeMatrixQrCodeLoginDataFactory(
|
||||||
|
|
@ -20,4 +21,8 @@ class FakeMatrixQrCodeLoginDataFactory(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FakeMatrixQrCodeLoginData : MatrixQrCodeLoginData
|
class FakeMatrixQrCodeLoginData(
|
||||||
|
private val serverNameResult: () -> String? = { lambdaError() },
|
||||||
|
) : MatrixQrCodeLoginData {
|
||||||
|
override fun serverName() = serverNameResult()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,8 @@ import kotlin.coroutines.suspendCoroutine
|
||||||
@Composable
|
@Composable
|
||||||
fun QrCodeCameraView(
|
fun QrCodeCameraView(
|
||||||
onScanQrCode: (ByteArray) -> Unit,
|
onScanQrCode: (ByteArray) -> Unit,
|
||||||
|
renderPreview: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
renderPreview: Boolean = true,
|
|
||||||
) {
|
) {
|
||||||
if (LocalInspectionMode.current) {
|
if (LocalInspectionMode.current) {
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -62,9 +62,11 @@ fun QrCodeCameraView(
|
||||||
var cameraProvider by remember { mutableStateOf<ProcessCameraProvider?>(null) }
|
var cameraProvider by remember { mutableStateOf<ProcessCameraProvider?>(null) }
|
||||||
val previewUseCase = remember { Preview.Builder().build() }
|
val previewUseCase = remember { Preview.Builder().build() }
|
||||||
var lastFrame by remember { mutableStateOf<Bitmap?>(null) }
|
var lastFrame by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
val imageAnalysis = ImageAnalysis.Builder()
|
val imageAnalysis = remember {
|
||||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
ImageAnalysis.Builder()
|
||||||
.build()
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
cameraProvider = localContext.getCameraProvider()
|
cameraProvider = localContext.getCameraProvider()
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6c1257fbdf388933be8fb2ae211c8abacc4cdb01c2a924a7ba3729337c9b6707
|
oid sha256:a1e37b08acfdf40279b0990dd65c58d38d2248026f0d2fe2a1c396245ea931fd
|
||||||
size 15434
|
size 15892
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:cb9a2d2145791122cc2ab7d903485de361fb6098216bf578dca7cc350e9f94a0
|
oid sha256:8db77f38d81113a5411c40c1ebf811090ce809fd0f0df3d29eab05d3ed0bb64a
|
||||||
size 13650
|
size 14114
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5f90a8ecae26714e76d5536682716a15aefc3d4c8836617add9a0ea946e7d242
|
||||||
|
size 31486
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:34cd9f4558efc143b4ab3bec96342a42cfad6906da0d46e75d78db36b7e59bd6
|
||||||
|
size 30242
|
||||||
Loading…
Add table
Add a link
Reference in a new issue