Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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)
}

Expand All @@ -543,6 +541,30 @@ internal class OneSignalImp : IOneSignal,

override fun <T> getAllServices(c: Class<T>): List<T> = 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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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<LoginData>? =
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."
Expand All @@ -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
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<IllegalStateException> {
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<IllegalStateException> {
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") {
Expand All @@ -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<IllegalStateException> {
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
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -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<IllegalStateException> {
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<IllegalStateException> {
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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<Exception> {
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<Exception> {
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") {
Expand Down Expand Up @@ -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<Exception> {
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)
Expand Down
Loading