Browse Source

Verification: integrate with new statemachine library

feature/fga/small_timeline_improvements
ganfra 1 year ago
parent
commit
2179c17de8
  1. 1
      features/verifysession/impl/build.gradle.kts
  2. 87
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt
  3. 137
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt
  4. 148
      features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt
  5. 1
      gradle/libs.versions.toml
  6. 7
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt

1
features/verifysession/impl/build.gradle.kts

@ -40,6 +40,7 @@ dependencies {
implementation(projects.libraries.elementresources) implementation(projects.libraries.elementresources)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.libraries.statemachine) implementation(projects.libraries.statemachine)
api(libs.statemachine)
api(projects.features.verifysession.api) api(projects.features.verifysession.api)
testImplementation(libs.test.junit) testImplementation(libs.test.junit)

87
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt

@ -14,24 +14,31 @@
* limitations under the License. * limitations under the License.
*/ */
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.verifysession.impl package io.element.android.features.verifysession.impl
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import com.freeletics.flowredux.compose.rememberStateAndDispatch
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
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.VerificationFlowState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject import javax.inject.Inject
import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.Event as StateMachineEvent import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.Event as StateMachineEvent
import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.State as StateMachineState import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.State as StateMachineState
class VerifySelfSessionPresenter @Inject constructor( class VerifySelfSessionPresenter @Inject constructor(
private val sessionVerificationService: SessionVerificationService, private val sessionVerificationService: SessionVerificationService,
private val stateMachine: VerifySelfSessionStateMachine,
) : Presenter<VerifySelfSessionState> { ) : Presenter<VerifySelfSessionState> {
@Composable @Composable
@ -40,48 +47,36 @@ class VerifySelfSessionPresenter @Inject constructor(
// Force reset, just in case the service was left in a broken state // Force reset, just in case the service was left in a broken state
sessionVerificationService.reset() sessionVerificationService.reset()
} }
val stateAndDispatch = stateMachine.rememberStateAndDispatch()
val coroutineScope = rememberCoroutineScope() val verificationFlowStep by remember {
val stateMachine = remember { VerifySelfSessionStateMachine(coroutineScope, sessionVerificationService) } derivedStateOf { stateAndDispatch.state.value.toVerificationStep() }
}
// Create the new view state from the StateMachine state // Start this after observing state machine
val stateMachineCurrentState by stateMachine.state.collectAsState() LaunchedEffect(Unit) {
val verificationFlowState by remember { observeVerificationService()
derivedStateOf { stateMachineStateToViewState(stateMachineCurrentState) }
} }
fun handleEvents(event: VerifySelfSessionViewEvents) { fun handleEvents(event: VerifySelfSessionViewEvents) {
when (event) { when (event) {
VerifySelfSessionViewEvents.RequestVerification -> stateMachine.process(StateMachineEvent.RequestVerification) VerifySelfSessionViewEvents.RequestVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.RequestVerification)
VerifySelfSessionViewEvents.StartSasVerification -> stateMachine.process(StateMachineEvent.StartSasVerification) VerifySelfSessionViewEvents.StartSasVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.StartSasVerification)
VerifySelfSessionViewEvents.Restart -> stateMachine.process(StateMachineEvent.Restart) VerifySelfSessionViewEvents.Restart -> stateAndDispatch.dispatchAction(StateMachineEvent.Restart)
VerifySelfSessionViewEvents.ConfirmVerification -> stateMachine.process(StateMachineEvent.AcceptChallenge) VerifySelfSessionViewEvents.ConfirmVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.AcceptChallenge)
VerifySelfSessionViewEvents.DeclineVerification -> stateMachine.process(StateMachineEvent.DeclineChallenge) VerifySelfSessionViewEvents.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge)
VerifySelfSessionViewEvents.CancelAndClose -> { VerifySelfSessionViewEvents.CancelAndClose -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel)
if (stateMachineCurrentState !in sequenceOf(
StateMachineState.Initial,
StateMachineState.Completed,
StateMachineState.Canceled
)
) {
stateMachine.process(StateMachineEvent.Cancel)
}
}
} }
} }
return VerifySelfSessionState( return VerifySelfSessionState(
verificationFlowStep = verificationFlowState, verificationFlowStep = verificationFlowStep,
eventSink = ::handleEvents, eventSink = ::handleEvents,
) )
} }
private fun stateMachineStateToViewState(state: StateMachineState): VerifySelfSessionState.VerificationStep = private fun StateMachineState?.toVerificationStep(): VerifySelfSessionState.VerificationStep =
when (state) { when (val machineState = this) {
StateMachineState.Initial -> { StateMachineState.Initial, null -> {
VerifySelfSessionState.VerificationStep.Initial VerifySelfSessionState.VerificationStep.Initial
} }
StateMachineState.RequestingVerification, StateMachineState.RequestingVerification,
StateMachineState.StartingSasVerification, StateMachineState.StartingSasVerification,
StateMachineState.SasVerificationStarted, StateMachineState.SasVerificationStarted,
@ -98,15 +93,41 @@ class VerifySelfSessionPresenter @Inject constructor(
} }
is StateMachineState.Verifying -> { is StateMachineState.Verifying -> {
val async = when (state) { val async = when (machineState) {
is StateMachineState.Verifying.Replying -> Async.Loading() is StateMachineState.Verifying.Replying -> Async.Loading()
else -> Async.Uninitialized else -> Async.Uninitialized
} }
VerifySelfSessionState.VerificationStep.Verifying(state.emojis, async) VerifySelfSessionState.VerificationStep.Verifying(machineState.emojis, async)
} }
StateMachineState.Completed -> { StateMachineState.Completed -> {
VerifySelfSessionState.VerificationStep.Completed VerifySelfSessionState.VerificationStep.Completed
} }
} }
private fun CoroutineScope.observeVerificationService() {
sessionVerificationService.verificationFlowState.onEach { verificationAttemptState ->
when (verificationAttemptState) {
VerificationFlowState.Initial -> stateMachine.dispatch(VerifySelfSessionStateMachine.Event.Restart)
VerificationFlowState.AcceptedVerificationRequest -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptVerificationRequest)
}
VerificationFlowState.StartedSasVerification -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidStartSasVerification)
}
is VerificationFlowState.ReceivedVerificationData -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidReceiveChallenge(verificationAttemptState.emoji))
}
VerificationFlowState.Finished -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptChallenge)
}
VerificationFlowState.Canceled -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidCancel)
}
VerificationFlowState.Failed -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidFail)
}
}
}.launchIn(this)
}
} }

