Skip to content

Commit c0156d7

Browse files
committed
fire Jwt callback from subscription executor
Includes tests Currently delete and update requests don't have an identity model attached. This may need to be changed for JWT
1 parent 2530e2f commit c0156d7

File tree

6 files changed

+245
-4
lines changed

6 files changed

+245
-4
lines changed

iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@
356356
DE2D8F4A2947D86200844084 /* OneSignalOutcomes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D188027037F43002D3A5D /* OneSignalOutcomes.framework */; };
357357
DE3568EA2C88F56600AF447C /* PropertyExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */; };
358358
DE3568EC2C88F5BD00AF447C /* OneSignalExecutorMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568EB2C88F5BD00AF447C /* OneSignalExecutorMocks.swift */; };
359+
DE3568F02C89067400AF447C /* SubscriptionsExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568EF2C89067400AF447C /* SubscriptionsExecutorTests.swift */; };
359360
DE3784842888CFF900453A8E /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; };
360361
DE3784852888D00300453A8E /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; };
361362
DE3784862888D00B00453A8E /* OneSignalUser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -1518,6 +1519,7 @@
15181519
DE20425D24E21C2C00350E4F /* UIApplication+OneSignal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIApplication+OneSignal.m"; sourceTree = "<group>"; };
15191520
DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyExecutorTests.swift; sourceTree = "<group>"; };
15201521
DE3568EB2C88F5BD00AF447C /* OneSignalExecutorMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalExecutorMocks.swift; sourceTree = "<group>"; };
1522+
DE3568EF2C89067400AF447C /* SubscriptionsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsExecutorTests.swift; sourceTree = "<group>"; };
15211523
DE3CD2FE270FA9F200A5BECD /* OSOutcomes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSOutcomes.m; sourceTree = "<group>"; };
15221524
DE51DDE3294262AB0073D5C4 /* OSRemoteParamController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSRemoteParamController.m; sourceTree = "<group>"; };
15231525
DE51DDE4294262AB0073D5C4 /* OSRemoteParamController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSRemoteParamController.h; sourceTree = "<group>"; };
@@ -2207,6 +2209,7 @@
22072209
children = (
22082210
3CF11E3C2C6D6155002856F5 /* UserExecutorTests.swift */,
22092211
DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */,
2212+
DE3568EF2C89067400AF447C /* SubscriptionsExecutorTests.swift */,
22102213
);
22112214
path = Executors;
22122215
sourceTree = "<group>";
@@ -4149,6 +4152,7 @@
41494152
3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */,
41504153
3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */,
41514154
3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */,
4155+
DE3568F02C89067400AF447C /* SubscriptionsExecutorTests.swift in Sources */,
41524156
3CDE664C2BFC2A56006DA114 /* OneSignalUserObjcTests.m in Sources */,
41534157
);
41544158
runOnlyForDeploymentPostprocessing = 0;

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

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,12 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
278278
}
279279
}
280280
}
281+
282+
func handleUnauthorizedError(externalId: String, error: NSError) {
283+
if (jwtConfig.isRequired ?? false) {
284+
OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error)
285+
}
286+
}
281287

