quality: add bunch of tests for Security&Privacy new features
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
package io.element.android.features.securityandprivacy.impl
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@@ -71,7 +72,9 @@ class SecurityAndPrivacyFlowNode(
|
||||
}
|
||||
|
||||
private val callback: SecurityAndPrivacyEntryPoint.Callback = callback()
|
||||
private val navigator = BackstackSecurityAndPrivacyNavigator(callback, backstack)
|
||||
|
||||
@VisibleForTesting
|
||||
val navigator = BackstackSecurityAndPrivacyNavigator(callback, backstack)
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
|
||||
@@ -20,14 +20,14 @@ open class ManageAuthorizedSpacesStateProvider : PreviewParameterProvider<Manage
|
||||
get() = sequenceOf(
|
||||
aManageAuthorizedSpacesState(),
|
||||
aManageAuthorizedSpacesState(
|
||||
authorizedSpacesData = anAuthorizedSpacesData(
|
||||
authorizedSpacesSelection = anAuthorizedSpaceSelection(
|
||||
unknownSpaceIds = listOf(aRoomId(99))
|
||||
)
|
||||
),
|
||||
aManageAuthorizedSpacesState(
|
||||
currentSelection = listOf(aRoomId(1), aRoomId(3)),
|
||||
authorizedSpacesData = anAuthorizedSpacesData(
|
||||
initialSelection = listOf(aRoomId(1)),
|
||||
selectedIds = listOf(aRoomId(1), aRoomId(3)),
|
||||
authorizedSpacesSelection = anAuthorizedSpaceSelection(
|
||||
initialSelectedIds = listOf(aRoomId(1)),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -49,23 +49,23 @@ private fun aSpaceRoomList(count: Int): List<SpaceRoom> {
|
||||
}
|
||||
}
|
||||
|
||||
fun anAuthorizedSpacesData(
|
||||
fun anAuthorizedSpaceSelection(
|
||||
joinedSpaces: List<SpaceRoom> = aSpaceRoomList(5),
|
||||
unknownSpaceIds: List<RoomId> = emptyList(),
|
||||
initialSelection: List<RoomId> = emptyList(),
|
||||
initialSelectedIds: List<RoomId> = emptyList(),
|
||||
) = AuthorizedSpacesSelection(
|
||||
joinedSpaces = joinedSpaces.toImmutableList(),
|
||||
unknownSpaceIds = unknownSpaceIds.toImmutableList(),
|
||||
initialSelectedIds = initialSelection.toImmutableList(),
|
||||
initialSelectedIds = initialSelectedIds.toImmutableList(),
|
||||
)
|
||||
|
||||
private fun aManageAuthorizedSpacesState(
|
||||
authorizedSpacesData: AuthorizedSpacesSelection = anAuthorizedSpacesData(),
|
||||
currentSelection: List<RoomId> = emptyList(),
|
||||
authorizedSpacesSelection: AuthorizedSpacesSelection = anAuthorizedSpaceSelection(),
|
||||
selectedIds: List<RoomId> = emptyList(),
|
||||
eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {},
|
||||
) = ManageAuthorizedSpacesState(
|
||||
selection = authorizedSpacesData,
|
||||
selectedIds = currentSelection.toImmutableList(),
|
||||
selection = authorizedSpacesSelection,
|
||||
selectedIds = selectedIds.toImmutableList(),
|
||||
isSelectionComplete = false,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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.securityandprivacy.impl
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.bumble.appyx.core.modality.AncestryInfo
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.activeElement
|
||||
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint
|
||||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SecurityAndPrivacyFlowNodeTest {
|
||||
|
||||
@Test
|
||||
fun `initial backstack contains SecurityAndPrivacy`() = runTest {
|
||||
val flowNode = createFlowNode()
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.SecurityAndPrivacy)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openEditRoomAddress navigates to EditRoomAddress`() = runTest {
|
||||
val flowNode = createFlowNode()
|
||||
flowNode.navigator.openEditRoomAddress()
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `closeEditRoomAddress pops backstack`() = runTest {
|
||||
val flowNode = createFlowNode()
|
||||
flowNode.navigator.openEditRoomAddress()
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress)
|
||||
flowNode.navigator.closeEditRoomAddress()
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.SecurityAndPrivacy)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openManageAuthorizedSpaces navigates with forKnockRestricted false`() = runTest {
|
||||
val flowNode = createFlowNode()
|
||||
flowNode.navigator.openManageAuthorizedSpaces(forKnockRestricted = false)
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(
|
||||
SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces(forKnockRestricted = false)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openManageAuthorizedSpaces navigates with forKnockRestricted true`() = runTest {
|
||||
val flowNode = createFlowNode()
|
||||
flowNode.navigator.openManageAuthorizedSpaces(forKnockRestricted = true)
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(
|
||||
SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces(forKnockRestricted = true)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `closeManageAuthorizedSpaces pops backstack`() = runTest {
|
||||
val flowNode = createFlowNode()
|
||||
flowNode.navigator.openManageAuthorizedSpaces(forKnockRestricted = false)
|
||||
assertThat(flowNode.currentNavTarget())
|
||||
.isInstanceOf(SecurityAndPrivacyFlowNode.NavTarget.ManageAuthorizedSpaces::class.java)
|
||||
flowNode.navigator.closeManageAuthorizedSpaces()
|
||||
assertThat(flowNode.currentNavTarget()).isEqualTo(SecurityAndPrivacyFlowNode.NavTarget.SecurityAndPrivacy)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onDone invokes callback`() = runTest {
|
||||
var onDoneCalled = false
|
||||
val callback = object : SecurityAndPrivacyEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
onDoneCalled = true
|
||||
}
|
||||
}
|
||||
val flowNode = createFlowNode(callback = callback)
|
||||
flowNode.navigator.onDone()
|
||||
assertThat(onDoneCalled).isTrue()
|
||||
}
|
||||
|
||||
private fun createFlowNode(
|
||||
callback: SecurityAndPrivacyEntryPoint.Callback = object : SecurityAndPrivacyEntryPoint.Callback {
|
||||
override fun onDone() {}
|
||||
},
|
||||
): SecurityAndPrivacyFlowNode {
|
||||
val buildContext = BuildContext(
|
||||
ancestryInfo = AncestryInfo.Root,
|
||||
savedStateMap = null,
|
||||
customisations = NodeCustomisationDirectoryImpl()
|
||||
)
|
||||
val room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
initialRoomInfo = aRoomInfo(
|
||||
joinRule = JoinRule.Invite,
|
||||
historyVisibility = RoomHistoryVisibility.Shared
|
||||
)
|
||||
)
|
||||
)
|
||||
return SecurityAndPrivacyFlowNode(
|
||||
buildContext = buildContext,
|
||||
plugins = listOf(callback),
|
||||
room = room,
|
||||
)
|
||||
}
|
||||
|
||||
private fun SecurityAndPrivacyFlowNode.currentNavTarget() = backstack.activeElement
|
||||
}
|
||||
@@ -18,21 +18,29 @@ import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||
import io.element.android.libraries.matrix.api.room.join.AllowRule
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
|
||||
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.FakeBaseRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
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.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@@ -50,7 +58,6 @@ class SecurityAndPrivacyPresenterTest {
|
||||
assertThat(showRoomVisibilitySections).isFalse()
|
||||
assertThat(showHistoryVisibilitySection).isFalse()
|
||||
assertThat(showEncryptionSection).isFalse()
|
||||
assertThat(isKnockEnabled).isFalse()
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings).isEqualTo(savedSettings)
|
||||
@@ -61,7 +68,6 @@ class SecurityAndPrivacyPresenterTest {
|
||||
assertThat(showRoomVisibilitySections).isFalse()
|
||||
assertThat(showHistoryVisibilitySection).isTrue()
|
||||
assertThat(showEncryptionSection).isTrue()
|
||||
assertThat(isKnockEnabled).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -364,17 +370,364 @@ class SecurityAndPrivacyPresenterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - isKnockEnabled is true if the Knock feature flag is enabled`() = runTest {
|
||||
fun `present - Restricted join rule maps to SpaceMember`() = runTest {
|
||||
val room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
initialRoomInfo = aRoomInfo(
|
||||
joinRule = JoinRule.Restricted(
|
||||
rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))
|
||||
),
|
||||
historyVisibility = RoomHistoryVisibility.Shared,
|
||||
)
|
||||
)
|
||||
)
|
||||
val presenter = createSecurityAndPrivacyPresenter(room = room)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.SpaceMember::class.java)
|
||||
val access = editedSettings.roomAccess as SecurityAndPrivacyRoomAccess.SpaceMember
|
||||
assertThat(access.spaceIds).containsExactly(A_ROOM_ID)
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - SelectSpaceMemberAccess with single space auto-selects`() = runTest {
|
||||
val room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
initialRoomInfo = aRoomInfo(
|
||||
historyVisibility = RoomHistoryVisibility.Shared,
|
||||
joinRule = JoinRule.Invite
|
||||
)
|
||||
)
|
||||
)
|
||||
val client = FakeMatrixClient(
|
||||
userIdServerNameLambda = { "matrix.org" },
|
||||
spaceService = FakeSpaceService(
|
||||
joinedParentsResult = { _ ->
|
||||
Result.success(listOf(aSpaceRoom(roomId = A_ROOM_ID)))
|
||||
}
|
||||
)
|
||||
)
|
||||
val presenter = createSecurityAndPrivacyPresenter(
|
||||
room = room,
|
||||
matrixClient = client,
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(
|
||||
FeatureFlags.Knock.key to true,
|
||||
FeatureFlags.SpaceSettings.key to true,
|
||||
)
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
assertThat(awaitItem().isKnockEnabled).isFalse()
|
||||
assertThat(awaitItem().isKnockEnabled).isTrue()
|
||||
skipItems(1)
|
||||
val state = awaitItem()
|
||||
assertThat(state.isSpaceMemberSelectable).isTrue()
|
||||
state.eventSink(SecurityAndPrivacyEvent.SelectSpaceMemberAccess)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.SpaceMember::class.java)
|
||||
val access = editedSettings.roomAccess as SecurityAndPrivacyRoomAccess.SpaceMember
|
||||
assertThat(access.spaceIds).containsExactly(A_ROOM_ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - SelectSpaceMemberAccess with multiple spaces opens ManageAuthorizedSpaces`() = runTest {
|
||||
val openManageAuthorizedSpacesLambda = lambdaRecorder<Boolean, Unit> { }
|
||||
val navigator = FakeSecurityAndPrivacyNavigator(openManageAuthorizedSpacesLambda = openManageAuthorizedSpacesLambda)
|
||||
val room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
initialRoomInfo = aRoomInfo(
|
||||
historyVisibility = RoomHistoryVisibility.Shared,
|
||||
joinRule = JoinRule.Invite
|
||||
)
|
||||
)
|
||||
)
|
||||
val client = FakeMatrixClient(
|
||||
userIdServerNameLambda = { "matrix.org" },
|
||||
spaceService = FakeSpaceService(
|
||||
joinedParentsResult = { _ ->
|
||||
Result.success(listOf(aSpaceRoom(roomId = A_ROOM_ID), aSpaceRoom(roomId = RoomId("!space2:matrix.org"))))
|
||||
}
|
||||
)
|
||||
)
|
||||
val presenter = createSecurityAndPrivacyPresenter(
|
||||
room = room,
|
||||
navigator = navigator,
|
||||
matrixClient = client,
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(
|
||||
FeatureFlags.SpaceSettings.key to true,
|
||||
)
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
val state = awaitItem()
|
||||
assertThat(state.isSpaceMemberSelectable).isTrue()
|
||||
state.eventSink(SecurityAndPrivacyEvent.SelectSpaceMemberAccess)
|
||||
assert(openManageAuthorizedSpacesLambda).isCalledOnce().with(value(false))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - SpaceMember saves as Restricted join rule`() = runTest {
|
||||
val updateJoinRuleLambda = lambdaRecorder<JoinRule, Result<Unit>> { Result.success(Unit) }
|
||||
val updateRoomVisibilityLambda = lambdaRecorder<RoomVisibility, Result<Unit>> { Result.success(Unit) }
|
||||
val room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
|
||||
initialRoomInfo = aRoomInfo(
|
||||
historyVisibility = RoomHistoryVisibility.Shared,
|
||||
joinRule = JoinRule.Invite
|
||||
)
|
||||
),
|
||||
updateJoinRuleResult = updateJoinRuleLambda,
|
||||
updateRoomVisibilityResult = updateRoomVisibilityLambda,
|
||||
)
|
||||
val onDoneLambda = lambdaRecorder<Unit> { }
|
||||
val navigator = FakeSecurityAndPrivacyNavigator(onDoneLambda = onDoneLambda)
|
||||
val presenter = createSecurityAndPrivacyPresenter(room = room, navigator = navigator)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly)
|
||||
val spaceMemberAccess = SecurityAndPrivacyRoomAccess.SpaceMember(
|
||||
spaceIds = persistentListOf(A_ROOM_ID)
|
||||
)
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(spaceMemberAccess))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.SpaceMember::class.java)
|
||||
assertThat(canBeSaved).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvent.Save)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Loading)
|
||||
}
|
||||
room.givenRoomInfo(
|
||||
aRoomInfo(
|
||||
joinRule = JoinRule.Restricted(
|
||||
rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))
|
||||
),
|
||||
historyVisibility = RoomHistoryVisibility.Shared,
|
||||
)
|
||||
)
|
||||
skipItems(2)
|
||||
with(awaitItem()) {
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
}
|
||||
assert(updateJoinRuleLambda).isCalledOnce().with(
|
||||
value(JoinRule.Restricted(rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))))
|
||||
)
|
||||
onDoneLambda.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - room visibility is NOT configurable for SpaceMember`() = runTest {
|
||||
val room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
|
||||
initialRoomInfo = aRoomInfo(
|
||||
historyVisibility = RoomHistoryVisibility.Shared,
|
||||
joinRule = JoinRule.Restricted(
|
||||
rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val presenter = createSecurityAndPrivacyPresenter(room = room)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.SpaceMember::class.java)
|
||||
assertThat(showRoomVisibilitySections).isFalse()
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - KnockRestricted join rule maps to AskToJoinWithSpaceMembers`() = runTest {
|
||||
val room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
initialRoomInfo = aRoomInfo(
|
||||
joinRule = JoinRule.KnockRestricted(
|
||||
rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))
|
||||
),
|
||||
historyVisibility = RoomHistoryVisibility.Shared,
|
||||
)
|
||||
)
|
||||
)
|
||||
val presenter = createSecurityAndPrivacyPresenter(room = room)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember::class.java)
|
||||
val access = editedSettings.roomAccess as SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember
|
||||
assertThat(access.spaceIds).containsExactly(A_ROOM_ID)
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - showAskToJoinWithSpaceMembersOption is true when both FFs enabled and spaces available`() = runTest {
|
||||
val presenter = createSecurityAndPrivacyPresenter(
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(
|
||||
FeatureFlags.Knock.key to true,
|
||||
FeatureFlags.SpaceSettings.key to true,
|
||||
)
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
// Without spaces available, AskToJoinWithSpaceMembers should not be selectable
|
||||
with(awaitItem()) {
|
||||
assertThat(isAskToJoinWithSpaceMembersSelectable).isFalse()
|
||||
assertThat(showAskToJoinWithSpaceMemberOption).isFalse()
|
||||
// AskToJoin should be shown instead
|
||||
assertThat(showAskToJoinOption).isTrue()
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - SelectAskToJoinWithSpaceMembersAccess with multiple spaces opens ManageAuthorizedSpaces`() = runTest {
|
||||
val openManageAuthorizedSpacesLambda = lambdaRecorder<Boolean, Unit> { }
|
||||
val navigator = FakeSecurityAndPrivacyNavigator(openManageAuthorizedSpacesLambda = openManageAuthorizedSpacesLambda)
|
||||
val room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
initialRoomInfo = aRoomInfo(
|
||||
historyVisibility = RoomHistoryVisibility.Shared,
|
||||
joinRule = JoinRule.Invite
|
||||
)
|
||||
)
|
||||
)
|
||||
val client = FakeMatrixClient(
|
||||
userIdServerNameLambda = { "matrix.org" },
|
||||
spaceService = FakeSpaceService(
|
||||
joinedParentsResult = { _ ->
|
||||
Result.success(listOf(aSpaceRoom(roomId = A_ROOM_ID), aSpaceRoom(roomId = RoomId("!space2:matrix.org"))))
|
||||
}
|
||||
)
|
||||
)
|
||||
val presenter = createSecurityAndPrivacyPresenter(
|
||||
room = room,
|
||||
navigator = navigator,
|
||||
matrixClient = client,
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(
|
||||
FeatureFlags.Knock.key to true,
|
||||
FeatureFlags.SpaceSettings.key to true,
|
||||
)
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
// Wait for space selection mode to be set
|
||||
val state = awaitItem()
|
||||
assertThat(state.isAskToJoinWithSpaceMembersSelectable).isTrue()
|
||||
state.eventSink(SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess)
|
||||
assert(openManageAuthorizedSpacesLambda).isCalledOnce().with(value(true))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - AskToJoinWithSpaceMember saves as KnockRestricted join rule`() = runTest {
|
||||
val updateJoinRuleLambda = lambdaRecorder<JoinRule, Result<Unit>> { Result.success(Unit) }
|
||||
val room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
|
||||
initialRoomInfo = aRoomInfo(
|
||||
historyVisibility = RoomHistoryVisibility.Shared,
|
||||
joinRule = JoinRule.Invite
|
||||
)
|
||||
),
|
||||
updateJoinRuleResult = updateJoinRuleLambda,
|
||||
)
|
||||
val onDoneLambda = lambdaRecorder<Unit> { }
|
||||
val navigator = FakeSecurityAndPrivacyNavigator(onDoneLambda = onDoneLambda)
|
||||
val presenter = createSecurityAndPrivacyPresenter(room = room, navigator = navigator)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly)
|
||||
val askToJoinAccess = SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember(
|
||||
spaceIds = persistentListOf(A_ROOM_ID)
|
||||
)
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(askToJoinAccess))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember::class.java)
|
||||
assertThat(canBeSaved).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvent.Save)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Loading)
|
||||
}
|
||||
room.givenRoomInfo(
|
||||
aRoomInfo(
|
||||
joinRule = JoinRule.KnockRestricted(
|
||||
rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))
|
||||
),
|
||||
historyVisibility = RoomHistoryVisibility.Shared,
|
||||
)
|
||||
)
|
||||
// Saved settings are updated multiple times to match the edited settings
|
||||
skipItems(2)
|
||||
with(awaitItem()) {
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
}
|
||||
assert(updateJoinRuleLambda).isCalledOnce().with(
|
||||
value(JoinRule.KnockRestricted(rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))))
|
||||
)
|
||||
onDoneLambda.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - room visibility is configurable for AskToJoinWithSpaceMember`() = runTest {
|
||||
val room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
|
||||
initialRoomInfo = aRoomInfo(
|
||||
historyVisibility = RoomHistoryVisibility.Shared,
|
||||
joinRule = JoinRule.KnockRestricted(
|
||||
rules = persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val presenter = createSecurityAndPrivacyPresenter(room = room)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isInstanceOf(SecurityAndPrivacyRoomAccess.AskToJoinWithSpaceMember::class.java)
|
||||
assertThat(showRoomVisibilitySections).isTrue()
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(false))
|
||||
eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true))
|
||||
assertThat(canBeSaved).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,12 +761,17 @@ class SecurityAndPrivacyPresenterTest {
|
||||
),
|
||||
navigator: SecurityAndPrivacyNavigator = FakeSecurityAndPrivacyNavigator(),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
matrixClient: MatrixClient = FakeMatrixClient(
|
||||
userIdServerNameLambda = { serverName },
|
||||
spaceService = FakeSpaceService(
|
||||
joinedParentsResult = { Result.success(emptyList()) },
|
||||
getSpaceRoomResult = { null }
|
||||
),
|
||||
),
|
||||
): SecurityAndPrivacyPresenter {
|
||||
return SecurityAndPrivacyPresenter(
|
||||
room = room,
|
||||
matrixClient = FakeMatrixClient(
|
||||
userIdServerNameLambda = { serverName },
|
||||
),
|
||||
matrixClient = matrixClient,
|
||||
navigator = navigator,
|
||||
featureFlagService = featureFlagService,
|
||||
)
|
||||
|
||||
@@ -19,8 +19,11 @@ import io.element.android.features.securityandprivacy.impl.root.SecurityAndPriva
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyRoomAccess
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyState
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyView
|
||||
import io.element.android.features.securityandprivacy.impl.root.SpaceSelectionMode
|
||||
import io.element.android.features.securityandprivacy.impl.root.aSecurityAndPrivacySettings
|
||||
import io.element.android.features.securityandprivacy.impl.root.aSecurityAndPrivacyState
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@@ -179,6 +182,48 @@ class SecurityAndPrivacyViewTest {
|
||||
rule.clickOn(R.string.screen_security_and_privacy_enable_encryption_alert_confirm_button_title)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `click on space member access emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
|
||||
val state = aSecurityAndPrivacyState(
|
||||
eventSink = recorder,
|
||||
spaceSelectionMode = SpaceSelectionMode.Single(A_ROOM_ID, null),
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.clickOn(R.string.screen_security_and_privacy_room_access_space_members_option_title)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvent.SelectSpaceMemberAccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `click on ask to join with space members emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
|
||||
val state = aSecurityAndPrivacyState(
|
||||
eventSink = recorder,
|
||||
spaceSelectionMode = SpaceSelectionMode.Single(A_ROOM_ID, null),
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.clickOn(R.string.screen_security_and_privacy_ask_to_join_option_title)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `manage spaces footer is shown when space member access is selected`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>(expectEvents = false)
|
||||
val state = aSecurityAndPrivacyState(
|
||||
eventSink = recorder,
|
||||
spaceSelectionMode = SpaceSelectionMode.Multiple,
|
||||
editedSettings = aSecurityAndPrivacySettings(
|
||||
roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember(persistentListOf(A_ROOM_ID)),
|
||||
),
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
// The footer text uses AnnotatedString with a link. Verify the footer text is displayed.
|
||||
rule.onNodeWithText("Choose which spaces", substring = true).assertExists()
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSecurityAndPrivacyView(
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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.securityandprivacy.impl.manageauthorizedspaces
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class ManageAuthorizedSpacesPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state has empty selection`() = runTest {
|
||||
val presenter = ManageAuthorizedSpacesPresenter()
|
||||
presenter.test {
|
||||
with(awaitItem()) {
|
||||
assertThat(selectedIds).isEmpty()
|
||||
assertThat(isSelectionComplete).isFalse()
|
||||
assertThat(isDoneButtonEnabled).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - SetData event updates selection and initial selectedIds`() = runTest {
|
||||
val presenter = ManageAuthorizedSpacesPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
val roomId = A_ROOM_ID
|
||||
val data = AuthorizedSpacesSelection(
|
||||
joinedSpaces = persistentListOf(),
|
||||
unknownSpaceIds = persistentListOf(),
|
||||
initialSelectedIds = persistentListOf(roomId)
|
||||
)
|
||||
initialState.eventSink(ManageAuthorizedSpacesEvent.SetData(data))
|
||||
// SetData updates two state variables, which may emit intermediate states
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(selection).isEqualTo(data)
|
||||
assertThat(selectedIds).containsExactly(roomId)
|
||||
assertThat(isDoneButtonEnabled).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - ToggleSpace event adds space to selectedIds`() = runTest {
|
||||
val presenter = ManageAuthorizedSpacesPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
val roomId = A_ROOM_ID
|
||||
initialState.eventSink(ManageAuthorizedSpacesEvent.ToggleSpace(roomId))
|
||||
with(awaitItem()) {
|
||||
assertThat(selectedIds).containsExactly(roomId)
|
||||
assertThat(isDoneButtonEnabled).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - ToggleSpace event removes space when already selected`() = runTest {
|
||||
val presenter = ManageAuthorizedSpacesPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
val roomId = A_ROOM_ID
|
||||
initialState.eventSink(ManageAuthorizedSpacesEvent.ToggleSpace(roomId))
|
||||
val stateWithSelection = awaitItem()
|
||||
assertThat(stateWithSelection.selectedIds).containsExactly(roomId)
|
||||
stateWithSelection.eventSink(ManageAuthorizedSpacesEvent.ToggleSpace(roomId))
|
||||
with(awaitItem()) {
|
||||
assertThat(selectedIds).isEmpty()
|
||||
assertThat(isDoneButtonEnabled).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - Done event sets isSelectionComplete to true`() = runTest {
|
||||
val presenter = ManageAuthorizedSpacesPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(ManageAuthorizedSpacesEvent.Done)
|
||||
with(awaitItem()) {
|
||||
assertThat(isSelectionComplete).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* 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.securityandprivacy.impl.manageauthorizedspaces
|
||||
|
||||
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.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||
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 kotlinx.collections.immutable.toImmutableList
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ManageAuthorizedSpacesViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking back invokes callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setManageAuthorizedSpacesView(onBackClick = callback)
|
||||
rule.pressBack()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking space checkbox emits ToggleSpace event`() {
|
||||
val roomId = A_ROOM_ID
|
||||
val space = aSpaceRoom(roomId = roomId, displayName = "Test Space")
|
||||
val recorder = EventsRecorder<ManageAuthorizedSpacesEvent>()
|
||||
val state = aManageAuthorizedSpacesState(
|
||||
selection = anAuthorizedSpaceSelection(
|
||||
joinedSpaces = listOf(space)
|
||||
),
|
||||
eventSink = recorder
|
||||
)
|
||||
rule.setManageAuthorizedSpacesView(state)
|
||||
rule.onNodeWithText("Test Space").performClick()
|
||||
recorder.assertSingle(ManageAuthorizedSpacesEvent.ToggleSpace(roomId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking done button emits Done event`() {
|
||||
val recorder = EventsRecorder<ManageAuthorizedSpacesEvent>()
|
||||
val state = aManageAuthorizedSpacesState(
|
||||
selectedIds = listOf(A_ROOM_ID),
|
||||
eventSink = recorder
|
||||
)
|
||||
rule.setManageAuthorizedSpacesView(state)
|
||||
rule.clickOn(CommonStrings.action_done)
|
||||
recorder.assertSingle(ManageAuthorizedSpacesEvent.Done)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `done button is disabled when no spaces selected`() {
|
||||
val recorder = EventsRecorder<ManageAuthorizedSpacesEvent>(expectEvents = false)
|
||||
val state = aManageAuthorizedSpacesState(
|
||||
selectedIds = emptyList(),
|
||||
eventSink = recorder
|
||||
)
|
||||
rule.setManageAuthorizedSpacesView(state)
|
||||
rule.clickOn(CommonStrings.action_done)
|
||||
recorder.assertEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setManageAuthorizedSpacesView(
|
||||
state: ManageAuthorizedSpacesState = aManageAuthorizedSpacesState(
|
||||
eventSink = EventsRecorder(expectEvents = false)
|
||||
),
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
ManageAuthorizedSpacesView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun aManageAuthorizedSpacesState(
|
||||
selection: AuthorizedSpacesSelection = AuthorizedSpacesSelection(),
|
||||
selectedIds: List<RoomId> = emptyList(),
|
||||
isSelectionComplete: Boolean = false,
|
||||
eventSink: (ManageAuthorizedSpacesEvent) -> Unit = {},
|
||||
) = ManageAuthorizedSpacesState(
|
||||
selection = selection,
|
||||
selectedIds = selectedIds.toImmutableList(),
|
||||
isSelectionComplete = isSelectionComplete,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
Reference in New Issue
Block a user