Notification events resolving and rendering in batches (#4722)

- Use `NotiticationService.getNotifications()` function so we resolve the events in bulk.
- Added `NotifierResolverQueue` to group the notifications to resolve based on a debounce strategy.
- Batch rendering of these events as notifications.
This commit is contained in:
Jorge Martin Espinosa 2025-05-26 17:10:20 +02:00 committed by GitHub
parent 5ec485911e
commit f8d7b4dfc9
30 changed files with 882 additions and 523 deletions

View file

@ -9,12 +9,14 @@ package io.element.android.libraries.matrix.api.notification
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
data class NotificationData( data class NotificationData(
val sessionId: SessionId,
val eventId: EventId, val eventId: EventId,
val threadId: ThreadId?, val threadId: ThreadId?,
val roomId: RoomId, val roomId: RoomId,

View file

@ -11,5 +11,5 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
interface NotificationService { interface NotificationService {
suspend fun getNotification(roomId: RoomId, eventId: EventId): Result<NotificationData?> suspend fun getNotifications(ids: Map<RoomId, List<EventId>>): Result<Map<EventId, NotificationData>>
} }

View file

@ -152,7 +152,7 @@ class RustMatrixClient(
) )
private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(innerSyncService) private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(innerSyncService)
private val innerNotificationClient = runBlocking { innerClient.notificationClient(notificationProcessSetup) } private val innerNotificationClient = runBlocking { innerClient.notificationClient(notificationProcessSetup) }
private val notificationService = RustNotificationService(innerNotificationClient, dispatchers, clock) private val notificationService = RustNotificationService(sessionId, innerNotificationClient, dispatchers, clock)
private val notificationSettingsService = RustNotificationSettingsService(innerClient, sessionCoroutineScope, dispatchers) private val notificationSettingsService = RustNotificationSettingsService(innerClient, sessionCoroutineScope, dispatchers)
private val encryptionService = RustEncryptionService( private val encryptionService = RustEncryptionService(
client = innerClient, client = innerClient,

View file

@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationContent 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.notification.NotificationData
@ -25,6 +26,7 @@ class NotificationMapper(
private val notificationContentMapper = NotificationContentMapper() private val notificationContentMapper = NotificationContentMapper()
fun map( fun map(
sessionId: SessionId,
eventId: EventId, eventId: EventId,
roomId: RoomId, roomId: RoomId,
notificationItem: NotificationItem notificationItem: NotificationItem
@ -35,6 +37,7 @@ class NotificationMapper(
activeMembersCount = item.roomInfo.joinedMembersCount.toInt(), activeMembersCount = item.roomInfo.joinedMembersCount.toInt(),
) )
NotificationData( NotificationData(
sessionId = sessionId,
eventId = eventId, eventId = eventId,
// FIXME once the `NotificationItem` in the SDK returns the thread id // FIXME once the `NotificationItem` in the SDK returns the thread id
threadId = null, threadId = null,

View file

@ -10,28 +10,45 @@ package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.NotificationClient import org.matrix.rustcomponents.sdk.NotificationClient
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.NotificationItemsRequest
import timber.log.Timber
class RustNotificationService( class RustNotificationService(
private val sessionId: SessionId,
private val notificationClient: NotificationClient, private val notificationClient: NotificationClient,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
clock: SystemClock, clock: SystemClock,
) : NotificationService { ) : NotificationService {
private val notificationMapper: NotificationMapper = NotificationMapper(clock) private val notificationMapper: NotificationMapper = NotificationMapper(clock)
override suspend fun getNotification( override suspend fun getNotifications(
roomId: RoomId, ids: Map<RoomId, List<EventId>>
eventId: EventId, ): Result<Map<EventId, NotificationData>> = withContext(dispatchers.io) {
): Result<NotificationData?> = withContext(dispatchers.io) {
runCatching { runCatching {
val item = notificationClient.getNotification(roomId.value, eventId.value) val requests = ids.map { (roomId, eventIds) ->
item?.use { NotificationItemsRequest(
notificationMapper.map(eventId, roomId, it) roomId = roomId.value,
eventIds = eventIds.map { it.value }
)
}
val items = notificationClient.getNotifications(requests)
buildMap {
val eventIds = requests.flatMap { it.eventIds }
for (eventId in eventIds) {
val item = items[eventId]
if (item != null) {
val roomId = RoomId(requests.find { it.eventIds.contains(eventId) }?.roomId!!)
put(EventId(eventId), notificationMapper.map(sessionId, EventId(eventId), roomId, item))
} else {
Timber.e("Could not retrieve event for notification with $eventId")
}
}
} }
} }
} }

View file

@ -7,15 +7,15 @@
package io.element.android.libraries.matrix.impl.fixtures.fakes package io.element.android.libraries.matrix.impl.fixtures.fakes
import io.element.android.tests.testutils.simulateLongTask
import org.matrix.rustcomponents.sdk.NoPointer import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.NotificationClient import org.matrix.rustcomponents.sdk.NotificationClient
import org.matrix.rustcomponents.sdk.NotificationItem import org.matrix.rustcomponents.sdk.NotificationItem
import org.matrix.rustcomponents.sdk.NotificationItemsRequest
class FakeRustNotificationClient( class FakeRustNotificationClient(
var notificationItemResult: NotificationItem? = null var notificationItemResult: Map<String, NotificationItem> = emptyMap(),
) : NotificationClient(NoPointer) { ) : NotificationClient(NoPointer) {
override suspend fun getNotification(roomId: String, eventId: String): NotificationItem? = simulateLongTask { override suspend fun getNotifications(requests: List<NotificationItemsRequest>): Map<String, NotificationItem> {
notificationItemResult return notificationItemResult
} }
} }

View file

@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustNotificat
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_MESSAGE
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_SESSION_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.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
@ -28,12 +29,12 @@ class RustNotificationServiceTest {
@Test @Test
fun test() = runTest { fun test() = runTest {
val notificationClient = FakeRustNotificationClient( val notificationClient = FakeRustNotificationClient(
notificationItemResult = aRustNotificationItem(), notificationItemResult = mapOf(AN_EVENT_ID.value to aRustNotificationItem()),
) )
val sut = createRustNotificationService( val sut = createRustNotificationService(
notificationClient = notificationClient, notificationClient = notificationClient,
) )
val result = sut.getNotification(A_ROOM_ID, AN_EVENT_ID).getOrThrow()!! val result = sut.getNotifications(mapOf(A_ROOM_ID to listOf(AN_EVENT_ID))).getOrThrow()[AN_EVENT_ID]!!
assertThat(result.isEncrypted).isTrue() assertThat(result.isEncrypted).isTrue()
assertThat(result.content).isEqualTo( assertThat(result.content).isEqualTo(
NotificationContent.MessageLike.RoomMessage( NotificationContent.MessageLike.RoomMessage(
@ -51,6 +52,7 @@ class RustNotificationServiceTest {
clock: SystemClock = FakeSystemClock(), clock: SystemClock = FakeSystemClock(),
) = ) =
RustNotificationService( RustNotificationService(
sessionId = A_SESSION_ID,
notificationClient = notificationClient, notificationClient = notificationClient,
dispatchers = testCoroutineDispatchers(), dispatchers = testCoroutineDispatchers(),
clock = clock, clock = clock,

View file

@ -13,16 +13,13 @@ import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notification.NotificationService
class FakeNotificationService : NotificationService { class FakeNotificationService : NotificationService {
private var getNotificationResult: Result<NotificationData?> = Result.success(null) private var getNotificationsResult: Result<Map<EventId, NotificationData>> = Result.success(emptyMap())
fun givenGetNotificationResult(result: Result<NotificationData?>) { fun givenGetNotificationsResult(result: Result<Map<EventId, NotificationData>>) {
getNotificationResult = result getNotificationsResult = result
} }
override suspend fun getNotification( override suspend fun getNotifications(ids: Map<RoomId, List<EventId>>): Result<Map<EventId, NotificationData>> {
roomId: RoomId, return getNotificationsResult
eventId: EventId,
): Result<NotificationData?> {
return getNotificationResult
} }
} }

View file

@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
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_NAME import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_TIMESTAMP import io.element.android.libraries.matrix.test.A_TIMESTAMP
import io.element.android.libraries.matrix.test.A_USER_NAME_2 import io.element.android.libraries.matrix.test.A_USER_NAME_2
@ -27,6 +28,7 @@ fun aNotificationData(
roomDisplayName: String? = A_ROOM_NAME roomDisplayName: String? = A_ROOM_NAME
): NotificationData { ): NotificationData {
return NotificationData( return NotificationData(
sessionId = A_SESSION_ID,
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
threadId = threadId, threadId = threadId,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,

View file

@ -11,10 +11,10 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.extensions.flatMap
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
@ -61,10 +61,14 @@ private val loggerTag = LoggerTag("DefaultNotifiableEventResolver", LoggerTag.No
* this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk. * this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
*/ */
interface NotifiableEventResolver { interface NotifiableEventResolver {
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): Result<ResolvedPushEvent> suspend fun resolveEvents(
sessionId: SessionId,
notificationEventRequests: List<NotificationEventRequest>
): Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>
} }
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class DefaultNotifiableEventResolver @Inject constructor( class DefaultNotifiableEventResolver @Inject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val clock: SystemClock, private val clock: SystemClock,
@ -75,29 +79,34 @@ class DefaultNotifiableEventResolver @Inject constructor(
private val callNotificationEventResolver: CallNotificationEventResolver, private val callNotificationEventResolver: CallNotificationEventResolver,
private val appPreferencesStore: AppPreferencesStore, private val appPreferencesStore: AppPreferencesStore,
) : NotifiableEventResolver { ) : NotifiableEventResolver {
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): Result<ResolvedPushEvent> { override suspend fun resolveEvents(
// Restore session sessionId: SessionId,
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return Result.failure( notificationEventRequests: List<NotificationEventRequest>
ResolvingException("Unable to restore session for $sessionId") ): Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>> {
) Timber.d("Queueing notifications: $notificationEventRequests")
val notificationService = client.notificationService() val client = matrixClientProvider.getOrRestore(sessionId).getOrElse {
val notificationData = notificationService.getNotification( return Result.failure(IllegalStateException("Couldn't get or restore client for session $sessionId"))
roomId = roomId,
eventId = eventId,
).onFailure {
Timber.tag(loggerTag.value).e(it, "Unable to resolve event: $eventId.")
} }
val ids = notificationEventRequests.groupBy { it.roomId }.mapValues { (_, value) -> value.map { it.eventId } }
// TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event // TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event
return notificationData.flatMap { val notifications = client.notificationService().getNotifications(ids).mapCatching { map ->
if (it == null) { map.mapValues { (_, notificationData) ->
Timber.tag(loggerTag.value).d("No notification data found for event $eventId") notificationData.asNotifiableEvent(client, sessionId)
return@flatMap Result.failure(ResolvingException("Unable to resolve event $eventId")) }
}
return Result.success(
notificationEventRequests.associate {
val notificationData = notifications.getOrNull()?.get(it.eventId)
if (notificationData != null) {
it to notificationData
} else { } else {
Timber.tag(loggerTag.value).d("Found notification item for $eventId") // TODO once the SDK can actually return what went wrong, we should return it here instead of this generic error
it.asNotifiableEvent(client, sessionId) it to Result.failure(ResolvingException("No notification data for ${it.roomId} - ${it.eventId}"))
} }
} }
)
} }
private suspend fun NotificationData.asNotifiableEvent( private suspend fun NotificationData.asNotifiableEvent(

View file

@ -113,6 +113,11 @@ class DefaultNotificationDrawerManager @Inject constructor(
renderEvents(listOf(notifiableEvent)) renderEvents(listOf(notifiableEvent))
} }
suspend fun onNotifiableEventsReceived(notifiableEvents: List<NotifiableEvent>) {
val eventsToNotify = notifiableEvents.filter { !it.shouldIgnoreEventInRoom(appNavigationStateService.appNavigationState.value) }
renderEvents(eventsToNotify)
}
/** /**
* Clear all known message events for a [sessionId]. * Clear all known message events for a [sessionId].
*/ */

View file

@ -30,8 +30,9 @@ class DefaultOnMissedCallNotificationHandler @Inject constructor(
// Resolve the event and add a notification for it, at this point it should no longer be a ringing one // Resolve the event and add a notification for it, at this point it should no longer be a ringing one
val notificationData = matrixClientProvider.getOrRestore(sessionId).getOrNull() val notificationData = matrixClientProvider.getOrRestore(sessionId).getOrNull()
?.notificationService() ?.notificationService()
?.getNotification(roomId, eventId) ?.getNotifications(mapOf(roomId to listOf(eventId)))
?.getOrNull() ?.getOrNull()
?.get(eventId)
?: return ?: return
val notifiableEvent = callNotificationEventResolver.resolveEvent( val notifiableEvent = callNotificationEventResolver.resolveEvent(

View file

@ -163,7 +163,7 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
roomIsDm = room.isDm(), roomIsDm = room.isDm(),
outGoingMessage = true, outGoingMessage = true,
) )
onNotifiableEventReceived.onNotifiableEventReceived(notifiableMessageEvent) onNotifiableEventReceived.onNotifiableEventsReceived(listOf(notifiableMessageEvent))
if (threadId != null && replyToEventId != null) { if (threadId != null && replyToEventId != null) {
room.liveTimeline.replyMessage( room.liveTimeline.replyMessage(
@ -181,11 +181,13 @@ class NotificationBroadcastReceiverHandler @Inject constructor(
) )
}.onFailure { }.onFailure {
Timber.e(it, "Failed to send smart reply message") Timber.e(it, "Failed to send smart reply message")
onNotifiableEventReceived.onNotifiableEventReceived( onNotifiableEventReceived.onNotifiableEventsReceived(
listOf(
notifiableMessageEvent.copy( notifiableMessageEvent.copy(
outGoingMessageFailed = true outGoingMessageFailed = true
) )
) )
)
} }
} }
} }

View file

@ -37,6 +37,7 @@ class DefaultNotificationDisplayer @Inject constructor(
return false return false
} }
notificationManager.notify(tag, id, notification) notificationManager.notify(tag, id, notification)
Timber.d("Notifying with tag: $tag, id: $id")
return true return true
} }

View file

@ -0,0 +1,101 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.impl.notifications
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
/**
* This class is responsible for periodically batching notification requests and resolving them in a single call,
* so that we can avoid having to resolve each notification individually in the SDK.
*/
@OptIn(ExperimentalCoroutinesApi::class)
@SingleIn(AppScope::class)
class NotificationResolverQueue @Inject constructor(
private val notifiableEventResolver: NotifiableEventResolver,
private val appCoroutineScope: CoroutineScope,
) {
companion object {
private const val BATCH_WINDOW_MS = 250L
}
private val requestQueue = Channel<NotificationEventRequest>(capacity = 100)
private var currentProcessingJob: Job? = null
/**
* A flow that emits pairs of a list of notification event requests and a map of the resolved events.
* The map contains the original request as the key and the resolved event as the value.
*/
val results: SharedFlow<Pair<List<NotificationEventRequest>, Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> = MutableSharedFlow()
/**
* Enqueues a notification event request to be resolved.
* The request will be processed in batches, so it may not be resolved immediately.
*
* @param request The notification event request to enqueue.
*/
suspend fun enqueue(request: NotificationEventRequest) {
// Cancel previous processing job if it exists, acting as a debounce operation
Timber.d("Cancelling job: $currentProcessingJob")
currentProcessingJob?.cancel()
// Enqueue the request and start a delayed processing job
requestQueue.send(request)
currentProcessingJob = processQueue()
Timber.d("Starting processing job for request: $request")
}
private fun processQueue() = appCoroutineScope.launch(SupervisorJob()) {
delay(BATCH_WINDOW_MS.milliseconds)
// If this job is still active (so this is the latest job), we launch a separate one that won't be cancelled when enqueueing new items
// to process the existing queued items.
appCoroutineScope.launch {
val groupedRequestsById = buildList {
while (!requestQueue.isEmpty) {
requestQueue.receiveCatching().getOrNull()?.let(this::add)
}
}.groupBy { it.sessionId }
val sessionIds = groupedRequestsById.keys
for (sessionId in sessionIds) {
val requests = groupedRequestsById[sessionId].orEmpty()
Timber.d("Fetching notifications for $sessionId: $requests. Pending requests: ${!requestQueue.isEmpty}")
// Resolving the events in parallel should improve performance since each session id will query a different Client
launch {
// No need for a Mutex since the SDK already has one internally
val notifications = notifiableEventResolver.resolveEvents(sessionId, requests).getOrNull().orEmpty()
(results as MutableSharedFlow).emit(requests to notifications)
}
}
}
}
}
data class NotificationEventRequest(
val sessionId: SessionId,
val roomId: RoomId,
val eventId: EventId,
val providerInfo: String,
)

View file

@ -12,12 +12,22 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
sealed interface ResolvedPushEvent { sealed interface ResolvedPushEvent {
data class Event(val notifiableEvent: NotifiableEvent) : ResolvedPushEvent val sessionId: SessionId
val roomId: RoomId
val eventId: EventId
data class Event(val notifiableEvent: NotifiableEvent) : ResolvedPushEvent {
override val sessionId: SessionId = notifiableEvent.sessionId
override val roomId: RoomId = notifiableEvent.roomId
override val eventId: EventId = notifiableEvent.eventId
}
data class Redaction( data class Redaction(
val sessionId: SessionId, override val sessionId: SessionId,
val roomId: RoomId, override val roomId: RoomId,
val redactedEventId: EventId, val redactedEventId: EventId,
val reason: String?, val reason: String?,
) : ResolvedPushEvent ) : ResolvedPushEvent {
override val eventId: EventId = redactedEventId
}
} }

View file

@ -13,6 +13,7 @@ import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.push.impl.history.PushHistoryService import io.element.android.libraries.push.impl.history.PushHistoryService
import io.element.android.libraries.push.impl.history.onDiagnosticPush import io.element.android.libraries.push.impl.history.onDiagnosticPush
@ -20,8 +21,10 @@ import io.element.android.libraries.push.impl.history.onInvalidPushReceived
import io.element.android.libraries.push.impl.history.onSuccess import io.element.android.libraries.push.impl.history.onSuccess
import io.element.android.libraries.push.impl.history.onUnableToResolveEvent import io.element.android.libraries.push.impl.history.onUnableToResolveEvent
import io.element.android.libraries.push.impl.history.onUnableToRetrieveSession import io.element.android.libraries.push.impl.history.onUnableToRetrieveSession
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver import io.element.android.libraries.push.impl.notifications.NotificationEventRequest
import io.element.android.libraries.push.impl.notifications.NotificationResolverQueue
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.libraries.push.impl.test.DefaultTestPush import io.element.android.libraries.push.impl.test.DefaultTestPush
@ -30,17 +33,21 @@ import io.element.android.libraries.pushproviders.api.PushData
import io.element.android.libraries.pushproviders.api.PushHandler import io.element.android.libraries.pushproviders.api.PushHandler
import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag) private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag)
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultPushHandler @Inject constructor( class DefaultPushHandler @Inject constructor(
private val onNotifiableEventReceived: OnNotifiableEventReceived, private val onNotifiableEventReceived: OnNotifiableEventReceived,
private val onRedactedEventReceived: OnRedactedEventReceived, private val onRedactedEventReceived: OnRedactedEventReceived,
private val notifiableEventResolver: NotifiableEventResolver,
private val incrementPushDataStore: IncrementPushDataStore, private val incrementPushDataStore: IncrementPushDataStore,
private val userPushStoreFactory: UserPushStoreFactory, private val userPushStoreFactory: UserPushStoreFactory,
private val pushClientSecret: PushClientSecret, private val pushClientSecret: PushClientSecret,
@ -50,7 +57,85 @@ class DefaultPushHandler @Inject constructor(
private val elementCallEntryPoint: ElementCallEntryPoint, private val elementCallEntryPoint: ElementCallEntryPoint,
private val notificationChannels: NotificationChannels, private val notificationChannels: NotificationChannels,
private val pushHistoryService: PushHistoryService, private val pushHistoryService: PushHistoryService,
private val resolverQueue: NotificationResolverQueue,
private val appCoroutineScope: CoroutineScope,
) : PushHandler { ) : PushHandler {
init {
processPushEventResults()
}
/**
* Process the push notification event results emitted by the [resolverQueue].
*/
private fun processPushEventResults() {
resolverQueue.results
.map { (requests, resolvedEvents) ->
for (request in requests) {
// Log the result of the push notification event
val result = resolvedEvents[request]
if (result?.isSuccess == true) {
pushHistoryService.onSuccess(
providerInfo = request.providerInfo,
eventId = request.eventId,
roomId = request.roomId,
sessionId = request.sessionId,
comment = "Push handled successfully",
)
} else {
pushHistoryService.onUnableToResolveEvent(
providerInfo = request.providerInfo,
eventId = request.eventId,
roomId = request.roomId,
sessionId = request.sessionId,
reason = "Push not handled",
)
}
}
val events = mutableListOf<NotifiableEvent>()
val redactions = mutableListOf<ResolvedPushEvent.Redaction>()
@Suppress("LoopWithTooManyJumpStatements")
for (result in resolvedEvents.values) {
val event = result.getOrNull() ?: continue
val userPushStore = userPushStoreFactory.getOrCreate(event.sessionId)
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
// If notifications are disabled for this session and device, we don't want to show the notification
// But if it's a ringing call, we want to show it anyway
val isRingingCall = (event as? ResolvedPushEvent.Event)?.notifiableEvent is NotifiableRingingCallEvent
if (!areNotificationsEnabled && !isRingingCall) continue
// We categorise each result into either a NotifiableEvent or a Redaction
when (event) {
is ResolvedPushEvent.Event -> {
events.add(event.notifiableEvent)
}
is ResolvedPushEvent.Redaction -> {
redactions.add(event)
}
}
}
// Process redactions of messages
if (redactions.isNotEmpty()) {
onRedactedEventReceived.onRedactedEventsReceived(redactions)
}
// Find and process ringing call notifications separately
val (ringingCallEvents, nonRingingCallEvents) = events.partition { it is NotifiableRingingCallEvent }
for (ringingCallEvent in ringingCallEvents) {
Timber.tag(loggerTag.value).d("Ringing call event: $ringingCallEvent")
handleRingingCallEvent(ringingCallEvent as NotifiableRingingCallEvent)
}
// Finally, process other notifications (messages, invites, generic notifications, etc.)
if (nonRingingCallEvents.isNotEmpty()) {
onNotifiableEventReceived.onNotifiableEventsReceived(nonRingingCallEvents)
}
}
.launchIn(appCoroutineScope)
}
/** /**
* Called when message is received. * Called when message is received.
* *
@ -119,52 +204,17 @@ class DefaultPushHandler @Inject constructor(
) )
return return
} }
notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId).fold(
onSuccess = { resolvedPushEvent ->
pushHistoryService.onSuccess(
providerInfo = providerInfo,
eventId = pushData.eventId,
roomId = pushData.roomId,
sessionId = userId,
comment = resolvedPushEvent.javaClass.simpleName,
)
when (resolvedPushEvent) { appCoroutineScope.launch {
is ResolvedPushEvent.Event -> { val notificationEventRequest = NotificationEventRequest(
when (val notifiableEvent = resolvedPushEvent.notifiableEvent) {
is NotifiableRingingCallEvent -> {
Timber.tag(loggerTag.value).d("Notifiable event ${pushData.eventId} is ringing call: $notifiableEvent")
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
handleRingingCallEvent(notifiableEvent)
}
else -> {
Timber.tag(loggerTag.value).d("Notifiable event ${pushData.eventId} is normal event: $notifiableEvent")
val userPushStore = userPushStoreFactory.getOrCreate(userId)
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
if (areNotificationsEnabled) {
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
} else {
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
}
}
}
}
is ResolvedPushEvent.Redaction -> {
onRedactedEventReceived.onRedactedEventReceived(resolvedPushEvent)
}
}
},
onFailure = { failure ->
Timber.tag(loggerTag.value).w(failure, "Unable to get a notification data")
pushHistoryService.onUnableToResolveEvent(
providerInfo = providerInfo,
eventId = pushData.eventId,
roomId = pushData.roomId,
sessionId = userId, sessionId = userId,
reason = failure.message ?: failure.javaClass.simpleName, roomId = pushData.roomId,
eventId = pushData.eventId,
providerInfo = providerInfo,
) )
Timber.d("Queueing notification: $notificationEventRequest")
resolverQueue.enqueue(notificationEventRequest)
} }
)
} catch (e: Exception) { } catch (e: Exception) {
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed") Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
} }

View file

@ -17,7 +17,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
interface OnNotifiableEventReceived { interface OnNotifiableEventReceived {
fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) fun onNotifiableEventsReceived(notifiableEvents: List<NotifiableEvent>)
} }
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
@ -26,12 +26,10 @@ class DefaultOnNotifiableEventReceived @Inject constructor(
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val syncOnNotifiableEvent: SyncOnNotifiableEvent, private val syncOnNotifiableEvent: SyncOnNotifiableEvent,
) : OnNotifiableEventReceived { ) : OnNotifiableEventReceived {
override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { override fun onNotifiableEventsReceived(notifiableEvents: List<NotifiableEvent>) {
coroutineScope.launch { coroutineScope.launch {
launch { syncOnNotifiableEvent(notifiableEvent) } launch { syncOnNotifiableEvent(notifiableEvents) }
if (notifiableEvent !is NotifiableRingingCallEvent) { defaultNotificationDrawerManager.onNotifiableEventsReceived(notifiableEvents.filter { it !is NotifiableRingingCallEvent })
defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
}
} }
} }
} }

View file

@ -29,7 +29,7 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
interface OnRedactedEventReceived { interface OnRedactedEventReceived {
fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction) fun onRedactedEventsReceived(redactions: List<ResolvedPushEvent.Redaction>)
} }
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
@ -40,11 +40,16 @@ class DefaultOnRedactedEventReceived @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
) : OnRedactedEventReceived { ) : OnRedactedEventReceived {
override fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction) { override fun onRedactedEventsReceived(redactions: List<ResolvedPushEvent.Redaction>) {
coroutineScope.launch { coroutineScope.launch {
val redactionsBySessionIdAndRoom = redactions.groupBy { redaction ->
redaction.sessionId to redaction.roomId
}
for ((keys, roomRedactions) in redactionsBySessionIdAndRoom) {
val (sessionId, roomId) = keys
val notifications = activeNotificationsProvider.getMessageNotificationsForRoom( val notifications = activeNotificationsProvider.getMessageNotificationsForRoom(
redaction.sessionId, sessionId,
redaction.roomId, roomId,
) )
if (notifications.isEmpty()) { if (notifications.isEmpty()) {
Timber.d("No notifications found for redacted event") Timber.d("No notifications found for redacted event")
@ -57,7 +62,7 @@ class DefaultOnRedactedEventReceived @Inject constructor(
return@forEach return@forEach
} }
val messageToRedactIndex = messagingStyle.messages.indexOfFirst { message -> val messageToRedactIndex = messagingStyle.messages.indexOfFirst { message ->
message.extras.getString(DefaultNotificationCreator.MESSAGE_EVENT_ID) == redaction.redactedEventId.value roomRedactions.any { it.redactedEventId.value == message.extras.getString(DefaultNotificationCreator.MESSAGE_EVENT_ID) }
} }
if (messageToRedactIndex == -1) { if (messageToRedactIndex == -1) {
Timber.d("Unable to find the message to remove from notification") Timber.d("Unable to find the message to remove from notification")
@ -85,4 +90,5 @@ class DefaultOnRedactedEventReceived @Inject constructor(
} }
} }
} }
}
} }

View file

@ -8,15 +8,14 @@
package io.element.android.libraries.push.impl.push package io.element.android.libraries.push.impl.push
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.parallelMap
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import io.element.android.services.appnavstate.api.ActiveRoomsHolder import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.api.AppForegroundStateService import io.element.android.services.appnavstate.api.AppForegroundStateService
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -33,57 +32,44 @@ class SyncOnNotifiableEvent @Inject constructor(
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val activeRoomsHolder: ActiveRoomsHolder, private val activeRoomsHolder: ActiveRoomsHolder,
) { ) {
suspend operator fun invoke(notifiableEvent: NotifiableEvent) = withContext(dispatchers.io) { suspend operator fun invoke(notifiableEvents: List<NotifiableEvent>) = withContext(dispatchers.io) {
val isRingingCallEvent = notifiableEvent is NotifiableRingingCallEvent if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncOnPush)) {
if (!featureFlagService.isFeatureEnabled(FeatureFlags.SyncOnPush) && !isRingingCallEvent) {
return@withContext return@withContext
} }
val activeRoom = activeRoomsHolder.getActiveRoomMatching(notifiableEvent.sessionId, notifiableEvent.roomId)
if (activeRoom != null) {
// If the room is already active, we can use it directly
activeRoom.subscribeToSyncAndWait(notifiableEvent, isRingingCallEvent)
} else {
// Otherwise, we need to get the room from the matrix client
val room = matrixClientProvider
.getOrRestore(notifiableEvent.sessionId)
.mapCatching { it.getJoinedRoom(notifiableEvent.roomId) }
.getOrNull()
room?.use { it.subscribeToSyncAndWait(notifiableEvent, isRingingCallEvent) }
}
}
private suspend fun JoinedRoom.subscribeToSyncAndWait(notifiableEvent: NotifiableEvent, isRingingCallEvent: Boolean) {
subscribeToSync()
// If the app is in foreground, sync is already running, so we just add the subscription above.
if (!appForegroundStateService.isInForeground.value) {
if (isRingingCallEvent) {
waitsUntilUserIsInTheCall(timeout = 60.seconds)
} else {
try { try {
val eventsBySession = notifiableEvents.groupBy { it.sessionId }
appForegroundStateService.updateIsSyncingNotificationEvent(true) appForegroundStateService.updateIsSyncingNotificationEvent(true)
waitsUntilEventIsKnown(eventId = notifiableEvent.eventId, timeout = 10.seconds)
for ((sessionId, events) in eventsBySession) {
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: continue
val eventsByRoomId = events.groupBy { it.roomId }
client.roomListService.subscribeToVisibleRooms(eventsByRoomId.keys.toList())
if (!appForegroundStateService.isInForeground.value) {
for ((roomId, eventsInRoom) in eventsByRoomId) {
val activeRoom = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
val room = activeRoom ?: client.getJoinedRoom(roomId)
if (room != null) {
eventsInRoom.parallelMap { event ->
room.waitsUntilEventIsKnown(event.eventId, timeout = 10.seconds)
}
}
if (room != null && activeRoom == null) {
// Destroy the room we just instantiated to reset its live timeline
room.destroy()
}
}
}
}
} finally { } finally {
appForegroundStateService.updateIsSyncingNotificationEvent(false) appForegroundStateService.updateIsSyncingNotificationEvent(false)
} }
} }
}
}
/**
* User can be in the call if they answer using another session.
* If the user does not join the call, the timeout will be reached.
*/
private suspend fun BaseRoom.waitsUntilUserIsInTheCall(timeout: Duration) {
withTimeoutOrNull(timeout) {
roomInfoFlow.first {
sessionId in it.activeRoomCallParticipants
}
}
}
private suspend fun JoinedRoom.waitsUntilEventIsKnown(eventId: EventId, timeout: Duration) { private suspend fun JoinedRoom.waitsUntilEventIsKnown(eventId: EventId, timeout: Duration) {
withTimeoutOrNull(timeout) { withTimeoutOrNull(timeout) {

View file

@ -9,6 +9,7 @@ package io.element.android.libraries.push.impl.notifications
import android.content.Context import android.content.Context
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.notification.CallNotifyType import io.element.android.libraries.matrix.api.notification.CallNotifyType
import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationContent
@ -67,7 +68,7 @@ class DefaultNotifiableEventResolverTest {
@Test @Test
fun `resolve event no session`() = runTest { fun `resolve event no session`() = runTest {
val sut = createDefaultNotifiableEventResolver(notificationService = null) val sut = createDefaultNotifiableEventResolver(notificationService = null)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) val result = sut.resolveEvents(A_SESSION_ID, listOf(NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")))
assertThat(result.isFailure).isTrue() assertThat(result.isFailure).isTrue()
} }
@ -76,24 +77,17 @@ class DefaultNotifiableEventResolverTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.failure(AN_EXCEPTION) notificationResult = Result.failure(AN_EXCEPTION)
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
assertThat(result.isFailure).isTrue() val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
} assertThat(result.getEvent(request)?.isFailure).isTrue()
@Test
fun `resolve event null`() = runTest {
val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(null)
)
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
assertThat(result.isFailure).isTrue()
} }
@Test @Test
fun `resolve event message text`() = runTest { fun `resolve event message text`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = TextMessageType(body = "Hello world", formatted = null) messageType = TextMessageType(body = "Hello world", formatted = null)
@ -101,11 +95,13 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
aNotifiableMessageEvent(body = "Hello world") aNotifiableMessageEvent(body = "Hello world")
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
@ -113,7 +109,8 @@ class DefaultNotifiableEventResolverTest {
fun `resolve event message with mention`() = runTest { fun `resolve event message with mention`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = TextMessageType(body = "Hello world", formatted = null) messageType = TextMessageType(body = "Hello world", formatted = null)
@ -122,18 +119,21 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
aNotifiableMessageEvent(body = "Hello world", hasMentionOrReply = true) aNotifiableMessageEvent(body = "Hello world", hasMentionOrReply = true)
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve HTML formatted event message text takes plain text version`() = runTest { fun `resolve HTML formatted event message text takes plain text version`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = TextMessageType( messageType = TextMessageType(
@ -147,18 +147,21 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
aNotifiableMessageEvent(body = "Hello world") aNotifiableMessageEvent(body = "Hello world")
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve incorrectly formatted event message text uses fallback`() = runTest { fun `resolve incorrectly formatted event message text uses fallback`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = TextMessageType( messageType = TextMessageType(
@ -172,18 +175,21 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
aNotifiableMessageEvent(body = "Hello world") aNotifiableMessageEvent(body = "Hello world")
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve event message audio`() = runTest { fun `resolve event message audio`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = AudioMessageType("Audio", null, null, MediaSource("url"), null) messageType = AudioMessageType("Audio", null, null, MediaSource("url"), null)
@ -191,18 +197,21 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
aNotifiableMessageEvent(body = "Audio") aNotifiableMessageEvent(body = "Audio")
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve event message video`() = runTest { fun `resolve event message video`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = VideoMessageType("Video", null, null, MediaSource("url"), null) messageType = VideoMessageType("Video", null, null, MediaSource("url"), null)
@ -210,18 +219,21 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
aNotifiableMessageEvent(body = "Video") aNotifiableMessageEvent(body = "Video")
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve event message voice`() = runTest { fun `resolve event message voice`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = VoiceMessageType("Voice", null, null, MediaSource("url"), null, null) messageType = VoiceMessageType("Voice", null, null, MediaSource("url"), null, null)
@ -229,18 +241,21 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
aNotifiableMessageEvent(body = "Voice message") aNotifiableMessageEvent(body = "Voice message")
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve event message image`() = runTest { fun `resolve event message image`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = ImageMessageType("Image", null, null, MediaSource("url"), null), messageType = ImageMessageType("Image", null, null, MediaSource("url"), null),
@ -248,18 +263,21 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
aNotifiableMessageEvent(body = "Image") aNotifiableMessageEvent(body = "Image")
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve event message sticker`() = runTest { fun `resolve event message sticker`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = StickerMessageType("Sticker", null, null, MediaSource("url"), null), messageType = StickerMessageType("Sticker", null, null, MediaSource("url"), null),
@ -267,18 +285,21 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
aNotifiableMessageEvent(body = "Sticker") aNotifiableMessageEvent(body = "Sticker")
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve event message file`() = runTest { fun `resolve event message file`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = FileMessageType("File", null, null, MediaSource("url"), null), messageType = FileMessageType("File", null, null, MediaSource("url"), null),
@ -286,18 +307,21 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
aNotifiableMessageEvent(body = "File") aNotifiableMessageEvent(body = "File")
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve event message location`() = runTest { fun `resolve event message location`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = LocationMessageType("Location", "geo:1,2", null), messageType = LocationMessageType("Location", "geo:1,2", null),
@ -305,18 +329,21 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
aNotifiableMessageEvent(body = "Location") aNotifiableMessageEvent(body = "Location")
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve event message notice`() = runTest { fun `resolve event message notice`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = NoticeMessageType("Notice", null), messageType = NoticeMessageType("Notice", null),
@ -324,18 +351,21 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
aNotifiableMessageEvent(body = "Notice") aNotifiableMessageEvent(body = "Notice")
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve event message emote`() = runTest { fun `resolve event message emote`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = EmoteMessageType("is happy", null), messageType = EmoteMessageType("is happy", null),
@ -343,18 +373,21 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
aNotifiableMessageEvent(body = "* Bob is happy") aNotifiableMessageEvent(body = "* Bob is happy")
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve poll`() = runTest { fun `resolve poll`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.Poll( content = NotificationContent.MessageLike.Poll(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
question = "A question" question = "A question"
@ -362,18 +395,21 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
aNotifiableMessageEvent(body = "Poll: A question") aNotifiableMessageEvent(body = "Poll: A question")
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve RoomMemberContent invite room`() = runTest { fun `resolve RoomMemberContent invite room`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.StateEvent.RoomMemberContent( content = NotificationContent.StateEvent.RoomMemberContent(
userId = A_USER_ID_2, userId = A_USER_ID_2,
membershipState = RoomMembershipState.INVITE membershipState = RoomMembershipState.INVITE
@ -382,15 +418,18 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
assertThat(result.getOrNull()).isNull() val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
assertThat(result.getEvent(request)?.getOrNull()).isNull()
} }
@Test @Test
fun `resolve invite room`() = runTest { fun `resolve invite room`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.Invite( content = NotificationContent.Invite(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
), ),
@ -398,7 +437,9 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
InviteNotifiableEvent( InviteNotifiableEvent(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
@ -417,14 +458,15 @@ class DefaultNotifiableEventResolverTest {
isUpdated = false, isUpdated = false,
) )
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve invite direct`() = runTest { fun `resolve invite direct`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.Invite( content = NotificationContent.Invite(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
), ),
@ -432,7 +474,9 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
InviteNotifiableEvent( InviteNotifiableEvent(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
@ -451,14 +495,15 @@ class DefaultNotifiableEventResolverTest {
isUpdated = false, isUpdated = false,
) )
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve invite direct, no display name`() = runTest { fun `resolve invite direct, no display name`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.Invite( content = NotificationContent.Invite(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
), ),
@ -467,7 +512,9 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
InviteNotifiableEvent( InviteNotifiableEvent(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
@ -486,14 +533,15 @@ class DefaultNotifiableEventResolverTest {
isUpdated = false, isUpdated = false,
) )
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve invite direct, ambiguous display name`() = runTest { fun `resolve invite direct, ambiguous display name`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.Invite( content = NotificationContent.Invite(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
), ),
@ -502,7 +550,9 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
InviteNotifiableEvent( InviteNotifiableEvent(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
@ -521,14 +571,15 @@ class DefaultNotifiableEventResolverTest {
isUpdated = false, isUpdated = false,
) )
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve RoomMemberContent other`() = runTest { fun `resolve RoomMemberContent other`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.StateEvent.RoomMemberContent( content = NotificationContent.StateEvent.RoomMemberContent(
userId = A_USER_ID_2, userId = A_USER_ID_2,
membershipState = RoomMembershipState.JOIN membershipState = RoomMembershipState.JOIN
@ -536,20 +587,21 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
assertThat(result.getOrNull()).isNull() val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
assertThat(result.getEvent(request)?.getOrNull()).isNull()
} }
@Test @Test
fun `resolve RoomEncrypted`() = runTest { fun `resolve RoomEncrypted`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(AN_EVENT_ID to aNotificationData(content = NotificationContent.MessageLike.RoomEncrypted))
content = NotificationContent.MessageLike.RoomEncrypted
) )
) )
) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
FallbackNotifiableEvent( FallbackNotifiableEvent(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
@ -563,19 +615,22 @@ class DefaultNotifiableEventResolverTest {
timestamp = A_FAKE_TIMESTAMP, timestamp = A_FAKE_TIMESTAMP,
) )
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve CallInvite`() = runTest { fun `resolve CallInvite`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.CallInvite(A_USER_ID_2), content = NotificationContent.MessageLike.CallInvite(A_USER_ID_2),
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
val expectedResult = ResolvedPushEvent.Event( val expectedResult = ResolvedPushEvent.Event(
NotifiableMessageEvent( NotifiableMessageEvent(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
@ -601,7 +656,7 @@ class DefaultNotifiableEventResolverTest {
isUpdated = false isUpdated = false
) )
) )
assertThat(result.getOrNull()).isEqualTo(expectedResult) assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
@ -609,12 +664,14 @@ class DefaultNotifiableEventResolverTest {
val callNotificationEventResolver = FakeCallNotificationEventResolver() val callNotificationEventResolver = FakeCallNotificationEventResolver()
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.CallNotify( content = NotificationContent.MessageLike.CallNotify(
A_USER_ID_2, A_USER_ID_2,
CallNotifyType.NOTIFY CallNotifyType.NOTIFY
), ),
) )
)
), ),
callNotificationEventResolver = callNotificationEventResolver, callNotificationEventResolver = callNotificationEventResolver,
) )
@ -639,15 +696,17 @@ class DefaultNotifiableEventResolverTest {
) )
) )
callNotificationEventResolver.resolveEventLambda = { _, _, _ -> Result.success(expectedResult.notifiableEvent) } callNotificationEventResolver.resolveEventLambda = { _, _, _ -> Result.success(expectedResult.notifiableEvent) }
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
assertThat(result.getOrNull()).isEqualTo(expectedResult) val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve RoomRedaction`() = runTest { fun `resolve RoomRedaction`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.RoomRedaction( content = NotificationContent.MessageLike.RoomRedaction(
AN_EVENT_ID_2, AN_EVENT_ID_2,
A_REDACTION_REASON, A_REDACTION_REASON,
@ -655,21 +714,24 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
)
val expectedResult = ResolvedPushEvent.Redaction( val expectedResult = ResolvedPushEvent.Redaction(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
redactedEventId = AN_EVENT_ID_2, redactedEventId = AN_EVENT_ID_2,
reason = A_REDACTION_REASON, reason = A_REDACTION_REASON,
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
assertThat(result.getOrNull()).isEqualTo(expectedResult) val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult))
} }
@Test @Test
fun `resolve RoomRedaction with null redactedEventId should return null`() = runTest { fun `resolve RoomRedaction with null redactedEventId should return null`() = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(
AN_EVENT_ID to aNotificationData(
content = NotificationContent.MessageLike.RoomRedaction( content = NotificationContent.MessageLike.RoomRedaction(
null, null,
A_REDACTION_REASON, A_REDACTION_REASON,
@ -677,66 +739,73 @@ class DefaultNotifiableEventResolverTest {
) )
) )
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) )
assertThat(result.isFailure).isTrue() val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
assertThat(result.getEvent(request)?.getOrNull()).isNull()
} }
@Test @Test
fun `resolve null cases`() { fun `resolve null cases`() {
testFailure(NotificationContent.MessageLike.CallAnswer) testNoResults(NotificationContent.MessageLike.CallAnswer)
testFailure(NotificationContent.MessageLike.CallHangup) testNoResults(NotificationContent.MessageLike.CallHangup)
testFailure(NotificationContent.MessageLike.CallCandidates) testNoResults(NotificationContent.MessageLike.CallCandidates)
testFailure(NotificationContent.MessageLike.KeyVerificationReady) testNoResults(NotificationContent.MessageLike.KeyVerificationReady)
testFailure(NotificationContent.MessageLike.KeyVerificationStart) testNoResults(NotificationContent.MessageLike.KeyVerificationStart)
testFailure(NotificationContent.MessageLike.KeyVerificationCancel) testNoResults(NotificationContent.MessageLike.KeyVerificationCancel)
testFailure(NotificationContent.MessageLike.KeyVerificationAccept) testNoResults(NotificationContent.MessageLike.KeyVerificationAccept)
testFailure(NotificationContent.MessageLike.KeyVerificationKey) testNoResults(NotificationContent.MessageLike.KeyVerificationKey)
testFailure(NotificationContent.MessageLike.KeyVerificationMac) testNoResults(NotificationContent.MessageLike.KeyVerificationMac)
testFailure(NotificationContent.MessageLike.KeyVerificationDone) testNoResults(NotificationContent.MessageLike.KeyVerificationDone)
testFailure(NotificationContent.MessageLike.ReactionContent(relatedEventId = AN_EVENT_ID_2.value)) testNoResults(NotificationContent.MessageLike.ReactionContent(relatedEventId = AN_EVENT_ID_2.value))
testFailure(NotificationContent.MessageLike.Sticker) testNoResults(NotificationContent.MessageLike.Sticker)
testFailure(NotificationContent.StateEvent.PolicyRuleRoom) testNoResults(NotificationContent.StateEvent.PolicyRuleRoom)
testFailure(NotificationContent.StateEvent.PolicyRuleServer) testNoResults(NotificationContent.StateEvent.PolicyRuleServer)
testFailure(NotificationContent.StateEvent.PolicyRuleUser) testNoResults(NotificationContent.StateEvent.PolicyRuleUser)
testFailure(NotificationContent.StateEvent.RoomAliases) testNoResults(NotificationContent.StateEvent.RoomAliases)
testFailure(NotificationContent.StateEvent.RoomAvatar) testNoResults(NotificationContent.StateEvent.RoomAvatar)
testFailure(NotificationContent.StateEvent.RoomCanonicalAlias) testNoResults(NotificationContent.StateEvent.RoomCanonicalAlias)
testFailure(NotificationContent.StateEvent.RoomCreate) testNoResults(NotificationContent.StateEvent.RoomCreate)
testFailure(NotificationContent.StateEvent.RoomEncryption) testNoResults(NotificationContent.StateEvent.RoomEncryption)
testFailure(NotificationContent.StateEvent.RoomGuestAccess) testNoResults(NotificationContent.StateEvent.RoomGuestAccess)
testFailure(NotificationContent.StateEvent.RoomHistoryVisibility) testNoResults(NotificationContent.StateEvent.RoomHistoryVisibility)
testFailure(NotificationContent.StateEvent.RoomJoinRules) testNoResults(NotificationContent.StateEvent.RoomJoinRules)
testFailure(NotificationContent.StateEvent.RoomName) testNoResults(NotificationContent.StateEvent.RoomName)
testFailure(NotificationContent.StateEvent.RoomPinnedEvents) testNoResults(NotificationContent.StateEvent.RoomPinnedEvents)
testFailure(NotificationContent.StateEvent.RoomPowerLevels) testNoResults(NotificationContent.StateEvent.RoomPowerLevels)
testFailure(NotificationContent.StateEvent.RoomServerAcl) testNoResults(NotificationContent.StateEvent.RoomServerAcl)
testFailure(NotificationContent.StateEvent.RoomThirdPartyInvite) testNoResults(NotificationContent.StateEvent.RoomThirdPartyInvite)
testFailure(NotificationContent.StateEvent.RoomTombstone) testNoResults(NotificationContent.StateEvent.RoomTombstone)
testFailure(NotificationContent.StateEvent.RoomTopic("")) testNoResults(NotificationContent.StateEvent.RoomTopic(""))
testFailure(NotificationContent.StateEvent.SpaceChild) testNoResults(NotificationContent.StateEvent.SpaceChild)
testFailure(NotificationContent.StateEvent.SpaceParent) testNoResults(NotificationContent.StateEvent.SpaceParent)
} }
private fun testFailure(content: NotificationContent) = runTest { private fun testNoResults(content: NotificationContent) = runTest {
val sut = createDefaultNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
aNotificationData( mapOf(AN_EVENT_ID to aNotificationData(content = content))
content = content
) )
) )
) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase")
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) val result = sut.resolveEvents(A_SESSION_ID, listOf(request))
assertThat(result.isFailure).isTrue() assertThat(result.getEvent(request)?.getOrNull()).isNull()
}
private fun Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>.getEvent(
request: NotificationEventRequest
): Result<ResolvedPushEvent>? {
return getOrNull()?.get(request)
} }
private fun createDefaultNotifiableEventResolver( private fun createDefaultNotifiableEventResolver(
notificationService: FakeNotificationService? = FakeNotificationService(), notificationService: FakeNotificationService? = FakeNotificationService(),
notificationResult: Result<NotificationData?> = Result.success(null), notificationResult: Result<Map<EventId, NotificationData>> = Result.success(emptyMap()),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
callNotificationEventResolver: FakeCallNotificationEventResolver = FakeCallNotificationEventResolver(), callNotificationEventResolver: FakeCallNotificationEventResolver = FakeCallNotificationEventResolver(),
): DefaultNotifiableEventResolver { ): DefaultNotifiableEventResolver {
val context = RuntimeEnvironment.getApplication() as Context val context = RuntimeEnvironment.getApplication() as Context
notificationService?.givenGetNotificationResult(notificationResult) notificationService?.givenGetNotificationsResult(notificationResult)
val matrixClientProvider = FakeMatrixClientProvider(getClient = { val matrixClientProvider = FakeMatrixClientProvider(getClient = {
if (notificationService == null) { if (notificationService == null) {
Result.failure(IllegalStateException("Client not found")) Result.failure(IllegalStateException("Client not found"))

View file

@ -42,7 +42,9 @@ class DefaultOnMissedCallNotificationHandlerTest {
// Create a fake matrix client provider that returns a fake matrix client with a fake notification service that returns a valid notification data // Create a fake matrix client provider that returns a fake matrix client with a fake notification service that returns a valid notification data
val matrixClientProvider = FakeMatrixClientProvider(getClient = { val matrixClientProvider = FakeMatrixClientProvider(getClient = {
val notificationService = FakeNotificationService().apply { val notificationService = FakeNotificationService().apply {
givenGetNotificationResult(Result.success(aNotificationData(senderDisplayName = A_USER_NAME, senderIsNameAmbiguous = false))) givenGetNotificationsResult(
Result.success(mapOf(AN_EVENT_ID to aNotificationData(senderDisplayName = A_USER_NAME, senderIsNameAmbiguous = false)))
)
} }
Result.success(FakeMatrixClient(notificationService = notificationService)) Result.success(FakeMatrixClient(notificationService = notificationService))
}) })

View file

@ -7,16 +7,18 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
class FakeNotifiableEventResolver( class FakeNotifiableEventResolver(
private val notifiableEventResult: (SessionId, RoomId, EventId) -> Result<ResolvedPushEvent> = { _, _, _ -> lambdaError() } private val resolveEventsResult: (SessionId, List<NotificationEventRequest>) -> Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>> =
{ _, _ -> lambdaError() }
) : NotifiableEventResolver { ) : NotifiableEventResolver {
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): Result<ResolvedPushEvent> { override suspend fun resolveEvents(
return notifiableEventResult(sessionId, roomId, eventId) sessionId: SessionId,
notificationEventRequests: List<NotificationEventRequest>
): Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>> {
return resolveEventsResult(sessionId, notificationEventRequests)
} }
} }

View file

@ -354,8 +354,8 @@ class NotificationBroadcastReceiverHandlerTest {
) )
) )
} }
val onNotifiableEventReceivedResult = lambdaRecorder<NotifiableEvent, Unit> { _ -> } val onNotifiableEventsReceivedResult = lambdaRecorder<List<NotifiableEvent>, Unit> { _ -> }
val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult) val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceivedResult = onNotifiableEventsReceivedResult)
val sut = createNotificationBroadcastReceiverHandler( val sut = createNotificationBroadcastReceiverHandler(
joinedRoom = joinedRoom, joinedRoom = joinedRoom,
onNotifiableEventReceived = onNotifiableEventReceived, onNotifiableEventReceived = onNotifiableEventReceived,
@ -371,7 +371,7 @@ class NotificationBroadcastReceiverHandlerTest {
sendMessage.assertions() sendMessage.assertions()
.isCalledOnce() .isCalledOnce()
.with(value(A_MESSAGE), value(null), value(emptyList<IntentionalMention>())) .with(value(A_MESSAGE), value(null), value(emptyList<IntentionalMention>()))
onNotifiableEventReceivedResult.assertions() onNotifiableEventsReceivedResult.assertions()
.isCalledOnce() .isCalledOnce()
replyMessage.assertions() replyMessage.assertions()
.isNeverCalled() .isNeverCalled()
@ -421,8 +421,8 @@ class NotificationBroadcastReceiverHandlerTest {
) )
) )
} }
val onNotifiableEventReceivedResult = lambdaRecorder<NotifiableEvent, Unit> { _ -> } val onNotifiableEventsReceivedResult = lambdaRecorder<List<NotifiableEvent>, Unit> { _ -> }
val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceivedResult = onNotifiableEventReceivedResult) val onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceivedResult = onNotifiableEventsReceivedResult)
val sut = createNotificationBroadcastReceiverHandler( val sut = createNotificationBroadcastReceiverHandler(
joinedRoom = joinedRoom, joinedRoom = joinedRoom,
onNotifiableEventReceived = onNotifiableEventReceived, onNotifiableEventReceived = onNotifiableEventReceived,
@ -439,7 +439,7 @@ class NotificationBroadcastReceiverHandlerTest {
runCurrent() runCurrent()
sendMessage.assertions() sendMessage.assertions()
.isNeverCalled() .isNeverCalled()
onNotifiableEventReceivedResult.assertions() onNotifiableEventsReceivedResult.assertions()
.isCalledOnce() .isCalledOnce()
replyMessage.assertions() replyMessage.assertions()
.isCalledOnce() .isCalledOnce()

View file

@ -34,7 +34,7 @@ class DefaultOnRedactedEventReceivedTest {
val sut = createDefaultOnRedactedEventReceived( val sut = createDefaultOnRedactedEventReceived(
getMessageNotificationsForRoomResult = { _, _ -> emptyList() } getMessageNotificationsForRoomResult = { _, _ -> emptyList() }
) )
sut.onRedactedEventReceived(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null)) sut.onRedactedEventsReceived(listOf(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null)))
} }
@Test @Test
@ -48,7 +48,7 @@ class DefaultOnRedactedEventReceivedTest {
) )
} }
) )
sut.onRedactedEventReceived(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null)) sut.onRedactedEventsReceived(listOf(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null)))
} }
private fun TestScope.createDefaultOnRedactedEventReceived( private fun TestScope.createDefaultOnRedactedEventReceived(

View file

@ -31,6 +31,8 @@ import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.push.impl.history.FakePushHistoryService import io.element.android.libraries.push.impl.history.FakePushHistoryService
import io.element.android.libraries.push.impl.history.PushHistoryService import io.element.android.libraries.push.impl.history.PushHistoryService
import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver
import io.element.android.libraries.push.impl.notifications.NotificationEventRequest
import io.element.android.libraries.push.impl.notifications.NotificationResolverQueue
import io.element.android.libraries.push.impl.notifications.ResolvingException import io.element.android.libraries.push.impl.notifications.ResolvingException
import io.element.android.libraries.push.impl.notifications.channels.FakeNotificationChannels import io.element.android.libraries.push.impl.notifications.channels.FakeNotificationChannels
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent
@ -48,11 +50,15 @@ import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.Fa
import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.matching
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import java.time.Instant import java.time.Instant
import kotlin.time.Duration.Companion.milliseconds
private const val A_PUSHER_INFO = "info" private const val A_PUSHER_INFO = "info"
@ -80,10 +86,11 @@ class DefaultPushHandlerTest {
fun `when classical PushData is received, the notification drawer is informed`() = runTest { fun `when classical PushData is received, the notification drawer is informed`() = runTest {
val aNotifiableMessageEvent = aNotifiableMessageEvent() val aNotifiableMessageEvent = aNotifiableMessageEvent()
val notifiableEventResult = val notifiableEventResult =
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent>> { _, _, _ -> lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _, ->
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent)) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))))
} }
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {} val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {} val incrementPushCounterResult = lambdaRecorder<Unit> {}
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> } val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
val pushHistoryService = FakePushHistoryService( val pushHistoryService = FakePushHistoryService(
@ -96,8 +103,8 @@ class DefaultPushHandlerTest {
clientSecret = A_SECRET, clientSecret = A_SECRET,
) )
val defaultPushHandler = createDefaultPushHandler( val defaultPushHandler = createDefaultPushHandler(
onNotifiableEventReceived = onNotifiableEventReceived, onNotifiableEventsReceived = onNotifiableEventsReceived,
notifiableEventResult = notifiableEventResult, notifiableEventsResult = notifiableEventResult,
pushClientSecret = FakePushClientSecret( pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { A_USER_ID } getUserIdFromSecretResult = { A_USER_ID }
), ),
@ -105,14 +112,17 @@ class DefaultPushHandlerTest {
pushHistoryService = pushHistoryService, pushHistoryService = pushHistoryService,
) )
defaultPushHandler.handle(aPushData, A_PUSHER_INFO) defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
advanceTimeBy(300.milliseconds)
incrementPushCounterResult.assertions() incrementPushCounterResult.assertions()
.isCalledOnce() .isCalledOnce()
notifiableEventResult.assertions() notifiableEventResult.assertions()
.isCalledOnce() .isCalledOnce()
.with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID)) .with(value(A_USER_ID), any())
onNotifiableEventReceived.assertions() onNotifiableEventsReceived.assertions()
.isCalledOnce() .isCalledOnce()
.with(value(aNotifiableMessageEvent)) .with(value(listOf(aNotifiableMessageEvent)))
onPushReceivedResult.assertions() onPushReceivedResult.assertions()
.isCalledOnce() .isCalledOnce()
} }
@ -122,10 +132,11 @@ class DefaultPushHandlerTest {
runTest { runTest {
val aNotifiableMessageEvent = aNotifiableMessageEvent() val aNotifiableMessageEvent = aNotifiableMessageEvent()
val notifiableEventResult = val notifiableEventResult =
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent.Event>> { _, _, _ -> lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _ ->
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent)) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))))
} }
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {} val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {} val incrementPushCounterResult = lambdaRecorder<Unit> {}
val aPushData = PushData( val aPushData = PushData(
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
@ -138,8 +149,8 @@ class DefaultPushHandlerTest {
onPushReceivedResult = onPushReceivedResult, onPushReceivedResult = onPushReceivedResult,
) )
val defaultPushHandler = createDefaultPushHandler( val defaultPushHandler = createDefaultPushHandler(
onNotifiableEventReceived = onNotifiableEventReceived, onNotifiableEventsReceived = onNotifiableEventsReceived,
notifiableEventResult = notifiableEventResult, notifiableEventsResult = notifiableEventResult,
pushClientSecret = FakePushClientSecret( pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { A_USER_ID } getUserIdFromSecretResult = { A_USER_ID }
), ),
@ -150,11 +161,14 @@ class DefaultPushHandlerTest {
pushHistoryService = pushHistoryService, pushHistoryService = pushHistoryService,
) )
defaultPushHandler.handle(aPushData, A_PUSHER_INFO) defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
advanceTimeBy(300.milliseconds)
incrementPushCounterResult.assertions() incrementPushCounterResult.assertions()
.isCalledOnce() .isCalledOnce()
notifiableEventResult.assertions() notifiableEventResult.assertions()
.isCalledOnce() .isCalledOnce()
onNotifiableEventReceived.assertions() onNotifiableEventsReceived.assertions()
.isNeverCalled() .isNeverCalled()
onPushReceivedResult.assertions() onPushReceivedResult.assertions()
.isCalledOnce() .isCalledOnce()
@ -165,10 +179,11 @@ class DefaultPushHandlerTest {
runTest { runTest {
val aNotifiableMessageEvent = aNotifiableMessageEvent() val aNotifiableMessageEvent = aNotifiableMessageEvent()
val notifiableEventResult = val notifiableEventResult =
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent.Event>> { _, _, _ -> lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _ ->
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent)) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))))
} }
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {} val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {} val incrementPushCounterResult = lambdaRecorder<Unit> {}
val aPushData = PushData( val aPushData = PushData(
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
@ -181,8 +196,8 @@ class DefaultPushHandlerTest {
onPushReceivedResult = onPushReceivedResult, onPushReceivedResult = onPushReceivedResult,
) )
val defaultPushHandler = createDefaultPushHandler( val defaultPushHandler = createDefaultPushHandler(
onNotifiableEventReceived = onNotifiableEventReceived, onNotifiableEventsReceived = onNotifiableEventsReceived,
notifiableEventResult = notifiableEventResult, notifiableEventsResult = notifiableEventResult,
pushClientSecret = FakePushClientSecret( pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { null } getUserIdFromSecretResult = { null }
), ),
@ -193,14 +208,17 @@ class DefaultPushHandlerTest {
pushHistoryService = pushHistoryService, pushHistoryService = pushHistoryService,
) )
defaultPushHandler.handle(aPushData, A_PUSHER_INFO) defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
advanceTimeBy(300.milliseconds)
incrementPushCounterResult.assertions() incrementPushCounterResult.assertions()
.isCalledOnce() .isCalledOnce()
notifiableEventResult.assertions() notifiableEventResult.assertions()
.isCalledOnce() .isCalledOnce()
.with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID)) .with(value(A_USER_ID), any())
onNotifiableEventReceived.assertions() onNotifiableEventsReceived.assertions()
.isCalledOnce() .isCalledOnce()
.with(value(aNotifiableMessageEvent)) .with(value(listOf(aNotifiableMessageEvent)))
onPushReceivedResult.assertions() onPushReceivedResult.assertions()
.isCalledOnce() .isCalledOnce()
} }
@ -210,10 +228,11 @@ class DefaultPushHandlerTest {
runTest { runTest {
val aNotifiableMessageEvent = aNotifiableMessageEvent() val aNotifiableMessageEvent = aNotifiableMessageEvent()
val notifiableEventResult = val notifiableEventResult =
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent.Event>> { _, _, _ -> lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _ ->
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent)) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))))
} }
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {} val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {} val incrementPushCounterResult = lambdaRecorder<Unit> {}
val aPushData = PushData( val aPushData = PushData(
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
@ -226,8 +245,8 @@ class DefaultPushHandlerTest {
onPushReceivedResult = onPushReceivedResult, onPushReceivedResult = onPushReceivedResult,
) )
val defaultPushHandler = createDefaultPushHandler( val defaultPushHandler = createDefaultPushHandler(
onNotifiableEventReceived = onNotifiableEventReceived, onNotifiableEventsReceived = onNotifiableEventsReceived,
notifiableEventResult = notifiableEventResult, notifiableEventsResult = notifiableEventResult,
pushClientSecret = FakePushClientSecret( pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { null } getUserIdFromSecretResult = { null }
), ),
@ -242,7 +261,7 @@ class DefaultPushHandlerTest {
.isCalledOnce() .isCalledOnce()
notifiableEventResult.assertions() notifiableEventResult.assertions()
.isNeverCalled() .isNeverCalled()
onNotifiableEventReceived.assertions() onNotifiableEventsReceived.assertions()
.isNeverCalled() .isNeverCalled()
onPushReceivedResult.assertions() onPushReceivedResult.assertions()
.isCalledOnce() .isCalledOnce()
@ -252,10 +271,10 @@ class DefaultPushHandlerTest {
fun `when classical PushData is received, but not able to resolve the event, nothing happen`() = fun `when classical PushData is received, but not able to resolve the event, nothing happen`() =
runTest { runTest {
val notifiableEventResult = val notifiableEventResult =
lambdaRecorder<SessionId, RoomId, EventId, Result<ResolvedPushEvent.Event>> { _, _, _ -> lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _ ->
Result.failure(ResolvingException("Unable to resolve")) Result.failure(ResolvingException("Unable to resolve"))
} }
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {} val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {} val incrementPushCounterResult = lambdaRecorder<Unit> {}
val aPushData = PushData( val aPushData = PushData(
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
@ -268,8 +287,8 @@ class DefaultPushHandlerTest {
onPushReceivedResult = onPushReceivedResult, onPushReceivedResult = onPushReceivedResult,
) )
val defaultPushHandler = createDefaultPushHandler( val defaultPushHandler = createDefaultPushHandler(
onNotifiableEventReceived = onNotifiableEventReceived, onNotifiableEventsReceived = onNotifiableEventsReceived,
notifiableEventResult = notifiableEventResult, notifiableEventsResult = notifiableEventResult,
buildMeta = aBuildMeta( buildMeta = aBuildMeta(
// Also test `lowPrivacyLoggingEnabled = false` here // Also test `lowPrivacyLoggingEnabled = false` here
lowPrivacyLoggingEnabled = false lowPrivacyLoggingEnabled = false
@ -281,12 +300,15 @@ class DefaultPushHandlerTest {
pushHistoryService = pushHistoryService, pushHistoryService = pushHistoryService,
) )
defaultPushHandler.handle(aPushData, A_PUSHER_INFO) defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
advanceTimeBy(300.milliseconds)
incrementPushCounterResult.assertions() incrementPushCounterResult.assertions()
.isCalledOnce() .isCalledOnce()
notifiableEventResult.assertions() notifiableEventResult.assertions()
.isCalledOnce() .isCalledOnce()
.with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID)) .with(value(A_USER_ID), any())
onNotifiableEventReceived.assertions() onNotifiableEventsReceived.assertions()
.isNeverCalled() .isNeverCalled()
onPushReceivedResult.assertions() onPushReceivedResult.assertions()
.isCalledOnce() .isCalledOnce()
@ -313,28 +335,38 @@ class DefaultPushHandlerTest {
Unit, Unit,
> { _, _, _, _, _, _, _, _ -> } > { _, _, _, _, _, _, _, _ -> }
val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda) val elementCallEntryPoint = FakeElementCallEntryPoint(handleIncomingCallResult = handleIncomingCallLambda)
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {} val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> } val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
val pushHistoryService = FakePushHistoryService( val pushHistoryService = FakePushHistoryService(
onPushReceivedResult = onPushReceivedResult, onPushReceivedResult = onPushReceivedResult,
) )
val defaultPushHandler = createDefaultPushHandler( val defaultPushHandler = createDefaultPushHandler(
elementCallEntryPoint = elementCallEntryPoint, elementCallEntryPoint = elementCallEntryPoint,
notifiableEventResult = { _, _, _ -> notifiableEventsResult = { _, _ ->
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
Result.success( Result.success(
ResolvedPushEvent.Event(aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli())) mapOf(
request to Result.success(
ResolvedPushEvent.Event(
aNotifiableCallEvent(callNotifyType = CallNotifyType.RING, timestamp = Instant.now().toEpochMilli())
)
)
)
) )
}, },
incrementPushCounterResult = {}, incrementPushCounterResult = {},
pushClientSecret = FakePushClientSecret( pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { A_USER_ID } getUserIdFromSecretResult = { A_USER_ID }
), ),
onNotifiableEventReceived = onNotifiableEventReceived, onNotifiableEventsReceived = onNotifiableEventsReceived,
pushHistoryService = pushHistoryService, pushHistoryService = pushHistoryService,
) )
defaultPushHandler.handle(aPushData, A_PUSHER_INFO) defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
advanceTimeBy(300.milliseconds)
handleIncomingCallLambda.assertions().isCalledOnce() handleIncomingCallLambda.assertions().isCalledOnce()
onNotifiableEventReceived.assertions().isCalledOnce() onNotifiableEventsReceived.assertions().isNeverCalled()
onPushReceivedResult.assertions().isCalledOnce() onPushReceivedResult.assertions().isCalledOnce()
} }
@ -346,7 +378,7 @@ class DefaultPushHandlerTest {
unread = 0, unread = 0,
clientSecret = A_SECRET, clientSecret = A_SECRET,
) )
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {} val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
val handleIncomingCallLambda = lambdaRecorder< val handleIncomingCallLambda = lambdaRecorder<
CallType.RoomCall, CallType.RoomCall,
EventId, EventId,
@ -365,9 +397,10 @@ class DefaultPushHandlerTest {
) )
val defaultPushHandler = createDefaultPushHandler( val defaultPushHandler = createDefaultPushHandler(
elementCallEntryPoint = elementCallEntryPoint, elementCallEntryPoint = elementCallEntryPoint,
onNotifiableEventReceived = onNotifiableEventReceived, onNotifiableEventsReceived = onNotifiableEventsReceived,
notifiableEventResult = { _, _, _ -> notifiableEventsResult = { _, _ ->
Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent(type = EventType.CALL_NOTIFY))) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent(type = EventType.CALL_NOTIFY)))))
}, },
incrementPushCounterResult = {}, incrementPushCounterResult = {},
pushClientSecret = FakePushClientSecret( pushClientSecret = FakePushClientSecret(
@ -377,8 +410,10 @@ class DefaultPushHandlerTest {
) )
defaultPushHandler.handle(aPushData, A_PUSHER_INFO) defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
advanceTimeBy(300.milliseconds)
handleIncomingCallLambda.assertions().isNeverCalled() handleIncomingCallLambda.assertions().isNeverCalled()
onNotifiableEventReceived.assertions().isCalledOnce() onNotifiableEventsReceived.assertions().isCalledOnce()
onPushReceivedResult.assertions().isCalledOnce() onPushReceivedResult.assertions().isCalledOnce()
} }
@ -390,7 +425,7 @@ class DefaultPushHandlerTest {
unread = 0, unread = 0,
clientSecret = A_SECRET, clientSecret = A_SECRET,
) )
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {} val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
val handleIncomingCallLambda = lambdaRecorder< val handleIncomingCallLambda = lambdaRecorder<
CallType.RoomCall, CallType.RoomCall,
EventId, EventId,
@ -409,9 +444,10 @@ class DefaultPushHandlerTest {
) )
val defaultPushHandler = createDefaultPushHandler( val defaultPushHandler = createDefaultPushHandler(
elementCallEntryPoint = elementCallEntryPoint, elementCallEntryPoint = elementCallEntryPoint,
onNotifiableEventReceived = onNotifiableEventReceived, onNotifiableEventsReceived = onNotifiableEventsReceived,
notifiableEventResult = { _, _, _ -> notifiableEventsResult = { _, _ ->
Result.success(ResolvedPushEvent.Event(aNotifiableCallEvent())) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableCallEvent()))))
}, },
incrementPushCounterResult = {}, incrementPushCounterResult = {},
userPushStore = FakeUserPushStore().apply { userPushStore = FakeUserPushStore().apply {
@ -423,8 +459,11 @@ class DefaultPushHandlerTest {
pushHistoryService = pushHistoryService, pushHistoryService = pushHistoryService,
) )
defaultPushHandler.handle(aPushData, A_PUSHER_INFO) defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
advanceTimeBy(300.milliseconds)
handleIncomingCallLambda.assertions().isCalledOnce() handleIncomingCallLambda.assertions().isCalledOnce()
onNotifiableEventReceived.assertions().isCalledOnce() onNotifiableEventsReceived.assertions().isNeverCalled()
onPushReceivedResult.assertions().isCalledOnce() onPushReceivedResult.assertions().isCalledOnce()
} }
@ -442,26 +481,32 @@ class DefaultPushHandlerTest {
redactedEventId = AN_EVENT_ID_2, redactedEventId = AN_EVENT_ID_2,
reason = null reason = null
) )
val onRedactedEventReceived = lambdaRecorder<ResolvedPushEvent.Redaction, Unit> { } val onRedactedEventReceived = lambdaRecorder<List<ResolvedPushEvent.Redaction>, Unit> { }
val incrementPushCounterResult = lambdaRecorder<Unit> {} val incrementPushCounterResult = lambdaRecorder<Unit> {}
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> } val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
val pushHistoryService = FakePushHistoryService( val pushHistoryService = FakePushHistoryService(
onPushReceivedResult = onPushReceivedResult, onPushReceivedResult = onPushReceivedResult,
) )
val defaultPushHandler = createDefaultPushHandler( val defaultPushHandler = createDefaultPushHandler(
onRedactedEventReceived = onRedactedEventReceived, onRedactedEventsReceived = onRedactedEventReceived,
incrementPushCounterResult = incrementPushCounterResult, incrementPushCounterResult = incrementPushCounterResult,
notifiableEventResult = { _, _, _ -> Result.success(aRedaction) }, notifiableEventsResult = { _, _ ->
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
Result.success(mapOf(request to Result.success(aRedaction)))
},
pushClientSecret = FakePushClientSecret( pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { A_USER_ID } getUserIdFromSecretResult = { A_USER_ID }
), ),
pushHistoryService = pushHistoryService, pushHistoryService = pushHistoryService,
) )
defaultPushHandler.handle(aPushData, A_PUSHER_INFO) defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
advanceTimeBy(300.milliseconds)
incrementPushCounterResult.assertions() incrementPushCounterResult.assertions()
.isCalledOnce() .isCalledOnce()
onRedactedEventReceived.assertions().isCalledOnce() onRedactedEventReceived.assertions().isCalledOnce()
.with(value(aRedaction)) .with(value(listOf(aRedaction)))
onPushReceivedResult.assertions() onPushReceivedResult.assertions()
.isCalledOnce() .isCalledOnce()
} }
@ -493,10 +538,64 @@ class DefaultPushHandlerTest {
.isCalledOnce() .isCalledOnce()
} }
private fun createDefaultPushHandler( @Test
onNotifiableEventReceived: (NotifiableEvent) -> Unit = { lambdaError() }, fun `when receiving several push notifications at the same time, those are batched before being processed`() = runTest {
onRedactedEventReceived: (ResolvedPushEvent.Redaction) -> Unit = { lambdaError() }, val aNotifiableMessageEvent = aNotifiableMessageEvent()
notifiableEventResult: (SessionId, RoomId, EventId) -> Result<ResolvedPushEvent> = { _, _, _ -> lambdaError() }, val notifiableEventResult =
lambdaRecorder<SessionId, List<NotificationEventRequest>, Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>> { _, _, ->
val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO)
Result.success(mapOf(request to Result.success(ResolvedPushEvent.Event(aNotifiableMessageEvent))))
}
val onNotifiableEventsReceived = lambdaRecorder<List<NotifiableEvent>, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {}
val onPushReceivedResult = lambdaRecorder<String, EventId?, RoomId?, SessionId?, Boolean, Boolean, String?, Unit> { _, _, _, _, _, _, _ -> }
val pushHistoryService = FakePushHistoryService(
onPushReceivedResult = onPushReceivedResult,
)
val aPushData = PushData(
eventId = AN_EVENT_ID,
roomId = A_ROOM_ID,
unread = 0,
clientSecret = A_SECRET,
)
val anotherPushData = PushData(
eventId = AN_EVENT_ID_2,
roomId = A_ROOM_ID,
unread = 0,
clientSecret = A_SECRET,
)
val defaultPushHandler = createDefaultPushHandler(
onNotifiableEventsReceived = onNotifiableEventsReceived,
notifiableEventsResult = notifiableEventResult,
pushClientSecret = FakePushClientSecret(
getUserIdFromSecretResult = { A_USER_ID }
),
incrementPushCounterResult = incrementPushCounterResult,
pushHistoryService = pushHistoryService,
)
defaultPushHandler.handle(aPushData, A_PUSHER_INFO)
defaultPushHandler.handle(anotherPushData, A_PUSHER_INFO)
advanceTimeBy(300.milliseconds)
incrementPushCounterResult.assertions()
.isCalledExactly(2)
notifiableEventResult.assertions()
.isCalledOnce()
.with(value(A_USER_ID), matching<List<NotificationEventRequest>> { requests ->
requests.size == 2 && requests.first().eventId == AN_EVENT_ID && requests.last().eventId == AN_EVENT_ID_2
})
onNotifiableEventsReceived.assertions()
.isCalledOnce()
onPushReceivedResult.assertions()
.isCalledExactly(2)
}
private fun TestScope.createDefaultPushHandler(
onNotifiableEventsReceived: (List<NotifiableEvent>) -> Unit = { lambdaError() },
onRedactedEventsReceived: (List<ResolvedPushEvent.Redaction>) -> Unit = { lambdaError() },
notifiableEventsResult: (SessionId, List<NotificationEventRequest>) -> Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>> =
{ _, _, -> lambdaError() },
incrementPushCounterResult: () -> Unit = { lambdaError() }, incrementPushCounterResult: () -> Unit = { lambdaError() },
userPushStore: UserPushStore = FakeUserPushStore(), userPushStore: UserPushStore = FakeUserPushStore(),
pushClientSecret: PushClientSecret = FakePushClientSecret(), pushClientSecret: PushClientSecret = FakePushClientSecret(),
@ -508,9 +607,8 @@ class DefaultPushHandlerTest {
pushHistoryService: PushHistoryService = FakePushHistoryService(), pushHistoryService: PushHistoryService = FakePushHistoryService(),
): DefaultPushHandler { ): DefaultPushHandler {
return DefaultPushHandler( return DefaultPushHandler(
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceived), onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceived),
onRedactedEventReceived = FakeOnRedactedEventReceived(onRedactedEventReceived), onRedactedEventReceived = FakeOnRedactedEventReceived(onRedactedEventsReceived),
notifiableEventResolver = FakeNotifiableEventResolver(notifiableEventResult),
incrementPushDataStore = object : IncrementPushDataStore { incrementPushDataStore = object : IncrementPushDataStore {
override suspend fun incrementPushCounter() { override suspend fun incrementPushCounter() {
incrementPushCounterResult() incrementPushCounterResult()
@ -524,6 +622,8 @@ class DefaultPushHandlerTest {
elementCallEntryPoint = elementCallEntryPoint, elementCallEntryPoint = elementCallEntryPoint,
notificationChannels = notificationChannels, notificationChannels = notificationChannels,
pushHistoryService = pushHistoryService, pushHistoryService = pushHistoryService,
resolverQueue = NotificationResolverQueue(notifiableEventResolver = FakeNotifiableEventResolver(notifiableEventsResult), backgroundScope),
appCoroutineScope = backgroundScope,
) )
} }
} }

View file

@ -11,9 +11,9 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
class FakeOnNotifiableEventReceived( class FakeOnNotifiableEventReceived(
private val onNotifiableEventReceivedResult: (NotifiableEvent) -> Unit = { lambdaError() }, private val onNotifiableEventsReceivedResult: (List<NotifiableEvent>) -> Unit = { lambdaError() },
) : OnNotifiableEventReceived { ) : OnNotifiableEventReceived {
override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { override fun onNotifiableEventsReceived(notifiableEvents: List<NotifiableEvent>) {
onNotifiableEventReceivedResult(notifiableEvent) onNotifiableEventsReceivedResult(notifiableEvents)
} }
} }

View file

@ -11,9 +11,9 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
class FakeOnRedactedEventReceived( class FakeOnRedactedEventReceived(
private val onRedactedEventReceivedResult: (ResolvedPushEvent.Redaction) -> Unit = { lambdaError() }, private val onRedactedEventsReceivedResult: (List<ResolvedPushEvent.Redaction>) -> Unit = { lambdaError() },
) : OnRedactedEventReceived { ) : OnRedactedEventReceived {
override fun onRedactedEventReceived(redaction: ResolvedPushEvent.Redaction) { override fun onRedactedEventsReceived(redactions: List<ResolvedPushEvent.Redaction>) {
onRedactedEventReceivedResult(redaction) onRedactedEventsReceivedResult(redactions)
} }
} }

View file

@ -76,7 +76,7 @@ class SyncOnNotifiableEventTest {
fun `when feature flag is disabled, nothing happens`() = runTest { fun `when feature flag is disabled, nothing happens`() = runTest {
val sut = createSyncOnNotifiableEvent(client = client, isSyncOnPushEnabled = false) val sut = createSyncOnNotifiableEvent(client = client, isSyncOnPushEnabled = false)
sut(notifiableEvent) sut(listOf(notifiableEvent))
assert(startSyncLambda).isNeverCalled() assert(startSyncLambda).isNeverCalled()
assert(stopSyncLambda).isNeverCalled() assert(stopSyncLambda).isNeverCalled()
@ -97,7 +97,7 @@ class SyncOnNotifiableEventTest {
unlocked.set(true) unlocked.set(true)
room.givenRoomInfo(aRoomInfo(hasRoomCall = true)) room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
} }
sut(incomingCallNotifiableEvent) sut(listOf(incomingCallNotifiableEvent))
// The process was completed before the timeout // The process was completed before the timeout
assertThat(unlocked.get()).isTrue() assertThat(unlocked.get()).isTrue()
@ -117,30 +117,12 @@ class SyncOnNotifiableEventTest {
unlocked.set(true) unlocked.set(true)
room.givenRoomInfo(aRoomInfo(hasRoomCall = true)) room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
} }
sut(incomingCallNotifiableEvent) sut(listOf(incomingCallNotifiableEvent))
// Didn't unlock before the timeout // Didn't unlock before the timeout
assertThat(unlocked.get()).isFalse() assertThat(unlocked.get()).isFalse()
} }
@Test
fun `when feature flag is enabled and app is in foreground, sync is not started`() = runTest {
val appForegroundStateService = FakeAppForegroundStateService(
initialForegroundValue = true,
)
val sut = createSyncOnNotifiableEvent(client = client, appForegroundStateService = appForegroundStateService, isSyncOnPushEnabled = true)
appForegroundStateService.isSyncingNotificationEvent.test {
sut(notifiableEvent)
sut(incomingCallNotifiableEvent)
// It's initially false
assertThat(awaitItem()).isFalse()
// It never becomes true
ensureAllEventsConsumed()
}
}
@Test @Test
fun `when feature flag is enabled and app is in background, sync is started and stopped`() = runTest { fun `when feature flag is enabled and app is in background, sync is started and stopped`() = runTest {
val appForegroundStateService = FakeAppForegroundStateService( val appForegroundStateService = FakeAppForegroundStateService(
@ -154,7 +136,7 @@ class SyncOnNotifiableEventTest {
appForegroundStateService.isSyncingNotificationEvent.test { appForegroundStateService.isSyncingNotificationEvent.test {
syncService.emitSyncState(SyncState.Running) syncService.emitSyncState(SyncState.Running)
sut(notifiableEvent) sut(listOf(notifiableEvent))
// It's initially false // It's initially false
assertThat(awaitItem()).isFalse() assertThat(awaitItem()).isFalse()
@ -175,8 +157,8 @@ class SyncOnNotifiableEventTest {
val sut = createSyncOnNotifiableEvent(client = client, appForegroundStateService = appForegroundStateService, isSyncOnPushEnabled = true) val sut = createSyncOnNotifiableEvent(client = client, appForegroundStateService = appForegroundStateService, isSyncOnPushEnabled = true)
appForegroundStateService.isSyncingNotificationEvent.test { appForegroundStateService.isSyncingNotificationEvent.test {
launch { sut(notifiableEvent) } launch { sut(listOf(notifiableEvent)) }
launch { sut(notifiableEvent) } launch { sut(listOf(notifiableEvent)) }
launch { launch {
delay(1) delay(1)
timelineItems.emit( timelineItems.emit(

View file

@ -24,6 +24,18 @@ fun <T> value(expectedValue: T) = object : ParameterMatcher {
override fun toString(): String = "value($expectedValue)" override fun toString(): String = "value($expectedValue)"
} }
/**
* A matcher that matches a value based on a condition.
* Can be used to assert that a lambda has been called with a value that satisfies a specific condition.
*/
fun <T> matching(check: (T) -> Boolean) = object : ParameterMatcher {
override fun match(param: Any?): Boolean {
@Suppress("UNCHECKED_CAST")
return (param as? T)?.let { check(it) } ?: false
}
override fun toString(): String = "matching(condition)"
}
/** /**
* A matcher that matches any value. * A matcher that matches any value.
* Can be used when we don't care about the value of a parameter. * Can be used when we don't care about the value of a parameter.