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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@
3CEE93572B7C78FD008440BD /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17E627026B95002D3A5D /* OneSignalCore.framework */; };
3CEE93582B7C78FE008440BD /* OneSignalCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17E627026B95002D3A5D /* OneSignalCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
3CF11E3D2C6D6155002856F5 /* UserExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF11E3C2C6D6155002856F5 /* UserExecutorTests.swift */; };
354E0C59BA9B18437C36215B /* UserExecutorRetryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C310B6C569E96C204F8CD68 /* UserExecutorRetryTests.swift */; };
3CF11E402C6E6DE2002856F5 /* MockNewRecordsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF11E3F2C6E6DE2002856F5 /* MockNewRecordsState.swift */; };
3CF1A5632C669EA40056B3AA /* OSNewRecordsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF1A5622C669EA40056B3AA /* OSNewRecordsState.swift */; };
3CF8629E28A183F900776CA4 /* OSIdentityModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF8629D28A183F900776CA4 /* OSIdentityModel.swift */; };
Expand Down Expand Up @@ -363,6 +364,7 @@
7AFE856C2368DDB80091D6A5 /* OSFocusCallParams.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFE856A2368DDB80091D6A5 /* OSFocusCallParams.m */; };
7AFE856D2368DDB80091D6A5 /* OSFocusCallParams.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFE856A2368DDB80091D6A5 /* OSFocusCallParams.m */; };
7EB69F3B404D0AEF46EC1536 /* UserJwtLifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BFE2F960129386AFA6D5F41 /* UserJwtLifecycleTests.swift */; };
E42087CB1AB15481E55D34FE /* UserStateReportingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA6355B75EDC9A3100B98F5 /* UserStateReportingTests.swift */; };
9345FA2E8878EA1B0216A49A /* UserJwtAskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3567F486AA4335CCA79D659B /* UserJwtAskTests.swift */; };
8D2F4893453206700BB60F85 /* OSOperationRepoTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5221EEDBA5A74BD565490D52 /* OSOperationRepoTestSupport.swift */; };
8E949FF4C7A7A2C7182E53EA /* OSUserJwtConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9376A4957E9090C748BCB18 /* OSUserJwtConfigTests.swift */; };
Expand Down Expand Up @@ -1524,6 +1526,7 @@
3CEE90A62BFE6ABD00B0FB5B /* OSPropertiesSupportedProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesSupportedProperty.swift; sourceTree = "<group>"; };
3CEE90A82C000BD500B0FB5B /* OneSignalRequest+UnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OneSignalRequest+UnitTests.swift"; sourceTree = "<group>"; };
3CF11E3C2C6D6155002856F5 /* UserExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserExecutorTests.swift; sourceTree = "<group>"; };
6C310B6C569E96C204F8CD68 /* UserExecutorRetryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserExecutorRetryTests.swift; sourceTree = "<group>"; };
3CF11E3F2C6E6DE2002856F5 /* MockNewRecordsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNewRecordsState.swift; sourceTree = "<group>"; };
3CF1A5622C669EA40056B3AA /* OSNewRecordsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSNewRecordsState.swift; sourceTree = "<group>"; };
3CF8629D28A183F900776CA4 /* OSIdentityModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1600,6 +1603,7 @@
5BC1DE632C90BB9000CA8807 /* OSIamFetchReadyCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIamFetchReadyCondition.swift; sourceTree = "<group>"; };
5BC1DE672C90C23E00CA8807 /* OSConsistencyManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSConsistencyManagerTests.swift; sourceTree = "<group>"; };
5BFE2F960129386AFA6D5F41 /* UserJwtLifecycleTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserJwtLifecycleTests.swift; sourceTree = "<group>"; };
FDA6355B75EDC9A3100B98F5 /* UserStateReportingTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserStateReportingTests.swift; sourceTree = "<group>"; };
3567F486AA4335CCA79D659B /* UserJwtAskTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserJwtAskTests.swift; sourceTree = "<group>"; };
6552F2A6DF7776B0582CFAEF /* OSUserJwtConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OSUserJwtConfig.swift; sourceTree = "<group>"; };
67ECA2928D863073B785F93F /* IamFetchIdentityVerificationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = IamFetchIdentityVerificationTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2539,6 +2543,7 @@
047D8F5E1095A20C9C54FD33 /* OSRequestAuthTests.swift */,
3016921C1F6B7B7793F67567 /* RequestPathEncodingTests.swift */,
5BFE2F960129386AFA6D5F41 /* UserJwtLifecycleTests.swift */,
FDA6355B75EDC9A3100B98F5 /* UserStateReportingTests.swift */,
3567F486AA4335CCA79D659B /* UserJwtAskTests.swift */,
);
path = OneSignalUserTests;
Expand All @@ -2556,6 +2561,7 @@
isa = PBXGroup;
children = (
3CF11E3C2C6D6155002856F5 /* UserExecutorTests.swift */,
6C310B6C569E96C204F8CD68 /* UserExecutorRetryTests.swift */,
3CA93BC3300AEFFA000724B3 /* SubscriptionUpdateRaceTests.swift */,
C95B2AD05FFF9FC12C9D5878 /* ExecutorReadYourWriteTests.swift */,
3C5181A1B2C3D4E5F6A7B801 /* SubscriptionCreateResponseTests.swift */,
Expand Down Expand Up @@ -4714,6 +4720,7 @@
files = (
3CB331682F281679000E1801 /* CustomEventsIntegrationTests.swift in Sources */,
3CF11E3D2C6D6155002856F5 /* UserExecutorTests.swift in Sources */,
354E0C59BA9B18437C36215B /* UserExecutorRetryTests.swift in Sources */,
3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */,
3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */,
3CA93BC7300B0100000724B3 /* SubscriptionModelConcurrencyTests.swift in Sources */,
Expand All @@ -4733,6 +4740,7 @@
AAFA2D46E6C5FD3D14D39F27 /* OSRequestAuthTests.swift in Sources */,
23D66BEB40CE76DFF89744A3 /* RequestPathEncodingTests.swift in Sources */,
7EB69F3B404D0AEF46EC1536 /* UserJwtLifecycleTests.swift in Sources */,
E42087CB1AB15481E55D34FE /* UserStateReportingTests.swift in Sources */,
9345FA2E8878EA1B0216A49A /* UserJwtAskTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,9 @@ extension MockOneSignalClient {
request.isKind(of: type)
}.count
}

/// Held requests count as in flight. Read under the lock, so safe from the test thread.
public var isIdle: Bool {
return lock.withLock { startedRequests.count == completedRequests.count }
}
}
8 changes: 8 additions & 0 deletions iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/OSCoreMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,12 @@ extension OSOperationRepo {
}
paused = false
}

/**
The queue as of right now. Tests poll it while the repo appends on its own queue, so reading
`deltaQueue` directly is a data race even when only the count is wanted.
*/
public func snapshotDeltaQueue() -> [OSDelta] {
return dispatchQueue.sync { deltaQueue }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import Foundation
import XCTest
import OneSignalCore
import OneSignalCoreMocks
import OneSignalOSCoreMocks
@testable import OneSignalOSCore

/// Covers `flushDeltaQueue` routing: matched deltas go to executors and leave the repo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import Foundation
import XCTest
import OneSignalCore
import OneSignalCoreMocks
import OneSignalOSCoreMocks
import OneSignalKMP
@testable import OneSignalOSCore

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,6 @@ enum OSOperationRepoTestEnvironment {
}
}

extension OSOperationRepo {
/**
The queue as of right now. Tests poll it while the repo appends on its own queue, so reading
`deltaQueue` directly is a data race even when only the count is wanted.
*/
func snapshotDeltaQueue() -> [OSDelta] {
return dispatchQueue.sync { deltaQueue }
}
}

/// Records what the Operation Repo hands it, so tests can assert on routing rather than on requests.
final class MockOperationExecutor: OSOperationExecutor {
let supportedDeltas: [String]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,9 +440,8 @@ extension OSSubscriptionOperationExecutor {
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSSubscriptionOperationExecutor delete subscription request failed with error: \(error.debugDescription)")
self.dispatchQueue.async {
let responseType = OSNetworkingUtils.getResponseStatusType(error.code)
if responseType == .unauthorized, self.auth.handleUnauthorized(request) {
OneSignalLog.onesignalLog(.LL_DEBUG, message: "OSSubscriptionOperationExecutor holding \(request) for a new token")
} else if responseType != .retryable {
// No token handling: the delete is never signed, so a 401 here is not about the user's JWT.
if responseType != .retryable {
// Fail, no retry, remove from cache and queue
// If this request returns a missing status, that is ok as this is a delete request
self.removeRequestQueue.removeAll(where: { $0 == request})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,13 @@ class OSUserExecutor {
Runs on every send rather than only when the requirement hydrates, and reads the live model: this
executor sends nothing while `requirement` is unknown, so the queue is always judged against a known
value, and the check is cheap.

With the requirement off there is nothing to reshape, but a login kept at start while the requirement
was still unknown may never become sendable; see `dropIdentifyUsersThatCanNeverPrepare`.
*/
private func reshapeInvalidRequests() {
guard identityVerificationService.ivBehaviorActive else {
dropIdentifyUsersThatCanNeverPrepare()
return
}

Expand Down Expand Up @@ -121,6 +125,43 @@ class OSUserExecutor {
return request is OSRequestFetchIdentityBySubscription
}

/**
An Identify User whose user has no `onesignal_id`, and no queued Create User or Fetch Identity By
Subscription to supply one, can never prepare, so drop it rather than let it block the logins behind it.
Runs on every send, so a handler that supplies an id must hydrate before it removes its Request from
the queue; a pass between the two would see no supplier and drop the login.
*/
private func dropIdentifyUsersThatCanNeverPrepare() {
guard identityVerificationService.requirement == .off else {
return
}
let modelIdsAwaitingAnId = Set(userRequestQueue.compactMap { request -> String? in
if let createUser = request as? OSRequestCreateUser {
return createUser.identityModel.modelId
}
if let fetchIdentity = request as? OSRequestFetchIdentityBySubscription {
return fetchIdentity.identityModel.modelId
}
return nil
})
let kept = userRequestQueue.filter { request in
guard let identifyUser = request as? OSRequestIdentifyUser,
identifyUser.identityModelToIdentify.onesignalId == nil,
!modelIdsAwaitingAnId.contains(identifyUser.identityModelToIdentify.modelId)
else {
return true
}
let reason = "its user never received an onesignal_id and nothing queued can supply one"
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor dropped \(identifyUser), \(reason)")
Comment thread
nan-li marked this conversation as resolved.
return false
}
guard kept.count != userRequestQueue.count else {
return
}
userRequestQueue = kept
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, withValue: userRequestQueue)
}

/// Read in requests from the cache, do not read in FetchUser requests as this is not needed.
private func uncacheUserRequests() {
var userRequestQueue: [OSUserRequest] = []
Expand Down Expand Up @@ -160,9 +201,15 @@ class OSUserExecutor {
req.identityModelToUpdate = updateInRepo
}

// `prepareForExecution` is false under IV so `reshapeInvalidRequests` can promote
// this login; do not treat that as a permanent drop.
if auth.ivBehaviorActive || request.prepareForExecution(newRecordsState: newRecordsState, auth: auth) {
// Keep the login when the user it identifies is known to the repo: a restored Create User
// ahead of it put the model there, and that response supplies the `onesignal_id` prepare
// needs. Keep it too while the requirement is not known to be off, since
// `reshapeInvalidRequests` decides what a login made under Identity Verification becomes
// once the requirement is known. Otherwise only a Request that can be sent as is stays:
// one that can never prepare would hold the queue and block the logins behind it.
if identifyInRepo != nil
|| identityVerificationService.requirement != .off
|| request.prepareForExecution(newRecordsState: newRecordsState, auth: auth) {
if identifyInRepo == nil {
addIdentityModel(req.identityModelToIdentify)
}
Expand Down Expand Up @@ -253,7 +300,9 @@ class OSUserExecutor {
else {
// Only the app can end this wait (`updateUserJwt` → `storeJwt`); do not poll for it.
// A login for another user behind this one must not be stranded, so step over it.
if self.auth.awaitsToken(request) {
// Anything else that stops a prepare, the cool-down or an id still to arrive, resolves
// on its own, and the delayed retry below is what picks it up.
if self.auth.parkedForToken(request) {
awaitingToken = true
continue
}
Expand Down Expand Up @@ -339,8 +388,6 @@ extension OSUserExecutor {
request.sentToClient = true

OneSignalCoreImpl.sharedClient().execute(request) { response in
self.removeFromQueue(request)

// Create User's response won't send us the user's complete info if this user already exists
if let response = response {
// Parse the response for any data we need to update
Expand All @@ -350,6 +397,8 @@ extension OSUserExecutor {
originalPushToken: request.originalPushToken,
addNewRecords: request.addsNewRecords
)
// Must follow the hydrate; see `dropIdentifyUsersThatCanNeverPrepare`.
self.removeFromQueue(request)

// If this user already exists and we logged into an external_id, fetch the user data
// Fetch the user only if its the current user and non-anonymous
Expand All @@ -371,6 +420,8 @@ extension OSUserExecutor {
)
OSConsistencyManager.shared.setRywTokenAndDelay(id: onesignalId, key: OSIamFetchOffsetKey.userCreate, value: rywData)
}
} else {
self.removeFromQueue(request)
Comment thread
nan-li marked this conversation as resolved.
}
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = false
} onFailure: { error in
Expand Down Expand Up @@ -417,11 +468,12 @@ extension OSUserExecutor {
request.sentToClient = true

OneSignalCoreImpl.sharedClient().execute(request) { response in
self.removeFromQueue(request)

if let identityObject = self.parseIdentityObjectResponse(response),
let onesignalId = identityObject[OS_ONESIGNAL_ID] {
request.identityModel.hydrate(identityObject)
OSUserStateSnapshot.fireUserStateChangedIfCurrent(request.identityModel)
// Must follow the hydrate; see `dropIdentifyUsersThatCanNeverPrepare`.
self.removeFromQueue(request)

// Fetch this user's data if it is the current user
guard OneSignalUserManagerImpl.sharedInstance.currentUser(matching: request.identityModel.modelId) != nil
Expand All @@ -431,6 +483,8 @@ extension OSUserExecutor {
}

self.fetchUser(aliasLabel: OS_ONESIGNAL_ID, aliasId: onesignalId, identityModel: request.identityModel)
} else {
self.removeFromQueue(request)
}
} onFailure: { error in
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor executeFetchIdentityBySubscriptionRequest failed with error: \(error.debugDescription)")
Expand Down Expand Up @@ -470,10 +524,9 @@ extension OSUserExecutor {
request.sentToClient = true

OneSignalCoreImpl.sharedClient().execute(request) { _ in
self.removeFromQueue(request)

guard let onesignalId = request.identityModelToIdentify.onesignalId else {
OneSignalLog.onesignalLog(.LL_ERROR, message: "executeIdentifyUserRequest succeeded but is now missing OneSignal ID!")
self.removeFromQueue(request)
self.executePendingRequests()
return
}
Expand All @@ -484,6 +537,9 @@ extension OSUserExecutor {
request.aliasLabel: request.aliasId
]
request.identityModelToUpdate.hydrate(aliases)
OSUserStateSnapshot.fireUserStateChangedIfCurrent(request.identityModelToUpdate)
// Must follow the hydrate; see `dropIdentifyUsersThatCanNeverPrepare`.
self.removeFromQueue(request)

// the anonymous user has been identified, still need to Fetch User as we cleared local data
if OneSignalUserManagerImpl.sharedInstance.currentUser(matching: request.identityModelToUpdate.modelId) != nil {
Expand Down Expand Up @@ -608,8 +664,10 @@ extension OSUserExecutor {

// If this was a create user, it hydrates the onesignal_id of the request's identityModel
// The model in the store may be different, and it may be waiting on the onesignal_id of this previous model
// Only a current user is reported to the app; a parked Create User can complete after a switch.
if let identityObject = parseIdentityObjectResponse(response) {
identityModel.hydrate(identityObject)
OSUserStateSnapshot.fireUserStateChangedIfCurrent(identityModel)
if addNewRecords, let onesignalId = identityObject[OS_ONESIGNAL_ID] {
newRecordsState.add(onesignalId)
}
Expand Down
Loading