Skip to content

Commit d5514e4

Browse files
committed
fire Jwt callback for add and remove aliases 401
Includes tests
1 parent c0156d7 commit d5514e4

File tree

5 files changed

+207
-1
lines changed

5 files changed

+207
-1
lines changed

iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@
357357
DE3568EA2C88F56600AF447C /* PropertyExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */; };
358358
DE3568EC2C88F5BD00AF447C /* OneSignalExecutorMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568EB2C88F5BD00AF447C /* OneSignalExecutorMocks.swift */; };
359359
DE3568F02C89067400AF447C /* SubscriptionsExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568EF2C89067400AF447C /* SubscriptionsExecutorTests.swift */; };
360+
DE3568F22C8911EA00AF447C /* IdentityExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568F12C8911EA00AF447C /* IdentityExecutorTests.swift */; };
360361
DE3784842888CFF900453A8E /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; };
361362
DE3784852888D00300453A8E /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; };
362363
DE3784862888D00B00453A8E /* OneSignalUser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -1520,6 +1521,7 @@
15201521
DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyExecutorTests.swift; sourceTree = "<group>"; };
15211522
DE3568EB2C88F5BD00AF447C /* OneSignalExecutorMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalExecutorMocks.swift; sourceTree = "<group>"; };
15221523
DE3568EF2C89067400AF447C /* SubscriptionsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsExecutorTests.swift; sourceTree = "<group>"; };
1524+
DE3568F12C8911EA00AF447C /* IdentityExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityExecutorTests.swift; sourceTree = "<group>"; };
15231525
DE3CD2FE270FA9F200A5BECD /* OSOutcomes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSOutcomes.m; sourceTree = "<group>"; };
15241526
DE51DDE3294262AB0073D5C4 /* OSRemoteParamController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSRemoteParamController.m; sourceTree = "<group>"; };
15251527
DE51DDE4294262AB0073D5C4 /* OSRemoteParamController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSRemoteParamController.h; sourceTree = "<group>"; };
@@ -2210,6 +2212,7 @@
22102212
3CF11E3C2C6D6155002856F5 /* UserExecutorTests.swift */,
22112213
DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */,
22122214
DE3568EF2C89067400AF447C /* SubscriptionsExecutorTests.swift */,
2215+
DE3568F12C8911EA00AF447C /* IdentityExecutorTests.swift */,
22132216
);
22142217
path = Executors;
22152218
sourceTree = "<group>";
@@ -4149,6 +4152,7 @@
41494152
files = (
41504153
3CF11E3D2C6D6155002856F5 /* UserExecutorTests.swift in Sources */,
41514154
DE3568EA2C88F56600AF447C /* PropertyExecutorTests.swift in Sources */,
4155+
DE3568F22C8911EA00AF447C /* IdentityExecutorTests.swift in Sources */,
41524156
3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */,
41534157
3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */,
41544158
3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */,

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,12 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
227227
}
228228
}
229229
}
230+
231+
func handleUnauthorizedError(externalId: String, error: NSError) {
232+
if (jwtConfig.isRequired ?? false) {
233+
OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error)
234+
}
235+
}
230236

