diff --git a/.swiftlint.yml b/.swiftlint.yml index f289b606e..82a9ad2ec 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1 +1,4 @@ -line_length: 150 \ No newline at end of file +line_length: 150 +disabled_rules: + # Track https://github.com/realm/SwiftLint/pull/5521 + - opening_brace diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index ac5dacd67..970382b96 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -177,6 +177,9 @@ 3CEE93542B7C78EC008440BD /* OneSignalUser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 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 */; }; + 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 */; }; 3CF862A028A1964F00776CA4 /* OSPropertiesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF8629F28A1964F00776CA4 /* OSPropertiesModel.swift */; }; 3CF862A228A197D200776CA4 /* OSPropertiesModelStoreListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF862A128A197D200776CA4 /* OSPropertiesModelStoreListener.swift */; }; @@ -1294,6 +1297,9 @@ 3CE92279289FA88B001B1062 /* OSIdentityModelStoreListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModelStoreListener.swift; sourceTree = ""; }; 3CEE90A62BFE6ABD00B0FB5B /* OSPropertiesSupportedProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesSupportedProperty.swift; sourceTree = ""; }; 3CEE90A82C000BD500B0FB5B /* OneSignalRequest+UnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OneSignalRequest+UnitTests.swift"; sourceTree = ""; }; + 3CF11E3C2C6D6155002856F5 /* UserExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserExecutorTests.swift; sourceTree = ""; }; + 3CF11E3F2C6E6DE2002856F5 /* MockNewRecordsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNewRecordsState.swift; sourceTree = ""; }; + 3CF1A5622C669EA40056B3AA /* OSNewRecordsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSNewRecordsState.swift; sourceTree = ""; }; 3CF8629D28A183F900776CA4 /* OSIdentityModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModel.swift; sourceTree = ""; }; 3CF8629F28A1964F00776CA4 /* OSPropertiesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesModel.swift; sourceTree = ""; }; 3CF862A128A197D200776CA4 /* OSPropertiesModelStoreListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesModelStoreListener.swift; sourceTree = ""; }; @@ -2051,6 +2057,7 @@ 3C115188289ADEA300565C41 /* OSModelStore.swift */, 3C115186289ADE7700565C41 /* OSModelStoreListener.swift */, 3C115184289ADE4F00565C41 /* OSModel.swift */, + 3CF1A5622C669EA40056B3AA /* OSNewRecordsState.swift */, 3C11518A289ADEEB00565C41 /* OSEventProducer.swift */, 3C11518C289AF5E800565C41 /* OSModelChangedHandler.swift */, 3C4F9E4328A4466C009F453A /* OSOperationRepo.swift */, @@ -2070,6 +2077,7 @@ children = ( 3C8544B82C5AEFF700F542A9 /* OneSignalOSCoreMocks.h */, 3C8544C22C5AF18B00F542A9 /* OSCoreMocks.swift */, + 3CF11E3F2C6E6DE2002856F5 /* MockNewRecordsState.swift */, ); path = OneSignalOSCoreMocks; sourceTree = ""; @@ -2150,6 +2158,7 @@ isa = PBXGroup; children = ( 3CDE664A2BFC2A55006DA114 /* OneSignalUserTests-Bridging-Header.h */, + 3CF11E3E2C6D61AC002856F5 /* Executors */, 3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */, 3CC890342C5BF9A7002CB4CC /* UserConcurrencyTests.swift */, 3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */, @@ -2166,6 +2175,14 @@ path = Support; sourceTree = ""; }; + 3CF11E3E2C6D61AC002856F5 /* Executors */ = { + isa = PBXGroup; + children = ( + 3CF11E3C2C6D6155002856F5 /* UserExecutorTests.swift */, + ); + path = Executors; + sourceTree = ""; + }; 3E2400391D4FFC31008BDE70 /* OneSignalFramework */ = { isa = PBXGroup; children = ( @@ -4038,6 +4055,7 @@ 3C115165289A259500565C41 /* OneSignalOSCore.docc in Sources */, 3C115189289ADEA300565C41 /* OSModelStore.swift in Sources */, 3C115185289ADE4F00565C41 /* OSModel.swift in Sources */, + 3CF1A5632C669EA40056B3AA /* OSNewRecordsState.swift in Sources */, 3C448BA22936B474002F96BC /* OSBackgroundTaskManager.swift in Sources */, 3C115187289ADE7700565C41 /* OSModelStoreListener.swift in Sources */, 3CE5F9E3289D88DC004A156E /* OSModelStoreChangedHandler.swift in Sources */, @@ -4054,6 +4072,7 @@ buildActionMask = 2147483647; files = ( 3C8544C32C5AF18B00F542A9 /* OSCoreMocks.swift in Sources */, + 3CF11E402C6E6DE2002856F5 /* MockNewRecordsState.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4092,6 +4111,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3CF11E3D2C6D6155002856F5 /* UserExecutorTests.swift in Sources */, 3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */, 3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */, 3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index 825cbafb6..42d48d10d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -259,6 +259,13 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP // Flush interval for operation repo in milliseconds #define POLL_INTERVAL_MS 5000 + + /** + The number of seconds to delay after an operation completes that creates or changes IDs. + This is a "cold down" period to avoid a caveat with OneSignal's backend replication, where you may + incorrectlyget a 404 when attempting a GET or PATCH REST API call on something just after it is created. + */ + #define OP_REPO_POST_CREATE_DELAY_SECONDS 3 #else // Test defines for API Client #define REATTEMPT_DELAY 0.004 @@ -279,6 +286,8 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP // Reduce flush interval for operation repo in tests #define POLL_INTERVAL_MS 100 + // Reduce delay in tests + #define OP_REPO_POST_CREATE_DELAY_SECONDS 0 #endif // A max timeout for a request, which might include multiple reattempts diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift index 0f8df1b02..b1b870f32 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift @@ -93,10 +93,6 @@ public class MockOneSignalClient: NSObject, IOneSignalClient { public func execute(_ request: OneSignalRequest, onSuccess successBlock: @escaping OSResultSuccessBlock, onFailure failureBlock: @escaping OSFailureBlock) { print("🧪 MockOneSignalClient execute called") - lock.withLock { - executedRequests.append(request) - } - if executeInstantaneously { finishExecutingRequest(request, onSuccess: successBlock, onFailure: failureBlock) } else { @@ -127,6 +123,9 @@ public class MockOneSignalClient: NSObject, IOneSignalClient { print("🧪 completing HTTP request: \(request)") // TODO: Check for existence of app_id in the request and fail if not. + lock.withLock { + executedRequests.append(request) + } self.didCompleteRequest(request) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSNewRecordsState.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSNewRecordsState.swift new file mode 100644 index 000000000..2fc1e322f --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSNewRecordsState.swift @@ -0,0 +1,68 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import OneSignalCore + +/** + * Purpose: Keeps track of IDs that were just created on the backend. + * This list gets used to delay network calls to ensure upcoming + * requests are ready to be accepted by the backend. + */ +public class OSNewRecordsState { + /** + Params: + - Key - a string ID such as onesignal ID or subscription ID + - Value - a Date timestamp of when the ID was created + */ + private var records: [String: Date] = [:] + private let lock = NSRecursiveLock() + + public init() { } + + /** + Only add a new record with the current timestamp if overwriting is requested, or it is not already present + */ + public func add(_ key: String, _ overwrite: Bool = false) { + lock.withLock { + if overwrite || records[key] == nil { + records[key] = Date() + } + } + } + + public func canAccess(_ key: String) -> Bool { + lock.withLock { + guard let timeLastMovedOrCreated = records[key] else { + return true + } + + let minimumTime = timeLastMovedOrCreated.addingTimeInterval(TimeInterval(OP_REPO_POST_CREATE_DELAY_SECONDS)) + + return Date() >= minimumTime + } + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/MockNewRecordsState.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/MockNewRecordsState.swift new file mode 100644 index 000000000..4a1c8d278 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/MockNewRecordsState.swift @@ -0,0 +1,60 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +@testable import OneSignalOSCore + +public class MockNewRecordsState: OSNewRecordsState { + public struct MockNewRecord { + let key: String + let overwrite: Bool + } + + public var records: [MockNewRecord] = [] + + override public func add(_ key: String, _ overwrite: Bool = false) { + let record = MockNewRecord(key: key, overwrite: overwrite) + records.append(record) + + super.add(key, overwrite) + } + + override public func canAccess(_ key: String) -> Bool { + return super.canAccess(key) + } + + public func get(_ key: String?) -> [MockNewRecord] { + return records.filter { $0.key == key } + } + + public func contains(_ key: String?) -> Bool { + return get(key).count > 0 + } + + public func wasOverwritten(_ key: String?) -> Bool { + return records.filter { $0.key == key && $0.overwrite }.count > 0 + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift index 0a428abaa..6a7b44b4d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift @@ -34,11 +34,13 @@ class OSIdentityOperationExecutor: OSOperationExecutor { // To simplify uncaching, we maintain separate request queues for each type var addRequestQueue: [OSRequestAddAliases] = [] var removeRequestQueue: [OSRequestRemoveAlias] = [] + let newRecordsState: OSNewRecordsState // The Identity executor dispatch queue, serial. This synchronizes access to the delta and request queues. private let dispatchQueue = DispatchQueue(label: "OneSignal.OSIdentityOperationExecutor", target: .global()) - init() { + init(newRecordsState: OSNewRecordsState) { + self.newRecordsState = newRecordsState // Read unfinished deltas and requests from cache, if any... uncacheDeltas() uncacheAddAliasRequests() @@ -72,7 +74,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { // 1. The model exists in the repo, so set it to be the Request's models request.identityModel = identityModel - } else if request.prepareForExecution() { + } else if request.prepareForExecution(newRecordsState: newRecordsState) { // 2. The request can be sent, add the model to the repo OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel) } else { @@ -95,7 +97,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { // 1. The model exists in the repo, so set it to be the Request's model request.identityModel = identityModel - } else if request.prepareForExecution() { + } else if request.prepareForExecution(newRecordsState: newRecordsState) { // 2. The request can be sent, add the model to the repo OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel) } else { @@ -191,7 +193,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { guard !request.sentToClient else { return } - guard request.prepareForExecution() else { + guard request.prepareForExecution(newRecordsState: newRecordsState) else { return } request.sentToClient = true @@ -250,7 +252,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { guard !request.sentToClient else { return } - guard request.prepareForExecution() else { + guard request.prepareForExecution(newRecordsState: newRecordsState) else { return } request.sentToClient = true diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index 5a0625f67..5e1ea285c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -64,11 +64,13 @@ class OSPropertyOperationExecutor: OSOperationExecutor { var supportedDeltas: [String] = [OS_UPDATE_PROPERTIES_DELTA] var deltaQueue: [OSDelta] = [] var updateRequestQueue: [OSRequestUpdateProperties] = [] + let newRecordsState: OSNewRecordsState // The property executor dispatch queue, serial. This synchronizes access to `deltaQueue` and `updateRequestQueue`. private let dispatchQueue = DispatchQueue(label: "OneSignal.OSPropertyOperationExecutor", target: .global()) - init() { + init(newRecordsState: OSNewRecordsState) { + self.newRecordsState = newRecordsState // Read unfinished deltas and requests from cache, if any... // Note that we should only have deltas for the current user as old ones are flushed.. uncacheDeltas() @@ -98,7 +100,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor { if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { // 1. The identity model exist in the repo, set it to be the Request's model request.identityModel = identityModel - } else if request.prepareForExecution() { + } else if request.prepareForExecution(newRecordsState: newRecordsState) { // 2. The request can be sent, add the model to the repo OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel) } else { @@ -233,7 +235,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor { guard !request.sentToClient else { return } - guard request.prepareForExecution() else { + guard request.prepareForExecution(newRecordsState: newRecordsState) else { return } request.sentToClient = true diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift index 837a49eb1..84b60ad3b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift @@ -36,11 +36,13 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { var removeRequestQueue: [OSRequestDeleteSubscription] = [] var updateRequestQueue: [OSRequestUpdateSubscription] = [] var subscriptionModels: [String: OSSubscriptionModel] = [:] + let newRecordsState: OSNewRecordsState // The Subscription executor dispatch queue, serial. This synchronizes access to the delta and request queues. private let dispatchQueue = DispatchQueue(label: "OneSignal.OSSubscriptionOperationExecutor", target: .global()) - init() { + init(newRecordsState: OSNewRecordsState) { + self.newRecordsState = newRecordsState // Read unfinished deltas and requests from cache, if any... uncacheDeltas() uncacheCreateSubscriptionRequests() @@ -89,7 +91,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { // a. The model exist in the repo request.identityModel = identityModel - } else if request.prepareForExecution() { + } else if request.prepareForExecution(newRecordsState: newRecordsState) { // b. The request can be sent, add the model to the repo OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel) } else { @@ -116,7 +118,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } else if let subscriptionModel = subscriptionModels[request.subscriptionModel.modelId] { // 2. The model exists in the dict of seen subscription models request.subscriptionModel = subscriptionModel - } else if !request.prepareForExecution() { + } else if !request.prepareForExecution(newRecordsState: newRecordsState) { // 3. The model does not exist AND this request cannot be sent, drop this Request OneSignalLog.onesignalLog(.LL_ERROR, message: "OSSubscriptionOperationExecutor.init dropped \(request)") removeRequestQueue.remove(at: index) @@ -139,7 +141,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } else if let subscriptionModel = subscriptionModels[request.subscriptionModel.modelId] { // 2. The model exists in the dict of seen subscription models request.subscriptionModel = subscriptionModel - } else if !request.prepareForExecution() { + } else if !request.prepareForExecution(newRecordsState: newRecordsState) { // 3. The models do not exist AND this request cannot be sent, drop this Request OneSignalLog.onesignalLog(.LL_ERROR, message: "OSSubscriptionOperationExecutor.init dropped \(request)") updateRequestQueue.remove(at: index) @@ -271,7 +273,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { guard !request.sentToClient else { return } - guard request.prepareForExecution() else { + guard request.prepareForExecution(newRecordsState: newRecordsState) else { return } request.sentToClient = true @@ -336,7 +338,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { guard !request.sentToClient else { return } - guard request.prepareForExecution() else { + guard request.prepareForExecution(newRecordsState: newRecordsState) else { return } request.sentToClient = true @@ -381,7 +383,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { guard !request.sentToClient else { return } - guard request.prepareForExecution() else { + guard request.prepareForExecution(newRecordsState: newRecordsState) else { return } request.sentToClient = true diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 52e5d0534..43ad5b3d1 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -35,17 +35,21 @@ import OneSignalOSCore */ class OSUserExecutor { var userRequestQueue: [OSUserRequest] = [] + private let newRecordsState: OSNewRecordsState + /// Delay by the "cool down" period plus a buffer of a set amount of milliseconds + private let flushDelayMilliseconds = Int(OP_REPO_POST_CREATE_DELAY_SECONDS * 1_000 + 200) // TODO: This could come from a config, plist, method, remote params - // The User executor dispatch queue, serial. This synchronizes access to the request queues. + /// The User executor dispatch queue, serial. This synchronizes access to the request queues. private let dispatchQueue = DispatchQueue(label: "OneSignal.OSUserExecutor", target: .global()) - init() { + init(newRecordsState: OSNewRecordsState) { + self.newRecordsState = newRecordsState uncacheUserRequests() migrateTransferSubscriptionRequests() executePendingRequests() } - // Read in requests from the cache, do not read in FetchUser requests as this is not needed. + /// Read in requests from the cache, do not read in FetchUser requests as this is not needed. private func uncacheUserRequests() { var userRequestQueue: [OSUserRequest] = [] @@ -90,7 +94,7 @@ class OSUserExecutor { // 3. Both models don't exist yet // Drop the request if the identityModelToIdentify does not already exist AND the request is missing OSID // Otherwise, this request will forever fail `prepareForExecution` and block pending requests such as recovery calls to `logout` or `login` - guard request.prepareForExecution() else { + guard request.prepareForExecution(newRecordsState: newRecordsState) else { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor.start() dropped: \(request)") continue } @@ -143,37 +147,47 @@ class OSUserExecutor { } } - func executePendingRequests() { - self.dispatchQueue.async { - OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSUserExecutor.executePendingRequests called with queue \(self.userRequestQueue)") + /** + Requests are flushed after a delay when they need to wait for the "cool down" period to access a user or subscription after its creation. + */ + func executePendingRequests(withDelay: Bool = false) { + if withDelay { + self.dispatchQueue.asyncAfter(deadline: .now() + .milliseconds(flushDelayMilliseconds)) { [weak self] in + self?._executePendingRequests() + } + } else { + self.dispatchQueue.async { + self._executePendingRequests() + } + } + } + + private func _executePendingRequests() { + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSUserExecutor.executePendingRequests called with queue \(self.userRequestQueue)") - if self.userRequestQueue.isEmpty { + for request in self.userRequestQueue { + // Return as soon as we reach an un-executable request + guard request.prepareForExecution(newRecordsState: self.newRecordsState) + else { + OneSignalLog.onesignalLog(.LL_WARN, message: "OSUserExecutor.executePendingRequests() is blocked by unexecutable request \(request)") + executePendingRequests(withDelay: true) return } - for request in self.userRequestQueue { - // Return as soon as we reach an un-executable request - if !request.prepareForExecution() { - OneSignalLog.onesignalLog(.LL_WARN, message: "OSUserExecutor.executePendingRequests() is blocked by unexecutable request \(request)") - return - } - - if request.isKind(of: OSRequestFetchIdentityBySubscription.self), let fetchIdentityRequest = request as? OSRequestFetchIdentityBySubscription { - self.executeFetchIdentityBySubscriptionRequest(fetchIdentityRequest) - return - } else if request.isKind(of: OSRequestCreateUser.self), let createUserRequest = request as? OSRequestCreateUser { - self.executeCreateUserRequest(createUserRequest) - return - } else if request.isKind(of: OSRequestIdentifyUser.self), let identifyUserRequest = request as? OSRequestIdentifyUser { - self.executeIdentifyUserRequest(identifyUserRequest) - return - } else if request.isKind(of: OSRequestFetchUser.self), let fetchUserRequest = request as? OSRequestFetchUser { - self.executeFetchUserRequest(fetchUserRequest) - return - } else { - // Log Error - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor met incompatible Request type that cannot be executed.") - } + if request.isKind(of: OSRequestFetchIdentityBySubscription.self), let fetchIdentityRequest = request as? OSRequestFetchIdentityBySubscription { + self.executeFetchIdentityBySubscriptionRequest(fetchIdentityRequest) + return + } else if request.isKind(of: OSRequestCreateUser.self), let createUserRequest = request as? OSRequestCreateUser { + self.executeCreateUserRequest(createUserRequest) + return + } else if request.isKind(of: OSRequestIdentifyUser.self), let identifyUserRequest = request as? OSRequestIdentifyUser { + self.executeIdentifyUserRequest(identifyUserRequest) + return + } else if request.isKind(of: OSRequestFetchUser.self), let fetchUserRequest = request as? OSRequestFetchUser { + self.executeFetchUserRequest(fetchUserRequest) + return + } else { + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor met incompatible Request type that cannot be executed.") } } } @@ -204,11 +218,6 @@ extension OSUserExecutor { guard !request.sentToClient else { return } - guard request.prepareForExecution() else { - // Currently there are no requirements needed before sending this request, so this will set the path - return - } - request.sentToClient = true // Hook up push subscription model if exists, it may be updated with a subscription_id, etc. if let modelId = request.pushSubscriptionModel?.modelId, @@ -217,17 +226,29 @@ extension OSUserExecutor { request.updatePushSubscriptionModel(pushSubscriptionModel) } + guard request.prepareForExecution(newRecordsState: newRecordsState) + else { + executePendingRequests(withDelay: true) + return + } + + request.sentToClient = true + OneSignalCoreImpl.sharedClient().execute(request) { response in self.removeFromQueue(request) - // TODO: Differentiate if we need to fetch the user based on response code of 200, 201, 202 // Create User's response won't send us the user's complete info if this user already exists if let response = response { + let shouldAddNewRecords = request.pushSubscriptionModel != nil // Parse the response for any data we need to update - self.parseFetchUserResponse(response: response, identityModel: request.identityModel, originalPushToken: request.originalPushToken) + self.parseFetchUserResponse( + response: response, + identityModel: request.identityModel, + originalPushToken: request.originalPushToken, + addNewRecords: shouldAddNewRecords + ) // If this user already exists and we logged into an external_id, fetch the user data - // TODO: Only do this if response code is 200 or 202 // Fetch the user only if its the current user and non-anonymous if OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModel), let identity = request.parameters?["identity"] as? [String: String], @@ -270,9 +291,13 @@ extension OSUserExecutor { guard !request.sentToClient else { return } - guard request.prepareForExecution() else { + + // newRecordsState is unused for this request + guard request.prepareForExecution(newRecordsState: newRecordsState) else { + executePendingRequests(withDelay: true) return } + request.sentToClient = true OneSignalCoreImpl.sharedClient().execute(request) { response in @@ -322,10 +347,12 @@ extension OSUserExecutor { guard !request.sentToClient else { return } - guard request.prepareForExecution() else { - // Missing onesignal_id + + guard request.prepareForExecution(newRecordsState: newRecordsState) else { + executePendingRequests(withDelay: true) return } + request.sentToClient = true OneSignalCoreImpl.sharedClient().execute(request) { _ in @@ -345,8 +372,9 @@ extension OSUserExecutor { request.identityModelToUpdate.hydrate(aliases) // the anonymous user has been identified, still need to Fetch User as we cleared local data - // Fetch the user only if its the current user if OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModelToUpdate) { + // Add onesignal ID to new records because an immediate fetch may not return the newly-applied external ID + self.newRecordsState.add(onesignalId, true) self.fetchUser(aliasLabel: OS_ONESIGNAL_ID, aliasId: onesignalId, identityModel: request.identityModelToUpdate) } else { self.executePendingRequests() @@ -394,18 +422,22 @@ extension OSUserExecutor { appendToQueue(request) - executePendingRequests() + // User fetch will always be called after a delay unless it is to refresh the user state on a new session + executePendingRequests(withDelay: !onNewSession) } func executeFetchUserRequest(_ request: OSRequestFetchUser) { guard !request.sentToClient else { return } - guard request.prepareForExecution() else { - // This should not happen as we set the alias to use for the request path + + guard request.prepareForExecution(newRecordsState: newRecordsState) else { + executePendingRequests(withDelay: true) return } + request.sentToClient = true + OneSignalCoreImpl.sharedClient().execute(request) { response in self.removeFromQueue(request) @@ -465,12 +497,15 @@ extension OSUserExecutor { /** Used to parse Create User and Fetch User responses. The `originalPushToken` is the push token when the request was created, which may be different from the push token currently in the SDK. For example, when the request was created, there may be no push token yet, but soon after, the SDK receives a push token. This is used to determine whether or not to hydrate the push subscription. */ - func parseFetchUserResponse(response: [AnyHashable: Any], identityModel: OSIdentityModel, originalPushToken: String?) { + func parseFetchUserResponse(response: [AnyHashable: Any], identityModel: OSIdentityModel, originalPushToken: String?, addNewRecords: Bool = false) { // 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 if let identityObject = parseIdentityObjectResponse(response) { identityModel.hydrate(identityObject) + if addNewRecords, let onesignalId = identityObject[OS_ONESIGNAL_ID] { + newRecordsState.add(onesignalId) + } } // TODO: Determine how to hydrate the push subscription, which is still faulty. @@ -480,13 +515,19 @@ extension OSUserExecutor { // Hydrate the push subscription if we don't already have a subscription ID AND token matches the original request if OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId == nil, - let subscriptionObject = parseSubscriptionObjectResponse(response) { + let subscriptionObject = parseSubscriptionObjectResponse(response) + { for subModel in subscriptionObject { if subModel["type"] as? String == "iOSPush", - areTokensEqual(tokenA: originalPushToken, tokenB: subModel["token"] as? String) { // response may have "" token or no token + // response may have "" token or no token + areTokensEqual(tokenA: originalPushToken, tokenB: subModel["token"] as? String) + { OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.hydrate(subModel) if let subId = subModel["id"] as? String { OSNotificationsManager.setPushSubscriptionId(subId) + if addNewRecords { + newRecordsState.add(subId) + } } break } @@ -510,7 +551,8 @@ extension OSUserExecutor { if let address = subModel["token"] as? String, let rawType = subModel["type"] as? String, rawType != "iOSPush", - let type = OSSubscriptionType(rawValue: rawType) { + let type = OSSubscriptionType(rawValue: rawType) + { if let model = models[address] { // This subscription exists in the store, hydrate model.hydrate(subModel) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 9a2973d74..77ecd4c79 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -120,6 +120,8 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { var identityModelRepo = OSIdentityModelRepo() + let newRecordsState = OSNewRecordsState() + var hasCalledStart = false private var jwtExpiredHandler: OSJwtExpiredHandler? @@ -222,13 +224,13 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { // Setup the executors // The OSUserExecutor has to run first, before other executors - self.userExecutor = OSUserExecutor() + self.userExecutor = OSUserExecutor(newRecordsState: newRecordsState) OSOperationRepo.sharedInstance.start() // Cannot initialize these executors in `init` as they reference the sharedInstance - let propertyExecutor = OSPropertyOperationExecutor() - let identityExecutor = OSIdentityOperationExecutor() - let subscriptionExecutor = OSSubscriptionOperationExecutor() + let propertyExecutor = OSPropertyOperationExecutor(newRecordsState: newRecordsState) + let identityExecutor = OSIdentityOperationExecutor(newRecordsState: newRecordsState) + let subscriptionExecutor = OSSubscriptionOperationExecutor(newRecordsState: newRecordsState) self.propertyExecutor = propertyExecutor self.identityExecutor = identityExecutor self.subscriptionExecutor = subscriptionExecutor @@ -440,7 +442,7 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { /** Creates and sets a blank new SDK user with the provided externalId, if any. */ - private func setNewInternalUser(externalId: String?, pushSubscriptionModel: OSSubscriptionModel?) -> OSUserInternal { + func setNewInternalUser(externalId: String?, pushSubscriptionModel: OSSubscriptionModel?) -> OSUserInternal { let aliases: [String: String]? if let externalIdToUse = externalId { aliases = [OS_EXTERNAL_ID: externalIdToUse] diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestAddAliases.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestAddAliases.swift index 52d4d0cfa..c52a84e20 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestAddAliases.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestAddAliases.swift @@ -26,6 +26,7 @@ */ import OneSignalCore +import OneSignalOSCore class OSRequestAddAliases: OneSignalRequest, OSUserRequest { var sentToClient = false @@ -37,15 +38,16 @@ class OSRequestAddAliases: OneSignalRequest, OSUserRequest { var identityModel: OSIdentityModel let aliases: [String: String] - // requires a `onesignal_id` to send this request - func prepareForExecution() -> Bool { - if let onesignalId = identityModel.onesignalId, let appId = OneSignalConfigManager.getAppId() { + /// requires a `onesignal_id` to send this request + func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { + if let onesignalId = identityModel.onesignalId, + newRecordsState.canAccess(onesignalId), + let appId = OneSignalConfigManager.getAppId() + { self.addJWTHeader(identityModel: identityModel) self.path = "apps/\(appId)/users/by/\(OS_ONESIGNAL_ID)/\(onesignalId)/identity" return true } else { - // self.path is non-nil, so set to empty string - self.path = "" return false } } @@ -57,7 +59,6 @@ class OSRequestAddAliases: OneSignalRequest, OSUserRequest { super.init() self.parameters = ["identity": aliases] self.method = PATCH - _ = prepareForExecution() // sets the path property } func encode(with coder: NSCoder) { @@ -86,6 +87,5 @@ class OSRequestAddAliases: OneSignalRequest, OSUserRequest { self.parameters = parameters self.method = HTTPMethod(rawValue: rawMethod) self.timestamp = timestamp - _ = prepareForExecution() } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateSubscription.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateSubscription.swift index e46870b2c..51b383d6e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateSubscription.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateSubscription.swift @@ -26,6 +26,7 @@ */ import OneSignalCore +import OneSignalOSCore /** Primary uses of this request are for adding Email and SMS subscriptions. Push subscriptions typically won't be created using @@ -43,13 +44,15 @@ class OSRequestCreateSubscription: OneSignalRequest, OSUserRequest { var identityModel: OSIdentityModel // Need the onesignal_id of the user - func prepareForExecution() -> Bool { - if let onesignalId = identityModel.onesignalId, let appId = OneSignalConfigManager.getAppId() { + func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { + if let onesignalId = identityModel.onesignalId, + newRecordsState.canAccess(onesignalId), + let appId = OneSignalConfigManager.getAppId() + { self.addJWTHeader(identityModel: identityModel) self.path = "apps/\(appId)/users/by/\(OS_ONESIGNAL_ID)/\(onesignalId)/subscriptions" return true } else { - self.path = "" // self.path is non-nil, so set to empty string return false } } @@ -61,7 +64,6 @@ class OSRequestCreateSubscription: OneSignalRequest, OSUserRequest { super.init() self.parameters = ["subscription": subscriptionModel.jsonRepresentation()] self.method = POST - _ = prepareForExecution() // sets the path property } func encode(with coder: NSCoder) { @@ -90,6 +92,5 @@ class OSRequestCreateSubscription: OneSignalRequest, OSUserRequest { self.parameters = parameters self.method = HTTPMethod(rawValue: rawMethod) self.timestamp = timestamp - _ = prepareForExecution() } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateUser.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateUser.swift index d88bde925..ef4e4a5e3 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateUser.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateUser.swift @@ -26,6 +26,7 @@ */ import OneSignalCore +import OneSignalOSCore /** This request will be made with the minimum information needed. The payload will contain an externalId or no identities. @@ -44,15 +45,23 @@ class OSRequestCreateUser: OneSignalRequest, OSUserRequest { var pushSubscriptionModel: OSSubscriptionModel? var originalPushToken: String? - func prepareForExecution() -> Bool { + /// Checks if the subscription ID can be accessed, if a subscription is being included in the request + func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { guard let appId = OneSignalConfigManager.getAppId() else { - OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the create user request due to null app ID.") + OneSignalLog.onesignalLog(.LL_ERROR, message: "Cannot generate the create user request due to null app ID.") return false } + + if let subscriptionId = pushSubscriptionModel?.subscriptionId, + !newRecordsState.canAccess(subscriptionId) + { + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the create user request yet.") + return false + } + _ = self.addPushSubscriptionIdToAdditionalHeaders() self.addJWTHeader(identityModel: identityModel) self.path = "apps/\(appId)/users" - // The pushSub doesn't need to have a token. return true } @@ -99,7 +108,7 @@ class OSRequestCreateUser: OneSignalRequest, OSUserRequest { super.init() self.parameters = [ "identity": [aliasLabel: aliasId], - "refresh_device_metadata": true, + "refresh_device_metadata": true ] self.method = POST } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestDeleteSubscription.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestDeleteSubscription.swift index 83c7275b1..35c36c2ad 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestDeleteSubscription.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestDeleteSubscription.swift @@ -26,6 +26,7 @@ */ import OneSignalCore +import OneSignalOSCore /** Delete the subscription specified by the `subscriptionId` in the `subscriptionModel`. @@ -42,13 +43,14 @@ class OSRequestDeleteSubscription: OneSignalRequest, OSUserRequest { var subscriptionModel: OSSubscriptionModel // Need the subscription_id - func prepareForExecution() -> Bool { - if let subscriptionId = subscriptionModel.subscriptionId, let appId = OneSignalConfigManager.getAppId() { + func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { + if let subscriptionId = subscriptionModel.subscriptionId, + newRecordsState.canAccess(subscriptionId), + let appId = OneSignalConfigManager.getAppId() + { self.path = "apps/\(appId)/subscriptions/\(subscriptionId)" return true } else { - // self.path is non-nil, so set to empty string - self.path = "" return false } } @@ -58,7 +60,6 @@ class OSRequestDeleteSubscription: OneSignalRequest, OSUserRequest { self.stringDescription = "" super.init() self.method = DELETE - _ = prepareForExecution() // sets the path property } func encode(with coder: NSCoder) { @@ -81,6 +82,5 @@ class OSRequestDeleteSubscription: OneSignalRequest, OSUserRequest { super.init() self.method = HTTPMethod(rawValue: rawMethod) self.timestamp = timestamp - _ = prepareForExecution() } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchIdentityBySubscription.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchIdentityBySubscription.swift index 25dd80be0..e26c7855d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchIdentityBySubscription.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchIdentityBySubscription.swift @@ -26,6 +26,7 @@ */ import OneSignalCore +import OneSignalOSCore class OSRequestFetchIdentityBySubscription: OneSignalRequest, OSUserRequest { var sentToClient = false @@ -38,7 +39,8 @@ class OSRequestFetchIdentityBySubscription: OneSignalRequest, OSUserRequest { var identityModel: OSIdentityModel var pushSubscriptionModel: OSSubscriptionModel - func prepareForExecution() -> Bool { + func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { + // newRecordsState is unused for this request guard let appId = OneSignalConfigManager.getAppId() else { OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the FetchIdentityBySubscription request due to null app ID.") return false diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchUser.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchUser.swift index cd12416b3..2e03e0c3e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchUser.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchUser.swift @@ -26,6 +26,7 @@ */ import OneSignalCore +import OneSignalOSCore /** Fetch the user by the provided alias. This is expected to be `onesignal_id` in most cases. @@ -43,9 +44,11 @@ class OSRequestFetchUser: OneSignalRequest, OSUserRequest { let aliasId: String let onNewSession: Bool - func prepareForExecution() -> Bool { - guard let appId = OneSignalConfigManager.getAppId() else { - OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the fetch user request due to null app ID.") + func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { + guard let appId = OneSignalConfigManager.getAppId(), + newRecordsState.canAccess(aliasId) + else { + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the fetch user request for \(aliasLabel): \(aliasId) yet.") return false } self.addJWTHeader(identityModel: identityModel) @@ -61,7 +64,6 @@ class OSRequestFetchUser: OneSignalRequest, OSUserRequest { self.stringDescription = "" super.init() self.method = GET - _ = prepareForExecution() // sets the path property } func encode(with coder: NSCoder) { @@ -92,6 +94,5 @@ class OSRequestFetchUser: OneSignalRequest, OSUserRequest { super.init() self.method = HTTPMethod(rawValue: rawMethod) self.timestamp = timestamp - _ = prepareForExecution() } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestIdentifyUser.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestIdentifyUser.swift index 5572efe05..b63c275a2 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestIdentifyUser.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestIdentifyUser.swift @@ -26,6 +26,7 @@ */ import OneSignalCore +import OneSignalOSCore /** The `identityModelToIdentify` is used for the `onesignal_id` of the user we want to associate with this alias. @@ -46,16 +47,19 @@ class OSRequestIdentifyUser: OneSignalRequest, OSUserRequest { let aliasLabel: String let aliasId: String - // requires a onesignal_id to send this request - func prepareForExecution() -> Bool { - if let onesignalId = identityModelToIdentify.onesignalId, let appId = OneSignalConfigManager.getAppId() { + /// requires a `onesignal_id` to send this request + func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { + if let onesignalId = identityModelToIdentify.onesignalId, + newRecordsState.canAccess(onesignalId), + let appId = OneSignalConfigManager.getAppId() + { self.addJWTHeader(identityModel: identityModelToIdentify) self.path = "apps/\(appId)/users/by/\(OS_ONESIGNAL_ID)/\(onesignalId)/identity" return true } else { // self.path is non-nil, so set to empty string self.path = "" - OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the Identify User request due to null app ID or null OneSignal ID.") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the Identify User request yet.") return false } } @@ -76,7 +80,6 @@ class OSRequestIdentifyUser: OneSignalRequest, OSUserRequest { super.init() self.parameters = ["identity": [aliasLabel: aliasId]] self.method = PATCH - _ = prepareForExecution() // sets the path property } func encode(with coder: NSCoder) { @@ -111,6 +114,5 @@ class OSRequestIdentifyUser: OneSignalRequest, OSUserRequest { self.timestamp = timestamp self.parameters = parameters self.method = HTTPMethod(rawValue: rawMethod) - _ = prepareForExecution() } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestRemoveAlias.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestRemoveAlias.swift index 49e6ea691..adf98e568 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestRemoveAlias.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestRemoveAlias.swift @@ -26,6 +26,7 @@ */ import OneSignalCore +import OneSignalOSCore class OSRequestRemoveAlias: OneSignalRequest, OSUserRequest { var sentToClient = false @@ -37,14 +38,15 @@ class OSRequestRemoveAlias: OneSignalRequest, OSUserRequest { let labelToRemove: String var identityModel: OSIdentityModel - func prepareForExecution() -> Bool { - if let onesignalId = identityModel.onesignalId, let appId = OneSignalConfigManager.getAppId() { + func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { + if let onesignalId = identityModel.onesignalId, + newRecordsState.canAccess(onesignalId), + let appId = OneSignalConfigManager.getAppId() + { self.addJWTHeader(identityModel: identityModel) self.path = "apps/\(appId)/users/by/\(OS_ONESIGNAL_ID)/\(onesignalId)/identity/\(labelToRemove)" return true } else { - // self.path is non-nil, so set to empty string - self.path = "" return false } } @@ -55,7 +57,6 @@ class OSRequestRemoveAlias: OneSignalRequest, OSUserRequest { self.stringDescription = "OSRequestRemoveAlias with aliasLabel: \(labelToRemove)" super.init() self.method = DELETE - _ = prepareForExecution() // sets the path property } func encode(with coder: NSCoder) { @@ -81,6 +82,5 @@ class OSRequestRemoveAlias: OneSignalRequest, OSUserRequest { super.init() self.method = HTTPMethod(rawValue: rawMethod) self.timestamp = timestamp - _ = prepareForExecution() } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestTransferSubscription.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestTransferSubscription.swift index 854f12ce9..df0e589a8 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestTransferSubscription.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestTransferSubscription.swift @@ -26,6 +26,7 @@ */ import OneSignalCore +import OneSignalOSCore /** Deprecated as of `5.2.3`. Use CreateUser instead. This class skeleton remains due to potentially cached requests. @@ -42,7 +43,7 @@ class OSRequestTransferSubscription: OneSignalRequest, OSUserRequest { let aliasLabel: String let aliasId: String - func prepareForExecution() -> Bool { + func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { return false } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift index 83146ab33..b46ff3894 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift @@ -26,6 +26,7 @@ */ import OneSignalCore +import OneSignalOSCore class OSRequestUpdateProperties: OneSignalRequest, OSUserRequest { var sentToClient = false @@ -38,16 +39,16 @@ class OSRequestUpdateProperties: OneSignalRequest, OSUserRequest { // TODO: Decide if addPushSubscriptionIdToAdditionalHeadersIfNeeded should block. // Note Android adds it to requests, if the push sub ID exists - func prepareForExecution() -> Bool { + func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { if let onesignalId = identityModel.onesignalId, - let appId = OneSignalConfigManager.getAppId() { + newRecordsState.canAccess(onesignalId), + let appId = OneSignalConfigManager.getAppId() + { _ = self.addPushSubscriptionIdToAdditionalHeaders() self.addJWTHeader(identityModel: identityModel) self.path = "apps/\(appId)/users/by/\(OS_ONESIGNAL_ID)/\(onesignalId)" return true } else { - // self.path is non-nil, so set to empty string - self.path = "" return false } } @@ -58,7 +59,6 @@ class OSRequestUpdateProperties: OneSignalRequest, OSUserRequest { super.init() self.parameters = params self.method = PATCH - _ = prepareForExecution() // sets the path property } func encode(with coder: NSCoder) { @@ -84,6 +84,5 @@ class OSRequestUpdateProperties: OneSignalRequest, OSUserRequest { self.parameters = parameters self.method = HTTPMethod(rawValue: rawMethod) self.timestamp = timestamp - _ = prepareForExecution() } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateSubscription.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateSubscription.swift index 3dfe05581..39fba8c5c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateSubscription.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateSubscription.swift @@ -26,6 +26,7 @@ */ import OneSignalCore +import OneSignalOSCore /** Currently, only the Push Subscription will make this Update Request. @@ -40,12 +41,14 @@ class OSRequestUpdateSubscription: OneSignalRequest, OSUserRequest { var subscriptionModel: OSSubscriptionModel // Need the subscription_id - func prepareForExecution() -> Bool { - if let subscriptionId = subscriptionModel.subscriptionId, let appId = OneSignalConfigManager.getAppId() { + func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { + if let subscriptionId = subscriptionModel.subscriptionId, + newRecordsState.canAccess(subscriptionId), + let appId = OneSignalConfigManager.getAppId() + { self.path = "apps/\(appId)/subscriptions/\(subscriptionId)" return true } else { - self.path = "" // self.path is non-nil, so set to empty string return false } } @@ -77,7 +80,6 @@ class OSRequestUpdateSubscription: OneSignalRequest, OSUserRequest { self.parameters = ["subscription": subscriptionParams] self.method = PATCH - _ = prepareForExecution() // sets the path property } func encode(with coder: NSCoder) { @@ -103,6 +105,5 @@ class OSRequestUpdateSubscription: OneSignalRequest, OSUserRequest { self.parameters = parameters self.method = HTTPMethod(rawValue: rawMethod) self.timestamp = timestamp - _ = prepareForExecution() } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSUserRequest.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSUserRequest.swift index f6020c140..133473ba8 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSUserRequest.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSUserRequest.swift @@ -26,10 +26,11 @@ */ import OneSignalCore +import OneSignalOSCore protocol OSUserRequest: OneSignalRequest, NSCoding { var sentToClient: Bool { get set } - func prepareForExecution() -> Bool + func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool } internal extension OneSignalRequest { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift index e79ceea8e..77560bd28 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift @@ -20,7 +20,7 @@ public class MockUserRequests: NSObject { public static func testDefaultPushSubPayload(id: String) -> [String: Any] { return [ - "id": testPushSubId, + "id": id, "app_id": "test-app-id", "type": "iOSPush", "token": "", @@ -43,7 +43,7 @@ public class MockUserRequests: NSObject { public static func testDefaultFullCreateUserResponse(onesignalId: String, externalId: String?, subscriptionId: String?) -> [String: Any] { let identity = testIdentityPayload(onesignalId: onesignalId, externalId: externalId) - let subscription = testDefaultPushSubPayload(id: testPushSubId) + let subscription = testDefaultPushSubPayload(id: subscriptionId ?? testPushSubId) let properties = [ "language": "en", "timezone_id": "America/Los_Angeles", @@ -84,11 +84,10 @@ extension MockUserRequests { response: anonCreateResponse) } - public static func setDefaultCreateUserResponses(with client: MockOneSignalClient, externalId: String) { + public static func setDefaultCreateUserResponses(with client: MockOneSignalClient, externalId: String, subscriptionId: String? = nil) { let osid = getOneSignalId(for: externalId) - let userResponse = MockUserRequests.testIdentityPayload(onesignalId: osid, externalId: externalId) - + let userResponse = testDefaultFullCreateUserResponse(onesignalId: osid, externalId: externalId, subscriptionId: subscriptionId) client.setMockResponseForRequest( request: "", response: userResponse diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift new file mode 100644 index 000000000..31dd74e29 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift @@ -0,0 +1,197 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection +with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import XCTest +import OneSignalCore +import OneSignalOSCore +import OneSignalCoreMocks +import OneSignalOSCoreMocks +import OneSignalUserMocks +@testable import OneSignalUser + +/// This class has helpers that can be used in other tests and can be extracted out, as they are used +private class Mocks { + let client = MockOneSignalClient() + let newRecordsState = MockNewRecordsState() + let userExecutor: OSUserExecutor + + init() { + OneSignalCoreImpl.setSharedClient(client) + userExecutor = OSUserExecutor(newRecordsState: newRecordsState) + } + + func createUserInstance(externalId: String) -> OSUserInternal { + let identityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: externalId], changeNotifier: OSEventProducer()) + let propertiesModel = OSPropertiesModel(changeNotifier: OSEventProducer()) + let pushModel = OSSubscriptionModel(type: .push, address: "", subscriptionId: nil, reachable: false, isDisabled: false, changeNotifier: OSEventProducer()) + return OSUserInternalImpl(identityModel: identityModel, propertiesModel: propertiesModel, pushSubscriptionModel: pushModel) + } + + func setUserManagerInternalUser(externalId: String) -> OSUserInternal { + return OneSignalUserManagerImpl.sharedInstance.setNewInternalUser( + externalId: externalId, + pushSubscriptionModel: OSSubscriptionModel(type: .push, address: "", subscriptionId: testPushSubId, reachable: false, isDisabled: false, changeNotifier: OSEventProducer()) + ) + } +} + +final class UserExecutorTests: XCTestCase { + + override func setUpWithError() throws { + OneSignalCoreMocks.clearUserDefaults() + OneSignalUserMocks.reset() + // App ID is set because requests have guards against null App ID + OneSignalConfigManager.setAppId("test-app-id") + // Temp. logging to help debug during testing + OneSignalLog.setLogLevel(.LL_VERBOSE) + } + + override func tearDownWithError() throws { } + + func testCreateUser_withPushSubscription_addsToNewRecords() { + /* Setup */ + let mocks = Mocks() + MockUserRequests.setDefaultCreateUserResponses(with: mocks.client, externalId: userA_EUID, subscriptionId: "push-sub-id") + + /* When */ + mocks.userExecutor.createUser(mocks.createUserInstance(externalId: userA_EUID)) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.newRecordsState.contains(userA_OSID)) + XCTAssertTrue(mocks.newRecordsState.contains("push-sub-id")) + } + + func testCreateUser_withoutPushSubscription_doesNot_addToNewRecords() { + /* Setup */ + let mocks = Mocks() + MockUserRequests.setDefaultCreateUserResponses(with: mocks.client, externalId: userA_EUID) + + /* When */ + let identityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) + mocks.userExecutor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userA_EUID, identityModel: identityModel) + + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) + XCTAssertTrue(mocks.newRecordsState.records.isEmpty) + } + + /** + When an external ID is successfully applied to an anonymous user, its Onesignal ID should be force re-added to + the new records state with an updated timestamp. This is to prevent an immediate fetch where the external ID + can be missing from the fetch response, as it has not finished being applied to the user on the backend. + */ + func testIdentifyUser_successfully_forcesAddToNewRecords() { + /* Setup */ + let mocks = Mocks() + MockUserRequests.setDefaultIdentifyUserResponses(with: mocks.client, externalId: userA_EUID, conflicted: false) + + /* When */ + let anonIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID], changeNotifier: OSEventProducer()) + let newIdentityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) + + // The current user needs to be the same, set it in the user manager + OneSignalUserManagerImpl.sharedInstance.identityModelStore.add(id: OS_IDENTITY_MODEL_KEY, model: newIdentityModel, hydrating: false) + mocks.userExecutor.identifyUser(externalId: userA_EUID, identityModelToIdentify: anonIdentityModel, identityModelToUpdate: newIdentityModel) + + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestIdentifyUser.self)) + XCTAssertTrue(mocks.newRecordsState.contains(userA_OSID)) + XCTAssertTrue(mocks.newRecordsState.wasOverwritten(userA_OSID)) + } + + /** + When an external ID is successfully applied to an anonymous user, but the current user is no longer the same, + nothing is added to the new records state. + */ + func testIdentifyUserSuccessful_butUserHasChangedSince_doesNotAddToNewRecords() { + /* Setup */ + let mocks = Mocks() + MockUserRequests.setDefaultIdentifyUserResponses(with: mocks.client, externalId: userA_EUID, conflicted: false) + + /* When */ + let anonIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID], changeNotifier: OSEventProducer()) + let newIdentityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) + + mocks.userExecutor.identifyUser(externalId: userA_EUID, identityModelToIdentify: anonIdentityModel, identityModelToUpdate: newIdentityModel) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestIdentifyUser.self)) + XCTAssertTrue(mocks.newRecordsState.records.isEmpty) + } + + /** + When Identify User encounters a 409 conflict, a Create User call will be made. + The response from that request will add its Onesignal ID to the new records state. + */ + func testIdentifyUser_withConflict_addsToNewRecords() { + /* Setup */ + let mocks = Mocks() + let user = mocks.setUserManagerInternalUser(externalId: userB_EUID) + + let anonIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID], changeNotifier: OSEventProducer()) + let newIdentityModel = user.identityModel + + MockUserRequests.setDefaultIdentifyUserResponses(with: mocks.client, externalId: userB_EUID, conflicted: true) + + /* When */ + mocks.userExecutor.identifyUser(externalId: userB_EUID, identityModelToIdentify: anonIdentityModel, identityModelToUpdate: newIdentityModel) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestIdentifyUser.self)) + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) + XCTAssertEqual(mocks.newRecordsState.records.count, 1) + XCTAssertTrue(mocks.newRecordsState.contains(userB_OSID)) + } + + func testIdentifyUserWithConflict_butUserHasChangedSince_doesNot_addToNewRecords() { + /* Setup */ + let mocks = Mocks() + + let user = mocks.setUserManagerInternalUser(externalId: "new-eid") + let anonIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID], changeNotifier: OSEventProducer()) + let newIdentityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userB_EUID], changeNotifier: OSEventProducer()) + + MockUserRequests.setDefaultIdentifyUserResponses(with: mocks.client, externalId: userB_EUID, conflicted: true) + + /* When */ + mocks.userExecutor.identifyUser(externalId: userB_EUID, identityModelToIdentify: anonIdentityModel, identityModelToUpdate: newIdentityModel) + + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestIdentifyUser.self)) + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) + XCTAssertTrue(mocks.newRecordsState.records.isEmpty) + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift index 55ca2873f..95859d323 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift @@ -321,7 +321,7 @@ final class SwitchUserIntegrationTests: XCTestCase { // Increase flush interval to allow all the updates to batch OSOperationRepo.sharedInstance.pollIntervalMilliseconds = 300 // Wait to let any pending flushes in the Operation Repo to run - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.1) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.3) // 1. Set up mock responses for the first anonymous user let tagsUserAnon = ["tag_anon": "value_anon"] @@ -369,7 +369,7 @@ final class SwitchUserIntegrationTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.addEmail("email_b@example.com") // 3. Run background threads - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 2) /* Then */ diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift index f624c0e62..bcf076a74 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift @@ -92,7 +92,7 @@ final class UserConcurrencyTests: XCTestCase { ) OneSignalCoreImpl.setSharedClient(client) - let executor = OSSubscriptionOperationExecutor() + let executor = OSSubscriptionOperationExecutor(newRecordsState: OSNewRecordsState()) OSOperationRepo.sharedInstance.addExecutor(executor) /* When */ @@ -131,7 +131,7 @@ final class UserConcurrencyTests: XCTestCase { OneSignalCoreImpl.setSharedClient(client) MockUserRequests.setAddAliasesResponse(with: client, aliases: aliases) - let executor = OSIdentityOperationExecutor() + let executor = OSIdentityOperationExecutor(newRecordsState: OSNewRecordsState()) OSOperationRepo.sharedInstance.addExecutor(executor) /* When */ @@ -172,7 +172,7 @@ final class UserConcurrencyTests: XCTestCase { let identityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()) OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(identityModel) - let executor = OSPropertyOperationExecutor() + let executor = OSPropertyOperationExecutor(newRecordsState: OSNewRecordsState()) OSOperationRepo.sharedInstance.addExecutor(executor) /* When */ @@ -213,7 +213,7 @@ final class UserConcurrencyTests: XCTestCase { let identityModel1 = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()) let identityModel2 = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()) - let userExecutor = OSUserExecutor() + let userExecutor = OSUserExecutor(newRecordsState: OSNewRecordsState()) /* When */