From fc7898ca36fa5e5c89a745ccc2c15ade5dba1cc0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 23 Nov 2023 17:36:16 +0100 Subject: [PATCH] Add tests for `NotifiableEventResolver` --- .../notification/FakeNotificationService.kt | 14 +- libraries/push/impl/build.gradle.kts | 9 + .../NotifiableEventResolverTest.kt | 496 ++++++++++++++++++ .../test/systemclock/FakeSystemClock.kt | 27 + 4 files changed, 544 insertions(+), 2 deletions(-) create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt create mode 100644 services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/systemclock/FakeSystemClock.kt diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt index 7cb92d35c5..31fc10f342 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt @@ -23,7 +23,17 @@ import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationService class FakeNotificationService : NotificationService { - override suspend fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result { - return Result.success(null) + private var getNotificationResult: Result = Result.success(null) + + fun givenGetNotificationResult(result: Result) { + getNotificationResult = result + } + + override suspend fun getNotification( + userId: SessionId, + roomId: RoomId, + eventId: EventId, + ): Result { + return getNotificationResult } } diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index c67d805fb6..6c765fc44f 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -21,6 +21,12 @@ plugins { android { namespace = "io.element.android.libraries.push.impl" + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } anvil { @@ -55,10 +61,13 @@ dependencies { implementation(projects.services.toolbox.api) testImplementation(libs.test.junit) + testImplementation(libs.test.robolectric) testImplementation(libs.test.mockk) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(libs.coroutines.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.services.appnavstate.test) + testImplementation(projects.services.toolbox.impl) + testImplementation(projects.services.toolbox.test) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt new file mode 100644 index 0000000000..9e18f225b8 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt @@ -0,0 +1,496 @@ +/* + * 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.push.impl.notifications + +import android.content.Context +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.api.notification.NotificationContent +import io.element.android.libraries.matrix.api.notification.NotificationData +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.FakeMatrixClientProvider +import io.element.android.libraries.matrix.test.notification.FakeNotificationService +import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent +import io.element.android.services.toolbox.impl.strings.AndroidStringProvider +import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP +import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class NotifiableEventResolverTest { + + @Test + fun `resolve event no session`() = runTest { + val sut = createNotifiableEventResolver(notificationService = null) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + assertThat(result).isNull() + } + + @Test + fun `resolve event failure`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.failure(AN_EXCEPTION) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + assertThat(result).isNull() + } + + @Test + fun `resolve event null`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success(null) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + assertThat(result).isNull() + } + + @Test + fun `resolve event message text`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = TextMessageType(body = "Hello world", formatted = null) + ) + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = createNotifiableMessageEvent(body = "Hello world") + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve event message audio`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = AudioMessageType(body = "Audio", MediaSource("url"), null) + ) + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = createNotifiableMessageEvent(body = "Audio") + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve event message video`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = VideoMessageType(body = "Video", MediaSource("url"), null) + ) + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = createNotifiableMessageEvent(body = "Video") + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve event message voice`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = VoiceMessageType(body = "Voice", MediaSource("url"), null, null) + ) + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = createNotifiableMessageEvent(body = "Voice message") + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve event message image`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = ImageMessageType("Image", MediaSource("url"), null), + ) + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = createNotifiableMessageEvent(body = "Image") + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve event message file`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = FileMessageType("File", MediaSource("url"), null), + ) + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = createNotifiableMessageEvent(body = "File") + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve event message location`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = LocationMessageType("Location", "geo:1,2", null), + ) + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = createNotifiableMessageEvent(body = "Location") + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve event message notice`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = NoticeMessageType("Notice", null), + ) + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = createNotifiableMessageEvent(body = "Notice") + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve event message emote`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = EmoteMessageType("is happy", null), + ) + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = createNotifiableMessageEvent(body = "* Bob is happy") + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve event message unknown`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = UnknownMessageType, + ) + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = createNotifiableMessageEvent(body = "Unsupported event") + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve poll`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.Poll( + senderId = A_USER_ID_2, + question = "A question" + ) + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = createNotifiableMessageEvent(body = "Poll: A question") + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve RoomMemberContent invite room`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.StateEvent.RoomMemberContent( + userId = A_USER_ID_2.value, + membershipState = RoomMembershipState.INVITE + ), + isDirect = false, + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = InviteNotifiableEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + canBeReplaced = true, + roomName = null, + noisy = false, + title = null, + description = "Invited you to join the room", + type = null, + timestamp = A_TIMESTAMP, + soundName = null, + isRedacted = false, + isUpdated = false, + ) + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve RoomMemberContent invite direct`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.StateEvent.RoomMemberContent( + userId = A_USER_ID_2.value, + membershipState = RoomMembershipState.INVITE + ), + isDirect = true, + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = InviteNotifiableEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + canBeReplaced = true, + roomName = null, + noisy = false, + title = null, + description = "Invited you to chat", + type = null, + timestamp = A_TIMESTAMP, + soundName = null, + isRedacted = false, + isUpdated = false, + ) + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve RoomMemberContent other`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.StateEvent.RoomMemberContent( + userId = A_USER_ID_2.value, + membershipState = RoomMembershipState.JOIN + ) + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + assertThat(result).isNull() + } + + @Test + fun `resolve RoomEncrypted`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.RoomEncrypted + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = FallbackNotifiableEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + description = "Notification", + canBeReplaced = true, + isRedacted = false, + isUpdated = false, + timestamp = A_FAKE_TIMESTAMP, + ) + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve null cases`() { + testNull(NotificationContent.MessageLike.CallAnswer) + testNull(NotificationContent.MessageLike.CallInvite) + testNull(NotificationContent.MessageLike.CallHangup) + testNull(NotificationContent.MessageLike.CallCandidates) + testNull(NotificationContent.MessageLike.KeyVerificationReady) + testNull(NotificationContent.MessageLike.KeyVerificationStart) + testNull(NotificationContent.MessageLike.KeyVerificationCancel) + testNull(NotificationContent.MessageLike.KeyVerificationAccept) + testNull(NotificationContent.MessageLike.KeyVerificationKey) + testNull(NotificationContent.MessageLike.KeyVerificationMac) + testNull(NotificationContent.MessageLike.KeyVerificationDone) + testNull(NotificationContent.MessageLike.ReactionContent(relatedEventId = AN_EVENT_ID_2.value)) + testNull(NotificationContent.MessageLike.RoomRedaction) + testNull(NotificationContent.MessageLike.Sticker) + testNull(NotificationContent.StateEvent.PolicyRuleRoom) + testNull(NotificationContent.StateEvent.PolicyRuleServer) + testNull(NotificationContent.StateEvent.PolicyRuleUser) + testNull(NotificationContent.StateEvent.RoomAliases) + testNull(NotificationContent.StateEvent.RoomAvatar) + testNull(NotificationContent.StateEvent.RoomCanonicalAlias) + testNull(NotificationContent.StateEvent.RoomCreate) + testNull(NotificationContent.StateEvent.RoomEncryption) + testNull(NotificationContent.StateEvent.RoomGuestAccess) + testNull(NotificationContent.StateEvent.RoomHistoryVisibility) + testNull(NotificationContent.StateEvent.RoomJoinRules) + testNull(NotificationContent.StateEvent.RoomName) + testNull(NotificationContent.StateEvent.RoomPinnedEvents) + testNull(NotificationContent.StateEvent.RoomPowerLevels) + testNull(NotificationContent.StateEvent.RoomServerAcl) + testNull(NotificationContent.StateEvent.RoomThirdPartyInvite) + testNull(NotificationContent.StateEvent.RoomTombstone) + testNull(NotificationContent.StateEvent.RoomTopic) + testNull(NotificationContent.StateEvent.SpaceChild) + testNull(NotificationContent.StateEvent.SpaceParent) + } + + private fun testNull(content: NotificationContent) = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = content + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + assertThat(result).isNull() + } + + private fun createNotifiableEventResolver( + notificationService: FakeNotificationService? = FakeNotificationService(), + notificationResult: Result = Result.success(null), + ): NotifiableEventResolver { + val context = RuntimeEnvironment.getApplication() as Context + notificationService?.givenGetNotificationResult(notificationResult) + val matrixClientProvider = FakeMatrixClientProvider(getClient = { + if (notificationService == null) { + Result.failure(IllegalStateException("Client not found")) + } else { + Result.success(FakeMatrixClient(notificationService = notificationService)) + } + }) + + return NotifiableEventResolver( + stringProvider = AndroidStringProvider(context.resources), + clock = FakeSystemClock(), + matrixClientProvider = matrixClientProvider, + ) + } + + private fun createNotificationData( + content: NotificationContent, + isDirect: Boolean = false, + ): NotificationData { + return NotificationData( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + senderAvatarUrl = null, + senderDisplayName = "Bob", + roomAvatarUrl = null, + roomDisplayName = null, + isDirect = isDirect, + isEncrypted = false, + isNoisy = false, + timestamp = A_TIMESTAMP, + content = content, + contentUrl = null, + hasMention = false, + ) + } + + private fun createNotifiableMessageEvent(body: String): NotifiableMessageEvent { + return NotifiableMessageEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + canBeReplaced = false, + senderId = A_USER_ID_2, + noisy = false, + timestamp = A_TIMESTAMP, + senderName = "Bob", + body = body, + imageUriString = null, + threadId = null, + roomName = null, + roomIsDirect = false, + roomAvatarPath = null, + senderAvatarPath = null, + soundName = null, + outGoingMessage = false, + outGoingMessageFailed = false, + isRedacted = false, + isUpdated = false + ) + } +} + +private const val A_TIMESTAMP = 567L diff --git a/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/systemclock/FakeSystemClock.kt b/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/systemclock/FakeSystemClock.kt new file mode 100644 index 0000000000..64d631c411 --- /dev/null +++ b/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/systemclock/FakeSystemClock.kt @@ -0,0 +1,27 @@ +/* + * 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.services.toolbox.test.systemclock + +import io.element.android.services.toolbox.api.systemclock.SystemClock + +const val A_FAKE_TIMESTAMP = 123L + +class FakeSystemClock : SystemClock { + override fun epochMillis(): Long { + return A_FAKE_TIMESTAMP + } +}