Skip to content

Commit 1ae0deb

Browse files
committed
store alias update requests that need auth
When a requests fails with a 401 due to JWT or fails when preparing for execution we remove the request from the request queue and add it to the pending dictionary. Once we get the onJWTUpdated callback for that externalId we requeue the pending requests and try again.
1 parent aca978e commit 1ae0deb

File tree

2 files changed

+171
-3
lines changed

2 files changed

+171
-3
lines changed

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

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
3434
// To simplify uncaching, we maintain separate request queues for each type
3535
var addRequestQueue: [OSRequestAddAliases] = []
3636
var removeRequestQueue: [OSRequestRemoveAlias] = []
37+
var pendingAuthRequests: [String: [OSUserRequest]] = [String:[OSUserRequest]]()
3738
let newRecordsState: OSNewRecordsState
3839
let jwtConfig: OSUserJwtConfig
3940

@@ -228,16 +229,38 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
228229
}
229230
}
230231

231-
func handleUnauthorizedError(externalId: String, error: NSError) {
232+
func handleUnauthorizedError(externalId: String, error: NSError, request: OSUserRequest) {
232233
if (jwtConfig.isRequired ?? false) {
234+
self.pendRequestUntilAuthUpdated(request, externalId: externalId)
233235
OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error)
234236
}
235237
}
238+
239+
func pendRequestUntilAuthUpdated(_ request: OSUserRequest, externalId: String?) {
240+
self.dispatchQueue.async {
241+
self.addRequestQueue.removeAll(where: { $0 == request})
242+
self.removeRequestQueue.removeAll(where: { $0 == request})
243+
guard let externalId = externalId else {
244+
return
245+
}
246+
var requests = self.pendingAuthRequests[externalId] ?? []
247+
let inQueue = requests.contains(where: {$0 == request})
248+
guard !inQueue else {
249+
return
250+
}
251+
requests.append(request)
252+
self.pendingAuthRequests[externalId] = requests
253+
}
254+
}
236255

237256
func executeAddAliasesRequest(_ request: OSRequestAddAliases, inBackground: Bool) {
238257
guard !request.sentToClient else {
239258
return
240259
}
260+
guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else {
261+
pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId)
262+
return
263+
}
241264
guard request.prepareForExecution(newRecordsState: newRecordsState) else {
242265
return
243266
}
@@ -281,7 +304,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
281304
OneSignalUserManagerImpl.sharedInstance._logout()
282305
} else if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) {
283306
if let externalId = request.identityModel.externalId {
284-
self.handleUnauthorizedError(externalId: externalId, error: nsError)
307+
self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request)
285308
}
286309
request.sentToClient = false
287310
} else if responseType != .retryable {
@@ -301,6 +324,10 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
301324
guard !request.sentToClient else {
302325
return
303326
}
327+
guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else {
328+
pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId)
329+
return
330+
}
304331
guard request.prepareForExecution(newRecordsState: newRecordsState) else {
305332
return
306333
}
@@ -330,7 +357,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
330357
let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code)
331358
if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) {
332359
if let externalId = request.identityModel.externalId {
333-
self.handleUnauthorizedError(externalId: externalId, error: nsError)
360+
self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request)
334361
}
335362
request.sentToClient = false
336363
}
@@ -360,6 +387,26 @@ extension OSIdentityOperationExecutor: OSUserJwtConfigListener {
360387

361388
func onJwtUpdated(externalId: String, token: String?) {
362389
print("❌ OSIdentityOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: token))")
390+
reQueuePendingRequestsForExternalId(externalId: externalId)
391+
}
392+
393+
private func reQueuePendingRequestsForExternalId(externalId: String) {
394+
self.dispatchQueue.async {
395+
guard let requests = self.pendingAuthRequests[externalId] else {
396+
return
397+
}
398+
for request in requests {
399+
if let addRequest = request as? OSRequestAddAliases {
400+
self.addRequestQueue.append(addRequest)
401+
} else if let removeRequest = request as? OSRequestRemoveAlias {
402+
self.removeRequestQueue.append(removeRequest)
403+
}
404+
}
405+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
406+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)
407+
self.pendingAuthRequests[externalId] = nil
408+
self.processRequestQueue(inBackground: false)
409+
}
363410
}
364411

