diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt index f0f59fea1..d1dfd2af9 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt @@ -491,9 +491,7 @@ internal class OneSignalImp : IOneSignal, ) { Logging.log(LogLevel.DEBUG, "Calling deprecated login(externalId: $externalId, jwtBearerToken: ...${jwtBearerToken?.takeLast(8)})") - waitForInit(operationName = "login") - - if (isMissing(externalId, "login: externalId")) return + if (!isReadyFor("login") || isMissing(externalId, "login: externalId")) return val context = loginHelper.switchUser(externalId, jwtBearerToken).context ?: return @@ -503,7 +501,7 @@ internal class OneSignalImp : IOneSignal, override fun logout() { Logging.log(LogLevel.DEBUG, "Calling deprecated logout()") - waitForInit(operationName = "logout") + if (!isReadyFor("logout")) return val context = logoutHelper.switchUser() ?: return @@ -516,7 +514,7 @@ internal class OneSignalImp : IOneSignal, ) { Logging.log(LogLevel.DEBUG, "updateUserJwt(externalId: $externalId, token: ...${token.takeLast(8)})") - waitForInit(operationName = "updateUserJwt") + if (!isReadyFor("updateUserJwt")) return if (isMissing(externalId, "updateUserJwt: externalId") || isMissing(token, "updateUserJwt: token")) return @@ -526,12 +524,12 @@ internal class OneSignalImp : IOneSignal, } override fun addUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) { - waitForInit(operationName = "addUserJwtInvalidatedListener") + if (!isReadyFor("addUserJwtInvalidatedListener")) return jwtTokenStore.addUserJwtInvalidatedListener(listener) } override fun removeUserJwtInvalidatedListener(listener: IUserJwtInvalidatedListener) { - waitForInit(operationName = "removeUserJwtInvalidatedListener") + if (!isReadyFor("removeUserJwtInvalidatedListener")) return jwtTokenStore.removeUserJwtInvalidatedListener(listener) } @@ -543,6 +541,30 @@ internal class OneSignalImp : IOneSignal, override fun getAllServices(c: Class): List = services.getAllServices(c) + // Calls that can be dropped safely log and return false instead of throwing when init never + // started or failed. Accessors still throw, since they have no value to return. + private fun isReadyFor(operationName: String): Boolean = + ignoreIfNotInitialized(operationName) { waitForInit(operationName) } == null + + private suspend fun isReadyForSuspend(operationName: String): Boolean = + ignoreIfNotInitialized(operationName) { suspendUntilInit(operationName) } == null + + /** Runs [wait] and returns the init failure it threw, after logging it, or null once init succeeded. */ + private inline fun ignoreIfNotInitialized( + operationName: String, + wait: () -> Unit, + ): IllegalStateException? = + try { + wait() + null + } catch (e: CancellationException) { + // A subclass of IllegalStateException, so it must be rethrown before the catch below. + throw e + } catch (e: IllegalStateException) { + Logging.error("'$operationName' ignored: ${e.message}", e) + e + } + /** * Blocking version that waits for initialization to complete. * Uses runBlocking to bridge to the suspend implementation. @@ -841,9 +863,7 @@ internal class OneSignalImp : IOneSignal, ) = withContext(ioDispatcher) { Logging.log(LogLevel.DEBUG, "login(externalId: $externalId, jwtBearerToken: ...${jwtBearerToken?.takeLast(8)})") - // suspendUntilInit throws on NOT_STARTED / FAILED (preserving initFailureException as the - // cause), and only returns once initState == SUCCESS — so no post-check is needed here. - suspendUntilInit(operationName = "login") + if (!isReadyForSuspend("login")) return@withContext if (isMissing(externalId, "login: externalId")) return@withContext @@ -898,17 +918,8 @@ internal class OneSignalImp : IOneSignal, /** Waits for init; returns a NOT_INITIALIZED failure instead of throwing when it never started or failed. */ internal suspend fun awaitInitForLogin(): OneSignalResult? = - try { - suspendUntilInit(operationName = "login") - null - } catch (e: CancellationException) { - // A subclass of IllegalStateException, so it must be rethrown before the catch below. - throw e - } catch (e: IllegalStateException) { - // Callers can ignore the result, so the failure has to show up in the log. - Logging.error("login(externalId, profile) skipped: ${e.message}", e) - OneSignalResult.failure(ErrorCode.NOT_INITIALIZED, e.message, cause = e) - } + ignoreIfNotInitialized("login") { suspendUntilInit(operationName = "login") } + ?.let { OneSignalResult.failure(ErrorCode.NOT_INITIALIZED, it.message, cause = it) } private fun loginFailureMessage(wait: OperationWaitResult): String = wait.httpStatusCode?.let { "Login did not complete (HTTP $it)." } ?: "Login did not complete." @@ -919,11 +930,7 @@ internal class OneSignalImp : IOneSignal, ) = withContext(ioDispatcher) { Logging.log(LogLevel.DEBUG, "updateUserJwtSuspend(externalId: $externalId, token: ...${token.takeLast(8)})") - suspendUntilInit(operationName = "updateUserJwt") - - if (!isInitialized) { - throw IllegalStateException("'initWithContext failed' before 'updateUserJwt'") - } + if (!isReadyForSuspend("updateUserJwt")) return@withContext if (isMissing(externalId, "updateUserJwt: externalId") || isMissing(token, "updateUserJwt: token")) { return@withContext @@ -937,7 +944,7 @@ internal class OneSignalImp : IOneSignal, withContext(ioDispatcher) { Logging.log(LogLevel.DEBUG, "logoutSuspend()") - suspendUntilInit(operationName = "logout") + if (!isReadyForSuspend("logout")) return@withContext val context = logoutHelper.switchUser() ?: return@withContext logoutHelper.enqueueLogout(context) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitSuspendTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitSuspendTests.kt index 55d7b6ece..02f3c9b2a 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitSuspendTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitSuspendTests.kt @@ -8,7 +8,6 @@ import com.onesignal.OneSignalUserProfile import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.internal.OneSignalImp -import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import kotlinx.coroutines.runBlocking @@ -263,34 +262,20 @@ class SDKInitSuspendTests : FunSpec({ } } - test("login should throw exception when initWithContext is never called") { - // Given + test("login is ignored when initWithContext is never called") { val oneSignalImp = OneSignalImp() - // When/Then - should throw exception immediately - val exception = - shouldThrow { - oneSignalImp.login("testUser", null) - } + oneSignalImp.login("testUser", null) - // Should throw immediately because isInitialized is false - exception.message shouldBe "Must call 'initWithContext' before 'login'" + oneSignalImp.isInitialized shouldBe false } - test("loginSuspend should throw exception when initWithContext is never called") { - // Given + test("loginSuspend is ignored when initWithContext is never called") { val oneSignalImp = OneSignalImp() - // When/Then - should throw exception immediately - runBlocking { - val exception = - shouldThrow { - oneSignalImp.loginSuspend("testUser", null) - } + runBlocking { oneSignalImp.loginSuspend("testUser", null) } - // Should throw immediately because isInitialized is false - exception.message shouldBe "Must call 'initWithContext' before 'login'" - } + oneSignalImp.isInitialized shouldBe false } test("composite login returns NOT_INITIALIZED when initWithContext is never called") { @@ -305,19 +290,14 @@ class SDKInitSuspendTests : FunSpec({ } } - test("logoutSuspend should throw exception when initWithContext is never called") { - // Given + test("logoutSuspend and updateUserJwtSuspend are ignored when initWithContext is never called") { val oneSignalImp = OneSignalImp() - // When/Then - should throw exception immediately runBlocking { - val exception = - shouldThrow { - oneSignalImp.logoutSuspend() - } - - // Should throw immediately because isInitialized is false - exception.message shouldBe "Must call 'initWithContext' before 'logout'" + oneSignalImp.logoutSuspend() + oneSignalImp.updateUserJwtSuspend("user", "token") } + + oneSignalImp.isInitialized shouldBe false } }) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitTests.kt index 7f5c9e855..84e6e1a9f 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitTests.kt @@ -677,32 +677,20 @@ class SDKInitTests : FunSpec({ os.isInitialized shouldBe true } - test("login should throw exception when initWithContext is never called") { - // Given + test("login is ignored when initWithContext is never called") { val oneSignalImp = OneSignalImp() - // When/Then - should throw exception immediately - val exception = - shouldThrow { - oneSignalImp.login("testUser", null) - } + oneSignalImp.login("testUser", null) - // Should throw immediately because isInitialized is false - exception.message shouldBe "Must call 'initWithContext' before 'login'" + oneSignalImp.isInitialized shouldBe false } - test("logout should throw exception when initWithContext is never called") { - // Given + test("logout is ignored when initWithContext is never called") { val oneSignalImp = OneSignalImp() - // When/Then - should throw exception immediately - val exception = - shouldThrow { - oneSignalImp.logout() - } + oneSignalImp.logout() - // Should throw immediately because isInitialized is false - exception.message shouldBe "Must call 'initWithContext' before 'logout'" + oneSignalImp.isInitialized shouldBe false } // Regression test for the SessionService NPE during in-flight background init. 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 051d89fbc..b645e12cd 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 @@ -1,12 +1,12 @@ package com.onesignal.internal import com.onesignal.ErrorCode +import com.onesignal.IUserJwtInvalidatedListener import com.onesignal.LoginData import com.onesignal.OneSignalResult import com.onesignal.OneSignalUserProfile import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging -import io.kotest.assertions.throwables.shouldThrowUnit import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import kotlinx.coroutines.CancellationException @@ -40,29 +40,31 @@ class OneSignalImpTests : FunSpec({ field.set(os, value) } - test("attempting login before initWithContext throws exception") { - // Given + test("login, logout, updateUserJwt and the JWT listeners before initWithContext are ignored") { val os = OneSignalImp() + Logging.logLevel = LogLevel.NONE + val listener = IUserJwtInvalidatedListener { } - // When - val exception = - shouldThrowUnit { - os.login("login-id") - } + os.login("login-id") + os.login("") + os.logout() + os.updateUserJwt("user", "token") + os.addUserJwtInvalidatedListener(listener) + os.removeUserJwtInvalidatedListener(listener) - // Then - exception.message shouldBe "Must call 'initWithContext' before 'login'" + os.isInitialized shouldBe false } - test("login with empty externalId before init still throws") { + test("login and logout after a failed init are ignored") { val os = OneSignalImp() + Logging.logLevel = LogLevel.NONE + setPrivateField(os, "initState", InitState.FAILED) - val exception = - shouldThrowUnit { - os.login("") - } + os.login("login-id") + os.logout() + runBlocking { os.loginSuspend("login-id") } - exception.message shouldBe "Must call 'initWithContext' before 'login'" + os.isInitialized shouldBe false } test("login with empty externalId after init does not switch user") { @@ -91,20 +93,6 @@ class OneSignalImpTests : FunSpec({ os.updateUserJwt("user", "") } - test("attempting logout before initWithContext throws exception") { - // Given - val os = OneSignalImp() - - // When - val exception = - shouldThrowUnit { - os.logout() - } - - // Then - exception.message shouldBe "Must call 'initWithContext' before 'logout'" - } - test("waitForInit returns synchronously without dispatching when initState is SUCCESS") { val os = OneSignalImp() markInitialized(os)