From f3cb3d8e3d2e05af7fbba6db9b3eb1180e209fbc Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Wed, 11 Sep 2024 13:43:38 -0700 Subject: [PATCH] Store UserExecutor requests that need auth This PR adds a pendingAuthRequests dictionary that stores the requests that are waiting for an updated JWT keyed on externalId. 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. Also update tests to account for the callback object change and add tests for the new case --- .../Source/Executors/OSUserExecutor.swift | 54 ++++++-- .../OneSignalUserMocks/MockUserDefines.swift | 5 +- .../Executors/IdentityExecutorTests.swift | 8 +- .../Executors/PropertyExecutorTests.swift | 5 +- .../SubscriptionsExecutorTests.swift | 11 +- .../Executors/UserExecutorTests.swift | 126 +++++++++++++++++- 6 files changed, 176 insertions(+), 33 deletions(-) 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..6407cdcd1 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,119 @@ 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(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertEqual(mocks.client.networkRequestCount, 4) + } + + func testUserRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() { + /* 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 + + let userBIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userB_OSID, OS_EXTERNAL_ID: userB_EUID], changeNotifier: OSEventProducer()) + userBIdentityModel.jwtBearerToken = userA_InvalidJwtToken + + MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) + MockUserRequests.setUnauthorizedCreateUserFailureResponses(with: mocks.client, externalId: userB_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: userB_EUID, identityModel: userBIdentityModel) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) + MockUserRequests.setDefaultCreateUserResponses(with: mocks.client, externalId: userB_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, 3) + } }