Browse Source

Improve APIs, add tests

pull/3298/head
Jorge Martín 1 month ago
parent
commit
7fd0ad09dc
  1. 5
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt
  2. 12
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt
  3. 6
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordEvent.kt
  4. 12
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt
  5. 22
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenter.kt
  6. 4
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordState.kt
  7. 14
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt
  8. 6
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootEvent.kt
  9. 6
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt
  10. 12
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenter.kt
  11. 4
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootState.kt
  12. 8
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootStateProvider.kt
  13. 14
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt
  14. 119
      features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTests.kt
  15. 96
      features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenterTest.kt
  16. 97
      features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt
  17. 65
      features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTests.kt
  18. 107
      features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTests.kt
  19. 4
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt
  20. 8
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
  21. 10
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt
  22. 7
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt

5
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt

@ -19,7 +19,6 @@ package io.element.android.features.securebackup.impl.reset @@ -19,7 +19,6 @@ package io.element.android.features.securebackup.impl.reset
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
@ -46,10 +45,6 @@ class ResetIdentityFlowManager @Inject constructor( @@ -46,10 +45,6 @@ class ResetIdentityFlowManager @Inject constructor(
}
}
fun currentSessionId(): SessionId {
return matrixClient.sessionId
}
fun getResetHandle(): StateFlow<AsyncData<IdentityResetHandle>> {
return if (resetHandleFlow.value.isLoading() || resetHandleFlow.value.isSuccess()) {
resetHandleFlow

12
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt

@ -34,8 +34,8 @@ import com.bumble.appyx.navmodel.backstack.operation.push @@ -34,8 +34,8 @@ import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.securebackup.impl.reset.password.ResetKeyPasswordNode
import io.element.android.features.securebackup.impl.reset.root.ResetKeyRootNode
import io.element.android.features.securebackup.impl.reset.password.ResetIdentityPasswordNode
import io.element.android.features.securebackup.impl.reset.root.ResetIdentityRootNode
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.BackstackView
@ -108,18 +108,18 @@ class ResetIdentityFlowNode @AssistedInject constructor( @@ -108,18 +108,18 @@ class ResetIdentityFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
is NavTarget.Root -> {
val callback = object : ResetKeyRootNode.Callback {
val callback = object : ResetIdentityRootNode.Callback {
override fun onContinue() {
coroutineScope.startReset()
}
}
createNode<ResetKeyRootNode>(buildContext, listOf(callback))
createNode<ResetIdentityRootNode>(buildContext, listOf(callback))
}
is NavTarget.ResetPassword -> {
val handle = resetIdentityFlowManager.currentHandleFlow.value.dataOrNull() as? IdentityPasswordResetHandle ?: error("No password handle found")
createNode<ResetKeyPasswordNode>(
createNode<ResetIdentityPasswordNode>(
buildContext,
listOf(ResetKeyPasswordNode.Inputs(resetIdentityFlowManager.currentSessionId(), handle))
listOf(ResetIdentityPasswordNode.Inputs(handle))
)
}
is NavTarget.ResetOidc -> {

6
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordEvent.kt → features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordEvent.kt

@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
package io.element.android.features.securebackup.impl.reset.password
sealed interface ResetKeyPasswordEvent {
data class Reset(val password: String) : ResetKeyPasswordEvent
data object DismissError : ResetKeyPasswordEvent
sealed interface ResetIdentityPasswordEvent {
data class Reset(val password: String) : ResetIdentityPasswordEvent
data object DismissError : ResetIdentityPasswordEvent
}

12
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordNode.kt → features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordNode.kt

@ -21,33 +21,33 @@ import androidx.compose.ui.Modifier @@ -21,33 +21,33 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
@ContributesNode(SessionScope::class)
class ResetKeyPasswordNode @AssistedInject constructor(
class ResetIdentityPasswordNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val coroutineDispatchers: CoroutineDispatchers,
) : Node(buildContext, plugins = plugins) {
data class Inputs(val userId: UserId, val handle: IdentityPasswordResetHandle) : NodeInputs
data class Inputs(val handle: IdentityPasswordResetHandle) : NodeInputs
private val presenter by lazy {
val inputs = inputs<Inputs>()
ResetKeyPasswordPresenter(inputs.userId, inputs.handle)
ResetIdentityPasswordPresenter(inputs.handle, dispatchers = coroutineDispatchers)
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
ResetKeyPasswordView(
ResetIdentityPasswordView(
state = state,
onBack = ::navigateUp
)

22
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordPresenter.kt → features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenter.kt

@ -24,37 +24,37 @@ import androidx.compose.runtime.rememberCoroutineScope @@ -24,37 +24,37 @@ import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class ResetKeyPasswordPresenter(
private val userId: UserId,
class ResetIdentityPasswordPresenter(
private val identityPasswordResetHandle: IdentityPasswordResetHandle,
) : Presenter<ResetKeyPasswordState> {
private val dispatchers: CoroutineDispatchers,
) : Presenter<ResetIdentityPasswordState> {
@Composable
override fun present(): ResetKeyPasswordState {
override fun present(): ResetIdentityPasswordState {
val coroutineScope = rememberCoroutineScope()
val resetAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
fun handleEvent(event: ResetKeyPasswordEvent) {
fun handleEvent(event: ResetIdentityPasswordEvent) {
when (event) {
is ResetKeyPasswordEvent.Reset -> coroutineScope.reset(userId, event.password, resetAction)
ResetKeyPasswordEvent.DismissError -> resetAction.value = AsyncAction.Uninitialized
is ResetIdentityPasswordEvent.Reset -> coroutineScope.reset(event.password, resetAction)
ResetIdentityPasswordEvent.DismissError -> resetAction.value = AsyncAction.Uninitialized
}
}
return ResetKeyPasswordState(
return ResetIdentityPasswordState(
resetAction = resetAction.value,
eventSink = ::handleEvent
)
}
private fun CoroutineScope.reset(userId: UserId, password: String, action: MutableState<AsyncAction<Unit>>) = launch {
private fun CoroutineScope.reset(password: String, action: MutableState<AsyncAction<Unit>>) = launch(dispatchers.io) {
suspend {
identityPasswordResetHandle.resetPassword(userId, password).getOrThrow()
identityPasswordResetHandle.resetPassword(password).getOrThrow()
}.runCatchingUpdatingState(action)
}
}

4
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordState.kt → features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordState.kt

@ -18,7 +18,7 @@ package io.element.android.features.securebackup.impl.reset.password @@ -18,7 +18,7 @@ package io.element.android.features.securebackup.impl.reset.password
import io.element.android.libraries.architecture.AsyncAction
data class ResetKeyPasswordState(
data class ResetIdentityPasswordState(
val resetAction: AsyncAction<Unit>,
val eventSink: (ResetKeyPasswordEvent) -> Unit,
val eventSink: (ResetIdentityPasswordEvent) -> Unit,
)

14
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetKeyPasswordView.kt → features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt

@ -46,8 +46,8 @@ import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKe @@ -46,8 +46,8 @@ import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKe
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun ResetKeyPasswordView(
state: ResetKeyPasswordState,
fun ResetIdentityPasswordView(
state: ResetIdentityPasswordState,
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -63,7 +63,7 @@ fun ResetKeyPasswordView( @@ -63,7 +63,7 @@ fun ResetKeyPasswordView(
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_reset_identity),
onClick = { state.eventSink(ResetKeyPasswordEvent.Reset(passwordState.value)) },
onClick = { state.eventSink(ResetIdentityPasswordEvent.Reset(passwordState.value)) },
destructive = true,
)
}
@ -74,7 +74,7 @@ fun ResetKeyPasswordView( @@ -74,7 +74,7 @@ fun ResetKeyPasswordView(
} else if (state.resetAction.isFailure()) {
ErrorDialog(
content = stringResource(CommonStrings.error_unknown),
onDismiss = { state.eventSink(ResetKeyPasswordEvent.DismissError) }
onDismiss = { state.eventSink(ResetIdentityPasswordEvent.DismissError) }
)
}
}
@ -107,10 +107,10 @@ private fun Content(textFieldState: MutableState<String>) { @@ -107,10 +107,10 @@ private fun Content(textFieldState: MutableState<String>) {
@PreviewsDayNight
@Composable
internal fun ResetKeyPasswordViewPreview() {
internal fun ResetIdentityPasswordViewPreview() {
ElementPreview {
ResetKeyPasswordView(
state = ResetKeyPasswordState(
ResetIdentityPasswordView(
state = ResetIdentityPasswordState(
resetAction = AsyncAction.Uninitialized,
eventSink = {}
),

6
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootEvent.kt → features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootEvent.kt

@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
package io.element.android.features.securebackup.impl.reset.root
sealed interface ResetKeyRootEvent {
data object Continue : ResetKeyRootEvent
data object DismissDialog : ResetKeyRootEvent
sealed interface ResetIdentityRootEvent {
data object Continue : ResetIdentityRootEvent
data object DismissDialog : ResetIdentityRootEvent
}

6
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootNode.kt → features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootNode.kt

@ -27,7 +27,7 @@ import io.element.android.anvilannotations.ContributesNode @@ -27,7 +27,7 @@ import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
class ResetKeyRootNode @AssistedInject constructor(
class ResetIdentityRootNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
) : Node(buildContext, plugins = plugins) {
@ -35,13 +35,13 @@ class ResetKeyRootNode @AssistedInject constructor( @@ -35,13 +35,13 @@ class ResetKeyRootNode @AssistedInject constructor(
fun onContinue()
}
private val presenter = ResetKeyRootPresenter()
private val presenter = ResetIdentityRootPresenter()
private val callback: Callback = plugins.filterIsInstance<Callback>().first()
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
ResetKeyRootView(
ResetIdentityRootView(
state = state,
onContinue = callback::onContinue,
onBack = ::navigateUp,

12
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootPresenter.kt → features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenter.kt

@ -23,19 +23,19 @@ import androidx.compose.runtime.remember @@ -23,19 +23,19 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import io.element.android.libraries.architecture.Presenter
class ResetKeyRootPresenter : Presenter<ResetKeyRootState> {
class ResetIdentityRootPresenter : Presenter<ResetIdentityRootState> {
@Composable
override fun present(): ResetKeyRootState {
override fun present(): ResetIdentityRootState {
var displayConfirmDialog by remember { mutableStateOf(false) }
fun handleEvent(event: ResetKeyRootEvent) {
fun handleEvent(event: ResetIdentityRootEvent) {
displayConfirmDialog = when (event) {
ResetKeyRootEvent.Continue -> true
ResetKeyRootEvent.DismissDialog -> false
ResetIdentityRootEvent.Continue -> true
ResetIdentityRootEvent.DismissDialog -> false
}
}
return ResetKeyRootState(
return ResetIdentityRootState(
displayConfirmationDialog = displayConfirmDialog,
eventSink = ::handleEvent
)

4
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootState.kt → features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootState.kt

@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
package io.element.android.features.securebackup.impl.reset.root
data class ResetKeyRootState(
data class ResetIdentityRootState(
val displayConfirmationDialog: Boolean,
val eventSink: (ResetKeyRootEvent) -> Unit,
val eventSink: (ResetIdentityRootEvent) -> Unit,
)

8
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootStateProvider.kt → features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootStateProvider.kt

@ -18,14 +18,14 @@ package io.element.android.features.securebackup.impl.reset.root @@ -18,14 +18,14 @@ package io.element.android.features.securebackup.impl.reset.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
class ResetKeyRootStateProvider : PreviewParameterProvider<ResetKeyRootState> {
override val values: Sequence<ResetKeyRootState>
class ResetIdentityRootStateProvider : PreviewParameterProvider<ResetIdentityRootState> {
override val values: Sequence<ResetIdentityRootState>
get() = sequenceOf(
ResetKeyRootState(
ResetIdentityRootState(
displayConfirmationDialog = false,
eventSink = {}
),
ResetKeyRootState(
ResetIdentityRootState(
displayConfirmationDialog = true,
eventSink = {}
)

14
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetKeyRootView.kt → features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt

@ -43,8 +43,8 @@ import io.element.android.libraries.ui.strings.CommonStrings @@ -43,8 +43,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.persistentListOf
@Composable
fun ResetKeyRootView(
state: ResetKeyRootState,
fun ResetIdentityRootView(
state: ResetIdentityRootState,
onContinue: () -> Unit,
onBack: () -> Unit,
) {
@ -58,7 +58,7 @@ fun ResetKeyRootView( @@ -58,7 +58,7 @@ fun ResetKeyRootView(
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = CommonStrings.action_continue),
onClick = { state.eventSink(ResetKeyRootEvent.Continue) },
onClick = { state.eventSink(ResetIdentityRootEvent.Continue) },
destructive = true,
)
},
@ -71,11 +71,11 @@ fun ResetKeyRootView( @@ -71,11 +71,11 @@ fun ResetKeyRootView(
content = stringResource(CommonStrings.screen_reset_encryption_confirmation_alert_subtitle),
submitText = stringResource(CommonStrings.screen_reset_encryption_confirmation_alert_action),
onSubmitClick = {
state.eventSink(ResetKeyRootEvent.DismissDialog)
state.eventSink(ResetIdentityRootEvent.DismissDialog)
onContinue()
},
destructiveSubmit = true,
onDismiss = { state.eventSink(ResetKeyRootEvent.DismissDialog) }
onDismiss = { state.eventSink(ResetIdentityRootEvent.DismissDialog) }
)
}
}
@ -138,9 +138,9 @@ private fun Content() { @@ -138,9 +138,9 @@ private fun Content() {
@PreviewsDayNight
@Composable
internal fun ResetKeyRootViewPreview(@PreviewParameter(ResetKeyRootStateProvider::class) state: ResetKeyRootState) {
internal fun ResetIdentityRootViewPreview(@PreviewParameter(ResetIdentityRootStateProvider::class) state: ResetIdentityRootState) {
ElementPreview {
ResetKeyRootView(
ResetIdentityRootView(
state = state,
onContinue = {},
onBack = {},

119
features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManagerTests.kt

@ -0,0 +1,119 @@ @@ -0,0 +1,119 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.securebackup.impl.reset
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.encryption.FakeIdentityPasswordResetHandle
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
class ResetIdentityFlowManagerTests {
@Test
fun `getResetHandle - emits a reset handle`() = runTest {
val startResetLambda = lambdaRecorder<Result<IdentityResetHandle?>> { Result.success(FakeIdentityPasswordResetHandle()) }
val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda)
val flowManager = createFlowManager(encryptionService = encryptionService)
flowManager.getResetHandle().test {
assertThat(awaitItem().isLoading()).isTrue()
assertThat(awaitItem().isSuccess()).isTrue()
startResetLambda.assertions().isCalledOnce()
}
}
@Test
fun `getResetHandle - om successful handle retrieval returns that same handle`() = runTest {
val startResetLambda = lambdaRecorder<Result<IdentityResetHandle?>> { Result.success(FakeIdentityPasswordResetHandle()) }
val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda)
val flowManager = createFlowManager(encryptionService = encryptionService)
var result: AsyncData.Success<IdentityResetHandle>? = null
flowManager.getResetHandle().test {
assertThat(awaitItem().isLoading()).isTrue()
result = awaitItem() as? AsyncData.Success<IdentityResetHandle>
assertThat(result).isNotNull()
}
flowManager.getResetHandle().test {
assertThat(awaitItem()).isSameInstanceAs(result)
}
}
@Test
fun `getResetHandle - will fail if it receives a null reset handle`() = runTest {
val startResetLambda = lambdaRecorder<Result<IdentityResetHandle?>> { Result.success(null) }
val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda)
val flowManager = createFlowManager(encryptionService = encryptionService)
flowManager.getResetHandle().test {
assertThat(awaitItem().isLoading()).isTrue()
assertThat(awaitItem().isFailure()).isTrue()
startResetLambda.assertions().isCalledOnce()
}
}
@Test
fun `getResetHandle - fails gracefully when receiving an exception from the encryption service`() = runTest {
val startResetLambda = lambdaRecorder<Result<IdentityResetHandle?>> { Result.failure(IllegalStateException("Failure")) }
val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda)
val flowManager = createFlowManager(encryptionService = encryptionService)
flowManager.getResetHandle().test {
assertThat(awaitItem().isLoading()).isTrue()
assertThat(awaitItem().isFailure()).isTrue()
startResetLambda.assertions().isCalledOnce()
}
}
@Test
fun `cancel - resets the state and calls cancel on the reset handle`() = runTest {
val cancelLambda = lambdaRecorder<Unit> { }
val resetHandle = FakeIdentityPasswordResetHandle(cancelLambda = cancelLambda)
val startResetLambda = lambdaRecorder<Result<IdentityResetHandle?>> { Result.success(resetHandle) }
val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda)
val flowManager = createFlowManager(encryptionService = encryptionService)
flowManager.getResetHandle().test {
assertThat(awaitItem().isLoading()).isTrue()
assertThat(awaitItem().isSuccess()).isTrue()
flowManager.cancel()
cancelLambda.assertions().isCalledOnce()
assertThat(awaitItem().isUninitialized()).isTrue()
}
}
private fun TestScope.createFlowManager(
encryptionService: FakeEncryptionService = FakeEncryptionService(),
client: FakeMatrixClient = FakeMatrixClient(encryptionService = encryptionService),
sessionCoroutineScope: CoroutineScope = this,
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
) = ResetIdentityFlowManager(
matrixClient = client,
sessionCoroutineScope = sessionCoroutineScope,
sessionVerificationService = sessionVerificationService,
)
}

96
features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordPresenterTest.kt

@ -0,0 +1,96 @@ @@ -0,0 +1,96 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.securebackup.impl.reset.password
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.encryption.FakeIdentityPasswordResetHandle
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
class ResetIdentityPasswordPresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.resetAction.isUninitialized()).isTrue()
}
}
@Test
fun `present - Reset event succeeds`() = runTest {
val resetLambda = lambdaRecorder<String, Result<Unit>> { _ -> Result.success(Unit) }
val resetHandle = FakeIdentityPasswordResetHandle(resetPasswordLambda = resetLambda)
val presenter = createPresenter(identityResetHandle = resetHandle)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(ResetIdentityPasswordEvent.Reset("password"))
assertThat(awaitItem().resetAction.isLoading()).isTrue()
assertThat(awaitItem().resetAction.isSuccess()).isTrue()
}
}
@Test
fun `present - Reset event can fail gracefully`() = runTest {
val resetLambda = lambdaRecorder<String, Result<Unit>> { _ -> Result.failure(IllegalStateException("Failed")) }
val resetHandle = FakeIdentityPasswordResetHandle(resetPasswordLambda = resetLambda)
val presenter = createPresenter(identityResetHandle = resetHandle)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(ResetIdentityPasswordEvent.Reset("password"))
assertThat(awaitItem().resetAction.isLoading()).isTrue()
assertThat(awaitItem().resetAction.isFailure()).isTrue()
}
}
@Test
fun `present - DismissError event resets the state`() = runTest {
val resetLambda = lambdaRecorder<String, Result<Unit>> { _ -> Result.failure(IllegalStateException("Failed")) }
val resetHandle = FakeIdentityPasswordResetHandle(resetPasswordLambda = resetLambda)
val presenter = createPresenter(identityResetHandle = resetHandle)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(ResetIdentityPasswordEvent.Reset("password"))
assertThat(awaitItem().resetAction.isLoading()).isTrue()
assertThat(awaitItem().resetAction.isFailure()).isTrue()
initialState.eventSink(ResetIdentityPasswordEvent.DismissError)
assertThat(awaitItem().resetAction.isUninitialized()).isTrue()
}
}
private fun TestScope.createPresenter(
identityResetHandle: FakeIdentityPasswordResetHandle = FakeIdentityPasswordResetHandle(),
) = ResetIdentityPasswordPresenter(
identityPasswordResetHandle = identityResetHandle,
dispatchers = testCoroutineDispatchers(),
)
}

97
features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt

@ -0,0 +1,97 @@ @@ -0,0 +1,97 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.securebackup.impl.reset.password
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ResetIdentityPasswordViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `pressing the back HW button invokes the expected callback`() {
ensureCalledOnce {
rule.setResetPasswordView(
ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}),
onBack = it,
)
rule.pressBackKey()
}
}
@Test
fun `clicking on the back navigation button invokes the expected callback`() {
ensureCalledOnce {
rule.setResetPasswordView(
ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}),
onBack = it,
)
rule.pressBack()
}
}
@Test
fun `clicking 'Reset identity' confirms the reset`() {
val eventsRecorder = EventsRecorder<ResetIdentityPasswordEvent>()
rule.setResetPasswordView(
ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = eventsRecorder),
)
rule.onNodeWithText("Password").performTextInput("A password")
rule.clickOn(CommonStrings.action_reset_identity)
eventsRecorder.assertSingle(ResetIdentityPasswordEvent.Reset("A password"))
}
@Test
fun `clicking OK dismisses the error dialog`() {
val eventsRecorder = EventsRecorder<ResetIdentityPasswordEvent>()
rule.setResetPasswordView(
ResetIdentityPasswordState(resetAction = AsyncAction.Failure(IllegalStateException("A failure")), eventSink = eventsRecorder),
)
rule.clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(ResetIdentityPasswordEvent.DismissError)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setResetPasswordView(
state: ResetIdentityPasswordState,
onBack: () -> Unit = EnsureNeverCalled(),
) {
setContent {
ResetIdentityPasswordView(state = state, onBack = onBack)
}
}

65
features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootPresenterTests.kt

@ -0,0 +1,65 @@ @@ -0,0 +1,65 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.securebackup.impl.reset.root
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
class ResetIdentityRootPresenterTests {
@Test
fun `present - initial state`() = runTest {
val presenter = ResetIdentityRootPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.displayConfirmationDialog).isFalse()
}
}
@Test
fun `present - Continue event displays the confirmation dialog`() = runTest {
val presenter = ResetIdentityRootPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(ResetIdentityRootEvent.Continue)
assertThat(awaitItem().displayConfirmationDialog).isTrue()
}
}
@Test
fun `present - DismissDialog event hides the confirmation dialog`() = runTest {
val presenter = ResetIdentityRootPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(ResetIdentityRootEvent.Continue)
assertThat(awaitItem().displayConfirmationDialog).isTrue()
initialState.eventSink(ResetIdentityRootEvent.DismissDialog)
assertThat(awaitItem().displayConfirmationDialog).isFalse()
}
}
}

107
features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTests.kt

@ -0,0 +1,107 @@ @@ -0,0 +1,107 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.securebackup.impl.reset.root
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
class ResetIdentityRootViewTests {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `pressing the back HW button invokes the expected callback`() {
ensureCalledOnce {
rule.setResetRootView(
ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}),
onBack = it,
)
rule.pressBackKey()
}
}
@Test
fun `clicking on the back navigation button invokes the expected callback`() {
ensureCalledOnce {
rule.setResetRootView(
ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}),
onBack = it,
)
rule.pressBack()
}
}
@Test
@Config(qualifiers = "h720dp")
fun `clicking Continue displays the confirmation dialog`() {
val eventsRecorder = EventsRecorder<ResetIdentityRootEvent>()
rule.setResetRootView(
ResetIdentityRootState(displayConfirmationDialog = false, eventSink = eventsRecorder),
)
rule.clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(ResetIdentityRootEvent.Continue)
}
@Test
fun `clicking 'Yes, reset now' confirms the reset`() {
ensureCalledOnce {
rule.setResetRootView(
ResetIdentityRootState(displayConfirmationDialog = true, eventSink = {}),
onContinue = it,
)
rule.clickOn(CommonStrings.screen_reset_encryption_confirmation_alert_action)
}
}
@Test
fun `clicking Cancel dismisses the dialog`() {
val eventsRecorder = EventsRecorder<ResetIdentityRootEvent>()
rule.setResetRootView(
ResetIdentityRootState(displayConfirmationDialog = true, eventSink = eventsRecorder),
)
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ResetIdentityRootEvent.DismissDialog)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setResetRootView(
state: ResetIdentityRootState,
onBack: () -> Unit = EnsureNeverCalled(),
onContinue: () -> Unit = EnsureNeverCalled(),
) {
setContent {
ResetIdentityRootView(state = state, onContinue = onContinue, onBack = onBack)
}
}

4
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt

@ -16,7 +16,6 @@ @@ -16,7 +16,6 @@
package io.element.android.libraries.matrix.api.encryption
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@ -90,10 +89,9 @@ interface IdentityPasswordResetHandle : IdentityResetHandle { @@ -90,10 +89,9 @@ interface IdentityPasswordResetHandle : IdentityResetHandle {
* This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is
* called, or the identity is reset.
*
* @param userId the user id of the user to reset the password for.
* @param password the current password, which will be validated before the process takes place.
*/
suspend fun resetPassword(userId: UserId, password: String): Result<Unit>
suspend fun resetPassword(password: String): Result<Unit>
}
/**

8
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt

@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.impl.encryption @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.impl.encryption
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
@ -55,6 +56,7 @@ internal class RustEncryptionService( @@ -55,6 +56,7 @@ internal class RustEncryptionService(
private val dispatchers: CoroutineDispatchers,
) : EncryptionService {
private val service: Encryption = client.encryption()
private val sessionId = SessionId(client.session().userId)
private val enableRecoveryProgressMapper = EnableRecoveryProgressMapper()
private val backupUploadStateMapper = BackupUploadStateMapper()
@ -201,6 +203,10 @@ internal class RustEncryptionService( @@ -201,6 +203,10 @@ internal class RustEncryptionService(
}
override suspend fun startIdentityReset(): Result<IdentityResetHandle?> {
return runCatching { service.resetIdentity()?.let(RustIdentityResetHandleFactory::create)?.getOrNull() }
return runCatching {
service.resetIdentity()?.let { handle ->
RustIdentityResetHandleFactory.create(sessionId, handle)
}?.getOrNull()
}
}
}

10
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt

@ -25,21 +25,25 @@ import org.matrix.rustcomponents.sdk.AuthDataPasswordDetails @@ -25,21 +25,25 @@ import org.matrix.rustcomponents.sdk.AuthDataPasswordDetails
import org.matrix.rustcomponents.sdk.CrossSigningResetAuthType
object RustIdentityResetHandleFactory {
fun create(identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle): Result<IdentityResetHandle> {
fun create(
userId: UserId,
identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle
): Result<IdentityResetHandle> {
return runCatching {
when (val authType = identityResetHandle.authType()) {
is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl)
// User interactive authentication (user + password)
CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(identityResetHandle)
CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle)
}
}
}
}
class RustPasswordIdentityResetHandle(
private val userId: UserId,
private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle,
) : IdentityPasswordResetHandle {
override suspend fun resetPassword(userId: UserId, password: String): Result<Unit> {
override suspend fun resetPassword(password: String): Result<Unit> {
return runCatching { identityResetHandle.reset(AuthData.Password(AuthDataPasswordDetails(userId.value, password))) }
}

7
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt

@ -16,7 +16,6 @@ @@ -16,7 +16,6 @@
package io.element.android.libraries.matrix.test.encryption
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
@ -35,11 +34,11 @@ class FakeIdentityOidcResetHandle( @@ -35,11 +34,11 @@ class FakeIdentityOidcResetHandle(
}
class FakeIdentityPasswordResetHandle(
var resetPasswordLambda: (UserId, String) -> Result<Unit> = { _, _ -> error("Not implemented") },
var resetPasswordLambda: (String) -> Result<Unit> = { _ -> error("Not implemented") },
var cancelLambda: () -> Unit = { error("Not implemented") },
) : IdentityPasswordResetHandle {
override suspend fun resetPassword(userId: UserId, password: String): Result<Unit> {
return resetPasswordLambda(userId, password)
override suspend fun resetPassword(password: String): Result<Unit> {
return resetPasswordLambda(password)
}
override suspend fun cancel() {

Loading…
Cancel
Save