Merge branch 'release/0.6.3' into main

This commit is contained in:
Benoit Marty 2024-09-19 10:36:36 +02:00
commit 4c3d06ddb6
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) 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.architecture)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
api(projects.features.deactivation.api) 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.TopAppBar
import io.element.android.libraries.designsystem.theme.components.autofill import io.element.android.libraries.designsystem.theme.components.autofill
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext 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 io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -277,6 +279,7 @@ private fun Content(
.padding(top = 8.dp) .padding(top = 8.dp)
.fillMaxWidth() .fillMaxWidth()
.onTabOrEnterKeyFocusNext(focusManager) .onTabOrEnterKeyFocusNext(focusManager)
.testTag(TestTags.loginPassword)
.autofill( .autofill(
autofillTypes = listOf(AutofillType.Password), autofillTypes = listOf(AutofillType.Password),
onFill = { onFill = {

View file

@ -10,15 +10,26 @@ package io.element.android.features.logout.impl
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule 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 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.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder 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.ensureCalledOnce
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressTag
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class AccountDeactivationViewTest { 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( private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAccountDeactivationView(

View file

@ -374,7 +374,8 @@ private fun VerifiedUserSendFailureView(
fun VerifiedUserSendFailure.headline(): String { fun VerifiedUserSendFailure.headline(): String {
return when (this) { return when (this) {
is None -> "" 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) 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 { sealed interface VerifiedUserSendFailure {
data object None : VerifiedUserSendFailure data object None : VerifiedUserSendFailure
data class UnsignedDevice( sealed interface UnsignedDevice : VerifiedUserSendFailure {
val userDisplayName: String, data object FromYou : UnsignedDevice
) : VerifiedUserSendFailure data class FromOther(val userDisplayName: String) : UnsignedDevice
}
data class ChangedIdentity( data class ChangedIdentity(
val userDisplayName: String, val userDisplayName: String,

View file

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

View file

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

View file

@ -113,28 +113,33 @@ fun ResolveVerifiedUserSendFailureView(
@Composable @Composable
private fun VerifiedUserSendFailure.title(): String { private fun VerifiedUserSendFailure.title(): String {
return when (this) { 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( is VerifiedUserSendFailure.ChangedIdentity -> stringResource(
id = CommonStrings.screen_resolve_send_failure_changed_identity_title, id = CommonStrings.screen_resolve_send_failure_changed_identity_title,
userDisplayName userDisplayName
) )
VerifiedUserSendFailure.None -> error("This method should never be called for this state") VerifiedUserSendFailure.None -> ""
} }
} }
@Composable @Composable
private fun VerifiedUserSendFailure.subtitle(): String { private fun VerifiedUserSendFailure.subtitle(): String {
return when (this) { return when (this) {
is VerifiedUserSendFailure.UnsignedDevice -> stringResource( is VerifiedUserSendFailure.UnsignedDevice.FromOther -> stringResource(
id = CommonStrings.screen_resolve_send_failure_unsigned_device_subtitle, id = CommonStrings.screen_resolve_send_failure_unsigned_device_subtitle,
userDisplayName, userDisplayName,
userDisplayName, userDisplayName,
) )
VerifiedUserSendFailure.UnsignedDevice.FromYou -> stringResource(id = CommonStrings.screen_resolve_send_failure_you_unsigned_device_subtitle)
is VerifiedUserSendFailure.ChangedIdentity -> stringResource( is VerifiedUserSendFailure.ChangedIdentity -> stringResource(
id = CommonStrings.screen_resolve_send_failure_changed_identity_subtitle, id = CommonStrings.screen_resolve_send_failure_changed_identity_subtitle,
userDisplayName 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) { return when (this) {
is VerifiedUserSendFailure.UnsignedDevice -> stringResource(id = CommonStrings.screen_resolve_send_failure_unsigned_device_primary_button_title) 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) 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.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
class PinnedMessagesBannerPresenter @Inject constructor( class PinnedMessagesBannerPresenter @Inject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
@ -123,7 +121,6 @@ class PinnedMessagesBannerPresenter @Inject constructor(
is AsyncData.Loading -> flowOf(AsyncData.Loading()) is AsyncData.Loading -> flowOf(AsyncData.Loading())
is AsyncData.Success -> { is AsyncData.Success -> {
asyncTimeline.data.timelineItems asyncTimeline.data.timelineItems
.debounce(300.milliseconds)
.map { timelineItems -> .map { timelineItems ->
val pinnedItems = timelineItems.mapNotNull { timelineItem -> val pinnedItems = timelineItems.mapNotNull { timelineItem ->
itemFactory.create(timelineItem) itemFactory.create(timelineItem)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -55,6 +55,7 @@ dependencies {
testImplementation(libs.test.mockk) testImplementation(libs.test.mockk)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.sessionStorage.implMemory) testImplementation(projects.libraries.sessionStorage.implMemory)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.test)
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)
testImplementation(projects.services.toolbox.test) 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.core.aBuildMeta
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.network.useragent.DefaultUserAgentProvider 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.impl.memory.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -94,7 +93,7 @@ class DefaultBugReporterTest {
server.start() server.start()
val mockSessionStore = InMemorySessionStore().apply { val mockSessionStore = InMemorySessionStore().apply {
storeData(mockSessionData("@foo:eample.com", "ABCDEFGH")) storeData(aSessionData(sessionId = "@foo:example.com", deviceId = "ABCDEFGH"))
} }
val buildMeta = aBuildMeta() val buildMeta = aBuildMeta()
@ -143,7 +142,7 @@ class DefaultBugReporterTest {
assertThat(foundValues["can_contact"]).isEqualTo("true") assertThat(foundValues["can_contact"]).isEqualTo("true")
assertThat(foundValues["device_id"]).isEqualTo("ABCDEFGH") assertThat(foundValues["device_id"]).isEqualTo("ABCDEFGH")
assertThat(foundValues["sdk_sha"]).isEqualTo("123456789") 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["text"]).isEqualTo("a bug occurred")
assertThat(foundValues["device_keys"]).isEqualTo("curve25519:CURVECURVECURVE, ed25519:EDKEYEDKEYEDKY") assertThat(foundValues["device_keys"]).isEqualTo("curve25519:CURVECURVECURVE, ed25519:EDKEYEDKEYEDKY")
@ -163,7 +162,7 @@ class DefaultBugReporterTest {
server.start() server.start()
val mockSessionStore = InMemorySessionStore().apply { val mockSessionStore = InMemorySessionStore().apply {
storeData(mockSessionData("@foo:eample.com", "ABCDEFGH")) storeData(aSessionData("@foo:example.com", "ABCDEFGH"))
} }
val buildMeta = aBuildMeta() val buildMeta = aBuildMeta()
@ -267,21 +266,6 @@ class DefaultBugReporterTest {
return foundValues 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 @Test
fun `test sendBugReport error`() = runTest { fun `test sendBugReport error`() = runTest {
val server = MockWebServer() val server = MockWebServer()

View file

@ -61,7 +61,7 @@ import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -73,6 +73,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
private const val EXTENDED_RANGE_SIZE = 40 private const val EXTENDED_RANGE_SIZE = 40
private const val SUBSCRIBE_TO_VISIBLE_ROOMS_DEBOUNCE_IN_MILLIS = 300L
class RoomListPresenter @Inject constructor( class RoomListPresenter @Inject constructor(
private val client: MatrixClient, private val client: MatrixClient,
@ -301,7 +302,10 @@ class RoomListPresenter @Inject constructor(
private var currentUpdateVisibleRangeJob: Job? = null private var currentUpdateVisibleRangeJob: Job? = null
private fun CoroutineScope.updateVisibleRange(range: IntRange) { private fun CoroutineScope.updateVisibleRange(range: IntRange) {
currentUpdateVisibleRangeJob?.cancel() 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 if (range.isEmpty()) return@launch
val currentRoomList = roomListDataSource.allRooms.first() val currentRoomList = roomListDataSource.allRooms.first()
// Use extended range to 'prefetch' the next rooms info // 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.test
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import kotlin.time.Duration.Companion.seconds
class RoomListPresenterTest { class RoomListPresenterTest {
@get:Rule @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 @Test
fun `present - UpdateVisibleRange subscribes to rooms in visible range`() = runTest { fun `present - UpdateVisibleRange subscribes to rooms in visible range`() = runTest {
val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> } val subscribeToVisibleRoomsLambda = lambdaRecorder { _: List<RoomId> -> }
@ -622,10 +657,12 @@ class RoomListPresenterTest {
}.last() }.last()
state.eventSink(RoomListEvents.UpdateVisibleRange(IntRange(0, 10))) state.eventSink(RoomListEvents.UpdateVisibleRange(IntRange(0, 10)))
advanceTimeBy(1.seconds)
subscribeToVisibleRoomsLambda.assertions().isCalledOnce() 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))) state.eventSink(RoomListEvents.UpdateVisibleRange(IntRange(0, 11)))
advanceTimeBy(1.seconds)
subscribeToVisibleRoomsLambda.assertions().isCalledExactly(2) subscribeToVisibleRoomsLambda.assertions().isCalledExactly(2)
} }
} }

View file

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

View file

@ -8,7 +8,6 @@
package io.element.android.features.signedout.impl package io.element.android.features.signedout.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider 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.LoginType
import io.element.android.libraries.sessionstorage.api.SessionData 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", appName = "AppName",
signedOutSession = aSessionData(), signedOutSession = aSessionData(),
eventSink = {}, eventSink = {},
) )
fun aSessionData( private fun aSessionData(
sessionId: SessionId = SessionId("@alice:server.org"), sessionId: String = "@alice:server.org",
isTokenValid: Boolean = false, isTokenValid: Boolean = false,
): SessionData { ): SessionData {
return SessionData( return SessionData(
userId = sessionId.value, userId = sessionId,
deviceId = "aDeviceId", deviceId = "aDeviceId",
accessToken = "anAccessToken", accessToken = "anAccessToken",
refreshToken = "aRefreshToken", 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.matrix.test.core.aBuildMeta
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore 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 io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule 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" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0" molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
timber = "com.jakewharton.timber:timber:5.0.1" 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 = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", 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" } 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)) add(MatrixPatternResult(MatrixPatternType.USER_ID, permalink.userId.toString(), match.range.first, match.range.last + 1))
} }
is PermalinkData.RoomLink -> { 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 else -> Unit
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -8,14 +8,21 @@
package io.element.android.libraries.matrix.impl.auth package io.element.android.libraries.matrix.impl.auth
import io.element.android.libraries.matrix.api.auth.AuthenticationException 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 { fun Throwable.mapAuthenticationException(): AuthenticationException {
val message = this.message ?: "Unknown error" val message = this.message ?: "Unknown error"
return when (this) { return when (this) {
is RustAuthenticationException.Generic -> AuthenticationException.Generic(message) is ClientBuildException -> when (this) {
is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(message) is ClientBuildException.Generic -> AuthenticationException.Generic(message)
is RustAuthenticationException.SlidingSyncVersion -> AuthenticationException.SlidingSyncVersion(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) else -> AuthenticationException.Generic(message)
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -21,13 +21,17 @@ class RoomDescriptionMapper {
topic = roomDescription.topic, topic = roomDescription.topic,
avatarUrl = roomDescription.avatarUrl, avatarUrl = roomDescription.avatarUrl,
alias = roomDescription.alias?.let(::RoomAlias), alias = roomDescription.alias?.let(::RoomAlias),
joinRule = when (roomDescription.joinRule) { joinRule = roomDescription.joinRule.map(),
PublicRoomJoinRule.PUBLIC -> RoomDescription.JoinRule.PUBLIC
PublicRoomJoinRule.KNOCK -> RoomDescription.JoinRule.KNOCK
null -> RoomDescription.JoinRule.UNKNOWN
},
isWorldReadable = roomDescription.isWorldReadable, isWorldReadable = roomDescription.isWorldReadable,
numberOfMembers = roomDescription.joinedMembers.toLong(), 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 package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
@Suppress("unused")
@ExcludeFromCoverage
internal fun RoomListEntriesUpdate.describe(): String { internal fun RoomListEntriesUpdate.describe(): String {
return when (this) { return when (this) {
is RoomListEntriesUpdate.Set -> { is RoomListEntriesUpdate.Set -> {

View file

@ -53,11 +53,7 @@ internal class RustRoomListService(
} }
override suspend fun subscribeToVisibleRooms(roomIds: List<RoomId>) { override suspend fun subscribeToVisibleRooms(roomIds: List<RoomId>) {
val toSubscribe = roomIds.filterNot { roomSyncSubscriber.isSubscribedTo(it) } roomSyncSubscriber.batchSubscribe(roomIds)
if (toSubscribe.isNotEmpty()) {
Timber.d("Subscribe to ${toSubscribe.size} rooms: $toSubscribe")
roomSyncSubscriber.batchSubscribe(toSubscribe)
}
} }
override val allRooms: DynamicRoomList = roomListFactory.createRoomList( 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.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import org.matrix.rustcomponents.sdk.SyncServiceInterface
import org.matrix.rustcomponents.sdk.SyncServiceState import org.matrix.rustcomponents.sdk.SyncServiceState
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import org.matrix.rustcomponents.sdk.SyncService as InnerSyncService
class RustSyncService( class RustSyncService(
private val innerSyncService: SyncServiceInterface, private val innerSyncService: InnerSyncService,
sessionCoroutineScope: CoroutineScope sessionCoroutineScope: CoroutineScope
) : SyncService { ) : SyncService {
private val isServiceReady = AtomicBoolean(true)
override suspend fun startSync() = runCatching { override suspend fun startSync() = runCatching {
if (!isServiceReady.get()) {
Timber.d("Can't start sync: service is not ready")
return@runCatching
}
Timber.i("Start sync") Timber.i("Start sync")
innerSyncService.start() innerSyncService.start()
}.onFailure { }.onFailure {
@ -32,12 +39,24 @@ class RustSyncService(
} }
override suspend fun stopSync() = runCatching { override suspend fun stopSync() = runCatching {
if (!isServiceReady.get()) {
Timber.d("Can't stop sync: service is not ready")
return@runCatching
}
Timber.i("Stop sync") Timber.i("Stop sync")
innerSyncService.stop() innerSyncService.stop()
}.onFailure { }.onFailure {
Timber.d("Stop sync failed: $it") 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> = override val syncState: StateFlow<SyncState> =
innerSyncService.stateFlow() innerSyncService.stateFlow()
.map(SyncServiceState::toSyncState) .map(SyncServiceState::toSyncState)

View file

@ -100,7 +100,8 @@ internal class MatrixTimelineDiffProcessor(
clear() clear()
} }
TimelineChange.TRUNCATE -> { 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() { private suspend fun updateVerificationStatus() {
if (verificationFlowState.value == VerificationFlowState.Finished) { if (verificationFlowState.value == VerificationFlowState.Finished) {
// Calling `encryptionService.verificationState()` performs a network call and it will deadlock if there is no network // 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()) assertThat(ClientBuildException.InvalidServerName("Invalid server name").mapAuthenticationException())
.isException<AuthenticationException.InvalidServerName>("Invalid server name") .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()) assertThat(ClientBuildException.SlidingSyncVersion("Sliding sync not available").mapAuthenticationException())
.isException<AuthenticationException.SlidingSyncVersion>("Sliding sync not available") .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) { private inline fun <reified T> ThrowableSubject.isException(message: String) {
isInstanceOf(T::class.java) isInstanceOf(T::class.java)
hasMessageThat().isEqualTo(message) 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 app.cash.turbine.test
import com.google.common.truth.Truth.assertThat 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.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.roomMembers 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
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.CACHE_AND_SERVER
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher.Source.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
import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.A_USER_ID_3 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.Dispatchers
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test 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 { class RoomMemberListFetcherTest {
@Test @Test
fun `fetchRoomMembers with CACHE source - emits cached members, if any`() = runTest { fun `fetchRoomMembers with CACHE source - emits cached members, if any`() = runTest {
val room = FakeRustRoom(getMembersNoSync = { val room = FakeRustRoom(getMembersNoSync = {
FakeRoomMembersIterator( FakeRustRoomMembersIterator(
listOf( listOf(
fakeRustRoomMember(A_USER_ID), aRustRoomMember(A_USER_ID),
fakeRustRoomMember(A_USER_ID_2), aRustRoomMember(A_USER_ID_2),
fakeRustRoomMember(A_USER_ID_3), aRustRoomMember(A_USER_ID_3),
) )
) )
}) })
@ -55,17 +50,13 @@ class RoomMemberListFetcherTest {
val cachedItemsState = awaitItem() val cachedItemsState = awaitItem()
assertThat(cachedItemsState).isInstanceOf(MatrixRoomMembersState.Ready::class.java) assertThat(cachedItemsState).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
assertThat((cachedItemsState as? MatrixRoomMembersState.Ready)?.roomMembers).hasSize(3) 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 @Test
fun `fetchRoomMembers with CACHE source - emits empty list, if no members exist`() = runTest { fun `fetchRoomMembers with CACHE source - emits empty list, if no members exist`() = runTest {
val room = FakeRustRoom(getMembersNoSync = { val room = FakeRustRoom(getMembersNoSync = {
FakeRoomMembersIterator(emptyList()) FakeRustRoomMembersIterator(emptyList())
}) })
val fetcher = RoomMemberListFetcher(room, Dispatchers.Default) val fetcher = RoomMemberListFetcher(room, Dispatchers.Default)
@ -95,11 +86,11 @@ class RoomMemberListFetcherTest {
@Test @Test
fun `fetchRoomMembers with CACHE source - emits all items at once`() = runTest { fun `fetchRoomMembers with CACHE source - emits all items at once`() = runTest {
val room = FakeRustRoom(getMembersNoSync = { val room = FakeRustRoom(getMembersNoSync = {
FakeRoomMembersIterator( FakeRustRoomMembersIterator(
listOf( listOf(
fakeRustRoomMember(A_USER_ID), aRustRoomMember(A_USER_ID),
fakeRustRoomMember(A_USER_ID_2), aRustRoomMember(A_USER_ID_2),
fakeRustRoomMember(A_USER_ID_3), aRustRoomMember(A_USER_ID_3),
) )
) )
}) })
@ -122,11 +113,11 @@ class RoomMemberListFetcherTest {
@Test @Test
fun `fetchRoomMembers with SERVER source - emits only new members, if any`() = runTest { fun `fetchRoomMembers with SERVER source - emits only new members, if any`() = runTest {
val room = FakeRustRoom(getMembers = { val room = FakeRustRoom(getMembers = {
FakeRoomMembersIterator( FakeRustRoomMembersIterator(
listOf( listOf(
fakeRustRoomMember(A_USER_ID), aRustRoomMember(A_USER_ID),
fakeRustRoomMember(A_USER_ID_2), aRustRoomMember(A_USER_ID_2),
fakeRustRoomMember(A_USER_ID_3), aRustRoomMember(A_USER_ID_3),
) )
) )
}) })
@ -138,10 +129,6 @@ class RoomMemberListFetcherTest {
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java) assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Unknown::class.java)
assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java) assertThat(awaitItem()).isInstanceOf(MatrixRoomMembersState.Pending::class.java)
assertThat((awaitItem() as? MatrixRoomMembersState.Ready)?.roomMembers?.size).isEqualTo(3) 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 { fun `fetchRoomMembers with CACHE_AND_SERVER source - returns cached items first, then new ones`() = runTest {
val room = FakeRustRoom( val room = FakeRustRoom(
getMembersNoSync = { getMembersNoSync = {
FakeRoomMembersIterator(listOf(fakeRustRoomMember(A_USER_ID_4))) FakeRustRoomMembersIterator(listOf(aRustRoomMember(A_USER_ID_4)))
}, },
getMembers = { getMembers = {
FakeRoomMembersIterator( FakeRustRoomMembersIterator(
listOf( listOf(
fakeRustRoomMember(A_USER_ID), aRustRoomMember(A_USER_ID),
fakeRustRoomMember(A_USER_ID_2), aRustRoomMember(A_USER_ID_2),
fakeRustRoomMember(A_USER_ID_3), aRustRoomMember(A_USER_ID_3),
) )
) )
} }
@ -196,76 +183,6 @@ class RoomMemberListFetcherTest {
assertThat(ready).isInstanceOf(MatrixRoomMembersState.Ready::class.java) assertThat(ready).isInstanceOf(MatrixRoomMembersState.Ready::class.java)
assertThat(ready.roomMembers()).hasSize(3) 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.google.common.truth.Truth.assertThat
import com.sun.jna.Pointer 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.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
import io.element.android.libraries.matrix.test.A_ROOM_ID_2 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.aRoomSummary
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -22,19 +21,12 @@ import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test 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.RoomList
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomListServiceInterface import org.matrix.rustcomponents.sdk.RoomListServiceInterface
import org.matrix.rustcomponents.sdk.RoomListServiceStateListener import org.matrix.rustcomponents.sdk.RoomListServiceStateListener
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicatorListener 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.RoomSubscription
import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.TaskHandle
@ -48,7 +40,7 @@ class RoomSummaryListProcessorTest {
summaries.value = listOf(aRoomSummary()) summaries.value = listOf(aRoomSummary())
val processor = createProcessor() 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)))) processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(newEntry, newEntry, newEntry))))
assertThat(summaries.value.count()).isEqualTo(4) 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 { fun `PushBack adds a new entry at the end of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled()) summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor() 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.count()).isEqualTo(2)
assertThat(summaries.value.last().roomId).isEqualTo(A_ROOM_ID_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 { fun `PushFront inserts a new entry at the start of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled()) summaries.value = listOf(aRoomSummaryFilled())
val processor = createProcessor() 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.count()).isEqualTo(2)
assertThat(summaries.value.first().roomId).isEqualTo(A_ROOM_ID_2) assertThat(summaries.value.first().roomId).isEqualTo(A_ROOM_ID_2)
@ -81,7 +73,7 @@ class RoomSummaryListProcessorTest {
val processor = createProcessor() val processor = createProcessor()
val index = 0 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.count()).isEqualTo(1)
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_2) assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_2)
@ -93,7 +85,7 @@ class RoomSummaryListProcessorTest {
val processor = createProcessor() val processor = createProcessor()
val index = 0 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.count()).isEqualTo(2)
assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_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) 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( private fun TestScope.createProcessor() = RoomSummaryListProcessor(
summaries, summaries,
fakeRoomListService, fakeRoomListService,
@ -185,85 +189,3 @@ class RoomSummaryListProcessorTest {
override fun subscribeToRooms(roomIds: List<String>, settings: RoomSubscription?) = Unit 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 package io.element.android.libraries.matrix.impl.timeline.postprocessor
import com.google.common.truth.Truth.assertThat 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.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
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 import org.junit.Test
class RoomBeginningPostProcessorTest { 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 @Test
fun `processor returns empty list when empty list is provided`() { fun `processor returns empty list when empty list is provided`() {
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) 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, isTombstoned: Boolean = false,
isFavorite: Boolean = false, isFavorite: Boolean = false,
canonicalAlias: RoomAlias? = null, canonicalAlias: RoomAlias? = null,
alternativeAliases: List<String> = emptyList(), alternativeAliases: List<RoomAlias> = emptyList(),
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED, currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
inviter: RoomMember? = null, inviter: RoomMember? = null,
activeMembersCount: Long = 1, activeMembersCount: Long = 1,
@ -531,7 +531,7 @@ fun aRoomInfo(
userDefinedNotificationMode: RoomNotificationMode? = null, userDefinedNotificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false, hasRoomCall: Boolean = false,
userPowerLevels: ImmutableMap<UserId, Long> = persistentMapOf(), userPowerLevels: ImmutableMap<UserId, Long> = persistentMapOf(),
activeRoomCallParticipants: List<String> = emptyList(), activeRoomCallParticipants: List<UserId> = emptyList(),
heroes: List<MatrixUser> = emptyList(), heroes: List<MatrixUser> = emptyList(),
pinnedEventIds: List<EventId> = emptyList(), pinnedEventIds: List<EventId> = emptyList(),
roomCreator: UserId? = null, roomCreator: UserId? = null,

View file

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

View file

@ -7,19 +7,19 @@
package io.element.android.libraries.sessionstorage.test 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.LoginType
import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionData
fun aSessionData( fun aSessionData(
sessionId: SessionId = SessionId("@alice:server.org"), sessionId: String = "@alice:server.org",
deviceId: String = "aDeviceId",
isTokenValid: Boolean = false, isTokenValid: Boolean = false,
sessionPath: String = "/a/path/to/a/session", sessionPath: String = "/a/path/to/a/session",
cachePath: String = "/a/path/to/a/cache", cachePath: String = "/a/path/to/a/cache",
): SessionData { ): SessionData {
return SessionData( return SessionData(
userId = sessionId.value, userId = sessionId,
deviceId = "aDeviceId", deviceId = deviceId,
accessToken = "anAccessToken", accessToken = "anAccessToken",
refreshToken = "aRefreshToken", refreshToken = "aRefreshToken",
homeserverUrl = "aHomeserverUrl", 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. // 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 // When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release. // is the value for the next regular release.
private const val versionPatch = 2 private const val versionPatch = 3
object Versions { object Versions {
val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch

View file

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