diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 9eaccd6f1..ef6dfec1a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -35,6 +35,7 @@ import OneSignalOSCore */ class OSUserExecutor { var userRequestQueue: [OSUserRequest] = [] + var pendingAuthRequests: [String: [OSUserRequest]] = [String:[OSUserRequest]]() private let newRecordsState: OSNewRecordsState let jwtConfig: OSUserJwtConfig @@ -289,6 +290,22 @@ extension OSUserExecutor { appendToQueue(request) executePendingRequests() } + + func pendRequestUntilAuthUpdated(_ request: OSUserRequest, externalId: String?) { + self.dispatchQueue.async { + self.userRequestQueue.removeAll(where: { $0 == request}) + guard let externalId = externalId else { + return + } + var requests = self.pendingAuthRequests[externalId] ?? [] + let inQueue = requests.contains(where: {$0 == request}) + guard !inQueue else { + return + } + requests.append(request) + self.pendingAuthRequests[externalId] = requests + } + } func executeCreateUserRequest(_ request: OSRequestCreateUser) { guard !request.sentToClient else { @@ -301,6 +318,11 @@ extension OSUserExecutor { request.pushSubscriptionModel = pushSubscriptionModel request.updatePushSubscriptionModel(pushSubscriptionModel) } + + guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { + pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId) + return + } guard request.prepareForExecution(newRecordsState: newRecordsState) else { @@ -344,7 +366,7 @@ extension OSUserExecutor { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor no externalId for unauthorized request.") return } - self.handleUnauthorizedError(externalId: externalId, error: nsError) + self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request) request.sentToClient = false } else if responseType != .retryable { // A failed create user request would leave the SDK in a bad state @@ -361,8 +383,9 @@ extension OSUserExecutor { } } - func handleUnauthorizedError(externalId: String, error: NSError) { + func handleUnauthorizedError(externalId: String, error: NSError, request: OSUserRequest) { if (jwtConfig.isRequired ?? false) { + self.pendRequestUntilAuthUpdated(request, externalId: externalId) OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error) } } @@ -376,6 +399,7 @@ extension OSUserExecutor { /** For migrating legacy players from 3.x to 5.x. This request will fetch the identity object for a subscription ID, and we will use the returned onesignalId to fetch and hydrate the local user. + ECM can this ever succeed with identity verification on? */ func executeFetchIdentityBySubscriptionRequest(_ request: OSRequestFetchIdentityBySubscription) { guard !request.sentToClient else { @@ -485,7 +509,7 @@ extension OSUserExecutor { // This will hydrate the OneSignal ID for any pending requests self.createUser(aliasLabel: request.aliasLabel, aliasId: request.aliasId, identityModel: request.identityModelToUpdate) } - } else if responseType == .invalid || responseType == .unauthorized { + } else if responseType == .invalid || responseType == .unauthorized { //Identify User should never be called with identity verification on // Failed, no retry self.removeFromQueue(request) self.executePendingRequests() @@ -574,7 +598,7 @@ extension OSUserExecutor { OneSignalUserManagerImpl.sharedInstance._logout() } else if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { if let externalId = request.identityModel.externalId { - self.handleUnauthorizedError(externalId: externalId, error: nsError) + self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request) } request.sentToClient = false } else if responseType != .retryable { @@ -707,15 +731,23 @@ extension OSUserExecutor: OSUserJwtConfigListener { } func onJwtUpdated(externalId: String, token: String?) { - /* - ECM - Do we actually even need this callback? - Requests that are invalidated do not pass prepare for execution - Once they are valid they will pass prepare for execution. - We could use this callback to optimize sending requests immediately - */ + reQueuePendingRequestsForExternalId(externalId: externalId) print("❌ OSUserExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") } + + private func reQueuePendingRequestsForExternalId(externalId: String) { + self.dispatchQueue.async { + guard let requests = self.pendingAuthRequests[externalId] else { + return + } + for request in requests { + self.userRequestQueue.append(request) + } + self.pendingAuthRequests[externalId] = nil + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, withValue: self.userRequestQueue) + self.executePendingRequests(withDelay: true) + } + } private func removeInvalidRequests() { self.dispatchQueue.async { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift index ae8225a40..1f4cc6249 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift @@ -7,7 +7,10 @@ public let userB_EUID = "test_user_b_external_id" public let testPushSubId = "test_push_subscription_id" public let testEmailSubId = "test_email_subscription_id" public let testPushToken = "2b7347630b72265c83b1c1d2227f563ce6169d5aaf274b06f1a1fadf3a04be69" -public let userA_JwtToken = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w" +public let userA_InvalidJwtToken = "byJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w" + +public let userA_ValidJwtToken = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w" +public let userB_ValidJwtToken = "fyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w" public let userA_email = "userA@onesignal.com" diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift index bab751323..e50af82b4 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift @@ -100,7 +100,7 @@ final class IdentityExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken let aliases = userA_Aliases MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases) mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) @@ -120,14 +120,13 @@ final class IdentityExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken let aliases = userA_Aliases MockUserRequests.setUnauthorizedAddAliasFailureResponse(with: mocks.client, aliases: aliases) mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) var invalidatedCallbackWasCalled = false OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in - XCTAssertTrue(event.message == "token has invalid claims: token is expired") invalidatedCallbackWasCalled = true } @@ -147,14 +146,13 @@ final class IdentityExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken let aliases = userA_Aliases MockUserRequests.setUnauthorizedRemoveAliasFailureResponse(with: mocks.client, aliasLabel: userA_AliasLabel) mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) var invalidatedCallbackWasCalled = false OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in - XCTAssertTrue(event.message == "token has invalid claims: token is expired") invalidatedCallbackWasCalled = true } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift index 6820dee43..f41b49d29 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift @@ -100,7 +100,7 @@ final class PropertyExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken let tags = ["testUserA" : "true"] MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags) mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags)) @@ -120,7 +120,7 @@ final class PropertyExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken @@ -130,7 +130,6 @@ final class PropertyExecutorTests: XCTestCase { var invalidatedCallbackWasCalled = false OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in - XCTAssertTrue(event.message == "token has invalid claims: token is expired") invalidatedCallbackWasCalled = true } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift index 0a81c2d8f..32c753314 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift @@ -100,7 +100,7 @@ final class SubscriptionExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken let email = userA_email MockUserRequests.setAddEmailResponse(with: mocks.client, email: email) 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)) @@ -120,14 +120,13 @@ final class SubscriptionExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken let email = userA_email MockUserRequests.setUnauthorizedAddEmailFailureResponse(with: mocks.client, email: email) 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)) var invalidatedCallbackWasCalled = false OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in - XCTAssertTrue(event.message == "token has invalid claims: token is expired") invalidatedCallbackWasCalled = true } @@ -147,14 +146,13 @@ final class SubscriptionExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken let email = userA_email MockUserRequests.setUnauthorizedRemoveEmailFailureResponse(with: mocks.client, email: email) 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)) var invalidatedCallbackWasCalled = false OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in - XCTAssertTrue(event.message == "token has invalid claims: token is expired") invalidatedCallbackWasCalled = true } @@ -174,14 +172,13 @@ final class SubscriptionExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken let token = testPushToken MockUserRequests.setUnauthorizedUpdateSubscriptionFailureResponse(with: mocks.client, token: token) 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)) var invalidatedCallbackWasCalled = false OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in - XCTAssertTrue(event.message == "token has invalid claims: token is expired") invalidatedCallbackWasCalled = true } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift index f76b55d20..879432453 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift @@ -42,6 +42,7 @@ private class Mocks: OneSignalExecutorMocks { } } + final class UserExecutorTests: XCTestCase { override func setUpWithError() throws { @@ -210,7 +211,7 @@ final class UserExecutorTests: XCTestCase { let _ = mocks.setUserManagerInternalUser(externalId: "") let newIdentityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) - newIdentityModel.jwtBearerToken = userA_JwtToken + newIdentityModel.jwtBearerToken = userA_InvalidJwtToken MockUserRequests.setDefaultCreateUserResponses(with: mocks.client, externalId: userA_EUID) /* When */ @@ -229,12 +230,11 @@ final class UserExecutorTests: XCTestCase { let _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) let newIdentityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) - newIdentityModel.jwtBearerToken = userA_JwtToken + newIdentityModel.jwtBearerToken = userA_InvalidJwtToken MockUserRequests.setUnauthorizedCreateUserFailureResponses(with: mocks.client, externalId: userA_EUID) var invalidatedCallbackWasCalled = false OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in - XCTAssertTrue(event.message == "token has invalid claims: token is expired") invalidatedCallbackWasCalled = true } @@ -273,7 +273,7 @@ final class UserExecutorTests: XCTestCase { let _ = mocks.setUserManagerInternalUser(externalId: "") let newIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) - newIdentityModel.jwtBearerToken = userA_JwtToken + newIdentityModel.jwtBearerToken = userA_InvalidJwtToken MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) /* When */ @@ -292,12 +292,11 @@ final class UserExecutorTests: XCTestCase { let _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) let newIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) - newIdentityModel.jwtBearerToken = userA_JwtToken + newIdentityModel.jwtBearerToken = userA_InvalidJwtToken MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) var invalidatedCallbackWasCalled = false OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in - XCTAssertTrue(event.message == "token has invalid claims: token is expired") invalidatedCallbackWasCalled = true } @@ -310,4 +309,76 @@ final class UserExecutorTests: XCTestCase { XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) XCTAssertTrue(invalidatedCallbackWasCalled) } + + func testUserRequests_Retry_OnTokenUpdate() { + /* Setup */ + let mocks = Mocks() + + mocks.setAuthRequired(true) + + let _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) + // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor + let executor = OneSignalUserManagerImpl.sharedInstance.userExecutor! + + let userAIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) + userAIdentityModel.jwtBearerToken = userA_InvalidJwtToken + + MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + invalidatedCallbackWasCalled = true + MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) + OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) + } + + /* When */ + executor.fetchUser(onesignalId: userA_OSID, identityModel: userAIdentityModel) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // The executor should execute this request since identity verification is required and the token was set + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertEqual(mocks.client.networkRequestCount, 2) + } + + func testUserRequests_RetryAllRequests_OnTokenUpdate() { + /* Setup */ + let mocks = Mocks() + + mocks.setAuthRequired(true) + + let _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) + // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor + let executor = OneSignalUserManagerImpl.sharedInstance.userExecutor! + + let userAIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) + userAIdentityModel.jwtBearerToken = userA_InvalidJwtToken + + MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) + MockUserRequests.setUnauthorizedCreateUserFailureResponses(with: mocks.client, externalId: userA_EUID) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + invalidatedCallbackWasCalled = true + } + + /* When */ + executor.fetchUser(onesignalId: userA_OSID, identityModel: userAIdentityModel) + executor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userA_EUID, identityModel: userAIdentityModel) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) + MockUserRequests.setDefaultCreateUserResponses(with: mocks.client, externalId: userA_EUID) + + OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // The executor should execute this request since identity verification is required and the token was set + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertEqual(mocks.client.networkRequestCount, 4) + } }