Skip to content

Commit 2530e2f

Browse files
committed
Fire callback for 401 error in property executor
Also adds tests for the property executor and refactors some testing code that can be shared with other executor tests
1 parent 02acc2a commit 2530e2f

File tree

6 files changed

+251
-29
lines changed

6 files changed

+251
-29
lines changed

iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,8 @@
354354
DE20426024E21C2C00350E4F /* UIApplication+OneSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = DE20425D24E21C2C00350E4F /* UIApplication+OneSignal.m */; };
355355
DE2D8F452947D85800844084 /* OneSignalExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17F927026BA3002D3A5D /* OneSignalExtension.framework */; };
356356
DE2D8F4A2947D86200844084 /* OneSignalOutcomes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D188027037F43002D3A5D /* OneSignalOutcomes.framework */; };
357+
DE3568EA2C88F56600AF447C /* PropertyExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */; };
358+
DE3568EC2C88F5BD00AF447C /* OneSignalExecutorMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568EB2C88F5BD00AF447C /* OneSignalExecutorMocks.swift */; };
357359
DE3784842888CFF900453A8E /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; };
358360
DE3784852888D00300453A8E /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; };
359361
DE3784862888D00B00453A8E /* OneSignalUser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -1514,6 +1516,8 @@
15141516
DE1DD05F2C87D87B00787071 /* OSJwtInvalidatedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSJwtInvalidatedEvent.swift; sourceTree = "<group>"; };
15151517
DE20425C24E21C1500350E4F /* UIApplication+OneSignal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIApplication+OneSignal.h"; sourceTree = "<group>"; };
15161518
DE20425D24E21C2C00350E4F /* UIApplication+OneSignal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIApplication+OneSignal.m"; sourceTree = "<group>"; };
1519+
DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyExecutorTests.swift; sourceTree = "<group>"; };
1520+
DE3568EB2C88F5BD00AF447C /* OneSignalExecutorMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalExecutorMocks.swift; sourceTree = "<group>"; };
15171521
DE3CD2FE270FA9F200A5BECD /* OSOutcomes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSOutcomes.m; sourceTree = "<group>"; };
15181522
DE51DDE3294262AB0073D5C4 /* OSRemoteParamController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSRemoteParamController.m; sourceTree = "<group>"; };
15191523
DE51DDE4294262AB0073D5C4 /* OSRemoteParamController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSRemoteParamController.h; sourceTree = "<group>"; };
@@ -2171,6 +2175,7 @@
21712175
3C87066F2BDE0957000D8CD2 /* MockUserRequests.swift */,
21722176
3C8706712BDEE076000D8CD2 /* MockUserDefines.swift */,
21732177
3CC063E52B6D7F96002BB07F /* OneSignalUserMocks.swift */,
2178+
DE3568EB2C88F5BD00AF447C /* OneSignalExecutorMocks.swift */,
21742179
);
21752180
path = OneSignalUserMocks;
21762181
sourceTree = "<group>";
@@ -2201,6 +2206,7 @@
22012206
isa = PBXGroup;
22022207
children = (
22032208
3CF11E3C2C6D6155002856F5 /* UserExecutorTests.swift */,
2209+
DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */,
22042210
);
22052211
path = Executors;
22062212
sourceTree = "<group>";
@@ -4130,6 +4136,7 @@
41304136
3C8706702BDE0957000D8CD2 /* MockUserRequests.swift in Sources */,
41314137
3C8706722BDEE076000D8CD2 /* MockUserDefines.swift in Sources */,
41324138
3CC063E62B6D7F96002BB07F /* OneSignalUserMocks.swift in Sources */,
4139+
DE3568EC2C88F5BD00AF447C /* OneSignalExecutorMocks.swift in Sources */,
41334140
);
41344141
runOnlyForDeploymentPostprocessing = 0;
41354142
};
@@ -4138,6 +4145,7 @@
41384145
buildActionMask = 2147483647;
41394146
files = (
41404147
3CF11E3D2C6D6155002856F5 /* UserExecutorTests.swift in Sources */,
4148+
DE3568EA2C88F56600AF447C /* PropertyExecutorTests.swift in Sources */,
41414149
3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */,
41424150
3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */,
41434151
3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */,

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor {
182182
guard let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId)
183183
else {
184184
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSPropertyOperationExecutor.processDeltaQueue dropped: \(delta)")
185+
// ECM Remove the delta here. Need an iterator to do it in place
185186
continue
186187
}
187188

@@ -266,6 +267,12 @@ class OSPropertyOperationExecutor: OSOperationExecutor {
266267
executeUpdatePropertiesRequest(request, inBackground: inBackground)
267268
}
268269
}
270+
271+
func handleUnauthorizedError(externalId: String, error: NSError) {
272+
if (jwtConfig.isRequired ?? false) {
273+
OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error)
274+
}
275+
}
269276

