diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d108601db..c5e77c3d09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: - name: "[Test] SDK Unit Tests" working-directory: OneSignalSDK run: | - ./gradlew test --console=plain + ./gradlew testReleaseUnitTest --console=plain --continue - name: Unit tests results if: failure() uses: actions/upload-artifact@v3 diff --git a/OneSignalSDK/build.gradle b/OneSignalSDK/build.gradle index 878c7ec981..c2609ddcfb 100644 --- a/OneSignalSDK/build.gradle +++ b/OneSignalSDK/build.gradle @@ -4,8 +4,8 @@ buildscript { ext { buildVersions = [ - compileSdkVersion: 31, - targetSdkVersion: 31, + compileSdkVersion: 34, + targetSdkVersion: 34, minSdkVersion: 19 ] androidGradlePluginVersion = '7.2.0' diff --git a/OneSignalSDK/onesignal/core/build.gradle b/OneSignalSDK/onesignal/core/build.gradle index 50df86cd10..ec7d5212a2 100644 --- a/OneSignalSDK/onesignal/core/build.gradle +++ b/OneSignalSDK/onesignal/core/build.gradle @@ -78,6 +78,7 @@ dependencies { } } + testImplementation(project(':OneSignal:testhelpers')) testImplementation("junit:junit:$junitVersion") testImplementation("io.kotest:kotest-runner-junit4:$kotestVersion") testImplementation("io.kotest:kotest-runner-junit4-jvm:$kotestVersion") diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/SubscriptionBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/SubscriptionBackendService.kt index ac48f4fc7d..e6f6d23c92 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/SubscriptionBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/SubscriptionBackendService.kt @@ -17,9 +17,9 @@ internal class SubscriptionBackendService( aliasValue: String, subscription: SubscriptionObject, ): String? { - val requestJSON = - JSONObject() - .put("subscription", JSONConverter.convertToJSON(subscription)) + val jsonSubscription = JSONConverter.convertToJSON(subscription) + jsonSubscription.remove("id") + val requestJSON = JSONObject().put("subscription", jsonSubscription) val response = _httpClient.post("apps/$appId/users/by/$aliasLabel/$aliasValue/subscriptions", requestJSON) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/ApplicationServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/ApplicationServiceTests.kt index 900959ee1a..8408df863d 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/ApplicationServiceTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/ApplicationServiceTests.kt @@ -8,7 +8,7 @@ import com.onesignal.common.threading.suspendifyOnThread import com.onesignal.core.internal.application.impl.ApplicationService import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging -import com.onesignal.extensions.RobolectricTest +import com.onesignal.testhelpers.extensions.RobolectricTest import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.kotest.runner.junit4.KotestTestRunner diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/database/OSDatabaseTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/database/OSDatabaseTests.kt index a32d20b438..0500ba704b 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/database/OSDatabaseTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/database/OSDatabaseTests.kt @@ -6,8 +6,8 @@ import com.onesignal.core.internal.database.impl.OSDatabase import com.onesignal.core.internal.database.impl.OneSignalDbContract import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging -import com.onesignal.extensions.RobolectricTest import com.onesignal.session.internal.outcomes.impl.OutcomeTableProvider +import com.onesignal.testhelpers.extensions.RobolectricTest import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.kotest.runner.junit4.KotestTestRunner diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/preferences/PreferencesServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/preferences/PreferencesServiceTests.kt index 1a9065b6da..d1fdb5d13f 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/preferences/PreferencesServiceTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/preferences/PreferencesServiceTests.kt @@ -5,9 +5,9 @@ import androidx.test.core.app.ApplicationProvider import com.onesignal.core.internal.preferences.impl.PreferencesService import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging -import com.onesignal.extensions.RobolectricTest import com.onesignal.mocks.AndroidMockHelper import com.onesignal.mocks.MockHelper +import com.onesignal.testhelpers.extensions.RobolectricTest import io.kotest.assertions.throwables.shouldThrowUnit import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/extensions/RobolectricExtension.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/extensions/RobolectricExtension.kt deleted file mode 100644 index f5353ceab5..0000000000 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/extensions/RobolectricExtension.kt +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Code taken from https://github.com/kotest/kotest-extensions-robolectric with a - * fix in the intercept method. - * - * LICENSE: https://github.com/kotest/kotest-extensions-robolectric/blob/master/LICENSE - */ -package com.onesignal.extensions - -import android.app.Application -import io.kotest.core.extensions.ConstructorExtension -import io.kotest.core.extensions.TestCaseExtension -import io.kotest.core.spec.AutoScan -import io.kotest.core.spec.Spec -import io.kotest.core.test.TestCase -import io.kotest.core.test.TestResult -import org.robolectric.annotation.Config -import kotlin.reflect.KClass -import kotlin.reflect.full.findAnnotation - -/** - * We override TestCaseExtension to configure the Robolectric environment because TestCase intercept - * occurs on the same thread the test is run. This is unfortunate because it is run for every test, - * rather than every spec. But the SpecExtension intercept is run on a different thread. - */ -@AutoScan -internal class RobolectricExtension : ConstructorExtension, TestCaseExtension { - private fun Class<*>.getParentClass(): List> { - if (superclass == null) return listOf() - return listOf(superclass) + superclass.getParentClass() - } - - private fun KClass<*>.getConfig(): Config { - val annotations = - listOf(this.java).plus(this.java.getParentClass()) - .mapNotNull { it.kotlin.findAnnotation() } - .asSequence() - - val application: KClass? = - annotations - .firstOrNull { it.application != KotestDefaultApplication::class }?.application - val sdk: Int? = annotations.firstOrNull { it.sdk != -1 }?.takeUnless { it.sdk == -1 }?.sdk - - return Config.Builder() - .also { builder -> - if (application != null) { - builder.setApplication(application.java) - } - - if (sdk != null) { - builder.setSdk(sdk) - } - }.build() - } - - override fun instantiate(clazz: KClass): Spec? { - clazz.findAnnotation() ?: return null - - return ContainedRobolectricRunner(clazz.getConfig()) - .sdkEnvironment.bootstrappedClass(clazz.java).newInstance() - } - - override suspend fun intercept( - testCase: TestCase, - execute: suspend (TestCase) -> TestResult, - ): TestResult { - // FIXED: Updated code based on https://github.com/kotest/kotest/issues/2717 - val hasRobolectricAnnotation = - testCase.spec::class.annotations.any { annotation -> - annotation.annotationClass.qualifiedName == RobolectricTest::class.qualifiedName - } - - if (!hasRobolectricAnnotation) { - return execute(testCase) - } - - val containedRobolectricRunner = ContainedRobolectricRunner(testCase.spec::class.getConfig()) - containedRobolectricRunner.containedBefore() - val result = execute(testCase) - containedRobolectricRunner.containedAfter() - return result - } -} - -internal class KotestDefaultApplication : Application() - -annotation class RobolectricTest( - val application: KClass = KotestDefaultApplication::class, - val sdk: Int = -1, -) 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 05a05f14fe..efede32e7c 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 @@ -2,7 +2,6 @@ package com.onesignal.session.internal.outcomes import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging -import com.onesignal.extensions.RobolectricTest import com.onesignal.mocks.DatabaseMockHelper import com.onesignal.session.internal.influence.Influence import com.onesignal.session.internal.influence.InfluenceChannel @@ -13,6 +12,7 @@ import com.onesignal.session.internal.outcomes.impl.OutcomeEventsRepository import com.onesignal.session.internal.outcomes.impl.OutcomeEventsTable import com.onesignal.session.internal.outcomes.impl.OutcomeSource import com.onesignal.session.internal.outcomes.impl.OutcomeSourceBody +import com.onesignal.testhelpers.extensions.RobolectricTest import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe @@ -171,6 +171,7 @@ class OutcomeEventsRepositoryTests : FunSpec({ 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", ), @@ -178,6 +179,7 @@ class OutcomeEventsRepositoryTests : FunSpec({ 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", @@ -187,6 +189,7 @@ class OutcomeEventsRepositoryTests : FunSpec({ 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", @@ -205,12 +208,14 @@ class OutcomeEventsRepositoryTests : FunSpec({ 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 @@ -223,6 +228,7 @@ class OutcomeEventsRepositoryTests : FunSpec({ 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 diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/SubscriptionBackendServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/SubscriptionBackendServiceTests.kt index 2fe09b7a4e..e98293db46 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/SubscriptionBackendServiceTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/SubscriptionBackendServiceTests.kt @@ -5,7 +5,6 @@ import com.onesignal.core.internal.http.HttpResponse import com.onesignal.core.internal.http.IHttpClient import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging -import com.onesignal.extensions.RobolectricTest import com.onesignal.user.internal.backend.impl.SubscriptionBackendService import com.onesignal.user.internal.subscriptions.SubscriptionStatus import io.kotest.assertions.throwables.shouldThrowUnit @@ -17,7 +16,8 @@ import io.mockk.coVerify import io.mockk.mockk import org.junit.runner.RunWith -@RobolectricTest +// WARNING: Adding @RobolectricTest will cause JSONObject.map() to stop working +// at runtime. @RunWith(KotestTestRunner::class) class SubscriptionBackendServiceTests : FunSpec({ beforeAny { @@ -29,7 +29,7 @@ class SubscriptionBackendServiceTests : FunSpec({ val aliasLabel = "onesignal_id" val aliasValue = "11111111-1111-1111-1111-111111111111" val spyHttpClient = mockk() - coEvery { spyHttpClient.post(any(), any()) } returns HttpResponse(202, "{id: \"subscriptionId\"}") + coEvery { spyHttpClient.post(any(), any()) } returns HttpResponse(202, "{ \"subscription\": { id: \"subscriptionId\" } }") val subscriptionBackendService = SubscriptionBackendService(spyHttpClient) // When @@ -48,13 +48,14 @@ class SubscriptionBackendServiceTests : FunSpec({ response shouldBe "subscriptionId" coVerify { spyHttpClient.post( - "apps/appId/user/by/$aliasLabel/$aliasValue/subscription", + "apps/appId/users/by/$aliasLabel/$aliasValue/subscriptions", withArg { - it.has("id") shouldBe false - it.getString("type") shouldBe "AndroidPush" - it.getString("token") shouldBe "pushToken" - it.getBoolean("enabled") shouldBe true - it.getInt("notification_types") shouldBe 1 + val sub = it.getJSONObject("subscription") + sub.has("id") shouldBe false + sub.getString("type") shouldBe "AndroidPush" + sub.getString("token") shouldBe "pushToken" + sub.getBoolean("enabled") shouldBe true + sub.getInt("notification_types") shouldBe 1 }, ) } @@ -92,13 +93,14 @@ class SubscriptionBackendServiceTests : FunSpec({ // Then coVerify { spyHttpClient.post( - "apps/appId/user/by/$aliasLabel/$aliasValue/subscription", + "apps/appId/users/by/$aliasLabel/$aliasValue/subscriptions", withArg { - it.has("id") shouldBe false - it.getString("type") shouldBe "AndroidPush" - it.getString("token") shouldBe "pushToken" - it.getBoolean("enabled") shouldBe true - it.getInt("notification_types") shouldBe 1 + val sub = it.getJSONObject("subscription") + sub.has("id") shouldBe false + sub.getString("type") shouldBe "AndroidPush" + sub.getString("token") shouldBe "pushToken" + sub.getBoolean("enabled") shouldBe true + sub.getInt("notification_types") shouldBe 1 }, ) } diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/LoginUserOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/LoginUserOperationExecutorTests.kt index 75a2187841..1a3b9fc3c9 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/LoginUserOperationExecutorTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/LoginUserOperationExecutorTests.kt @@ -4,9 +4,9 @@ import com.onesignal.common.exceptions.BackendException import com.onesignal.core.internal.operations.ExecutionResponse import com.onesignal.core.internal.operations.ExecutionResult import com.onesignal.core.internal.operations.Operation -import com.onesignal.extensions.RobolectricTest import com.onesignal.mocks.AndroidMockHelper import com.onesignal.mocks.MockHelper +import com.onesignal.testhelpers.extensions.RobolectricTest import com.onesignal.user.internal.backend.CreateUserResponse import com.onesignal.user.internal.backend.IUserBackendService import com.onesignal.user.internal.backend.IdentityConstants @@ -129,7 +129,7 @@ class LoginUserOperationExecutorTests : FunSpec({ val response = loginUserOperationExecutor.execute(operations) // Then - response.result shouldBe ExecutionResult.FAIL_NORETRY + response.result shouldBe ExecutionResult.FAIL_PAUSE_OPREPO coVerify(exactly = 1) { mockUserBackendService.createUser(appId, mapOf(), any(), any()) } } @@ -392,13 +392,18 @@ class LoginUserOperationExecutorTests : FunSpec({ coVerify(exactly = 1) { mockUserBackendService.createUser( appId, - mapOf("aliasLabel1" to "aliasValue1-2"), + // Aliases omitted intentionally in PR #1794, to avoid failed create user calls. + // - Ideally we batch as much as possible into the create as most of the time it + // should be successful. Then when there are failures omit the "bad data" and try + // again, however this is more complex which is why it wasn't done initially. + mapOf(), withArg { it.count() shouldBe 1 - it[0].type shouldBe SubscriptionObjectType.ANDROID_PUSH - it[0].enabled shouldBe true - it[0].token shouldBe "pushToken2" - it[0].notificationTypes shouldBe SubscriptionStatus.SUBSCRIBED + val subscription = it[0] + subscription.type shouldBe SubscriptionObjectType.ANDROID_PUSH + subscription.enabled shouldBe true + subscription.token shouldBe "pushToken2" + SubscriptionStatus.fromInt(subscription.notificationTypes!!) shouldBe SubscriptionStatus.SUBSCRIBED }, any(), ) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/SubscriptionOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/SubscriptionOperationExecutorTests.kt index ea38d8cf6a..9b31d48b27 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/SubscriptionOperationExecutorTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/SubscriptionOperationExecutorTests.kt @@ -3,9 +3,9 @@ package com.onesignal.user.internal.operations import com.onesignal.common.exceptions.BackendException import com.onesignal.core.internal.operations.ExecutionResult import com.onesignal.core.internal.operations.Operation -import com.onesignal.extensions.RobolectricTest import com.onesignal.mocks.AndroidMockHelper import com.onesignal.mocks.MockHelper +import com.onesignal.testhelpers.extensions.RobolectricTest import com.onesignal.user.internal.backend.ISubscriptionBackendService import com.onesignal.user.internal.backend.IdentityConstants import com.onesignal.user.internal.backend.SubscriptionObjectType @@ -149,6 +149,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ val mockSubscriptionsModelStore = mockk() val mockBuildUserService = mockk() + every { mockBuildUserService.getRebuildOperationsIfCurrentUser(any(), any()) } answers { null } val subscriptionOperationExecutor = SubscriptionOperationExecutor( @@ -350,7 +351,6 @@ class SubscriptionOperationExecutorTests : FunSpec({ // Then response.result shouldBe ExecutionResult.SUCCESS - subscriptionModel1.address shouldBe "pushToken3" coVerify(exactly = 1) { mockSubscriptionBackendService.updateSubscription( appId, @@ -539,7 +539,9 @@ class SubscriptionOperationExecutorTests : FunSpec({ coVerify(exactly = 1) { mockSubscriptionBackendService.deleteSubscription(appId, remoteSubscriptionId) } } - test("delete subscription fails without retry when there is a backend error") { + // If we get a 404 then the subscription has already been deleted, + // so we count it as successful + test("delete subscription is successful if there is a 404") { // Given val mockSubscriptionBackendService = mockk() coEvery { mockSubscriptionBackendService.deleteSubscription(any(), any()) } throws BackendException(404) @@ -566,7 +568,7 @@ class SubscriptionOperationExecutorTests : FunSpec({ val response = subscriptionOperationExecutor.execute(operations) // Then - response.result shouldBe ExecutionResult.FAIL_NORETRY + response.result shouldBe ExecutionResult.SUCCESS coVerify(exactly = 1) { mockSubscriptionBackendService.deleteSubscription(appId, remoteSubscriptionId) } } }) diff --git a/OneSignalSDK/onesignal/in-app-messages/build.gradle b/OneSignalSDK/onesignal/in-app-messages/build.gradle index ced85d59d3..e5cf77856e 100644 --- a/OneSignalSDK/onesignal/in-app-messages/build.gradle +++ b/OneSignalSDK/onesignal/in-app-messages/build.gradle @@ -78,6 +78,7 @@ dependencies { } } + testImplementation(project(':OneSignal:testhelpers')) testImplementation("junit:junit:$junitVersion") testImplementation("io.kotest:kotest-runner-junit4:$kotestVersion") testImplementation("io.kotest:kotest-runner-junit4-jvm:$kotestVersion") diff --git a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/extensions/ContainedRobolectricRunner.kt b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/extensions/ContainedRobolectricRunner.kt deleted file mode 100644 index 123fe4a883..0000000000 --- a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/extensions/ContainedRobolectricRunner.kt +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Code taken from https://github.com/kotest/kotest-extensions-robolectric with no changes. - * - * LICENSE: https://github.com/kotest/kotest-extensions-robolectric/blob/master/LICENSE - */ -package com.onesignal.inAppMessages.extensions - -import org.junit.runners.model.FrameworkMethod -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.internal.bytecode.InstrumentationConfiguration -import org.robolectric.pluginapi.config.ConfigurationStrategy -import org.robolectric.plugins.ConfigConfigurer -import java.lang.reflect.Method - -internal class ContainedRobolectricRunner( - private val config: Config?, -) : RobolectricTestRunner(PlaceholderTest::class.java, injector) { - private val placeHolderMethod: FrameworkMethod = children[0] - val sdkEnvironment = - getSandbox(placeHolderMethod).also { - configureSandbox(it, placeHolderMethod) - } - private val bootStrapMethod = - sdkEnvironment.bootstrappedClass(testClass.javaClass) - .getMethod(PlaceholderTest::bootStrapMethod.name) - - fun containedBefore() { - Thread.currentThread().contextClassLoader = sdkEnvironment.robolectricClassLoader - super.beforeTest(sdkEnvironment, placeHolderMethod, bootStrapMethod) - } - - fun containedAfter() { - super.afterTest(placeHolderMethod, bootStrapMethod) - super.finallyAfterTest(placeHolderMethod) - Thread.currentThread().contextClassLoader = ContainedRobolectricRunner::class.java.classLoader - } - - override fun createClassLoaderConfig(method: FrameworkMethod?): InstrumentationConfiguration { - return InstrumentationConfiguration.Builder(super.createClassLoaderConfig(method)) - .doNotAcquirePackage("io.kotest") - .build() - } - - override fun getConfig(method: Method?): Config { - val defaultConfiguration = - injector.getInstance(ConfigurationStrategy::class.java) - .getConfig(testClass.javaClass, method) - - if (config != null) { - val configConfigurer = injector.getInstance(ConfigConfigurer::class.java) - return configConfigurer.merge(defaultConfiguration[Config::class.java], config) - } - - return super.getConfig(method) - } - - class PlaceholderTest { - @org.junit.Test - fun testPlaceholder() { - } - - fun bootStrapMethod() { - } - } - - companion object { - private val injector = defaultInjector().build() - } -} diff --git a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/extensions/RobolectricExtension.kt b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/extensions/RobolectricExtension.kt deleted file mode 100644 index 5e02113ecd..0000000000 --- a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/extensions/RobolectricExtension.kt +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Code taken from https://github.com/kotest/kotest-extensions-robolectric with a - * fix in the intercept method. - * - * LICENSE: https://github.com/kotest/kotest-extensions-robolectric/blob/master/LICENSE - */ -package com.onesignal.inAppMessages.extensions - -import android.app.Application -import io.kotest.core.extensions.ConstructorExtension -import io.kotest.core.extensions.TestCaseExtension -import io.kotest.core.spec.AutoScan -import io.kotest.core.spec.Spec -import io.kotest.core.test.TestCase -import io.kotest.core.test.TestResult -import org.robolectric.annotation.Config -import kotlin.reflect.KClass -import kotlin.reflect.full.findAnnotation - -/** - * We override TestCaseExtension to configure the Robolectric environment because TestCase intercept - * occurs on the same thread the test is run. This is unfortunate because it is run for every test, - * rather than every spec. But the SpecExtension intercept is run on a different thread. - */ -@AutoScan -internal class RobolectricExtension : ConstructorExtension, TestCaseExtension { - private fun Class<*>.getParentClass(): List> { - if (superclass == null) return listOf() - return listOf(superclass) + superclass.getParentClass() - } - - private fun KClass<*>.getConfig(): Config { - val configAnnotations = - listOf(this.java).plus(this.java.getParentClass()) - .mapNotNull { it.kotlin.findAnnotation() } - .asSequence() - - val configAnnotation = configAnnotations.firstOrNull() - - if (configAnnotation != null) { - return Config.Builder(configAnnotation).build() - } - - val robolectricTestAnnotations = - listOf(this.java).plus(this.java.getParentClass()) - .mapNotNull { it.kotlin.findAnnotation() } - .asSequence() - - val application: KClass? = - robolectricTestAnnotations - .firstOrNull { it.application != KotestDefaultApplication::class }?.application - val sdk: Int? = robolectricTestAnnotations.firstOrNull { it.sdk != -1 }?.takeUnless { it.sdk == -1 }?.sdk - - return Config.Builder() - .also { builder -> - if (application != null) { - builder.setApplication(application.java) - } - - if (sdk != null) { - builder.setSdk(sdk) - } - }.build() - } - - override fun instantiate(clazz: KClass): Spec? { - clazz.findAnnotation() ?: return null - - return ContainedRobolectricRunner(clazz.getConfig()) - .sdkEnvironment.bootstrappedClass(clazz.java).newInstance() - } - - override suspend fun intercept( - testCase: TestCase, - execute: suspend (TestCase) -> TestResult, - ): TestResult { - // FIXED: Updated code based on https://github.com/kotest/kotest/issues/2717 - val hasRobolectricAnnotation = - testCase.spec::class.annotations.any { annotation -> - annotation.annotationClass.qualifiedName == RobolectricTest::class.qualifiedName - } - - if (!hasRobolectricAnnotation) { - return execute(testCase) - } - - val containedRobolectricRunner = ContainedRobolectricRunner(testCase.spec::class.getConfig()) - containedRobolectricRunner.containedBefore() - val result = execute(testCase) - containedRobolectricRunner.containedAfter() - return result - } -} - -internal class KotestDefaultApplication : Application() - -annotation class RobolectricTest( - val application: KClass = KotestDefaultApplication::class, - val sdk: Int = -1, -) diff --git a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/preview/InAppMessagePreviewHandlerTests.kt b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/preview/InAppMessagePreviewHandlerTests.kt index 572bba363b..462a7c5429 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/preview/InAppMessagePreviewHandlerTests.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/preview/InAppMessagePreviewHandlerTests.kt @@ -3,13 +3,13 @@ package com.onesignal.inAppMessages.internal.preview import android.app.Activity import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging -import com.onesignal.inAppMessages.extensions.RobolectricTest import com.onesignal.inAppMessages.internal.display.IInAppDisplayer import com.onesignal.inAppMessages.internal.state.InAppStateService import com.onesignal.inAppMessages.mocks.MockHelper import com.onesignal.notifications.internal.INotificationActivityOpener import com.onesignal.notifications.internal.display.INotificationDisplayer import com.onesignal.notifications.internal.lifecycle.INotificationLifecycleService +import com.onesignal.testhelpers.extensions.RobolectricTest import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.kotest.runner.junit4.KotestTestRunner diff --git a/OneSignalSDK/onesignal/location/build.gradle b/OneSignalSDK/onesignal/location/build.gradle index 00f31eb7bf..5c4990d96f 100644 --- a/OneSignalSDK/onesignal/location/build.gradle +++ b/OneSignalSDK/onesignal/location/build.gradle @@ -77,6 +77,7 @@ dependencies { // KEEP as "compileOnly", so OneSignal isn't a direct dependency in the POM file. compileOnly "com.huawei.hms:location:$huaweiHMSLocationVersion" + testImplementation(project(':OneSignal:testhelpers')) testImplementation("junit:junit:$junitVersion") testImplementation("io.kotest:kotest-runner-junit4:$kotestVersion") testImplementation("io.kotest:kotest-runner-junit4-jvm:$kotestVersion") diff --git a/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/LocationModule.kt b/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/LocationModule.kt index 6a4a3bdcd4..3cb897e14e 100644 --- a/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/LocationModule.kt +++ b/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/LocationModule.kt @@ -12,8 +12,10 @@ import com.onesignal.location.internal.capture.ILocationCapturer import com.onesignal.location.internal.capture.impl.LocationCapturer import com.onesignal.location.internal.common.LocationUtils import com.onesignal.location.internal.controller.ILocationController +import com.onesignal.location.internal.controller.impl.FusedLocationApiWrapperImpl import com.onesignal.location.internal.controller.impl.GmsLocationController import com.onesignal.location.internal.controller.impl.HmsLocationController +import com.onesignal.location.internal.controller.impl.IFusedLocationApiWrapper import com.onesignal.location.internal.controller.impl.NullLocationController import com.onesignal.location.internal.permissions.LocationPermissionController import com.onesignal.location.internal.preferences.ILocationPreferencesService @@ -25,11 +27,15 @@ internal class LocationModule : IModule { .provides() .provides() + builder.register().provides() builder.register { val deviceService = it.getService(IDeviceService::class.java) val service = if (deviceService.isAndroidDeviceType && LocationUtils.hasGMSLocationLibrary()) { - GmsLocationController(it.getService(IApplicationService::class.java)) + GmsLocationController( + it.getService(IApplicationService::class.java), + it.getService(IFusedLocationApiWrapper::class.java), + ) } else if (deviceService.isHuaweiDeviceType && LocationUtils.hasHMSLocationLibrary()) { HmsLocationController(it.getService(IApplicationService::class.java)) } else { diff --git a/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/controller/impl/FusedLocationApiWrapperImpl.kt b/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/controller/impl/FusedLocationApiWrapperImpl.kt new file mode 100644 index 0000000000..b35cbfcb4c --- /dev/null +++ b/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/controller/impl/FusedLocationApiWrapperImpl.kt @@ -0,0 +1,46 @@ +package com.onesignal.location.internal.controller.impl + +import android.location.Location +import android.os.Looper +import com.google.android.gms.common.api.GoogleApiClient +import com.google.android.gms.location.LocationListener +import com.google.android.gms.location.LocationRequest +import com.google.android.gms.location.LocationServices +import com.onesignal.debug.internal.logging.Logging + +internal class FusedLocationApiWrapperImpl : IFusedLocationApiWrapper { + override fun cancelLocationUpdates( + googleApiClient: GoogleApiClient, + locationListener: LocationListener, + ) { + if (googleApiClient.isConnected) { + LocationServices.FusedLocationApi.removeLocationUpdates(googleApiClient, locationListener) + } else { + Logging.warn("GoogleApiClient is not connected. Unable to cancel location updates.") + } + } + + override fun requestLocationUpdates( + googleApiClient: GoogleApiClient, + locationRequest: LocationRequest, + locationListener: LocationListener, + ) { + try { + if (Looper.myLooper() == null) { + Looper.prepare() + } + if (googleApiClient.isConnected) { + LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, locationListener) + } + } catch (t: Throwable) { + Logging.warn("FusedLocationApi.requestLocationUpdates failed!", t) + } + } + + override fun getLastLocation(googleApiClient: GoogleApiClient): Location? { + if (googleApiClient.isConnected) { + return LocationServices.FusedLocationApi.getLastLocation(googleApiClient) + } + return null + } +} diff --git a/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/controller/impl/GmsLocationController.kt b/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/controller/impl/GmsLocationController.kt index e40923a88a..1c489f1670 100644 --- a/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/controller/impl/GmsLocationController.kt +++ b/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/controller/impl/GmsLocationController.kt @@ -4,7 +4,6 @@ import android.location.Location import android.os.Bundle import android.os.Handler import android.os.HandlerThread -import android.os.Looper import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.api.GoogleApiClient import com.google.android.gms.location.LocationListener @@ -29,6 +28,7 @@ import java.io.Closeable internal class GmsLocationController( private val _applicationService: IApplicationService, + private val _fusedLocationApiWrapper: IFusedLocationApiWrapper, ) : ILocationController { private val locationHandlerThread = LocationHandlerThread() private val startStopMutex = Mutex() @@ -75,14 +75,14 @@ internal class GmsLocationController( if (result?.isSuccess == true) { if (lastLocation == null) { - var lastLocation = FusedLocationApiWrapper.getLastLocation(googleApiClient) + var lastLocation = _fusedLocationApiWrapper.getLastLocation(googleApiClient) if (lastLocation != null) { setLocationAndFire(lastLocation) } } // only after the connect do we save - self.locationUpdateListener = LocationUpdateListener(_applicationService, self, proxyGoogleApiClient.realInstance) + self.locationUpdateListener = LocationUpdateListener(_applicationService, self, proxyGoogleApiClient.realInstance, _fusedLocationApiWrapper) self.googleApiClient = proxyGoogleApiClient wasSuccessful = true } else { @@ -121,7 +121,7 @@ internal class GmsLocationController( override fun getLastLocation(): Location? { val apiInstance = googleApiClient?.realInstance ?: return null - return FusedLocationApiWrapper.getLastLocation(apiInstance) + return _fusedLocationApiWrapper.getLastLocation(apiInstance) } override fun subscribe(handler: ILocationUpdatedHandler) = event.subscribe(handler) @@ -162,6 +162,7 @@ internal class GmsLocationController( private val _applicationService: IApplicationService, private val _parent: GmsLocationController, private val googleApiClient: GoogleApiClient, + private val _fusedLocationApiWrapper: IFusedLocationApiWrapper, ) : LocationListener, IApplicationLifecycleHandler, Closeable { private var hasExistingRequest = false @@ -188,7 +189,7 @@ internal class GmsLocationController( _applicationService.removeApplicationLifecycleHandler(this) if (hasExistingRequest) { - FusedLocationApiWrapper.cancelLocationUpdates(googleApiClient, this) + _fusedLocationApiWrapper.cancelLocationUpdates(googleApiClient, this) } } @@ -199,7 +200,7 @@ internal class GmsLocationController( } if (hasExistingRequest) { - FusedLocationApiWrapper.cancelLocationUpdates(googleApiClient, this) + _fusedLocationApiWrapper.cancelLocationUpdates(googleApiClient, this) } val updateInterval = @@ -216,7 +217,7 @@ internal class GmsLocationController( .setMaxWaitTime((updateInterval * 1.5).toLong()) .setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY) Logging.debug("GMSLocationController GoogleApiClient requestLocationUpdates!") - FusedLocationApiWrapper.requestLocationUpdates(googleApiClient, locationRequest, this) + _fusedLocationApiWrapper.requestLocationUpdates(googleApiClient, locationRequest, this) hasExistingRequest = true } @@ -226,43 +227,6 @@ internal class GmsLocationController( } } - internal object FusedLocationApiWrapper { - fun cancelLocationUpdates( - googleApiClient: GoogleApiClient, - locationListener: LocationListener, - ) { - if (googleApiClient.isConnected) { - LocationServices.FusedLocationApi.removeLocationUpdates(googleApiClient, locationListener) - } else { - Logging.warn("GoogleApiClient is not connected. Unable to cancel location updates.") - } - } - - fun requestLocationUpdates( - googleApiClient: GoogleApiClient, - locationRequest: LocationRequest, - locationListener: LocationListener, - ) { - try { - if (Looper.myLooper() == null) { - Looper.prepare() - } - if (googleApiClient.isConnected) { - LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, locationListener) - } - } catch (t: Throwable) { - Logging.warn("FusedLocationApi.requestLocationUpdates failed!", t) - } - } - - fun getLastLocation(googleApiClient: GoogleApiClient): Location? { - if (googleApiClient.isConnected) { - return LocationServices.FusedLocationApi.getLastLocation(googleApiClient) - } - return null - } - } - protected class LocationHandlerThread internal constructor() : HandlerThread("OSH_LocationHandlerThread") { var mHandler: Handler diff --git a/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/controller/impl/IFusedLocationApiWrapper.kt b/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/controller/impl/IFusedLocationApiWrapper.kt new file mode 100644 index 0000000000..1854bac323 --- /dev/null +++ b/OneSignalSDK/onesignal/location/src/main/java/com/onesignal/location/internal/controller/impl/IFusedLocationApiWrapper.kt @@ -0,0 +1,21 @@ +package com.onesignal.location.internal.controller.impl + +import android.location.Location +import com.google.android.gms.common.api.GoogleApiClient +import com.google.android.gms.location.LocationListener +import com.google.android.gms.location.LocationRequest + +internal interface IFusedLocationApiWrapper { + fun cancelLocationUpdates( + googleApiClient: GoogleApiClient, + locationListener: LocationListener, + ) + + fun requestLocationUpdates( + googleApiClient: GoogleApiClient, + locationRequest: LocationRequest, + locationListener: LocationListener, + ) + + fun getLastLocation(googleApiClient: GoogleApiClient): Location? +} diff --git a/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/extensions/ContainedRobolectricRunner.kt b/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/extensions/ContainedRobolectricRunner.kt deleted file mode 100644 index fa722e93ad..0000000000 --- a/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/extensions/ContainedRobolectricRunner.kt +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Code taken from https://github.com/kotest/kotest-extensions-robolectric with no changes. - * - * LICENSE: https://github.com/kotest/kotest-extensions-robolectric/blob/master/LICENSE - */ -package com.onesignal.notifications.extensions - -import org.junit.runners.model.FrameworkMethod -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.internal.bytecode.InstrumentationConfiguration -import org.robolectric.pluginapi.config.ConfigurationStrategy -import org.robolectric.plugins.ConfigConfigurer -import java.lang.reflect.Method - -internal class ContainedRobolectricRunner( - private val config: Config?, -) : RobolectricTestRunner(PlaceholderTest::class.java, injector) { - private val placeHolderMethod: FrameworkMethod = children[0] - val sdkEnvironment = - getSandbox(placeHolderMethod).also { - configureSandbox(it, placeHolderMethod) - } - private val bootStrapMethod = - sdkEnvironment.bootstrappedClass(testClass.javaClass) - .getMethod(PlaceholderTest::bootStrapMethod.name) - - fun containedBefore() { - Thread.currentThread().contextClassLoader = sdkEnvironment.robolectricClassLoader - super.beforeTest(sdkEnvironment, placeHolderMethod, bootStrapMethod) - } - - fun containedAfter() { - super.afterTest(placeHolderMethod, bootStrapMethod) - super.finallyAfterTest(placeHolderMethod) - Thread.currentThread().contextClassLoader = ContainedRobolectricRunner::class.java.classLoader - } - - override fun createClassLoaderConfig(method: FrameworkMethod?): InstrumentationConfiguration { - return InstrumentationConfiguration.Builder(super.createClassLoaderConfig(method)) - .doNotAcquirePackage("io.kotest") - .build() - } - - override fun getConfig(method: Method?): Config { - val defaultConfiguration = - injector.getInstance(ConfigurationStrategy::class.java) - .getConfig(testClass.javaClass, method) - - if (config != null) { - val configConfigurer = injector.getInstance(ConfigConfigurer::class.java) - return configConfigurer.merge(defaultConfiguration[Config::class.java], config) - } - - return super.getConfig(method) - } - - class PlaceholderTest { - @org.junit.Test - fun testPlaceholder() { - } - - fun bootStrapMethod() { - } - } - - companion object { - private val injector = defaultInjector().build() - } -} diff --git a/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/internal/background/LocationBackgroundServiceTests.kt b/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/internal/background/LocationBackgroundServiceTests.kt index eb65a886b0..a300b68565 100644 --- a/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/internal/background/LocationBackgroundServiceTests.kt +++ b/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/internal/background/LocationBackgroundServiceTests.kt @@ -11,7 +11,7 @@ import com.onesignal.location.internal.common.LocationConstants import com.onesignal.location.internal.preferences.ILocationPreferencesService import com.onesignal.mocks.AndroidMockHelper import com.onesignal.mocks.MockHelper -import com.onesignal.notifications.extensions.RobolectricTest +import com.onesignal.testhelpers.extensions.RobolectricTest import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.kotest.runner.junit4.KotestTestRunner diff --git a/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/internal/controller/GmsLocationControllerTests.kt b/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/internal/controller/GmsLocationControllerTests.kt index 39cccfe6e8..6cf711be47 100644 --- a/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/internal/controller/GmsLocationControllerTests.kt +++ b/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/internal/controller/GmsLocationControllerTests.kt @@ -4,11 +4,11 @@ import android.location.Location import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.location.internal.controller.impl.GmsLocationController -import com.onesignal.location.shadows.ShadowFusedLocationProviderApi +import com.onesignal.location.mocks.FusedLocationApiWrapperMock import com.onesignal.location.shadows.ShadowGoogleApiClient import com.onesignal.location.shadows.ShadowGoogleApiClientBuilder import com.onesignal.mocks.AndroidMockHelper -import com.onesignal.notifications.extensions.RobolectricTest +import com.onesignal.testhelpers.extensions.RobolectricTest import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe @@ -37,11 +37,11 @@ class GmsLocationControllerTests : FunSpec({ val location = Location("TEST_PROVIDER") location.latitude = 123.45 location.longitude = 678.91 + val fusedLocationApiWrapperMock = FusedLocationApiWrapperMock(listOf(location)) - ShadowFusedLocationProviderApi.injectToLocationServices(listOf(location)) val applicationService = AndroidMockHelper.applicationService() every { applicationService.isInForeground } returns true - val gmsLocationController = GmsLocationController(applicationService) + val gmsLocationController = GmsLocationController(applicationService, fusedLocationApiWrapperMock) val spyLocationUpdateHandler = spyk() gmsLocationController.subscribe(spyLocationUpdateHandler) @@ -71,11 +71,11 @@ class GmsLocationControllerTests : FunSpec({ val location2 = Location("TEST_PROVIDER") location2.latitude = 678.91 location2.longitude = 123.45 + val fusedLocationApiWrapperMock = FusedLocationApiWrapperMock(listOf(location1, location2)) - ShadowFusedLocationProviderApi.injectToLocationServices(listOf(location1, location2)) val applicationService = AndroidMockHelper.applicationService() every { applicationService.isInForeground } returns true - val gmsLocationController = GmsLocationController(applicationService) + val gmsLocationController = GmsLocationController(applicationService, fusedLocationApiWrapperMock) val spyLocationUpdateHandler = spyk() gmsLocationController.subscribe(spyLocationUpdateHandler) @@ -114,10 +114,10 @@ class GmsLocationControllerTests : FunSpec({ location2.latitude = 678.91 location2.longitude = 123.45 - ShadowFusedLocationProviderApi.injectToLocationServices(listOf(location1, location2)) + val fusedLocationApiWrapperMock = FusedLocationApiWrapperMock(listOf(location1, location2)) val applicationService = AndroidMockHelper.applicationService() every { applicationService.isInForeground } returns true - val gmsLocationController = GmsLocationController(applicationService) + val gmsLocationController = GmsLocationController(applicationService, fusedLocationApiWrapperMock) val spyLocationUpdateHandler = spyk() gmsLocationController.subscribe(spyLocationUpdateHandler) @@ -152,10 +152,10 @@ class GmsLocationControllerTests : FunSpec({ location2.latitude = 678.91 location2.longitude = 123.45 - ShadowFusedLocationProviderApi.injectToLocationServices(listOf(location1, location2)) + val fusedLocationApiWrapperMock = FusedLocationApiWrapperMock(listOf(location1, location2)) val applicationService = AndroidMockHelper.applicationService() every { applicationService.isInForeground } returns true - val gmsLocationController = GmsLocationController(applicationService) + val gmsLocationController = GmsLocationController(applicationService, fusedLocationApiWrapperMock) val spyLocationUpdateHandler = spyk() gmsLocationController.subscribe(spyLocationUpdateHandler) diff --git a/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/internal/permissions/LocationPermissionControllerTests.kt b/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/internal/permissions/LocationPermissionControllerTests.kt index 1b5e132596..713253a18a 100644 --- a/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/internal/permissions/LocationPermissionControllerTests.kt +++ b/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/internal/permissions/LocationPermissionControllerTests.kt @@ -4,7 +4,7 @@ import com.onesignal.core.internal.permissions.IRequestPermissionService import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.mocks.AndroidMockHelper -import com.onesignal.notifications.extensions.RobolectricTest +import com.onesignal.testhelpers.extensions.RobolectricTest import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.longs.shouldBeGreaterThan import io.kotest.matchers.shouldBe diff --git a/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/mocks/FusedLocationApiWrapperMock.kt b/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/mocks/FusedLocationApiWrapperMock.kt new file mode 100644 index 0000000000..89961b7a65 --- /dev/null +++ b/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/mocks/FusedLocationApiWrapperMock.kt @@ -0,0 +1,32 @@ +package com.onesignal.location.mocks + +import android.location.Location +import com.google.android.gms.common.api.GoogleApiClient +import com.google.android.gms.location.LocationListener +import com.google.android.gms.location.LocationRequest +import com.onesignal.location.internal.controller.impl.IFusedLocationApiWrapper +import java.util.LinkedList +import java.util.Queue + +internal class FusedLocationApiWrapperMock(locationsList: List) : IFusedLocationApiWrapper { + private val locations: Queue + + init { + this.locations = LinkedList(locationsList) + } + + override fun cancelLocationUpdates( + googleApiClient: GoogleApiClient, + locationListener: LocationListener, + ) {} + + override fun requestLocationUpdates( + googleApiClient: GoogleApiClient, + locationRequest: LocationRequest, + locationListener: LocationListener, + ) {} + + override fun getLastLocation(googleApiClient: GoogleApiClient): Location? { + return locations.remove() + } +} diff --git a/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/shadows/ShadowFusedLocationProviderApi.kt b/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/shadows/ShadowFusedLocationProviderApi.kt deleted file mode 100644 index 395cb2b6e1..0000000000 --- a/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/shadows/ShadowFusedLocationProviderApi.kt +++ /dev/null @@ -1,161 +0,0 @@ -/** - * Modified MIT License - * - * Copyright 2017 OneSignal - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * 1. The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * 2. All copies of substantial portions of the Software may only be used in connection - * with services provided by OneSignal. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package com.onesignal.location.shadows - -import android.app.PendingIntent -import android.location.Location -import android.os.Looper -import com.google.android.gms.common.api.GoogleApiClient -import com.google.android.gms.common.api.PendingResult -import com.google.android.gms.common.api.ResultCallback -import com.google.android.gms.common.api.Status -import com.google.android.gms.location.FusedLocationProviderApi -import com.google.android.gms.location.LocationAvailability -import com.google.android.gms.location.LocationCallback -import com.google.android.gms.location.LocationListener -import com.google.android.gms.location.LocationRequest -import com.google.android.gms.location.LocationServices -import java.lang.reflect.Field -import java.lang.reflect.Modifier -import java.util.concurrent.TimeUnit - -class ShadowFusedLocationProviderApi : FusedLocationProviderApi { - val pendingResult = - object : PendingResult() { - override fun await(): Status = Status.RESULT_SUCCESS - - override fun await( - p0: Long, - p1: TimeUnit, - ): Status = Status.RESULT_SUCCESS - - override fun cancel() { } - - override fun isCanceled(): Boolean = false - - override fun setResultCallback(p0: ResultCallback) { } - - override fun setResultCallback( - p0: ResultCallback, - p1: Long, - p2: TimeUnit, - ) {} - } - - override fun getLastLocation(p0: GoogleApiClient): Location { - if (locations != null) { - val location = locations!![index] - index++ - if (index >= locations!!.count()) { - index = 0 - } - - return location - } - - val location = Location("TEST_PROVIDER") - location.latitude = 123.45 - location.longitude = 678.91 - return location - } - - override fun getLocationAvailability(p0: GoogleApiClient): LocationAvailability = throw Exception() - - override fun requestLocationUpdates( - p0: GoogleApiClient, - p1: LocationRequest, - p2: LocationListener, - ): PendingResult = pendingResult - - override fun requestLocationUpdates( - p0: GoogleApiClient, - p1: LocationRequest, - p2: LocationListener, - p3: Looper, - ): PendingResult = pendingResult - - override fun requestLocationUpdates( - p0: GoogleApiClient, - p1: LocationRequest, - p2: LocationCallback, - p3: Looper, - ): PendingResult = pendingResult - - override fun requestLocationUpdates( - p0: GoogleApiClient, - p1: LocationRequest, - p2: PendingIntent, - ): PendingResult = pendingResult - - override fun removeLocationUpdates( - p0: GoogleApiClient, - p1: LocationListener, - ): PendingResult = pendingResult - - override fun removeLocationUpdates( - p0: GoogleApiClient, - p1: PendingIntent, - ): PendingResult = pendingResult - - override fun removeLocationUpdates( - p0: GoogleApiClient, - p1: LocationCallback, - ): PendingResult = pendingResult - - override fun setMockMode( - p0: GoogleApiClient, - p1: Boolean, - ): PendingResult = pendingResult - - override fun setMockLocation( - p0: GoogleApiClient, - p1: Location, - ): PendingResult = pendingResult - - override fun flushLocations(p0: GoogleApiClient): PendingResult = pendingResult - - companion object { - private var locations: List? = null - private var index: Int = 0 - - fun injectToLocationServices(locations: List) { - this.index = 0 - this.locations = locations - val currentFused = LocationServices.FusedLocationApi - val newFused = ShadowFusedLocationProviderApi() - - val field = LocationServices::class.java.getDeclaredField("FusedLocationApi") - field.isAccessible = true - - val modifiersField: Field = Field::class.java.getDeclaredField("modifiers") - modifiersField.isAccessible = true - modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv()) - - field.set(null, newFused) - } - } -} diff --git a/OneSignalSDK/onesignal/notifications/build.gradle b/OneSignalSDK/onesignal/notifications/build.gradle index 10da2da4f7..c31add7d5c 100644 --- a/OneSignalSDK/onesignal/notifications/build.gradle +++ b/OneSignalSDK/onesignal/notifications/build.gradle @@ -90,6 +90,7 @@ dependencies { } } + testImplementation(project(':OneSignal:testhelpers')) testImplementation("junit:junit:$junitVersion") testImplementation("io.kotest:kotest-runner-junit4:$kotestVersion") testImplementation("io.kotest:kotest-runner-junit4-jvm:$kotestVersion") diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/extensions/ContainedRobolectricRunner.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/extensions/ContainedRobolectricRunner.kt deleted file mode 100644 index fa722e93ad..0000000000 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/extensions/ContainedRobolectricRunner.kt +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Code taken from https://github.com/kotest/kotest-extensions-robolectric with no changes. - * - * LICENSE: https://github.com/kotest/kotest-extensions-robolectric/blob/master/LICENSE - */ -package com.onesignal.notifications.extensions - -import org.junit.runners.model.FrameworkMethod -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.internal.bytecode.InstrumentationConfiguration -import org.robolectric.pluginapi.config.ConfigurationStrategy -import org.robolectric.plugins.ConfigConfigurer -import java.lang.reflect.Method - -internal class ContainedRobolectricRunner( - private val config: Config?, -) : RobolectricTestRunner(PlaceholderTest::class.java, injector) { - private val placeHolderMethod: FrameworkMethod = children[0] - val sdkEnvironment = - getSandbox(placeHolderMethod).also { - configureSandbox(it, placeHolderMethod) - } - private val bootStrapMethod = - sdkEnvironment.bootstrappedClass(testClass.javaClass) - .getMethod(PlaceholderTest::bootStrapMethod.name) - - fun containedBefore() { - Thread.currentThread().contextClassLoader = sdkEnvironment.robolectricClassLoader - super.beforeTest(sdkEnvironment, placeHolderMethod, bootStrapMethod) - } - - fun containedAfter() { - super.afterTest(placeHolderMethod, bootStrapMethod) - super.finallyAfterTest(placeHolderMethod) - Thread.currentThread().contextClassLoader = ContainedRobolectricRunner::class.java.classLoader - } - - override fun createClassLoaderConfig(method: FrameworkMethod?): InstrumentationConfiguration { - return InstrumentationConfiguration.Builder(super.createClassLoaderConfig(method)) - .doNotAcquirePackage("io.kotest") - .build() - } - - override fun getConfig(method: Method?): Config { - val defaultConfiguration = - injector.getInstance(ConfigurationStrategy::class.java) - .getConfig(testClass.javaClass, method) - - if (config != null) { - val configConfigurer = injector.getInstance(ConfigConfigurer::class.java) - return configConfigurer.merge(defaultConfiguration[Config::class.java], config) - } - - return super.getConfig(method) - } - - class PlaceholderTest { - @org.junit.Test - fun testPlaceholder() { - } - - fun bootStrapMethod() { - } - } - - companion object { - private val injector = defaultInjector().build() - } -} diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/extensions/RobolectricExtension.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/extensions/RobolectricExtension.kt deleted file mode 100644 index 3250906490..0000000000 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/extensions/RobolectricExtension.kt +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Code taken from https://github.com/kotest/kotest-extensions-robolectric with a - * fix in the intercept method. - * - * LICENSE: https://github.com/kotest/kotest-extensions-robolectric/blob/master/LICENSE - */ -package com.onesignal.notifications.extensions - -import android.app.Application -import io.kotest.core.extensions.ConstructorExtension -import io.kotest.core.extensions.TestCaseExtension -import io.kotest.core.spec.AutoScan -import io.kotest.core.spec.Spec -import io.kotest.core.test.TestCase -import io.kotest.core.test.TestResult -import org.robolectric.annotation.Config -import kotlin.reflect.KClass -import kotlin.reflect.full.findAnnotation - -/** - * We override TestCaseExtension to configure the Robolectric environment because TestCase intercept - * occurs on the same thread the test is run. This is unfortunate because it is run for every test, - * rather than every spec. But the SpecExtension intercept is run on a different thread. - */ -@AutoScan -internal class RobolectricExtension : ConstructorExtension, TestCaseExtension { - private fun Class<*>.getParentClass(): List> { - if (superclass == null) return listOf() - return listOf(superclass) + superclass.getParentClass() - } - - private fun KClass<*>.getConfig(): Config { - val configAnnotations = - listOf(this.java).plus(this.java.getParentClass()) - .mapNotNull { it.kotlin.findAnnotation() } - .asSequence() - - val configAnnotation = configAnnotations.firstOrNull() - - if (configAnnotation != null) { - return Config.Builder(configAnnotation).build() - } - - val robolectricTestAnnotations = - listOf(this.java).plus(this.java.getParentClass()) - .mapNotNull { it.kotlin.findAnnotation() } - .asSequence() - - val application: KClass? = - robolectricTestAnnotations - .firstOrNull { it.application != KotestDefaultApplication::class }?.application - val sdk: Int? = robolectricTestAnnotations.firstOrNull { it.sdk != -1 }?.takeUnless { it.sdk == -1 }?.sdk - - return Config.Builder() - .also { builder -> - if (application != null) { - builder.setApplication(application.java) - } - - if (sdk != null) { - builder.setSdk(sdk) - } - }.build() - } - - override fun instantiate(clazz: KClass): Spec? { - clazz.findAnnotation() ?: return null - - return ContainedRobolectricRunner(clazz.getConfig()) - .sdkEnvironment.bootstrappedClass(clazz.java).newInstance() - } - - override suspend fun intercept( - testCase: TestCase, - execute: suspend (TestCase) -> TestResult, - ): TestResult { - // FIXED: Updated code based on https://github.com/kotest/kotest/issues/2717 - val hasRobolectricAnnotation = - testCase.spec::class.annotations.any { annotation -> - annotation.annotationClass.qualifiedName == RobolectricTest::class.qualifiedName - } - - if (!hasRobolectricAnnotation) { - return execute(testCase) - } - - val containedRobolectricRunner = ContainedRobolectricRunner(testCase.spec::class.getConfig()) - containedRobolectricRunner.containedBefore() - val result = execute(testCase) - containedRobolectricRunner.containedAfter() - return result - } -} - -internal class KotestDefaultApplication : Application() - -annotation class RobolectricTest( - val application: KClass = KotestDefaultApplication::class, - val sdk: Int = -1, -) diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/channels/NotificationChannelManagerTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/channels/NotificationChannelManagerTests.kt index a5fa36f336..382787c2d4 100644 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/channels/NotificationChannelManagerTests.kt +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/channels/NotificationChannelManagerTests.kt @@ -9,10 +9,10 @@ import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.mocks.AndroidMockHelper import com.onesignal.mocks.MockHelper -import com.onesignal.notifications.extensions.RobolectricTest import com.onesignal.notifications.internal.channels.impl.NotificationChannelManager import com.onesignal.notifications.internal.common.NotificationGenerationJob import com.onesignal.notifications.shadows.ShadowRoboNotificationManager +import com.onesignal.testhelpers.extensions.RobolectricTest import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe 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 33d66ddb03..3697ccb66b 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 @@ -8,12 +8,12 @@ import com.onesignal.mocks.AndroidMockHelper import com.onesignal.mocks.MockHelper import com.onesignal.notifications.INotificationReceivedEvent import com.onesignal.notifications.INotificationWillDisplayEvent -import com.onesignal.notifications.extensions.RobolectricTest 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 com.onesignal.testhelpers.extensions.RobolectricTest import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.kotest.runner.junit4.KotestTestRunner diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/limiting/NotificationLimitManagerTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/limiting/NotificationLimitManagerTests.kt index 79ba17c980..f925daf36e 100644 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/limiting/NotificationLimitManagerTests.kt +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/limiting/NotificationLimitManagerTests.kt @@ -7,10 +7,10 @@ import androidx.test.core.app.ApplicationProvider import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.mocks.AndroidMockHelper -import com.onesignal.notifications.extensions.RobolectricTest import com.onesignal.notifications.internal.data.INotificationRepository import com.onesignal.notifications.internal.limiting.impl.NotificationLimitManager import com.onesignal.notifications.internal.summary.INotificationSummaryManager +import com.onesignal.testhelpers.extensions.RobolectricTest import io.kotest.core.spec.style.FunSpec import io.kotest.runner.junit4.KotestTestRunner import io.mockk.coEvery diff --git a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/summary/NotificationSummaryManagerTests.kt b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/summary/NotificationSummaryManagerTests.kt index cb727dc58d..87e36f99e9 100644 --- a/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/summary/NotificationSummaryManagerTests.kt +++ b/OneSignalSDK/onesignal/notifications/src/test/java/com/onesignal/notifications/internal/summary/NotificationSummaryManagerTests.kt @@ -4,12 +4,12 @@ import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.mocks.AndroidMockHelper import com.onesignal.mocks.MockHelper -import com.onesignal.notifications.extensions.RobolectricTest import com.onesignal.notifications.internal.data.INotificationRepository import com.onesignal.notifications.internal.display.ISummaryNotificationDisplayer import com.onesignal.notifications.internal.restoration.INotificationRestoreProcessor import com.onesignal.notifications.internal.summary.impl.NotificationSummaryManager import com.onesignal.notifications.shadows.ShadowRoboNotificationManager +import com.onesignal.testhelpers.extensions.RobolectricTest import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.kotest.runner.junit4.KotestTestRunner diff --git a/OneSignalSDK/onesignal/testhelpers/.gitignore b/OneSignalSDK/onesignal/testhelpers/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/OneSignalSDK/onesignal/testhelpers/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/OneSignalSDK/onesignal/testhelpers/build.gradle b/OneSignalSDK/onesignal/testhelpers/build.gradle new file mode 100644 index 0000000000..e612ba8333 --- /dev/null +++ b/OneSignalSDK/onesignal/testhelpers/build.gradle @@ -0,0 +1,47 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'org.jlleitschuh.gradle.ktlint' +} + +android { + compileSdkVersion rootProject.buildVersions.compileSdkVersion + defaultConfig { + minSdkVersion rootProject.buildVersions.minSdkVersion + } + + buildTypes { + original { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + release { + minifyEnabled false + } + unity { + minifyEnabled false + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + namespace 'com.onesignal.testhelpers' + + kotlinOptions.freeCompilerArgs += ['-module-name', namespace] +} + +dependencies { + implementation("junit:junit:$junitVersion") + implementation("io.kotest:kotest-runner-junit4:$kotestVersion") + implementation("org.robolectric:robolectric:4.8.1") + implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") +} + +ktlint { + version = "$ktlintVersion" +} diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/extensions/ContainedRobolectricRunner.kt b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/testhelpers/extensions/ContainedRobolectricRunner.kt similarity index 96% rename from OneSignalSDK/onesignal/core/src/test/java/com/onesignal/extensions/ContainedRobolectricRunner.kt rename to OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/testhelpers/extensions/ContainedRobolectricRunner.kt index 3f6529695a..ab8d352e2c 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/extensions/ContainedRobolectricRunner.kt +++ b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/testhelpers/extensions/ContainedRobolectricRunner.kt @@ -3,7 +3,7 @@ * * LICENSE: https://github.com/kotest/kotest-extensions-robolectric/blob/master/LICENSE */ -package com.onesignal.extensions +package com.onesignal.testhelpers.extensions import org.junit.runners.model.FrameworkMethod import org.robolectric.RobolectricTestRunner @@ -13,7 +13,7 @@ import org.robolectric.pluginapi.config.ConfigurationStrategy import org.robolectric.plugins.ConfigConfigurer import java.lang.reflect.Method -internal class ContainedRobolectricRunner( +class ContainedRobolectricRunner( private val config: Config?, ) : RobolectricTestRunner(PlaceholderTest::class.java, injector) { private val placeHolderMethod: FrameworkMethod = children[0] diff --git a/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/extensions/RobolectricExtension.kt b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/testhelpers/extensions/RobolectricExtension.kt similarity index 68% rename from OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/extensions/RobolectricExtension.kt rename to OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/testhelpers/extensions/RobolectricExtension.kt index 3250906490..e6ea8cf0a0 100644 --- a/OneSignalSDK/onesignal/location/src/test/java/com/onesignal/location/extensions/RobolectricExtension.kt +++ b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/testhelpers/extensions/RobolectricExtension.kt @@ -4,9 +4,10 @@ * * LICENSE: https://github.com/kotest/kotest-extensions-robolectric/blob/master/LICENSE */ -package com.onesignal.notifications.extensions +package com.onesignal.testhelpers.extensions import android.app.Application +import io.kotest.common.runBlocking import io.kotest.core.extensions.ConstructorExtension import io.kotest.core.extensions.TestCaseExtension import io.kotest.core.spec.AutoScan @@ -14,8 +15,10 @@ import io.kotest.core.spec.Spec import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult import org.robolectric.annotation.Config +import java.util.concurrent.Callable import kotlin.reflect.KClass import kotlin.reflect.full.findAnnotation +import kotlin.time.Duration /** * We override TestCaseExtension to configure the Robolectric environment because TestCase intercept @@ -23,7 +26,7 @@ import kotlin.reflect.full.findAnnotation * rather than every spec. But the SpecExtension intercept is run on a different thread. */ @AutoScan -internal class RobolectricExtension : ConstructorExtension, TestCaseExtension { +class RobolectricExtension : ConstructorExtension, TestCaseExtension { private fun Class<*>.getParentClass(): List> { if (superclass == null) return listOf() return listOf(superclass) + superclass.getParentClass() @@ -73,6 +76,19 @@ internal class RobolectricExtension : ConstructorExtension, TestCaseExtension { override suspend fun intercept( testCase: TestCase, execute: suspend (TestCase) -> TestResult, + ): TestResult { + return try { + runTest(testCase, execute) + } catch (t: Throwable) { + // Without this the whole test class will be silently be skipped + // if something throws + TestResult.Error(Duration.ZERO, t) + } + } + + private suspend fun runTest( + testCase: TestCase, + execute: suspend (TestCase) -> TestResult, ): TestResult { // FIXED: Updated code based on https://github.com/kotest/kotest/issues/2717 val hasRobolectricAnnotation = @@ -80,15 +96,29 @@ internal class RobolectricExtension : ConstructorExtension, TestCaseExtension { annotation.annotationClass.qualifiedName == RobolectricTest::class.qualifiedName } - if (!hasRobolectricAnnotation) { - return execute(testCase) + return if (hasRobolectricAnnotation) { + runTestRobolectric(testCase, execute) + } else { + execute(testCase) } + } - val containedRobolectricRunner = ContainedRobolectricRunner(testCase.spec::class.getConfig()) - containedRobolectricRunner.containedBefore() - val result = execute(testCase) - containedRobolectricRunner.containedAfter() - return result + private suspend fun runTestRobolectric( + testCase: TestCase, + execute: suspend (TestCase) -> TestResult, + ): TestResult { + val containedRobolectricRunner = + ContainedRobolectricRunner(testCase.spec::class.getConfig()) + // sdkEnvironment.runOnMainThread is important to ensure Robolectric's + // looper state doesn't carry over to the next test class. + return containedRobolectricRunner.sdkEnvironment.runOnMainThread( + Callable { + containedRobolectricRunner.containedBefore() + val result = runBlocking { execute(testCase) } + containedRobolectricRunner.containedAfter() + result + }, + ) } } diff --git a/OneSignalSDK/settings.gradle b/OneSignalSDK/settings.gradle index b4845708f3..b114b0b152 100644 --- a/OneSignalSDK/settings.gradle +++ b/OneSignalSDK/settings.gradle @@ -25,3 +25,4 @@ include ':OneSignal:core' include ':OneSignal:in-app-messages' include ':OneSignal:location' include ':OneSignal:notifications' +include ':OneSignal:testhelpers'