Jorge Martín
1 month ago
22 changed files with 562 additions and 76 deletions
@ -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, |
||||
) |
||||
} |
@ -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(), |
||||
) |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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() |
||||
} |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
Loading…
Reference in new issue