From 8117899a421d57492e1fadbecbce475dffcb656f Mon Sep 17 00:00:00 2001 From: Nan Date: Fri, 28 Jun 2024 11:14:13 -0700 Subject: [PATCH 1/3] Allow `preventDefault` to be fired up to two times * The preventDefault(discard) method can now be called up to two times with false and then true. * This is necessary for the `NotificationWillDisplayEvent` for wrappers which may need to call this twice, once to capture the event internally and once from the developer's intent. * For consistency, this ability is added to the `NotificationReceivedEvent` as well. --- .../INotificationReceivedEvent.kt | 3 +++ .../INotificationWillDisplayEvent.kt | 3 +++ .../notifications/internal/Notification.kt | 10 +++++++--- .../internal/NotificationReceivedEvent.kt | 9 ++++++++- .../internal/NotificationWillDisplayEvent.kt | 9 ++++++++- .../impl/NotificationGenerationProcessor.kt | 18 ++++++++---------- 6 files changed, 37 insertions(+), 15 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/notifications/INotificationReceivedEvent.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/notifications/INotificationReceivedEvent.kt index 5a16293206..f415bbf7aa 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/notifications/INotificationReceivedEvent.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/notifications/INotificationReceivedEvent.kt @@ -53,6 +53,9 @@ interface INotificationReceivedEvent { /** * Call this to prevent OneSignal from displaying the notification automatically. + * This method can be called up to two times with false and then true, if processing time is needed. + * Typically this is only possible within a short + * time-frame (~30 seconds) after the notification is received on the device. * @param discard an [preventDefault] set to true to dismiss the notification with no * possibility of displaying it in the future. */ diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/notifications/INotificationWillDisplayEvent.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/notifications/INotificationWillDisplayEvent.kt index f6b3d9e1be..c431dda2a3 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/notifications/INotificationWillDisplayEvent.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/notifications/INotificationWillDisplayEvent.kt @@ -41,6 +41,9 @@ interface INotificationWillDisplayEvent { /** * Call this to prevent OneSignal from displaying the notification automatically. + * This method can be called up to two times with false and then true, if processing time is needed. + * Typically this is only possible within a short + * time-frame (~30 seconds) after the notification is received on the device. * @param discard an [preventDefault] set to true to dismiss the notification with no * possibility of displaying it in the future. */ diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/Notification.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/Notification.kt index b8e41c1bff..182d4f9e54 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/Notification.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/Notification.kt @@ -3,7 +3,7 @@ package com.onesignal.notifications.internal import androidx.core.app.NotificationCompat import com.onesignal.common.safeJSONObject import com.onesignal.common.safeString -import com.onesignal.common.threading.Waiter +import com.onesignal.common.threading.WaiterWithValue import com.onesignal.core.internal.time.ITime import com.onesignal.debug.internal.logging.Logging import com.onesignal.notifications.BackgroundImageLayout @@ -24,7 +24,11 @@ import org.json.JSONObject */ class Notification : IDisplayableMutableNotification { var notificationExtender: NotificationCompat.Extender? = null - val displayWaiter: Waiter = Waiter() + + /** + * Wake with true to display the notification, or false to discard it permanently. + */ + val displayWaiter = WaiterWithValue() override var groupedNotifications: List? = null override var androidNotificationId = 0 @@ -251,7 +255,7 @@ class Notification : IDisplayableMutableNotification { } override fun display() { - displayWaiter.wake() + displayWaiter.wake(true) } /** diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/NotificationReceivedEvent.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/NotificationReceivedEvent.kt index 15c3e70558..9bf6cbf709 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/NotificationReceivedEvent.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/NotificationReceivedEvent.kt @@ -16,7 +16,14 @@ internal class NotificationReceivedEvent( } override fun preventDefault(discard: Boolean) { - Logging.debug("NotificationReceivedEvent.preventDefault()") + Logging.debug("NotificationReceivedEvent.preventDefault($discard)") + + // If preventDefault(false) has already been called and it is now called again with + // preventDefault(true), the waiter is fired to discard this notification. + // This is necessary for wrapper SDKs that can call preventDefault(discard) twice. + if (isPreventDefault && discard) { + notification.displayWaiter.wake(false) + } isPreventDefault = true this.discard = discard } diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/NotificationWillDisplayEvent.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/NotificationWillDisplayEvent.kt index 8ccaa2834d..375660f047 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/NotificationWillDisplayEvent.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/NotificationWillDisplayEvent.kt @@ -14,7 +14,14 @@ internal class NotificationWillDisplayEvent( } override fun preventDefault(discard: Boolean) { - Logging.debug("NotificationWillDisplayEvent.preventDefault()") + Logging.debug("NotificationWillDisplayEvent.preventDefault($discard)") + + // If preventDefault(false) has already been called and it is now called again with + // preventDefault(true), the waiter is fired to discard this notification. + // This is necessary for wrapper SDKs that can call preventDefault(discard) twice. + if (isPreventDefault && discard) { + notification.displayWaiter.wake(false) + } isPreventDefault = true this.discard = discard } diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/generation/impl/NotificationGenerationProcessor.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/generation/impl/NotificationGenerationProcessor.kt index 70d64b850e..3671abda70 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/generation/impl/NotificationGenerationProcessor.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/generation/impl/NotificationGenerationProcessor.kt @@ -76,12 +76,11 @@ internal class NotificationGenerationProcessor( if (notificationReceivedEvent.discard) { wantsToDisplay = false } else if (notificationReceivedEvent.isPreventDefault) { - // wait on display waiter. If the caller calls `display` on the notification, - // we will exit `waitForWake` and set `wantsToDisplay` to true. If the callback - // never calls `display` we will timeout and never set `wantsToDisplay` to true. wantsToDisplay = false - notification.displayWaiter.waitForWake() - wantsToDisplay = true + // wait on display waiter. If the caller calls `display` or `preventDefault(true)` on the notification, + // we will exit `waitForWake` and set `wantsToDisplay` to true or false respectively. If the callback + // never calls `display` or `preventDefault(true)`, we will timeout and never update `wantsToDisplay`. + wantsToDisplay = notification.displayWaiter.waitForWake() } }.join() } @@ -110,12 +109,11 @@ internal class NotificationGenerationProcessor( if (notificationWillDisplayEvent.discard) { wantsToDisplay = false } else if (notificationWillDisplayEvent.isPreventDefault) { - // wait on display waiter. If the caller calls `display` on the notification, - // we will exit `waitForWake` and set `wantsToDisplay` to true. If the callback - // never calls `display` we will timeout and never set `wantsToDisplay` to true. wantsToDisplay = false - notification.displayWaiter.waitForWake() - wantsToDisplay = true + // wait on display waiter. If the caller calls `display` or `preventDefault(true)` on the notification, + // we will exit `waitForWake` and set `wantsToDisplay` to true or false respectively. If the callback + // never calls `display` or `preventDefault(true)`, we will timeout and never update `wantsToDisplay`. + wantsToDisplay = notification.displayWaiter.waitForWake() } }.join() } From cf45cfe71c1c89d683d11342aeb1e046bf31a982 Mon Sep 17 00:00:00 2001 From: Nan Date: Mon, 1 Jul 2024 13:11:06 -0700 Subject: [PATCH 2/3] [tests] extract duplicated code in NotificationGenerationProcessorTests * There is a lot of duplicated setup code, so let's extract common ones into a helper class * There are no logic changes in this commit. --- .../NotificationGenerationProcessorTests.kt | 398 +++++------------- 1 file changed, 99 insertions(+), 299 deletions(-) diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/generation/NotificationGenerationProcessorTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/generation/NotificationGenerationProcessorTests.kt index b19a7111bf..0ddae6476c 100644 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/generation/NotificationGenerationProcessorTests.kt +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/generation/NotificationGenerationProcessorTests.kt @@ -13,7 +13,6 @@ import com.onesignal.notifications.internal.data.INotificationRepository import com.onesignal.notifications.internal.display.INotificationDisplayer import com.onesignal.notifications.internal.generation.impl.NotificationGenerationProcessor import com.onesignal.notifications.internal.lifecycle.INotificationLifecycleService -import com.onesignal.notifications.internal.summary.INotificationSummaryManager import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.mockk.coEvery @@ -27,6 +26,67 @@ import kotlinx.coroutines.withTimeout import org.json.JSONObject import org.robolectric.annotation.Config +// Mocks used by every test in this file +private class Mocks { + val notificationDisplayer = mockk() + + val applicationService = + run { + val mockApplicationService = AndroidMockHelper.applicationService() + every { mockApplicationService.isInForeground } returns true + mockApplicationService + } + + val notificationLifecycleService: INotificationLifecycleService = + run { + val mockNotificationLifecycleService = mockk() + coEvery { mockNotificationLifecycleService.canReceiveNotification(any()) } returns true + coEvery { mockNotificationLifecycleService.notificationReceived(any()) } just runs + mockNotificationLifecycleService + } + + val notificationRepository: INotificationRepository = + run { + val mockNotificationRepository = mockk() + coEvery { mockNotificationRepository.doesNotificationExist(any()) } returns false + coEvery { + mockNotificationRepository.createNotification( + any(), + any(), + any(), + any(), + any(), + any(), + any(), + any(), + any(), + any(), + ) + } just runs + mockNotificationRepository + } + + val notificationGenerationProcessor = NotificationGenerationProcessor( + applicationService, + notificationDisplayer, + MockHelper.configModelStore(), + notificationRepository, + mockk(), + notificationLifecycleService, + MockHelper.time(1111), + ) + + val notificationPayload: JSONObject = + JSONObject() + .put("alert", "test message") + .put("title", "test title") + .put( + "custom", + JSONObject() + .put("i", "UUID1"), + ) +} + @Config( packageName = "com.onesignal.example", sdk = [26], @@ -40,61 +100,17 @@ class NotificationGenerationProcessorTests : FunSpec({ test("processNotificationData should set title correctly") { // Given val context = ApplicationProvider.getApplicationContext() - val mockTime = MockHelper.time(1111) - val mockApplicationService = AndroidMockHelper.applicationService() - every { mockApplicationService.isInForeground } returns true - val mockNotificationDisplayer = mockk() - coEvery { mockNotificationDisplayer.displayNotification(any()) } returns true - val mockNotificationRepository = mockk() - coEvery { mockNotificationRepository.doesNotificationExist(any()) } returns false - coEvery { - mockNotificationRepository.createNotification( - any(), - any(), - any(), - any(), - any(), - any(), - any(), - any(), - any(), - any(), - ) - } just runs - val mockNotificationSummaryManager = mockk() - val mockNotificationLifecycleService = mockk() - coEvery { mockNotificationLifecycleService.canReceiveNotification(any()) } returns true - coEvery { mockNotificationLifecycleService.notificationReceived(any()) } just runs - coEvery { mockNotificationLifecycleService.externalRemoteNotificationReceived(any()) } just runs - coEvery { mockNotificationLifecycleService.externalNotificationWillShowInForeground(any()) } just runs - - val notificationGenerationProcessor = - NotificationGenerationProcessor( - mockApplicationService, - mockNotificationDisplayer, - MockHelper.configModelStore(), - mockNotificationRepository, - mockNotificationSummaryManager, - mockNotificationLifecycleService, - mockTime, - ) - - val payload = - JSONObject() - .put("alert", "test message") - .put("title", "test title") - .put( - "custom", - JSONObject() - .put("i", "UUID1"), - ) + val mocks = Mocks() + coEvery { mocks.notificationDisplayer.displayNotification(any()) } returns true + coEvery { mocks.notificationLifecycleService.externalRemoteNotificationReceived(any()) } just runs + coEvery { mocks.notificationLifecycleService.externalNotificationWillShowInForeground(any()) } just runs // When - notificationGenerationProcessor.processNotificationData(context, 1, payload, false, 1111) + mocks.notificationGenerationProcessor.processNotificationData(context, 1, mocks.notificationPayload, false, 1111) // Then coVerify(exactly = 1) { - mockNotificationDisplayer.displayNotification( + mocks.notificationDisplayer.displayNotification( withArg { it.androidId shouldBe 1 it.apiNotificationId shouldBe "UUID1" @@ -106,54 +122,24 @@ class NotificationGenerationProcessorTests : FunSpec({ ) } coVerify(exactly = 1) { - mockNotificationRepository.createNotification("UUID1", null, null, any(), false, 1, "test title", "test message", any(), any()) + mocks.notificationRepository.createNotification("UUID1", null, null, any(), false, 1, "test title", "test message", any(), any()) } } test("processNotificationData should restore notification correctly") { // Given val context = ApplicationProvider.getApplicationContext() - val mockTime = MockHelper.time(1111) - val mockApplicationService = AndroidMockHelper.applicationService() - every { mockApplicationService.isInForeground } returns true - val mockNotificationDisplayer = mockk() - coEvery { mockNotificationDisplayer.displayNotification(any()) } returns true - val mockNotificationRepository = mockk() - coEvery { mockNotificationRepository.doesNotificationExist(any()) } returns false - val mockNotificationSummaryManager = mockk() - val mockNotificationLifecycleService = mockk() - coEvery { mockNotificationLifecycleService.canReceiveNotification(any()) } returns true - coEvery { mockNotificationLifecycleService.notificationReceived(any()) } just runs - coEvery { mockNotificationLifecycleService.externalRemoteNotificationReceived(any()) } just runs - coEvery { mockNotificationLifecycleService.externalNotificationWillShowInForeground(any()) } just runs - - val notificationGenerationProcessor = - NotificationGenerationProcessor( - mockApplicationService, - mockNotificationDisplayer, - MockHelper.configModelStore(), - mockNotificationRepository, - mockNotificationSummaryManager, - mockNotificationLifecycleService, - mockTime, - ) - - val payload = - JSONObject() - .put("alert", "test message") - .put("title", "test title") - .put( - "custom", - JSONObject() - .put("i", "UUID1"), - ) + val mocks = Mocks() + coEvery { mocks.notificationDisplayer.displayNotification(any()) } returns true + coEvery { mocks.notificationLifecycleService.externalRemoteNotificationReceived(any()) } just runs + coEvery { mocks.notificationLifecycleService.externalNotificationWillShowInForeground(any()) } just runs // When - notificationGenerationProcessor.processNotificationData(context, 1, payload, true, 1111) + mocks.notificationGenerationProcessor.processNotificationData(context, 1, mocks.notificationPayload, true, 1111) // Then coVerify(exactly = 1) { - mockNotificationDisplayer.displayNotification( + mocks.notificationDisplayer.displayNotification( withArg { it.androidId shouldBe 1 it.apiNotificationId shouldBe "UUID1" @@ -169,45 +155,14 @@ class NotificationGenerationProcessorTests : FunSpec({ test("processNotificationData should not display notification when external callback indicates not to") { // Given val context = ApplicationProvider.getApplicationContext() - val mockTime = MockHelper.time(1111) - val mockApplicationService = AndroidMockHelper.applicationService() - every { mockApplicationService.isInForeground } returns true - val mockNotificationDisplayer = mockk() - val mockNotificationRepository = mockk() - coEvery { mockNotificationRepository.doesNotificationExist(any()) } returns false - coEvery { mockNotificationRepository.createNotification(any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } just runs - val mockNotificationSummaryManager = mockk() - val mockNotificationLifecycleService = mockk() - coEvery { mockNotificationLifecycleService.canReceiveNotification(any()) } returns true - coEvery { mockNotificationLifecycleService.notificationReceived(any()) } just runs - coEvery { mockNotificationLifecycleService.externalRemoteNotificationReceived(any()) } answers { + val mocks = Mocks() + coEvery { mocks.notificationLifecycleService.externalRemoteNotificationReceived(any()) } answers { val receivedEvent = firstArg() receivedEvent.preventDefault() } - val notificationGenerationProcessor = - NotificationGenerationProcessor( - mockApplicationService, - mockNotificationDisplayer, - MockHelper.configModelStore(), - mockNotificationRepository, - mockNotificationSummaryManager, - mockNotificationLifecycleService, - mockTime, - ) - - val payload = - JSONObject() - .put("alert", "test message") - .put("title", "test title") - .put( - "custom", - JSONObject() - .put("i", "UUID1"), - ) - // When - notificationGenerationProcessor.processNotificationData(context, 1, payload, false, 1111) + mocks.notificationGenerationProcessor.processNotificationData(context, 1, mocks.notificationPayload, false, 1111) // Then } @@ -215,49 +170,18 @@ class NotificationGenerationProcessorTests : FunSpec({ test("processNotificationData should display notification when external callback takes longer than 30 seconds") { // Given val context = ApplicationProvider.getApplicationContext() - val mockTime = MockHelper.time(1111) - val mockApplicationService = AndroidMockHelper.applicationService() - every { mockApplicationService.isInForeground } returns true - val mockNotificationDisplayer = mockk() - coEvery { mockNotificationDisplayer.displayNotification(any()) } returns true - val mockNotificationRepository = mockk() - coEvery { mockNotificationRepository.doesNotificationExist(any()) } returns false - coEvery { mockNotificationRepository.createNotification(any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } just runs - val mockNotificationSummaryManager = mockk() - val mockNotificationLifecycleService = mockk() - coEvery { mockNotificationLifecycleService.canReceiveNotification(any()) } returns true - coEvery { mockNotificationLifecycleService.notificationReceived(any()) } just runs - coEvery { mockNotificationLifecycleService.externalRemoteNotificationReceived(any()) } coAnswers { + val mocks = Mocks() + coEvery { mocks.notificationDisplayer.displayNotification(any()) } returns true + coEvery { mocks.notificationLifecycleService.externalRemoteNotificationReceived(any()) } coAnswers { delay(40000) } - val notificationGenerationProcessor = - NotificationGenerationProcessor( - mockApplicationService, - mockNotificationDisplayer, - MockHelper.configModelStore(), - mockNotificationRepository, - mockNotificationSummaryManager, - mockNotificationLifecycleService, - mockTime, - ) - - val payload = - JSONObject() - .put("alert", "test message") - .put("title", "test title") - .put( - "custom", - JSONObject() - .put("i", "UUID1"), - ) - // When - notificationGenerationProcessor.processNotificationData(context, 1, payload, true, 1111) + mocks.notificationGenerationProcessor.processNotificationData(context, 1, mocks.notificationPayload, true, 1111) // Then coVerify(exactly = 1) { - mockNotificationDisplayer.displayNotification( + mocks.notificationDisplayer.displayNotification( withArg { it.androidId shouldBe 1 it.apiNotificationId shouldBe "UUID1" @@ -273,46 +197,15 @@ class NotificationGenerationProcessorTests : FunSpec({ test("processNotificationData should not display notification when foreground callback indicates not to") { // Given val context = ApplicationProvider.getApplicationContext() - val mockTime = MockHelper.time(1111) - val mockApplicationService = AndroidMockHelper.applicationService() - every { mockApplicationService.isInForeground } returns true - val mockNotificationDisplayer = mockk() - val mockNotificationRepository = mockk() - coEvery { mockNotificationRepository.doesNotificationExist(any()) } returns false - coEvery { mockNotificationRepository.createNotification(any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } just runs - val mockNotificationSummaryManager = mockk() - val mockNotificationLifecycleService = mockk() - coEvery { mockNotificationLifecycleService.canReceiveNotification(any()) } returns true - coEvery { mockNotificationLifecycleService.notificationReceived(any()) } just runs - coEvery { mockNotificationLifecycleService.externalRemoteNotificationReceived(any()) } just runs - coEvery { mockNotificationLifecycleService.externalNotificationWillShowInForeground(any()) } answers { + val mocks = Mocks() + coEvery { mocks.notificationLifecycleService.externalRemoteNotificationReceived(any()) } just runs + coEvery { mocks.notificationLifecycleService.externalNotificationWillShowInForeground(any()) } answers { val receivedEvent = firstArg() receivedEvent.preventDefault() } - val notificationGenerationProcessor = - NotificationGenerationProcessor( - mockApplicationService, - mockNotificationDisplayer, - MockHelper.configModelStore(), - mockNotificationRepository, - mockNotificationSummaryManager, - mockNotificationLifecycleService, - mockTime, - ) - - val payload = - JSONObject() - .put("alert", "test message") - .put("title", "test title") - .put( - "custom", - JSONObject() - .put("i", "UUID1"), - ) - // When - notificationGenerationProcessor.processNotificationData(context, 1, payload, false, 1111) + mocks.notificationGenerationProcessor.processNotificationData(context, 1, mocks.notificationPayload, false, 1111) // Then } @@ -320,49 +213,18 @@ class NotificationGenerationProcessorTests : FunSpec({ test("processNotificationData should display notification when foreground callback takes longer than 30 seconds") { // Given val context = ApplicationProvider.getApplicationContext() - val mockTime = MockHelper.time(1111) - val mockApplicationService = AndroidMockHelper.applicationService() - every { mockApplicationService.isInForeground } returns true - val mockNotificationDisplayer = mockk() - coEvery { mockNotificationDisplayer.displayNotification(any()) } returns true - val mockNotificationRepository = mockk() - coEvery { mockNotificationRepository.doesNotificationExist(any()) } returns false - coEvery { mockNotificationRepository.createNotification(any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } just runs - val mockNotificationSummaryManager = mockk() - val mockNotificationLifecycleService = mockk() - coEvery { mockNotificationLifecycleService.canReceiveNotification(any()) } returns true - coEvery { mockNotificationLifecycleService.notificationReceived(any()) } just runs - coEvery { mockNotificationLifecycleService.externalNotificationWillShowInForeground(any()) } coAnswers { + val mocks = Mocks() + coEvery { mocks.notificationDisplayer.displayNotification(any()) } returns true + coEvery { mocks.notificationLifecycleService.externalNotificationWillShowInForeground(any()) } coAnswers { delay(40000) } - val notificationGenerationProcessor = - NotificationGenerationProcessor( - mockApplicationService, - mockNotificationDisplayer, - MockHelper.configModelStore(), - mockNotificationRepository, - mockNotificationSummaryManager, - mockNotificationLifecycleService, - mockTime, - ) - - val payload = - JSONObject() - .put("alert", "test message") - .put("title", "test title") - .put( - "custom", - JSONObject() - .put("i", "UUID1"), - ) - // When - notificationGenerationProcessor.processNotificationData(context, 1, payload, true, 1111) + mocks.notificationGenerationProcessor.processNotificationData(context, 1, mocks.notificationPayload, true, 1111) // Then coVerify(exactly = 1) { - mockNotificationDisplayer.displayNotification( + mocks.notificationDisplayer.displayNotification( withArg { it.androidId shouldBe 1 it.apiNotificationId shouldBe "UUID1" @@ -378,95 +240,33 @@ class NotificationGenerationProcessorTests : FunSpec({ test("processNotificationData should immediately drop the notification when will display callback indicates to") { // Given val context = ApplicationProvider.getApplicationContext() - val mockTime = MockHelper.time(1111) - val mockApplicationService = AndroidMockHelper.applicationService() - every { mockApplicationService.isInForeground } returns true - val mockNotificationDisplayer = mockk() - val mockNotificationRepository = mockk() - coEvery { mockNotificationRepository.doesNotificationExist(any()) } returns false - coEvery { mockNotificationRepository.createNotification(any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } just runs - val mockNotificationSummaryManager = mockk() - val mockNotificationLifecycleService = mockk() - coEvery { mockNotificationLifecycleService.canReceiveNotification(any()) } returns true - coEvery { mockNotificationLifecycleService.notificationReceived(any()) } just runs - coEvery { mockNotificationLifecycleService.externalRemoteNotificationReceived(any()) } just runs - coEvery { mockNotificationLifecycleService.externalNotificationWillShowInForeground(any()) } answers { + val mocks = Mocks() + coEvery { mocks.notificationLifecycleService.externalRemoteNotificationReceived(any()) } just runs + coEvery { mocks.notificationLifecycleService.externalNotificationWillShowInForeground(any()) } answers { val willDisplayEvent = firstArg() // Setting discard parameter to true indicating we should immediately discard willDisplayEvent.preventDefault(true) } - val notificationGenerationProcessor = - NotificationGenerationProcessor( - mockApplicationService, - mockNotificationDisplayer, - MockHelper.configModelStore(), - mockNotificationRepository, - mockNotificationSummaryManager, - mockNotificationLifecycleService, - mockTime, - ) - - val payload = - JSONObject() - .put("alert", "test message") - .put("title", "test title") - .put( - "custom", - JSONObject() - .put("i", "UUID1"), - ) - // If discard is set to false this should timeout waiting for display() withTimeout(1_000) { - notificationGenerationProcessor.processNotificationData(context, 1, payload, false, 1111) + mocks.notificationGenerationProcessor.processNotificationData(context, 1, mocks.notificationPayload, false, 1111) } } test("processNotificationData should immediately drop the notification when received event callback indicates to") { // Given val context = ApplicationProvider.getApplicationContext() - val mockTime = MockHelper.time(1111) - val mockApplicationService = AndroidMockHelper.applicationService() - every { mockApplicationService.isInForeground } returns true - val mockNotificationDisplayer = mockk() - val mockNotificationRepository = mockk() - coEvery { mockNotificationRepository.doesNotificationExist(any()) } returns false - coEvery { mockNotificationRepository.createNotification(any(), any(), any(), any(), any(), any(), any(), any(), any(), any()) } just runs - val mockNotificationSummaryManager = mockk() - val mockNotificationLifecycleService = mockk() - coEvery { mockNotificationLifecycleService.canReceiveNotification(any()) } returns true - coEvery { mockNotificationLifecycleService.notificationReceived(any()) } just runs - coEvery { mockNotificationLifecycleService.externalRemoteNotificationReceived(any()) } answers { + val mocks = Mocks() + coEvery { mocks.notificationLifecycleService.externalRemoteNotificationReceived(any()) } answers { val receivedEvent = firstArg() receivedEvent.preventDefault(true) } - coEvery { mockNotificationLifecycleService.externalNotificationWillShowInForeground(any()) } just runs - - val notificationGenerationProcessor = - NotificationGenerationProcessor( - mockApplicationService, - mockNotificationDisplayer, - MockHelper.configModelStore(), - mockNotificationRepository, - mockNotificationSummaryManager, - mockNotificationLifecycleService, - mockTime, - ) - - val payload = - JSONObject() - .put("alert", "test message") - .put("title", "test title") - .put( - "custom", - JSONObject() - .put("i", "UUID1"), - ) + coEvery { mocks.notificationLifecycleService.externalNotificationWillShowInForeground(any()) } just runs // If discard is set to false this should timeout waiting for display() withTimeout(1_000) { - notificationGenerationProcessor.processNotificationData(context, 1, payload, false, 1111) + mocks.notificationGenerationProcessor.processNotificationData(context, 1, mocks.notificationPayload, false, 1111) } } }) From 9b7b91d0fb50e50b77bb4c15c76b1212dc911cc7 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 9 Jul 2024 11:30:29 -0700 Subject: [PATCH 3/3] Add tests for calling preventDefault twice * The tests ensure calling preventDefault twice work when called in different threads * And a followup call to display does not display the notification --- .../NotificationGenerationProcessorTests.kt | 72 ++++++++++++++++--- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/generation/NotificationGenerationProcessorTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/generation/NotificationGenerationProcessorTests.kt index 0ddae6476c..0731b597c6 100644 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/generation/NotificationGenerationProcessorTests.kt +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/generation/NotificationGenerationProcessorTests.kt @@ -21,7 +21,9 @@ import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.runs +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import org.json.JSONObject import org.robolectric.annotation.Config @@ -66,15 +68,16 @@ private class Mocks { mockNotificationRepository } - val notificationGenerationProcessor = NotificationGenerationProcessor( - applicationService, - notificationDisplayer, - MockHelper.configModelStore(), - notificationRepository, - mockk(), - notificationLifecycleService, - MockHelper.time(1111), - ) + val notificationGenerationProcessor = + NotificationGenerationProcessor( + applicationService, + notificationDisplayer, + MockHelper.configModelStore(), + notificationRepository, + mockk(), + notificationLifecycleService, + MockHelper.time(1111), + ) val notificationPayload: JSONObject = JSONObject() @@ -269,4 +272,55 @@ class NotificationGenerationProcessorTests : FunSpec({ mocks.notificationGenerationProcessor.processNotificationData(context, 1, mocks.notificationPayload, false, 1111) } } + + test("processNotificationData allows the will display callback to prevent default behavior twice") { + // Given + val context = ApplicationProvider.getApplicationContext() + val mocks = Mocks() + coEvery { mocks.notificationDisplayer.displayNotification(any()) } returns true + coEvery { mocks.notificationLifecycleService.externalRemoteNotificationReceived(any()) } just runs + coEvery { mocks.notificationLifecycleService.externalNotificationWillShowInForeground(any()) } coAnswers { + val willDisplayEvent = firstArg() + willDisplayEvent.preventDefault(false) + GlobalScope.launch { + delay(100) + willDisplayEvent.preventDefault(true) + delay(100) + willDisplayEvent.notification.display() + } + } + + // When + mocks.notificationGenerationProcessor.processNotificationData(context, 1, mocks.notificationPayload, false, 1111) + + // Then + coVerify(exactly = 0) { + mocks.notificationDisplayer.displayNotification(any()) + } + } + + test("processNotificationData allows the received event callback to prevent default behavior twice") { + // Given + val context = ApplicationProvider.getApplicationContext() + val mocks = Mocks() + coEvery { mocks.notificationDisplayer.displayNotification(any()) } returns true + coEvery { mocks.notificationLifecycleService.externalRemoteNotificationReceived(any()) } coAnswers { + val receivedEvent = firstArg() + receivedEvent.preventDefault(false) + GlobalScope.launch { + delay(100) + receivedEvent.preventDefault(true) + delay(100) + receivedEvent.notification.display() + } + } + + // When + mocks.notificationGenerationProcessor.processNotificationData(context, 1, mocks.notificationPayload, true, 1111) + + // Then + coVerify(exactly = 0) { + mocks.notificationDisplayer.displayNotification(any()) + } + } })