@@ -739,4 +739,124 @@ class LoginUserOperationExecutorTests : FunSpec({
739739 exactly = 0,
740740 ) { mockUserBackendService.createUser(appId, any(), any(), any()) }
741741 }
742+
743+ test("create user maps subscriptions when backend order is different (match by id/token)") {
744+ // Given
745+ val mockUserBackendService = mockk<IUserBackendService >()
746+ // backend returns EMAIL first (with token), then PUSH — out of order
747+ coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } returns
748+ CreateUserResponse (
749+ mapOf(IdentityConstants .ONESIGNAL_ID to remoteOneSignalId),
750+ PropertiesObject (),
751+ listOf(
752+ SubscriptionObject (id = remoteSubscriptionId2, type = SubscriptionObjectType .EMAIL , token = "name@company.com"),
753+ SubscriptionObject (id = remoteSubscriptionId1, type = SubscriptionObjectType .ANDROID_PUSH , token = "pushToken2"),
754+ ),
755+ )
756+
757+ val mockIdentityOperationExecutor = mockk<IdentityOperationExecutor >()
758+ val mockIdentityModelStore = MockHelper .identityModelStore()
759+ val mockPropertiesModelStore = MockHelper .propertiesModelStore()
760+ val mockSubscriptionsModelStore = mockk<SubscriptionModelStore >()
761+ every { mockSubscriptionsModelStore.get(any()) } returns null
762+
763+ val executor =
764+ LoginUserOperationExecutor (
765+ mockIdentityOperationExecutor,
766+ AndroidMockHelper .applicationService(),
767+ MockHelper .deviceService(),
768+ mockUserBackendService,
769+ mockIdentityModelStore,
770+ mockPropertiesModelStore,
771+ mockSubscriptionsModelStore,
772+ MockHelper .configModelStore(),
773+ MockHelper .languageContext(),
774+ )
775+
776+ // send PUSH then EMAIL (local IDs 1,2) — order differs from backend response
777+ val ops =
778+ listOf(
779+ LoginUserOperation (appId, localOneSignalId, null, null),
780+ CreateSubscriptionOperation (appId, localOneSignalId, localSubscriptionId1, SubscriptionType .PUSH , true, "pushToken2", SubscriptionStatus .SUBSCRIBED ),
781+ CreateSubscriptionOperation (appId, localOneSignalId, localSubscriptionId2, SubscriptionType .EMAIL , true, "name@company.com", SubscriptionStatus .SUBSCRIBED ),
782+ )
783+
784+ // When
785+ val response = executor.execute(ops)
786+
787+ // Then
788+ response.result shouldBe ExecutionResult .SUCCESS
789+ // ensure local to remote mapping is correct despite different order
790+ response.idTranslations shouldBe
791+ mapOf(
792+ localOneSignalId to remoteOneSignalId,
793+ // push
794+ localSubscriptionId1 to remoteSubscriptionId1,
795+ // email
796+ localSubscriptionId2 to remoteSubscriptionId2,
797+ )
798+ coVerify(exactly = 1) { mockUserBackendService.createUser(appId, mapOf(), any(), any()) }
799+ }
800+
801+ test("create user maps push subscription by type when id and token don't match (case for deleted push sub)") {
802+ // Given
803+ val mockUserBackendService = mockk<IUserBackendService >()
804+ // simulate server-side push sub recreated with new ID and no token; must match by type
805+ coEvery { mockUserBackendService.createUser(any(), any(), any(), any()) } returns
806+ CreateUserResponse (
807+ mapOf(IdentityConstants .ONESIGNAL_ID to remoteOneSignalId),
808+ PropertiesObject (),
809+ listOf(
810+ SubscriptionObject (id = remoteSubscriptionId1, type = SubscriptionObjectType .ANDROID_PUSH , token = null),
811+ ),
812+ )
813+
814+ val mockIdentityOperationExecutor = mockk<IdentityOperationExecutor >()
815+ val mockIdentityModelStore = MockHelper .identityModelStore()
816+ val mockPropertiesModelStore = MockHelper .propertiesModelStore()
817+
818+ val mockSubscriptionsModelStore = mockk<SubscriptionModelStore >()
819+ // provide a local push model so the executor can hydrate its id
820+ val localPushModel = SubscriptionModel ().apply { id = localSubscriptionId1 }
821+ every { mockSubscriptionsModelStore.get(localSubscriptionId1) } returns localPushModel
822+
823+ val configModelStore = MockHelper .configModelStore()
824+ // assume current push sub is the local one we are creating
825+ configModelStore.model.pushSubscriptionId = localSubscriptionId1
826+
827+ val executor =
828+ LoginUserOperationExecutor (
829+ mockIdentityOperationExecutor,
830+ AndroidMockHelper .applicationService(),
831+ MockHelper .deviceService(),
832+ mockUserBackendService,
833+ mockIdentityModelStore,
834+ mockPropertiesModelStore,
835+ mockSubscriptionsModelStore,
836+ configModelStore,
837+ MockHelper .languageContext(),
838+ )
839+
840+ val ops =
841+ listOf(
842+ LoginUserOperation (appId, localOneSignalId, null, null),
843+ CreateSubscriptionOperation (appId, localOneSignalId, localSubscriptionId1, SubscriptionType .PUSH , true, "pushToken1", SubscriptionStatus .SUBSCRIBED ),
844+ )
845+
846+ // When
847+ val response = executor.execute(ops)
848+
849+ // Then
850+ response.result shouldBe ExecutionResult .SUCCESS
851+ // should map by type and update both idTranslations and local model
852+ response.idTranslations shouldBe
853+ mapOf(
854+ localOneSignalId to remoteOneSignalId,
855+ localSubscriptionId1 to remoteSubscriptionId1,
856+ )
857+ localPushModel.id shouldBe remoteSubscriptionId1
858+ // pushSubscriptionId should be updated from local to remote id
859+ configModelStore.model.pushSubscriptionId shouldBe remoteSubscriptionId1
860+ coVerify(exactly = 1) { mockUserBackendService.createUser(appId, mapOf(), any(), any()) }
861+ }
742862})
0 commit comments