231237
func executeAddAliasesRequest(_ request: OSRequestAddAliases, inBackground: Bool) {
232238
guard !request.sentToClient else {
@@ -273,6 +279,11 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
273279
// The subscription has been deleted along with the user, so remove the subscription_id but keep the same push subscription model
274280
OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId = nil
275281
OneSignalUserManagerImpl.sharedInstance._logout()
282+
} else if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) {
283+
if let externalId = request.identityModel.externalId {
284+
self.handleUnauthorizedError(externalId: externalId, error: nsError)
285+
}
286+
request.sentToClient = false
276287
} else if responseType != .retryable {
277288
// Fail, no retry, remove from cache and queue
278289
self.addRequestQueue.removeAll(where: { $0 == request})
@@ -317,7 +328,13 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
317328
self.dispatchQueue.async {
318329
if let nsError = error as? NSError {
319330
let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code)
320-
if responseType != .retryable {
331+
if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) {
332+
if let externalId = request.identityModel.externalId {
333+
self.handleUnauthorizedError(externalId: externalId, error: nsError)
334+
}
335+
request.sentToClient = false
336+
}
337+
else if responseType != .retryable {
321338
// Fail, no retry, remove from cache and queue
322339
// A response of .missing could mean the alias doesn't exist on this user OR this user has been deleted
323340
self.removeRequestQueue.removeAll(where: { $0 == request})

iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ public let testPushToken = "2b7347630b72265c83b1c1d2227f563ce6169d5aaf274b06f1a1
1010
public let userA_JwtToken = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w"
1111

1212
public let userA_email = "[email protected]"
13+
14+
public let userA_AliasLabel = "testAliasLabel"
15+
public let userA_Aliases = [userA_AliasLabel : "userAValue"]
16+

iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,16 @@ extension MockUserRequests {
143143
let error = testUnauthorizedailureError()
144144
client.setMockFailureResponseForRequest(request:"OSRequestUpdateSubscription with subscriptionObject: [\"token\": \"\(token)\"]", error: error)
145145
}
146+
147+
public static func setUnauthorizedAddAliasFailureResponse(with client: MockOneSignalClient, aliases: [String: String]) {
148+
let error = testUnauthorizedailureError()
149+
client.setMockFailureResponseForRequest(request:"<OSRequestAddAliases with aliases: \(aliases)>", error: error)
150+
}
151+
152+
public static func setUnauthorizedRemoveAliasFailureResponse(with client: MockOneSignalClient, aliasLabel: String) {
153+
let error = testUnauthorizedailureError()
154+
client.setMockFailureResponseForRequest(request:"OSRequestRemoveAlias with aliasLabel: \(aliasLabel)", error: error)
155+
}
146156

147157
public static func setDefaultIdentifyUserResponses(with client: MockOneSignalClient, externalId: String, conflicted: Bool = false) {
148158
var osid: String
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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 identityExecutor: OSIdentityOperationExecutor!
38+
39+
override init() {
40+
super.init()
41+
identityExecutor = OSIdentityOperationExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig)
42+
}
43+
}
44+
45+
final class IdentityExecutorTests: 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 testAddAliasSendsWhenProcessed() {
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 aliases = userA_Aliases
66+
MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases)
67+
mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases))
68+
69+
/* When */
70+
mocks.identityExecutor.processDeltaQueue(inBackground: false)
71+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
72+
73+
/* Then */
74+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self))
75+
}
76+
77+
func testAddAlias_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 aliases = userA_Aliases
85+
MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases)
86+
mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases))
87+
88+
/* When */
89+
mocks.identityExecutor.processDeltaQueue(inBackground: false)
90+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
91+
92+
/* Then */
93+
XCTAssertFalse(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self))
94+
}
95+
96+
func testAddAlias_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 aliases = userA_Aliases
105+
MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases)
106+
mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases))
107+
108+
/* When */
109+
mocks.identityExecutor.processDeltaQueue(inBackground: false)
110+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
111+
112+
/* Then */
113+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self))
114+
}
115+
116+
func testAddAlias_IdentityVerificationRequired_withInvalidToken_firesCallback() {
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 aliases = userA_Aliases
125+
MockUserRequests.setUnauthorizedAddAliasFailureResponse(with: mocks.client, aliases: aliases)
126+
mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases))
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.identityExecutor.processDeltaQueue(inBackground: false)
136+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
137+
138+
/* Then */
139+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self))
140+
XCTAssertTrue(invalidatedCallbackWasCalled)
141+
}
142+
143+
func testRemoveAlias_IdentityVerificationRequired_withInvalidToken_firesCallback() {
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 aliases = userA_Aliases
152+
MockUserRequests.setUnauthorizedRemoveAliasFailureResponse(with: mocks.client, aliasLabel: userA_AliasLabel)
153+
mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases))
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.identityExecutor.processDeltaQueue(inBackground: false)
163+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
164+
165+
/* Then */
166+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestRemoveAlias.self))
167+
XCTAssertTrue(invalidatedCallbackWasCalled)
168+
}
169+
170+
171+
}

0 commit comments

Comments
 (0)