diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt index 8f7f63e503d..423fe925dcf 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt @@ -20,18 +20,19 @@ import io.element.android.libraries.matrix.api.auth.AuthenticationException import org.matrix.rustcomponents.sdk.AuthenticationException as RustAuthenticationException fun Throwable.mapAuthenticationException(): AuthenticationException { + val message = this.message ?: "Unknown error" return when (this) { - is RustAuthenticationException.ClientMissing -> AuthenticationException.ClientMissing(this.message!!) - is RustAuthenticationException.Generic -> AuthenticationException.Generic(this.message!!) - is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(this.message!!) - is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(this.message!!) - is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(this.message!!) - is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message!!) - is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message!!) - is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message!!) - is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message!!) - is RustAuthenticationException.OidcCancelled -> AuthenticationException.OidcError("OidcCancelled", message!!) - is RustAuthenticationException.OidcCallbackUrlInvalid -> AuthenticationException.OidcError("OidcCallbackUrlInvalid", message!!) - else -> AuthenticationException.Generic(this.message ?: "Unknown error") + is RustAuthenticationException.ClientMissing -> AuthenticationException.ClientMissing(message) + is RustAuthenticationException.Generic -> AuthenticationException.Generic(message) + is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(message) + is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(message) + is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(message) + is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message) + is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message) + is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message) + is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message) + is RustAuthenticationException.OidcCancelled -> AuthenticationException.OidcError("OidcCancelled", message) + is RustAuthenticationException.OidcCallbackUrlInvalid -> AuthenticationException.OidcError("OidcCallbackUrlInvalid", message) + else -> AuthenticationException.Generic(message) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt index 28297426c4d..b95eb953334 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt @@ -23,14 +23,14 @@ import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.catch -import org.matrix.rustcomponents.sdk.RoomList import org.matrix.rustcomponents.sdk.RoomListEntriesListener import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate import org.matrix.rustcomponents.sdk.RoomListEntry +import org.matrix.rustcomponents.sdk.RoomListInterface import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListLoadingState import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener -import org.matrix.rustcomponents.sdk.RoomListService +import org.matrix.rustcomponents.sdk.RoomListServiceInterface import org.matrix.rustcomponents.sdk.RoomListServiceState import org.matrix.rustcomponents.sdk.RoomListServiceStateListener import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator @@ -40,7 +40,7 @@ import timber.log.Timber private const val SYNC_INDICATOR_DELAY_BEFORE_SHOWING = 1000u private const val SYNC_INDICATOR_DELAY_BEFORE_HIDING = 0u -fun RoomList.loadingStateFlow(): Flow = +fun RoomListInterface.loadingStateFlow(): Flow = mxCallbackFlow { val listener = object : RoomListLoadingStateListener { override fun onUpdate(state: RoomListLoadingState) { @@ -58,7 +58,7 @@ fun RoomList.loadingStateFlow(): Flow = Timber.d(it, "loadingStateFlow() failed") }.buffer(Channel.UNLIMITED) -fun RoomList.entriesFlow(onInitialList: suspend (List) -> Unit): Flow> = +fun RoomListInterface.entriesFlow(onInitialList: suspend (List) -> Unit): Flow> = mxCallbackFlow { val listener = object : RoomListEntriesListener { override fun onUpdate(roomEntriesUpdate: List) { @@ -76,7 +76,7 @@ fun RoomList.entriesFlow(onInitialList: suspend (List) -> Unit): Timber.d(it, "entriesFlow() failed") }.buffer(Channel.UNLIMITED) -fun RoomListService.stateFlow(): Flow = +fun RoomListServiceInterface.stateFlow(): Flow = mxCallbackFlow { val listener = object : RoomListServiceStateListener { override fun onUpdate(state: RoomListServiceState) { @@ -88,7 +88,7 @@ fun RoomListService.stateFlow(): Flow = } }.buffer(Channel.UNLIMITED) -fun RoomListService.syncIndicator(): Flow = +fun RoomListServiceInterface.syncIndicator(): Flow = mxCallbackFlow { val listener = object : RoomListServiceSyncIndicatorListener { override fun onUpdate(syncIndicator: RoomListServiceSyncIndicator) { @@ -104,7 +104,7 @@ fun RoomListService.syncIndicator(): Flow = } }.buffer(Channel.UNLIMITED) -fun RoomListService.roomOrNull(roomId: String): RoomListItem? { +fun RoomListServiceInterface.roomOrNull(roomId: String): RoomListItem? { return try { room(roomId) } catch (exception: Exception) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt index 778637bdc7c..754e35ec662 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt @@ -26,14 +26,14 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate import org.matrix.rustcomponents.sdk.RoomListEntry -import org.matrix.rustcomponents.sdk.RoomListService +import org.matrix.rustcomponents.sdk.RoomListServiceInterface import org.matrix.rustcomponents.sdk.use import timber.log.Timber import java.util.UUID class RoomSummaryListProcessor( private val roomSummaries: MutableStateFlow>, - private val roomListService: RoomListService, + private val roomListService: RoomListServiceInterface, private val dispatcher: CoroutineDispatcher, private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(), ) { diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTests.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTests.kt new file mode 100644 index 00000000000..7c68bdc31b7 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTests.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.auth + +import com.google.common.truth.ThrowableSubject +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.auth.AuthenticationException +import org.junit.Test +import org.matrix.rustcomponents.sdk.AuthenticationException as RustAuthenticationException + +class AuthenticationExceptionMappingTests { + + @Test + fun `mapping an exception with no message returns 'Unknown error' message`() { + val exception = Exception() + val mappedException = exception.mapAuthenticationException() + assertThat(mappedException.message).isEqualTo("Unknown error") + } + + @Test + fun `mapping a generic exception returns a Generic AuthenticationException`() { + val exception = Exception("Generic exception") + val mappedException = exception.mapAuthenticationException() + assertThat(mappedException).isException("Generic exception") + } + + @Test + fun `mapping specific exceptions map to their kotlin counterparts`() { + assertThat(RustAuthenticationException.ClientMissing("Client missing").mapAuthenticationException()) + .isException("Client missing") + + assertThat(RustAuthenticationException.Generic("Generic").mapAuthenticationException()).isException("Generic") + + assertThat(RustAuthenticationException.InvalidServerName("Invalid server name").mapAuthenticationException()) + .isException("Invalid server name") + + assertThat(RustAuthenticationException.SessionMissing("Session missing").mapAuthenticationException()) + .isException("Session missing") + + assertThat(RustAuthenticationException.SlidingSyncNotAvailable("Sliding sync not available").mapAuthenticationException()) + .isException("Sliding sync not available") + } + + @Test + fun `mapping Oidc related exceptions creates an 'OidcError' with different types`() { + assertIsOidcError( + throwable = RustAuthenticationException.OidcException("Oidc exception"), + type = "OidcException", + message = "Oidc exception" + ) + assertIsOidcError( + throwable = RustAuthenticationException.OidcMetadataInvalid("Oidc metadata invalid"), + type = "OidcMetadataInvalid", + message = "Oidc metadata invalid" + ) + assertIsOidcError( + throwable = RustAuthenticationException.OidcMetadataMissing("Oidc metadata missing"), + type = "OidcMetadataMissing", + message = "Oidc metadata missing" + ) + assertIsOidcError( + throwable = RustAuthenticationException.OidcNotSupported("Oidc not supported"), + type = "OidcNotSupported", + message = "Oidc not supported" + ) + assertIsOidcError( + throwable = RustAuthenticationException.OidcCancelled("Oidc cancelled"), + type = "OidcCancelled", + message = "Oidc cancelled" + ) + assertIsOidcError( + throwable = RustAuthenticationException.OidcCallbackUrlInvalid("Oidc callback url invalid"), + type = "OidcCallbackUrlInvalid", + message = "Oidc callback url invalid" + ) + } + + private inline fun ThrowableSubject.isException(message: String) { + isInstanceOf(T::class.java) + hasMessageThat().isEqualTo(message) + } + + private inline fun assertIsOidcError(throwable: Throwable, type: String, message: String) { + val authenticationException = throwable.mapAuthenticationException() + assertThat(authenticationException).isInstanceOf(AuthenticationException.OidcError::class.java) + assertThat((authenticationException as? AuthenticationException.OidcError)?.type).isEqualTo(type) + assertThat(authenticationException.message).isEqualTo(message) + } + +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTests.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTests.kt new file mode 100644 index 00000000000..3eee4e9d7b3 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTests.kt @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.roomlist + +import com.google.common.truth.Truth.assertThat +import com.sun.jna.Pointer +import io.element.android.libraries.matrix.api.roomlist.RoomSummary +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.room.aRoomSummaryFilled +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withTimeout +import org.junit.Test +import org.matrix.rustcomponents.sdk.RoomList +import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate +import org.matrix.rustcomponents.sdk.RoomListEntry +import org.matrix.rustcomponents.sdk.RoomListInput +import org.matrix.rustcomponents.sdk.RoomListItem +import org.matrix.rustcomponents.sdk.RoomListServiceInterface +import org.matrix.rustcomponents.sdk.RoomListServiceStateListener +import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicatorListener +import org.matrix.rustcomponents.sdk.TaskHandle +import kotlin.time.Duration.Companion.milliseconds + +// NOTE: this class is using a fake implementation of a Rust SDK interface which returns actual Rust objects with pointers. +// Since we don't access the data in those objects, this is fine for our tests, but that's as far as we can test this class. +class RoomSummaryListProcessorTests { + + private val summaries = MutableStateFlow>(emptyList()) + + @Test + fun `postUpdates can't start until postEntries is done`() = runTest { + val processor = createProcessor() + val update = listOf(RoomListEntriesUpdate.Reset(emptyList())) + + val timeoutError = runCatching { + withTimeout(10.milliseconds) { processor.postUpdate(update) } + }.exceptionOrNull() + assertThat(timeoutError).isInstanceOf(CancellationException::class.java) + + processor.postEntries(listOf(RoomListEntry.Empty)) + processor.postUpdate(update) + } + + @Test + fun `postEntries adds all new entries with no diffing`() = runTest { + summaries.value = listOf(aRoomSummaryFilled()) + val processor = createProcessor() + + processor.postEntries(listOf(RoomListEntry.Empty, RoomListEntry.Empty, RoomListEntry.Empty)) + + assertThat(summaries.value.count()).isEqualTo(4) + } + + @Test + fun `Append adds new entries at the end of the list`() = runTest { + summaries.value = listOf(aRoomSummaryFilled()) + val processor = createProcessor() + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.Append(listOf(RoomListEntry.Empty, RoomListEntry.Empty, RoomListEntry.Empty)))) + + assertThat(summaries.value.count()).isEqualTo(4) + assertThat(summaries.value.subList(1, 4).all { it is RoomSummary.Empty }).isTrue() + } + + @Test + fun `PushBack adds a new entry at the end of the list`() = runTest { + summaries.value = listOf(aRoomSummaryFilled()) + val processor = createProcessor() + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(RoomListEntry.Empty))) + + assertThat(summaries.value.count()).isEqualTo(2) + assertThat(summaries.value.last()).isInstanceOf(RoomSummary.Empty::class.java) + } + + @Test + fun `PushFront inserts a new entry at the start of the list`() = runTest { + summaries.value = listOf(aRoomSummaryFilled()) + val processor = createProcessor() + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(RoomListEntry.Empty))) + + assertThat(summaries.value.count()).isEqualTo(2) + assertThat(summaries.value.first()).isInstanceOf(RoomSummary.Empty::class.java) + } + + @Test + fun `Set replaces an entry at some index`() = runTest { + summaries.value = listOf(aRoomSummaryFilled()) + val processor = createProcessor() + val index = 0 + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.Set(index.toUInt(), RoomListEntry.Empty))) + + assertThat(summaries.value.count()).isEqualTo(1) + assertThat(summaries.value[index]).isInstanceOf(RoomSummary.Empty::class.java) + } + + @Test + fun `Insert inserts a new entry at the provided index`() = runTest { + summaries.value = listOf(aRoomSummaryFilled()) + val processor = createProcessor() + val index = 0 + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(index.toUInt(), RoomListEntry.Empty))) + + assertThat(summaries.value.count()).isEqualTo(2) + assertThat(summaries.value[index]).isInstanceOf(RoomSummary.Empty::class.java) + } + + @Test + fun `Remove removes an entry at some index`() = runTest { + summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) + val processor = createProcessor() + val index = 0 + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.Remove(index.toUInt()))) + + assertThat(summaries.value.count()).isEqualTo(1) + assertThat((summaries.value[index] as RoomSummary.Filled).identifier()).isEqualTo(A_ROOM_ID_2.value) + } + + @Test + fun `PopBack removes an entry at the end of the list`() = runTest { + summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) + val processor = createProcessor() + val index = 0 + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.PopBack)) + + assertThat(summaries.value.count()).isEqualTo(1) + assertThat((summaries.value[index] as RoomSummary.Filled).identifier()).isEqualTo(A_ROOM_ID.value) + } + + @Test + fun `PopFront removes an entry at the start of the list`() = runTest { + summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) + val processor = createProcessor() + val index = 0 + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.PopFront)) + + assertThat(summaries.value.count()).isEqualTo(1) + assertThat((summaries.value[index] as RoomSummary.Filled).identifier()).isEqualTo(A_ROOM_ID_2.value) + } + + @Test + fun `Clear removes all the entries`() = runTest { + summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) + val processor = createProcessor() + val index = 0 + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.Clear)) + + assertThat(summaries.value).isEmpty() + } + + @Test + fun `Truncate removes all entries after the provided length`() = runTest { + summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) + val processor = createProcessor() + val index = 0 + + // Start processing updates + processor.postEntries(listOf()) + // Process actual update + processor.postUpdate(listOf(RoomListEntriesUpdate.Truncate(1u))) + + assertThat(summaries.value.count()).isEqualTo(1) + assertThat((summaries.value[index] as RoomSummary.Filled).identifier()).isEqualTo(A_ROOM_ID.value) + } + + private fun TestScope.createProcessor() = RoomSummaryListProcessor( + summaries, + fakeRoomListService, + dispatcher = StandardTestDispatcher(testScheduler), + roomSummaryDetailsFactory = RoomSummaryDetailsFactory(), + ) + + // Fake room list service that returns Rust objects with null pointers. Luckily for us, they don't crash for our test cases + private val fakeRoomListService = object : RoomListServiceInterface { + override suspend fun allRooms(): RoomList { + return RoomList(Pointer.NULL) + } + + override suspend fun applyInput(input: RoomListInput) = Unit + + override suspend fun invites(): RoomList { + return RoomList(Pointer.NULL) + } + + override fun room(roomId: String): RoomListItem { + return RoomListItem(Pointer.NULL) + } + + override fun state(listener: RoomListServiceStateListener): TaskHandle { + return TaskHandle(Pointer.NULL) + } + + override fun syncIndicator(delayBeforeShowingInMs: UInt, delayBeforeHidingInMs: UInt, listener: RoomListServiceSyncIndicatorListener): TaskHandle { + return TaskHandle(Pointer.NULL) + } + } +}