137
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt

@ -15,117 +15,114 @@
*/ */
@file:Suppress("WildcardImport") @file:Suppress("WildcardImport")
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.verifysession.impl package io.element.android.features.verifysession.impl
import com.freeletics.flowredux.dsl.FlowReduxStateMachine
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.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.matrix.api.verification.VerificationFlowState import kotlinx.coroutines.ExperimentalCoroutinesApi
import io.element.android.libraries.statemachine.createStateMachine import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import com.freeletics.flowredux.dsl.State as MachineState
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn class VerifySelfSessionStateMachine @Inject constructor(
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class VerifySelfSessionStateMachine(
coroutineScope: CoroutineScope,
private val sessionVerificationService: SessionVerificationService, private val sessionVerificationService: SessionVerificationService,
) : FlowReduxStateMachine<VerifySelfSessionStateMachine.State, VerifySelfSessionStateMachine.Event>(
initialState = State.Initial
) { ) {
private val stateMachine = createStateMachine { init {
addInitialState(State.Initial) { spec {
on<Event.RequestVerification>(State.RequestingVerification) inState<State.Initial> {
on<Event.StartSasVerification>(State.StartingSasVerification) on { _: Event.RequestVerification, state: MachineState<State.Initial> ->
state.override { State.RequestingVerification }
}
on { _: Event.StartSasVerification, state: MachineState<State.Initial> ->
state.override { State.StartingSasVerification }
} }
addState<State.RequestingVerification> { }
onEnter { inState<State.RequestingVerification> {
coroutineScope.launch { onEnterEffect {
sessionVerificationService.requestVerification() sessionVerificationService.requestVerification()
} }
on { _: Event.DidAcceptVerificationRequest, state: MachineState<State.RequestingVerification> ->
state.override { State.VerificationRequestAccepted }
}
on { _: Event.DidFail, state: MachineState<State.RequestingVerification> ->
state.override { State.Initial }
} }
on<Event.DidAcceptVerificationRequest>(State.VerificationRequestAccepted)
on<Event.DidFail>(State.Initial)
} }
addState<State.StartingSasVerification> { inState<State.StartingSasVerification> {
onEnter { onEnterEffect {
coroutineScope.launch {
sessionVerificationService.startVerification() sessionVerificationService.startVerification()
} }
} }
inState<State.VerificationRequestAccepted> {
on { _: Event.StartSasVerification, state: MachineState<State.VerificationRequestAccepted> ->
state.override { State.StartingSasVerification }
}
}
inState<State.Canceled> {
on { _: Event.Restart, state: MachineState<State.Canceled> ->
state.override { State.RequestingVerification }
}
}
inState<State.SasVerificationStarted> {
on { event: Event.DidReceiveChallenge, state: MachineState<State.SasVerificationStarted> ->
state.override { State.Verifying.ChallengeReceived(event.emojis) }
} }
addState<State.VerificationRequestAccepted> {
on<Event.StartSasVerification>(State.StartingSasVerification)
} }
addState<State.Canceled> { inState<State.Verifying.ChallengeReceived> {
on<Event.Restart>(State.RequestingVerification) on { _: Event.AcceptChallenge, state: MachineState<State.Verifying.ChallengeReceived> ->
state.override { State.Verifying.Replying(state.snapshot.emojis, accept = true) }
} }
addState<State.SasVerificationStarted> { on { _: Event.DeclineChallenge, state: MachineState<State.Verifying.ChallengeReceived> ->
on<Event.DidReceiveChallenge> { event, _ -> State.Verifying.ChallengeReceived(event.emojis) } state.override { State.Verifying.Replying(state.snapshot.emojis, accept = false) }
} }
addState<State.Verifying.ChallengeReceived> {
on<Event.AcceptChallenge> { _, prevState -> State.Verifying.Replying(prevState.emojis, true) }
on<Event.DeclineChallenge> { _, prevState -> State.Verifying.Replying(prevState.emojis, false) }
} }
addState<State.Verifying.Replying> { inState<State.Verifying.Replying> {
onEnter { state -> onEnterEffect { state ->
coroutineScope.launch {
if (state.accept) { if (state.accept) {
sessionVerificationService.approveVerification() sessionVerificationService.approveVerification()
} else { } else {
sessionVerificationService.declineVerification() sessionVerificationService.declineVerification()
} }
} }
on { _: Event.DidAcceptChallenge, state: MachineState<State.Verifying.Replying> ->
state.override { State.Completed }
} }
on<Event.DidAcceptChallenge>(State.Completed)
} }
addState<State.Canceling> { inState<State.Canceling> {
onEnter { onEnterEffect {
coroutineScope.launch {
sessionVerificationService.cancelVerification() sessionVerificationService.cancelVerification()
} }
} }
inState {
on { _: Event.DidStartSasVerification, state: MachineState<State> ->
state.override { State.SasVerificationStarted }
} }
on<Event.DidStartSasVerification>(State.SasVerificationStarted) on { _: Event.Cancel, state: MachineState<State> ->
on<Event.Cancel>(State.Canceling) if (state.snapshot in sequenceOf(
on<Event.DidCancel>(State.Canceled) State.Initial,
on<Event.DidFail>(State.Canceled) State.Completed,
} State.Canceled
)) {
init { state.noChange()
// Observe the verification service state, translate it to state machine input events } else {
sessionVerificationService.verificationFlowState.onEach { verificationAttemptState -> state.override { State.Canceling }
when (verificationAttemptState) {
VerificationFlowState.Initial -> stateMachine.restart()
VerificationFlowState.AcceptedVerificationRequest -> {
stateMachine.process(Event.DidAcceptVerificationRequest)
}
VerificationFlowState.StartedSasVerification -> {
stateMachine.process(Event.DidStartSasVerification)
}
is VerificationFlowState.ReceivedVerificationData -> {
// For some reason we receive this state twice, we need to discard the 2nd one
if (stateMachine.currentState == State.SasVerificationStarted) {
stateMachine.process(Event.DidReceiveChallenge(verificationAttemptState.emoji))
} }
} }
VerificationFlowState.Finished -> { on { _: Event.DidCancel, state: MachineState<State> ->
stateMachine.process(Event.DidAcceptChallenge) state.override { State.Canceled }
} }
VerificationFlowState.Canceled -> { on { _: Event.DidFail, state: MachineState<State> ->
stateMachine.process(Event.DidCancel) state.override { State.Canceled }
} }
VerificationFlowState.Failed -> {
stateMachine.process(Event.DidFail)
} }
} }
}.launchIn(coroutineScope)
} }
val state: StateFlow<State> = stateMachine.stateFlow
fun process(event: Event) = stateMachine.process(event)
sealed interface State { sealed interface State {
/** The initial state, before verification started. */ /** The initial state, before verification started. */
object Initial : State object Initial : State

148
features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt

@ -18,14 +18,13 @@ package io.element.android.features.verifysession.impl
import app.cash.molecule.RecompositionClock import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.Event
import app.cash.turbine.ReceiveTurbine import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep as VerificationStep import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -36,8 +35,7 @@ class VerifySelfSessionPresenterTests {
@Test @Test
fun `present - Initial state is received`() = runTest { fun `present - Initial state is received`() = runTest {
val service = FakeSessionVerificationService() val presenter = createPresenter()
val presenter = VerifySelfSessionPresenter(service)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -48,30 +46,18 @@ class VerifySelfSessionPresenterTests {
@Test @Test
fun `present - Handles requestVerification`() = runTest { fun `present - Handles requestVerification`() = runTest {
val service = FakeSessionVerificationService() val service = FakeSessionVerificationService()
val presenter = VerifySelfSessionPresenter(service) val presenter = createPresenter(service)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() requestVerificationAndAwaitVerifyingState(service)
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial)
val eventSink = initialState.eventSink
eventSink(VerifySelfSessionViewEvents.RequestVerification)
// Await for other device response:
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
// Await for the state to be Ready
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Ready)
// Await for other device response (again):
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
// Finally, ChallengeReceived:
val verifyingState = awaitItem()
assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
} }
} }
@Test @Test
fun `present - Handles startSasVerification`() = runTest { fun `present - Handles startSasVerification`() = runTest {
val service = FakeSessionVerificationService() val service = FakeSessionVerificationService()
val presenter = VerifySelfSessionPresenter(service) val presenter = createPresenter(service)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -82,6 +68,7 @@ class VerifySelfSessionPresenterTests {
// Await for other device response: // Await for other device response:
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
// ChallengeReceived: // ChallengeReceived:
service.triggerReceiveVerificationData()
val verifyingState = awaitItem() val verifyingState = awaitItem()
assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java) assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
} }
@ -89,8 +76,7 @@ class VerifySelfSessionPresenterTests {
@Test @Test
fun `present - Cancelation on initial state does nothing`() = runTest { fun `present - Cancelation on initial state does nothing`() = runTest {
val service = FakeSessionVerificationService() val presenter = createPresenter()
val presenter = VerifySelfSessionPresenter(service)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -105,65 +91,43 @@ class VerifySelfSessionPresenterTests {
@Test @Test
fun `present - A fail in the flow cancels it`() = runTest { fun `present - A fail in the flow cancels it`() = runTest {
val service = FakeSessionVerificationService() val service = FakeSessionVerificationService()
val presenter = VerifySelfSessionPresenter(service) val presenter = createPresenter(service)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val state = requestVerificationAndAwaitVerifyingState(service)
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial)
val eventSink = initialState.eventSink
eventSink(VerifySelfSessionViewEvents.RequestVerification)
val verifyingState = awaitChallengeReceivedState()
assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
service.shouldFail = true service.shouldFail = true
eventSink(VerifySelfSessionViewEvents.ConfirmVerification) state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
// Cancelling
val remainingEvents = cancelAndConsumeRemainingEvents().mapNotNull { (it as? Event.Item<VerifySelfSessionState>)?.value } assertThat(awaitItem().verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
assertThat(remainingEvents.last().verificationFlowStep).isEqualTo(VerificationStep.Canceled) // Cancelled
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
} }
} }
@Test @Test
fun `present - Canceling the flow once it's verifying cancels it`() = runTest { fun `present - Canceling the flow once it's verifying cancels it`() = runTest {
val service = FakeSessionVerificationService() val service = FakeSessionVerificationService()
val presenter = VerifySelfSessionPresenter(service) val presenter = createPresenter(service)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val state = requestVerificationAndAwaitVerifyingState(service)
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) state.eventSink(VerifySelfSessionViewEvents.CancelAndClose)
val eventSink = initialState.eventSink assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
eventSink(VerifySelfSessionViewEvents.RequestVerification) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
val verifyingState = awaitChallengeReceivedState()
assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
eventSink(VerifySelfSessionViewEvents.CancelAndClose)
val remainingEvents = cancelAndConsumeRemainingEvents().mapNotNull { (it as? Event.Item<VerifySelfSessionState>)?.value }
assertThat(remainingEvents.last().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
} }
} }
@Test @Test
fun `present - When verifying, if we receive another challenge we ignore it`() = runTest { fun `present - When verifying, if we receive another challenge we ignore it`() = runTest {
val service = FakeSessionVerificationService() val service = FakeSessionVerificationService()
val presenter = VerifySelfSessionPresenter(service) val presenter = createPresenter(service)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() requestVerificationAndAwaitVerifyingState(service)
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial)
val eventSink = initialState.eventSink
eventSink(VerifySelfSessionViewEvents.RequestVerification)
val verifyingState = awaitChallengeReceivedState()
assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
service.givenVerificationFlowState(VerificationFlowState.ReceivedVerificationData(emptyList())) service.givenVerificationFlowState(VerificationFlowState.ReceivedVerificationData(emptyList()))
ensureAllEventsConsumed() ensureAllEventsConsumed()
} }
} }
@ -171,21 +135,14 @@ class VerifySelfSessionPresenterTests {
@Test @Test
fun `present - Restart after cancelation returns to requesting verification`() = runTest { fun `present - Restart after cancelation returns to requesting verification`() = runTest {
val service = FakeSessionVerificationService() val service = FakeSessionVerificationService()
val presenter = VerifySelfSessionPresenter(service) val presenter = createPresenter(service)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val state = requestVerificationAndAwaitVerifyingState(service)
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial)
val eventSink = initialState.eventSink
eventSink(VerifySelfSessionViewEvents.RequestVerification)
assertThat(awaitChallengeReceivedState().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emptyList(), Async.Uninitialized))
service.givenVerificationFlowState(VerificationFlowState.Canceled) service.givenVerificationFlowState(VerificationFlowState.Canceled)
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
state.eventSink(VerifySelfSessionViewEvents.Restart)
eventSink(VerifySelfSessionViewEvents.Restart)
// Went back to requesting verification // Went back to requesting verification
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
@ -200,18 +157,12 @@ class VerifySelfSessionPresenterTests {
val service = FakeSessionVerificationService().apply { val service = FakeSessionVerificationService().apply {
givenEmojiList(emojis) givenEmojiList(emojis)
} }
val presenter = VerifySelfSessionPresenter(service) val presenter = createPresenter(service)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val state = requestVerificationAndAwaitVerifyingState(service)
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
val eventSink = initialState.eventSink
eventSink(VerifySelfSessionViewEvents.RequestVerification)
assertThat(awaitChallengeReceivedState().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emojis, Async.Uninitialized))
eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emojis, Async.Loading())) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emojis, Async.Loading()))
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Completed) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Completed)
} }
@ -220,28 +171,41 @@ class VerifySelfSessionPresenterTests {
@Test @Test
fun `present - When verification is declined, the flow is canceled`() = runTest { fun `present - When verification is declined, the flow is canceled`() = runTest {
val service = FakeSessionVerificationService() val service = FakeSessionVerificationService()
val presenter = VerifySelfSessionPresenter(service) val presenter = createPresenter(service)
moleculeFlow(RecompositionClock.Immediate) { moleculeFlow(RecompositionClock.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val state = requestVerificationAndAwaitVerifyingState(service)
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) state.eventSink(VerifySelfSessionViewEvents.DeclineVerification)
val eventSink = initialState.eventSink
eventSink(VerifySelfSessionViewEvents.RequestVerification)
assertThat(awaitChallengeReceivedState().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emptyList(), Async.Uninitialized))
eventSink(VerifySelfSessionViewEvents.DeclineVerification)
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emptyList(), Async.Loading())) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Verifying(emptyList(), Async.Loading()))
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
} }
} }
private suspend fun ReceiveTurbine<VerifySelfSessionState>.awaitChallengeReceivedState(): VerifySelfSessionState { private suspend fun ReceiveTurbine<VerifySelfSessionState>.requestVerificationAndAwaitVerifyingState(
// Skip 'waiting for response', 'ready' and 'starting verification' state fakeService: FakeSessionVerificationService
skipItems(3) ): VerifySelfSessionState {
// Received challenge var state = awaitItem()
return awaitItem() assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Initial)
state.eventSink(VerifySelfSessionViewEvents.RequestVerification)
// Await for other device response:
state = awaitItem()
assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
// Await for the state to be Ready
state = awaitItem()
assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Ready)
state.eventSink(VerifySelfSessionViewEvents.StartSasVerification)
// Await for other device response (again):
state = awaitItem()
assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
fakeService.triggerReceiveVerificationData()
// Finally, ChallengeReceived:
state = awaitItem()
assertThat(state.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
return state
}
private fun createPresenter(service: FakeSessionVerificationService = FakeSessionVerificationService()): VerifySelfSessionPresenter {
return VerifySelfSessionPresenter(service, VerifySelfSessionStateMachine(service))
} }
} }

