diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/CoroutineDispatcherProvider.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/CoroutineDispatcherProvider.kt new file mode 100644 index 0000000000..8b57c5659d --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/CoroutineDispatcherProvider.kt @@ -0,0 +1,23 @@ +package com.onesignal.common.threading + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Job + +/** + * Provider interface for coroutine dispatchers. + * This allows for proper dependency injection and easier testing. + */ +interface CoroutineDispatcherProvider { + val io: CoroutineDispatcher + val default: CoroutineDispatcher + + /** + * Launch a coroutine on the IO dispatcher. + */ + fun launchOnIO(block: suspend () -> Unit): Job + + /** + * Launch a coroutine on the Default dispatcher. + */ + fun launchOnDefault(block: suspend () -> Unit): Job +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/DefaultDispatcherProvider.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/DefaultDispatcherProvider.kt new file mode 100644 index 0000000000..8ca50d5b5e --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/DefaultDispatcherProvider.kt @@ -0,0 +1,26 @@ +package com.onesignal.common.threading + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Job + +/** + * Production implementation of [CoroutineDispatcherProvider] that uses OneSignalDispatchers. + * + * This delegates to the existing scopes in OneSignalDispatchers to avoid creating duplicate scopes. + * The OneSignalDispatchers already maintains IOScope and DefaultScope with SupervisorJob, + * so we reuse those instead of creating new ones. + */ +class DefaultDispatcherProvider : CoroutineDispatcherProvider { + override val io: CoroutineDispatcher = OneSignalDispatchers.IO + override val default: CoroutineDispatcher = OneSignalDispatchers.Default + + override fun launchOnIO(block: suspend () -> Unit): Job { + // Delegate to OneSignalDispatchers which already has IOScope with SupervisorJob + return OneSignalDispatchers.launchOnIO(block) + } + + override fun launchOnDefault(block: suspend () -> Unit): Job { + // Delegate to OneSignalDispatchers which already has DefaultScope with SupervisorJob + return OneSignalDispatchers.launchOnDefault(block) + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/startup/StartupService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/startup/StartupService.kt index 9d1c112d64..6d209734f9 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/startup/StartupService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/startup/StartupService.kt @@ -1,18 +1,20 @@ package com.onesignal.core.internal.startup import com.onesignal.common.services.ServiceProvider -import com.onesignal.common.threading.OneSignalDispatchers +import com.onesignal.common.threading.CoroutineDispatcherProvider +import com.onesignal.common.threading.DefaultDispatcherProvider internal class StartupService( private val services: ServiceProvider, + private val dispatchers: CoroutineDispatcherProvider = DefaultDispatcherProvider(), ) { fun bootstrap() { services.getAllServices().forEach { it.bootstrap() } } - // schedule to start all startable services using OneSignal dispatcher + // schedule to start all startable services using the provided dispatcher fun scheduleStart() { - OneSignalDispatchers.launchOnDefault { + dispatchers.launchOnDefault { services.getAllServices().forEach { it.start() } } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/outcomes/impl/OutcomeEventsRepository.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/outcomes/impl/OutcomeEventsRepository.kt index 8a9b7ecb66..16c4f2d32f 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/outcomes/impl/OutcomeEventsRepository.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/session/internal/outcomes/impl/OutcomeEventsRepository.kt @@ -1,6 +1,7 @@ package com.onesignal.session.internal.outcomes.impl import android.content.ContentValues +import com.onesignal.common.threading.OneSignalDispatchers import com.onesignal.core.internal.database.IDatabaseProvider import com.onesignal.core.internal.database.impl.OneSignalDbContract import com.onesignal.debug.internal.logging.Logging @@ -9,7 +10,7 @@ import com.onesignal.session.internal.influence.InfluenceChannel import com.onesignal.session.internal.influence.InfluenceType import com.onesignal.session.internal.influence.InfluenceType.Companion.fromString import com.onesignal.session.internal.outcomes.migrations.RemoveInvalidSessionTimeRecords -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import org.json.JSONArray import org.json.JSONException @@ -17,12 +18,13 @@ import java.util.Locale internal class OutcomeEventsRepository( private val _databaseProvider: IDatabaseProvider, + private val ioDispatcher: CoroutineDispatcher = OneSignalDispatchers.IO, ) : IOutcomeEventsRepository { /** * Delete event from the DB */ override suspend fun deleteOldOutcomeEvent(event: OutcomeEventParams) { - withContext(Dispatchers.IO) { + withContext(ioDispatcher) { _databaseProvider.os.delete( OutcomeEventsTable.TABLE_NAME, OutcomeEventsTable.COLUMN_NAME_TIMESTAMP + " = ?", @@ -36,7 +38,7 @@ internal class OutcomeEventsRepository( * For offline mode and contingency of errors */ override suspend fun saveOutcomeEvent(eventParams: OutcomeEventParams) { - withContext(Dispatchers.IO) { + withContext(ioDispatcher) { var notificationIds = JSONArray() var iamIds = JSONArray() var notificationInfluenceType = InfluenceType.UNATTRIBUTED @@ -101,7 +103,7 @@ internal class OutcomeEventsRepository( */ override suspend fun getAllEventsToSend(): List { val events: MutableList = ArrayList() - withContext(Dispatchers.IO) { + withContext(ioDispatcher) { RemoveInvalidSessionTimeRecords.run(_databaseProvider) _databaseProvider.os.query(OutcomeEventsTable.TABLE_NAME) { cursor -> if (cursor.moveToFirst()) { @@ -248,7 +250,7 @@ internal class OutcomeEventsRepository( override suspend fun saveUniqueOutcomeEventParams(eventParams: OutcomeEventParams) { Logging.debug("OutcomeEventsCache.saveUniqueOutcomeEventParams(eventParams: $eventParams)") - withContext(Dispatchers.IO) { + withContext(ioDispatcher) { val outcomeName = eventParams.outcomeId val cachedUniqueOutcomes: MutableList = ArrayList() val directBody = eventParams.outcomeSource?.directBody @@ -283,7 +285,7 @@ internal class OutcomeEventsRepository( ): List { val uniqueInfluences: MutableList = ArrayList() - withContext(Dispatchers.IO) { + withContext(ioDispatcher) { try { for (influence in influences) { val availableInfluenceIds = JSONArray() @@ -333,7 +335,7 @@ internal class OutcomeEventsRepository( val notificationTableName = OneSignalDbContract.NotificationTable.TABLE_NAME val notificationIdColumnName = OneSignalDbContract.NotificationTable.COLUMN_NAME_NOTIFICATION_ID - withContext(Dispatchers.IO) { + withContext(ioDispatcher) { val whereStr = "NOT EXISTS(" + "SELECT NULL FROM " + notificationTableName + " n " + diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/startup/StartupServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/startup/StartupServiceTests.kt index 7416b2910c..820ed4fe37 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/startup/StartupServiceTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/startup/StartupServiceTests.kt @@ -4,8 +4,7 @@ import com.onesignal.common.services.ServiceBuilder import com.onesignal.common.services.ServiceProvider import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging -import com.onesignal.mocks.IOMockHelper -import com.onesignal.mocks.IOMockHelper.awaitIO +import com.onesignal.mocks.TestDispatcherProvider import io.kotest.assertions.throwables.shouldThrowUnit import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.comparables.shouldBeLessThan @@ -14,7 +13,12 @@ import io.mockk.every import io.mockk.mockk import io.mockk.spyk import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +@OptIn(ExperimentalCoroutinesApi::class) class StartupServiceTests : FunSpec({ fun setupServiceProvider( bootstrapServices: List, @@ -27,111 +31,127 @@ class StartupServiceTests : FunSpec({ serviceBuilder.register(reg).provides() return serviceBuilder.build() } - - listener(IOMockHelper) + val testDispatcher = StandardTestDispatcher() + val dispatcherProvider = TestDispatcherProvider(testDispatcher) beforeAny { Logging.logLevel = LogLevel.NONE } test("bootstrap with no IBootstrapService dependencies is a no-op") { - // Given - val startupService = StartupService(setupServiceProvider(listOf(), listOf())) + runTest(testDispatcher.scheduler) { + // Given + val startupService = StartupService(setupServiceProvider(listOf(), listOf()), dispatcherProvider) - // When - startupService.bootstrap() + // When + startupService.bootstrap() - // Then + // Then + } } test("bootstrap will call all IBootstrapService dependencies successfully") { - // Given - val mockBootstrapService1 = mockk(relaxed = true) - val mockBootstrapService2 = mockk(relaxed = true) + runTest(testDispatcher.scheduler) { + // Given + val mockBootstrapService1 = mockk(relaxed = true) + val mockBootstrapService2 = mockk(relaxed = true) - val startupService = StartupService(setupServiceProvider(listOf(mockBootstrapService1, mockBootstrapService2), listOf())) + val startupService = StartupService(setupServiceProvider(listOf(mockBootstrapService1, mockBootstrapService2), listOf()), dispatcherProvider) - // When - startupService.bootstrap() + // When + startupService.bootstrap() - // Then - verify(exactly = 1) { mockBootstrapService1.bootstrap() } - verify(exactly = 1) { mockBootstrapService2.bootstrap() } + // Then + verify(exactly = 1) { mockBootstrapService1.bootstrap() } + verify(exactly = 1) { mockBootstrapService2.bootstrap() } + } } test("bootstrap will propagate exception when an IBootstrapService throws an exception") { - // Given - val exception = Exception("SOMETHING BAD") - - val mockBootstrapService1 = mockk() - every { mockBootstrapService1.bootstrap() } throws exception - val mockBootstrapService2 = spyk() - - val startupService = StartupService(setupServiceProvider(listOf(mockBootstrapService1, mockBootstrapService2), listOf())) - - // When - val actualException = - shouldThrowUnit { - startupService.bootstrap() - } - - // Then - actualException shouldBe exception - verify(exactly = 1) { mockBootstrapService1.bootstrap() } - verify(exactly = 0) { mockBootstrapService2.bootstrap() } + runTest(testDispatcher.scheduler) { + // Given + val exception = Exception("SOMETHING BAD") + + val mockBootstrapService1 = mockk() + every { mockBootstrapService1.bootstrap() } throws exception + val mockBootstrapService2 = spyk() + + val startupService = StartupService(setupServiceProvider(listOf(mockBootstrapService1, mockBootstrapService2), listOf()), dispatcherProvider) + + // When + val actualException = + shouldThrowUnit { + startupService.bootstrap() + } + + // Then + actualException shouldBe exception + verify(exactly = 1) { mockBootstrapService1.bootstrap() } + verify(exactly = 0) { mockBootstrapService2.bootstrap() } + } } test("startup will call all IStartableService dependencies successfully after a short delay") { - // Given - val mockStartupService1 = mockk(relaxed = true) - val mockStartupService2 = mockk(relaxed = true) - - val startupService = StartupService(setupServiceProvider(listOf(), listOf(mockStartupService1, mockStartupService2))) - - // When - startupService.scheduleStart() - - // Then - wait deterministically for both services to start using IOMockHelper - awaitIO() - verify(exactly = 1) { mockStartupService1.start() } - verify(exactly = 1) { mockStartupService2.start() } + runTest(testDispatcher.scheduler) { + // Given + val mockStartupService1 = mockk(relaxed = true) + val mockStartupService2 = mockk(relaxed = true) + + val startupService = StartupService( + setupServiceProvider(listOf(), listOf(mockStartupService1, mockStartupService2)), + dispatcherProvider + ) + + // When + startupService.scheduleStart() + + // Then - wait deterministically for both services to start using advanceUntilIdle + advanceUntilIdle() + verify(exactly = 1) { mockStartupService1.start() } + verify(exactly = 1) { mockStartupService2.start() } + } } test("scheduleStart does not block main thread") { - // Given - val mockStartableService1 = mockk(relaxed = true) - val mockStartableService2 = spyk() - val mockStartableService3 = spyk() - // Only service1 and service2 are scheduled - service3 is NOT scheduled - val startupService = StartupService(setupServiceProvider(listOf(), listOf(mockStartableService1, mockStartableService2))) - - // When - scheduleStart() is async, so it doesn't block - val startTime = System.currentTimeMillis() - startupService.scheduleStart() - val scheduleTime = System.currentTimeMillis() - startTime - - // This should execute immediately since scheduleStart() doesn't block - // service3 is NOT part of scheduled services, so this is a direct call - mockStartableService3.start() - val immediateTime = System.currentTimeMillis() - startTime - - // Then - verify scheduleStart() returned quickly (non-blocking) - // Should return in < 50ms (proving it doesn't wait for services to start) - scheduleTime shouldBeLessThan 50L - immediateTime shouldBeLessThan 50L - - // Verify service3 was called immediately (proving main thread wasn't blocked) - verify(exactly = 1) { mockStartableService3.start() } - - // Wait deterministically for async execution using IOMockHelper - awaitIO() - - // Verify scheduled services were called - verify(exactly = 1) { mockStartableService1.start() } - verify(exactly = 1) { mockStartableService2.start() } - - // The key assertion: scheduleStart() returned immediately without blocking, - // allowing service3.start() to be called synchronously before scheduled services - // complete. This proves scheduleStart() is non-blocking. + runTest(testDispatcher.scheduler) { + // Given + val mockStartableService1 = mockk(relaxed = true) + val mockStartableService2 = spyk() + val mockStartableService3 = spyk() + // Only service1 and service2 are scheduled - service3 is NOT scheduled + val startupService = StartupService( + setupServiceProvider(listOf(), listOf(mockStartableService1, mockStartableService2)), + dispatcherProvider + ) + + // When - scheduleStart() is async, so it doesn't block + val startTime = System.currentTimeMillis() + startupService.scheduleStart() + val scheduleTime = System.currentTimeMillis() - startTime + + // This should execute immediately since scheduleStart() doesn't block + // service3 is NOT part of scheduled services, so this is a direct call + mockStartableService3.start() + val immediateTime = System.currentTimeMillis() - startTime + + // Then - verify scheduleStart() returned quickly (non-blocking) + // Should return in < 50ms (proving it doesn't wait for services to start) + scheduleTime shouldBeLessThan 50L + immediateTime shouldBeLessThan 50L + + // Verify service3 was called immediately (proving main thread wasn't blocked) + verify(exactly = 1) { mockStartableService3.start() } + + // Wait deterministically for async execution using advanceUntilIdle + advanceUntilIdle() + + // Verify scheduled services were called + verify(exactly = 1) { mockStartableService1.start() } + verify(exactly = 1) { mockStartableService2.start() } + + // The key assertion: scheduleStart() returned immediately without blocking, + // allowing service3.start() to be called synchronously before scheduled services + // complete. This proves scheduleStart() is non-blocking. + } } }) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OneSignalImpTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OneSignalImpTests.kt index d660fa2525..08cfc6d79f 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OneSignalImpTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/internal/OneSignalImpTests.kt @@ -2,41 +2,51 @@ package com.onesignal.internal import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging +import com.onesignal.mocks.TestDispatcherProvider import io.kotest.assertions.throwables.shouldThrowUnit import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest class OneSignalImpTests : FunSpec({ beforeAny { Logging.logLevel = LogLevel.NONE } + val testDispatcher = StandardTestDispatcher() + val dispatcherProvider = TestDispatcherProvider(testDispatcher) + test("attempting login before initWithContext throws exception") { - // Given - val os = OneSignalImp() + runTest(testDispatcher.scheduler) { + // Given + val os = OneSignalImp() - // When - val exception = - shouldThrowUnit { - os.login("login-id") - } + // When + val exception = + shouldThrowUnit { + os.login("login-id") + } - // Then - exception.message shouldBe "Must call 'initWithContext' before 'login'" + // Then + exception.message shouldBe "Must call 'initWithContext' before 'login'" + } } test("attempting logout before initWithContext throws exception") { - // Given - val os = OneSignalImp() + runTest(testDispatcher.scheduler) { + // Given + val os = OneSignalImp() - // When - val exception = - shouldThrowUnit { - os.logout() - } + // When + val exception = + shouldThrowUnit { + os.logout() + } - // Then - exception.message shouldBe "Must call 'initWithContext' before 'logout'" + // Then + exception.message shouldBe "Must call 'initWithContext' before 'logout'" + } } // Comprehensive tests for deprecated properties that should work before and after initialization @@ -44,7 +54,7 @@ class OneSignalImpTests : FunSpec({ context("before initWithContext") { test("get returns false by default") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When & Then os.consentRequired shouldBe false @@ -52,7 +62,7 @@ class OneSignalImpTests : FunSpec({ test("set and get works correctly") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When os.consentRequired = true @@ -69,7 +79,7 @@ class OneSignalImpTests : FunSpec({ test("set should not throw") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When & Then - should not throw os.consentRequired = false @@ -82,7 +92,7 @@ class OneSignalImpTests : FunSpec({ context("before initWithContext") { test("get returns false by default") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When & Then os.consentGiven shouldBe false @@ -90,7 +100,7 @@ class OneSignalImpTests : FunSpec({ test("set and get works correctly") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When os.consentGiven = true @@ -107,7 +117,7 @@ class OneSignalImpTests : FunSpec({ test("set should not throw") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When & Then - should not throw os.consentGiven = true @@ -120,7 +130,7 @@ class OneSignalImpTests : FunSpec({ context("before initWithContext") { test("get returns false by default") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When & Then os.disableGMSMissingPrompt shouldBe false @@ -128,7 +138,7 @@ class OneSignalImpTests : FunSpec({ test("set and get works correctly") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When os.disableGMSMissingPrompt = true @@ -145,7 +155,7 @@ class OneSignalImpTests : FunSpec({ test("set should not throw") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When & Then - should not throw os.disableGMSMissingPrompt = true @@ -157,7 +167,7 @@ class OneSignalImpTests : FunSpec({ context("property consistency tests") { test("all properties maintain state correctly") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When - set all properties to true os.consentRequired = true @@ -182,7 +192,7 @@ class OneSignalImpTests : FunSpec({ test("properties are independent of each other") { // Given - val os = OneSignalImp() + val os = OneSignalImp(dispatcherProvider.io) // When - set only consentRequired to true os.consentRequired = true @@ -218,7 +228,7 @@ class OneSignalImpTests : FunSpec({ // waitForInit() would timeout after 30 seconds and log a warning (not throw) // Given - a fresh OneSignalImp instance - val oneSignalImp = OneSignalImp() + val oneSignalImp = OneSignalImp(dispatcherProvider.io) // The timeout behavior is built into waitUntilInitInternal() // which uses withTimeout() to wait for up to 30 seconds (or 4.8 seconds on main thread) @@ -236,7 +246,7 @@ class OneSignalImpTests : FunSpec({ // until initialization completes (per PR #2412) // Given - val oneSignalImp = OneSignalImp() + val oneSignalImp = OneSignalImp(dispatcherProvider.io) // We can verify the wait behavior by checking: // 1. The suspendCompletion (CompletableDeferred) is properly initialized diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/session/internal/outcomes/OutcomeEventsRepositoryTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/session/internal/outcomes/OutcomeEventsRepositoryTests.kt index e7fad98ac3..f1cd47491c 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/session/internal/outcomes/OutcomeEventsRepositoryTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/session/internal/outcomes/OutcomeEventsRepositoryTests.kt @@ -4,6 +4,7 @@ import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.mocks.DatabaseMockHelper +import com.onesignal.mocks.TestDispatcherProvider import com.onesignal.session.internal.influence.Influence import com.onesignal.session.internal.influence.InfluenceChannel import com.onesignal.session.internal.influence.InfluenceType @@ -19,6 +20,9 @@ import io.kotest.matchers.shouldNotBe import io.mockk.verify import io.mockk.verifyAll import io.mockk.verifySequence +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.runTest import org.json.JSONArray @RobolectricTest @@ -27,481 +31,512 @@ class OutcomeEventsRepositoryTests : FunSpec({ Logging.logLevel = LogLevel.NONE } + // avoids initialization happening too early (before Robolectric’s environment exists). + lateinit var testDispatcher: TestDispatcher + lateinit var dispatcherProvider: TestDispatcherProvider + + beforeTest { + testDispatcher = StandardTestDispatcher() + dispatcherProvider = TestDispatcherProvider(testDispatcher) + } + test("delete outcome event should use the timestamp to delete row from database") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - outcomeEventsRepository.deleteOldOutcomeEvent(OutcomeEventParams("outcomeId", null, 0f, 0, 1111)) - - // Then - verify(exactly = 1) { - mockDatabasePair.second.delete( - OutcomeEventsTable.TABLE_NAME, - withArg { - it.contains(OutcomeEventsTable.COLUMN_NAME_TIMESTAMP) - }, - withArg { it.contains("1111") }, - ) + runTest(dispatcherProvider.io) { + val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + outcomeEventsRepository.deleteOldOutcomeEvent(OutcomeEventParams("outcomeId", null, 0f, 0, 1111)) + + // Then + verify(exactly = 1) { + mockDatabasePair.second.delete( + OutcomeEventsTable.TABLE_NAME, + withArg { + it.contains(OutcomeEventsTable.COLUMN_NAME_TIMESTAMP) + }, + withArg { it.contains("1111") }, + ) + } } } test("save outcome event should insert row into database") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - outcomeEventsRepository.saveOutcomeEvent(OutcomeEventParams("outcomeId1", null, 0f, 0, 1111)) - outcomeEventsRepository.saveOutcomeEvent( - OutcomeEventParams( - "outcomeId2", - OutcomeSource( - OutcomeSourceBody(JSONArray().put("notificationId1")), - OutcomeSourceBody(null, JSONArray().put("iamId1").put("iamId2")), - ), - .2f, - 0, - 2222, - ), - ) - outcomeEventsRepository.saveOutcomeEvent( - OutcomeEventParams( - "outcomeId3", - OutcomeSource( - OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1")), - null, - ), - .4f, - 0, - 3333, - ), - ) - outcomeEventsRepository.saveOutcomeEvent( - OutcomeEventParams( - "outcomeId4", - OutcomeSource( - null, - OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1").put("iamId2")), + runTest(dispatcherProvider.io) { + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + outcomeEventsRepository.saveOutcomeEvent(OutcomeEventParams("outcomeId1", null, 0f, 0, 1111)) + outcomeEventsRepository.saveOutcomeEvent( + OutcomeEventParams( + "outcomeId2", + OutcomeSource( + OutcomeSourceBody(JSONArray().put("notificationId1")), + OutcomeSourceBody(null, JSONArray().put("iamId1").put("iamId2")), + ), + .2f, + 0, + 2222, ), - .6f, - 0, - 4444, - ), - ) - - // Then - verifySequence { - mockDatabasePair.second.insert( - OutcomeEventsTable.TABLE_NAME, - null, - withArg { - it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe 0f - it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 1111L - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "unattributed" - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray().toString() - it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "unattributed" - it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray().toString() - }, ) - mockDatabasePair.second.insert( - OutcomeEventsTable.TABLE_NAME, - null, - withArg { - it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId2" - it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .2f - it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 2222L - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "direct" - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() - it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "indirect" - it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\", \"iamId2\"]").toString() - }, - ) - mockDatabasePair.second.insert( - OutcomeEventsTable.TABLE_NAME, - null, - withArg { - it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId3" - it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .4f - it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 3333L - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "direct" - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() - it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "direct" - it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\"]").toString() - }, + outcomeEventsRepository.saveOutcomeEvent( + OutcomeEventParams( + "outcomeId3", + OutcomeSource( + OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1")), + null, + ), + .4f, + 0, + 3333, + ), ) - mockDatabasePair.second.insert( - OutcomeEventsTable.TABLE_NAME, - null, - withArg { - it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId4" - it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .6f - it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 4444L - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "indirect" - it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() - it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "indirect" - it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\", \"iamId2\"]").toString() - }, + outcomeEventsRepository.saveOutcomeEvent( + OutcomeEventParams( + "outcomeId4", + OutcomeSource( + null, + OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1").put("iamId2")), + ), + .6f, + 0, + 4444, + ), ) + + // Then + verifySequence { + mockDatabasePair.second.insert( + OutcomeEventsTable.TABLE_NAME, + null, + withArg { + it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe 0f + it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 1111L + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "unattributed" + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray().toString() + it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "unattributed" + it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray().toString() + }, + ) + mockDatabasePair.second.insert( + OutcomeEventsTable.TABLE_NAME, + null, + withArg { + it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId2" + it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .2f + it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 2222L + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "direct" + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() + it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "indirect" + it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\", \"iamId2\"]").toString() + }, + ) + mockDatabasePair.second.insert( + OutcomeEventsTable.TABLE_NAME, + null, + withArg { + it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId3" + it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .4f + it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 3333L + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "direct" + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() + it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "direct" + it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\"]").toString() + }, + ) + mockDatabasePair.second.insert( + OutcomeEventsTable.TABLE_NAME, + null, + withArg { + it[OutcomeEventsTable.COLUMN_NAME_NAME] shouldBe "outcomeId4" + it[OutcomeEventsTable.COLUMN_NAME_WEIGHT] shouldBe .6f + it[OutcomeEventsTable.COLUMN_NAME_TIMESTAMP] shouldBe 4444L + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE] shouldBe "indirect" + it[OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS] shouldBe JSONArray("[\"notificationId1\"]").toString() + it[OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE] shouldBe "indirect" + it[OutcomeEventsTable.COLUMN_NAME_IAM_IDS] shouldBe JSONArray("[\"iamId1\", \"iamId2\"]").toString() + }, + ) + } } } test("get events should retrieve return empty list when database is empty") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) + runTest(dispatcherProvider.io) { + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) - // When - val events = outcomeEventsRepository.getAllEventsToSend() + // When + val events = outcomeEventsRepository.getAllEventsToSend() - // Then - events.count() shouldBe 0 + // Then + events.count() shouldBe 0 + } } test("get events should retrieve return an item per row in database") { - // Given - val records = - listOf( - mapOf( - OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId1", - OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.2f, - OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 1111L, - OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 1L, - OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "unattributed", - OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "unattributed", - ), - mapOf( - OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId2", - OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.4f, - OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 2222L, - OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 2L, - OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "indirect", - OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS to "[\"notificationId1\",\"notificationId2\"]", - OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "indirect", - OutcomeEventsTable.COLUMN_NAME_IAM_IDS to "[\"iamId1\",\"iamId2\"]", - ), - mapOf( - OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId3", - OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.6f, - OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 3333L, - OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 3L, - OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "direct", - OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS to "[\"notificationId3\"]", - OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "direct", - OutcomeEventsTable.COLUMN_NAME_IAM_IDS to "[\"iamId3\"]", - ), - ) - val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME, records) - - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - val events = outcomeEventsRepository.getAllEventsToSend() - - // Then - events.count() shouldBe 3 - events[0].outcomeId shouldBe "outcomeId1" - events[0].weight shouldBe 0.2f - events[0].timestamp shouldBe 1111L - events[0].sessionTime shouldBe 1L - events[0].outcomeSource shouldNotBe null - events[0].outcomeSource!!.directBody shouldBe null - events[0].outcomeSource!!.indirectBody shouldBe null - events[1].outcomeId shouldBe "outcomeId2" - events[1].weight shouldBe 0.4f - events[1].timestamp shouldBe 2222L - events[1].sessionTime shouldBe 2L - events[1].outcomeSource shouldNotBe null - events[1].outcomeSource!!.directBody shouldBe null - events[1].outcomeSource!!.indirectBody shouldNotBe null - events[1].outcomeSource!!.indirectBody!!.notificationIds!!.length() shouldBe 2 - events[1].outcomeSource!!.indirectBody!!.notificationIds!!.getString(0) shouldBe "notificationId1" - events[1].outcomeSource!!.indirectBody!!.notificationIds!!.getString(1) shouldBe "notificationId2" - events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.length() shouldBe 2 - events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.getString(0) shouldBe "iamId1" - events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.getString(1) shouldBe "iamId2" - events[2].outcomeId shouldBe "outcomeId3" - events[2].weight shouldBe 0.6f - events[2].timestamp shouldBe 3333L - events[2].sessionTime shouldBe 3L - events[2].outcomeSource shouldNotBe null - events[2].outcomeSource!!.indirectBody shouldBe null - events[2].outcomeSource!!.directBody shouldNotBe null - events[2].outcomeSource!!.directBody!!.notificationIds!!.length() shouldBe 1 - events[2].outcomeSource!!.directBody!!.notificationIds!!.getString(0) shouldBe "notificationId3" - events[2].outcomeSource!!.directBody!!.inAppMessagesIds!!.length() shouldBe 1 - events[2].outcomeSource!!.directBody!!.inAppMessagesIds!!.getString(0) shouldBe "iamId3" + runTest(dispatcherProvider.io) { + // Given + val records = + listOf( + mapOf( + OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId1", + OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.2f, + OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 1111L, + OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 1L, + OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "unattributed", + OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "unattributed", + ), + mapOf( + OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId2", + OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.4f, + OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 2222L, + OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 2L, + OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "indirect", + OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS to "[\"notificationId1\",\"notificationId2\"]", + OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "indirect", + OutcomeEventsTable.COLUMN_NAME_IAM_IDS to "[\"iamId1\",\"iamId2\"]", + ), + mapOf( + OutcomeEventsTable.COLUMN_NAME_NAME to "outcomeId3", + OutcomeEventsTable.COLUMN_NAME_WEIGHT to 0.6f, + OutcomeEventsTable.COLUMN_NAME_TIMESTAMP to 3333L, + OutcomeEventsTable.COLUMN_NAME_SESSION_TIME to 3L, + OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_INFLUENCE_TYPE to "direct", + OutcomeEventsTable.COLUMN_NAME_NOTIFICATION_IDS to "[\"notificationId3\"]", + OutcomeEventsTable.COLUMN_NAME_IAM_INFLUENCE_TYPE to "direct", + OutcomeEventsTable.COLUMN_NAME_IAM_IDS to "[\"iamId3\"]", + ), + ) + val mockDatabasePair = DatabaseMockHelper.databaseProvider(OutcomeEventsTable.TABLE_NAME, records) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + val events = outcomeEventsRepository.getAllEventsToSend() + + // Then + events.count() shouldBe 3 + events[0].outcomeId shouldBe "outcomeId1" + events[0].weight shouldBe 0.2f + events[0].timestamp shouldBe 1111L + events[0].sessionTime shouldBe 1L + events[0].outcomeSource shouldNotBe null + events[0].outcomeSource!!.directBody shouldBe null + events[0].outcomeSource!!.indirectBody shouldBe null + events[1].outcomeId shouldBe "outcomeId2" + events[1].weight shouldBe 0.4f + events[1].timestamp shouldBe 2222L + events[1].sessionTime shouldBe 2L + events[1].outcomeSource shouldNotBe null + events[1].outcomeSource!!.directBody shouldBe null + events[1].outcomeSource!!.indirectBody shouldNotBe null + events[1].outcomeSource!!.indirectBody!!.notificationIds!!.length() shouldBe 2 + events[1].outcomeSource!!.indirectBody!!.notificationIds!!.getString(0) shouldBe "notificationId1" + events[1].outcomeSource!!.indirectBody!!.notificationIds!!.getString(1) shouldBe "notificationId2" + events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.length() shouldBe 2 + events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.getString(0) shouldBe "iamId1" + events[1].outcomeSource!!.indirectBody!!.inAppMessagesIds!!.getString(1) shouldBe "iamId2" + events[2].outcomeId shouldBe "outcomeId3" + events[2].weight shouldBe 0.6f + events[2].timestamp shouldBe 3333L + events[2].sessionTime shouldBe 3L + events[2].outcomeSource shouldNotBe null + events[2].outcomeSource!!.indirectBody shouldBe null + events[2].outcomeSource!!.directBody shouldNotBe null + events[2].outcomeSource!!.directBody!!.notificationIds!!.length() shouldBe 1 + events[2].outcomeSource!!.directBody!!.notificationIds!!.getString(0) shouldBe "notificationId3" + events[2].outcomeSource!!.directBody!!.inAppMessagesIds!!.length() shouldBe 1 + events[2].outcomeSource!!.directBody!!.inAppMessagesIds!!.getString(0) shouldBe "iamId3" + } } test("save unique outcome should insert no rows when no influences") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.COLUMN_NAME_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) + runTest(dispatcherProvider.io) { + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.COLUMN_NAME_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) - // When - outcomeEventsRepository.saveUniqueOutcomeEventParams(OutcomeEventParams("outcomeId1", null, 0f, 0, 1111)) + // When + outcomeEventsRepository.saveUniqueOutcomeEventParams(OutcomeEventParams("outcomeId1", null, 0f, 0, 1111)) - // Then - verify(exactly = 0) { mockDatabasePair.second.insert(CachedUniqueOutcomeTable.COLUMN_NAME_NAME, null, any()) } + // Then + verify(exactly = 0) { mockDatabasePair.second.insert(CachedUniqueOutcomeTable.COLUMN_NAME_NAME, null, any()) } + } } test("save unique outcome should insert 1 row for each unique influence when direct notification and indiract iam") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - outcomeEventsRepository.saveUniqueOutcomeEventParams( - OutcomeEventParams( - "outcomeId1", - OutcomeSource( - OutcomeSourceBody(JSONArray().put("notificationId1")), - OutcomeSourceBody(null, JSONArray().put("iamId1").put("iamId2")), + runTest(dispatcherProvider.io) { + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + outcomeEventsRepository.saveUniqueOutcomeEventParams( + OutcomeEventParams( + "outcomeId1", + OutcomeSource( + OutcomeSourceBody(JSONArray().put("notificationId1")), + OutcomeSourceBody(null, JSONArray().put("iamId1").put("iamId2")), + ), + .2f, + 0, + 2222, ), - .2f, - 0, - 2222, - ), - ) - - // Then - verifyAll { - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId2" - }, ) + + // Then + verifyAll { + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId2" + }, + ) + } } } test("save unique outcome should insert 1 row for each unique influence when direct iam and indiract notifications") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - outcomeEventsRepository.saveUniqueOutcomeEventParams( - OutcomeEventParams( - "outcomeId1", - OutcomeSource( - OutcomeSourceBody(null, JSONArray().put("iamId1")), - OutcomeSourceBody(JSONArray().put("notificationId1").put("notificationId2")), + runTest(dispatcherProvider.io) { + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + outcomeEventsRepository.saveUniqueOutcomeEventParams( + OutcomeEventParams( + "outcomeId1", + OutcomeSource( + OutcomeSourceBody(null, JSONArray().put("iamId1")), + OutcomeSourceBody(JSONArray().put("notificationId1").put("notificationId2")), + ), + .2f, + 0, + 2222, ), - .2f, - 0, - 2222, - ), - ) - - // Then - verifyAll { - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId2" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" - }, ) + + // Then + verifyAll { + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId2" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" + }, + ) + } } } test("save unique outcome should insert 1 row for each unique influence when direct notification and iam") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - outcomeEventsRepository.saveUniqueOutcomeEventParams( - OutcomeEventParams( - "outcomeId1", - OutcomeSource( - OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1")), - null, + runTest(dispatcherProvider.io) { + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + outcomeEventsRepository.saveUniqueOutcomeEventParams( + OutcomeEventParams( + "outcomeId1", + OutcomeSource( + OutcomeSourceBody(JSONArray().put("notificationId1"), JSONArray().put("iamId1")), + null, + ), + .2f, + 0, + 2222, ), - .2f, - 0, - 2222, - ), - ) - - // Then - verifyAll { - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" - }, ) + + // Then + verifyAll { + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" + }, + ) + } } } test("save unique outcome should insert 1 row for each unique influence when indirect notification and iam") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - outcomeEventsRepository.saveUniqueOutcomeEventParams( - OutcomeEventParams( - "outcomeId1", - OutcomeSource( - null, - OutcomeSourceBody(JSONArray().put("notificationId1").put("notificationId2"), JSONArray().put("iamId1").put("iamId2")), + runTest(dispatcherProvider.io) { + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + outcomeEventsRepository.saveUniqueOutcomeEventParams( + OutcomeEventParams( + "outcomeId1", + OutcomeSource( + null, + OutcomeSourceBody(JSONArray().put("notificationId1").put("notificationId2"), JSONArray().put("iamId1").put("iamId2")), + ), + .2f, + 0, + 2222, ), - .2f, - 0, - 2222, - ), - ) - - // Then - verifyAll { - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId2" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" - }, - ) - mockDatabasePair.second.insert( - CachedUniqueOutcomeTable.TABLE_NAME, - null, - withArg { - it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" - it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId2" - }, ) + + // Then + verifyAll { + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "notification" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "notificationId2" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId1" + }, + ) + mockDatabasePair.second.insert( + CachedUniqueOutcomeTable.TABLE_NAME, + null, + withArg { + it[CachedUniqueOutcomeTable.COLUMN_NAME_NAME] shouldBe "outcomeId1" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_TYPE] shouldBe "iam" + it[CachedUniqueOutcomeTable.COLUMN_CHANNEL_INFLUENCE_ID] shouldBe "iamId2" + }, + ) + } } } test("retrieve non-cached influence should return full list when there are no cached unique influences") { - // Given - val records = listOf>() - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME, records) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - val influences = - outcomeEventsRepository.getNotCachedUniqueInfluencesForOutcome( - "outcomeId1", - listOf( - Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId1")), - Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId2")), - ), - ) - - // Then - influences.count() shouldBe 2 + runTest(dispatcherProvider.io) { + // Given + val records = listOf>() + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME, records) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + val influences = + outcomeEventsRepository.getNotCachedUniqueInfluencesForOutcome( + "outcomeId1", + listOf( + Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId1")), + Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId2")), + ), + ) + + // Then + influences.count() shouldBe 2 + } } test("retrieve non-cached influence should filter out an influence when there are is a matching influence") { - // Given - val records = listOf(mapOf(CachedUniqueOutcomeTable.COLUMN_NAME_NAME to "outcomeId1")) - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME, records) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - val influences = - outcomeEventsRepository.getNotCachedUniqueInfluencesForOutcome( - "outcomeId1", - listOf( - Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId1")), - Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId2")), - ), - ) - - // Then - influences.count() shouldBe 0 + runTest(dispatcherProvider.io) { + // Given + val records = listOf(mapOf(CachedUniqueOutcomeTable.COLUMN_NAME_NAME to "outcomeId1")) + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME, records) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + val influences = + outcomeEventsRepository.getNotCachedUniqueInfluencesForOutcome( + "outcomeId1", + listOf( + Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId1")), + Influence(InfluenceChannel.NOTIFICATION, InfluenceType.DIRECT, JSONArray().put("notificationId2")), + ), + ) + + // Then + influences.count() shouldBe 0 + } } test("clear unique influence should delete out an influence when there are is a matching influence") { - // Given - val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) - val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first) - - // When - outcomeEventsRepository.cleanCachedUniqueOutcomeEventNotifications() - - // Then - verifyAll { - mockDatabasePair.second.delete(CachedUniqueOutcomeTable.TABLE_NAME, any(), any()) + runTest(dispatcherProvider.io) { + // Given + val mockDatabasePair = DatabaseMockHelper.databaseProvider(CachedUniqueOutcomeTable.TABLE_NAME) + val outcomeEventsRepository = OutcomeEventsRepository(mockDatabasePair.first, testDispatcher) + + // When + outcomeEventsRepository.cleanCachedUniqueOutcomeEventNotifications() + + // Then + verifyAll { + mockDatabasePair.second.delete(CachedUniqueOutcomeTable.TABLE_NAME, any(), any()) + } } } }) diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/data/impl/NotificationRepository.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/data/impl/NotificationRepository.kt index 66c750e3c0..d1ad883e12 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/data/impl/NotificationRepository.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/data/impl/NotificationRepository.kt @@ -4,6 +4,8 @@ import android.app.NotificationManager import android.content.ContentValues import android.provider.BaseColumns import android.text.TextUtils +import com.onesignal.common.threading.CoroutineDispatcherProvider +import com.onesignal.common.threading.DefaultDispatcherProvider import com.onesignal.core.internal.application.IApplicationService import com.onesignal.core.internal.database.IDatabaseProvider import com.onesignal.core.internal.database.impl.OneSignalDbContract @@ -14,7 +16,6 @@ import com.onesignal.notifications.internal.common.NotificationHelper import com.onesignal.notifications.internal.data.INotificationQueryHelper import com.onesignal.notifications.internal.data.INotificationRepository import com.onesignal.notifications.internal.limiting.INotificationLimitManager -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONException @@ -24,6 +25,7 @@ internal class NotificationRepository( private val _databaseProvider: IDatabaseProvider, private val _time: ITime, private val _badgeCountUpdater: IBadgeCountUpdater, + private val dispatchers: CoroutineDispatcherProvider = DefaultDispatcherProvider(), ) : INotificationRepository { /** * Deletes notifications with created timestamps older than 7 days @@ -31,7 +33,7 @@ internal class NotificationRepository( * 1. NotificationTable.TABLE_NAME */ override suspend fun deleteExpiredNotifications() { - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val whereStr: String = OneSignalDbContract.NotificationTable.COLUMN_NAME_CREATED_TIME.toString() + " < ?" val sevenDaysAgoInSeconds: String = java.lang.String.valueOf( @@ -48,7 +50,7 @@ internal class NotificationRepository( } override suspend fun markAsDismissedForOutstanding() { - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val appContext = _applicationService.appContext val notificationManager: NotificationManager = NotificationHelper.getNotificationManager(appContext) val retColumn = arrayOf(OneSignalDbContract.NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID) @@ -79,7 +81,7 @@ internal class NotificationRepository( } override suspend fun markAsDismissedForGroup(group: String) { - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val appContext = _applicationService.appContext val notificationManager: NotificationManager = NotificationHelper.getNotificationManager(appContext) @@ -124,7 +126,7 @@ internal class NotificationRepository( override suspend fun markAsDismissed(androidId: Int): Boolean { var didDismiss: Boolean = false - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { didDismiss = internalMarkAsDismissed(androidId) } @@ -159,7 +161,7 @@ internal class NotificationRepository( var result = false - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val retColumn = arrayOf(OneSignalDbContract.NotificationTable.COLUMN_NAME_NOTIFICATION_ID) val whereArgs = arrayOf(id!!) @@ -188,7 +190,7 @@ internal class NotificationRepository( androidId: Int, groupId: String, ) { - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { // There currently isn't a visible notification from for this group_id. // Save the group summary notification id so it can be updated later. val values = ContentValues() @@ -218,7 +220,7 @@ internal class NotificationRepository( expireTime: Long, jsonPayload: String, ) { - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { Logging.debug("Saving Notification id=$id") try { @@ -302,7 +304,7 @@ internal class NotificationRepository( summaryGroup: String?, clearGroupOnSummaryClick: Boolean, ) { - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { var whereStr: String var whereArgs: Array? = null if (summaryGroup != null) { @@ -358,7 +360,7 @@ internal class NotificationRepository( override suspend fun getGroupId(androidId: Int): String? { var groupId: String? = null - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { _databaseProvider.os.query( OneSignalDbContract.NotificationTable.TABLE_NAME, // retColumn @@ -378,7 +380,7 @@ internal class NotificationRepository( override suspend fun getAndroidIdFromCollapseKey(collapseKey: String): Int? { var androidId: Int? = null - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { _databaseProvider.os.query( OneSignalDbContract.NotificationTable.TABLE_NAME, // retColumn @@ -402,7 +404,7 @@ internal class NotificationRepository( notificationsToMakeRoomFor: Int, maxNumberOfNotificationsInt: Int, ) { - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val maxNumberOfNotificationsString = maxNumberOfNotificationsInt.toString() try { @@ -437,7 +439,7 @@ internal class NotificationRepository( override suspend fun listNotificationsForGroup(summaryGroup: String): List { val listOfNotifications = mutableListOf() - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val whereArgs = arrayOf(summaryGroup) _databaseProvider.os.query( @@ -512,7 +514,7 @@ internal class NotificationRepository( val whereArgs = if (isGroupless) null else arrayOf(group) - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { // Order by timestamp in descending and limit to 1 _databaseProvider.os.query( OneSignalDbContract.NotificationTable.TABLE_NAME, @@ -538,7 +540,7 @@ internal class NotificationRepository( override suspend fun listNotificationsForOutstanding(excludeAndroidIds: List?): List { val listOfNotifications = mutableListOf() - withContext(Dispatchers.IO) { + withContext(dispatchers.io) { val dbQuerySelection = _queryHelper.recentUninteractedWithNotificationsWhere() if (excludeAndroidIds != null) { diff --git a/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/TestDispatcherProvider.kt b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/TestDispatcherProvider.kt new file mode 100644 index 0000000000..2288659dc8 --- /dev/null +++ b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/TestDispatcherProvider.kt @@ -0,0 +1,65 @@ +package com.onesignal.mocks + +import com.onesignal.common.threading.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher + +/** + * Test implementation of [CoroutineDispatcherProvider] for unit tests. + * Uses a [TestDispatcher] for deterministic testing. + * + * Usage in tests: + * ``` + * test("my test") { + * val testDispatcher = StandardTestDispatcher() + * val dispatcherProvider = TestDispatcherProvider(testDispatcher) + * + * runTest(testDispatcher.scheduler) { + * val service = MyService(dispatcherProvider) + * service.doWork() + * + * // Option 1: Advance until all pending coroutines complete + * advanceUntilIdle() + * + * // Option 2: Advance virtual time by a specific amount (e.g., 100ms) + * // advanceTimeBy(100) + * + * // Option 3: Run only coroutines scheduled at current time + * // runCurrent() + * + * // Make assertions + * } + * } + * ``` + * + * Methods to control execution: + * - [advanceUntilIdle()] - Runs all pending coroutines until there's nothing left to execute + * - [advanceTimeBy(delayTime)] - Advances virtual time by the specified amount and runs + * coroutines scheduled for that time period + * - [runCurrent()] - Runs only the coroutines that are scheduled to run at the current + * virtual time (doesn't advance time) + * - [currentTime] - Property to check the current virtual time + */ +class TestDispatcherProvider( + private val testDispatcher: TestDispatcher = StandardTestDispatcher() +) : CoroutineDispatcherProvider { + override val io: CoroutineDispatcher = testDispatcher + override val default: CoroutineDispatcher = testDispatcher + + private val scope: CoroutineScope by lazy { + CoroutineScope(SupervisorJob() + testDispatcher) + } + + override fun launchOnIO(block: suspend () -> Unit): Job { + return scope.launch { block() } + } + + override fun launchOnDefault(block: suspend () -> Unit): Job { + return scope.launch { block() } + } +}