feat(security&privacy) : add all tests for EditRoomAddress classes

This commit is contained in:
ganfra
2025-01-27 22:43:10 +01:00
parent 73281be1af
commit 76bc87275c
10 changed files with 515 additions and 13 deletions

View File

@@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediapickers.test.FakePickerProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor

View File

@@ -14,7 +14,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
interface SecurityAndPrivacyNavigator : Plugin {
fun openEditRoomAddress()
fun closeEditorRoomAddress()
fun closeEditRoomAddress()
}
class BackstackSecurityAndPrivacyNavigator(
@@ -24,7 +24,7 @@ class BackstackSecurityAndPrivacyNavigator(
backStack.push(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress)
}
override fun closeEditorRoomAddress() {
override fun closeEditRoomAddress() {
backStack.pop()
}
}

View File

@@ -128,7 +128,7 @@ class EditRoomAddressPresenter @AssistedInject constructor(
room.updateCanonicalAlias(savedCanonicalAlias, newAlternativeAliases).getOrThrow()
}
}
navigator.closeEditorRoomAddress()
navigator.closeEditRoomAddress()
}.runCatchingUpdatingState(saveAction)
}
}

View File

@@ -14,15 +14,15 @@ import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
open class EditRoomAddressStateProvider : PreviewParameterProvider<EditRoomAddressState> {
override val values: Sequence<EditRoomAddressState>
get() = sequenceOf(
aEditRoomAddressState(),
aEditRoomAddressState(roomAddressValidity = RoomAddressValidity.NotAvailable),
aEditRoomAddressState(roomAddressValidity = RoomAddressValidity.InvalidSymbols),
aEditRoomAddressState(roomAddressValidity = RoomAddressValidity.Valid),
aEditRoomAddressState(roomAddressValidity = RoomAddressValidity.Valid, saveAction = AsyncAction.Loading),
anEditRoomAddressState(),
anEditRoomAddressState(roomAddressValidity = RoomAddressValidity.NotAvailable),
anEditRoomAddressState(roomAddressValidity = RoomAddressValidity.InvalidSymbols),
anEditRoomAddressState(roomAddressValidity = RoomAddressValidity.Valid),
anEditRoomAddressState(roomAddressValidity = RoomAddressValidity.Valid, saveAction = AsyncAction.Loading),
)
}
fun aEditRoomAddressState(
fun anEditRoomAddressState(
roomAddress: String = "therapy",
roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Unknown,
homeserverName: String = ":myserver.org",

View File

@@ -12,13 +12,13 @@ import io.element.android.tests.testutils.lambda.lambdaError
class FakeSecurityAndPrivacyNavigator(
private val openEditRoomAddressLambda: () -> Unit = { lambdaError() },
private val closeEditorRoomAddressLambda: () -> Unit = { lambdaError() },
private val closeEditRoomAddressLambda: () -> Unit = { lambdaError() },
) : SecurityAndPrivacyNavigator {
override fun openEditRoomAddress() {
openEditRoomAddressLambda()
}
override fun closeEditorRoomAddress() {
closeEditorRoomAddressLambda()
override fun closeEditRoomAddress() {
closeEditRoomAddressLambda()
}
}

View File

@@ -192,6 +192,20 @@ class SecurityAndPrivacyPresenterTest {
}
}
@Test
fun `present - edit room address`() = runTest {
val openEditRoomAddressLambda = lambdaRecorder<Unit> { }
val navigator = FakeSecurityAndPrivacyNavigator(openEditRoomAddressLambda)
val presenter = createSecurityAndPrivacyPresenter(navigator = navigator)
presenter.test {
skipItems(1)
with(awaitItem()) {
eventSink(SecurityAndPrivacyEvents.EditRoomAddress)
}
assert(openEditRoomAddressLambda).isCalledOnce()
}
}
@Test
fun `present - save success`() = runTest {
val enableEncryptionLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }

View File

@@ -0,0 +1,355 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomdetails.securityandprivacy.editroomaddress
import com.google.common.truth.Truth.assertThat
import io.element.android.features.roomdetails.impl.securityandprivacy.SecurityAndPrivacyNavigator
import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressEvents
import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressPresenter
import io.element.android.features.roomdetails.securityandprivacy.FakeSecurityAndPrivacyNavigator
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.util.Optional
class EditRoomAddressPresenterTest {
@Test
fun `present - initial state no address`() = runTest {
val presenter = createEditRoomAddressPresenter()
presenter.test {
with(awaitItem()) {
assertThat(homeserverName).isEqualTo("matrix.org")
assertThat(canBeSaved).isFalse()
assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown)
assertThat(roomAddress).isEmpty()
}
}
}
@Test
fun `present - initial state address matching own homeserver`() = runTest {
val room = FakeMatrixRoom(
canonicalAlias = RoomAlias("#canonical:matrix.org"),
)
val presenter = createEditRoomAddressPresenter(room = room)
presenter.test {
with(awaitItem()) {
assertThat(homeserverName).isEqualTo("matrix.org")
assertThat(canBeSaved).isFalse()
assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown)
assertThat(roomAddress).isEqualTo("canonical")
}
}
}
@Test
fun `present - initial state address not matching own homeserver`() = runTest {
val room = FakeMatrixRoom(
canonicalAlias = RoomAlias("#canonical:notmatrix.org"),
)
val presenter = createEditRoomAddressPresenter(room = room)
presenter.test {
with(awaitItem()) {
assertThat(homeserverName).isEqualTo("matrix.org")
assertThat(canBeSaved).isFalse()
assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown)
assertThat(roomAddress).isEmpty()
}
}
}
@Test
fun `present - room address change invalid state`() = runTest {
val roomAliasHelper = FakeRoomAliasHelper(
isRoomAliasValidLambda = { false }
)
val presenter = createEditRoomAddressPresenter(roomAliasHelper = roomAliasHelper)
presenter.test {
with(awaitItem()) {
eventSink(EditRoomAddressEvents.RoomAddressChanged("invalid"))
}
with(awaitItem()) {
assertThat(roomAddress).isEqualTo("invalid")
assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown)
}
with(awaitItem()) {
assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.InvalidSymbols)
assertThat(canBeSaved).isFalse()
}
}
}
@Test
fun `present - room address change valid state`() = runTest {
val presenter = createEditRoomAddressPresenter()
presenter.test {
with(awaitItem()) {
eventSink(EditRoomAddressEvents.RoomAddressChanged("valid"))
}
with(awaitItem()) {
assertThat(roomAddress).isEqualTo("valid")
assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown)
}
with(awaitItem()) {
assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid)
assertThat(canBeSaved).isTrue()
}
}
}
@Test
fun `present - room address change alias unavailable`() = runTest {
val client = createMatrixClient(isAliasAvailable = false)
val presenter = createEditRoomAddressPresenter(client = client)
presenter.test {
with(awaitItem()) {
eventSink(EditRoomAddressEvents.RoomAddressChanged("valid"))
}
with(awaitItem()) {
assertThat(roomAddress).isEqualTo("valid")
assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Unknown)
}
with(awaitItem()) {
assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.NotAvailable)
assertThat(canBeSaved).isFalse()
}
}
}
@Test
fun `present - save success no current alias`() = runTest {
val publishAliasInRoomDirectoryResult = lambdaRecorder<RoomAlias, Result<Boolean>> { _ -> Result.success(true) }
val updateCanonicalAliasResult = lambdaRecorder<RoomAlias?, List<RoomAlias>, Result<Unit>> { _, _ -> Result.success(Unit) }
val removeAliasFromRoomDirectoryResult = lambdaRecorder<RoomAlias, Result<Boolean>> { _ -> Result.success(true) }
val closeEditAddressLambda = lambdaRecorder<Unit> { }
val navigator = FakeSecurityAndPrivacyNavigator(
closeEditRoomAddressLambda = closeEditAddressLambda
)
val room = FakeMatrixRoom(
updateCanonicalAliasResult = updateCanonicalAliasResult,
publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult
)
val presenter = createEditRoomAddressPresenter(room = room, navigator = navigator)
presenter.test {
with(awaitItem()) {
eventSink(EditRoomAddressEvents.RoomAddressChanged("valid"))
}
skipItems(1)
with(awaitItem()) {
assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid)
assertThat(canBeSaved).isTrue()
eventSink(EditRoomAddressEvents.Save)
}
with(awaitItem()) {
assertThat(saveAction).isEqualTo(AsyncAction.Loading)
}
with(awaitItem()) {
assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit))
}
val createdAlias = RoomAlias("#valid:matrix.org")
assert(updateCanonicalAliasResult)
.isCalledOnce()
.with(value(createdAlias), value(emptyList<RoomAlias>()))
assert(publishAliasInRoomDirectoryResult)
.isCalledOnce()
.with(value(createdAlias))
assert(removeAliasFromRoomDirectoryResult).isNeverCalled()
assert(closeEditAddressLambda).isCalledOnce()
}
}
@Test
fun `present - save success current canonical alias from own homeserver`() = runTest {
val publishAliasInRoomDirectoryResult = lambdaRecorder<RoomAlias, Result<Boolean>> { _ -> Result.success(true) }
val removeAliasFromRoomDirectoryResult = lambdaRecorder<RoomAlias, Result<Boolean>> { _ -> Result.success(true) }
val updateCanonicalAliasResult = lambdaRecorder<RoomAlias?, List<RoomAlias>, Result<Unit>> { _, _ -> Result.success(Unit) }
val closeEditAddressLambda = lambdaRecorder<Unit> { }
val navigator = FakeSecurityAndPrivacyNavigator(closeEditRoomAddressLambda = closeEditAddressLambda)
val canonicalAlias = RoomAlias("#canonical:matrix.org")
val room = FakeMatrixRoom(
canonicalAlias = canonicalAlias,
updateCanonicalAliasResult = updateCanonicalAliasResult,
publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult,
removeRoomAliasFromRoomDirectoryResult = removeAliasFromRoomDirectoryResult
)
val presenter = createEditRoomAddressPresenter(room = room, navigator = navigator)
presenter.test {
with(awaitItem()) {
eventSink(EditRoomAddressEvents.RoomAddressChanged("valid"))
}
skipItems(1)
with(awaitItem()) {
assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid)
assertThat(canBeSaved).isTrue()
eventSink(EditRoomAddressEvents.Save)
}
with(awaitItem()) {
assertThat(saveAction).isEqualTo(AsyncAction.Loading)
}
with(awaitItem()) {
assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit))
}
val createdAlias = RoomAlias("#valid:matrix.org")
assert(updateCanonicalAliasResult)
.isCalledOnce()
.with(value(createdAlias), value(emptyList<RoomAlias>()))
assert(publishAliasInRoomDirectoryResult)
.isCalledOnce()
.with(value(createdAlias))
assert(removeAliasFromRoomDirectoryResult)
.isCalledOnce()
.with(value(canonicalAlias))
assert(closeEditAddressLambda).isCalledOnce()
}
}
@Test
fun `present - save success current canonical alias from other homeserver`() = runTest {
val publishAliasInRoomDirectoryResult = lambdaRecorder<RoomAlias, Result<Boolean>> { _ -> Result.success(true) }
val removeAliasFromRoomDirectoryResult = lambdaRecorder<RoomAlias, Result<Boolean>> { _ -> Result.success(true) }
val updateCanonicalAliasResult = lambdaRecorder<RoomAlias?, List<RoomAlias>, Result<Unit>> { _, _ -> Result.success(Unit) }
val closeEditAddressLambda = lambdaRecorder<Unit> { }
val navigator = FakeSecurityAndPrivacyNavigator(closeEditRoomAddressLambda = closeEditAddressLambda)
val canonicalAlias = RoomAlias("#canonical:notmatrix.org")
val room = FakeMatrixRoom(
canonicalAlias = canonicalAlias,
updateCanonicalAliasResult = updateCanonicalAliasResult,
publishRoomAliasInRoomDirectoryResult = publishAliasInRoomDirectoryResult,
removeRoomAliasFromRoomDirectoryResult = removeAliasFromRoomDirectoryResult
)
val presenter = createEditRoomAddressPresenter(room = room, navigator = navigator)
presenter.test {
with(awaitItem()) {
eventSink(EditRoomAddressEvents.RoomAddressChanged("valid"))
}
skipItems(1)
with(awaitItem()) {
assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid)
assertThat(canBeSaved).isTrue()
eventSink(EditRoomAddressEvents.Save)
}
with(awaitItem()) {
assertThat(saveAction).isEqualTo(AsyncAction.Loading)
}
with(awaitItem()) {
assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit))
}
val createdAlias = RoomAlias("#valid:matrix.org")
assert(updateCanonicalAliasResult)
.isCalledOnce()
.with(value(canonicalAlias), value(listOf(createdAlias)))
assert(publishAliasInRoomDirectoryResult)
.isCalledOnce()
.with(value(createdAlias))
assert(removeAliasFromRoomDirectoryResult).isNeverCalled()
assert(closeEditAddressLambda).isCalledOnce()
}
}
@Test
fun `present - save failure`() = runTest {
val closeEditAddressLambda = lambdaRecorder<Unit> { }
val navigator = FakeSecurityAndPrivacyNavigator(
closeEditRoomAddressLambda = closeEditAddressLambda
)
val presenter = createEditRoomAddressPresenter(navigator = navigator)
presenter.test {
with(awaitItem()) {
eventSink(EditRoomAddressEvents.RoomAddressChanged("valid"))
}
skipItems(1)
with(awaitItem()) {
assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid)
assertThat(canBeSaved).isTrue()
eventSink(EditRoomAddressEvents.Save)
}
with(awaitItem()) {
assertThat(saveAction).isEqualTo(AsyncAction.Loading)
}
with(awaitItem()) {
assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java)
}
assert(closeEditAddressLambda).isNeverCalled()
}
}
@Test
fun `present - dismiss error`() = runTest {
val presenter = createEditRoomAddressPresenter()
presenter.test {
with(awaitItem()) {
eventSink(EditRoomAddressEvents.Save)
}
with(awaitItem()) {
assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java)
eventSink(EditRoomAddressEvents.DismissError)
}
with(awaitItem()) {
assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
}
}
}
private fun createMatrixClient(isAliasAvailable: Boolean = true) = FakeMatrixClient(
userIdServerNameLambda = { "matrix.org" },
resolveRoomAliasResult = {
val resolvedRoomAlias = if (isAliasAvailable) {
Optional.empty()
} else {
Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
}
Result.success(resolvedRoomAlias)
}
)
private fun createEditRoomAddressPresenter(
client: FakeMatrixClient = createMatrixClient(),
room: MatrixRoom = FakeMatrixRoom(),
navigator: SecurityAndPrivacyNavigator = FakeSecurityAndPrivacyNavigator(),
roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper()
): EditRoomAddressPresenter {
return EditRoomAddressPresenter(
room = room,
client = client,
roomAliasHelper = roomAliasHelper,
navigator = navigator
)
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomdetails.securityandprivacy.editroomaddress
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressEvents
import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressState
import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.EditRoomAddressView
import io.element.android.features.roomdetails.impl.securityandprivacy.editroomaddress.anEditRoomAddressState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
import io.element.android.libraries.testtags.TestTags
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 org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class EditRoomAddressViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `click on back invokes expected callback`() {
ensureCalledOnce { callback ->
rule.setEditRoomAddressView(onBackClick = callback)
rule.pressBack()
}
}
@Test
fun `click on disabled save doesn't emit event`() {
val recorder = EventsRecorder<EditRoomAddressEvents>(expectEvents = false)
val state = anEditRoomAddressState(eventSink = recorder)
rule.setEditRoomAddressView(state)
rule.clickOn(CommonStrings.action_save)
recorder.assertEmpty()
}
@Test
fun `click on enabled save emits the expected event`() {
val recorder = EventsRecorder<EditRoomAddressEvents>()
val state = anEditRoomAddressState(
roomAddress = "room",
roomAddressValidity = RoomAddressValidity.Valid,
eventSink = recorder
)
rule.setEditRoomAddressView(state)
rule.clickOn(CommonStrings.action_save)
recorder.assertSingle(EditRoomAddressEvents.Save)
}
@Test
fun `text changes on text field emits the expected event`() {
val recorder = EventsRecorder<EditRoomAddressEvents>()
val state = anEditRoomAddressState(
roomAddress = "",
eventSink = recorder
)
rule.setEditRoomAddressView(state)
rule.onNodeWithTag(TestTags.roomAddressField.value).performTextInput("alias")
recorder.assertSingle(EditRoomAddressEvents.RoomAddressChanged("alias"))
}
@Test
fun `click on dismiss error emits the expected event`() {
val recorder = EventsRecorder<EditRoomAddressEvents>()
val state = anEditRoomAddressState(
roomAddress = "",
saveAction = AsyncAction.Failure(IllegalStateException()),
eventSink = recorder
)
rule.setEditRoomAddressView(state)
rule.clickOn(CommonStrings.action_cancel)
recorder.assertSingle(EditRoomAddressEvents.DismissError)
}
@Test
fun `click on retry error emits the expected event`() {
val recorder = EventsRecorder<EditRoomAddressEvents>()
val state = anEditRoomAddressState(
roomAddress = "",
saveAction = AsyncAction.Failure(IllegalStateException()),
eventSink = recorder
)
rule.setEditRoomAddressView(state)
rule.clickOn(CommonStrings.action_retry)
recorder.assertSingle(EditRoomAddressEvents.Save)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setEditRoomAddressView(
state: EditRoomAddressState = anEditRoomAddressState(
eventSink = EventsRecorder(expectEvents = false),
),
onBackClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
EditRoomAddressView(
state = state,
onBackClick = onBackClick,
)
}
}

View File

@@ -15,6 +15,8 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
@@ -28,7 +30,7 @@ fun RoomAddressField(
modifier: Modifier = Modifier,
) {
TextField(
modifier = modifier,
modifier = modifier.testTag(TestTags.roomAddressField),
value = address,
label = label,
leadingIcon = {

View File

@@ -111,4 +111,11 @@ object TestTags {
* Generic call to action.
*/
val callToAction = TestTag("call_to_action")
/**
* Room address field.
*
*/
val roomAddressField = TestTag("room_address_field")
}