Add tests for NotifiableEventResolver

This commit is contained in:
Benoit Marty
2023-11-23 17:36:16 +01:00
parent 6e4635b4cd
commit fc7898ca36
4 changed files with 544 additions and 2 deletions

View File

@@ -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<NotificationData?> {
return Result.success(null)
private var getNotificationResult: Result<NotificationData?> = Result.success(null)
fun givenGetNotificationResult(result: Result<NotificationData?>) {
getNotificationResult = result
}
override suspend fun getNotification(
userId: SessionId,
roomId: RoomId,
eventId: EventId,
): Result<NotificationData?> {
return getNotificationResult
}
}

View File

@@ -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)
}

View File

@@ -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<NotificationData?> = 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

View File

@@ -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
}
}