1
gradle/libs.versions.toml

@ -150,6 +150,7 @@ gujun_span = "me.gujun.android:span:1.7"
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5" otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5"
vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0" vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" } telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
statemachine = "com.freeletics.flowredux:compose:1.1.0"
# Analytics # Analytics
posthog = "com.posthog.android:posthog:2.0.3" posthog = "com.posthog.android:posthog:2.0.3"

7
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt

@ -39,8 +39,6 @@ class FakeSessionVerificationService : SessionVerificationService {
override suspend fun requestVerification() { override suspend fun requestVerification() {
_verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest _verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest
_verificationFlowState.value = VerificationFlowState.StartedSasVerification
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojiList)
} }
override suspend fun cancelVerification() { override suspend fun cancelVerification() {
@ -63,9 +61,12 @@ class FakeSessionVerificationService : SessionVerificationService {
} }
} }
fun triggerReceiveVerificationData() {
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojiList)
}
override suspend fun startVerification() { override suspend fun startVerification() {
_verificationFlowState.value = VerificationFlowState.StartedSasVerification _verificationFlowState.value = VerificationFlowState.StartedSasVerification
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojiList)
} }
fun givenVerifiedStatus(status: SessionVerifiedStatus) { fun givenVerifiedStatus(status: SessionVerifiedStatus) {

Loading…
Cancel
Save