Skip to content

Commit 3003044

Browse files
authored
Merge pull request #1418 from OneSignal/use_central_identity_model_repo
[Bug] Some pending properties can be sent to new user incorrectly, when users change
2 parents 4bf6826 + ef9d2c6 commit 3003044

27 files changed

+624
-171
lines changed

iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj

Lines changed: 244 additions & 45 deletions
Large diffs are not rendered by default.

iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/xcshareddata/xcschemes/OneSignalUserTests.xcscheme

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
buildImplicitDependencies = "YES">
88
</BuildAction>
99
<TestAction
10-
buildConfiguration = "Debug"
10+
buildConfiguration = "Test"
1111
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
1212
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
1313
shouldUseLaunchSchemeArgsEnv = "YES"
@@ -27,7 +27,7 @@
2727
</Testables>
2828
</TestAction>
2929
<LaunchAction
30-
buildConfiguration = "Debug"
30+
buildConfiguration = "Test"
3131
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
3232
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
3333
launchStyle = "0"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# in tests, we may want to force cast and throw any errors
2+
disabled_rules:
3+
- force_cast
4+
- identifier_name
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
extension NSDictionary {
2+
func contains(key: String, value: Any) -> Bool {
3+
guard let dictVal = self[key] else {
4+
return false
5+
}
6+
7+
return equals(dictVal, value)
8+
}
9+
10+
func contains(_ dict: [String: Any]) -> Bool {
11+
for (key, value) in dict {
12+
if !contains(key: key, value: value) {
13+
return false
14+
}
15+
}
16+
return true
17+
}
18+
19+
private func equals(_ x: Any, _ y: Any) -> Bool {
20+
guard x is AnyHashable else { return false }
21+
guard y is AnyHashable else { return false }
22+
return (x as! AnyHashable) == (y as! AnyHashable)
23+
}
24+
}

iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ public class MockOneSignalClient: NSObject, IOneSignalClient {
4040
var shouldUseProvisionalAuthorization = false // new in iOS 12 (aka Direct to History)
4141
var remoteParamsOutcomes: [String: Any] = [:]
4242

43+
public var allRequestsHandled = true
44+
4345
/** May add to or change this default remote params response*/
4446
public func getRemoteParamsResponse() -> [String: Any] {
4547
return remoteParamsResponse ?? [
@@ -70,7 +72,7 @@ public class MockOneSignalClient: NSObject, IOneSignalClient {
7072

7173
// Temp. method to log info while building unit tests
7274
@objc public func logSelfInfo() {
73-
print("🧪 MockOneSignalClient with executionQueue \(executionQueue)")
75+
print("🧪 MockOneSignalClient with executedRequests \(executedRequests)")
7476
}
7577

7678
public func reset() {
@@ -115,6 +117,7 @@ public class MockOneSignalClient: NSObject, IOneSignalClient {
115117
if (mockResponses[String(describing: request)]) != nil {
116118
successBlock(mockResponses[String(describing: request)])
117119
} else {
120+
allRequestsHandled = false
118121
print("🧪 cannot find a mock response for request: \(request)")
119122
}
120123
}
@@ -137,3 +140,34 @@ public class MockOneSignalClient: NSObject, IOneSignalClient {
137140
mockResponses[request] = response
138141
}
139142
}
143+
144+
// MARK: - Asserts
145+
146+
extension MockOneSignalClient {
147+
/**
148+
Checks if there is only one executed request that contains the payload provided, and the url matches the path provided.
149+
*/
150+
public func onlyOneRequest(contains path: String, contains payload: [String: Any]) -> Bool {
151+
var found = false
152+
153+
for request in executedRequests {
154+
guard let params = request.parameters as? NSDictionary else {
155+
continue
156+
}
157+
158+
if params.contains(payload) {
159+
if request.path == path {
160+
guard !found else {
161+
// False if more than 1 request satisfies both requirements
162+
return false
163+
}
164+
found = true
165+
} else {
166+
return false
167+
}
168+
}
169+
}
170+
171+
return found
172+
}
173+
}

iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import Foundation
2424
import OneSignalCore
25+
import XCTest
2526

2627
@objc
2728
public class OneSignalCoreMocks: NSObject {
@@ -43,4 +44,10 @@ public class OneSignalCoreMocks: NSObject {
4344
sharedUserDefaults.removeObject(forKey: key)
4445
}
4546
}
47+
48+
/** Wait specified number of seconds for any async methods to run */
49+
public static func waitForBackgroundThreads(seconds: Double) {
50+
let expectation = XCTestExpectation(description: "Wait for \(seconds) seconds")
51+
_ = XCTWaiter.wait(for: [expectation], timeout: seconds)
52+
}
4653
}

iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSDelta.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
*/
2727

2828
import Foundation
29+
import OneSignalCore
2930

3031
// TODO: Known Issue: Since these don't carry the app_id, it may have changed by the time Deltas become Requests, if app_id changes.
3132
// All requests requiring unique ID's will effectively be dropped.
@@ -34,6 +35,7 @@ open class OSDelta: NSObject, NSCoding {
3435
public let name: String
3536
public let deltaId: String
3637
public let timestamp: Date
38+
public let identityModelId: String
3739
public var model: OSModel
3840
public let property: String
3941
public let value: Any
@@ -42,10 +44,11 @@ open class OSDelta: NSObject, NSCoding {
4244
return "<OSDelta \(name) with property: \(property) value: \(value)>"
4345
}
4446

45-
public init(name: String, model: OSModel, property: String, value: Any) {
47+
public init(name: String, identityModelId: String, model: OSModel, property: String, value: Any) {
4648
self.name = name
4749
self.deltaId = UUID().uuidString
4850
self.timestamp = Date()
51+
self.identityModelId = identityModelId
4952
self.model = model
5053
self.property = property
5154
self.value = value
@@ -55,6 +58,7 @@ open class OSDelta: NSObject, NSCoding {
5558
coder.encode(name, forKey: "name")
5659
coder.encode(deltaId, forKey: "deltaId")
5760
coder.encode(timestamp, forKey: "timestamp")
61+
coder.encode(identityModelId, forKey: "identityModelId")
5862
coder.encode(model, forKey: "model")
5963
coder.encode(property, forKey: "property")
6064
coder.encode(value, forKey: "value")
@@ -64,17 +68,19 @@ open class OSDelta: NSObject, NSCoding {
6468
guard let name = coder.decodeObject(forKey: "name") as? String,
6569
let deltaId = coder.decodeObject(forKey: "deltaId") as? String,
6670
let timestamp = coder.decodeObject(forKey: "timestamp") as? Date,
71+
let identityModelId = coder.decodeObject(forKey: "identityModelId") as? String,
6772
let model = coder.decodeObject(forKey: "model") as? OSModel,
6873
let property = coder.decodeObject(forKey: "property") as? String,
6974
let value = coder.decodeObject(forKey: "value")
7075
else {
71-
// Log error
76+
OneSignalLog.onesignalLog(.LL_ERROR, message: "Unable to init OSDelta from cache")
7277
return nil
7378
}
7479

7580
self.name = name
7681
self.deltaId = deltaId
7782
self.timestamp = timestamp
83+
self.identityModelId = identityModelId
7884
self.model = model
7985
self.property = property
8086
self.value = value

iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStoreListener.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ extension OSModelStoreListener {
4747
store.changeSubscription.subscribe(self)
4848
}
4949

50-
func close() {
50+
public func close() {
5151
store.changeSubscription.unsubscribe(self)
5252
}
5353

iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
4040
if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] {
4141
// Hook each uncached Delta to the model in the store
4242
for (index, delta) in deltaQueue.enumerated().reversed() {
43-
if let modelInStore = OneSignalUserManagerImpl.sharedInstance.identityModelStore.getModel(modelId: delta.model.modelId) {
44-
// The model exists in the store, set it to be the Delta's model
43+
if let modelInStore = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.model.modelId) {
44+
// The model exists in the repo, set it to be the Delta's model
4545
delta.model = modelInStore
4646
} else {
4747
// The model does not exist, drop this Delta
48+
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor.init dropped \(delta)")
4849
deltaQueue.remove(at: index)
4950
}
5051
}
@@ -59,14 +60,15 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
5960
if var addRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestAddAliases] {
6061
// Hook each uncached Request to the model in the store
6162
for (index, request) in addRequestQueue.enumerated().reversed() {
62-
if let identityModel = OneSignalUserManagerImpl.sharedInstance.identityModelStore.getModel(modelId: request.identityModel.modelId) {
63-
// 1. The model exists in the store, so set it to be the Request's models
63+
if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) {
64+
// 1. The model exists in the repo, so set it to be the Request's models
6465
request.identityModel = identityModel
65-
} else if let identityModel = OSUserExecutor.identityModels[request.identityModel.modelId] {
66-
// 2. The model exists in the user executor
67-
request.identityModel = identityModel
68-
} else if !request.prepareForExecution() {
69-
// 3. The models do not exist AND this request cannot be sent, drop this Request
66+
} else if request.prepareForExecution() {
67+
// 2. The request can be sent, add the model to the repo
68+
OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel)
69+
} else {
70+
// 3. The model do not exist AND this request cannot be sent, drop this Request
71+
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor.init dropped \(request)")
7072
addRequestQueue.remove(at: index)
7173
}
7274
}
@@ -79,14 +81,15 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
7981
if var removeRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestRemoveAlias] {
8082
// Hook each uncached Request to the model in the store
8183
for (index, request) in removeRequestQueue.enumerated().reversed() {
82-
if let identityModel = OneSignalUserManagerImpl.sharedInstance.identityModelStore.getModel(modelId: request.identityModel.modelId) {
83-
// 1. The model exists in the store, so set it to be the Request's model
84+
if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) {
85+
// 1. The model exists in the repo, so set it to be the Request's model
8486
request.identityModel = identityModel
85-
} else if let identityModel = OSUserExecutor.identityModels[request.identityModel.modelId] {
86-
// 2. The model exists in the user executor
87-
request.identityModel = identityModel
88-
} else if !request.prepareForExecution() {
87+
} else if request.prepareForExecution() {
88+
// 2. The request can be sent, add the model to the repo
89+
OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel)
90+
} else {
8991
// 3. The model does not exist AND this request cannot be sent, drop this Request
92+
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor.init dropped \(request)")
9093
removeRequestQueue.remove(at: index)
9194
}
9295
}

iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,9 @@ class OSPropertyOperationExecutor: OSOperationExecutor {
4040
// Read unfinished deltas from cache, if any...
4141
// Note that we should only have deltas for the current user as old ones are flushed..
4242
if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] {
43-
// Hook each uncached Delta to the model in the store
4443
for (index, delta) in deltaQueue.enumerated().reversed() {
45-
if let modelInStore = OneSignalUserManagerImpl.sharedInstance.propertiesModelStore.getModel(modelId: delta.model.modelId) {
46-
// 1. The model exists in the properties model store, set it to be the Delta's model
47-
delta.model = modelInStore
48-
} else {
49-
// 2. The model does not exist, drop this Delta
44+
if OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) == nil {
45+
// The identity model does not exist, drop this Delta
5046
OneSignalLog.onesignalLog(.LL_WARN, message: "OSPropertyOperationExecutor.init dropped: \(delta)")
5147
deltaQueue.remove(at: index)
5248
}
@@ -61,17 +57,13 @@ class OSPropertyOperationExecutor: OSOperationExecutor {
6157
if var updateRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestUpdateProperties] {
6258
// Hook each uncached Request to the model in the store
6359
for (index, request) in updateRequestQueue.enumerated().reversed() {
64-
// 0. Hook up the properties model if its the current user's so it can hydrate
65-
if let propertiesModel = OneSignalUserManagerImpl.sharedInstance.propertiesModelStore.getModel(modelId: request.modelToUpdate.modelId) {
66-
request.modelToUpdate = propertiesModel
67-
}
68-
if let identityModel = OneSignalUserManagerImpl.sharedInstance.identityModelStore.getModel(modelId: request.identityModel.modelId) {
69-
// 1. The identity model exist in the store, set it to be the Request's models
60+
if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) {
61+
// 1. The identity model exist in the repo, set it to be the Request's model
7062
request.identityModel = identityModel
71-
} else if let identityModel = OSUserExecutor.identityModels[request.identityModel.modelId] {
72-
// 2. The model exists in the user executor
73-
request.identityModel = identityModel
74-
} else if !request.prepareForExecution() {
63+
} else if request.prepareForExecution() {
64+
// 2. The request can be sent, add the model to the repo
65+
OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel)
66+
} else {
7567
// 3. The identitymodel do not exist AND this request cannot be sent, drop this Request
7668
OneSignalLog.onesignalLog(.LL_WARN, message: "OSPropertyOperationExecutor.init dropped: \(request)")
7769
updateRequestQueue.remove(at: index)
@@ -103,17 +95,18 @@ class OSPropertyOperationExecutor: OSOperationExecutor {
10395
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSPropertyOperationExecutor processDeltaQueue with queue: \(self.deltaQueue)")
10496
}
10597
for delta in self.deltaQueue {
106-
guard let model = delta.model as? OSPropertiesModel else {
107-
// Log error
98+
guard let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId)
99+
else {
100+
// drop this delta
101+
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSPropertyOperationExecutor.processDeltaQueue dropped: \(delta)")
108102
continue
109103
}
110104

111105
let request = OSRequestUpdateProperties(
112106
properties: [delta.property: delta.value],
113107
deltas: nil,
114108
refreshDeviceMetadata: false, // Sort this out.
115-
modelToUpdate: model,
116-
identityModel: OneSignalUserManagerImpl.sharedInstance.user.identityModel // TODO: Make sure this is ok
109+
identityModel: identityModel
117110
)
118111
self.updateRequestQueue.append(request)
119112
}
@@ -204,7 +197,6 @@ extension OSPropertyOperationExecutor {
204197
properties: [:],
205198
deltas: propertiesDeltas.jsonRepresentation(),
206199
refreshDeviceMetadata: refreshDeviceMetadata,
207-
modelToUpdate: propertiesModel,
208200
identityModel: identityModel)
209201

210202
if sendImmediately {

0 commit comments

Comments
 (0)