365412
private func removeInvalidDeltasAndRequests() {

iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,5 +165,126 @@ final class IdentityExecutorTests: XCTestCase {
165165
XCTAssertTrue(invalidatedCallbackWasCalled)
166166
}
167167

168+
func testAddAliasRequests_Retry_OnTokenUpdate() {
169+
170+
/* Setup */
171+
let mocks = Mocks()
172+
mocks.setAuthRequired(true)
173+
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
174+
175+
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
176+
user.identityModel.jwtBearerToken = userA_InvalidJwtToken
177+
178+
// We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor
179+
let executor = OneSignalUserManagerImpl.sharedInstance.identityExecutor!
180+
181+
let aliases = userA_Aliases
182+
MockUserRequests.setUnauthorizedAddAliasFailureResponse(with: mocks.client, aliases: userA_Aliases)
183+
executor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases))
184+
185+
var invalidatedCallbackWasCalled = false
186+
OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in
187+
invalidatedCallbackWasCalled = true
188+
MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases)
189+
OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken)
190+
}
191+
192+
/* When */
193+
executor.processDeltaQueue(inBackground: false)
194+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
195+
196+
/* Then */
197+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self))
198+
XCTAssertTrue(invalidatedCallbackWasCalled)
199+
XCTAssertEqual(mocks.client.networkRequestCount, 2)
200+
}
201+
202+
func testAddAliasRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() {
203+
/* Setup */
204+
let mocks = Mocks()
205+
206+
mocks.setAuthRequired(true)
207+
208+
let userA = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
209+
userA.identityModel.jwtBearerToken = userA_InvalidJwtToken
210+
211+
let userB = mocks.setUserManagerInternalUser(externalId: userB_EUID, onesignalId: userB_OSID)
212+
userB.identityModel.jwtBearerToken = userA_InvalidJwtToken
213+
// We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor
214+
let executor = OneSignalUserManagerImpl.sharedInstance.identityExecutor!
215+
216+
let aliases = userA_Aliases
217+
MockUserRequests.setUnauthorizedAddAliasFailureResponse(with: mocks.client, aliases: userA_Aliases)
218+
219+
executor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: userA.identityModel.modelId, model: userA.identityModel, property: "aliases", value:aliases))
220+
executor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: userB.identityModel.modelId, model: userB.identityModel, property: "aliases", value:aliases))
221+
222+
var invalidatedCallbackWasCalled = false
223+
OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in
224+
invalidatedCallbackWasCalled = true
225+
}
226+
227+
/* When */
228+
executor.processDeltaQueue(inBackground: false)
229+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
230+
231+
MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases)
232+
OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userB_EUID, token: userB_ValidJwtToken)
233+
234+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
235+
236+
/* Then */
237+
// The executor should execute this request since identity verification is required and the token was set
238+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self))
239+
XCTAssertTrue(invalidatedCallbackWasCalled)
240+
let addAliasRequests = mocks.client.executedRequests.filter { request in
241+
request.isKind(of: OSRequestAddAliases.self)
242+
}
243+
// It is 4 because setting user B's OneSignal ID counts as an add alias request
244+
XCTAssertEqual(addAliasRequests.count, 4)
245+
}
168246

247+
func testRemoveAliasRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() {
248+
/* Setup */
249+
let mocks = Mocks()
250+
251+
mocks.setAuthRequired(true)
252+
253+
let userA = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
254+
userA.identityModel.jwtBearerToken = userA_InvalidJwtToken
255+
256+
let userB = mocks.setUserManagerInternalUser(externalId: userB_EUID, onesignalId: userB_OSID)
257+
userB.identityModel.jwtBearerToken = userA_InvalidJwtToken
258+
// We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor
259+
let executor = OneSignalUserManagerImpl.sharedInstance.identityExecutor!
260+
261+
let aliases = userA_Aliases
262+
MockUserRequests.setUnauthorizedRemoveAliasFailureResponse(with: mocks.client, aliasLabel: userA_AliasLabel)
263+
264+
executor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: userA.identityModel.modelId, model: userA.identityModel, property: "aliases", value:aliases))
265+
executor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: userB.identityModel.modelId, model: userB.identityModel, property: "aliases", value:aliases))
266+
267+
var invalidatedCallbackWasCalled = false
268+
OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in
269+
invalidatedCallbackWasCalled = true
270+
}
271+
272+
/* When */
273+
executor.processDeltaQueue(inBackground: false)
274+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
275+
276+
OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userB_EUID, token: userB_ValidJwtToken)
277+
278+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
279+
280+
/* Then */
281+
// The executor should execute this request since identity verification is required and the token was set
282+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestRemoveAlias.self))
283+
XCTAssertTrue(invalidatedCallbackWasCalled)
284+
let removeAliasRequests = mocks.client.executedRequests.filter { request in
285+
request.isKind(of: OSRequestRemoveAlias.self)
286+
}
287+
288+
XCTAssertEqual(removeAliasRequests.count, 3)
289+
}
169290
}

0 commit comments

Comments
 (0)