diff --git a/OneSignalSDK/detekt/detekt-baseline-core.xml b/OneSignalSDK/detekt/detekt-baseline-core.xml index 98692b29ce..69b24399f0 100644 --- a/OneSignalSDK/detekt/detekt-baseline-core.xml +++ b/OneSignalSDK/detekt/detekt-baseline-core.xml @@ -36,6 +36,8 @@ ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _buildUserService: IRebuildUserService ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _identityBackend: IIdentityBackendService ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _identityModelStore: IdentityModelStore + ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _identityVerificationService: IdentityVerificationService + ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _jwtTokenStore: JwtTokenStore ConstructorParameterNaming:IdentityOperationExecutor.kt$IdentityOperationExecutor$private val _newRecordState: NewRecordsState ConstructorParameterNaming:InfluenceDataRepository.kt$InfluenceDataRepository$private val _configModelStore: ConfigModelStore ConstructorParameterNaming:InfluenceManager.kt$InfluenceManager$private val _applicationService: IApplicationService @@ -45,6 +47,7 @@ ConstructorParameterNaming:JwtTokenStore.kt$JwtTokenStore$private val _prefs: IPreferencesService ConstructorParameterNaming:LanguageContext.kt$LanguageContext$private val _propertiesModelStore: PropertiesModelStore ConstructorParameterNaming:LoginUserFromSubscriptionOperationExecutor.kt$LoginUserFromSubscriptionOperationExecutor$private val _identityModelStore: IdentityModelStore + ConstructorParameterNaming:LoginUserFromSubscriptionOperationExecutor.kt$LoginUserFromSubscriptionOperationExecutor$private val _identityVerificationService: IdentityVerificationService ConstructorParameterNaming:LoginUserFromSubscriptionOperationExecutor.kt$LoginUserFromSubscriptionOperationExecutor$private val _propertiesModelStore: PropertiesModelStore ConstructorParameterNaming:LoginUserFromSubscriptionOperationExecutor.kt$LoginUserFromSubscriptionOperationExecutor$private val _subscriptionBackend: ISubscriptionBackendService ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _application: IApplicationService @@ -52,6 +55,8 @@ ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _deviceService: IDeviceService ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _identityModelStore: IdentityModelStore ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _identityOperationExecutor: IdentityOperationExecutor + ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _identityVerificationService: IdentityVerificationService + ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _jwtTokenStore: JwtTokenStore ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _languageContext: ILanguageContext ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _propertiesModelStore: PropertiesModelStore ConstructorParameterNaming:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private val _subscriptionsModelStore: SubscriptionModelStore @@ -97,6 +102,8 @@ ConstructorParameterNaming:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private val _buildUserService: IRebuildUserService ConstructorParameterNaming:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private val _configModelStore: ConfigModelStore ConstructorParameterNaming:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private val _identityModelStore: IdentityModelStore + ConstructorParameterNaming:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private val _identityVerificationService: IdentityVerificationService + ConstructorParameterNaming:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private val _jwtTokenStore: JwtTokenStore ConstructorParameterNaming:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private val _newRecordState: NewRecordsState ConstructorParameterNaming:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private val _propertiesModelStore: PropertiesModelStore ConstructorParameterNaming:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$private val _subscriptionsModelStore: SubscriptionModelStore @@ -127,6 +134,8 @@ ConstructorParameterNaming:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private val _configModelStore: ConfigModelStore ConstructorParameterNaming:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private val _consistencyManager: IConsistencyManager ConstructorParameterNaming:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private val _deviceService: IDeviceService + ConstructorParameterNaming:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private val _identityVerificationService: IdentityVerificationService + ConstructorParameterNaming:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private val _jwtTokenStore: JwtTokenStore ConstructorParameterNaming:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private val _newRecordState: NewRecordsState ConstructorParameterNaming:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private val _subscriptionBackend: ISubscriptionBackendService ConstructorParameterNaming:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$private val _subscriptionModelStore: SubscriptionModelStore @@ -138,6 +147,8 @@ ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _buildUserService: IRebuildUserService ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _consistencyManager: IConsistencyManager ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _identityModelStore: IdentityModelStore + ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _identityVerificationService: IdentityVerificationService + ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _jwtTokenStore: JwtTokenStore ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _newRecordState: NewRecordsState ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _propertiesModelStore: PropertiesModelStore ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _userBackend: IUserBackendService @@ -177,6 +188,7 @@ InstanceOfCheckForException:HttpClient.kt$HttpClient$t is UnknownHostException LongMethod:ApplicationService.kt$ApplicationService$override suspend fun waitUntilSystemConditionsAvailable(): Boolean LongMethod:ConfigModelStoreListener.kt$ConfigModelStoreListener$private fun fetchParams() + LongMethod:FeatureFlagsBackendService.kt$FeatureFlagsBackendService$override suspend fun fetchRemoteFeatureFlags(appId: String): RemoteFeatureFlagsFetchOutcome LongMethod:HttpClient.kt$HttpClient$private suspend fun makeRequestIODispatcher( url: String, method: String?, jsonBody: JSONObject?, timeout: Int, headers: OptionalHeaders?, ): HttpResponse LongMethod:IdentityOperationExecutor.kt$IdentityOperationExecutor$override suspend fun execute(operations: List<Operation>): ExecutionResponse LongMethod:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$private suspend fun createUser( createUserOperation: LoginUserOperation, operations: List<Operation>, ): ExecutionResponse @@ -197,16 +209,17 @@ LongMethod:TrackGooglePurchase.kt$TrackGooglePurchase$private fun sendPurchases( skusToAdd: ArrayList<String>, newPurchaseTokens: ArrayList<String>, ) LongMethod:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$override suspend fun execute(operations: List<Operation>): ExecutionResponse LongParameterList:CreateSubscriptionOperation.kt$CreateSubscriptionOperation$(appId: String, onesignalId: String, externalId: String?, subscriptionId: String, type: SubscriptionType, enabled: Boolean, address: String, status: SubscriptionStatus) - LongParameterList:ICustomEventBackendService.kt$ICustomEventBackendService$( appId: String, onesignalId: String, externalId: String?, timestamp: Long, eventName: String, eventProperties: String?, metadata: CustomEventMetadata, ) + LongParameterList:ICustomEventBackendService.kt$ICustomEventBackendService$( appId: String, onesignalId: String, externalId: String?, timestamp: Long, eventName: String, eventProperties: String?, metadata: CustomEventMetadata, jwt: String? = null, ) LongParameterList:IDatabase.kt$IDatabase$( table: String, columns: Array<String>? = null, whereClause: String? = null, whereArgs: Array<String>? = null, groupBy: String? = null, having: String? = null, orderBy: String? = null, limit: String? = null, action: (ICursor) -> Unit, ) LongParameterList:IOutcomeEventsBackendService.kt$IOutcomeEventsBackendService$( appId: String, userId: String, subscriptionId: String, deviceType: String, direct: Boolean?, event: OutcomeEvent, ) - LongParameterList:IParamsBackendService.kt$ParamsObject$( var googleProjectNumber: String? = null, var enterprise: Boolean? = null, var useIdentityVerification: Boolean? = null, var notificationChannels: JSONArray? = null, var firebaseAnalytics: Boolean? = null, var restoreTTLFilter: Boolean? = null, var clearGroupOnSummaryClick: Boolean? = null, var receiveReceiptEnabled: Boolean? = null, var disableGMSMissingPrompt: Boolean? = null, var unsubscribeWhenNotificationsDisabled: Boolean? = null, var locationShared: Boolean? = null, var requiresUserPrivacyConsent: Boolean? = null, var opRepoExecutionInterval: Long? = null, var influenceParams: InfluenceParamsObject, var fcmParams: FCMParamsObject, ) - LongParameterList:IUserBackendService.kt$IUserBackendService$( appId: String, aliasLabel: String, aliasValue: String, properties: PropertiesObject, refreshDeviceMetadata: Boolean, propertyiesDelta: PropertiesDeltasObject, ) - LongParameterList:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$( private val _identityOperationExecutor: IdentityOperationExecutor, private val _application: IApplicationService, private val _deviceService: IDeviceService, private val _userBackend: IUserBackendService, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, private val _subscriptionsModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _languageContext: ILanguageContext, ) + LongParameterList:IUserBackendService.kt$IUserBackendService$( appId: String, aliasLabel: String, aliasValue: String, properties: PropertiesObject, refreshDeviceMetadata: Boolean, propertyiesDelta: PropertiesDeltasObject, jwt: String? = null, ) + LongParameterList:LoginUserOperationExecutor.kt$LoginUserOperationExecutor$( private val _identityOperationExecutor: IdentityOperationExecutor, private val _application: IApplicationService, private val _deviceService: IDeviceService, private val _userBackend: IUserBackendService, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, private val _subscriptionsModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _languageContext: ILanguageContext, private val _jwtTokenStore: JwtTokenStore, private val _identityVerificationService: IdentityVerificationService, ) LongParameterList:OutcomeEventsController.kt$OutcomeEventsController$( private val _session: ISessionService, private val _influenceManager: IInfluenceManager, private val _outcomeEventsCache: IOutcomeEventsRepository, private val _outcomeEventsPreferences: IOutcomeEventsPreferences, private val _outcomeEventsBackend: IOutcomeEventsBackendService, private val _configModelStore: ConfigModelStore, private val _identityModelStore: IdentityModelStore, private val _subscriptionManager: ISubscriptionManager, private val _deviceService: IDeviceService, private val _time: ITime, ) + LongParameterList:RefreshUserOperationExecutor.kt$RefreshUserOperationExecutor$( private val _userBackend: IUserBackendService, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, private val _subscriptionsModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, private val _jwtTokenStore: JwtTokenStore, private val _identityVerificationService: IdentityVerificationService, ) LongParameterList:SubscriptionObject.kt$SubscriptionObject$( val id: String? = null, val type: SubscriptionObjectType? = null, val token: String? = null, val enabled: Boolean? = null, val notificationTypes: Int? = null, val sdk: String? = null, val deviceModel: String? = null, val deviceOS: String? = null, val rooted: Boolean? = null, val netType: Int? = null, val carrier: String? = null, val appVersion: String? = null, ) - LongParameterList:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$( private val _subscriptionBackend: ISubscriptionBackendService, private val _deviceService: IDeviceService, private val _applicationService: IApplicationService, private val _subscriptionModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, private val _consistencyManager: IConsistencyManager, ) + LongParameterList:SubscriptionOperationExecutor.kt$SubscriptionOperationExecutor$( private val _subscriptionBackend: ISubscriptionBackendService, private val _deviceService: IDeviceService, private val _applicationService: IApplicationService, private val _subscriptionModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, private val _consistencyManager: IConsistencyManager, private val _jwtTokenStore: JwtTokenStore, private val _identityVerificationService: IdentityVerificationService, ) LongParameterList:UpdateSubscriptionOperation.kt$UpdateSubscriptionOperation$(appId: String, onesignalId: String, externalId: String?, subscriptionId: String, type: SubscriptionType, enabled: Boolean, address: String, status: SubscriptionStatus) + LongParameterList:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$( private val _userBackend: IUserBackendService, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, private val _consistencyManager: IConsistencyManager, private val _jwtTokenStore: JwtTokenStore, private val _identityVerificationService: IdentityVerificationService, ) LongParameterList:UserSwitcher.kt$UserSwitcher$( private val preferencesService: IPreferencesService, private val operationRepo: IOperationRepo, private val services: ServiceProvider, private val idManager: IDManager = IDManager, private val identityModelStore: IdentityModelStore, private val propertiesModelStore: PropertiesModelStore, private val subscriptionModelStore: SubscriptionModelStore, private val configModel: ConfigModel, private val oneSignalUtils: OneSignalUtils = OneSignalUtils, private val carrierName: String? = null, private val deviceOS: String? = null, private val androidUtils: AndroidUtils = AndroidUtils, private val appContextProvider: () -> Context, ) LoopWithTooManyJumpStatements:ModelStore.kt$ModelStore$for (index in jsonArray.length() - 1 downTo 0) { val newModel = create(jsonArray.getJSONObject(index)) ?: continue /* * NOTE: Migration fix for bug introduced in 5.1.12 * The following check is intended for the operation model store. * When the call to this method moved out of the operation model store's initializer, * duplicate operations could be cached. * See https://github.com/OneSignal/OneSignal-Android-SDK/pull/2099 */ val hasExisting = models.any { it.id == newModel.id } if (hasExisting) { Logging.debug("ModelStore<$name>: load - operation.id: ${newModel.id} already exists in the store.") continue } models.add(0, newModel) // listen for changes to this model newModel.subscribe(this) } MagicNumber:ApplicationService.kt$ApplicationService$50 @@ -266,6 +279,7 @@ MagicNumber:TrackGooglePurchase.kt$TrackGooglePurchase.Companion$4 MagicNumber:TrackGooglePurchase.kt$TrackGooglePurchase.Companion$99 MagicNumber:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$404 + MatchingDeclarationName:ExecutorsIvExtensions.kt$IvBackendParams MemberNameEqualsClassName:OneSignal.kt$OneSignal$private val oneSignal: IOneSignal by lazy { OneSignalImp() } NestedBlockDepth:IdentityOperationExecutor.kt$IdentityOperationExecutor$override suspend fun execute(operations: List<Operation>): ExecutionResponse NestedBlockDepth:InfluenceManager.kt$InfluenceManager$private fun attemptSessionUpgrade( entryAction: AppEntryAction, directId: String? = null, ) @@ -287,8 +301,9 @@ RethrowCaughtException:OSDatabase.kt$OSDatabase$throw e ReturnCount:AppIdResolution.kt$fun resolveAppId( inputAppId: String?, configModel: ConfigModel, preferencesService: IPreferencesService, ): AppIdResolution ReturnCount:BackgroundManager.kt$BackgroundManager$override fun cancelRunBackgroundServices(): Boolean - ReturnCount:OneSignalImp.kt$OneSignalImp$private fun internalInit( context: Context, appId: String?, ): Boolean ReturnCount:ConfigModel.kt$ConfigModel$override fun createModelForProperty( property: String, jsonObject: JSONObject, ): Model? + ReturnCount:ExecutorsIvExtensions.kt$internal fun resolveIvBackendParams( op: Operation, onesignalId: String, jwtTokenStore: JwtTokenStore, ivBehaviorActive: Boolean, ): IvBackendParams + ReturnCount:ExecutorsIvExtensions.kt$internal fun resolveIvJwt( op: Operation, jwtTokenStore: JwtTokenStore, ivBehaviorActive: Boolean, ): String? ReturnCount:FeatureFlagsBackendService.kt$FeatureFlagsBackendService$override suspend fun fetchRemoteFeatureFlags(appId: String): RemoteFeatureFlagsFetchOutcome ReturnCount:HttpClient.kt$HttpClient$private suspend fun makeRequest( url: String, method: String?, jsonBody: JSONObject?, timeout: Int, headers: OptionalHeaders?, ): HttpResponse ReturnCount:IdentityOperationExecutor.kt$IdentityOperationExecutor$override suspend fun execute(operations: List<Operation>): ExecutionResponse @@ -301,10 +316,11 @@ ReturnCount:Model.kt$Model$protected fun getOptIntProperty( name: String, create: (() -> Int?)? = null, ): Int? ReturnCount:Model.kt$Model$protected fun getOptLongProperty( name: String, create: (() -> Long?)? = null, ): Long? ReturnCount:Model.kt$Model$protected inline fun <reified T : Enum<T>> getOptEnumProperty(name: String): T? + ReturnCount:OneSignalImp.kt$OneSignalImp$private fun internalInit( context: Context, appId: String?, ): Boolean ReturnCount:OperationModelStore.kt$OperationModelStore$override fun create(jsonObject: JSONObject?): Operation? + ReturnCount:OperationModelStore.kt$OperationModelStore$private fun isValidOperation(jsonObject: JSONObject): Boolean ReturnCount:OperationRepoIvExtensions.kt$internal fun OperationRepo.handleFailUnauthorized( startingOp: OperationRepo.OperationQueueItem, ops: List<OperationRepo.OperationQueueItem>, jwtTokenStore: JwtTokenStore, ivBehaviorActive: Boolean, ): Boolean ReturnCount:OperationRepoIvExtensions.kt$internal fun OperationRepo.hasValidJwtIfRequired( jwtTokenStore: JwtTokenStore, op: com.onesignal.core.internal.operations.Operation, ivBehaviorActive: Boolean, ): Boolean - ReturnCount:OperationModelStore.kt$OperationModelStore$private fun isValidOperation(jsonObject: JSONObject): Boolean ReturnCount:OutcomeEventsController.kt$OutcomeEventsController$private suspend fun sendAndCreateOutcomeEvent( name: String, weight: Float, // Note: this is optional sessionTime: Long, influences: List<Influence>, ): OutcomeEvent? ReturnCount:OutcomeEventsController.kt$OutcomeEventsController$private suspend fun sendUniqueOutcomeEvent( name: String, sessionInfluences: List<Influence>, ): OutcomeEvent? ReturnCount:PermissionsViewModel.kt$PermissionsViewModel$private fun shouldShowSettings( permission: String, shouldShowRationaleAfter: Boolean, ): Boolean @@ -329,11 +345,13 @@ SwallowedException:JSONUtils.kt$JSONUtils$t: Throwable SwallowedException:PermissionsActivity.kt$PermissionsActivity$e: ClassNotFoundException SwallowedException:PreferencesService.kt$PreferencesService$ex: Exception + SwallowedException:PreferencesService.kt$PreferencesService$t: Throwable SwallowedException:SyncJobService.kt$SyncJobService$e: Exception SwallowedException:TrackGooglePurchase.kt$TrackGooglePurchase.Companion$t: Throwable ThrowsCount:OneSignalImp.kt$OneSignalImp$private suspend fun waitUntilInitInternal(operationName: String? = null) TooGenericExceptionCaught:AndroidUtils.kt$AndroidUtils$e: Throwable TooGenericExceptionCaught:DeviceUtils.kt$DeviceUtils$t: Throwable + TooGenericExceptionCaught:FeatureFlagsRefreshService.kt$FeatureFlagsRefreshService$e: Exception TooGenericExceptionCaught:HttpClient.kt$HttpClient$e: Throwable TooGenericExceptionCaught:HttpClient.kt$HttpClient$t: Throwable TooGenericExceptionCaught:JSONUtils.kt$JSONUtils$t: Throwable @@ -343,6 +361,7 @@ TooGenericExceptionCaught:PreferenceStoreFix.kt$PreferenceStoreFix$e: Throwable TooGenericExceptionCaught:PreferencesService.kt$PreferencesService$e: Throwable TooGenericExceptionCaught:PreferencesService.kt$PreferencesService$ex: Exception + TooGenericExceptionCaught:PreferencesService.kt$PreferencesService$t: Throwable TooGenericExceptionCaught:SyncJobService.kt$SyncJobService$e: Exception TooGenericExceptionCaught:ThreadUtils.kt$e: Exception TooGenericExceptionCaught:TrackGooglePurchase.kt$TrackGooglePurchase$e: Throwable @@ -421,10 +440,6 @@ UndocumentedPublicClass:IOperationExecutor.kt$ExecutionResponse UndocumentedPublicClass:IOperationExecutor.kt$ExecutionResult UndocumentedPublicClass:IOutcomeEvent.kt$IOutcomeEvent - UndocumentedPublicClass:IParamsBackendService.kt$FCMParamsObject - UndocumentedPublicClass:IParamsBackendService.kt$IParamsBackendService - UndocumentedPublicClass:IParamsBackendService.kt$InfluenceParamsObject - UndocumentedPublicClass:IParamsBackendService.kt$ParamsObject UndocumentedPublicClass:IPreferencesService.kt$PreferenceOneSignalKeys UndocumentedPublicClass:IPreferencesService.kt$PreferencePlayerPurchasesKeys UndocumentedPublicClass:IPreferencesService.kt$PreferenceStores @@ -557,8 +572,6 @@ UndocumentedPublicFunction:Logging.kt$Logging$@JvmStatic fun warn( message: String, throwable: Throwable? = null, ) UndocumentedPublicFunction:Logging.kt$Logging$fun addListener(listener: ILogListener) UndocumentedPublicFunction:Logging.kt$Logging$fun removeListener(listener: ILogListener) - UndocumentedPublicFunction:LoginHelper.kt$LoginHelper$suspend fun login( externalId: String, jwtBearerToken: String? = null, ) - UndocumentedPublicFunction:LogoutHelper.kt$LogoutHelper$fun logout() UndocumentedPublicFunction:Model.kt$Model$fun <T> setListProperty( name: String, value: List<T>, tag: String = ModelChangeTags.NORMAL, forceChange: Boolean = false, ) UndocumentedPublicFunction:Model.kt$Model$fun <T> setMapModelProperty( name: String, value: MapModel<T>, tag: String = ModelChangeTags.NORMAL, forceChange: Boolean = false, ) UndocumentedPublicFunction:Model.kt$Model$fun <T> setOptListProperty( name: String, value: List<T>?, tag: String = ModelChangeTags.NORMAL, forceChange: Boolean = false, ) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt index d1ea2036c2..2b6e1267f4 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt @@ -159,17 +159,10 @@ internal class HttpClient( con.doOutput = true } - logHTTPSent(con.requestMethod, con.url, jsonBody, con.requestProperties) - - if (jsonBody != null) { - val strJsonBody = JSONUtils.toUnescapedEUIDString(jsonBody) - val sendBytes = strJsonBody.toByteArray(charset("UTF-8")) - con.setFixedLengthStreamingMode(sendBytes.size) - val outputStream = con.outputStream - outputStream.write(sendBytes) - } - - // H E A D E R S + // H E A D E R S — must be set before any body write below. `getOutputStream()` + // (and `setFixedLengthStreamingMode`) commit the request line + headers to the + // wire; `setRequestProperty` after that point either throws IllegalStateException + // or is silently dropped, depending on the HttpURLConnection implementation. if (headers?.cacheKey != null) { val eTag = @@ -195,6 +188,20 @@ internal class HttpClient( con.setRequestProperty("OneSignal-Session-Duration", headers.sessionDuration.toString()) } + if (headers?.jwt != null) { + con.setRequestProperty("Authorization", "Bearer ${headers.jwt}") + } + + logHTTPSent(con.requestMethod, con.url, jsonBody, con.requestProperties) + + if (jsonBody != null) { + val strJsonBody = JSONUtils.toUnescapedEUIDString(jsonBody) + val sendBytes = strJsonBody.toByteArray(charset("UTF-8")) + con.setFixedLengthStreamingMode(sendBytes.size) + val outputStream = con.outputStream + outputStream.write(sendBytes) + } + // Network request is made from getResponseCode() httpResponse = con.responseCode diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/OptionalHeaders.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/OptionalHeaders.kt index f566fd04fc..8a0f3e7c95 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/OptionalHeaders.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/OptionalHeaders.kt @@ -17,4 +17,9 @@ data class OptionalHeaders( * Used to track delay between session start and request */ val sessionDuration: Long? = null, + /** + * JWT bearer token for identity verification. When non-null, sent as + * `Authorization: Bearer ` on the request. + */ + val jwt: String? = null, ) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IIdentityBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IIdentityBackendService.kt index 4278d8002b..b59dd71917 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IIdentityBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IIdentityBackendService.kt @@ -18,6 +18,7 @@ interface IIdentityBackendService { aliasLabel: String, aliasValue: String, identities: Map, + jwt: String? = null, ): Map /** @@ -35,6 +36,7 @@ interface IIdentityBackendService { aliasLabel: String, aliasValue: String, aliasLabelToDelete: String, + jwt: String? = null, ) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/ISubscriptionBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/ISubscriptionBackendService.kt index 7bcf23fdb2..45089fcb13 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/ISubscriptionBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/ISubscriptionBackendService.kt @@ -22,6 +22,7 @@ interface ISubscriptionBackendService { aliasLabel: String, aliasValue: String, subscription: SubscriptionObject, + jwt: String? = null, ): Pair? /** @@ -35,6 +36,7 @@ interface ISubscriptionBackendService { appId: String, subscriptionId: String, subscription: SubscriptionObject, + jwt: String? = null, ): RywData? /** @@ -46,6 +48,7 @@ interface ISubscriptionBackendService { suspend fun deleteSubscription( appId: String, subscriptionId: String, + jwt: String? = null, ) /** @@ -61,11 +64,15 @@ interface ISubscriptionBackendService { subscriptionId: String, aliasLabel: String, aliasValue: String, + jwt: String? = null, ) /** * Given an existing subscription, retrieve all identities associated to it. * + * Note: this endpoint is not used when `jwt_required == true`; the v4→v5 migration path it + * supports is explicitly blocked under IV. See [LoginUserFromSubscriptionOperationExecutor]. + * * @param appId The ID of the OneSignal application this subscription exists under. * @param subscriptionId The ID of the subscription to retrieve identities for. * diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IUserBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IUserBackendService.kt index 4cec114b5a..b849fc4c42 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IUserBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/IUserBackendService.kt @@ -24,6 +24,7 @@ interface IUserBackendService { identities: Map, subscriptions: List, properties: Map, + jwt: String? = null, ): CreateUserResponse // TODO: Change to send only the push subscription, optimally @@ -48,6 +49,7 @@ interface IUserBackendService { properties: PropertiesObject, refreshDeviceMetadata: Boolean, propertyiesDelta: PropertiesDeltasObject, + jwt: String? = null, ): RywData? /** @@ -65,6 +67,7 @@ interface IUserBackendService { appId: String, aliasLabel: String, aliasValue: String, + jwt: String? = null, ): CreateUserResponse } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/IdentityBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/IdentityBackendService.kt index adfff7bdc9..1bfd8c14af 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/IdentityBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/IdentityBackendService.kt @@ -4,6 +4,7 @@ import com.onesignal.common.exceptions.BackendException import com.onesignal.common.putMap import com.onesignal.common.toMap import com.onesignal.core.internal.http.IHttpClient +import com.onesignal.core.internal.http.impl.OptionalHeaders import com.onesignal.user.internal.backend.IIdentityBackendService import org.json.JSONObject @@ -15,12 +16,14 @@ internal class IdentityBackendService( aliasLabel: String, aliasValue: String, identities: Map, + jwt: String?, ): Map { val requestJSONObject = JSONObject() .put("identity", JSONObject().putMap(identities)) - val response = _httpClient.patch("apps/$appId/users/by/$aliasLabel/$aliasValue/identity", requestJSONObject) + val response = + _httpClient.patch("apps/$appId/users/by/$aliasLabel/$aliasValue/identity", requestJSONObject, OptionalHeaders(jwt = jwt)) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -36,8 +39,10 @@ internal class IdentityBackendService( aliasLabel: String, aliasValue: String, aliasLabelToDelete: String, + jwt: String?, ) { - val response = _httpClient.delete("apps/$appId/users/by/$aliasLabel/$aliasValue/identity/$aliasLabelToDelete") + val response = + _httpClient.delete("apps/$appId/users/by/$aliasLabel/$aliasValue/identity/$aliasLabelToDelete", OptionalHeaders(jwt = jwt)) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) 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 a2266d4d36..995e45393e 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 @@ -7,6 +7,7 @@ import com.onesignal.common.safeLong import com.onesignal.common.safeString import com.onesignal.common.toMap import com.onesignal.core.internal.http.IHttpClient +import com.onesignal.core.internal.http.impl.OptionalHeaders import com.onesignal.user.internal.backend.ISubscriptionBackendService import com.onesignal.user.internal.backend.SubscriptionObject import org.json.JSONObject @@ -19,11 +20,13 @@ internal class SubscriptionBackendService( aliasLabel: String, aliasValue: String, subscription: SubscriptionObject, + jwt: String?, ): Pair? { val jsonSubscription = JSONConverter.convertToJSON(subscription) val requestJSON = JSONObject().put("subscription", jsonSubscription) - val response = _httpClient.post("apps/$appId/users/by/$aliasLabel/$aliasValue/subscriptions", requestJSON) + val response = + _httpClient.post("apps/$appId/users/by/$aliasLabel/$aliasValue/subscriptions", requestJSON, OptionalHeaders(jwt = jwt)) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -50,12 +53,13 @@ internal class SubscriptionBackendService( appId: String, subscriptionId: String, subscription: SubscriptionObject, + jwt: String?, ): RywData? { val requestJSON = JSONObject() .put("subscription", JSONConverter.convertToJSON(subscription)) - val response = _httpClient.patch("apps/$appId/subscriptions/$subscriptionId", requestJSON) + val response = _httpClient.patch("apps/$appId/subscriptions/$subscriptionId", requestJSON, OptionalHeaders(jwt = jwt)) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -76,8 +80,9 @@ internal class SubscriptionBackendService( override suspend fun deleteSubscription( appId: String, subscriptionId: String, + jwt: String?, ) { - val response = _httpClient.delete("apps/$appId/subscriptions/$subscriptionId") + val response = _httpClient.delete("apps/$appId/subscriptions/$subscriptionId", OptionalHeaders(jwt = jwt)) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -89,12 +94,13 @@ internal class SubscriptionBackendService( subscriptionId: String, aliasLabel: String, aliasValue: String, + jwt: String?, ) { val requestJSON = JSONObject() .put("identity", JSONObject().put(aliasLabel, aliasValue)) - val response = _httpClient.patch("apps/$appId/subscriptions/$subscriptionId/owner", requestJSON) + val response = _httpClient.patch("apps/$appId/subscriptions/$subscriptionId/owner", requestJSON, OptionalHeaders(jwt = jwt)) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/UserBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/UserBackendService.kt index 1a1514018f..464c4963da 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/UserBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/UserBackendService.kt @@ -6,6 +6,7 @@ import com.onesignal.common.putMap import com.onesignal.common.safeLong import com.onesignal.common.safeString import com.onesignal.core.internal.http.IHttpClient +import com.onesignal.core.internal.http.impl.OptionalHeaders import com.onesignal.user.internal.backend.CreateUserResponse import com.onesignal.user.internal.backend.IUserBackendService import com.onesignal.user.internal.backend.PropertiesDeltasObject @@ -21,6 +22,7 @@ internal class UserBackendService( identities: Map, subscriptions: List, properties: Map, + jwt: String?, ): CreateUserResponse { val requestJSON = JSONObject() @@ -39,7 +41,7 @@ internal class UserBackendService( requestJSON.put("refresh_device_metadata", true) - val response = _httpClient.post("apps/$appId/users", requestJSON) + val response = _httpClient.post("apps/$appId/users", requestJSON, OptionalHeaders(jwt = jwt)) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -55,6 +57,7 @@ internal class UserBackendService( properties: PropertiesObject, refreshDeviceMetadata: Boolean, propertyiesDelta: PropertiesDeltasObject, + jwt: String?, ): RywData? { val jsonObject = JSONObject() @@ -68,7 +71,7 @@ internal class UserBackendService( jsonObject.put("deltas", JSONConverter.convertToJSON(propertyiesDelta)) } - val response = _httpClient.patch("apps/$appId/users/by/$aliasLabel/$aliasValue", jsonObject) + val response = _httpClient.patch("apps/$appId/users/by/$aliasLabel/$aliasValue", jsonObject, OptionalHeaders(jwt = jwt)) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) @@ -90,8 +93,9 @@ internal class UserBackendService( appId: String, aliasLabel: String, aliasValue: String, + jwt: String?, ): CreateUserResponse { - val response = _httpClient.get("apps/$appId/users/by/$aliasLabel/$aliasValue") + val response = _httpClient.get("apps/$appId/users/by/$aliasLabel/$aliasValue", OptionalHeaders(jwt = jwt)) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt index 92474635ab..8c624f1f76 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt @@ -20,5 +20,6 @@ interface ICustomEventBackendService { eventName: String, eventProperties: String?, metadata: CustomEventMetadata, + jwt: String? = null, ): ExecutionResponse } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt index 096fa67456..b6ff7e7033 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt @@ -3,6 +3,7 @@ package com.onesignal.user.internal.customEvents.impl import com.onesignal.common.DateUtils import com.onesignal.common.exceptions.BackendException import com.onesignal.core.internal.http.IHttpClient +import com.onesignal.core.internal.http.impl.OptionalHeaders import com.onesignal.core.internal.operations.ExecutionResponse import com.onesignal.core.internal.operations.ExecutionResult import com.onesignal.user.internal.customEvents.ICustomEventBackendService @@ -21,6 +22,7 @@ internal class CustomEventBackendService( eventName: String, eventProperties: String?, metadata: CustomEventMetadata, + jwt: String?, ): ExecutionResponse { val body = JSONObject() body.put("name", eventName) @@ -42,7 +44,7 @@ internal class CustomEventBackendService( body.put("payload", payload) val jsonObject = JSONObject().put("events", JSONArray().put(body)) - val response = httpClient.post("apps/$appId/custom_events", jsonObject) + val response = httpClient.post("apps/$appId/custom_events", jsonObject, OptionalHeaders(jwt = jwt)) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt index 2e1046e6c6..03e1265c50 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt @@ -6,6 +6,7 @@ import com.onesignal.common.NetworkUtils import com.onesignal.common.OneSignalUtils import com.onesignal.common.exceptions.BackendException import com.onesignal.core.internal.application.IApplicationService +import com.onesignal.core.internal.config.impl.IdentityVerificationService import com.onesignal.core.internal.device.IDeviceService import com.onesignal.core.internal.operations.ExecutionResponse import com.onesignal.core.internal.operations.ExecutionResult @@ -13,12 +14,15 @@ import com.onesignal.core.internal.operations.IOperationExecutor import com.onesignal.core.internal.operations.Operation import com.onesignal.user.internal.customEvents.ICustomEventBackendService import com.onesignal.user.internal.customEvents.impl.CustomEventMetadata +import com.onesignal.user.internal.jwt.JwtTokenStore import com.onesignal.user.internal.operations.TrackCustomEventOperation internal class CustomEventOperationExecutor( private val customEventBackendService: ICustomEventBackendService, private val applicationService: IApplicationService, private val deviceService: IDeviceService, + private val jwtTokenStore: JwtTokenStore, + private val identityVerificationService: IdentityVerificationService, ) : IOperationExecutor { override val operations: List get() = listOf(CUSTOM_EVENT) @@ -40,6 +44,7 @@ internal class CustomEventOperationExecutor( try { when (operation) { is TrackCustomEventOperation -> { + val jwt = resolveJwt(operation, jwtTokenStore, identityVerificationService) customEventBackendService.sendCustomEvent( operation.appId, operation.onesignalId, @@ -48,6 +53,7 @@ internal class CustomEventOperationExecutor( operation.eventName, operation.eventProperties, eventMetadataJson, + jwt, ) } } @@ -57,6 +63,8 @@ internal class CustomEventOperationExecutor( return when (responseType) { NetworkUtils.ResponseStatusType.RETRYABLE -> ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds) + NetworkUtils.ResponseStatusType.UNAUTHORIZED -> + ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED, retryAfterSeconds = ex.retryAfterSeconds) else -> ExecutionResponse(ExecutionResult.FAIL_NORETRY) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/ExecutorsIvExtensions.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/ExecutorsIvExtensions.kt new file mode 100644 index 0000000000..4eb85e02ce --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/ExecutorsIvExtensions.kt @@ -0,0 +1,117 @@ +package com.onesignal.user.internal.operations.impl.executors + +import com.onesignal.core.internal.config.impl.IdentityVerificationService +import com.onesignal.core.internal.operations.Operation +import com.onesignal.debug.internal.logging.Logging +import com.onesignal.user.internal.backend.IdentityConstants +import com.onesignal.user.internal.jwt.JwtTokenStore + +/** + * IV-specific parameter resolution for operation executors. Base-class call sites dispatch via + * `if (newCodePathsRun) resolveIvBackendParams(...) else IvBackendParams.legacyFor(...)`; + * the inner `ivBehaviorActive` check keeps Phase 3 users (new code path on, IV behavior off) + * on legacy alias/jwt values so they exercise the dispatch without any behavioral change. + * + * `ivBehaviorActive` is passed in by the executor (read from the injected + * `IdentityVerificationService`) — extension functions are not classes and cannot inject. + */ + +/** Alias + JWT triplet passed into backend calls. */ +internal data class IvBackendParams( + val aliasLabel: String, + val aliasValue: String, + val jwt: String?, +) { + companion object { + /** Values used when the new code path is off or IV behavior is inactive. Byte-for-byte identical to pre-IV behavior. */ + fun legacyFor(onesignalId: String) = IvBackendParams(IdentityConstants.ONESIGNAL_ID, onesignalId, null) + } +} + +/** + * Combined outer + inner gate for executors that just want "the right alias/JWT for this op." + * Replaces the call-site `if (newCodePathsRun) resolveIvBackendParams(...) else legacyFor(...)` + * boilerplate. Phase 1 (newCodePathsRun=false) skips the new code path entirely and returns + * legacy values directly, preserving the rollout-safety property that Phase 1 traffic does not + * exercise the IV resolution code. + */ +internal fun resolveBackendParams( + op: Operation, + onesignalId: String, + jwtTokenStore: JwtTokenStore, + identityVerificationService: IdentityVerificationService, +): IvBackendParams = + if (identityVerificationService.newCodePathsRun) { + resolveIvBackendParams(op, onesignalId, jwtTokenStore, identityVerificationService.ivBehaviorActive) + } else { + IvBackendParams.legacyFor(onesignalId) + } + +/** + * Resolves alias + JWT for a backend call. Intended to be called only when + * `newCodePathsRun` is true; base-class dispatch handles the outer gate. Most executors should + * call [resolveBackendParams] which encapsulates the outer gate. + * + * - IV behavior inactive (Phase 3) → legacy values; no alias switch, no JWT attach. + * - IV behavior active + op has externalId → `external_id` alias + JWT from [JwtTokenStore]. + * Note: `hasValidJwtIfRequired` already keeps anon ops and missing-JWT ops out of dispatch while + * IV is active, so the null-externalId / missing-JWT branches below are defensive only. + * + * [onesignalId] is passed explicitly because the base [Operation] class doesn't expose it; + * concrete executors all have their own onesignalId field on the operation they're about to send. + */ +internal fun resolveIvBackendParams( + op: Operation, + onesignalId: String, + jwtTokenStore: JwtTokenStore, + ivBehaviorActive: Boolean, +): IvBackendParams { + if (!ivBehaviorActive) return IvBackendParams.legacyFor(onesignalId) + val externalId = op.externalId + if (externalId == null) { + Logging.error("IV active but op has null externalId; falling back to onesignal_id") + return IvBackendParams.legacyFor(onesignalId) + } + return IvBackendParams(IdentityConstants.EXTERNAL_ID, externalId, jwtTokenStore.getJwt(externalId)) +} + +/** + * Combined outer + inner gate for executors that need only a JWT (no alias switch). Replaces + * the call-site `if (newCodePathsRun) resolveIvJwt(...) else null` boilerplate. Phase 1 + * (newCodePathsRun=false) skips the new code path entirely and returns null directly, + * preserving the rollout-safety property that Phase 1 traffic does not exercise IV resolution. + */ +internal fun resolveJwt( + op: Operation, + jwtTokenStore: JwtTokenStore, + identityVerificationService: IdentityVerificationService, +): String? = + if (identityVerificationService.newCodePathsRun) { + resolveIvJwt(op, jwtTokenStore, identityVerificationService.ivBehaviorActive) + } else { + null + } + +/** + * Resolves a JWT-only parameter (used by endpoints that don't take alias label/value, e.g. + * subscription update/delete, custom events). Same inner gating as [resolveIvBackendParams]. + * Intended to be called only when `newCodePathsRun` is true; most executors should call + * [resolveJwt] which encapsulates the outer gate. + */ +internal fun resolveIvJwt( + op: Operation, + jwtTokenStore: JwtTokenStore, + ivBehaviorActive: Boolean, +): String? { + if (!ivBehaviorActive) return null + val externalId = op.externalId ?: return null + return jwtTokenStore.getJwt(externalId) +} + +/** + * LoginUserFromSubscription path is blocked under IV — the backend endpoint the v4→v5 upgrade path + * relies on is not allowed when `jwt_required == true`. Returns `true` when the executor should + * short-circuit to `FAIL_NORETRY`. Called only when `newCodePathsRun` is true; Phase 3 users + * (behavior inactive) fall through and use the legacy migration path. + */ +internal fun shouldFailLoginUserFromSubscription(ivBehaviorActive: Boolean): Boolean = ivBehaviorActive diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/IdentityOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/IdentityOperationExecutor.kt index 104fe9569f..424690bb36 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/IdentityOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/IdentityOperationExecutor.kt @@ -3,15 +3,16 @@ package com.onesignal.user.internal.operations.impl.executors import com.onesignal.common.NetworkUtils import com.onesignal.common.exceptions.BackendException import com.onesignal.common.modeling.ModelChangeTags +import com.onesignal.core.internal.config.impl.IdentityVerificationService import com.onesignal.core.internal.operations.ExecutionResponse import com.onesignal.core.internal.operations.ExecutionResult import com.onesignal.core.internal.operations.IOperationExecutor import com.onesignal.core.internal.operations.Operation import com.onesignal.debug.internal.logging.Logging import com.onesignal.user.internal.backend.IIdentityBackendService -import com.onesignal.user.internal.backend.IdentityConstants import com.onesignal.user.internal.builduser.IRebuildUserService import com.onesignal.user.internal.identity.IdentityModelStore +import com.onesignal.user.internal.jwt.JwtTokenStore import com.onesignal.user.internal.operations.DeleteAliasOperation import com.onesignal.user.internal.operations.SetAliasOperation import com.onesignal.user.internal.operations.impl.states.NewRecordsState @@ -21,6 +22,8 @@ internal class IdentityOperationExecutor( private val _identityModelStore: IdentityModelStore, private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, + private val _jwtTokenStore: JwtTokenStore, + private val _identityVerificationService: IdentityVerificationService, ) : IOperationExecutor { override val operations: List get() = listOf(SET_ALIAS, DELETE_ALIAS) @@ -44,12 +47,14 @@ internal class IdentityOperationExecutor( val lastOperation = operations.last() if (lastOperation is SetAliasOperation) { + val params = resolveBackendParams(lastOperation, lastOperation.onesignalId, _jwtTokenStore, _identityVerificationService) try { _identityBackend.setAlias( lastOperation.appId, - IdentityConstants.ONESIGNAL_ID, - lastOperation.onesignalId, + params.aliasLabel, + params.aliasValue, mapOf(lastOperation.label to lastOperation.value), + params.jwt, ) // ensure the now created alias is in the model as long as the user is still current. @@ -87,12 +92,14 @@ internal class IdentityOperationExecutor( } } } else if (lastOperation is DeleteAliasOperation) { + val params = resolveBackendParams(lastOperation, lastOperation.onesignalId, _jwtTokenStore, _identityVerificationService) try { _identityBackend.deleteAlias( lastOperation.appId, - IdentityConstants.ONESIGNAL_ID, - lastOperation.onesignalId, + params.aliasLabel, + params.aliasValue, lastOperation.label, + params.jwt, ) // ensure the now deleted alias is not in the model as long as the user is still current. diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserFromSubscriptionOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserFromSubscriptionOperationExecutor.kt index 2894f52631..76d5551854 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserFromSubscriptionOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserFromSubscriptionOperationExecutor.kt @@ -3,6 +3,7 @@ package com.onesignal.user.internal.operations.impl.executors import com.onesignal.common.NetworkUtils import com.onesignal.common.exceptions.BackendException import com.onesignal.common.modeling.ModelChangeTags +import com.onesignal.core.internal.config.impl.IdentityVerificationService import com.onesignal.core.internal.operations.ExecutionResponse import com.onesignal.core.internal.operations.ExecutionResult import com.onesignal.core.internal.operations.IOperationExecutor @@ -20,6 +21,7 @@ internal class LoginUserFromSubscriptionOperationExecutor( private val _subscriptionBackend: ISubscriptionBackendService, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, + private val _identityVerificationService: IdentityVerificationService, ) : IOperationExecutor { override val operations: List get() = listOf(LOGIN_USER_FROM_SUBSCRIPTION_USER) @@ -27,6 +29,13 @@ internal class LoginUserFromSubscriptionOperationExecutor( override suspend fun execute(operations: List): ExecutionResponse { Logging.debug("LoginUserFromSubscriptionOperationExecutor(operation: $operations)") + // The getIdentityFromSubscription endpoint isn't allowed when `jwt_required == true`; the + // v4→v5 migration path this executor exists for only applies to non-IV apps. Drop on IV. + if (_identityVerificationService.newCodePathsRun && shouldFailLoginUserFromSubscription(_identityVerificationService.ivBehaviorActive)) { + Logging.warn("LoginUserFromSubscriptionOperation is not supported when identity verification is enabled. Dropping.") + return ExecutionResponse(ExecutionResult.FAIL_NORETRY) + } + if (operations.size > 1) { throw Exception("Only supports one operation! Attempted operations:\n$operations") } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt index 1d68b9a26b..4b980e801a 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt @@ -12,6 +12,7 @@ import com.onesignal.common.exceptions.BackendException import com.onesignal.common.modeling.ModelChangeTags import com.onesignal.core.internal.application.IApplicationService import com.onesignal.core.internal.config.ConfigModelStore +import com.onesignal.core.internal.config.impl.IdentityVerificationService import com.onesignal.core.internal.device.IDeviceService import com.onesignal.core.internal.language.ILanguageContext import com.onesignal.core.internal.operations.ExecutionResponse @@ -24,6 +25,7 @@ import com.onesignal.user.internal.backend.IdentityConstants import com.onesignal.user.internal.backend.SubscriptionObject import com.onesignal.user.internal.backend.SubscriptionObjectType import com.onesignal.user.internal.identity.IdentityModelStore +import com.onesignal.user.internal.jwt.JwtTokenStore import com.onesignal.user.internal.operations.CreateSubscriptionOperation import com.onesignal.user.internal.operations.DeleteSubscriptionOperation import com.onesignal.user.internal.operations.LoginUserOperation @@ -47,6 +49,8 @@ internal class LoginUserOperationExecutor( private val _subscriptionsModelStore: SubscriptionModelStore, private val _configModelStore: ConfigModelStore, private val _languageContext: ILanguageContext, + private val _jwtTokenStore: JwtTokenStore, + private val _identityVerificationService: IdentityVerificationService, ) : IOperationExecutor { override val operations: List get() = listOf(LOGIN_USER) @@ -74,10 +78,18 @@ internal class LoginUserOperationExecutor( if (!containsSubscriptionOperation && loginUserOp.externalId == null) { return ExecutionResponse(ExecutionResult.FAIL_NORETRY) } - if (loginUserOp.existingOnesignalId == null || loginUserOp.externalId == null) { + if (loginUserOp.existingOnesignalId == null || loginUserOp.externalId == null || + _identityVerificationService.ivBehaviorActive + ) { // When there is no existing user to attempt to associate with the externalId provided, we go right to // createUser. If there is no externalId provided this is an insert, if there is this will be an // "upsert with retrieval" as the user may already exist. + // + // Under IV, also skip the optimistic SetAliasOperation: that inline op identifies the + // target user by `onesignal_id = existingOnesignalId`, but IV's alias-resolution would + // rewrite the call to identify by the new (not-yet-registered) `external_id`, producing + // a 404 or idempotent success against the wrong user. createUser's identities-map path + // handles the merge correctly through backend upsert semantics. return createUser(loginUserOp, operations) } else { // before we create a user we attempt to associate the user defined by existingOnesignalId with the @@ -169,7 +181,9 @@ internal class LoginUserOperationExecutor( try { val subscriptionList = subscriptions.toList() - val response = _userBackend.createUser(createUserOperation.appId, identities, subscriptionList.map { it.second }, properties) + val jwt = resolveJwt(createUserOperation, _jwtTokenStore, _identityVerificationService) + val response = + _userBackend.createUser(createUserOperation.appId, identities, subscriptionList.map { it.second }, properties, jwt) val idTranslations = mutableMapOf() // Add the "local-to-backend" ID translation to the IdentifierTranslator for any operations that were // *not* executed but still reference the locally-generated IDs. diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/RefreshUserOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/RefreshUserOperationExecutor.kt index d7bfa0f671..3d3784b0db 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/RefreshUserOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/RefreshUserOperationExecutor.kt @@ -5,6 +5,7 @@ import com.onesignal.common.TimeUtils import com.onesignal.common.exceptions.BackendException import com.onesignal.common.modeling.ModelChangeTags import com.onesignal.core.internal.config.ConfigModelStore +import com.onesignal.core.internal.config.impl.IdentityVerificationService import com.onesignal.core.internal.operations.ExecutionResponse import com.onesignal.core.internal.operations.ExecutionResult import com.onesignal.core.internal.operations.IOperationExecutor @@ -12,11 +13,11 @@ import com.onesignal.core.internal.operations.Operation import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.user.internal.backend.IUserBackendService -import com.onesignal.user.internal.backend.IdentityConstants import com.onesignal.user.internal.backend.SubscriptionObjectType import com.onesignal.user.internal.builduser.IRebuildUserService import com.onesignal.user.internal.identity.IdentityModel import com.onesignal.user.internal.identity.IdentityModelStore +import com.onesignal.user.internal.jwt.JwtTokenStore import com.onesignal.user.internal.operations.RefreshUserOperation import com.onesignal.user.internal.operations.impl.states.NewRecordsState import com.onesignal.user.internal.properties.PropertiesModel @@ -34,6 +35,8 @@ internal class RefreshUserOperationExecutor( private val _configModelStore: ConfigModelStore, private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, + private val _jwtTokenStore: JwtTokenStore, + private val _identityVerificationService: IdentityVerificationService, ) : IOperationExecutor { override val operations: List get() = listOf(REFRESH_USER) @@ -54,12 +57,14 @@ internal class RefreshUserOperationExecutor( } private suspend fun getUser(op: RefreshUserOperation): ExecutionResponse { + val params = resolveBackendParams(op, op.onesignalId, _jwtTokenStore, _identityVerificationService) try { val response = _userBackend.getUser( op.appId, - IdentityConstants.ONESIGNAL_ID, - op.onesignalId, + params.aliasLabel, + params.aliasValue, + params.jwt, ) if (op.onesignalId != _identityModelStore.model.onesignalId) { diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/SubscriptionOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/SubscriptionOperationExecutor.kt index e6c82f1a70..3d240636a5 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/SubscriptionOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/SubscriptionOperationExecutor.kt @@ -14,6 +14,7 @@ import com.onesignal.common.exceptions.BackendException import com.onesignal.common.modeling.ModelChangeTags import com.onesignal.core.internal.application.IApplicationService import com.onesignal.core.internal.config.ConfigModelStore +import com.onesignal.core.internal.config.impl.IdentityVerificationService import com.onesignal.core.internal.device.IDeviceService import com.onesignal.core.internal.operations.ExecutionResponse import com.onesignal.core.internal.operations.ExecutionResult @@ -22,10 +23,10 @@ import com.onesignal.core.internal.operations.Operation import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.user.internal.backend.ISubscriptionBackendService -import com.onesignal.user.internal.backend.IdentityConstants import com.onesignal.user.internal.backend.SubscriptionObject import com.onesignal.user.internal.backend.SubscriptionObjectType import com.onesignal.user.internal.builduser.IRebuildUserService +import com.onesignal.user.internal.jwt.JwtTokenStore import com.onesignal.user.internal.operations.CreateSubscriptionOperation import com.onesignal.user.internal.operations.DeleteSubscriptionOperation import com.onesignal.user.internal.operations.TransferSubscriptionOperation @@ -44,6 +45,8 @@ internal class SubscriptionOperationExecutor( private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, private val _consistencyManager: IConsistencyManager, + private val _jwtTokenStore: JwtTokenStore, + private val _identityVerificationService: IdentityVerificationService, ) : IOperationExecutor { override val operations: List get() = listOf(CREATE_SUBSCRIPTION, UPDATE_SUBSCRIPTION, DELETE_SUBSCRIPTION, TRANSFER_SUBSCRIPTION) @@ -107,12 +110,14 @@ internal class SubscriptionOperationExecutor( AndroidUtils.getAppVersion(_applicationService.appContext), ) + val params = resolveBackendParams(createOperation, createOperation.onesignalId, _jwtTokenStore, _identityVerificationService) val result = _subscriptionBackend.createSubscription( createOperation.appId, - IdentityConstants.ONESIGNAL_ID, - createOperation.onesignalId, + params.aliasLabel, + params.aliasValue, subscription, + params.jwt, ) ?: return ExecutionResponse(ExecutionResult.SUCCESS) val backendSubscriptionId = result.first @@ -190,7 +195,8 @@ internal class SubscriptionOperationExecutor( AndroidUtils.getAppVersion(_applicationService.appContext), ) - val rywData = _subscriptionBackend.updateSubscription(lastOperation.appId, lastOperation.subscriptionId, subscription) + val jwt = resolveJwt(lastOperation, _jwtTokenStore, _identityVerificationService) + val rywData = _subscriptionBackend.updateSubscription(lastOperation.appId, lastOperation.subscriptionId, subscription, jwt) if (rywData != null) { _consistencyManager.setRywData(startingOperation.onesignalId, IamFetchRywTokenKey.SUBSCRIPTION, rywData) @@ -203,6 +209,8 @@ internal class SubscriptionOperationExecutor( return when (responseType) { NetworkUtils.ResponseStatusType.RETRYABLE -> ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds) + NetworkUtils.ResponseStatusType.UNAUTHORIZED -> + ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED, retryAfterSeconds = ex.retryAfterSeconds) NetworkUtils.ResponseStatusType.MISSING -> { if (ex.statusCode == 404 && listOf( @@ -240,12 +248,14 @@ internal class SubscriptionOperationExecutor( // TODO: whenever the end-user changes users, we need to add the read-your-write token here, currently no code to handle the re-fetch IAMs private suspend fun transferSubscription(startingOperation: TransferSubscriptionOperation): ExecutionResponse { + val params = resolveBackendParams(startingOperation, startingOperation.onesignalId, _jwtTokenStore, _identityVerificationService) try { _subscriptionBackend.transferSubscription( startingOperation.appId, startingOperation.subscriptionId, - IdentityConstants.ONESIGNAL_ID, - startingOperation.onesignalId, + params.aliasLabel, + params.aliasValue, + params.jwt, ) } catch (ex: BackendException) { val responseType = NetworkUtils.getResponseStatusType(ex.statusCode) @@ -253,6 +263,8 @@ internal class SubscriptionOperationExecutor( return when (responseType) { NetworkUtils.ResponseStatusType.RETRYABLE -> ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds) + NetworkUtils.ResponseStatusType.UNAUTHORIZED -> + ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED, retryAfterSeconds = ex.retryAfterSeconds) else -> ExecutionResponse(ExecutionResult.FAIL_NORETRY) } @@ -276,8 +288,9 @@ internal class SubscriptionOperationExecutor( } private suspend fun deleteSubscription(op: DeleteSubscriptionOperation): ExecutionResponse { + val jwt = resolveJwt(op, _jwtTokenStore, _identityVerificationService) try { - _subscriptionBackend.deleteSubscription(op.appId, op.subscriptionId) + _subscriptionBackend.deleteSubscription(op.appId, op.subscriptionId, jwt) // remove the subscription model as a HYDRATE in case for some reason it still exists. _subscriptionModelStore.remove(op.subscriptionId, ModelChangeTags.HYDRATE) @@ -300,6 +313,8 @@ internal class SubscriptionOperationExecutor( } NetworkUtils.ResponseStatusType.RETRYABLE -> ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds) + NetworkUtils.ResponseStatusType.UNAUTHORIZED -> + ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED, retryAfterSeconds = ex.retryAfterSeconds) else -> ExecutionResponse(ExecutionResult.FAIL_NORETRY) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/UpdateUserOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/UpdateUserOperationExecutor.kt index e529035ec1..f611c60ea5 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/UpdateUserOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/UpdateUserOperationExecutor.kt @@ -6,6 +6,7 @@ import com.onesignal.common.consistency.enums.IamFetchRywTokenKey import com.onesignal.common.consistency.models.IConsistencyManager import com.onesignal.common.exceptions.BackendException import com.onesignal.common.modeling.ModelChangeTags +import com.onesignal.core.internal.config.impl.IdentityVerificationService import com.onesignal.core.internal.operations.ExecutionResponse import com.onesignal.core.internal.operations.ExecutionResult import com.onesignal.core.internal.operations.IOperationExecutor @@ -13,12 +14,12 @@ import com.onesignal.core.internal.operations.Operation import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.user.internal.backend.IUserBackendService -import com.onesignal.user.internal.backend.IdentityConstants import com.onesignal.user.internal.backend.PropertiesDeltasObject import com.onesignal.user.internal.backend.PropertiesObject import com.onesignal.user.internal.backend.PurchaseObject import com.onesignal.user.internal.builduser.IRebuildUserService import com.onesignal.user.internal.identity.IdentityModelStore +import com.onesignal.user.internal.jwt.JwtTokenStore import com.onesignal.user.internal.operations.DeleteTagOperation import com.onesignal.user.internal.operations.SetPropertyOperation import com.onesignal.user.internal.operations.SetTagOperation @@ -35,6 +36,8 @@ internal class UpdateUserOperationExecutor( private val _buildUserService: IRebuildUserService, private val _newRecordState: NewRecordsState, private val _consistencyManager: IConsistencyManager, + private val _jwtTokenStore: JwtTokenStore, + private val _identityVerificationService: IdentityVerificationService, ) : IOperationExecutor { override val operations: List get() = listOf(SET_TAG, DELETE_TAG, SET_PROPERTY, TRACK_SESSION_START, TRACK_SESSION_END, TRACK_PURCHASE) @@ -137,15 +140,20 @@ internal class UpdateUserOperationExecutor( } if (appId != null && onesignalId != null) { + // Grouped update ops all belong to the same user (queue grouping is per-user), so + // pulling externalId from `operations.first()` is safe and represents the batch. + val firstOp = operations.first() + val params = resolveBackendParams(firstOp, onesignalId, _jwtTokenStore, _identityVerificationService) try { val rywData = _userBackend.updateUser( appId, - IdentityConstants.ONESIGNAL_ID, - onesignalId, + params.aliasLabel, + params.aliasValue, propertiesObject, refreshDeviceMetadata, deltasObject, + params.jwt, ) if (rywData != null) { diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/HttpClientTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/HttpClientTests.kt index 0fc991b861..93f9776b31 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/HttpClientTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/HttpClientTests.kt @@ -255,4 +255,48 @@ class HttpClientTests : FunSpec({ response2 shouldBe null response3 shouldNotBe null } + + test("Authorization header is set when OptionalHeaders.jwt is provided") { + // Given + val mocks = Mocks() + + // When + mocks.httpClient.get("URL", OptionalHeaders(jwt = "abc.def.ghi")) + mocks.httpClient.post("URL", JSONObject(), OptionalHeaders(jwt = "abc.def.ghi")) + + // Then + for (connection in mocks.factory.connections) { + connection.getRequestProperty("Authorization") shouldBe "Bearer abc.def.ghi" + } + } + + test("Authorization header is NOT set when OptionalHeaders.jwt is null") { + // Given + val mocks = Mocks() + + // When + mocks.httpClient.get("URL", OptionalHeaders(jwt = null)) + mocks.httpClient.post("URL", JSONObject()) + + // Then + for (connection in mocks.factory.connections) { + connection.getRequestProperty("Authorization") shouldBe null + } + } + + test("POST + JWT does not throw (headers must be set before body write)") { + // Given: MockHttpURLConnection now throws IllegalStateException if setRequestProperty is + // called after connect()/getOutputStream()/getResponseCode (matching real Android behavior). + // This test fails if the HttpClient regresses to setting Authorization after the body write. + val mocks = Mocks() + mocks.response.status = 200 + mocks.response.responseBody = "{}" + + // When + val response = mocks.httpClient.post("URL", JSONObject().put("k", "v"), OptionalHeaders(jwt = "the-jwt")) + + // Then: the request completed without the mock throwing, and the header actually stuck. + response.throwable shouldBe null + mocks.factory.connections.last().getRequestProperty("Authorization") shouldBe "Bearer the-jwt" + } }) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/MockHttpConnectionFactory.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/MockHttpConnectionFactory.kt index 595f6fcd51..45381b774c 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/MockHttpConnectionFactory.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/MockHttpConnectionFactory.kt @@ -32,6 +32,13 @@ internal class MockHttpConnectionFactory( url: URL?, private val mockResponse: MockResponse, ) : HttpURLConnection(url) { + // Real HttpURLConnection (both stock JDK and Android's OkHttp-backed impl) commits the + // request line + headers to the wire when `getOutputStream()` / `setFixedLengthStreamingMode` + // is used to stream a body, and rejects `setRequestProperty` afterward. The stock mock + // lets headers be set at any time, which hides header-ordering bugs. Simulate the real + // contract so HttpClientTests will fail fast if headers are set after the body begins. + private var headersCommitted: Boolean = false + override fun disconnect() {} override fun usingProxy(): Boolean { @@ -40,6 +47,14 @@ internal class MockHttpConnectionFactory( @Throws(IOException::class) override fun connect() { + headersCommitted = true + } + + override fun setRequestProperty(key: String, value: String?) { + if (headersCommitted) { + throw IllegalStateException("Cannot set request property '$key' after connection is made") + } + super.setRequestProperty(key, value) } override fun getHeaderField(name: String): String? { @@ -48,6 +63,7 @@ internal class MockHttpConnectionFactory( @Throws(IOException::class) override fun getResponseCode(): Int { + headersCommitted = true if (mockResponse.mockRequestTime != null) { try { Thread.sleep(mockResponse.mockRequestTime!!) @@ -60,6 +76,7 @@ internal class MockHttpConnectionFactory( } override fun getOutputStream(): OutputStream { + headersCommitted = true return NullOutputStream() } diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/LoggingTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/LoggingTests.kt index 4f9c377348..ba4e376473 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/LoggingTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/LoggingTests.kt @@ -22,14 +22,30 @@ infix fun > T.shouldHaveEachItemEndWith(expected: Array() + fun register(listener: ILogListener): ILogListener { + registered.add(listener) + Logging.addListener(listener) + return listener + } + beforeAny { Logging.logLevel = LogLevel.NONE } + afterEach { + registered.forEach { Logging.removeListener(it) } + registered.clear() + } + test("addListener") { // Given val listener = TestLogLister() - Logging.addListener(listener) + register(listener) // When Logging.debug("test") @@ -41,8 +57,8 @@ class LoggingTests : FunSpec({ test("addListener twice") { // Given val listener = TestLogLister() - Logging.addListener(listener) - Logging.addListener(listener) + register(listener) + register(listener) // When Logging.debug("test") @@ -54,7 +70,7 @@ class LoggingTests : FunSpec({ test("removeListener") { // Given val listener = TestLogLister() - Logging.addListener(listener) + register(listener) Logging.removeListener(listener) // When @@ -67,7 +83,7 @@ class LoggingTests : FunSpec({ test("removeListener twice") { // Given val listener = TestLogLister() - Logging.addListener(listener) + register(listener) Logging.removeListener(listener) Logging.removeListener(listener) @@ -81,7 +97,8 @@ class LoggingTests : FunSpec({ test("addListener nested") { // Given val nestedListener = TestLogLister() - Logging.addListener { Logging.addListener(nestedListener) } + val outerListener = ILogListener { register(nestedListener) } + register(outerListener) // When Logging.debug("test") @@ -101,7 +118,7 @@ class LoggingTests : FunSpec({ // Remove self from listeners Logging.removeListener(listener) } - Logging.addListener(listener) + register(listener) // When Logging.debug("test") diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt index 924d7f9f3f..3885c73d5d 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt @@ -35,7 +35,7 @@ class CustomEventBackendServiceTests : FunSpec({ test("track event") { // Given val spyHttpClient = mockk() - coEvery { spyHttpClient.post(any(), any()) } returns HttpResponse(202, "") + coEvery { spyHttpClient.post(any(), any(), any()) } returns HttpResponse(202, "") val customEventBackendService = CustomEventBackendService(spyHttpClient) // When @@ -80,6 +80,7 @@ class CustomEventBackendServiceTests : FunSpec({ payload.getJSONObject("os_sdk").toString() shouldBeEqual metadata.toJSONObject().toString() payload.getString("proKey1") shouldBeEqual "proVal1" }, + any(), ) } } diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/IdentityBackendServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/IdentityBackendServiceTests.kt index d66ee47ebf..36c99e652f 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/IdentityBackendServiceTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/IdentityBackendServiceTests.kt @@ -21,7 +21,7 @@ class IdentityBackendServiceTests : FunSpec({ val aliasLabel = "onesignal_id" val aliasValue = "11111111-1111-1111-1111-111111111111" val spyHttpClient = mockk() - coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(200, "{ identity: { aliasKey1: \"aliasValue1\"} }") + coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(200, "{ identity: { aliasKey1: \"aliasValue1\"} }") val identityBackendService = IdentityBackendService(spyHttpClient) val identities = mapOf("aliasKey1" to "aliasValue1") @@ -38,6 +38,7 @@ class IdentityBackendServiceTests : FunSpec({ it.getJSONObject("identity").has("aliasKey1") shouldBe true it.getJSONObject("identity").getString("aliasKey1") shouldBe "aliasValue1" }, + any(), ) } } @@ -48,7 +49,7 @@ class IdentityBackendServiceTests : FunSpec({ val aliasValue = "11111111-1111-1111-1111-111111111111" val aliasToDelete = "aliasKey1" val spyHttpClient = mockk() - coEvery { spyHttpClient.delete(any()) } returns HttpResponse(200, "") + coEvery { spyHttpClient.delete(any(), any()) } returns HttpResponse(200, "") val identityBackendService = IdentityBackendService(spyHttpClient) // When @@ -56,7 +57,7 @@ class IdentityBackendServiceTests : FunSpec({ // Then coVerify { - spyHttpClient.delete("apps/appId/users/by/$aliasLabel/$aliasValue/identity/$aliasToDelete") + spyHttpClient.delete("apps/appId/users/by/$aliasLabel/$aliasValue/identity/$aliasToDelete", any()) } } }) 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 0ff7a15f8f..5877ec5df9 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 @@ -27,7 +27,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, "{ \"subscription\": { id: \"subscriptionId\" }, \"ryw_token\": \"123\"}") + coEvery { spyHttpClient.post(any(), any(), any()) } returns HttpResponse(202, "{ \"subscription\": { id: \"subscriptionId\" }, \"ryw_token\": \"123\"}") val subscriptionBackendService = SubscriptionBackendService(spyHttpClient) // When @@ -55,6 +55,7 @@ class SubscriptionBackendServiceTests : FunSpec({ sub.getBoolean("enabled") shouldBe true sub.getInt("notification_types") shouldBe 1 }, + any(), ) } } @@ -64,7 +65,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(404, "NOT FOUND") + coEvery { spyHttpClient.post(any(), any(), any()) } returns HttpResponse(404, "NOT FOUND") val subscriptionBackendService = SubscriptionBackendService(spyHttpClient) // When @@ -100,6 +101,7 @@ class SubscriptionBackendServiceTests : FunSpec({ sub.getBoolean("enabled") shouldBe true sub.getInt("notification_types") shouldBe 1 }, + any(), ) } } diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/UserBackendServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/UserBackendServiceTests.kt index 0fa15fdcae..110c7566cb 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/UserBackendServiceTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/UserBackendServiceTests.kt @@ -22,7 +22,7 @@ class UserBackendServiceTests : FunSpec({ test("create user with nothing throws an exception") { // Given val spyHttpClient = mockk() - coEvery { spyHttpClient.post(any(), any()) } returns HttpResponse(403, "FORBIDDEN") + coEvery { spyHttpClient.post(any(), any(), any()) } returns HttpResponse(403, "FORBIDDEN") val userBackendService = UserBackendService(spyHttpClient) val identities = mapOf() val properties = mapOf() @@ -44,7 +44,7 @@ class UserBackendServiceTests : FunSpec({ val osId = "11111111-1111-1111-1111-111111111111" val spyHttpClient = mockk() coEvery { - spyHttpClient.post(any(), any()) + spyHttpClient.post(any(), any(), any()) } returns HttpResponse(202, "{identity:{onesignal_id: \"$osId\", aliasLabel1: \"aliasValue1\"}, properties:{timezone_id: \"testTimeZone\", language: \"testLanguage\"}}") val userBackendService = UserBackendService(spyHttpClient) val identities = mapOf("aliasLabel1" to "aliasValue1") @@ -70,6 +70,7 @@ class UserBackendServiceTests : FunSpec({ it.has("properties") shouldBe true it.has("subscriptions") shouldBe false }, + any(), ) } } @@ -79,7 +80,7 @@ class UserBackendServiceTests : FunSpec({ val osId = "11111111-1111-1111-1111-111111111111" val spyHttpClient = mockk() coEvery { - spyHttpClient.post(any(), any()) + spyHttpClient.post(any(), any(), any()) } returns HttpResponse(202, "{identity:{onesignal_id: \"$osId\"}, subscriptions:[{id:\"subscriptionId1\", type:\"AndroidPush\"}], properties:{timezone_id: \"testTimeZone\", language: \"testLanguage\"}}") val userBackendService = UserBackendService(spyHttpClient) val identities = mapOf() @@ -109,6 +110,7 @@ class UserBackendServiceTests : FunSpec({ it.getJSONArray("subscriptions").getJSONObject(0).has("type") shouldBe true it.getJSONArray("subscriptions").getJSONObject(0).getString("type") shouldBe "AndroidPush" }, + any(), ) } } @@ -118,7 +120,7 @@ class UserBackendServiceTests : FunSpec({ val aliasLabel = "onesignal_id" val aliasValue = "11111111-1111-1111-1111-111111111111" val spyHttpClient = mockk() - coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: { tags: {tagKey1: tagValue1}}}") + coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: { tags: {tagKey1: tagValue1}}}") val userBackendService = UserBackendService(spyHttpClient) val properties = PropertiesObject(tags = mapOf("tagkey1" to "tagValue1")) val propertiesDelta = PropertiesDeltasObject() @@ -136,6 +138,7 @@ class UserBackendServiceTests : FunSpec({ it.getJSONObject("properties").getJSONObject("tags").has("tagkey1") shouldBe true it.getJSONObject("properties").getJSONObject("tags").getString("tagkey1") shouldBe "tagValue1" }, + any(), ) } } @@ -145,7 +148,7 @@ class UserBackendServiceTests : FunSpec({ val aliasLabel = "onesignal_id" val aliasValue = "11111111-1111-1111-1111-111111111111" val spyHttpClient = mockk() - coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: { tags: {tagKey1: tagValue1}}}") + coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: { tags: {tagKey1: tagValue1}}}") val userBackendService = UserBackendService(spyHttpClient) val properties = PropertiesObject(language = "newLanguage") val propertiesDelta = PropertiesDeltasObject() @@ -162,6 +165,7 @@ class UserBackendServiceTests : FunSpec({ it.getJSONObject("properties").has("language") shouldBe true it.getJSONObject("properties").getString("language") shouldBe "newLanguage" }, + any(), ) } } @@ -171,7 +175,7 @@ class UserBackendServiceTests : FunSpec({ val aliasLabel = "onesignal_id" val aliasValue = "11111111-1111-1111-1111-111111111111" val spyHttpClient = mockk() - coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: { timezone_id: \"America/New_York\"}}") + coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: { timezone_id: \"America/New_York\"}}") val userBackendService = UserBackendService(spyHttpClient) val properties = PropertiesObject(timezoneId = "America/New_York") val propertiesDelta = PropertiesDeltasObject() @@ -188,6 +192,7 @@ class UserBackendServiceTests : FunSpec({ it.getJSONObject("properties").has("timezone_id") shouldBe true it.getJSONObject("properties").getString("timezone_id") shouldBe "America/New_York" }, + any(), ) } } @@ -197,7 +202,7 @@ class UserBackendServiceTests : FunSpec({ val aliasLabel = "onesignal_id" val aliasValue = "11111111-1111-1111-1111-111111111111" val spyHttpClient = mockk() - coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: { country: \"TV\"}}") + coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: { country: \"TV\"}}") val userBackendService = UserBackendService(spyHttpClient) val properties = PropertiesObject(country = "TV") val propertiesDelta = PropertiesDeltasObject() @@ -214,6 +219,7 @@ class UserBackendServiceTests : FunSpec({ it.getJSONObject("properties").has("country") shouldBe true it.getJSONObject("properties").getString("country") shouldBe "TV" }, + any(), ) } } @@ -223,7 +229,7 @@ class UserBackendServiceTests : FunSpec({ val aliasLabel = "onesignal_id" val aliasValue = "11111111-1111-1111-1111-111111111111" val spyHttpClient = mockk() - coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: { lat: 12.34, long: 45.67}}") + coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: { lat: 12.34, long: 45.67}}") val userBackendService = UserBackendService(spyHttpClient) val properties = PropertiesObject(latitude = 12.34, longitude = 45.67) val propertiesDelta = PropertiesDeltasObject() @@ -242,6 +248,7 @@ class UserBackendServiceTests : FunSpec({ it.getJSONObject("properties").has("long") shouldBe true it.getJSONObject("properties").getDouble("long") shouldBe 45.67 }, + any(), ) } } @@ -251,7 +258,7 @@ class UserBackendServiceTests : FunSpec({ val aliasLabel = "onesignal_id" val aliasValue = "11111111-1111-1111-1111-111111111111" val spyHttpClient = mockk() - coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: {} }") + coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: {} }") val userBackendService = UserBackendService(spyHttpClient) val properties = PropertiesObject(tags = mapOf("tagkey1" to "tagValue1")) val propertiesDelta = PropertiesDeltasObject() @@ -266,6 +273,7 @@ class UserBackendServiceTests : FunSpec({ withArg { it.getBoolean("refresh_device_metadata") shouldBe true }, + any(), ) } } @@ -275,7 +283,7 @@ class UserBackendServiceTests : FunSpec({ val aliasLabel = "onesignal_id" val aliasValue = "11111111-1111-1111-1111-111111111111" val spyHttpClient = mockk() - coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: {} }") + coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: {} }") val userBackendService = UserBackendService(spyHttpClient) val properties = PropertiesObject(tags = mapOf("tagkey1" to "tagValue1")) val propertiesDelta = PropertiesDeltasObject() @@ -290,6 +298,7 @@ class UserBackendServiceTests : FunSpec({ withArg { it.getBoolean("refresh_device_metadata") shouldBe false }, + any(), ) } } @@ -299,7 +308,7 @@ class UserBackendServiceTests : FunSpec({ val aliasLabel = "onesignal_id" val aliasValue = "11111111-1111-1111-1111-111111111111" val spyHttpClient = mockk() - coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: { }}") + coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: { }}") val userBackendService = UserBackendService(spyHttpClient) val properties = PropertiesObject() val propertiesDelta = PropertiesDeltasObject(sessionTime = 1111, sessionCount = 1) @@ -318,6 +327,7 @@ class UserBackendServiceTests : FunSpec({ it.getJSONObject("deltas").has("session_count") shouldBe true it.getJSONObject("deltas").getInt("session_count") shouldBe 1 }, + any(), ) } } @@ -327,7 +337,7 @@ class UserBackendServiceTests : FunSpec({ val aliasLabel = "onesignal_id" val aliasValue = "11111111-1111-1111-1111-111111111111" val spyHttpClient = mockk() - coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(202, "{properties: { }}") + coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(202, "{properties: { }}") val userBackendService = UserBackendService(spyHttpClient) val properties = PropertiesObject() val propertiesDelta = @@ -366,6 +376,7 @@ class UserBackendServiceTests : FunSpec({ it.getJSONObject("deltas").getJSONArray("purchases").getJSONObject(1).has("amount") shouldBe true it.getJSONObject("deltas").getJSONArray("purchases").getJSONObject(1).getDouble("amount") shouldBe 4444 }, + any(), ) } } @@ -375,7 +386,7 @@ class UserBackendServiceTests : FunSpec({ val aliasLabel = "onesignal_id" val aliasValue = "11111111-1111-1111-1111-111111111111" val spyHttpClient = mockk() - coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(404, "NOT FOUND") + coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(404, "NOT FOUND") val userBackendService = UserBackendService(spyHttpClient) val properties = PropertiesObject(tags = mapOf("tagkey1" to "tagValue1")) val propertiesDelta = PropertiesDeltasObject() @@ -396,7 +407,7 @@ class UserBackendServiceTests : FunSpec({ val aliasLabel = "onesignal_id" val aliasValue = "11111111-1111-1111-1111-111111111111" val spyHttpClient = mockk() - coEvery { spyHttpClient.patch(any(), any()) } returns HttpResponse(403, "FORBIDDEN") + coEvery { spyHttpClient.patch(any(), any(), any()) } returns HttpResponse(403, "FORBIDDEN") val userBackendService = UserBackendService(spyHttpClient) val properties = PropertiesObject(tags = mapOf("tagkey1" to "tagValue1")) val propertiesDelta = PropertiesDeltasObject() diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt index 044d4c3726..5681a12a77 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt @@ -3,12 +3,15 @@ package com.onesignal.user.internal.operations import android.content.Context import android.os.Build import com.onesignal.common.OneSignalUtils +import com.onesignal.common.exceptions.BackendException import com.onesignal.core.internal.device.IDeviceService import com.onesignal.core.internal.operations.ExecutionResponse import com.onesignal.core.internal.operations.ExecutionResult import com.onesignal.core.internal.operations.Operation import com.onesignal.mocks.MockHelper import com.onesignal.user.internal.customEvents.ICustomEventBackendService +import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getIdentityVerificationService +import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getJwtTokenStore import com.onesignal.user.internal.operations.impl.executors.CustomEventOperationExecutor import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.equals.shouldBeEqual @@ -23,7 +26,7 @@ class CustomEventOperationExecutorTests : FunSpec({ test("execution of track event operation") { // Given val mockCustomEventBackendService = mockk() - coEvery { mockCustomEventBackendService.sendCustomEvent(any(), any(), any(), any(), any(), any(), any()) } returns ExecutionResponse(ExecutionResult.SUCCESS) + coEvery { mockCustomEventBackendService.sendCustomEvent(any(), any(), any(), any(), any(), any(), any(), any()) } returns ExecutionResponse(ExecutionResult.SUCCESS) val mockApplicationService = MockHelper.applicationService() val mockContext = mockk(relaxed = true) @@ -36,7 +39,7 @@ class CustomEventOperationExecutorTests : FunSpec({ val properties = JSONObject().put("key", "value").toString() val customEventOperationExecutor = - CustomEventOperationExecutor(mockCustomEventBackendService, mockApplicationService, mockDeviceService) + CustomEventOperationExecutor(mockCustomEventBackendService, mockApplicationService, mockDeviceService, getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf(TrackCustomEventOperation("appId", "onesignalId", null, 1, "event-name", properties)) // When @@ -60,7 +63,40 @@ class CustomEventOperationExecutorTests : FunSpec({ it.deviceModel shouldBe deviceMode it.deviceOS shouldBe deviceOS }, + null, ) } } + + test("track event returns FAIL_UNAUTHORIZED on 401 so JWT gets invalidated under IV") { + // Given + val mockCustomEventBackendService = mockk() + coEvery { mockCustomEventBackendService.sendCustomEvent(any(), any(), any(), any(), any(), any(), any(), any()) } throws + BackendException(401, "UNAUTHORIZED", retryAfterSeconds = 5) + + val mockApplicationService = MockHelper.applicationService() + every { mockApplicationService.appContext } returns mockk(relaxed = true) + val mockDeviceService = MockHelper.deviceService() + every { mockDeviceService.deviceType } returns IDeviceService.DeviceType.Android + + val customEventOperationExecutor = + CustomEventOperationExecutor( + mockCustomEventBackendService, + mockApplicationService, + mockDeviceService, + getJwtTokenStore(), + getIdentityVerificationService(newCodePathsRun = true, ivBehaviorActive = true), + ) + val operations = + listOf( + TrackCustomEventOperation("appId", "onesignalId", "ext-1", 1, "event-name", JSONObject().toString()), + ) + + // When + val response = customEventOperationExecutor.execute(operations) + + // Then — must be FAIL_UNAUTHORIZED so OperationRepo.handleFailUnauthorized fires. + response.result shouldBe ExecutionResult.FAIL_UNAUTHORIZED + response.retryAfterSeconds shouldBe 5 + } }) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/ExecutorMocks.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/ExecutorMocks.kt index 9890563333..aaee22323e 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/ExecutorMocks.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/ExecutorMocks.kt @@ -1,12 +1,31 @@ package com.onesignal.user.internal.operations import com.onesignal.core.internal.config.ConfigModelStore +import com.onesignal.core.internal.config.impl.IdentityVerificationService import com.onesignal.core.internal.time.impl.Time import com.onesignal.mocks.MockHelper +import com.onesignal.mocks.MockPreferencesService +import com.onesignal.user.internal.jwt.JwtTokenStore import com.onesignal.user.internal.operations.impl.states.NewRecordsState +import io.mockk.every +import io.mockk.mockk class ExecutorMocks { companion object { fun getNewRecordState(configModelStore: ConfigModelStore = MockHelper.configModelStore()) = NewRecordsState(Time(), configModelStore) + + /** Real (empty) JWT store backed by a mock preferences service. `getJwt` returns null for all keys. */ + internal fun getJwtTokenStore() = JwtTokenStore(MockPreferencesService()) + + /** Mocked [IdentityVerificationService] with both gates returning false by default — IV inactive, new code paths off. */ + internal fun getIdentityVerificationService( + newCodePathsRun: Boolean = false, + ivBehaviorActive: Boolean = false, + ): IdentityVerificationService { + val mock = mockk(relaxed = true) + every { mock.newCodePathsRun } returns newCodePathsRun + every { mock.ivBehaviorActive } returns ivBehaviorActive + return mock + } } } diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/ExecutorsIvExtensionsTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/ExecutorsIvExtensionsTests.kt new file mode 100644 index 0000000000..a8de55e5dd --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/ExecutorsIvExtensionsTests.kt @@ -0,0 +1,100 @@ +package com.onesignal.user.internal.operations + +import com.onesignal.debug.LogLevel +import com.onesignal.debug.internal.logging.Logging +import com.onesignal.mocks.MockPreferencesService +import com.onesignal.user.internal.backend.IdentityConstants +import com.onesignal.user.internal.jwt.JwtTokenStore +import com.onesignal.user.internal.operations.impl.executors.IvBackendParams +import com.onesignal.user.internal.operations.impl.executors.resolveIvBackendParams +import com.onesignal.user.internal.operations.impl.executors.resolveIvJwt +import com.onesignal.user.internal.operations.impl.executors.shouldFailLoginUserFromSubscription +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class ExecutorsIvExtensionsTests : FunSpec({ + beforeAny { + Logging.logLevel = LogLevel.NONE + } + + test("resolveIvBackendParams returns legacy values when ivBehaviorActive is false (Phase 3)") { + // Given: new code path on but behavior inactive + val jwtStore = JwtTokenStore(MockPreferencesService()) + jwtStore.putJwt("ext-1", "jwt-value") + val op = LoginUserOperation("app", "os-1", "ext-1", null) + + // When + val params = resolveIvBackendParams(op, "os-1", jwtStore, ivBehaviorActive = false) + + // Then: onesignal_id alias, no JWT + params shouldBe IvBackendParams(IdentityConstants.ONESIGNAL_ID, "os-1", null) + } + + test("resolveIvBackendParams returns external_id alias + JWT when IV active and externalId present") { + // Given + val jwtStore = JwtTokenStore(MockPreferencesService()) + jwtStore.putJwt("ext-1", "jwt-value") + val op = LoginUserOperation("app", "os-1", "ext-1", null) + + // When + val params = resolveIvBackendParams(op, "os-1", jwtStore, ivBehaviorActive = true) + + // Then + params shouldBe IvBackendParams(IdentityConstants.EXTERNAL_ID, "ext-1", "jwt-value") + } + + test("resolveIvBackendParams falls back to onesignal_id when IV active but externalId null (defensive)") { + // Given: IV active, op has no externalId (shouldn't happen per hasValidJwtIfRequired gating) + val jwtStore = JwtTokenStore(MockPreferencesService()) + val op = LoginUserOperation("app", "os-1", null, null) + + // When + val params = resolveIvBackendParams(op, "os-1", jwtStore, ivBehaviorActive = true) + + // Then + params shouldBe IvBackendParams(IdentityConstants.ONESIGNAL_ID, "os-1", null) + } + + test("resolveIvBackendParams returns null JWT when IV active but no JWT stored (defensive)") { + // Given: IV active, externalId set, but no JWT in store + val jwtStore = JwtTokenStore(MockPreferencesService()) + val op = LoginUserOperation("app", "os-1", "ext-1", null) + + // When + val params = resolveIvBackendParams(op, "os-1", jwtStore, ivBehaviorActive = true) + + // Then: alias still switches to external_id but jwt is null + params shouldBe IvBackendParams(IdentityConstants.EXTERNAL_ID, "ext-1", null) + } + + test("resolveIvJwt returns null when ivBehaviorActive is false") { + val jwtStore = JwtTokenStore(MockPreferencesService()) + jwtStore.putJwt("ext-1", "jwt-value") + val op = LoginUserOperation("app", "os-1", "ext-1", null) + + resolveIvJwt(op, jwtStore, ivBehaviorActive = false) shouldBe null + } + + test("resolveIvJwt returns stored JWT when IV active and externalId present") { + val jwtStore = JwtTokenStore(MockPreferencesService()) + jwtStore.putJwt("ext-1", "jwt-value") + val op = LoginUserOperation("app", "os-1", "ext-1", null) + + resolveIvJwt(op, jwtStore, ivBehaviorActive = true) shouldBe "jwt-value" + } + + test("resolveIvJwt returns null when IV active but op is anonymous") { + val jwtStore = JwtTokenStore(MockPreferencesService()) + val op = LoginUserOperation("app", "os-1", null, null) + + resolveIvJwt(op, jwtStore, ivBehaviorActive = true) shouldBe null + } + + test("shouldFailLoginUserFromSubscription returns false when ivBehaviorActive is false") { + shouldFailLoginUserFromSubscription(ivBehaviorActive = false) shouldBe false + } + + test("shouldFailLoginUserFromSubscription returns true when ivBehaviorActive is true") { + shouldFailLoginUserFromSubscription(ivBehaviorActive = true) shouldBe true + } +}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/IdentityOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/IdentityOperationExecutorTests.kt index 057ef2c96a..3e3cc45a2e 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/IdentityOperationExecutorTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/IdentityOperationExecutorTests.kt @@ -5,11 +5,15 @@ import com.onesignal.common.modeling.ModelChangeTags import com.onesignal.core.internal.operations.ExecutionResult import com.onesignal.core.internal.operations.Operation import com.onesignal.mocks.MockHelper +import com.onesignal.mocks.MockPreferencesService import com.onesignal.user.internal.backend.IIdentityBackendService import com.onesignal.user.internal.backend.IdentityConstants import com.onesignal.user.internal.builduser.IRebuildUserService import com.onesignal.user.internal.identity.IdentityModel import com.onesignal.user.internal.identity.IdentityModelStore +import com.onesignal.user.internal.jwt.JwtTokenStore +import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getIdentityVerificationService +import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getJwtTokenStore import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getNewRecordState import com.onesignal.user.internal.operations.impl.executors.IdentityOperationExecutor import io.kotest.core.spec.style.FunSpec @@ -39,7 +43,7 @@ class IdentityOperationExecutorTests : FunSpec({ val mockBuildUserService = mockk() val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState()) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf(SetAliasOperation("appId", "onesignalId", null, "aliasKey1", "aliasValue1")) // When @@ -69,7 +73,7 @@ class IdentityOperationExecutorTests : FunSpec({ val mockBuildUserService = mockk() val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState()) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf(SetAliasOperation("appId", "onesignalId", null, "aliasKey1", "aliasValue1")) // When @@ -90,7 +94,7 @@ class IdentityOperationExecutorTests : FunSpec({ val mockBuildUserService = mockk() val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState()) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf(SetAliasOperation("appId", "onesignalId", null, "aliasKey1", "aliasValue1")) // When @@ -111,7 +115,7 @@ class IdentityOperationExecutorTests : FunSpec({ every { mockBuildUserService.getRebuildOperationsIfCurrentUser(any(), any()) } returns null val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState()) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf(SetAliasOperation("appId", "onesignalId", null, "aliasKey1", "aliasValue1")) // When @@ -134,7 +138,7 @@ class IdentityOperationExecutorTests : FunSpec({ val mockConfigModelStore = MockHelper.configModelStore().also { it.model.opRepoPostCreateRetryUpTo = 1_000 } val newRecordState = getNewRecordState(mockConfigModelStore).also { it.add("onesignalId") } val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, newRecordState) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, newRecordState, getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf(SetAliasOperation("appId", "onesignalId", null, "aliasKey1", "aliasValue1")) // When @@ -160,7 +164,7 @@ class IdentityOperationExecutorTests : FunSpec({ val mockBuildUserService = mockk() val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState()) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf(DeleteAliasOperation("appId", "onesignalId", null, "aliasKey1")) // When @@ -183,7 +187,7 @@ class IdentityOperationExecutorTests : FunSpec({ val mockBuildUserService = mockk() val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState()) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf(DeleteAliasOperation("appId", "onesignalId", null, "aliasKey1")) // When @@ -203,7 +207,7 @@ class IdentityOperationExecutorTests : FunSpec({ val mockBuildUserService = mockk() val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState()) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf(DeleteAliasOperation("appId", "onesignalId", null, "aliasKey1")) // When @@ -225,7 +229,7 @@ class IdentityOperationExecutorTests : FunSpec({ val mockBuildUserService = mockk() val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState()) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf(DeleteAliasOperation("appId", "onesignalId", null, "aliasKey1")) // When @@ -250,7 +254,7 @@ class IdentityOperationExecutorTests : FunSpec({ val mockConfigModelStore = MockHelper.configModelStore().also { it.model.opRepoPostCreateRetryUpTo = 1_000 } val newRecordState = getNewRecordState(mockConfigModelStore).also { it.add("onesignalId") } val identityOperationExecutor = - IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, newRecordState) + IdentityOperationExecutor(mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, newRecordState, getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf(DeleteAliasOperation("appId", "onesignalId", null, "aliasKey1")) // When @@ -259,4 +263,85 @@ class IdentityOperationExecutorTests : FunSpec({ // Then response.result shouldBe ExecutionResult.FAIL_RETRY } + + test("set alias uses external_id alias and attaches JWT when IV active") { + // Given + val mockIdentityBackendService = mockk() + coEvery { mockIdentityBackendService.setAlias(any(), any(), any(), any(), any()) } returns mapOf() + + val mockIdentityModel = mockk() + every { mockIdentityModel.onesignalId } returns "onesignalId" + every { mockIdentityModel.setStringProperty(any(), any(), any()) } just runs + + val mockIdentityModelStore = mockk() + every { mockIdentityModelStore.model } returns mockIdentityModel + + val mockBuildUserService = mockk() + + val jwtStore = JwtTokenStore(MockPreferencesService()) + jwtStore.putJwt("ext-1", "the-jwt") + + val identityOperationExecutor = + IdentityOperationExecutor( + mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), jwtStore, + getIdentityVerificationService(newCodePathsRun = true, ivBehaviorActive = true), + ) + val operations = listOf(SetAliasOperation("appId", "onesignalId", "ext-1", "aliasKey1", "aliasValue1")) + + // When + val response = identityOperationExecutor.execute(operations) + + // Then + response.result shouldBe ExecutionResult.SUCCESS + coVerify(exactly = 1) { + mockIdentityBackendService.setAlias( + "appId", + IdentityConstants.EXTERNAL_ID, + "ext-1", + mapOf("aliasKey1" to "aliasValue1"), + "the-jwt", + ) + } + } + + test("set alias keeps onesignal_id alias and null JWT in Phase 3 (newCodePathsRun + ivBehaviorActive=false)") { + // Given: new code path on but IV behavior off — extension runs but must return legacy values + val mockIdentityBackendService = mockk() + coEvery { mockIdentityBackendService.setAlias(any(), any(), any(), any(), any()) } returns mapOf() + + val mockIdentityModel = mockk() + every { mockIdentityModel.onesignalId } returns "onesignalId" + every { mockIdentityModel.setStringProperty(any(), any(), any()) } just runs + + val mockIdentityModelStore = mockk() + every { mockIdentityModelStore.model } returns mockIdentityModel + + val mockBuildUserService = mockk() + + val jwtStore = JwtTokenStore(MockPreferencesService()) + // A JWT is stored; Phase 3 must NOT attach it. + jwtStore.putJwt("ext-1", "the-jwt") + + val identityOperationExecutor = + IdentityOperationExecutor( + mockIdentityBackendService, mockIdentityModelStore, mockBuildUserService, getNewRecordState(), jwtStore, + getIdentityVerificationService(newCodePathsRun = true, ivBehaviorActive = false), + ) + val operations = listOf(SetAliasOperation("appId", "onesignalId", "ext-1", "aliasKey1", "aliasValue1")) + + // When + val response = identityOperationExecutor.execute(operations) + + // Then: onesignal_id alias, null JWT + response.result shouldBe ExecutionResult.SUCCESS + coVerify(exactly = 1) { + mockIdentityBackendService.setAlias( + "appId", + IdentityConstants.ONESIGNAL_ID, + "onesignalId", + mapOf("aliasKey1" to "aliasValue1"), + null, + ) + } + } }) 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 e3c8c1cfb4..0085360235 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 @@ -14,6 +14,8 @@ import com.onesignal.user.internal.backend.PropertiesObject import com.onesignal.user.internal.backend.SubscriptionObject import com.onesignal.user.internal.backend.SubscriptionObjectType import com.onesignal.user.internal.identity.IdentityModel +import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getIdentityVerificationService +import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getJwtTokenStore import com.onesignal.user.internal.operations.impl.executors.IdentityOperationExecutor import com.onesignal.user.internal.operations.impl.executors.LoginUserOperationExecutor import com.onesignal.user.internal.properties.PropertiesModel @@ -77,6 +79,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf( @@ -121,6 +124,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf( @@ -149,7 +153,7 @@ class LoginUserOperationExecutorTests : FunSpec({ val mockSubscriptionsModelStore = mockk() val loginUserOperationExecutor = - LoginUserOperationExecutor(mockIdentityOperationExecutor, AndroidMockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext()) + LoginUserOperationExecutor(mockIdentityOperationExecutor, AndroidMockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf( LoginUserOperation(appId, localOneSignalId, null, null), @@ -177,7 +181,7 @@ class LoginUserOperationExecutorTests : FunSpec({ val mockSubscriptionsModelStore = mockk() val loginUserOperationExecutor = - LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext()) + LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", null)) // When @@ -215,6 +219,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", null)) @@ -243,7 +248,7 @@ class LoginUserOperationExecutorTests : FunSpec({ val mockSubscriptionsModelStore = mockk() val loginUserOperationExecutor = - LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext()) + LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId")) // When @@ -279,7 +284,7 @@ class LoginUserOperationExecutorTests : FunSpec({ val mockSubscriptionsModelStore = mockk() val loginUserOperationExecutor = - LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext()) + LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId")) // When @@ -315,7 +320,7 @@ class LoginUserOperationExecutorTests : FunSpec({ val mockSubscriptionsModelStore = mockk() val loginUserOperationExecutor = - LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext()) + LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId")) // When @@ -353,7 +358,7 @@ class LoginUserOperationExecutorTests : FunSpec({ val mockSubscriptionsModelStore = mockk() val loginUserOperationExecutor = - LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext()) + LoginUserOperationExecutor(mockIdentityOperationExecutor, MockHelper.applicationService(), MockHelper.deviceService(), mockUserBackendService, mockIdentityModelStore, mockPropertiesModelStore, mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), getJwtTokenStore(), getIdentityVerificationService()) val operations = listOf(LoginUserOperation(appId, localOneSignalId, "externalId", "existingOneSignalId")) // When @@ -404,6 +409,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf( @@ -508,6 +514,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf( @@ -596,6 +603,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf( @@ -670,6 +678,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf( @@ -735,6 +744,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + getJwtTokenStore(), getIdentityVerificationService(), ) // anonymous Login request val operations = listOf(LoginUserOperation(appId, localOneSignalId, null, null)) @@ -781,6 +791,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, MockHelper.configModelStore(), MockHelper.languageContext(), + getJwtTokenStore(), getIdentityVerificationService(), ) // send PUSH then EMAIL (local IDs 1,2) — order differs from backend response @@ -845,6 +856,7 @@ class LoginUserOperationExecutorTests : FunSpec({ mockSubscriptionsModelStore, configModelStore, MockHelper.languageContext(), + getJwtTokenStore(), getIdentityVerificationService(), ) val ops = @@ -869,4 +881,65 @@ class LoginUserOperationExecutorTests : FunSpec({ configModelStore.model.pushSubscriptionId shouldBe remoteSubscriptionId1 coVerify(exactly = 1) { mockUserBackendService.createUser(appId, mapOf(), any(), any()) } } + + test("IV active: loginUser with existingOnesignalId + externalId goes straight to createUser, skipping optimistic SetAliasOperation") { + // Given: IV active. Under legacy or Phase 3 this input would hit the optimistic-merge + // SetAliasOperation path; under IV we must skip that because IdentityOperationExecutor + // would resolve alias to (external_id, newExternalId) and target the wrong user. + val mockUserBackendService = mockk() + coEvery { mockUserBackendService.createUser(any(), any(), any(), any(), any()) } returns + CreateUserResponse( + mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId), + PropertiesObject(), + listOf(), + ) + + // Strict mock: if the optimistic-merge path is reached, the test fails because no + // `execute` stub is registered on IdentityOperationExecutor. + val mockIdentityOperationExecutor = mockk() + + val mockIdentityModelStore = MockHelper.identityModelStore() + val mockPropertiesModelStore = MockHelper.propertiesModelStore() + val mockSubscriptionsModelStore = mockk() + + val loginUserOperationExecutor = + LoginUserOperationExecutor( + mockIdentityOperationExecutor, + AndroidMockHelper.applicationService(), + MockHelper.deviceService(), + mockUserBackendService, + mockIdentityModelStore, + mockPropertiesModelStore, + mockSubscriptionsModelStore, + MockHelper.configModelStore(), + MockHelper.languageContext(), + getJwtTokenStore(), + getIdentityVerificationService(newCodePathsRun = true, ivBehaviorActive = true), + ) + + // LoginUserOperation has existingOnesignalId AND externalId — the input shape that + // triggers the optimistic-merge branch under legacy. + val operations = + listOf( + LoginUserOperation(appId, localOneSignalId, "new-external-id", "existing-osid"), + ) + + // When + val response = loginUserOperationExecutor.execute(operations) + + // Then + response.result shouldBe ExecutionResult.SUCCESS + coVerify(exactly = 1) { + mockUserBackendService.createUser( + appId, + mapOf(IdentityConstants.EXTERNAL_ID to "new-external-id"), + any(), + any(), + any(), + ) + } + // IdentityOperationExecutor must NOT be invoked (strict mock enforces this via kotest's + // unstubbed-call failure; coVerify(exactly = 0) makes the expectation explicit). + coVerify(exactly = 0) { mockIdentityOperationExecutor.execute(any()) } + } }) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt index 0f3765c557..43aa2ef827 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt @@ -14,6 +14,8 @@ import com.onesignal.user.internal.backend.SubscriptionObject import com.onesignal.user.internal.backend.SubscriptionObjectType import com.onesignal.user.internal.builduser.IRebuildUserService import com.onesignal.user.internal.identity.IdentityModel +import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getIdentityVerificationService +import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getJwtTokenStore import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getNewRecordState import com.onesignal.user.internal.operations.impl.executors.RefreshUserOperationExecutor import com.onesignal.user.internal.properties.PropertiesModel @@ -107,6 +109,7 @@ class RefreshUserOperationExecutorTests : FunSpec({ mockConfigModelStore, mockBuildUserService, getNewRecordState(), + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null)) @@ -191,6 +194,7 @@ class RefreshUserOperationExecutorTests : FunSpec({ MockHelper.configModelStore(), mockBuildUserService, getNewRecordState(), + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null)) @@ -230,6 +234,7 @@ class RefreshUserOperationExecutorTests : FunSpec({ MockHelper.configModelStore(), mockBuildUserService, getNewRecordState(), + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null)) @@ -265,6 +270,7 @@ class RefreshUserOperationExecutorTests : FunSpec({ MockHelper.configModelStore(), mockBuildUserService, getNewRecordState(), + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null)) @@ -300,6 +306,7 @@ class RefreshUserOperationExecutorTests : FunSpec({ MockHelper.configModelStore(), mockBuildUserService, getNewRecordState(), + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null)) @@ -337,6 +344,7 @@ class RefreshUserOperationExecutorTests : FunSpec({ MockHelper.configModelStore(), mockBuildUserService, newRecordState, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf(RefreshUserOperation(appId, remoteOneSignalId, null)) 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 78f8ffc47f..19afa4c4dd 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 @@ -13,6 +13,8 @@ import com.onesignal.user.internal.backend.ISubscriptionBackendService import com.onesignal.user.internal.backend.IdentityConstants import com.onesignal.user.internal.backend.SubscriptionObjectType import com.onesignal.user.internal.builduser.IRebuildUserService +import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getIdentityVerificationService +import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getJwtTokenStore import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getNewRecordState import com.onesignal.user.internal.operations.impl.executors.SubscriptionOperationExecutor import com.onesignal.user.internal.subscriptions.SubscriptionModel @@ -68,6 +70,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = @@ -129,6 +132,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = @@ -180,6 +184,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = @@ -236,6 +241,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = @@ -292,6 +298,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, newRecordState, mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = @@ -336,6 +343,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = @@ -383,6 +391,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = @@ -455,6 +464,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = @@ -518,6 +528,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = @@ -571,6 +582,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = @@ -626,6 +638,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, newRecordState, mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = @@ -669,6 +682,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = @@ -703,6 +717,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = @@ -738,6 +753,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = @@ -773,6 +789,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, newRecordState, mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = @@ -814,6 +831,7 @@ class SubscriptionOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = @@ -837,4 +855,100 @@ class SubscriptionOperationExecutorTests : mockConsistencyManager.setRywData(remoteOneSignalId, IamFetchRywTokenKey.SUBSCRIPTION, rywData) } } + + test("update subscription returns FAIL_UNAUTHORIZED on 401 so JWT gets invalidated under IV") { + // Given + val mockSubscriptionBackendService = mockk() + coEvery { mockSubscriptionBackendService.updateSubscription(any(), any(), any(), any()) } throws + BackendException(401, "UNAUTHORIZED", retryAfterSeconds = 5) + + val subscriptionOperationExecutor = + SubscriptionOperationExecutor( + mockSubscriptionBackendService, + MockHelper.deviceService(), + AndroidMockHelper.applicationService(), + mockk(), + MockHelper.configModelStore(), + mockk(), + getNewRecordState(), + mockConsistencyManager, + getJwtTokenStore(), + getIdentityVerificationService(newCodePathsRun = true, ivBehaviorActive = true), + ) + val operations = + listOf( + UpdateSubscriptionOperation( + appId, remoteOneSignalId, "ext-1", remoteSubscriptionId, + SubscriptionType.PUSH, true, "pushToken2", SubscriptionStatus.SUBSCRIBED, + ), + ) + + // When + val response = subscriptionOperationExecutor.execute(operations) + + // Then — must be FAIL_UNAUTHORIZED so OperationRepo.handleFailUnauthorized fires. + response.result shouldBe ExecutionResult.FAIL_UNAUTHORIZED + response.retryAfterSeconds shouldBe 5 + } + + test("delete subscription returns FAIL_UNAUTHORIZED on 401 so JWT gets invalidated under IV") { + // Given + val mockSubscriptionBackendService = mockk() + coEvery { mockSubscriptionBackendService.deleteSubscription(any(), any(), any()) } throws + BackendException(401, "UNAUTHORIZED", retryAfterSeconds = 5) + + val subscriptionOperationExecutor = + SubscriptionOperationExecutor( + mockSubscriptionBackendService, + MockHelper.deviceService(), + AndroidMockHelper.applicationService(), + mockk(), + MockHelper.configModelStore(), + mockk(), + getNewRecordState(), + mockConsistencyManager, + getJwtTokenStore(), + getIdentityVerificationService(newCodePathsRun = true, ivBehaviorActive = true), + ) + val operations = + listOf(DeleteSubscriptionOperation(appId, remoteOneSignalId, "ext-1", remoteSubscriptionId)) + + // When + val response = subscriptionOperationExecutor.execute(operations) + + // Then + response.result shouldBe ExecutionResult.FAIL_UNAUTHORIZED + response.retryAfterSeconds shouldBe 5 + } + + test("transfer subscription returns FAIL_UNAUTHORIZED on 401 so JWT gets invalidated under IV") { + // Given + val mockSubscriptionBackendService = mockk() + coEvery { + mockSubscriptionBackendService.transferSubscription(any(), any(), any(), any(), any()) + } throws BackendException(403, "FORBIDDEN", retryAfterSeconds = 5) + + val subscriptionOperationExecutor = + SubscriptionOperationExecutor( + mockSubscriptionBackendService, + MockHelper.deviceService(), + AndroidMockHelper.applicationService(), + mockk(), + MockHelper.configModelStore(), + mockk(), + getNewRecordState(), + mockConsistencyManager, + getJwtTokenStore(), + getIdentityVerificationService(newCodePathsRun = true, ivBehaviorActive = true), + ) + val operations = + listOf(TransferSubscriptionOperation(appId, remoteOneSignalId, "ext-1", remoteSubscriptionId)) + + // When + val response = subscriptionOperationExecutor.execute(operations) + + // Then + response.result shouldBe ExecutionResult.FAIL_UNAUTHORIZED + response.retryAfterSeconds shouldBe 5 + } }) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/UpdateUserOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/UpdateUserOperationExecutorTests.kt index 2e0c122cbc..21e3bfdf17 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/UpdateUserOperationExecutorTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/UpdateUserOperationExecutorTests.kt @@ -10,6 +10,8 @@ import com.onesignal.mocks.MockHelper import com.onesignal.user.internal.backend.IUserBackendService import com.onesignal.user.internal.backend.IdentityConstants import com.onesignal.user.internal.builduser.IRebuildUserService +import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getIdentityVerificationService +import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getJwtTokenStore import com.onesignal.user.internal.operations.ExecutorMocks.Companion.getNewRecordState import com.onesignal.user.internal.operations.impl.executors.UpdateUserOperationExecutor import com.onesignal.user.internal.properties.PropertiesModel @@ -56,6 +58,7 @@ class UpdateUserOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf(SetTagOperation(appId, remoteOneSignalId, null, "tagKey1", "tagValue1")) @@ -96,6 +99,7 @@ class UpdateUserOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf( @@ -158,6 +162,7 @@ class UpdateUserOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf( @@ -203,6 +208,7 @@ class UpdateUserOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf( @@ -269,6 +275,7 @@ class UpdateUserOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf( @@ -317,6 +324,7 @@ class UpdateUserOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf(SetTagOperation(appId, remoteOneSignalId, null, "tagKey1", "tagValue1")) @@ -350,6 +358,7 @@ class UpdateUserOperationExecutorTests : mockBuildUserService, newRecordState, mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations = listOf(SetTagOperation(appId, remoteOneSignalId, null, "tagKey1", "tagValue1")) @@ -380,6 +389,7 @@ class UpdateUserOperationExecutorTests : mockBuildUserService, getNewRecordState(), mockConsistencyManager, + getJwtTokenStore(), getIdentityVerificationService(), ) val operations =