270277
func executeUpdatePropertiesRequest(_ request: OSRequestUpdateProperties, inBackground: Bool) {
271278
guard !request.sentToClient else {
@@ -310,6 +317,11 @@ class OSPropertyOperationExecutor: OSOperationExecutor {
310317
// The subscription has been deleted along with the user, so remove the subscription_id but keep the same push subscription model
311318
OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId = nil
312319
OneSignalUserManagerImpl.sharedInstance._logout()
320+
} else if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) {
321+
if let externalId = request.identityModel.externalId {
322+
self.handleUnauthorizedError(externalId: externalId, error: nsError)
323+
}
324+
request.sentToClient = false
313325
} else if responseType != .retryable {
314326
// Fail, no retry, remove from cache and queue
315327
self.updateRequestQueue.removeAll(where: { $0 == request})

iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,19 @@ extension MockUserRequests {
115115
let error = testUnauthorizedailureError()
116116
client.setMockFailureResponseForRequest(request:"<OSRequestFetchUser with onesignal_id: \(onesignalId)>", error: error)
117117
}
118+
119+
public static func setUnauthorizedUpdatePropertiesFailureResponses(with client: MockOneSignalClient, tags: [String: String]) {
120+
let error = testUnauthorizedailureError()
121+
122+
let params: NSDictionary = [
123+
"properties": [
124+
"tags": tags
125+
],
126+
"refresh_device_metadata": false
127+
]
128+
129+
client.setMockFailureResponseForRequest(request:"<OSRequestUpdateProperties with parameters: \(params.toSortedString())>", error: error)
130+
}
118131

119132
public static func setDefaultIdentifyUserResponses(with client: MockOneSignalClient, externalId: String, conflicted: Bool = false) {
120133
var osid: String
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
Modified MIT License
3+
4+
Copyright 2024 OneSignal
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
1. The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
2. All copies of substantial portions of the Software may only be used in connection
17+
with services provided by OneSignal.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
*/
27+
28+
import OneSignalCore
29+
import OneSignalOSCore
30+
import OneSignalCoreMocks
31+
import OneSignalOSCoreMocks
32+
@testable import OneSignalUser
33+
34+
@objc
35+
open class OneSignalExecutorMocks: NSObject {
36+
public let client = MockOneSignalClient()
37+
public let newRecordsState = MockNewRecordsState()
38+
public let jwtConfig = OSUserJwtConfig()
39+
40+
override public init() {
41+
super.init()
42+
OneSignalCoreImpl.setSharedClient(client)
43+
}
44+
45+
@objc
46+
open func setAuthRequired(_ required: Bool) {
47+
// Set User Manager's JWT to off, or it blocks requests in prepareForExecution
48+
OneSignalUserManagerImpl.sharedInstance.jwtConfig.isRequired = required
49+
jwtConfig.isRequired = required
50+
}
51+
52+
open func createUserInstance(externalId: String) -> OSUserInternal {
53+
let identityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: externalId], changeNotifier: OSEventProducer())
54+
let propertiesModel = OSPropertiesModel(changeNotifier: OSEventProducer())
55+
let pushModel = OSSubscriptionModel(type: .push, address: "", subscriptionId: nil, reachable: false, isDisabled: false, changeNotifier: OSEventProducer())
56+
return OSUserInternalImpl(identityModel: identityModel, propertiesModel: propertiesModel, pushSubscriptionModel: pushModel)
57+
}
58+
59+
open func setUserManagerInternalUser(externalId: String, onesignalId: String? = nil) -> OSUserInternal {
60+
let user = OneSignalUserManagerImpl.sharedInstance.setNewInternalUser(
61+
externalId: externalId,
62+
pushSubscriptionModel: OSSubscriptionModel(type: .push, address: "", subscriptionId: testPushSubId, reachable: false, isDisabled: false, changeNotifier: OSEventProducer()))
63+
if let onesignalId = onesignalId {
64+
user.identityModel.addAliases([OS_ONESIGNAL_ID: onesignalId])
65+
}
66+
return user
67+
}
68+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
Modified MIT License
3+
4+
Copyright 2024 OneSignal
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
1. The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
2. All copies of substantial portions of the Software may only be used in connection
17+
with services provided by OneSignal.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
*/
27+
28+
import XCTest
29+
import OneSignalCore
30+
import OneSignalOSCore
31+
import OneSignalCoreMocks
32+
import OneSignalOSCoreMocks
33+
import OneSignalUserMocks
34+
@testable import OneSignalUser
35+
36+
private class Mocks: OneSignalExecutorMocks {
37+
var propertyExecutor: OSPropertyOperationExecutor!
38+
39+
override init() {
40+
super.init()
41+
propertyExecutor = OSPropertyOperationExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig)
42+
}
43+
}
44+
45+
final class PropertyExecutorTests: XCTestCase {
46+
47+
override func setUpWithError() throws {
48+
OneSignalCoreMocks.clearUserDefaults()
49+
OneSignalUserMocks.reset()
50+
// App ID is set because requests have guards against null App ID
51+
OneSignalConfigManager.setAppId("test-app-id")
52+
// Temp. logging to help debug during testing
53+
OneSignalLog.setLogLevel(.LL_VERBOSE)
54+
}
55+
56+
override func tearDownWithError() throws { }
57+
58+
func testUpdateTagsSendsWhenProcessed() {
59+
/* Setup */
60+
let mocks = Mocks()
61+
mocks.setAuthRequired(false)
62+
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
63+
64+
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
65+
let tags = ["testUserA" : "true"]
66+
MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags)
67+
mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags))
68+
69+
/* When */
70+
mocks.propertyExecutor.processDeltaQueue(inBackground: false)
71+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
72+
73+
/* Then */
74+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self))
75+
}
76+
77+
func testUpdateTags_IdentityVerificationRequired_butNoToken() {
78+
/* Setup */
79+
let mocks = Mocks()
80+
mocks.setAuthRequired(true)
81+
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
82+
83+
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
84+
let tags = ["testUserA" : "true"]
85+
MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags)
86+
mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags))
87+
88+
/* When */
89+
mocks.propertyExecutor.processDeltaQueue(inBackground: false)
90+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
91+
92+
/* Then */
93+
XCTAssertFalse(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self))
94+
}
95+
96+
func testUpdateTags_IdentityVerificationRequired_withToken() {
97+
/* Setup */
98+
let mocks = Mocks()
99+
mocks.setAuthRequired(true)
100+
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
101+
102+
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
103+
user.identityModel.jwtBearerToken = userA_JwtToken
104+
let tags = ["testUserA" : "true"]
105+
MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags)
106+
mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags))
107+
108+
/* When */
109+
mocks.propertyExecutor.processDeltaQueue(inBackground: false)
110+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
111+
112+
/* Then */
113+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self))
114+
}
115+
116+
func testCreateUser_IdentityVerificationRequired_withInvalidToken() {
117+
/* Setup */
118+
let mocks = Mocks()
119+
mocks.setAuthRequired(true)
120+
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
121+
122+
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
123+
user.identityModel.jwtBearerToken = userA_JwtToken
124+
125+
126+
127+
let tags = ["testUserA" : "true"]
128+
MockUserRequests.setUnauthorizedUpdatePropertiesFailureResponses(with: mocks.client, tags: tags)
129+
mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags))
130+
131+
var invalidatedCallbackWasCalled = false
132+
OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in
133+
XCTAssertTrue(event.message == "token has invalid claims: token is expired")
134+
invalidatedCallbackWasCalled = true
135+
}
136+
137+
/* When */
138+
mocks.propertyExecutor.processDeltaQueue(inBackground: false)
139+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
140+
141+
/* Then */
142+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self))
143+
XCTAssertTrue(invalidatedCallbackWasCalled)
144+
}
145+
}

iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -33,36 +33,12 @@ import OneSignalOSCoreMocks
3333
import OneSignalUserMocks
3434
@testable import OneSignalUser
3535

36-
/// This class has helpers that can be used in other tests and can be extracted out, as they are used
37-
private class Mocks {
38-
let client = MockOneSignalClient()
39-
let newRecordsState = MockNewRecordsState()
40-
let jwtConfig = OSUserJwtConfig()
41-
let userExecutor: OSUserExecutor
42-
43-
init() {
44-
OneSignalCoreImpl.setSharedClient(client)
45-
userExecutor = OSUserExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig)
46-
}
47-
48-
func setAuthRequired(_ required: Bool) {
49-
// Set User Manager's JWT to off, or it blocks requests in prepareForExecution
50-
OneSignalUserManagerImpl.sharedInstance.jwtConfig.isRequired = required
51-
jwtConfig.isRequired = required
52-
}
36+
private class Mocks: OneSignalExecutorMocks {
37+
var userExecutor: OSUserExecutor!
5338

54-
func createUserInstance(externalId: String) -> OSUserInternal {
55-
let identityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: externalId], changeNotifier: OSEventProducer())
56-
let propertiesModel = OSPropertiesModel(changeNotifier: OSEventProducer())
57-
let pushModel = OSSubscriptionModel(type: .push, address: "", subscriptionId: nil, reachable: false, isDisabled: false, changeNotifier: OSEventProducer())
58-
return OSUserInternalImpl(identityModel: identityModel, propertiesModel: propertiesModel, pushSubscriptionModel: pushModel)
59-
}
60-
61-
func setUserManagerInternalUser(externalId: String) -> OSUserInternal {
62-
return OneSignalUserManagerImpl.sharedInstance.setNewInternalUser(
63-
externalId: externalId,
64-
pushSubscriptionModel: OSSubscriptionModel(type: .push, address: "", subscriptionId: testPushSubId, reachable: false, isDisabled: false, changeNotifier: OSEventProducer())
65-
)
39+
override init() {
40+
super.init()
41+
userExecutor = OSUserExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig)
6642
}
6743
}
6844

0 commit comments

Comments
 (0)