282288
func executeCreateSubscriptionRequest(_ request: OSRequestCreateSubscription, inBackground: Bool) {
283289
guard !request.sentToClient else {
@@ -331,6 +337,11 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
331337
// The subscription has been deleted along with the user, so remove the subscription_id but keep the same push subscription model
332338
OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId = nil
333339
OneSignalUserManagerImpl.sharedInstance._logout()
340+
} else if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) {
341+
if let externalId = request.identityModel.externalId {
342+
self.handleUnauthorizedError(externalId: externalId, error: nsError)
343+
}
344+
request.sentToClient = false
334345
} else if responseType != .retryable {
335346
// Fail, no retry, remove from cache and queue
336347
self.addRequestQueue.removeAll(where: { $0 == request})
@@ -375,7 +386,13 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
375386
self.dispatchQueue.async {
376387
if let nsError = error as? NSError {
377388
let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code)
378-
if responseType != .retryable {
389+
if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) {
390+
// ECM The delete subscription request doesn't have an identity model?
391+
if let externalId = OneSignalUserManagerImpl.sharedInstance.user.identityModel.externalId {
392+
self.handleUnauthorizedError(externalId: externalId, error: nsError)
393+
}
394+
request.sentToClient = false
395+
} else if responseType != .retryable {
379396
// Fail, no retry, remove from cache and queue
380397
// If this request returns a missing status, that is ok as this is a delete request
381398
self.removeRequestQueue.removeAll(where: { $0 == request})
@@ -417,7 +434,13 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
417434
self.dispatchQueue.async {
418435
if let nsError = error as? NSError {
419436
let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code)
420-
if responseType != .retryable {
437+
if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) {
438+
// ECM The update subscription request doesn't have an identity model?
439+
if let externalId = OneSignalUserManagerImpl.sharedInstance.user.identityModel.externalId {
440+
self.handleUnauthorizedError(externalId: externalId, error: nsError)
441+
}
442+
request.sentToClient = false
443+
} else if responseType != .retryable {
421444
// Fail, no retry, remove from cache and queue
422445
self.updateRequestQueue.removeAll(where: { $0 == request})
423446
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue)

iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,8 @@ public let userB_OSID = "test_user_b_onesignal_id"
55
public let userB_EUID = "test_user_b_external_id"
66

77
public let testPushSubId = "test_push_subscription_id"
8-
8+
public let testEmailSubId = "test_email_subscription_id"
9+
public let testPushToken = "2b7347630b72265c83b1c1d2227f563ce6169d5aaf274b06f1a1fadf3a04be69"
910
public let userA_JwtToken = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w"
11+
12+
public let userA_email = "[email protected]"

iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,21 @@ extension MockUserRequests {
128128

129129
client.setMockFailureResponseForRequest(request:"<OSRequestUpdateProperties with parameters: \(params.toSortedString())>", error: error)
130130
}
131+
132+
public static func setUnauthorizedAddEmailFailureResponse(with client: MockOneSignalClient, email: String) {
133+
let error = testUnauthorizedailureError()
134+
client.setMockFailureResponseForRequest(request:"<OSRequestCreateSubscription with token: \(email)>", error: error)
135+
}
136+
137+
public static func setUnauthorizedRemoveEmailFailureResponse(with client: MockOneSignalClient, email: String) {
138+
let error = testUnauthorizedailureError()
139+
client.setMockFailureResponseForRequest(request:"<OSRequestDeleteSubscription with subscriptionModel: \(email)>", error: error)
140+
}
141+
142+
public static func setUnauthorizedUpdateSubscriptionFailureResponse(with client: MockOneSignalClient, token: String) {
143+
let error = testUnauthorizedailureError()
144+
client.setMockFailureResponseForRequest(request:"OSRequestUpdateSubscription with subscriptionObject: [\"token\": \"\(token)\"]", error: error)
145+
}
131146

132147
public static func setDefaultIdentifyUserResponses(with client: MockOneSignalClient, externalId: String, conflicted: Bool = false) {
133148
var osid: String

iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ final class PropertyExecutorTests: XCTestCase {
113113
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self))
114114
}
115115

116-
func testCreateUser_IdentityVerificationRequired_withInvalidToken() {
116+
func testUpdateProperty_IdentityVerificationRequired_withInvalidToken() {
117117
/* Setup */
118118
let mocks = Mocks()
119119
mocks.setAuthRequired(true)
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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 subscriptionExecutor: OSSubscriptionOperationExecutor!
38+
39+
override init() {
40+
super.init()
41+
subscriptionExecutor = OSSubscriptionOperationExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig)
42+
}
43+
}
44+
45+
final class SubscriptionExecutorTests: 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 testAddEmailSendsWhenProcessed() {
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 email = userA_email
66+
MockUserRequests.setAddEmailResponse(with: mocks.client, email: email)
67+
mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email))
68+
69+
/* When */
70+
mocks.subscriptionExecutor.processDeltaQueue(inBackground: false)
71+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
72+
73+
/* Then */
74+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self))
75+
}
76+
77+
func testAddEmail_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 email = userA_email
85+
MockUserRequests.setAddEmailResponse(with: mocks.client, email: email)
86+
mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email))
87+
88+
/* When */
89+
mocks.subscriptionExecutor.processDeltaQueue(inBackground: false)
90+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
91+
92+
/* Then */
93+
XCTAssertFalse(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self))
94+
}
95+
96+
func testAddEmail_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 email = userA_email
105+
MockUserRequests.setAddEmailResponse(with: mocks.client, email: email)
106+
mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email))
107+
108+
/* When */
109+
mocks.subscriptionExecutor.processDeltaQueue(inBackground: false)
110+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
111+
112+
/* Then */
113+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self))
114+
}
115+
116+
func testAddEmail_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+
let email = userA_email
125+
MockUserRequests.setUnauthorizedAddEmailFailureResponse(with: mocks.client, email: email)
126+
mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email))
127+
128+
var invalidatedCallbackWasCalled = false
129+
OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in
130+
XCTAssertTrue(event.message == "token has invalid claims: token is expired")
131+
invalidatedCallbackWasCalled = true
132+
}
133+
134+
/* When */
135+
mocks.subscriptionExecutor.processDeltaQueue(inBackground: false)
136+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
137+
138+
/* Then */
139+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self))
140+
XCTAssertTrue(invalidatedCallbackWasCalled)
141+
}
142+
143+
func testDeleteEmail_IdentityVerificationRequired_withInvalidToken() {
144+
/* Setup */
145+
let mocks = Mocks()
146+
mocks.setAuthRequired(true)
147+
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
148+
149+
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
150+
user.identityModel.jwtBearerToken = userA_JwtToken
151+
let email = userA_email
152+
MockUserRequests.setUnauthorizedRemoveEmailFailureResponse(with: mocks.client, email: email)
153+
mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: testEmailSubId, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email))
154+
155+
var invalidatedCallbackWasCalled = false
156+
OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in
157+
XCTAssertTrue(event.message == "token has invalid claims: token is expired")
158+
invalidatedCallbackWasCalled = true
159+
}
160+
161+
/* When */
162+
mocks.subscriptionExecutor.processDeltaQueue(inBackground: false)
163+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
164+
165+
/* Then */
166+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestDeleteSubscription.self))
167+
XCTAssertTrue(invalidatedCallbackWasCalled)
168+
}
169+
170+
func testUpdateSubscription_IdentityVerificationRequired_withInvalidToken() {
171+
/* Setup */
172+
let mocks = Mocks()
173+
mocks.setAuthRequired(true)
174+
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
175+
176+
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
177+
user.identityModel.jwtBearerToken = userA_JwtToken
178+
let token = testPushToken
179+
MockUserRequests.setUnauthorizedUpdateSubscriptionFailureResponse(with: mocks.client, token: token)
180+
mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .push, address: token, subscriptionId: testPushSubId, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "token", value:token))
181+
182+
var invalidatedCallbackWasCalled = false
183+
OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in
184+
XCTAssertTrue(event.message == "token has invalid claims: token is expired")
185+
invalidatedCallbackWasCalled = true
186+
}
187+
188+
/* When */
189+
mocks.subscriptionExecutor.processDeltaQueue(inBackground: false)
190+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
191+
192+
/* Then */
193+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateSubscription.self))
194+
XCTAssertTrue(invalidatedCallbackWasCalled)
195+
}
196+
}

0 commit comments

Comments
 (0)