@@ -7,6 +7,7 @@ import com.onesignal.debug.internal.logging.Logging
77import com.onesignal.mocks.MockHelper
88import com.onesignal.mocks.MockPreferencesService
99import com.onesignal.user.internal.identity.IdentityModel
10+ import com.onesignal.user.internal.jwt.JwtRequirement
1011import com.onesignal.user.internal.jwt.JwtTokenStore
1112import com.onesignal.user.internal.operations.LoginUserOperation
1213import com.onesignal.user.internal.properties.PropertiesModel
@@ -50,6 +51,7 @@ class LoginHelperTests : FunSpec({
5051 val mockOperationRepo = mockk<IOperationRepo >(relaxed = true)
5152 val mockConfigModel = mockk<ConfigModel >()
5253 every { mockConfigModel.appId } returns appId
54+ every { mockConfigModel.useIdentityVerification } returns JwtRequirement .NOT_REQUIRED
5355 val loginLock = Any ()
5456
5557 val loginHelper =
@@ -91,6 +93,7 @@ class LoginHelperTests : FunSpec({
9193 val mockOperationRepo = mockk<IOperationRepo >()
9294 val mockConfigModel = mockk<ConfigModel >()
9395 every { mockConfigModel.appId } returns appId
96+ every { mockConfigModel.useIdentityVerification } returns JwtRequirement .NOT_REQUIRED
9497 val loginLock = Any ()
9598
9699 val userSwitcherSlot = slot<(IdentityModel , PropertiesModel ) -> Unit >()
@@ -158,6 +161,7 @@ class LoginHelperTests : FunSpec({
158161 val mockOperationRepo = mockk<IOperationRepo >()
159162 val mockConfigModel = mockk<ConfigModel >()
160163 every { mockConfigModel.appId } returns appId
164+ every { mockConfigModel.useIdentityVerification } returns JwtRequirement .NOT_REQUIRED
161165 val loginLock = Any ()
162166
163167 val userSwitcherSlot = slot<(IdentityModel , PropertiesModel ) -> Unit >()
@@ -202,6 +206,68 @@ class LoginHelperTests : FunSpec({
202206 }
203207 }
204208
209+ test("login under IV -required does NOT carry existingOnesignalId from anonymous user") {
210+ // Given - anonymous user, IV is REQUIRED. The anon user was never created server-side
211+ // (no JWT), so its onesignalId would be permanently local. Carrying it as
212+ // existingOnesignalId on the new LoginUserOperation would deadlock canStartExecute.
213+ val mockIdentityModelStore =
214+ MockHelper .identityModelStore { model ->
215+ model.externalId = null
216+ model.onesignalId = currentOneSignalId
217+ }
218+
219+ val newIdentityModel =
220+ IdentityModel ().apply {
221+ externalId = newExternalId
222+ onesignalId = newOneSignalId
223+ }
224+
225+ val mockUserSwitcher = mockk<UserSwitcher >()
226+ val mockOperationRepo = mockk<IOperationRepo >()
227+ val mockConfigModel = mockk<ConfigModel >()
228+ every { mockConfigModel.appId } returns appId
229+ every { mockConfigModel.useIdentityVerification } returns JwtRequirement .REQUIRED
230+
231+ val userSwitcherSlot = slot<(IdentityModel , PropertiesModel ) -> Unit >()
232+ every {
233+ mockUserSwitcher.createAndSwitchToNewUser(
234+ suppressBackendOperation = any(),
235+ modify = capture(userSwitcherSlot),
236+ )
237+ } answers {
238+ userSwitcherSlot.captured(newIdentityModel, PropertiesModel ())
239+ every { mockIdentityModelStore.model } returns newIdentityModel
240+ }
241+
242+ coEvery { mockOperationRepo.enqueueAndWait(any()) } returns true
243+
244+ val loginHelper =
245+ LoginHelper (
246+ identityModelStore = mockIdentityModelStore,
247+ userSwitcher = mockUserSwitcher,
248+ operationRepo = mockOperationRepo,
249+ configModel = mockConfigModel,
250+ jwtTokenStore = JwtTokenStore (MockPreferencesService ()),
251+ lock = Any (),
252+ )
253+
254+ // When
255+ runBlocking {
256+ val context = loginHelper.switchUser(newExternalId, jwtBearerToken = "fresh-jwt")
257+ if (context != null) loginHelper.enqueueLogin(context)
258+ }
259+
260+ // Then — under IV, the executor must take the createUser (upsert) path; no merge link.
261+ coVerify(exactly = 1) {
262+ mockOperationRepo.enqueueAndWait(
263+ withArg<LoginUserOperation > { operation ->
264+ operation.externalId shouldBe newExternalId
265+ operation.existingOnesignalId shouldBe null
266+ },
267+ )
268+ }
269+ }
270+
205271 test("login logs error when operation fails") {
206272 // Given
207273 val mockIdentityModelStore =
@@ -220,6 +286,7 @@ class LoginHelperTests : FunSpec({
220286 val mockOperationRepo = mockk<IOperationRepo >()
221287 val mockConfigModel = mockk<ConfigModel >()
222288 every { mockConfigModel.appId } returns appId
289+ every { mockConfigModel.useIdentityVerification } returns JwtRequirement .NOT_REQUIRED
223290 val loginLock = Any ()
224291
225292 val userSwitcherSlot = slot<(IdentityModel , PropertiesModel ) -> Unit >()
@@ -279,6 +346,7 @@ class LoginHelperTests : FunSpec({
279346 coEvery { mockOperationRepo.enqueueAndWait(any()) } returns true
280347 val mockConfigModel = mockk<ConfigModel >()
281348 every { mockConfigModel.appId } returns appId
349+ every { mockConfigModel.useIdentityVerification } returns JwtRequirement .NOT_REQUIRED
282350 val jwtTokenStore = JwtTokenStore (MockPreferencesService ())
283351
284352 val loginHelper =
@@ -312,6 +380,7 @@ class LoginHelperTests : FunSpec({
312380 val mockOperationRepo = mockk<IOperationRepo >(relaxed = true)
313381 val mockConfigModel = mockk<ConfigModel >()
314382 every { mockConfigModel.appId } returns appId
383+ every { mockConfigModel.useIdentityVerification } returns JwtRequirement .NOT_REQUIRED
315384 val jwtTokenStore = JwtTokenStore (MockPreferencesService ())
316385 jwtTokenStore.putJwt(currentExternalId, "old-jwt")
317386
0 commit comments