Improve APIs, add tests
This commit is contained in:
parent
252b3e11a0
commit
903d24ea2f
22 changed files with 562 additions and 76 deletions
|
|
@ -19,7 +19,6 @@ package io.element.android.features.securebackup.impl.reset
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
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.encryption.IdentityResetHandle
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
|
|
@ -46,10 +45,6 @@ class ResetIdentityFlowManager @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun currentSessionId(): SessionId {
|
|
||||||
return matrixClient.sessionId
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getResetHandle(): StateFlow<AsyncData<IdentityResetHandle>> {
|
fun getResetHandle(): StateFlow<AsyncData<IdentityResetHandle>> {
|
||||||
return if (resetHandleFlow.value.isLoading() || resetHandleFlow.value.isSuccess()) {
|
return if (resetHandleFlow.value.isLoading() || resetHandleFlow.value.isSuccess()) {
|
||||||
resetHandleFlow
|
resetHandleFlow
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,8 @@ import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.securebackup.impl.reset.password.ResetKeyPasswordNode
|
import io.element.android.features.securebackup.impl.reset.password.ResetIdentityPasswordNode
|
||||||
import io.element.android.features.securebackup.impl.reset.root.ResetKeyRootNode
|
import io.element.android.features.securebackup.impl.reset.root.ResetIdentityRootNode
|
||||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
|
|
@ -108,18 +108,18 @@ class ResetIdentityFlowNode @AssistedInject constructor(
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
is NavTarget.Root -> {
|
is NavTarget.Root -> {
|
||||||
val callback = object : ResetKeyRootNode.Callback {
|
val callback = object : ResetIdentityRootNode.Callback {
|
||||||
override fun onContinue() {
|
override fun onContinue() {
|
||||||
coroutineScope.startReset()
|
coroutineScope.startReset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createNode<ResetKeyRootNode>(buildContext, listOf(callback))
|
createNode<ResetIdentityRootNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
is NavTarget.ResetPassword -> {
|
is NavTarget.ResetPassword -> {
|
||||||
val handle = resetIdentityFlowManager.currentHandleFlow.value.dataOrNull() as? IdentityPasswordResetHandle ?: error("No password handle found")
|
val handle = resetIdentityFlowManager.currentHandleFlow.value.dataOrNull() as? IdentityPasswordResetHandle ?: error("No password handle found")
|
||||||
createNode<ResetKeyPasswordNode>(
|
createNode<ResetIdentityPasswordNode>(
|
||||||
buildContext,
|
buildContext,
|
||||||
listOf(ResetKeyPasswordNode.Inputs(resetIdentityFlowManager.currentSessionId(), handle))
|
listOf(ResetIdentityPasswordNode.Inputs(handle))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is NavTarget.ResetOidc -> {
|
is NavTarget.ResetOidc -> {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.securebackup.impl.reset.password
|
package io.element.android.features.securebackup.impl.reset.password
|
||||||
|
|
||||||
sealed interface ResetKeyPasswordEvent {
|
sealed interface ResetIdentityPasswordEvent {
|
||||||
data class Reset(val password: String) : ResetKeyPasswordEvent
|
data class Reset(val password: String) : ResetIdentityPasswordEvent
|
||||||
data object DismissError : ResetKeyPasswordEvent
|
data object DismissError : ResetIdentityPasswordEvent
|
||||||
}
|
}
|
||||||
|
|
@ -21,33 +21,33 @@ import androidx.compose.ui.Modifier
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
import io.element.android.libraries.architecture.inputs
|
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.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
|
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class ResetKeyPasswordNode @AssistedInject constructor(
|
class ResetIdentityPasswordNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : 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 {
|
private val presenter by lazy {
|
||||||
val inputs = inputs<Inputs>()
|
val inputs = inputs<Inputs>()
|
||||||
ResetKeyPasswordPresenter(inputs.userId, inputs.handle)
|
ResetIdentityPasswordPresenter(inputs.handle, dispatchers = coroutineDispatchers)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
ResetKeyPasswordView(
|
ResetIdentityPasswordView(
|
||||||
state = state,
|
state = state,
|
||||||
onBack = ::navigateUp
|
onBack = ::navigateUp
|
||||||
)
|
)
|
||||||
|
|
@ -24,37 +24,37 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
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
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
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 io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ResetKeyPasswordPresenter(
|
class ResetIdentityPasswordPresenter(
|
||||||
private val userId: UserId,
|
|
||||||
private val identityPasswordResetHandle: IdentityPasswordResetHandle,
|
private val identityPasswordResetHandle: IdentityPasswordResetHandle,
|
||||||
) : Presenter<ResetKeyPasswordState> {
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
) : Presenter<ResetIdentityPasswordState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ResetKeyPasswordState {
|
override fun present(): ResetIdentityPasswordState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val resetAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
val resetAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
fun handleEvent(event: ResetKeyPasswordEvent) {
|
fun handleEvent(event: ResetIdentityPasswordEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is ResetKeyPasswordEvent.Reset -> coroutineScope.reset(userId, event.password, resetAction)
|
is ResetIdentityPasswordEvent.Reset -> coroutineScope.reset(event.password, resetAction)
|
||||||
ResetKeyPasswordEvent.DismissError -> resetAction.value = AsyncAction.Uninitialized
|
ResetIdentityPasswordEvent.DismissError -> resetAction.value = AsyncAction.Uninitialized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResetKeyPasswordState(
|
return ResetIdentityPasswordState(
|
||||||
resetAction = resetAction.value,
|
resetAction = resetAction.value,
|
||||||
eventSink = ::handleEvent
|
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 {
|
suspend {
|
||||||
identityPasswordResetHandle.resetPassword(userId, password).getOrThrow()
|
identityPasswordResetHandle.resetPassword(password).getOrThrow()
|
||||||
}.runCatchingUpdatingState(action)
|
}.runCatchingUpdatingState(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.securebackup.impl.reset.password
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
data class ResetKeyPasswordState(
|
data class ResetIdentityPasswordState(
|
||||||
val resetAction: AsyncAction<Unit>,
|
val resetAction: AsyncAction<Unit>,
|
||||||
val eventSink: (ResetKeyPasswordEvent) -> Unit,
|
val eventSink: (ResetIdentityPasswordEvent) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
@ -46,8 +46,8 @@ import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKe
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ResetKeyPasswordView(
|
fun ResetIdentityPasswordView(
|
||||||
state: ResetKeyPasswordState,
|
state: ResetIdentityPasswordState,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -63,7 +63,7 @@ fun ResetKeyPasswordView(
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = stringResource(CommonStrings.action_reset_identity),
|
text = stringResource(CommonStrings.action_reset_identity),
|
||||||
onClick = { state.eventSink(ResetKeyPasswordEvent.Reset(passwordState.value)) },
|
onClick = { state.eventSink(ResetIdentityPasswordEvent.Reset(passwordState.value)) },
|
||||||
destructive = true,
|
destructive = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +74,7 @@ fun ResetKeyPasswordView(
|
||||||
} else if (state.resetAction.isFailure()) {
|
} else if (state.resetAction.isFailure()) {
|
||||||
ErrorDialog(
|
ErrorDialog(
|
||||||
content = stringResource(CommonStrings.error_unknown),
|
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>) {
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ResetKeyPasswordViewPreview() {
|
internal fun ResetIdentityPasswordViewPreview() {
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
ResetKeyPasswordView(
|
ResetIdentityPasswordView(
|
||||||
state = ResetKeyPasswordState(
|
state = ResetIdentityPasswordState(
|
||||||
resetAction = AsyncAction.Uninitialized,
|
resetAction = AsyncAction.Uninitialized,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
),
|
),
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.securebackup.impl.reset.root
|
package io.element.android.features.securebackup.impl.reset.root
|
||||||
|
|
||||||
sealed interface ResetKeyRootEvent {
|
sealed interface ResetIdentityRootEvent {
|
||||||
data object Continue : ResetKeyRootEvent
|
data object Continue : ResetIdentityRootEvent
|
||||||
data object DismissDialog : ResetKeyRootEvent
|
data object DismissDialog : ResetIdentityRootEvent
|
||||||
}
|
}
|
||||||
|
|
@ -27,7 +27,7 @@ import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
class ResetKeyRootNode @AssistedInject constructor(
|
class ResetIdentityRootNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
@ -35,13 +35,13 @@ class ResetKeyRootNode @AssistedInject constructor(
|
||||||
fun onContinue()
|
fun onContinue()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val presenter = ResetKeyRootPresenter()
|
private val presenter = ResetIdentityRootPresenter()
|
||||||
private val callback: Callback = plugins.filterIsInstance<Callback>().first()
|
private val callback: Callback = plugins.filterIsInstance<Callback>().first()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
ResetKeyRootView(
|
ResetIdentityRootView(
|
||||||
state = state,
|
state = state,
|
||||||
onContinue = callback::onContinue,
|
onContinue = callback::onContinue,
|
||||||
onBack = ::navigateUp,
|
onBack = ::navigateUp,
|
||||||
|
|
@ -23,19 +23,19 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
||||||
class ResetKeyRootPresenter : Presenter<ResetKeyRootState> {
|
class ResetIdentityRootPresenter : Presenter<ResetIdentityRootState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ResetKeyRootState {
|
override fun present(): ResetIdentityRootState {
|
||||||
var displayConfirmDialog by remember { mutableStateOf(false) }
|
var displayConfirmDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
fun handleEvent(event: ResetKeyRootEvent) {
|
fun handleEvent(event: ResetIdentityRootEvent) {
|
||||||
displayConfirmDialog = when (event) {
|
displayConfirmDialog = when (event) {
|
||||||
ResetKeyRootEvent.Continue -> true
|
ResetIdentityRootEvent.Continue -> true
|
||||||
ResetKeyRootEvent.DismissDialog -> false
|
ResetIdentityRootEvent.DismissDialog -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResetKeyRootState(
|
return ResetIdentityRootState(
|
||||||
displayConfirmationDialog = displayConfirmDialog,
|
displayConfirmationDialog = displayConfirmDialog,
|
||||||
eventSink = ::handleEvent
|
eventSink = ::handleEvent
|
||||||
)
|
)
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.securebackup.impl.reset.root
|
package io.element.android.features.securebackup.impl.reset.root
|
||||||
|
|
||||||
data class ResetKeyRootState(
|
data class ResetIdentityRootState(
|
||||||
val displayConfirmationDialog: Boolean,
|
val displayConfirmationDialog: Boolean,
|
||||||
val eventSink: (ResetKeyRootEvent) -> Unit,
|
val eventSink: (ResetIdentityRootEvent) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
@ -18,14 +18,14 @@ package io.element.android.features.securebackup.impl.reset.root
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
|
||||||
class ResetKeyRootStateProvider : PreviewParameterProvider<ResetKeyRootState> {
|
class ResetIdentityRootStateProvider : PreviewParameterProvider<ResetIdentityRootState> {
|
||||||
override val values: Sequence<ResetKeyRootState>
|
override val values: Sequence<ResetIdentityRootState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
ResetKeyRootState(
|
ResetIdentityRootState(
|
||||||
displayConfirmationDialog = false,
|
displayConfirmationDialog = false,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
),
|
),
|
||||||
ResetKeyRootState(
|
ResetIdentityRootState(
|
||||||
displayConfirmationDialog = true,
|
displayConfirmationDialog = true,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
@ -43,8 +43,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ResetKeyRootView(
|
fun ResetIdentityRootView(
|
||||||
state: ResetKeyRootState,
|
state: ResetIdentityRootState,
|
||||||
onContinue: () -> Unit,
|
onContinue: () -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -58,7 +58,7 @@ fun ResetKeyRootView(
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = stringResource(id = CommonStrings.action_continue),
|
text = stringResource(id = CommonStrings.action_continue),
|
||||||
onClick = { state.eventSink(ResetKeyRootEvent.Continue) },
|
onClick = { state.eventSink(ResetIdentityRootEvent.Continue) },
|
||||||
destructive = true,
|
destructive = true,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -71,11 +71,11 @@ fun ResetKeyRootView(
|
||||||
content = stringResource(CommonStrings.screen_reset_encryption_confirmation_alert_subtitle),
|
content = stringResource(CommonStrings.screen_reset_encryption_confirmation_alert_subtitle),
|
||||||
submitText = stringResource(CommonStrings.screen_reset_encryption_confirmation_alert_action),
|
submitText = stringResource(CommonStrings.screen_reset_encryption_confirmation_alert_action),
|
||||||
onSubmitClick = {
|
onSubmitClick = {
|
||||||
state.eventSink(ResetKeyRootEvent.DismissDialog)
|
state.eventSink(ResetIdentityRootEvent.DismissDialog)
|
||||||
onContinue()
|
onContinue()
|
||||||
},
|
},
|
||||||
destructiveSubmit = true,
|
destructiveSubmit = true,
|
||||||
onDismiss = { state.eventSink(ResetKeyRootEvent.DismissDialog) }
|
onDismiss = { state.eventSink(ResetIdentityRootEvent.DismissDialog) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -138,9 +138,9 @@ private fun Content() {
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ResetKeyRootViewPreview(@PreviewParameter(ResetKeyRootStateProvider::class) state: ResetKeyRootState) {
|
internal fun ResetIdentityRootViewPreview(@PreviewParameter(ResetIdentityRootStateProvider::class) state: ResetIdentityRootState) {
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
ResetKeyRootView(
|
ResetIdentityRootView(
|
||||||
state = state,
|
state = state,
|
||||||
onContinue = {},
|
onContinue = {},
|
||||||
onBack = {},
|
onBack = {},
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.encryption
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
|
@ -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
|
* 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.
|
* 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.
|
* @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>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.extensions.mapFailure
|
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.BackupState
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||||
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
|
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
|
||||||
|
|
@ -55,6 +56,7 @@ internal class RustEncryptionService(
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
) : EncryptionService {
|
) : EncryptionService {
|
||||||
private val service: Encryption = client.encryption()
|
private val service: Encryption = client.encryption()
|
||||||
|
private val sessionId = SessionId(client.session().userId)
|
||||||
|
|
||||||
private val enableRecoveryProgressMapper = EnableRecoveryProgressMapper()
|
private val enableRecoveryProgressMapper = EnableRecoveryProgressMapper()
|
||||||
private val backupUploadStateMapper = BackupUploadStateMapper()
|
private val backupUploadStateMapper = BackupUploadStateMapper()
|
||||||
|
|
@ -201,6 +203,10 @@ internal class RustEncryptionService(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startIdentityReset(): Result<IdentityResetHandle?> {
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,21 +25,25 @@ import org.matrix.rustcomponents.sdk.AuthDataPasswordDetails
|
||||||
import org.matrix.rustcomponents.sdk.CrossSigningResetAuthType
|
import org.matrix.rustcomponents.sdk.CrossSigningResetAuthType
|
||||||
|
|
||||||
object RustIdentityResetHandleFactory {
|
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 {
|
return runCatching {
|
||||||
when (val authType = identityResetHandle.authType()) {
|
when (val authType = identityResetHandle.authType()) {
|
||||||
is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl)
|
is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl)
|
||||||
// User interactive authentication (user + password)
|
// User interactive authentication (user + password)
|
||||||
CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(identityResetHandle)
|
CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RustPasswordIdentityResetHandle(
|
class RustPasswordIdentityResetHandle(
|
||||||
|
private val userId: UserId,
|
||||||
private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle,
|
private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle,
|
||||||
) : IdentityPasswordResetHandle {
|
) : 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))) }
|
return runCatching { identityResetHandle.reset(AuthData.Password(AuthDataPasswordDetails(userId.value, password))) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.test.encryption
|
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.IdentityOidcResetHandle
|
||||||
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
|
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
|
||||||
|
|
||||||
|
|
@ -35,11 +34,11 @@ class FakeIdentityOidcResetHandle(
|
||||||
}
|
}
|
||||||
|
|
||||||
class FakeIdentityPasswordResetHandle(
|
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") },
|
var cancelLambda: () -> Unit = { error("Not implemented") },
|
||||||
) : IdentityPasswordResetHandle {
|
) : IdentityPasswordResetHandle {
|
||||||
override suspend fun resetPassword(userId: UserId, password: String): Result<Unit> {
|
override suspend fun resetPassword(password: String): Result<Unit> {
|
||||||
return resetPasswordLambda(userId, password)
|
return resetPasswordLambda(password)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun cancel() {
|
override suspend fun cancel() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue