Skip to content

Commit

Permalink
Subscription executor finish up and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
nan-li committed Sep 24, 2024
1 parent afe7515 commit b93b255
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP
#define OS_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY @"OS_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY"

// Subscription Executor
#define OS_SUBSCRIPTION_EXECUTOR @"OS_SUBSCRIPTION_EXECUTOR"
#define OS_SUBSCRIPTION_EXECUTOR_DELTA_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_DELTA_QUEUE_KEY"
#define OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY"
#define OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
init(newRecordsState: OSNewRecordsState, jwtConfig: OSUserJwtConfig) {
self.newRecordsState = newRecordsState
self.jwtConfig = jwtConfig
// Read unfinished deltas and requests from cache, if any...
self.jwtConfig.subscribe(self, key: OS_SUBSCRIPTION_EXECUTOR)
uncacheDeltas()
uncacheRequests()
}
Expand Down Expand Up @@ -253,8 +253,20 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
self.addRequestQueue.append(request)

case OS_REMOVE_SUBSCRIPTION_DELTA:
// Only create the request if the identity model exists
guard let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) else {
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSSubscriptionOperationExecutor.processDeltaQueue dropped \(delta)")
continue
}

// If JWT is on but the external ID does not exist, drop this Delta
if self.jwtConfig.isRequired == true, identityModel.externalId == nil {
print("\(delta) is Invalid with JWT, being dropped")
}

let request = OSRequestDeleteSubscription(
subscriptionModel: subModel
subscriptionModel: subModel,
identityModel: identityModel
)
self.removeRequestQueue.append(request)

Expand Down Expand Up @@ -415,7 +427,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
guard !request.sentToClient else {
return
}
// ECM TODO
// ECM TODO - Delete Subscription not supported on JWT yet (9-23-2024)
// guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else {
// pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId)
// return
Expand Down Expand Up @@ -447,8 +459,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
if let nsError = error as? NSError {
let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code)
if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) {
// ECM The delete subscription request doesn't have an identity model?
if let externalId = OneSignalUserManagerImpl.sharedInstance.user.identityModel.externalId {
if let externalId = request.identityModel.externalId {
self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request)
}
request.sentToClient = false
Expand All @@ -470,6 +481,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
return
}
// ECM TODO
// ^ Updating Subscription is only for push, and user JWT is not used
// guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else {
// pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId)
// return
Expand Down Expand Up @@ -499,6 +511,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code)
if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) {
// ECM The update subscription request doesn't have an identity model?
// ^ not needed for this request, this error handling will need a revisit since it means push token is wrong (?)
if let externalId = OneSignalUserManagerImpl.sharedInstance.user.identityModel.externalId {
self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request)
}
Expand All @@ -519,7 +532,9 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor {
extension OSSubscriptionOperationExecutor: OSUserJwtConfigListener {
func onRequiresUserAuthChanged(from: OneSignalOSCore.OSRequiresUserAuth, to: OneSignalOSCore.OSRequiresUserAuth) {
print("❌ OSSubscriptionOperationExecutor onUserAuthChanged from \(String(describing: from)) to \(String(describing: to))")
// ECM TODO If auth changed from false or unknown to true, process requests
if to == .on {
removeInvalidDeltasAndRequests()
}
}

func onJwtUpdated(externalId: String, token: String?) {
Expand All @@ -537,18 +552,52 @@ extension OSSubscriptionOperationExecutor: OSUserJwtConfigListener {
self.addRequestQueue.append(addRequest)
} else if let removeRequest = request as? OSRequestDeleteSubscription {
self.removeRequestQueue.append(removeRequest)
} else if let updateRequest = request as? OSRequestUpdateSubscription {
self.updateRequestQueue.append(updateRequest)
}
}
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue)
self.pendingAuthRequests[externalId] = nil
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests)
self.processRequestQueue(inBackground: false)
}
}

/**
Drops deltas and requests that add and remove subscriptions on unidentified users.
Subscription updates are used only for push subscriptions, which are kept as they do not use User JWT.
*/
private func removeInvalidDeltasAndRequests() {
self.dispatchQueue.async {
print("❌ OSSubscriptionOperationExecutor.removeInvalidDeltasAndRequests called")

for (index, delta) in self.deltaQueue.enumerated().reversed() {
if delta.name != OS_UPDATE_SUBSCRIPTION_DELTA,
let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId),
identityModel.externalId == nil
{
print(" \(delta) is Invalid, being removed")
self.deltaQueue.remove(at: index)
}
}
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue)

for (index, request) in self.addRequestQueue.enumerated().reversed() {
if request.identityModel.externalId == nil {
print(" \(request) is Invalid, being removed")
self.addRequestQueue.remove(at: index)
}
}
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue)

for (index, request) in self.removeRequestQueue.enumerated().reversed() {
if request.identityModel.externalId == nil {
print(" \(request) is Invalid, being removed")
self.removeRequestQueue.remove(at: index)
}
}
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue)
}
}
}

extension OSSubscriptionOperationExecutor: OSLoggable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,43 +41,50 @@ class OSRequestDeleteSubscription: OneSignalRequest, OSUserRequest {
}

var subscriptionModel: OSSubscriptionModel
var identityModel: OSIdentityModel

// Need the subscription_id
func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool {
if let subscriptionId = subscriptionModel.subscriptionId,
newRecordsState.canAccess(subscriptionId),
let appId = OneSignalConfigManager.getAppId()
{
self.path = "apps/\(appId)/subscriptions/\(subscriptionId)"
return true
} else {
guard
let subscriptionId = subscriptionModel.subscriptionId,
let token = subscriptionModel.address,
newRecordsState.canAccess(subscriptionId),
let appId = OneSignalConfigManager.getAppId(),
let _ = checkUserRequirementsAndReturnAlias(identityModel, newRecordsState)
else {
return false
}

self.path = "apps/\(appId)/subscriptions/by/type/\(subscriptionModel.type)/token/\(token)"
return true
}

init(subscriptionModel: OSSubscriptionModel) {
init(subscriptionModel: OSSubscriptionModel, identityModel: OSIdentityModel) {
self.subscriptionModel = subscriptionModel
self.identityModel = identityModel
self.stringDescription = "<OSRequestDeleteSubscription with subscriptionModel: \(subscriptionModel.address ?? "nil")>"
super.init()
self.method = DELETE
}

func encode(with coder: NSCoder) {
coder.encode(subscriptionModel, forKey: "subscriptionModel")
coder.encode(identityModel, forKey: "identityModel")
coder.encode(method.rawValue, forKey: "method") // Encodes as String
coder.encode(timestamp, forKey: "timestamp")
}

required init?(coder: NSCoder) {
guard
let subscriptionModel = coder.decodeObject(forKey: "subscriptionModel") as? OSSubscriptionModel,
let identityModel = coder.decodeObject(forKey: "identityModel") as? OSIdentityModel,
let rawMethod = coder.decodeObject(forKey: "method") as? UInt32,
let timestamp = coder.decodeObject(forKey: "timestamp") as? Date
else {
// Log error
return nil
}
self.subscriptionModel = subscriptionModel
self.identityModel = identityModel
self.stringDescription = "<OSRequestDeleteSubscription with subscriptionModel: \(subscriptionModel.address ?? "nil")>"
super.init()
self.method = HTTPMethod(rawValue: rawMethod)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@ class OSUserUtils {
if let pushSubscriptionId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId {
headers["OneSignal-Subscription-Id"] = pushSubscriptionId
}
if let token = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.address {
headers["Device-Auth-Push-Token"] = "Basic \(token)"
if let token = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.address,
let data = token.data(using: .utf8)
{
let base64String = data.base64EncodedString()
headers["Device-Auth-Push-Token"] = "Basic \(base64String)"
}
return headers
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,125 @@ final class SubscriptionExecutorTests: XCTestCase {
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateSubscription.self))
XCTAssertTrue(invalidatedCallbackWasCalled)
}

func testCreateSubscriptionRequests_Retry_OnTokenUpdate() {
/* Setup */
let mocks = Mocks()
mocks.setAuthRequired(true)
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true

let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
user.identityModel.jwtBearerToken = userA_InvalidJwtToken

// We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor
let executor = OneSignalUserManagerImpl.sharedInstance.subscriptionExecutor!

let email = userA_email
MockUserRequests.setUnauthorizedAddEmailFailureResponse(with: mocks.client, email: email)
executor.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 { _ in
invalidatedCallbackWasCalled = true
MockUserRequests.setAddEmailResponse(with: mocks.client, email: email)
OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken)
}

/* When */
executor.processDeltaQueue(inBackground: false)
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)

/* Then */
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self))
XCTAssertTrue(invalidatedCallbackWasCalled)
XCTAssertEqual(mocks.client.networkRequestCount, 2)
}

func testCreateSubscriptionRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() {
/* Setup */
let mocks = Mocks()
mocks.setAuthRequired(true)

let userA = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
userA.identityModel.jwtBearerToken = userA_InvalidJwtToken

let userB = mocks.setUserManagerInternalUser(externalId: userB_EUID, onesignalId: userB_OSID)
userB.identityModel.jwtBearerToken = userA_InvalidJwtToken

// We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor
let executor = OneSignalUserManagerImpl.sharedInstance.subscriptionExecutor!

let email = userA_email
MockUserRequests.setUnauthorizedAddEmailFailureResponse(with: mocks.client, email: email)

executor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: userA.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email))
executor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: userB.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 { _ in
invalidatedCallbackWasCalled = true
}

/* When */
executor.processDeltaQueue(inBackground: false)
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)

OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userB_EUID, token: userB_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(OSRequestCreateSubscription.self))
XCTAssertTrue(invalidatedCallbackWasCalled)
let addRequests = mocks.client.executedRequests.filter { request in
request.isKind(of: OSRequestCreateSubscription.self)
}

XCTAssertEqual(addRequests.count, 3)
}

func testDeleteSubscriptionRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() {
/* Setup */
let mocks = Mocks()
mocks.setAuthRequired(true)

let userA = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
userA.identityModel.jwtBearerToken = userA_InvalidJwtToken

let userB = mocks.setUserManagerInternalUser(externalId: userB_EUID, onesignalId: userB_OSID)
userB.identityModel.jwtBearerToken = userA_InvalidJwtToken

// We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor
let executor = OneSignalUserManagerImpl.sharedInstance.subscriptionExecutor!

let email = userA_email
MockUserRequests.setUnauthorizedRemoveEmailFailureResponse(with: mocks.client, email: email)

executor.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: userA.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email))
executor.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: userB.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email))

var invalidatedCallbackWasCalled = false
OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in
invalidatedCallbackWasCalled = true
}

/* When */
executor.processDeltaQueue(inBackground: false)
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)

OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userB_EUID, token: userB_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(OSRequestDeleteSubscription.self))
XCTAssertTrue(invalidatedCallbackWasCalled)
let deleteRequests = mocks.client.executedRequests.filter { request in
request.isKind(of: OSRequestDeleteSubscription.self)
}

XCTAssertEqual(deleteRequests.count, 3)
}
}
Loading

0 comments on commit b93b255

Please sign in to comment.