Merge branch 'release/0.6.3' into main

This commit is contained in:
Benoit Marty 2024-09-19 10:36:36 +02:00
commit cec945cd07
96 changed files with 2597 additions and 369 deletions

View file

@ -1,3 +1,9 @@
Changes in Element X v0.6.2 (2024-09-17)
========================================
### ✨ Features
* Account deactivation. by @bmarty in https://github.com/element-hq/element-x-android/pull/3479
Changes in Element X v0.6.1 (2024-09-17)
========================================

View file

@ -0,0 +1,2 @@
Element X is the new generation of Element for professional and personal use on mobile. Its the fastest Matrix client with a seamless & intuitive user interface.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -33,6 +33,7 @@ dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
api(projects.features.deactivation.api)

View file

@ -65,6 +65,8 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.theme.components.autofill
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.persistentListOf
@ -277,6 +279,7 @@ private fun Content(
.padding(top = 8.dp)
.fillMaxWidth()
.onTabOrEnterKeyFocusNext(focusManager)
.testTag(TestTags.loginPassword)
.autofill(
autofillTypes = listOf(AutofillType.Password),
onFill = {

View file

@ -10,15 +10,26 @@ package io.element.android.features.logout.impl
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.deactivation.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_PASSWORD
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 io.element.android.tests.testutils.pressTag
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 AccountDeactivationViewTest {
@ -36,7 +47,96 @@ class AccountDeactivationViewTest {
}
}
// TODO Add more tests
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on Deactivate emits the expected Event`() {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
rule.setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
password = A_PASSWORD,
),
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_deactivate)
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false))
}
@Test
fun `clicking on Deactivate on the confirmation dialog emits the expected Event`() {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
rule.setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
password = A_PASSWORD,
),
accountDeactivationAction = AsyncAction.Confirming,
eventSink = eventsRecorder,
),
)
rule.pressTag(TestTags.dialogPositive.value)
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false))
}
@Test
fun `clicking on retry on the confirmation dialog emits the expected Event`() {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
rule.setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
password = A_PASSWORD,
),
accountDeactivationAction = AsyncAction.Failure(AN_EXCEPTION),
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_retry)
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(true))
}
@Test
fun `switching on the erase all switch emits the expected Event`() {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
rule.setAccountDeactivationView(
state = anAccountDeactivationState(
eventSink = eventsRecorder,
),
)
rule.clickOn(R.string.screen_deactivate_account_delete_all_messages)
eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(true))
}
@Test
fun `switching off the erase all switch emits the expected Event`() {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
rule.setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
eraseData = true,
),
eventSink = eventsRecorder,
),
)
rule.clickOn(R.string.screen_deactivate_account_delete_all_messages)
eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(false))
}
@Config(qualifiers = "h1024dp")
@Test
fun `typing text in the password field emits the expected Event`() {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
rule.setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
password = A_PASSWORD,
),
eventSink = eventsRecorder,
),
)
rule.onNodeWithTag(TestTags.loginPassword.value).performTextInput("A")
eventsRecorder.assertSingle(AccountDeactivationEvents.SetPassword("A$A_PASSWORD"))
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAccountDeactivationView(

View file

@ -374,7 +374,8 @@ private fun VerifiedUserSendFailureView(
fun VerifiedUserSendFailure.headline(): String {
return when (this) {
is None -> ""
is UnsignedDevice -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_unsigned_device, userDisplayName)
is UnsignedDevice.FromOther -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_unsigned_device, userDisplayName)
is UnsignedDevice.FromYou -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_you_unsigned_device)
is ChangedIdentity -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, userDisplayName)
}
}

View file

@ -13,9 +13,10 @@ import androidx.compose.runtime.Immutable
sealed interface VerifiedUserSendFailure {
data object None : VerifiedUserSendFailure
data class UnsignedDevice(
val userDisplayName: String,
) : VerifiedUserSendFailure
sealed interface UnsignedDevice : VerifiedUserSendFailure {
data object FromYou : UnsignedDevice
data class FromOther(val userDisplayName: String) : UnsignedDevice
}
data class ChangedIdentity(
val userDisplayName: String,

View file

@ -23,8 +23,12 @@ class VerifiedUserSendFailureFactory @Inject constructor(
if (userId == null) {
VerifiedUserSendFailure.None
} else {
val displayName = room.userDisplayName(userId).getOrNull() ?: userId.value
VerifiedUserSendFailure.UnsignedDevice(displayName)
if (userId == room.sessionId) {
VerifiedUserSendFailure.UnsignedDevice.FromYou
} else {
val displayName = room.userDisplayName(userId).getOrNull() ?: userId.value
VerifiedUserSendFailure.UnsignedDevice.FromOther(displayName)
}
}
}
is LocalEventSendState.Failed.VerifiedUserChangedIdentity -> {

View file

@ -36,7 +36,7 @@ fun aResolveVerifiedUserSendFailureState(
eventSink = eventSink
)
fun anUnsignedDeviceSendFailure(userDisplayName: String = "Alice") = VerifiedUserSendFailure.UnsignedDevice(
fun anUnsignedDeviceSendFailure(userDisplayName: String = "Alice") = VerifiedUserSendFailure.UnsignedDevice.FromOther(
userDisplayName = userDisplayName,
)

View file

@ -113,28 +113,33 @@ fun ResolveVerifiedUserSendFailureView(
@Composable
private fun VerifiedUserSendFailure.title(): String {
return when (this) {
is VerifiedUserSendFailure.UnsignedDevice -> stringResource(id = CommonStrings.screen_resolve_send_failure_unsigned_device_title, userDisplayName)
is VerifiedUserSendFailure.UnsignedDevice.FromOther -> stringResource(
id = CommonStrings.screen_resolve_send_failure_unsigned_device_title,
userDisplayName
)
VerifiedUserSendFailure.UnsignedDevice.FromYou -> stringResource(id = CommonStrings.screen_resolve_send_failure_you_unsigned_device_title)
is VerifiedUserSendFailure.ChangedIdentity -> stringResource(
id = CommonStrings.screen_resolve_send_failure_changed_identity_title,
userDisplayName
)
VerifiedUserSendFailure.None -> error("This method should never be called for this state")
VerifiedUserSendFailure.None -> ""
}
}
@Composable
private fun VerifiedUserSendFailure.subtitle(): String {
return when (this) {
is VerifiedUserSendFailure.UnsignedDevice -> stringResource(
is VerifiedUserSendFailure.UnsignedDevice.FromOther -> stringResource(
id = CommonStrings.screen_resolve_send_failure_unsigned_device_subtitle,
userDisplayName,
userDisplayName,
)
VerifiedUserSendFailure.UnsignedDevice.FromYou -> stringResource(id = CommonStrings.screen_resolve_send_failure_you_unsigned_device_subtitle)
is VerifiedUserSendFailure.ChangedIdentity -> stringResource(
id = CommonStrings.screen_resolve_send_failure_changed_identity_subtitle,
userDisplayName
)
VerifiedUserSendFailure.None -> error("This method should never be called for this state")
VerifiedUserSendFailure.None -> ""
}
}
@ -143,7 +148,7 @@ private fun VerifiedUserSendFailure.resolveAction(): String {
return when (this) {
is VerifiedUserSendFailure.UnsignedDevice -> stringResource(id = CommonStrings.screen_resolve_send_failure_unsigned_device_primary_button_title)
is VerifiedUserSendFailure.ChangedIdentity -> stringResource(id = CommonStrings.screen_resolve_send_failure_changed_identity_primary_button_title)
VerifiedUserSendFailure.None -> error("This method should never be called for this state")
VerifiedUserSendFailure.None -> ""
}
}

View file

@ -25,14 +25,12 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
class PinnedMessagesBannerPresenter @Inject constructor(
private val room: MatrixRoom,
@ -123,7 +121,6 @@ class PinnedMessagesBannerPresenter @Inject constructor(
is AsyncData.Loading -> flowOf(AsyncData.Loading())
is AsyncData.Success -> {
asyncTimeline.data.timelineItems
.debounce(300.milliseconds)
.map { timelineItems ->
val pinnedItems = timelineItems.mapNotNull { timelineItem ->
itemFactory.create(timelineItem)

View file

@ -43,14 +43,12 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.time.Duration.Companion.milliseconds
class PinnedMessagesListPresenter @AssistedInject constructor(
@Assisted private val navigator: PinnedMessagesListNavigator,
@ -174,7 +172,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
is AsyncData.Failure -> flowOf(AsyncData.Failure(asyncTimeline.error))
is AsyncData.Loading -> flowOf(AsyncData.Loading())
is AsyncData.Success -> {
val timelineItemsFlow = asyncTimeline.data.timelineItems.debounce(300.milliseconds)
val timelineItemsFlow = asyncTimeline.data.timelineItems
combine(timelineItemsFlow, room.membersStateFlow) { items, membersState ->
timelineItemsFactory.replaceWith(
timelineItems = items,

View file

@ -17,6 +17,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -81,6 +82,7 @@ class TimelinePresenter @AssistedInject constructor(
computeReactions = true,
)
)
private var timelineItems by mutableStateOf<ImmutableList<TimelineItem>>(persistentListOf())
@Composable
override fun present(): TimelineState {
@ -89,9 +91,12 @@ class TimelinePresenter @AssistedInject constructor(
mutableStateOf(FocusRequestState.None)
}
LaunchedEffect(Unit) {
timelineItemsFactory.timelineItems.collect { timelineItems = it }
}
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
val timelineItems by timelineItemsFactory.timelineItems.collectAsState(initial = persistentListOf())
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()

View file

@ -94,7 +94,7 @@ class ResolveVerifiedUserSendFailurePresenterTest {
initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage))
skipItems(1)
awaitItem().also { state ->
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID.value))
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromYou)
state.eventSink(ResolveVerifiedUserSendFailureEvents.Dismiss)
}
skipItems(1)
@ -124,7 +124,7 @@ class ResolveVerifiedUserSendFailurePresenterTest {
skipItems(1)
awaitItem().also { state ->
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID.value))
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromYou)
state.eventSink(ResolveVerifiedUserSendFailureEvents.Retry)
}
awaitItem().also { state ->
@ -158,7 +158,7 @@ class ResolveVerifiedUserSendFailurePresenterTest {
skipItems(1)
awaitItem().also { state ->
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID.value))
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromYou)
state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend)
}
awaitItem().also { state ->
@ -167,7 +167,7 @@ class ResolveVerifiedUserSendFailurePresenterTest {
// This should move to the next user
skipItems(2)
awaitItem().also { state ->
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID_2.value))
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromOther(A_USER_ID_2.value))
assertThat(state.resolveAction).isEqualTo(AsyncAction.Success(Unit))
state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend)
}
@ -199,14 +199,14 @@ class ResolveVerifiedUserSendFailurePresenterTest {
skipItems(1)
awaitItem().also { state ->
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID.value))
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromYou)
state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend)
}
awaitItem().also { state ->
assertThat(state.resolveAction).isEqualTo(AsyncAction.Loading)
}
awaitItem().also { state ->
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID.value))
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromYou)
assertThat(state.resolveAction).isInstanceOf(AsyncAction.Failure::class.java)
}
ensureAllEventsConsumed()

View file

@ -21,7 +21,7 @@ class AppMigration05Test {
val sessionStore = InMemorySessionStore().apply {
updateData(
aSessionData(
sessionId = A_SESSION_ID,
sessionId = A_SESSION_ID.value,
sessionPath = "",
)
)
@ -37,7 +37,7 @@ class AppMigration05Test {
val sessionStore = InMemorySessionStore().apply {
updateData(
aSessionData(
sessionId = A_SESSION_ID,
sessionId = A_SESSION_ID.value,
sessionPath = "/a/path/existing",
)
)

View file

@ -21,7 +21,7 @@ class AppMigration06Test {
val sessionStore = InMemorySessionStore().apply {
updateData(
aSessionData(
sessionId = A_SESSION_ID,
sessionId = A_SESSION_ID.value,
sessionPath = "/a/path/to/a/session/AN_ID",
cachePath = "",
)
@ -38,7 +38,7 @@ class AppMigration06Test {
val sessionStore = InMemorySessionStore().apply {
updateData(
aSessionData(
sessionId = A_SESSION_ID,
sessionId = A_SESSION_ID.value,
cachePath = "/a/path/existing",
)
)

View file

@ -55,6 +55,7 @@ dependencies {
testImplementation(libs.test.mockk)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.sessionStorage.implMemory)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.features.rageshake.test)
testImplementation(projects.tests.testutils)
testImplementation(projects.services.toolbox.test)

View file

@ -17,9 +17,8 @@ import io.element.android.libraries.matrix.test.FakeSdkMetadata
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.network.useragent.DefaultUserAgentProvider
import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@ -94,7 +93,7 @@ class DefaultBugReporterTest {
server.start()
val mockSessionStore = InMemorySessionStore().apply {
storeData(mockSessionData("@foo:eample.com", "ABCDEFGH"))
storeData(aSessionData(sessionId = "@foo:example.com", deviceId = "ABCDEFGH"))
}
val buildMeta = aBuildMeta()
@ -143,7 +142,7 @@ class DefaultBugReporterTest {
assertThat(foundValues["can_contact"]).isEqualTo("true")
assertThat(foundValues["device_id"]).isEqualTo("ABCDEFGH")
assertThat(foundValues["sdk_sha"]).isEqualTo("123456789")
assertThat(foundValues["user_id"]).isEqualTo("@foo:eample.com")
assertThat(foundValues["user_id"]).isEqualTo("@foo:example.com")
assertThat(foundValues["text"]).isEqualTo("a bug occurred")
assertThat(foundValues["device_keys"]).isEqualTo("curve25519:CURVECURVECURVE, ed25519:EDKEYEDKEYEDKY")
@ -163,7 +162,7 @@ class DefaultBugReporterTest {
server.start()
val mockSessionStore = InMemorySessionStore().apply {
storeData(mockSessionData("@foo:eample.com", "ABCDEFGH"))
storeData(aSessionData("@foo:example.com", "ABCDEFGH"))
}
val buildMeta = aBuildMeta()
@ -267,21 +266,6 @@ class DefaultBugReporterTest {
return foundValues
}
private fun mockSessionData(userId: String, deviceId: String) = SessionData(
userId = userId,
deviceId = deviceId,
homeserverUrl = "example.com",
accessToken = "AA",
isTokenValid = true,
loginType = LoginType.DIRECT,
loginTimestamp = null,
oidcData = null,
refreshToken = null,
slidingSyncProxy = null,
passphrase = null,
sessionPath = "session",
cachePath = "cache",
)
@Test
fun `test sendBugReport error`() = runTest {
val server = MockWebServer()

View file

@ -61,7 +61,7 @@ import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
@ -73,6 +73,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
private const val EXTENDED_RANGE_SIZE = 40
private const val SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS = 300L
class RoomListPresenter @Inject constructor(
private val client: MatrixClient,
@ -301,7 +302,10 @@ class RoomListPresenter @Inject constructor(
private var currentUpdateVisibleRangeJob: Job? = null
private fun CoroutineScope.updateVisibleRange(range: IntRange) {
currentUpdateVisibleRangeJob?.cancel()
currentUpdateVisibleRangeJob = launch(SupervisorJob()) {
currentUpdateVisibleRangeJob = launch {
// Debounce the subscription to avoid subscribing to too many rooms
delay(SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS)
if (range.isEmpty()) return@launch
val currentRoomList = roomListDataSource.allRooms.first()
// Use extended range to 'prefetch' the next rooms info

View file

@ -85,13 +85,16 @@ import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import kotlin.time.Duration.Companion.seconds
class RoomListPresenterTest {
@get:Rule
@ -599,6 +602,38 @@ class RoomListPresenterTest {
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - UpdateVisibleRange will cancel the previous subscription if called too soon`() = runTest {
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
val roomListService = FakeRoomListService(subscribeToVisibleRoomsLambda = subscribeToVisibleRoomsLambda)
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val matrixClient = FakeMatrixClient(
roomListService = roomListService,
)
val roomSummary = aRoomSummary(
currentUserMembership = CurrentUserMembership.INVITED
)
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
roomListService.postAllRooms(listOf(roomSummary))
val presenter = createRoomListPresenter(
coroutineScope = scope,
client = matrixClient,
)
presenter.test {
val state = consumeItemsUntilPredicate {
it.contentState is RoomListContentState.Rooms
}.last()
state.eventSink(RoomListEvents.UpdateVisibleRange(IntRange(0, 10)))
// If called again, it will cancel the current one, which should not result in a test failure
state.eventSink(RoomListEvents.UpdateVisibleRange(IntRange(0, 11)))
advanceTimeBy(1.seconds)
subscribeToVisibleRoomsLambda.assertions().isCalledOnce()
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - UpdateVisibleRange subscribes to rooms in visible range`() = runTest {
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
@ -622,10 +657,12 @@ class RoomListPresenterTest {
}.last()
state.eventSink(RoomListEvents.UpdateVisibleRange(IntRange(0, 10)))
advanceTimeBy(1.seconds)
subscribeToVisibleRoomsLambda.assertions().isCalledOnce()
// If called again, it will cancel the current one, which should not result in a test failure
// If called again, it will subscribe to the next items
state.eventSink(RoomListEvents.UpdateVisibleRange(IntRange(0, 11)))
advanceTimeBy(1.seconds)
subscribeToVisibleRoomsLambda.assertions().isCalledExactly(2)
}
}

View file

@ -37,5 +37,6 @@ dependencies {
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.sessionStorage.implMemory)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.tests.testutils)
}

View file

@ -8,7 +8,6 @@
package io.element.android.features.signedout.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionData
@ -20,18 +19,18 @@ open class SignedOutStateProvider : PreviewParameterProvider<SignedOutState> {
)
}
fun aSignedOutState() = SignedOutState(
private fun aSignedOutState() = SignedOutState(
appName = "AppName",
signedOutSession = aSessionData(),
eventSink = {},
)
fun aSessionData(
sessionId: SessionId = SessionId("@alice:server.org"),
private fun aSessionData(
sessionId: String = "@alice:server.org",
isTokenValid: Boolean = false,
): SessionData {
return SessionData(
userId = sessionId.value,
userId = sessionId,
deviceId = "aDeviceId",
accessToken = "anAccessToken",
refreshToken = "aRefreshToken",

View file

@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
import org.junit.Rule

View file

@ -162,7 +162,7 @@ jsoup = "org.jsoup:jsoup:1.18.1"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.46"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.47"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }

View file

@ -149,7 +149,12 @@ object MatrixPatterns {
add(MatrixPatternResult(MatrixPatternType.USER_ID, permalink.userId.toString(), match.range.first, match.range.last + 1))
}
is PermalinkData.RoomLink -> {
add(MatrixPatternResult(MatrixPatternType.ROOM_ALIAS, permalink.roomIdOrAlias.identifier, match.range.first, match.range.last + 1))
when (permalink.roomIdOrAlias) {
is RoomIdOrAlias.Alias -> MatrixPatternType.ROOM_ALIAS
is RoomIdOrAlias.Id -> if (permalink.eventId == null) MatrixPatternType.ROOM_ID else null
}?.let { type ->
add(MatrixPatternResult(type, permalink.roomIdOrAlias.identifier, match.range.first, match.range.last + 1))
}
}
else -> Unit
}

View file

@ -31,7 +31,7 @@ data class MatrixRoomInfo(
val isTombstoned: Boolean,
val isFavorite: Boolean,
val canonicalAlias: RoomAlias?,
val alternativeAliases: ImmutableList<String>,
val alternativeAliases: ImmutableList<RoomAlias>,
val currentUserMembership: CurrentUserMembership,
val inviter: RoomMember?,
val activeMembersCount: Long,
@ -42,7 +42,7 @@ data class MatrixRoomInfo(
val notificationCount: Long,
val userDefinedNotificationMode: RoomNotificationMode?,
val hasRoomCall: Boolean,
val activeRoomCallParticipants: ImmutableList<String>,
val activeRoomCallParticipants: ImmutableList<UserId>,
val heroes: ImmutableList<MatrixUser>,
val pinnedEventIds: ImmutableList<EventId>,
val creator: UserId?,

View file

@ -47,7 +47,10 @@ dependencies {
testImplementation(libs.test.truth)
testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.sessionStorage.implMemory)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.services.toolbox.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.coroutines.test)
testImplementation(libs.test.turbine)

View file

@ -50,7 +50,6 @@ class RustClientSessionDelegate(
*/
fun bindClient(client: RustMatrixClient) {
this.client = client
client.setDelegate(this)
}
override fun saveSessionInKeychain(session: Session) {

View file

@ -122,7 +122,7 @@ class RustMatrixClient(
private val baseDirectory: File,
baseCacheDirectory: File,
private val clock: SystemClock,
sessionDelegate: RustClientSessionDelegate,
private val sessionDelegate: RustClientSessionDelegate,
) : MatrixClient {
override val sessionId: UserId = UserId(client.userId())
override val deviceId: DeviceId = DeviceId(client.deviceId())
@ -195,7 +195,7 @@ class RustMatrixClient(
private val roomMembershipObserver = RoomMembershipObserver()
private val clientDelegateTaskHandle: TaskHandle? = client.setDelegate(sessionDelegate)
private var clientDelegateTaskHandle: TaskHandle? = client.setDelegate(sessionDelegate)
private val _userProfile: MutableStateFlow<MatrixUser> = MutableStateFlow(
MatrixUser(
@ -449,12 +449,12 @@ class RustMatrixClient(
override fun close() {
appCoroutineScope.launch {
roomFactory.destroy()
rustSyncService.destroy()
}
sessionCoroutineScope.cancel()
clientDelegateTaskHandle?.cancelAndDestroy()
notificationSettingsService.destroy()
verificationService.destroy()
syncService.destroy()
innerRoomListService.destroy()
notificationClient.destroy()
notificationProcessSetup.destroy()
@ -473,7 +473,9 @@ class RustMatrixClient(
override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean): String? {
var result: String? = null
syncService.stop()
// Remove current delegate so we don't receive an auth error
clientDelegateTaskHandle?.cancelAndDestroy()
clientDelegateTaskHandle = null
withContext(sessionDispatcher) {
if (userInitiated) {
try {
@ -482,12 +484,15 @@ class RustMatrixClient(
if (ignoreSdkError) {
Timber.e(failure, "Fail to call logout on HS. Still delete local files.")
} else {
// If the logout failed we need to restore the delegate
clientDelegateTaskHandle = client.setDelegate(sessionDelegate)
Timber.e(failure, "Fail to call logout on HS.")
throw failure
}
}
}
close()
deleteSessionDirectory(deleteCryptoDb = true)
if (userInitiated) {
sessionStore.removeSession(sessionId.value)
@ -506,7 +511,9 @@ class RustMatrixClient(
override suspend fun deactivateAccount(password: String, eraseData: Boolean): Result<Unit> = withContext(sessionDispatcher) {
Timber.w("Deactivating account")
syncService.stop()
// Remove current delegate so we don't receive an auth error
clientDelegateTaskHandle?.cancelAndDestroy()
clientDelegateTaskHandle = null
runCatching {
// First call without AuthData, should fail
val firstAttempt = runCatching {
@ -518,15 +525,22 @@ class RustMatrixClient(
if (firstAttempt.isFailure) {
Timber.w(firstAttempt.exceptionOrNull(), "Expected failure, try again")
// This is expected, try again with the password
client.deactivateAccount(
authData = AuthData.Password(
passwordDetails = AuthDataPasswordDetails(
identifier = sessionId.value,
password = password,
runCatching {
client.deactivateAccount(
authData = AuthData.Password(
passwordDetails = AuthDataPasswordDetails(
identifier = sessionId.value,
password = password,
),
),
),
eraseData = eraseData,
)
eraseData = eraseData,
)
}.onFailure {
Timber.e(it, "Failed to deactivate account")
// If the deactivation failed we need to restore the delegate
clientDelegateTaskHandle = client.setDelegate(sessionDelegate)
throw it
}
}
close()
deleteSessionDirectory(deleteCryptoDb = true)
@ -592,10 +606,6 @@ class RustMatrixClient(
return client.session().slidingSyncVersion == SlidingSyncVersion.Native
}
internal fun setDelegate(delegate: RustClientSessionDelegate) {
client.setDelegate(delegate)
}
private suspend fun File.getCacheSize(
includeCryptoDb: Boolean = false,
): Long = withContext(sessionDispatcher) {

View file

@ -10,9 +10,10 @@ package io.element.android.libraries.matrix.impl.analytics
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom
private fun Long?.toAnalyticsRoomSize(): JoinedRoom.RoomSize {
private fun Long.toAnalyticsRoomSize(): JoinedRoom.RoomSize {
return when (this) {
null,
0L,
1L -> JoinedRoom.RoomSize.One
2L -> JoinedRoom.RoomSize.Two
in 3..10 -> JoinedRoom.RoomSize.ThreeToTen
in 11..100 -> JoinedRoom.RoomSize.ElevenToOneHundred

View file

@ -8,14 +8,21 @@
package io.element.android.libraries.matrix.impl.auth
import io.element.android.libraries.matrix.api.auth.AuthenticationException
import org.matrix.rustcomponents.sdk.ClientBuildException as RustAuthenticationException
import org.matrix.rustcomponents.sdk.ClientBuildException
fun Throwable.mapAuthenticationException(): AuthenticationException {
val message = this.message ?: "Unknown error"
return when (this) {
is RustAuthenticationException.Generic -> AuthenticationException.Generic(message)
is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(message)
is RustAuthenticationException.SlidingSyncVersion -> AuthenticationException.SlidingSyncVersion(message)
is ClientBuildException -> when (this) {
is ClientBuildException.Generic -> AuthenticationException.Generic(message)
is ClientBuildException.InvalidServerName -> AuthenticationException.InvalidServerName(message)
is ClientBuildException.SlidingSyncVersion -> AuthenticationException.SlidingSyncVersion(message)
is ClientBuildException.Sdk -> AuthenticationException.Generic(message)
is ClientBuildException.ServerUnreachable -> AuthenticationException.Generic(message)
is ClientBuildException.SlidingSync -> AuthenticationException.Generic(message)
is ClientBuildException.WellKnownDeserializationException -> AuthenticationException.Generic(message)
is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message)
}
else -> AuthenticationException.Generic(message)
}
}

View file

@ -42,6 +42,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientBuildException
import org.matrix.rustcomponents.sdk.ClientBuilder
import org.matrix.rustcomponents.sdk.HumanQrLoginException
import org.matrix.rustcomponents.sdk.OidcConfiguration
@ -251,18 +252,17 @@ class RustMatrixAuthenticationService @Inject constructor(
oidcConfiguration = oidcConfiguration,
progressListener = progressListener,
)
client.use { rustClient ->
val sessionData = rustClient.session()
val sessionData = client.use { rustClient ->
rustClient.session()
.toSessionData(
isTokenValid = true,
loginType = LoginType.QR,
passphrase = pendingPassphrase,
sessionPaths = emptySessionPaths,
)
sessionStore.storeData(sessionData)
SessionId(sessionData.userId)
}
sessionStore.storeData(sessionData)
SessionId(sessionData.userId)
}.mapFailure {
when (it) {
is QrCodeDecodeException -> QrErrorMapper.map(it)
@ -285,15 +285,15 @@ class RustMatrixAuthenticationService @Inject constructor(
if (slidingSyncType is ClientBuilderSlidingSync.Simplified) {
Timber.d("Creating client with simplified sliding sync")
try {
return rustMatrixClientFactory
.getBaseClientBuilder(
sessionPaths = sessionPaths,
passphrase = pendingPassphrase,
slidingSyncType = slidingSyncType,
)
.run { config() }
.build()
} catch (e: HumanQrLoginException.SlidingSyncNotAvailable) {
return rustMatrixClientFactory
.getBaseClientBuilder(
sessionPaths = sessionPaths,
passphrase = pendingPassphrase,
slidingSyncType = slidingSyncType,
)
.config()
.build()
} catch (e: ClientBuildException.SlidingSyncVersion) {
Timber.e(e, "Failed to create client with simplified sliding sync, trying with Proxy now")
}
}
@ -304,7 +304,7 @@ class RustMatrixAuthenticationService @Inject constructor(
passphrase = pendingPassphrase,
slidingSyncType = getSlidingSyncProxy(),
)
.run { config() }
.config()
.build()
}

View file

@ -14,13 +14,17 @@ import org.matrix.rustcomponents.sdk.RecoveryException as RustRecoveryException
fun Throwable.mapRecoveryException(): RecoveryException {
return when (this) {
is RustRecoveryException.SecretStorage -> RecoveryException.SecretStorage(
message = errorMessage
)
is RustRecoveryException.BackupExistsOnServer -> RecoveryException.BackupExistsOnServer
is RustRecoveryException.Client -> RecoveryException.Client(
source.mapClientException()
)
is RustRecoveryException -> {
when (this) {
is RustRecoveryException.SecretStorage -> RecoveryException.SecretStorage(
message = errorMessage
)
is RustRecoveryException.BackupExistsOnServer -> RecoveryException.BackupExistsOnServer
is RustRecoveryException.Client -> RecoveryException.Client(
source.mapClientException()
)
}
}
else -> RecoveryException.Client(
ClientException.Other("Unknown error")
)

View file

@ -12,7 +12,11 @@ import org.matrix.rustcomponents.sdk.ClientException as RustClientException
fun Throwable.mapClientException(): ClientException {
return when (this) {
is RustClientException.Generic -> ClientException.Generic(msg)
is RustClientException -> {
when (this) {
is RustClientException.Generic -> ClientException.Generic(msg)
}
}
else -> ClientException.Other(message ?: "Unknown error")
}
}

View file

@ -39,7 +39,7 @@ class MatrixRoomInfoMapper {
isTombstoned = it.isTombstoned,
isFavorite = it.isFavourite,
canonicalAlias = it.canonicalAlias?.let(::RoomAlias),
alternativeAliases = it.alternativeAliases.toImmutableList(),
alternativeAliases = it.alternativeAliases.map(::RoomAlias).toImmutableList(),
currentUserMembership = it.membership.map(),
inviter = it.inviter?.let(RoomMemberMapper::map),
activeMembersCount = it.activeMembersCount.toLong(),
@ -50,7 +50,7 @@ class MatrixRoomInfoMapper {
notificationCount = it.notificationCount.toLong(),
userDefinedNotificationMode = it.cachedUserDefinedNotificationMode?.map(),
hasRoomCall = it.hasRoomCall,
activeRoomCallParticipants = it.activeRoomCallParticipants.toImmutableList(),
activeRoomCallParticipants = it.activeRoomCallParticipants.map(::UserId).toImmutableList(),
heroes = it.elementHeroes().toImmutableList(),
pinnedEventIds = it.pinnedEventIds.map(::EventId).toImmutableList(),
)

View file

@ -21,13 +21,17 @@ class RoomDescriptionMapper {
topic = roomDescription.topic,
avatarUrl = roomDescription.avatarUrl,
alias = roomDescription.alias?.let(::RoomAlias),
joinRule = when (roomDescription.joinRule) {
PublicRoomJoinRule.PUBLIC -> RoomDescription.JoinRule.PUBLIC
PublicRoomJoinRule.KNOCK -> RoomDescription.JoinRule.KNOCK
null -> RoomDescription.JoinRule.UNKNOWN
},
joinRule = roomDescription.joinRule.map(),
isWorldReadable = roomDescription.isWorldReadable,
numberOfMembers = roomDescription.joinedMembers.toLong(),
)
}
}
internal fun PublicRoomJoinRule?.map(): RoomDescription.JoinRule {
return when (this) {
PublicRoomJoinRule.PUBLIC -> RoomDescription.JoinRule.PUBLIC
PublicRoomJoinRule.KNOCK -> RoomDescription.JoinRule.KNOCK
null -> RoomDescription.JoinRule.UNKNOWN
}
}

View file

@ -7,8 +7,11 @@
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
@Suppress("unused")
@ExcludeFromCoverage
internal fun RoomListEntriesUpdate.describe(): String {
return when (this) {
is RoomListEntriesUpdate.Set -> {

View file

@ -53,11 +53,7 @@ internal class RustRoomListService(
}
override suspend fun subscribeToVisibleRooms(roomIds: List<RoomId>) {
val toSubscribe = roomIds.filterNot { roomSyncSubscriber.isSubscribedTo(it) }
if (toSubscribe.isNotEmpty()) {
Timber.d("Subscribe to ${toSubscribe.size} rooms: $toSubscribe")
roomSyncSubscriber.batchSubscribe(toSubscribe)
}
roomSyncSubscriber.batchSubscribe(roomIds)
}
override val allRooms: DynamicRoomList = roomListFactory.createRoomList(

View file

@ -16,15 +16,22 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import org.matrix.rustcomponents.sdk.SyncServiceInterface
import org.matrix.rustcomponents.sdk.SyncServiceState
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import org.matrix.rustcomponents.sdk.SyncService as InnerSyncService
class RustSyncService(
private val innerSyncService: SyncServiceInterface,
private val innerSyncService: InnerSyncService,
sessionCoroutineScope: CoroutineScope
) : SyncService {
private val isServiceReady = AtomicBoolean(true)
override suspend fun startSync() = runCatching {
if (!isServiceReady.get()) {
Timber.d("Can't start sync: service is not ready")
return@runCatching
}
Timber.i("Start sync")
innerSyncService.start()
}.onFailure {
@ -32,12 +39,24 @@ class RustSyncService(
}
override suspend fun stopSync() = runCatching {
if (!isServiceReady.get()) {
Timber.d("Can't stop sync: service is not ready")
return@runCatching
}
Timber.i("Stop sync")
innerSyncService.stop()
}.onFailure {
Timber.d("Stop sync failed: $it")
}
suspend fun destroy() {
// If the service was still running, stop it
stopSync()
Timber.d("Destroying sync service")
isServiceReady.set(false)
innerSyncService.destroy()
}
override val syncState: StateFlow<SyncState> =
innerSyncService.stateFlow()
.map(SyncServiceState::toSyncState)

View file

@ -100,7 +100,8 @@ internal class MatrixTimelineDiffProcessor(
clear()
}
TimelineChange.TRUNCATE -> {
// Not supported
val index = diff.truncate() ?: return
subList(index.toInt(), size).clear()
}
}
}

View file

@ -206,6 +206,7 @@ class RustSessionVerificationService(
}
}
}
private suspend fun updateVerificationStatus() {
if (verificationFlowState.value == VerificationFlowState.Finished) {
// Calling `encryptionService.verificationState()` performs a network call and it will deadlock if there is no network

View file

@ -0,0 +1,91 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.analytics
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import org.junit.Test
class JoinedRoomExtKtTest {
@Test
fun `test room size mapping`() {
mapOf(
listOf(0L, 1L) to JoinedRoom.RoomSize.One,
listOf(2L, 2L) to JoinedRoom.RoomSize.Two,
listOf(3L, 10L) to JoinedRoom.RoomSize.ThreeToTen,
listOf(11L, 100L) to JoinedRoom.RoomSize.ElevenToOneHundred,
listOf(101L, 1000L) to JoinedRoom.RoomSize.OneHundredAndOneToAThousand,
listOf(1001L, 2000L) to JoinedRoom.RoomSize.MoreThanAThousand
).forEach { (joinedMemberCounts, expectedRoomSize) ->
joinedMemberCounts.forEach { joinedMemberCount ->
assertThat(aMatrixRoom(joinedMemberCount = joinedMemberCount).toAnalyticsJoinedRoom(null))
.isEqualTo(
JoinedRoom(
isDM = false,
isSpace = false,
roomSize = expectedRoomSize,
trigger = null
)
)
}
}
}
@Test
fun `test isDirect parameter mapping`() {
assertThat(aMatrixRoom(isDirect = true).toAnalyticsJoinedRoom(null))
.isEqualTo(
JoinedRoom(
isDM = true,
isSpace = false,
roomSize = JoinedRoom.RoomSize.One,
trigger = null
)
)
}
@Test
fun `test isSpace parameter mapping`() {
assertThat(aMatrixRoom(isSpace = true).toAnalyticsJoinedRoom(null))
.isEqualTo(
JoinedRoom(
isDM = false,
isSpace = true,
roomSize = JoinedRoom.RoomSize.One,
trigger = null
)
)
}
@Test
fun `test trigger parameter mapping`() {
assertThat(aMatrixRoom().toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
.isEqualTo(
JoinedRoom(
isDM = false,
isSpace = false,
roomSize = JoinedRoom.RoomSize.One,
trigger = JoinedRoom.Trigger.Invite
)
)
}
private fun aMatrixRoom(
isDirect: Boolean = false,
isSpace: Boolean = false,
joinedMemberCount: Long = 0
): MatrixRoom {
return FakeMatrixRoom(
isDirect = isDirect,
isSpace = isSpace,
joinedMemberCount = joinedMemberCount
)
}
}

View file

@ -36,13 +36,24 @@ class AuthenticationExceptionMappingTest {
assertThat(ClientBuildException.InvalidServerName("Invalid server name").mapAuthenticationException())
.isException<AuthenticationException.InvalidServerName>("Invalid server name")
assertThat(ClientBuildException.Sdk("SDK issue").mapAuthenticationException())
.isException<AuthenticationException.Generic>("SDK issue")
assertThat(ClientBuildException.SlidingSyncVersion("Sliding sync not available").mapAuthenticationException())
.isException<AuthenticationException.SlidingSyncVersion>("Sliding sync not available")
}
@Test
fun `mapping other exceptions map to the Generic Kotlin`() {
assertThat(ClientBuildException.Sdk("SDK issue").mapAuthenticationException())
.isException<AuthenticationException.Generic>("SDK issue")
assertThat(ClientBuildException.ServerUnreachable("Server unreachable").mapAuthenticationException())
.isException<AuthenticationException.Generic>("Server unreachable")
assertThat(ClientBuildException.SlidingSync("Sliding Sync").mapAuthenticationException())
.isException<AuthenticationException.Generic>("Sliding Sync")
assertThat(ClientBuildException.WellKnownDeserializationException("WellKnown Deserialization").mapAuthenticationException())
.isException<AuthenticationException.Generic>("WellKnown Deserialization")
assertThat(ClientBuildException.WellKnownLookupFailed("WellKnown Lookup Failed").mapAuthenticationException())
.isException<AuthenticationException.Generic>("WellKnown Lookup Failed")
}
private inline fun <reified T> ThrowableSubject.isException(message: String) {
isInstanceOf(T::class.java)
hasMessageThat().isEqualTo(message)

View file

@ -0,0 +1,37 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.auth.qrlogin
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeDecodeException
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
import org.junit.Test
import org.matrix.rustcomponents.sdk.HumanQrLoginException as RustHumanQrLoginException
import org.matrix.rustcomponents.sdk.QrCodeDecodeException as RustQrCodeDecodeException
class QrErrorMapperTest {
@Test
fun `test map QrCodeDecodeException`() {
val result = QrErrorMapper.map(RustQrCodeDecodeException.Crypto("test"))
assertThat(result).isInstanceOf(QrCodeDecodeException.Crypto::class.java)
assertThat(result.message).isEqualTo("test")
}
@Test
fun `test map HumanQrLoginException`() {
assertThat(QrErrorMapper.map(RustHumanQrLoginException.Cancelled())).isEqualTo(QrLoginException.Cancelled)
assertThat(QrErrorMapper.map(RustHumanQrLoginException.ConnectionInsecure())).isEqualTo(QrLoginException.ConnectionInsecure)
assertThat(QrErrorMapper.map(RustHumanQrLoginException.Declined())).isEqualTo(QrLoginException.Declined)
assertThat(QrErrorMapper.map(RustHumanQrLoginException.Expired())).isEqualTo(QrLoginException.Expired)
assertThat(QrErrorMapper.map(RustHumanQrLoginException.OtherDeviceNotSignedIn())).isEqualTo(QrLoginException.OtherDeviceNotSignedIn)
assertThat(QrErrorMapper.map(RustHumanQrLoginException.LinkingNotSupported())).isEqualTo(QrLoginException.LinkingNotSupported)
assertThat(QrErrorMapper.map(RustHumanQrLoginException.Unknown())).isEqualTo(QrLoginException.Unknown)
assertThat(QrErrorMapper.map(RustHumanQrLoginException.OidcMetadataInvalid())).isEqualTo(QrLoginException.OidcMetadataInvalid)
assertThat(QrErrorMapper.map(RustHumanQrLoginException.SlidingSyncNotAvailable())).isEqualTo(QrLoginException.SlidingSyncNotAvailable)
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.auth.qrlogin
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
import org.junit.Test
import org.matrix.rustcomponents.sdk.QrLoginProgress
class QrLoginProgressExtensionsKtTest {
@Test
fun `mapping QrLoginProgress should return expected result`() {
assertThat(QrLoginProgress.Starting.toStep())
.isEqualTo(QrCodeLoginStep.Starting)
assertThat(QrLoginProgress.EstablishingSecureChannel(1u, "01").toStep())
.isEqualTo(QrCodeLoginStep.EstablishingSecureChannel("01"))
assertThat(QrLoginProgress.WaitingForToken("userCode").toStep())
.isEqualTo(QrCodeLoginStep.WaitingForToken("userCode"))
assertThat(QrLoginProgress.Done.toStep())
.isEqualTo(QrCodeLoginStep.Finished)
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.core
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.ProgressCallback
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.rustcomponents.sdk.TransmissionProgress
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class ProgressWatcherWrapperKtTest {
@Test
fun testToProgressWatcher() = runTest {
suspendCoroutine { continuation ->
val callback = object : ProgressCallback {
override fun onProgress(current: Long, total: Long) {
assertThat(current).isEqualTo(1)
assertThat(total).isEqualTo(2)
continuation.resume(Unit)
}
}
val result = callback.toProgressWatcher()
result.transmissionProgress(TransmissionProgress(1.toULong(), 2.toULong()))
}
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.di
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.matrix.api.tracing.TracingFilterConfigurations
import io.element.android.libraries.matrix.test.core.aBuildMeta
import org.junit.Test
class TracingMatrixModuleTest {
@Test
fun `providesTracingFilterConfiguration returns debug config for debug build`() {
assertThat(TracingMatrixModule.providesTracingFilterConfiguration(aBuildMeta(BuildType.DEBUG)))
.isEqualTo(TracingFilterConfigurations.debug)
}
@Test
fun `providesTracingFilterConfiguration returns nightly config for nightly build`() {
assertThat(TracingMatrixModule.providesTracingFilterConfiguration(aBuildMeta(BuildType.NIGHTLY)))
.isEqualTo(TracingFilterConfigurations.nightly)
}
@Test
fun `providesTracingFilterConfiguration returns release config for release build`() {
assertThat(TracingMatrixModule.providesTracingFilterConfiguration(aBuildMeta(BuildType.RELEASE)))
.isEqualTo(TracingFilterConfigurations.release)
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.encryption
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.encryption.BackupState
import org.junit.Test
import org.matrix.rustcomponents.sdk.BackupState as RustBackupState
class BackupStateMapperTest {
@Test
fun `Ensure that mapping is correct`() {
val sut = BackupStateMapper()
assertThat(sut.map(RustBackupState.UNKNOWN)).isEqualTo(BackupState.UNKNOWN)
assertThat(sut.map(RustBackupState.CREATING)).isEqualTo(BackupState.CREATING)
assertThat(sut.map(RustBackupState.ENABLING)).isEqualTo(BackupState.ENABLING)
assertThat(sut.map(RustBackupState.RESUMING)).isEqualTo(BackupState.RESUMING)
assertThat(sut.map(RustBackupState.ENABLED)).isEqualTo(BackupState.ENABLED)
assertThat(sut.map(RustBackupState.DOWNLOADING)).isEqualTo(BackupState.DOWNLOADING)
assertThat(sut.map(RustBackupState.DISABLING)).isEqualTo(BackupState.DISABLING)
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.encryption
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import org.junit.Test
import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState
class BackupUploadStateMapperTest {
@Test
fun `Ensure that mapping is correct`() {
val sut = BackupUploadStateMapper()
assertThat(sut.map(RustBackupUploadState.Waiting))
.isEqualTo(BackupUploadState.Waiting)
assertThat(sut.map(RustBackupUploadState.Error))
.isEqualTo(BackupUploadState.Error)
assertThat(sut.map(RustBackupUploadState.Done))
.isEqualTo(BackupUploadState.Done)
assertThat(sut.map(RustBackupUploadState.Uploading(1.toUInt(), 2.toUInt())))
.isEqualTo(BackupUploadState.Uploading(1, 2))
}
@Test
fun `Ensure that full uploading is mapper to Done`() {
val sut = BackupUploadStateMapper()
assertThat(sut.map(RustBackupUploadState.Uploading(2.toUInt(), 2.toUInt())))
.isEqualTo(BackupUploadState.Done)
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.encryption
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
import org.junit.Test
import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress
class EnableRecoveryProgressMapperTest {
@Test
fun `Ensure that mapping is correct`() {
val sut = EnableRecoveryProgressMapper()
assertThat(sut.map(RustEnableRecoveryProgress.CreatingRecoveryKey))
.isEqualTo(EnableRecoveryProgress.CreatingRecoveryKey)
assertThat(sut.map(RustEnableRecoveryProgress.CreatingBackup))
.isEqualTo(EnableRecoveryProgress.CreatingBackup)
assertThat(sut.map(RustEnableRecoveryProgress.Starting))
.isEqualTo(EnableRecoveryProgress.Starting)
assertThat(sut.map(RustEnableRecoveryProgress.BackingUp(1.toUInt(), 2.toUInt())))
.isEqualTo(EnableRecoveryProgress.BackingUp(1, 2))
assertThat(sut.map(RustEnableRecoveryProgress.RoomKeyUploadError))
.isEqualTo(EnableRecoveryProgress.RoomKeyUploadError)
assertThat(sut.map(RustEnableRecoveryProgress.Done("recoveryKey")))
.isEqualTo(EnableRecoveryProgress.Done("recoveryKey"))
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.encryption
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import org.junit.Test
import org.matrix.rustcomponents.sdk.RecoveryState as RustRecoveryState
class RecoveryStateMapperTest {
@Test
fun `Ensure that mapping is correct`() {
val sut = RecoveryStateMapper()
assertThat(sut.map(RustRecoveryState.UNKNOWN)).isEqualTo(RecoveryState.UNKNOWN)
assertThat(sut.map(RustRecoveryState.ENABLED)).isEqualTo(RecoveryState.ENABLED)
assertThat(sut.map(RustRecoveryState.DISABLED)).isEqualTo(RecoveryState.DISABLED)
assertThat(sut.map(RustRecoveryState.INCOMPLETE)).isEqualTo(RecoveryState.INCOMPLETE)
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.factories
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
import io.element.android.libraries.matrix.test.A_ROOM_ID
import org.matrix.rustcomponents.sdk.PublicRoomJoinRule
import org.matrix.rustcomponents.sdk.RoomDescription
internal fun aRustRoomDescription(): RoomDescription {
return RoomDescription(
roomId = A_ROOM_ID.value,
name = "name",
topic = "topic",
alias = A_ROOM_ALIAS.value,
avatarUrl = "avatarUrl",
joinRule = PublicRoomJoinRule.PUBLIC,
isWorldReadable = true,
joinedMembers = 2u
)
}

View file

@ -0,0 +1,22 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.factories
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.test.A_USER_ID
import org.matrix.rustcomponents.sdk.RoomHero
internal fun aRustRoomHero(
userId: UserId = A_USER_ID,
): RoomHero {
return RoomHero(
userId = userId.value,
displayName = "displayName",
avatarUrl = "avatarUrl",
)
}

View file

@ -0,0 +1,81 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.factories
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.RoomHero
import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.RoomNotificationMode
fun aRustRoomInfo(
id: String = A_ROOM_ID.value,
displayName: String? = A_ROOM_NAME,
rawName: String? = A_ROOM_NAME,
topic: String? = null,
avatarUrl: String? = null,
isDirect: Boolean = false,
isPublic: Boolean = false,
isSpace: Boolean = false,
isTombstoned: Boolean = false,
isFavourite: Boolean = false,
canonicalAlias: String? = null,
alternativeAliases: List<String> = listOf(),
membership: Membership = Membership.JOINED,
inviter: RoomMember? = null,
heroes: List<RoomHero> = listOf(),
activeMembersCount: ULong = 0uL,
invitedMembersCount: ULong = 0uL,
joinedMembersCount: ULong = 0uL,
userPowerLevels: Map<String, Long> = mapOf(),
highlightCount: ULong = 0uL,
notificationCount: ULong = 0uL,
userDefinedNotificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
activeRoomCallParticipants: List<String> = listOf(),
isMarkedUnread: Boolean = false,
numUnreadMessages: ULong = 0uL,
numUnreadNotifications: ULong = 0uL,
numUnreadMentions: ULong = 0uL,
pinnedEventIds: List<String> = listOf(),
roomCreator: UserId? = null,
) = RoomInfo(
id = id,
displayName = displayName,
rawName = rawName,
topic = topic,
avatarUrl = avatarUrl,
isDirect = isDirect,
isPublic = isPublic,
isSpace = isSpace,
isTombstoned = isTombstoned,
isFavourite = isFavourite,
canonicalAlias = canonicalAlias,
alternativeAliases = alternativeAliases,
membership = membership,
inviter = inviter,
heroes = heroes,
activeMembersCount = activeMembersCount,
invitedMembersCount = invitedMembersCount,
joinedMembersCount = joinedMembersCount,
userPowerLevels = userPowerLevels,
highlightCount = highlightCount,
notificationCount = notificationCount,
cachedUserDefinedNotificationMode = userDefinedNotificationMode,
hasRoomCall = hasRoomCall,
activeRoomCallParticipants = activeRoomCallParticipants,
isMarkedUnread = isMarkedUnread,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
numUnreadMentions = numUnreadMentions,
pinnedEventIds = pinnedEventIds,
creator = roomCreator?.value,
)

View file

@ -0,0 +1,34 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.factories
import io.element.android.libraries.matrix.api.core.UserId
import org.matrix.rustcomponents.sdk.MembershipState
import org.matrix.rustcomponents.sdk.RoomMember
import uniffi.matrix_sdk.RoomMemberRole
fun aRustRoomMember(
userId: UserId,
displayName: String? = null,
avatarUrl: String? = null,
membership: MembershipState = MembershipState.JOIN,
isNameAmbiguous: Boolean = false,
powerLevel: Long = 0L,
isIgnored: Boolean = false,
role: RoomMemberRole = RoomMemberRole.USER,
) = RoomMember(
userId = userId.value,
displayName = displayName,
avatarUrl = avatarUrl,
membership = membership,
isNameAmbiguous = isNameAmbiguous,
powerLevel = powerLevel,
normalizedPowerLevel = powerLevel,
isIgnored = isIgnored,
suggestedRoleForPowerLevel = role,
)

View file

@ -0,0 +1,34 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.factories
import org.matrix.rustcomponents.sdk.RoomPowerLevels
internal fun aRustRoomPowerLevels(
ban: Long,
invite: Long,
kick: Long,
redact: Long,
eventsDefault: Long,
stateDefault: Long,
usersDefault: Long,
roomName: Long,
roomAvatar: Long,
roomTopic: Long,
) = RoomPowerLevels(
ban = ban,
invite = invite,
kick = kick,
redact = redact,
eventsDefault = eventsDefault,
stateDefault = stateDefault,
usersDefault = usersDefault,
roomName = roomName,
roomAvatar = roomAvatar,
roomTopic = roomTopic,
)

View file

@ -0,0 +1,35 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.factories
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
import io.element.android.libraries.matrix.test.A_ROOM_ID
import org.matrix.rustcomponents.sdk.RoomPreview
internal fun aRustRoomPreview(
canonicalAlias: String? = A_ROOM_ALIAS.value,
isJoined: Boolean = true,
isInvited: Boolean = true,
isPublic: Boolean = true,
canKnock: Boolean = true,
): RoomPreview {
return RoomPreview(
roomId = A_ROOM_ID.value,
canonicalAlias = canonicalAlias,
name = "name",
topic = "topic",
avatarUrl = "avatarUrl",
numJoinedMembers = 1u,
roomType = null,
isHistoryWorldReadable = true,
isJoined = isJoined,
isInvited = isInvited,
isPublic = isPublic,
canKnock = canKnock,
)
}

View file

@ -0,0 +1,19 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.factories
import org.matrix.rustcomponents.sdk.SearchUsersResults
import org.matrix.rustcomponents.sdk.UserProfile
internal fun aRustSearchUsersResults(
results: List<UserProfile>,
limited: Boolean,
) = SearchUsersResults(
results = results,
limited = limited,
)

View file

@ -0,0 +1,28 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.factories
import io.element.android.libraries.matrix.test.A_DEVICE_ID
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.A_USER_ID
import org.matrix.rustcomponents.sdk.Session
import org.matrix.rustcomponents.sdk.SlidingSyncVersion
internal fun aRustSession(
proxy: SlidingSyncVersion = SlidingSyncVersion.None
): Session {
return Session(
accessToken = "accessToken",
refreshToken = "refreshToken",
userId = A_USER_ID.value,
deviceId = A_DEVICE_ID.value,
homeserverUrl = A_HOMESERVER_URL,
oidcData = null,
slidingSyncVersion = proxy,
)
}

View file

@ -0,0 +1,21 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.factories
import io.element.android.libraries.matrix.test.A_USER_ID
import org.matrix.rustcomponents.sdk.UserProfile
fun aRustUserProfile(
userId: String = A_USER_ID.value,
displayName: String = "displayName",
avatarUrl: String = "avatarUrl",
) = UserProfile(
userId = userId,
displayName = displayName,
avatarUrl = avatarUrl,
)

View file

@ -0,0 +1,37 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.fakes
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.lambda.lambdaError
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomMembersIterator
class FakeRustRoom(
private val roomId: RoomId = A_ROOM_ID,
private val getMembers: () -> RoomMembersIterator = { lambdaError() },
private val getMembersNoSync: () -> RoomMembersIterator = { lambdaError() },
) : Room(NoPointer) {
override fun id(): String {
return roomId.value
}
override suspend fun members(): RoomMembersIterator {
return getMembers()
}
override suspend fun membersNoSync(): RoomMembersIterator {
return getMembersNoSync()
}
override fun close() {
// No-op
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.fakes
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo
import org.matrix.rustcomponents.sdk.EventTimelineItem
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.RoomListItem
class FakeRustRoomListItem(
private val roomId: RoomId,
private val roomInfo: RoomInfo = aRustRoomInfo(id = roomId.value),
private val latestEvent: EventTimelineItem? = null,
) : RoomListItem(NoPointer) {
override fun id(): String {
return roomId.value
}
override suspend fun roomInfo(): RoomInfo {
return roomInfo
}
override suspend fun latestEvent(): EventTimelineItem? {
return latestEvent
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.fakes
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.RoomMembersIterator
class FakeRustRoomMembersIterator(
private var members: List<RoomMember>? = null
) : RoomMembersIterator(NoPointer) {
override fun len(): UInt {
return members?.size?.toUInt() ?: 0u
}
override fun nextChunk(chunkSize: UInt): List<RoomMember>? {
if (members?.isEmpty() == true) {
return null
}
return members?.let {
val result = it.take(chunkSize.toInt())
members = it.subList(result.size, it.size)
result
}
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.fakes
import org.matrix.rustcomponents.sdk.InsertData
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.SetData
import org.matrix.rustcomponents.sdk.TimelineChange
import org.matrix.rustcomponents.sdk.TimelineDiff
import org.matrix.rustcomponents.sdk.TimelineItem
class FakeRustTimelineDiff(
private val change: TimelineChange,
private val item: TimelineItem? = FakeRustTimelineItem()
) : TimelineDiff(NoPointer) {
override fun change() = change
override fun append(): List<TimelineItem>? = item?.let { listOf(it) }
override fun insert(): InsertData? = item?.let { InsertData(1u, it) }
override fun pushBack(): TimelineItem? = item
override fun pushFront(): TimelineItem? = item
override fun remove(): UInt? = 1u
override fun reset(): List<TimelineItem>? = item?.let { listOf(it) }
override fun set(): SetData? = item?.let { SetData(1u, it) }
override fun truncate(): UInt? = 1u
}

View file

@ -0,0 +1,20 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.fixtures.fakes
import org.matrix.rustcomponents.sdk.EventTimelineItem
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.TimelineItem
import org.matrix.rustcomponents.sdk.VirtualTimelineItem
class FakeRustTimelineItem : TimelineItem(NoPointer) {
override fun asEvent(): EventTimelineItem? = null
override fun asVirtual(): VirtualTimelineItem? = null
override fun fmtDebug(): String = "fmtDebug"
override fun uniqueId(): String = "uniqueId"
}

View file

@ -0,0 +1,23 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.keys
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class DefaultPassphraseGeneratorTest {
@Test
fun `check that generated passphrase has the expected length`() {
val passphraseGenerator = DefaultPassphraseGenerator()
val passphrase = passphraseGenerator.generatePassphrase()
assertThat(passphrase!!.length).isEqualTo(342)
}
}

View file

@ -0,0 +1,84 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.mapper
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.test.A_DEVICE_ID
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
import io.element.android.libraries.matrix.test.A_SECRET
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.sessionstorage.api.LoginType
import org.junit.Test
import org.matrix.rustcomponents.sdk.SlidingSyncVersion
import java.io.File
class SessionKtTest {
@Test
fun `toSessionData compute the expected result`() {
val result = aRustSession().toSessionData(
isTokenValid = true,
loginType = LoginType.PASSWORD,
passphrase = A_SECRET,
sessionPaths = SessionPaths(File("/a/file"), File("/a/cache")),
)
assertThat(result.userId).isEqualTo(A_USER_ID.value)
assertThat(result.deviceId).isEqualTo(A_DEVICE_ID.value)
assertThat(result.accessToken).isEqualTo("accessToken")
assertThat(result.refreshToken).isEqualTo("refreshToken")
assertThat(result.homeserverUrl).isEqualTo(A_HOMESERVER_URL)
assertThat(result.isTokenValid).isTrue()
assertThat(result.oidcData).isNull()
assertThat(result.slidingSyncProxy).isNull()
assertThat(result.loginType).isEqualTo(LoginType.PASSWORD)
assertThat(result.loginTimestamp).isNotNull()
assertThat(result.passphrase).isEqualTo(A_SECRET)
assertThat(result.sessionPath).isEqualTo("/a/file")
assertThat(result.cachePath).isEqualTo("/a/cache")
}
@Test
fun `toSessionData can change the validity of the token`() {
val result = aRustSession().toSessionData(
isTokenValid = false,
loginType = LoginType.PASSWORD,
passphrase = A_SECRET,
sessionPaths = SessionPaths(File("/a/file"), File("/a/cache")),
homeserverUrl = null,
)
assertThat(result.isTokenValid).isFalse()
}
@Test
fun `toSessionData can override the value of the homeserver url`() {
val result = aRustSession().toSessionData(
isTokenValid = true,
loginType = LoginType.PASSWORD,
passphrase = A_SECRET,
sessionPaths = SessionPaths(File("/a/file"), File("/a/cache")),
homeserverUrl = A_HOMESERVER_URL_2,
)
assertThat(result.homeserverUrl).isEqualTo(A_HOMESERVER_URL_2)
}
@Test
fun `toSessionData copy the sliding sync url if present`() {
val result = aRustSession(
proxy = SlidingSyncVersion.Proxy("proxyUrl")
).toSessionData(
isTokenValid = true,
loginType = LoginType.PASSWORD,
passphrase = A_SECRET,
sessionPaths = SessionPaths(File("/a/file"), File("/a/cache")),
homeserverUrl = A_HOMESERVER_URL_2,
)
assertThat(result.slidingSyncProxy).isEqualTo("proxyUrl")
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.poll
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.poll.PollKind
import org.junit.Test
import org.matrix.rustcomponents.sdk.PollKind as RustPollKind
class PollKindKtTest {
@Test
fun `map should return Disclosed when RustPollKind is Disclosed`() {
val pollKind = RustPollKind.DISCLOSED.map()
assertThat(pollKind).isEqualTo(PollKind.Disclosed)
}
@Test
fun `map should return Undisclosed when RustPollKind is Undisclosed`() {
val pollKind = RustPollKind.UNDISCLOSED.map()
assertThat(pollKind).isEqualTo(PollKind.Undisclosed)
}
@Test
fun `toInner should return DISCLOSED when PollKind is Disclosed`() {
val rustPollKind = PollKind.Disclosed.toInner()
assertThat(rustPollKind).isEqualTo(RustPollKind.DISCLOSED)
}
@Test
fun `toInner should return UNDISCLOSED when PollKind is Undisclosed`() {
val rustPollKind = PollKind.Undisclosed.toInner()
assertThat(rustPollKind).isEqualTo(RustPollKind.UNDISCLOSED)
}
}

View file

@ -0,0 +1,194 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.room
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomHero
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomMember
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.AN_EVENT_ID
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.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_3
import io.element.android.libraries.matrix.test.A_USER_ID_6
import io.element.android.libraries.matrix.test.room.aRoomMember
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toPersistentList
import org.junit.Test
import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode
class MatrixRoomInfoMapperTest {
@Test
fun `mapping of RustRoomInfo should map all the fields`() {
assertThat(
MatrixRoomInfoMapper().map(
aRustRoomInfo(
id = A_ROOM_ID.value,
displayName = "displayName",
rawName = "rawName",
topic = "topic",
avatarUrl = AN_AVATAR_URL,
isDirect = true,
isPublic = false,
isSpace = false,
isTombstoned = false,
isFavourite = false,
canonicalAlias = A_ROOM_ALIAS.value,
alternativeAliases = listOf(A_ROOM_ALIAS.value),
membership = Membership.JOINED,
inviter = aRustRoomMember(A_USER_ID),
heroes = listOf(aRustRoomHero()),
activeMembersCount = 2uL,
invitedMembersCount = 3uL,
joinedMembersCount = 4uL,
userPowerLevels = mapOf(A_USER_ID_6.value to 50L),
highlightCount = 10uL,
notificationCount = 11uL,
userDefinedNotificationMode = RustRoomNotificationMode.MUTE,
hasRoomCall = true,
activeRoomCallParticipants = listOf(A_USER_ID_3.value),
isMarkedUnread = false,
numUnreadMessages = 12uL,
numUnreadNotifications = 13uL,
numUnreadMentions = 14uL,
pinnedEventIds = listOf(AN_EVENT_ID.value),
roomCreator = A_USER_ID,
)
)
).isEqualTo(
MatrixRoomInfo(
id = A_ROOM_ID,
name = "displayName",
rawName = "rawName",
topic = "topic",
avatarUrl = AN_AVATAR_URL,
isDirect = true,
isPublic = false,
isSpace = false,
isTombstoned = false,
isFavorite = false,
canonicalAlias = A_ROOM_ALIAS,
alternativeAliases = listOf(A_ROOM_ALIAS).toImmutableList(),
currentUserMembership = CurrentUserMembership.JOINED,
inviter = aRoomMember(A_USER_ID),
activeMembersCount = 2L,
invitedMembersCount = 3L,
joinedMembersCount = 4L,
userPowerLevels = mapOf(A_USER_ID_6 to 50L).toImmutableMap(),
highlightCount = 10L,
notificationCount = 11L,
userDefinedNotificationMode = RoomNotificationMode.MUTE,
hasRoomCall = true,
activeRoomCallParticipants = listOf(A_USER_ID_3).toImmutableList(),
heroes = listOf(
MatrixUser(
userId = A_USER_ID,
displayName = "displayName",
avatarUrl = "avatarUrl",
)
).toImmutableList(),
pinnedEventIds = listOf(AN_EVENT_ID).toPersistentList(),
creator = A_USER_ID,
)
)
}
@Test
fun `mapping of RustRoomInfo with null members should map all the fields`() {
assertThat(
MatrixRoomInfoMapper().map(
aRustRoomInfo(
id = A_ROOM_ID.value,
displayName = null,
rawName = null,
topic = null,
avatarUrl = null,
isDirect = false,
isPublic = true,
isSpace = false,
isTombstoned = false,
isFavourite = true,
canonicalAlias = null,
alternativeAliases = emptyList(),
membership = Membership.INVITED,
inviter = null,
heroes = listOf(aRustRoomHero()),
activeMembersCount = 2uL,
invitedMembersCount = 3uL,
joinedMembersCount = 4uL,
userPowerLevels = emptyMap(),
highlightCount = 10uL,
notificationCount = 11uL,
userDefinedNotificationMode = null,
hasRoomCall = false,
activeRoomCallParticipants = emptyList(),
isMarkedUnread = true,
numUnreadMessages = 12uL,
numUnreadNotifications = 13uL,
numUnreadMentions = 14uL,
pinnedEventIds = emptyList(),
roomCreator = null,
)
)
).isEqualTo(
MatrixRoomInfo(
id = A_ROOM_ID,
name = null,
rawName = null,
topic = null,
avatarUrl = null,
isDirect = false,
isPublic = true,
isSpace = false,
isTombstoned = false,
isFavorite = true,
canonicalAlias = null,
alternativeAliases = emptyList<RoomAlias>().toPersistentList(),
currentUserMembership = CurrentUserMembership.INVITED,
inviter = null,
activeMembersCount = 2L,
invitedMembersCount = 3L,
joinedMembersCount = 4L,
userPowerLevels = emptyMap<UserId, Long>().toImmutableMap(),
highlightCount = 10L,
notificationCount = 11L,
userDefinedNotificationMode = null,
hasRoomCall = false,
activeRoomCallParticipants = emptyList<UserId>().toImmutableList(),
heroes = emptyList<MatrixUser>().toImmutableList(),
pinnedEventIds = emptyList<EventId>().toPersistentList(),
creator = null,
)
)
}
@Test
fun `mapping Membership`() {
assertThat(Membership.INVITED.map()).isEqualTo(CurrentUserMembership.INVITED)
assertThat(Membership.JOINED.map()).isEqualTo(CurrentUserMembership.JOINED)
assertThat(Membership.LEFT.map()).isEqualTo(CurrentUserMembership.LEFT)
}
@Test
fun `mapping RoomNotificationMode`() {
assertThat(RustRoomNotificationMode.ALL_MESSAGES.map()).isEqualTo(RoomNotificationMode.ALL_MESSAGES)
assertThat(RustRoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY.map()).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
assertThat(RustRoomNotificationMode.MUTE.map()).isEqualTo(RoomNotificationMode.MUTE)
}
}

View file

@ -0,0 +1,69 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.room
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.MessageEventType
import org.junit.Test
import org.matrix.rustcomponents.sdk.MessageLikeEventType
class MessageEventTypeKtTest {
@Test
fun `map Rust type should result to correct Kotlin type`() {
assertThat(MessageLikeEventType.CALL_ANSWER.map()).isEqualTo(MessageEventType.CALL_ANSWER)
assertThat(MessageLikeEventType.CALL_INVITE.map()).isEqualTo(MessageEventType.CALL_INVITE)
assertThat(MessageLikeEventType.CALL_HANGUP.map()).isEqualTo(MessageEventType.CALL_HANGUP)
assertThat(MessageLikeEventType.CALL_CANDIDATES.map()).isEqualTo(MessageEventType.CALL_CANDIDATES)
assertThat(MessageLikeEventType.CALL_NOTIFY.map()).isEqualTo(MessageEventType.CALL_NOTIFY)
assertThat(MessageLikeEventType.KEY_VERIFICATION_READY.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_READY)
assertThat(MessageLikeEventType.KEY_VERIFICATION_START.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_START)
assertThat(MessageLikeEventType.KEY_VERIFICATION_CANCEL.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_CANCEL)
assertThat(MessageLikeEventType.KEY_VERIFICATION_ACCEPT.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_ACCEPT)
assertThat(MessageLikeEventType.KEY_VERIFICATION_KEY.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_KEY)
assertThat(MessageLikeEventType.KEY_VERIFICATION_MAC.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_MAC)
assertThat(MessageLikeEventType.KEY_VERIFICATION_DONE.map()).isEqualTo(MessageEventType.KEY_VERIFICATION_DONE)
assertThat(MessageLikeEventType.REACTION.map()).isEqualTo(MessageEventType.REACTION)
assertThat(MessageLikeEventType.ROOM_ENCRYPTED.map()).isEqualTo(MessageEventType.ROOM_ENCRYPTED)
assertThat(MessageLikeEventType.ROOM_MESSAGE.map()).isEqualTo(MessageEventType.ROOM_MESSAGE)
assertThat(MessageLikeEventType.ROOM_REDACTION.map()).isEqualTo(MessageEventType.ROOM_REDACTION)
assertThat(MessageLikeEventType.STICKER.map()).isEqualTo(MessageEventType.STICKER)
assertThat(MessageLikeEventType.POLL_END.map()).isEqualTo(MessageEventType.POLL_END)
assertThat(MessageLikeEventType.POLL_RESPONSE.map()).isEqualTo(MessageEventType.POLL_RESPONSE)
assertThat(MessageLikeEventType.POLL_START.map()).isEqualTo(MessageEventType.POLL_START)
assertThat(MessageLikeEventType.UNSTABLE_POLL_END.map()).isEqualTo(MessageEventType.UNSTABLE_POLL_END)
assertThat(MessageLikeEventType.UNSTABLE_POLL_RESPONSE.map()).isEqualTo(MessageEventType.UNSTABLE_POLL_RESPONSE)
assertThat(MessageLikeEventType.UNSTABLE_POLL_START.map()).isEqualTo(MessageEventType.UNSTABLE_POLL_START)
}
@Test
fun `map Kotlin type should result to correct Rust type`() {
assertThat(MessageEventType.CALL_ANSWER.map()).isEqualTo(MessageLikeEventType.CALL_ANSWER)
assertThat(MessageEventType.CALL_INVITE.map()).isEqualTo(MessageLikeEventType.CALL_INVITE)
assertThat(MessageEventType.CALL_HANGUP.map()).isEqualTo(MessageLikeEventType.CALL_HANGUP)
assertThat(MessageEventType.CALL_CANDIDATES.map()).isEqualTo(MessageLikeEventType.CALL_CANDIDATES)
assertThat(MessageEventType.CALL_NOTIFY.map()).isEqualTo(MessageLikeEventType.CALL_NOTIFY)
assertThat(MessageEventType.KEY_VERIFICATION_READY.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_READY)
assertThat(MessageEventType.KEY_VERIFICATION_START.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_START)
assertThat(MessageEventType.KEY_VERIFICATION_CANCEL.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_CANCEL)
assertThat(MessageEventType.KEY_VERIFICATION_ACCEPT.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_ACCEPT)
assertThat(MessageEventType.KEY_VERIFICATION_KEY.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_KEY)
assertThat(MessageEventType.KEY_VERIFICATION_MAC.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_MAC)
assertThat(MessageEventType.KEY_VERIFICATION_DONE.map()).isEqualTo(MessageLikeEventType.KEY_VERIFICATION_DONE)
assertThat(MessageEventType.REACTION.map()).isEqualTo(MessageLikeEventType.REACTION)
assertThat(MessageEventType.ROOM_ENCRYPTED.map()).isEqualTo(MessageLikeEventType.ROOM_ENCRYPTED)
assertThat(MessageEventType.ROOM_MESSAGE.map()).isEqualTo(MessageLikeEventType.ROOM_MESSAGE)
assertThat(MessageEventType.ROOM_REDACTION.map()).isEqualTo(MessageLikeEventType.ROOM_REDACTION)
assertThat(MessageEventType.STICKER.map()).isEqualTo(MessageLikeEventType.STICKER)
assertThat(MessageEventType.POLL_END.map()).isEqualTo(MessageLikeEventType.POLL_END)
assertThat(MessageEventType.POLL_RESPONSE.map()).isEqualTo(MessageLikeEventType.POLL_RESPONSE)
assertThat(MessageEventType.POLL_START.map()).isEqualTo(MessageLikeEventType.POLL_START)
assertThat(MessageEventType.UNSTABLE_POLL_END.map()).isEqualTo(MessageLikeEventType.UNSTABLE_POLL_END)
assertThat(MessageEventType.UNSTABLE_POLL_RESPONSE.map()).isEqualTo(MessageLikeEventType.UNSTABLE_POLL_RESPONSE)
assertThat(MessageEventType.UNSTABLE_POLL_START.map()).isEqualTo(MessageLikeEventType.UNSTABLE_POLL_START)
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.room
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomHero
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo
import io.element.android.libraries.matrix.test.A_USER_ID
import org.junit.Test
class RoomInfoExtTest {
@Test
fun `get non empty element Heroes`() {
val result = aRustRoomInfo(
isDirect = true,
activeMembersCount = 2uL,
heroes = listOf(aRustRoomHero())
).elementHeroes()
assertThat(result).isEqualTo(
listOf(
MatrixUser(
userId = UserId(A_USER_ID.value),
displayName = "displayName",
avatarUrl = "avatarUrl",
)
)
)
}
@Test
fun `too many heroes and element Heroes is empty`() {
val result = aRustRoomInfo(
isDirect = true,
activeMembersCount = 2uL,
heroes = listOf(aRustRoomHero(), aRustRoomHero())
).elementHeroes()
assertThat(result).isEmpty()
}
@Test
fun `not direct and element Heroes is empty`() {
val result = aRustRoomInfo(
isDirect = false,
activeMembersCount = 2uL,
heroes = listOf(aRustRoomHero())
).elementHeroes()
assertThat(result).isEmpty()
}
@Test
fun `too many members and element Heroes is empty`() {
val result = aRustRoomInfo(
isDirect = true,
activeMembersCount = 3uL,
heroes = listOf(aRustRoomHero())
).elementHeroes()
assertThat(result).isEmpty()
}
}

View file

@ -0,0 +1,19 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.matrix.api.room.RoomType
import org.junit.Test
class RoomTypeKtTest {
@Test
fun toRoomType() {
assert(null.toRoomType() == RoomType.Room)
assert("m.space".toRoomType() == RoomType.Space)
assert("m.other".toRoomType() == RoomType.Other("m.other"))
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.room
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.StateEventType
import org.junit.Test
import org.matrix.rustcomponents.sdk.StateEventType as RustStateEventType
class StateEventTypeTest {
@Test
fun `mapping Rust type should work`() {
assertThat(RustStateEventType.CALL_MEMBER.map()).isEqualTo(StateEventType.CALL_MEMBER)
assertThat(RustStateEventType.POLICY_RULE_ROOM.map()).isEqualTo(StateEventType.POLICY_RULE_ROOM)
assertThat(RustStateEventType.POLICY_RULE_SERVER.map()).isEqualTo(StateEventType.POLICY_RULE_SERVER)
assertThat(RustStateEventType.POLICY_RULE_USER.map()).isEqualTo(StateEventType.POLICY_RULE_USER)
assertThat(RustStateEventType.ROOM_ALIASES.map()).isEqualTo(StateEventType.ROOM_ALIASES)
assertThat(RustStateEventType.ROOM_AVATAR.map()).isEqualTo(StateEventType.ROOM_AVATAR)
assertThat(RustStateEventType.ROOM_CANONICAL_ALIAS.map()).isEqualTo(StateEventType.ROOM_CANONICAL_ALIAS)
assertThat(RustStateEventType.ROOM_CREATE.map()).isEqualTo(StateEventType.ROOM_CREATE)
assertThat(RustStateEventType.ROOM_ENCRYPTION.map()).isEqualTo(StateEventType.ROOM_ENCRYPTION)
assertThat(RustStateEventType.ROOM_GUEST_ACCESS.map()).isEqualTo(StateEventType.ROOM_GUEST_ACCESS)
assertThat(RustStateEventType.ROOM_HISTORY_VISIBILITY.map()).isEqualTo(StateEventType.ROOM_HISTORY_VISIBILITY)
assertThat(RustStateEventType.ROOM_JOIN_RULES.map()).isEqualTo(StateEventType.ROOM_JOIN_RULES)
assertThat(RustStateEventType.ROOM_MEMBER_EVENT.map()).isEqualTo(StateEventType.ROOM_MEMBER_EVENT)
assertThat(RustStateEventType.ROOM_NAME.map()).isEqualTo(StateEventType.ROOM_NAME)
assertThat(RustStateEventType.ROOM_PINNED_EVENTS.map()).isEqualTo(StateEventType.ROOM_PINNED_EVENTS)
assertThat(RustStateEventType.ROOM_POWER_LEVELS.map()).isEqualTo(StateEventType.ROOM_POWER_LEVELS)
assertThat(RustStateEventType.ROOM_SERVER_ACL.map()).isEqualTo(StateEventType.ROOM_SERVER_ACL)
assertThat(RustStateEventType.ROOM_THIRD_PARTY_INVITE.map()).isEqualTo(StateEventType.ROOM_THIRD_PARTY_INVITE)
assertThat(RustStateEventType.ROOM_TOMBSTONE.map()).isEqualTo(StateEventType.ROOM_TOMBSTONE)
assertThat(RustStateEventType.ROOM_TOPIC.map()).isEqualTo(StateEventType.ROOM_TOPIC)
assertThat(RustStateEventType.SPACE_CHILD.map()).isEqualTo(StateEventType.SPACE_CHILD)
assertThat(RustStateEventType.SPACE_PARENT.map()).isEqualTo(StateEventType.SPACE_PARENT)
}
@Test
fun `mapping Kotlin type should work`() {
assertThat(StateEventType.CALL_MEMBER.map()).isEqualTo(RustStateEventType.CALL_MEMBER)
assertThat(StateEventType.POLICY_RULE_ROOM.map()).isEqualTo(RustStateEventType.POLICY_RULE_ROOM)
assertThat(StateEventType.POLICY_RULE_SERVER.map()).isEqualTo(RustStateEventType.POLICY_RULE_SERVER)
assertThat(StateEventType.POLICY_RULE_USER.map()).isEqualTo(RustStateEventType.POLICY_RULE_USER)
assertThat(StateEventType.ROOM_ALIASES.map()).isEqualTo(RustStateEventType.ROOM_ALIASES)
assertThat(StateEventType.ROOM_AVATAR.map()).isEqualTo(RustStateEventType.ROOM_AVATAR)
assertThat(StateEventType.ROOM_CANONICAL_ALIAS.map()).isEqualTo(RustStateEventType.ROOM_CANONICAL_ALIAS)
assertThat(StateEventType.ROOM_CREATE.map()).isEqualTo(RustStateEventType.ROOM_CREATE)
assertThat(StateEventType.ROOM_ENCRYPTION.map()).isEqualTo(RustStateEventType.ROOM_ENCRYPTION)
assertThat(StateEventType.ROOM_GUEST_ACCESS.map()).isEqualTo(RustStateEventType.ROOM_GUEST_ACCESS)
assertThat(StateEventType.ROOM_HISTORY_VISIBILITY.map()).isEqualTo(RustStateEventType.ROOM_HISTORY_VISIBILITY)
assertThat(StateEventType.ROOM_JOIN_RULES.map()).isEqualTo(RustStateEventType.ROOM_JOIN_RULES)
assertThat(StateEventType.ROOM_MEMBER_EVENT.map()).isEqualTo(RustStateEventType.ROOM_MEMBER_EVENT)
assertThat(StateEventType.ROOM_NAME.map()).isEqualTo(RustStateEventType.ROOM_NAME)
assertThat(StateEventType.ROOM_PINNED_EVENTS.map()).isEqualTo(RustStateEventType.ROOM_PINNED_EVENTS)
assertThat(StateEventType.ROOM_POWER_LEVELS.map()).isEqualTo(RustStateEventType.ROOM_POWER_LEVELS)
assertThat(StateEventType.ROOM_SERVER_ACL.map()).isEqualTo(RustStateEventType.ROOM_SERVER_ACL)
assertThat(StateEventType.ROOM_THIRD_PARTY_INVITE.map()).isEqualTo(RustStateEventType.ROOM_THIRD_PARTY_INVITE)
assertThat(StateEventType.ROOM_TOMBSTONE.map()).isEqualTo(RustStateEventType.ROOM_TOMBSTONE)
assertThat(StateEventType.ROOM_TOPIC.map()).isEqualTo(RustStateEventType.ROOM_TOPIC)
assertThat(StateEventType.SPACE_CHILD.map()).isEqualTo(RustStateEventType.SPACE_CHILD)
assertThat(StateEventType.SPACE_PARENT.map()).isEqualTo(RustStateEventType.SPACE_PARENT)
}
}

View file

@ -0,0 +1,20 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.room.location
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.location.AssetType
import org.junit.Test
class AssetTypeKtTest {
@Test
fun toInner() {
assertThat(AssetType.SENDER.toInner()).isEqualTo(org.matrix.rustcomponents.sdk.AssetType.SENDER)
assertThat(AssetType.PIN.toInner()).isEqualTo(org.matrix.rustcomponents.sdk.AssetType.PIN)
}
}

View file

@ -9,13 +9,14 @@ package io.element.android.libraries.matrix.impl.room.member
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomMember
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoom
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomMembersIterator
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher.Source.CACHE
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher.Source.CACHE_AND_SERVER
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher.Source.SERVER
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.A_USER_ID_3
@ -23,22 +24,16 @@ import io.element.android.libraries.matrix.test.A_USER_ID_4
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.rustcomponents.sdk.MembershipState
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.RoomMembersIterator
import uniffi.matrix_sdk.RoomMemberRole
class RoomMemberListFetcherTest {
@Test
fun `fetchRoomMembers with CACHE source - emits cached members, if any`() = runTest {
val room = FakeRustRoom(getMembersNoSync = {
FakeRoomMembersIterator(
FakeRustRoomMembersIterator(
listOf(
fakeRustRoomMember(A_USER_ID),
fakeRustRoomMember(A_USER_ID_2),
fakeRustRoomMember(A_USER_ID_3),
aRustRoomMember(A_USER_ID),
aRustRoomMember(A_USER_ID_2),
aRustRoomMember(A_USER_ID_3),
)
)
})
@ -55,17 +50,13 @@ class RoomMemberListFetcherTest {
val cachedItemsState = awaitItem()
assertThat(cachedItemsState).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
assertThat((cachedItemsState as? MatrixRoomMembersState.Ready)?.roomMembers).hasSize(3)
// Assert only the 'no sync' method was called, so no new member sync happened
assertThat(room.membersNoSyncCallCount).isEqualTo(1)
assertThat(room.membersCallCount).isEqualTo(0)
}
}
@Test
fun `fetchRoomMembers with CACHE source - emits empty list, if no members exist`() = runTest {
val room = FakeRustRoom(getMembersNoSync = {
FakeRoomMembersIterator(emptyList())
FakeRustRoomMembersIterator(emptyList())
})
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
@ -95,11 +86,11 @@ class RoomMemberListFetcherTest {
@Test
fun `fetchRoomMembers with CACHE source - emits all items at once`() = runTest {
val room = FakeRustRoom(getMembersNoSync = {
FakeRoomMembersIterator(
FakeRustRoomMembersIterator(
listOf(
fakeRustRoomMember(A_USER_ID),
fakeRustRoomMember(A_USER_ID_2),
fakeRustRoomMember(A_USER_ID_3),
aRustRoomMember(A_USER_ID),
aRustRoomMember(A_USER_ID_2),
aRustRoomMember(A_USER_ID_3),
)
)
})
@ -122,11 +113,11 @@ class RoomMemberListFetcherTest {
@Test
fun `fetchRoomMembers with SERVER source - emits only new members, if any`() = runTest {
val room = FakeRustRoom(getMembers = {
FakeRoomMembersIterator(
FakeRustRoomMembersIterator(
listOf(
fakeRustRoomMember(A_USER_ID),
fakeRustRoomMember(A_USER_ID_2),
fakeRustRoomMember(A_USER_ID_3),
aRustRoomMember(A_USER_ID),
aRustRoomMember(A_USER_ID_2),
aRustRoomMember(A_USER_ID_3),
)
)
})
@ -138,10 +129,6 @@ class RoomMemberListFetcherTest {
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3)
// Assert only the 'sync' method was called, so a new member sync happened
assertThat(room.membersNoSyncCallCount).isEqualTo(0)
assertThat(room.membersCallCount).isEqualTo(1)
}
}
@ -163,14 +150,14 @@ class RoomMemberListFetcherTest {
fun `fetchRoomMembers with CACHE_AND_SERVER source - returns cached items first, then new ones`() = runTest {
val room = FakeRustRoom(
getMembersNoSync = {
FakeRoomMembersIterator(listOf(fakeRustRoomMember(A_USER_ID_4)))
FakeRustRoomMembersIterator(listOf(aRustRoomMember(A_USER_ID_4)))
},
getMembers = {
FakeRoomMembersIterator(
FakeRustRoomMembersIterator(
listOf(
fakeRustRoomMember(A_USER_ID),
fakeRustRoomMember(A_USER_ID_2),
fakeRustRoomMember(A_USER_ID_3),
aRustRoomMember(A_USER_ID),
aRustRoomMember(A_USER_ID_2),
aRustRoomMember(A_USER_ID_3),
)
)
}
@ -196,76 +183,6 @@ class RoomMemberListFetcherTest {
assertThat(ready).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
assertThat(ready.roomMembers()).hasSize(3)
}
// Assert both member methods were called, so both the cache was hit and a new member sync happened
assertThat(room.membersNoSyncCallCount).isEqualTo(1)
assertThat(room.membersCallCount).isEqualTo(1)
}
}
}
class FakeRustRoom(
private val getMembers: () -> RoomMembersIterator = { FakeRoomMembersIterator() },
private val getMembersNoSync: () -> RoomMembersIterator = { FakeRoomMembersIterator() },
) : Room(NoPointer) {
var membersCallCount = 0
var membersNoSyncCallCount = 0
override fun id(): String {
return A_ROOM_ID.value
}
override suspend fun members(): RoomMembersIterator {
membersCallCount++
return getMembers()
}
override suspend fun membersNoSync(): RoomMembersIterator {
membersNoSyncCallCount++
return getMembersNoSync()
}
override fun close() {
// No-op
}
}
class FakeRoomMembersIterator(
private var members: List<RoomMember>? = null
) : RoomMembersIterator(NoPointer) {
override fun len(): UInt {
return members?.size?.toUInt() ?: 0u
}
override fun nextChunk(chunkSize: UInt): List<RoomMember>? {
if (members?.isEmpty() == true) {
return null
}
return members?.let {
val result = it.take(chunkSize.toInt())
members = it.subList(result.size, it.size)
result
}
}
}
private fun fakeRustRoomMember(
userId: UserId,
displayName: String? = null,
avatarUrl: String? = null,
membership: MembershipState = MembershipState.JOIN,
isNameAmbiguous: Boolean = false,
powerLevel: Long = 0L,
isIgnored: Boolean = false,
role: RoomMemberRole = RoomMemberRole.USER,
) = RoomMember(
userId = userId.value,
displayName = displayName,
avatarUrl = avatarUrl,
membership = membership,
isNameAmbiguous = isNameAmbiguous,
powerLevel = powerLevel,
normalizedPowerLevel = powerLevel,
isIgnored = isIgnored,
suggestedRoleForPowerLevel = role,
)

View file

@ -0,0 +1,33 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.room.member
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import org.junit.Test
import uniffi.matrix_sdk.RoomMemberRole
import org.matrix.rustcomponents.sdk.MembershipState as RustMembershipState
class RoomMemberMapperTest {
@Test
fun mapRole() {
assertThat(RoomMemberMapper.mapRole(RoomMemberRole.USER)).isEqualTo(RoomMember.Role.USER)
assertThat(RoomMemberMapper.mapRole(RoomMemberRole.MODERATOR)).isEqualTo(RoomMember.Role.MODERATOR)
assertThat(RoomMemberMapper.mapRole(RoomMemberRole.ADMINISTRATOR)).isEqualTo(RoomMember.Role.ADMIN)
}
@Test
fun mapMembership() {
assertThat(RoomMemberMapper.mapMembership(RustMembershipState.BAN)).isEqualTo(RoomMembershipState.BAN)
assertThat(RoomMemberMapper.mapMembership(RustMembershipState.INVITE)).isEqualTo(RoomMembershipState.INVITE)
assertThat(RoomMemberMapper.mapMembership(RustMembershipState.JOIN)).isEqualTo(RoomMembershipState.JOIN)
assertThat(RoomMemberMapper.mapMembership(RustMembershipState.KNOCK)).isEqualTo(RoomMembershipState.KNOCK)
assertThat(RoomMemberMapper.mapMembership(RustMembershipState.LEAVE)).isEqualTo(RoomMembershipState.LEAVE)
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.room.powerlevels
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomPowerLevels
import org.junit.Test
class RoomPowerLevelsMapperTest {
@Test
fun `test that mapping of RoomPowerLevels is correct`() {
assertThat(
RoomPowerLevelsMapper.map(
aRustRoomPowerLevels(
ban = 1,
invite = 2,
kick = 3,
redact = 4,
eventsDefault = 5,
stateDefault = 6,
usersDefault = 7,
roomName = 8,
roomAvatar = 9,
roomTopic = 10,
)
)
).isEqualTo(
MatrixRoomPowerLevels(
ban = 1,
invite = 2,
kick = 3,
sendEvents = 5,
redactEvents = 4,
roomName = 8,
roomAvatar = 9,
roomTopic = 10,
)
)
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.room.preview
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomPreview
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
import io.element.android.libraries.matrix.test.A_ROOM_ID
import org.junit.Test
class RoomPreviewMapperTest {
@Test
fun `map should map values 1`() {
assertThat(
RoomPreviewMapper.map(
aRustRoomPreview(
isJoined = false,
isInvited = false,
)
)
).isEqualTo(
RoomPreview(
roomId = A_ROOM_ID,
canonicalAlias = A_ROOM_ALIAS,
name = "name",
topic = "topic",
avatarUrl = "avatarUrl",
numberOfJoinedMembers = 1L,
roomType = RoomType.Room,
isHistoryWorldReadable = true,
isJoined = false,
isInvited = false,
isPublic = true,
canKnock = true,
)
)
}
@Test
fun `map should map values 2`() {
assertThat(
RoomPreviewMapper.map(
aRustRoomPreview(
canonicalAlias = null,
isPublic = false,
canKnock = false,
)
)
).isEqualTo(
RoomPreview(
roomId = A_ROOM_ID,
canonicalAlias = null,
name = "name",
topic = "topic",
avatarUrl = "avatarUrl",
numberOfJoinedMembers = 1L,
roomType = RoomType.Room,
isHistoryWorldReadable = true,
isJoined = true,
isInvited = true,
isPublic = false,
canKnock = false,
)
)
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.roomdirectory
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomDescription
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.roomdirectory.aRoomDescription
import org.junit.Test
import org.matrix.rustcomponents.sdk.PublicRoomJoinRule
class RoomDescriptionMapperTest {
@Test
fun map() {
assertThat(RoomDescriptionMapper().map(aRustRoomDescription())).isEqualTo(
aRoomDescription(
roomId = A_ROOM_ID,
name = "name",
topic = "topic",
alias = A_ROOM_ALIAS,
avatarUrl = "avatarUrl",
joinRule = RoomDescription.JoinRule.PUBLIC,
isWorldReadable = true,
joinedMembers = 2L
)
)
}
@Test
fun mapWithNullAlias() {
assertThat(RoomDescriptionMapper().map(aRustRoomDescription().copy(alias = null)).alias).isNull()
}
@Test
fun `map join rule`() {
assertThat(PublicRoomJoinRule.PUBLIC.map()).isEqualTo(RoomDescription.JoinRule.PUBLIC)
assertThat(PublicRoomJoinRule.KNOCK.map()).isEqualTo(RoomDescription.JoinRule.KNOCK)
assertThat(null.map()).isEqualTo(RoomDescription.JoinRule.UNKNOWN)
}
}

View file

@ -9,12 +9,11 @@ package io.element.android.libraries.matrix.impl.roomlist
import com.google.common.truth.Truth.assertThat
import com.sun.jna.Pointer
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListItem
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_ROOM_ID_3
import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
import kotlinx.coroutines.flow.MutableStateFlow
@ -22,19 +21,12 @@ import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.rustcomponents.sdk.EventTimelineItem
import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.RoomHero
import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.RoomList
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomListServiceInterface
import org.matrix.rustcomponents.sdk.RoomListServiceStateListener
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicatorListener
import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.RoomNotificationMode
import org.matrix.rustcomponents.sdk.RoomSubscription
import org.matrix.rustcomponents.sdk.TaskHandle
@ -48,7 +40,7 @@ class RoomSummaryListProcessorTest {
summaries.value = listOf(aRoomSummary())
val processor = createProcessor()
val newEntry = FakeRoomListItem(A_ROOM_ID_2)
val newEntry = FakeRustRoomListItem(A_ROOM_ID_2)
processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(newEntry, newEntry, newEntry))))
assertThat(summaries.value.count()).isEqualTo(4)
@ -59,7 +51,7 @@ class RoomSummaryListProcessorTest {
fun `PushBack adds a new entry at the end of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor()
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(FakeRoomListItem(A_ROOM_ID_2))))
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(FakeRustRoomListItem(A_ROOM_ID_2))))
assertThat(summaries.value.count()).isEqualTo(2)
assertThat(summaries.value.last().roomId).isEqualTo(A_ROOM_ID_2)
@ -69,7 +61,7 @@ class RoomSummaryListProcessorTest {
fun `PushFront inserts a new entry at the start of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor()
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(FakeRoomListItem(A_ROOM_ID_2))))
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(FakeRustRoomListItem(A_ROOM_ID_2))))
assertThat(summaries.value.count()).isEqualTo(2)
assertThat(summaries.value.first().roomId).isEqualTo(A_ROOM_ID_2)
@ -81,7 +73,7 @@ class RoomSummaryListProcessorTest {
val processor = createProcessor()
val index = 0
processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), FakeRoomListItem(A_ROOM_ID_2))))
processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), FakeRustRoomListItem(A_ROOM_ID_2))))
assertThat(summaries.value.count()).isEqualTo(1)
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_2)
@ -93,7 +85,7 @@ class RoomSummaryListProcessorTest {
val processor = createProcessor()
val index = 0
processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), FakeRoomListItem(A_ROOM_ID_2))))
processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), FakeRustRoomListItem(A_ROOM_ID_2))))
assertThat(summaries.value.count()).isEqualTo(2)
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_2)
@ -157,6 +149,18 @@ class RoomSummaryListProcessorTest {
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID)
}
@Test
fun `Reset removes all entries and add the provided ones`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2))
val processor = createProcessor()
val index = 0
processor.postUpdate(listOf(RoomListEntriesUpdate.Reset(listOf(FakeRustRoomListItem(A_ROOM_ID_3)))))
assertThat(summaries.value.count()).isEqualTo(1)
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_3)
}
private fun TestScope.createProcessor() = RoomSummaryListProcessor(
summaries,
fakeRoomListService,
@ -185,85 +189,3 @@ class RoomSummaryListProcessorTest {
override fun subscribeToRooms(roomIds: List<String>, settings: RoomSubscription?) = Unit
}
}
private fun aRustRoomInfo(
id: String = A_ROOM_ID.value,
displayName: String = A_ROOM_NAME,
rawName: String = A_ROOM_NAME,
topic: String? = null,
avatarUrl: String? = null,
isDirect: Boolean = false,
isPublic: Boolean = false,
isSpace: Boolean = false,
isTombstoned: Boolean = false,
isFavourite: Boolean = false,
canonicalAlias: String? = null,
alternativeAliases: List<String> = listOf(),
membership: Membership = Membership.JOINED,
inviter: RoomMember? = null,
heroes: List<RoomHero> = listOf(),
activeMembersCount: ULong = 0uL,
invitedMembersCount: ULong = 0uL,
joinedMembersCount: ULong = 0uL,
userPowerLevels: Map<String, Long> = mapOf(),
highlightCount: ULong = 0uL,
notificationCount: ULong = 0uL,
userDefinedNotificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
activeRoomCallParticipants: List<String> = listOf(),
isMarkedUnread: Boolean = false,
numUnreadMessages: ULong = 0uL,
numUnreadNotifications: ULong = 0uL,
numUnreadMentions: ULong = 0uL,
pinnedEventIds: List<String> = listOf(),
roomCreator: UserId? = null,
) = RoomInfo(
id = id,
displayName = displayName,
rawName = rawName,
topic = topic,
avatarUrl = avatarUrl,
isDirect = isDirect,
isPublic = isPublic,
isSpace = isSpace,
isTombstoned = isTombstoned,
isFavourite = isFavourite,
canonicalAlias = canonicalAlias,
alternativeAliases = alternativeAliases,
membership = membership,
inviter = inviter,
heroes = heroes,
activeMembersCount = activeMembersCount,
invitedMembersCount = invitedMembersCount,
joinedMembersCount = joinedMembersCount,
userPowerLevels = userPowerLevels,
highlightCount = highlightCount,
notificationCount = notificationCount,
cachedUserDefinedNotificationMode = userDefinedNotificationMode,
hasRoomCall = hasRoomCall,
activeRoomCallParticipants = activeRoomCallParticipants,
isMarkedUnread = isMarkedUnread,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
numUnreadMentions = numUnreadMentions,
pinnedEventIds = pinnedEventIds,
creator = roomCreator?.value,
)
class FakeRoomListItem(
private val roomId: RoomId,
private val roomInfo: RoomInfo = aRustRoomInfo(id = roomId.value),
private val latestEvent: EventTimelineItem? = null,
) : RoomListItem(NoPointer) {
override fun id(): String {
return roomId.value
}
override suspend fun roomInfo(): RoomInfo {
return roomInfo
}
override suspend fun latestEvent(): EventTimelineItem? {
return latestEvent
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.server
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.FakeMatrixClient
import org.junit.Test
class DefaultUserServerResolverTest {
@Test
fun resolve() {
// Given
val userServerResolver = DefaultUserServerResolver(FakeMatrixClient(
userIdServerNameLambda = { "dummy.org" }
))
// When
val result = userServerResolver.resolve()
// Then
assertThat(result).isEqualTo("dummy.org")
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.sync
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.sync.SyncState
import org.junit.Test
import org.matrix.rustcomponents.sdk.SyncServiceState
class AppStateMapperKtTest {
@Test
fun toSyncState() {
assertThat(SyncServiceState.IDLE.toSyncState()).isEqualTo(SyncState.Idle)
assertThat(SyncServiceState.RUNNING.toSyncState()).isEqualTo(SyncState.Running)
assertThat(SyncServiceState.TERMINATED.toSyncState()).isEqualTo(SyncState.Terminated)
assertThat(SyncServiceState.ERROR.toSyncState()).isEqualTo(SyncState.Error)
}
}

View file

@ -0,0 +1,171 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.timeline
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineDiff
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.rustcomponents.sdk.TimelineChange
class MatrixTimelineDiffProcessorTest {
private val timelineItems = MutableStateFlow<List<MatrixTimelineItem>>(emptyList())
private val anEvent = MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem())
private val anEvent2 = MatrixTimelineItem.Event(A_UNIQUE_ID_2, anEventTimelineItem())
@Test
fun `Append adds new entries at the end of the list`() = runTest {
timelineItems.value = listOf(anEvent)
val processor = createProcessor()
processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.APPEND)))
assertThat(timelineItems.value.count()).isEqualTo(2)
assertThat(timelineItems.value).containsExactly(
anEvent,
MatrixTimelineItem.Other,
)
}
@Test
fun `PushBack adds a new entry at the end of the list`() = runTest {
timelineItems.value = listOf(anEvent)
val processor = createProcessor()
processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.PUSH_BACK)))
assertThat(timelineItems.value.count()).isEqualTo(2)
assertThat(timelineItems.value).containsExactly(
anEvent,
MatrixTimelineItem.Other,
)
}
@Test
fun `PushFront inserts a new entry at the start of the list`() = runTest {
timelineItems.value = listOf(anEvent)
val processor = createProcessor()
processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.PUSH_FRONT)))
assertThat(timelineItems.value.count()).isEqualTo(2)
assertThat(timelineItems.value).containsExactly(
MatrixTimelineItem.Other,
anEvent,
)
}
@Test
fun `Set replaces an entry at some index`() = runTest {
timelineItems.value = listOf(anEvent, anEvent2)
val processor = createProcessor()
processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.SET)))
assertThat(timelineItems.value.count()).isEqualTo(2)
assertThat(timelineItems.value).containsExactly(
anEvent,
MatrixTimelineItem.Other
)
}
@Test
fun `Insert inserts a new entry at the provided index`() = runTest {
timelineItems.value = listOf(anEvent, anEvent2)
val processor = createProcessor()
processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.INSERT)))
assertThat(timelineItems.value.count()).isEqualTo(3)
assertThat(timelineItems.value).containsExactly(
anEvent,
MatrixTimelineItem.Other,
anEvent2,
)
}
@Test
fun `Remove removes an entry at some index`() = runTest {
timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2)
val processor = createProcessor()
processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.REMOVE)))
assertThat(timelineItems.value.count()).isEqualTo(2)
assertThat(timelineItems.value).containsExactly(
anEvent,
anEvent2,
)
}
@Test
fun `PopBack removes an entry at the end of the list`() = runTest {
timelineItems.value = listOf(anEvent, anEvent2)
val processor = createProcessor()
processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.POP_BACK)))
assertThat(timelineItems.value.count()).isEqualTo(1)
assertThat(timelineItems.value).containsExactly(
anEvent,
)
}
@Test
fun `PopFront removes an entry at the start of the list`() = runTest {
timelineItems.value = listOf(anEvent, anEvent2)
val processor = createProcessor()
processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.POP_FRONT)))
assertThat(timelineItems.value.count()).isEqualTo(1)
assertThat(timelineItems.value).containsExactly(
anEvent2,
)
}
@Test
fun `Clear removes all the entries`() = runTest {
timelineItems.value = listOf(anEvent, anEvent2)
val processor = createProcessor()
processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.CLEAR)))
assertThat(timelineItems.value).isEmpty()
}
@Test
fun `Truncate removes all entries after the provided length`() = runTest {
timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2)
val processor = createProcessor()
processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.TRUNCATE)))
assertThat(timelineItems.value.count()).isEqualTo(1)
assertThat(timelineItems.value).containsExactly(
anEvent,
)
}
@Test
fun `Reset removes all entries and add the provided ones`() = runTest {
timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2)
val processor = createProcessor()
processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.RESET)))
assertThat(timelineItems.value.count()).isEqualTo(1)
assertThat(timelineItems.value).containsExactly(
MatrixTimelineItem.Other,
)
}
private fun TestScope.createProcessor(): MatrixTimelineDiffProcessor {
val timelineEventContentMapper = TimelineEventContentMapper()
val timelineItemMapper = MatrixTimelineItemMapper(
fetchDetailsForEvent = { _ -> Result.success(Unit) },
coroutineScope = this,
virtualTimelineItemMapper = VirtualTimelineItemMapper(),
eventTimelineItemMapper = EventTimelineItemMapper(
contentMapper = timelineEventContentMapper
)
)
return MatrixTimelineDiffProcessor(
timelineItems,
timelineItemFactory = timelineItemMapper,
)
}
}

View file

@ -0,0 +1,21 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.timeline
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import org.junit.Test
import org.matrix.rustcomponents.sdk.ReceiptType as RustReceiptType
class ReceiptTypeMapperKtTest {
@Test
fun toRustReceiptType() {
assertThat(ReceiptType.READ.toRustReceiptType()).isEqualTo(RustReceiptType.READ)
assertThat(ReceiptType.READ_PRIVATE.toRustReceiptType()).isEqualTo(RustReceiptType.READ_PRIVATE)
assertThat(ReceiptType.FULLY_READ.toRustReceiptType()).isEqualTo(RustReceiptType.FULLY_READ)
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.timeline.postprocessor
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.timeline.aMessageContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
internal val roomCreateEvent = MatrixTimelineItem.Event(
uniqueId = UniqueId("m.room.create"),
event = anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))
)
internal val roomCreatorJoinEvent = MatrixTimelineItem.Event(
uniqueId = UniqueId("m.room.member"),
event = anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))
)
internal val otherMemberJoinEvent = MatrixTimelineItem.Event(
uniqueId = UniqueId("m.room.member_other"),
event = anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))
)
internal val messageEvent = MatrixTimelineItem.Event(
uniqueId = UniqueId("m.room.message"),
event = anEventTimelineItem(content = aMessageContent("hi"))
)
internal val messageEvent2 = MatrixTimelineItem.Event(
uniqueId = UniqueId("m.room.message2"),
event = anEventTimelineItem(content = aMessageContent("hello"))
)
internal val dayEvent = MatrixTimelineItem.Virtual(
uniqueId = UniqueId("day"),
virtual = VirtualTimelineItem.DayDivider(0),
)

View file

@ -0,0 +1,94 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.timeline.postprocessor
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import org.junit.Test
class LastForwardIndicatorsPostProcessorTest {
@Test
fun `LastForwardIndicatorsPostProcessor does not alter the items with mode not FOCUSED_ON_EVENT`() {
val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.LIVE)
val result = sut.process(listOf(messageEvent), true)
assertThat(result).containsExactly(messageEvent)
}
@Test
fun `LastForwardIndicatorsPostProcessor does not alter the items with mode FOCUSED_ON_EVENT but timeline not initialized`() {
val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT)
val result = sut.process(listOf(messageEvent), false)
assertThat(result).containsExactly(messageEvent)
}
@Test
fun `LastForwardIndicatorsPostProcessor add virtual items`() {
val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT)
val result = sut.process(listOf(messageEvent), true)
assertThat(result).containsExactly(
messageEvent,
MatrixTimelineItem.Virtual(
uniqueId = UniqueId("last_forward_indicator_${messageEvent.uniqueId}"),
virtual = VirtualTimelineItem.LastForwardIndicator
)
)
}
@Test
fun `LastForwardIndicatorsPostProcessor add virtual items on empty list`() {
val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT)
val result = sut.process(listOf(), true)
assertThat(result).containsExactly(
MatrixTimelineItem.Virtual(
uniqueId = UniqueId("last_forward_indicator_fake_id"),
virtual = VirtualTimelineItem.LastForwardIndicator
)
)
}
@Test
fun `LastForwardIndicatorsPostProcessor add virtual items but does not alter the list if called a second time`() {
val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT)
// Process a first time
sut.process(listOf(messageEvent), true)
// Process a second time with the same Event
val result = sut.process(listOf(messageEvent), true)
assertThat(result).containsExactly(
messageEvent,
MatrixTimelineItem.Virtual(
uniqueId = UniqueId("last_forward_indicator_${messageEvent.uniqueId}"),
virtual = VirtualTimelineItem.LastForwardIndicator
)
)
}
@Test
fun `LastForwardIndicatorsPostProcessor add virtual items each time it is called with new Events`() {
val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT)
// Process a first time
sut.process(listOf(dayEvent, messageEvent), true)
// Process a second time with the same Event
val result = sut.process(listOf(dayEvent, messageEvent, messageEvent2), true)
assertThat(result).containsExactly(
dayEvent,
messageEvent,
MatrixTimelineItem.Virtual(
uniqueId = UniqueId("last_forward_indicator_${messageEvent.uniqueId}"),
virtual = VirtualTimelineItem.LastForwardIndicator
),
messageEvent2,
MatrixTimelineItem.Virtual(
uniqueId = UniqueId("last_forward_indicator_${messageEvent2.uniqueId}"),
virtual = VirtualTimelineItem.LastForwardIndicator
)
)
}
}

View file

@ -0,0 +1,127 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.timeline.postprocessor
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import org.junit.Test
class LoadingIndicatorsPostProcessorTest {
@Test
fun `LoadingIndicatorsPostProcessor does not alter the items is the timeline is not initialized`() {
val sut = LoadingIndicatorsPostProcessor(FakeSystemClock())
val result = sut.process(
items = listOf(messageEvent, messageEvent2),
isTimelineInitialized = false,
hasMoreToLoadBackward = true,
hasMoreToLoadForward = true,
)
assertThat(result).containsExactly(messageEvent, messageEvent2)
}
@Test
fun `LoadingIndicatorsPostProcessor adds Loading indicator at the top of the list if hasMoreToLoadBackward is true`() {
val clock = FakeSystemClock()
val sut = LoadingIndicatorsPostProcessor(clock)
val result = sut.process(
items = listOf(messageEvent, messageEvent2),
isTimelineInitialized = true,
hasMoreToLoadBackward = true,
hasMoreToLoadForward = false,
)
assertThat(result).containsExactly(
MatrixTimelineItem.Virtual(
uniqueId = UniqueId("BackwardLoadingIndicator"),
virtual = VirtualTimelineItem.LoadingIndicator(
direction = Timeline.PaginationDirection.BACKWARDS,
timestamp = clock.epochMillis()
)
),
messageEvent,
messageEvent2,
)
}
@Test
fun `LoadingIndicatorsPostProcessor adds Loading indicator at the bottom of the list if hasMoreToLoadForward is true`() {
val clock = FakeSystemClock()
val sut = LoadingIndicatorsPostProcessor(clock)
val result = sut.process(
items = listOf(messageEvent, messageEvent2),
isTimelineInitialized = true,
hasMoreToLoadBackward = false,
hasMoreToLoadForward = true,
)
assertThat(result).containsExactly(
messageEvent,
messageEvent2,
MatrixTimelineItem.Virtual(
uniqueId = UniqueId("ForwardLoadingIndicator"),
virtual = VirtualTimelineItem.LoadingIndicator(
direction = Timeline.PaginationDirection.FORWARDS,
timestamp = clock.epochMillis()
)
),
)
}
@Test
fun `LoadingIndicatorsPostProcessor adds Loading indicator at the bottom and at the top of the list`() {
val clock = FakeSystemClock()
val sut = LoadingIndicatorsPostProcessor(clock)
val result = sut.process(
items = listOf(messageEvent, messageEvent2),
isTimelineInitialized = true,
hasMoreToLoadBackward = true,
hasMoreToLoadForward = true,
)
assertThat(result).containsExactly(
MatrixTimelineItem.Virtual(
uniqueId = UniqueId("BackwardLoadingIndicator"),
virtual = VirtualTimelineItem.LoadingIndicator(
direction = Timeline.PaginationDirection.BACKWARDS,
timestamp = clock.epochMillis()
)
),
messageEvent,
messageEvent2,
MatrixTimelineItem.Virtual(
uniqueId = UniqueId("ForwardLoadingIndicator"),
virtual = VirtualTimelineItem.LoadingIndicator(
direction = Timeline.PaginationDirection.FORWARDS,
timestamp = clock.epochMillis()
)
),
)
}
@Test
fun `LoadingIndicatorsPostProcessor only adds 1 Loading indicator if there is no items in the list`() {
val clock = FakeSystemClock()
val sut = LoadingIndicatorsPostProcessor(clock)
val result = sut.process(
items = listOf(),
isTimelineInitialized = true,
hasMoreToLoadBackward = true,
hasMoreToLoadForward = true,
)
assertThat(result).containsExactly(
MatrixTimelineItem.Virtual(
uniqueId = UniqueId("BackwardLoadingIndicator"),
virtual = VirtualTimelineItem.LoadingIndicator(
direction = Timeline.PaginationDirection.BACKWARDS,
timestamp = clock.epochMillis()
)
),
)
}
}

View file

@ -8,37 +8,11 @@
package io.element.android.libraries.matrix.impl.timeline.postprocessor
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.timeline.aMessageContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import org.junit.Test
class RoomBeginningPostProcessorTest {
private val roomCreateEvent = MatrixTimelineItem.Event(
uniqueId = UniqueId("m.room.create"),
event = anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))
)
private val roomCreatorJoinEvent = MatrixTimelineItem.Event(
uniqueId = UniqueId("m.room.member"),
event = anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, null, MembershipChange.JOINED))
)
private val otherMemberJoinEvent = MatrixTimelineItem.Event(
uniqueId = UniqueId("m.room.member_other"),
event = anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, null, MembershipChange.JOINED))
)
private val messageEvent = MatrixTimelineItem.Event(
uniqueId = UniqueId("m.room.message"),
event = anEventTimelineItem(content = aMessageContent("hi"))
)
@Test
fun `processor returns empty list when empty list is provided`() {
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)

View file

@ -0,0 +1,22 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.usersearch
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustUserProfile
import io.element.android.libraries.matrix.test.A_USER_ID
import org.junit.Test
class UserProfileMapperTest {
@Test
fun map() {
assertThat(UserProfileMapper.map(aRustUserProfile(A_USER_ID.value, "displayName", "avatarUrl")))
.isEqualTo(MatrixUser(A_USER_ID, "displayName", "avatarUrl"))
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.usersearch
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSearchUsersResults
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustUserProfile
import io.element.android.libraries.matrix.test.A_USER_ID
import kotlinx.collections.immutable.toImmutableList
import org.junit.Test
class UserSearchResultMapperTest {
@Test
fun `map limited list`() {
assertThat(
UserSearchResultMapper.map(
aRustSearchUsersResults(
results = listOf(aRustUserProfile(A_USER_ID.value, "displayName", "avatarUrl")),
limited = true,
)
)
)
.isEqualTo(
MatrixSearchUserResults(
results = listOf(MatrixUser(A_USER_ID, "displayName", "avatarUrl")).toImmutableList(),
limited = true,
)
)
}
@Test
fun `map not limited list`() {
assertThat(
UserSearchResultMapper.map(
aRustSearchUsersResults(
results = listOf(aRustUserProfile(A_USER_ID.value, "displayName", "avatarUrl")),
limited = false,
)
)
)
.isEqualTo(
MatrixSearchUserResults(
results = listOf(MatrixUser(A_USER_ID, "displayName", "avatarUrl")).toImmutableList(),
limited = false,
)
)
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.util
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import kotlinx.coroutines.test.runTest
import org.junit.Test
class SessionPathsProviderTest {
@Test
fun `if session is not found, provides returns null`() = runTest {
val sut = SessionPathsProvider(InMemorySessionStore())
val result = sut.provides(A_SESSION_ID)
assertThat(result).isNull()
}
@Test
fun `if session is found, provides returns the data`() = runTest {
val store = InMemorySessionStore()
val sut = SessionPathsProvider(store)
store.storeData(
aSessionData(
sessionPath = "/a/path/to/a/session",
cachePath = "/a/path/to/a/cache",
)
)
val result = sut.provides(A_SESSION_ID)!!
assertThat(result.fileDirectory.absolutePath).isEqualTo("/a/path/to/a/session")
assertThat(result.cacheDirectory.absolutePath).isEqualTo("/a/path/to/a/cache")
}
}

View file

@ -520,7 +520,7 @@ fun aRoomInfo(
isTombstoned: Boolean = false,
isFavorite: Boolean = false,
canonicalAlias: RoomAlias? = null,
alternativeAliases: List<String> = emptyList(),
alternativeAliases: List<RoomAlias> = emptyList(),
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
inviter: RoomMember? = null,
activeMembersCount: Long = 1,
@ -531,7 +531,7 @@ fun aRoomInfo(
userDefinedNotificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
userPowerLevels: ImmutableMap<UserId, Long> = persistentMapOf(),
activeRoomCallParticipants: List<String> = emptyList(),
activeRoomCallParticipants: List<UserId> = emptyList(),
heroes: List<MatrixUser> = emptyList(),
pinnedEventIds: List<EventId> = emptyList(),
roomCreator: UserId? = null,

View file

@ -51,9 +51,9 @@ class DefaultFirebaseNewTokenHandlerTest {
val pusherSubscriber = FakePusherSubscriber(registerPusherResult = registerPusherResult)
val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler(
sessionStore = InMemoryMultiSessionsStore().apply {
storeData(aSessionData(A_USER_ID))
storeData(aSessionData(A_USER_ID_2))
storeData(aSessionData(A_USER_ID_3))
storeData(aSessionData(A_USER_ID.value))
storeData(aSessionData(A_USER_ID_2.value))
storeData(aSessionData(A_USER_ID_3.value))
},
matrixClientProvider = FakeMatrixClientProvider { sessionId ->
when (sessionId) {
@ -90,7 +90,7 @@ class DefaultFirebaseNewTokenHandlerTest {
val pusherSubscriber = FakePusherSubscriber(registerPusherResult = registerPusherResult)
val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler(
sessionStore = InMemoryMultiSessionsStore().apply {
storeData(aSessionData(A_USER_ID))
storeData(aSessionData(A_USER_ID.value))
},
matrixClientProvider = FakeMatrixClientProvider {
Result.failure(IllegalStateException())
@ -114,7 +114,7 @@ class DefaultFirebaseNewTokenHandlerTest {
val pusherSubscriber = FakePusherSubscriber(registerPusherResult = registerPusherResult)
val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler(
sessionStore = InMemoryMultiSessionsStore().apply {
storeData(aSessionData(A_USER_ID))
storeData(aSessionData(A_USER_ID.value))
},
matrixClientProvider = FakeMatrixClientProvider {
Result.success(aMatrixClient1)

View file

@ -7,19 +7,19 @@
package io.element.android.libraries.sessionstorage.test
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionData
fun aSessionData(
sessionId: SessionId = SessionId("@alice:server.org"),
sessionId: String = "@alice:server.org",
deviceId: String = "aDeviceId",
isTokenValid: Boolean = false,
sessionPath: String = "/a/path/to/a/session",
cachePath: String = "/a/path/to/a/cache",
): SessionData {
return SessionData(
userId = sessionId.value,
deviceId = "aDeviceId",
userId = sessionId,
deviceId = deviceId,
accessToken = "anAccessToken",
refreshToken = "aRefreshToken",
homeserverUrl = "aHomeserverUrl",

View file

@ -47,7 +47,7 @@ private const val versionMinor = 6
// Note: even values are reserved for regular release, odd values for hotfix release.
// When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release.
private const val versionPatch = 2
private const val versionPatch = 3
object Versions {
val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch

View file

@ -68,11 +68,12 @@ class KonsistClassNameTest {
.withoutName(
"FakeFileSystem",
"FakeImageLoader",
"FakeRustRoom",
)
.assertTrue {
val interfaceName = it.name.replace("Fake", "")
it.name.startsWith("Fake") &&
val interfaceName = it.name
.replace("FakeRust", "")
.replace("Fake", "")
(it.name.startsWith("Fake") || it.name.startsWith("FakeRust")) &&
it.parents().any { parent -> parent.name.replace(".", "") == interfaceName }
}
}