Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -455,17 +455,12 @@ extension OSUserExecutor {
OneSignalUserManagerImpl.sharedInstance.clearUserData(user)
self.parseFetchUserResponse(response: response, identityModel: request.identityModel, originalPushToken: OneSignalUserManagerImpl.sharedInstance.pushSubscriptionImpl.token)

// If this is a on-new-session's fetch user call, check that the subscription still exists
// If this is a on-new-session's fetch user call, check that the subscription still exists.
// A user with no subscriptions has no "subscriptions" key at all, so an absent key means none.
if request.onNewSession,
let subId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId,
let subscriptionObjects = self.parseSubscriptionObjectResponse(response) {
var subscriptionExists = false
for subModel in subscriptionObjects {
if subModel["id"] as? String == subId {
subscriptionExists = true
break
}
}
let subId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId {
let subscriptionObjects = self.parseSubscriptionObjectResponse(response) ?? []
let subscriptionExists = subscriptionObjects.contains { $0["id"] as? String == subId }

if !subscriptionExists {
// This subscription probably has been deleted
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,14 @@ public class OneSignalUserMocks: NSObject {
OneSignalUserManagerImpl.sharedInstance.reset()
}

public static func setUserManagerInternalUser(externalId: String = "test-external-id", onesignalId: String?) -> OSUserInternal {
public static func setUserManagerInternalUser(
externalId: String = "test-external-id",
onesignalId: String?,
pushToken: String = ""
) -> OSUserInternal {
let user = OneSignalUserManagerImpl.sharedInstance.setNewInternalUser(
externalId: externalId,
pushSubscriptionModel: OSSubscriptionModel(type: .push, address: "", subscriptionId: testPushSubId, reachable: false, isDisabled: false, changeNotifier: OSEventProducer())
pushSubscriptionModel: OSSubscriptionModel(type: .push, address: pushToken, subscriptionId: testPushSubId, reachable: false, isDisabled: false, changeNotifier: OSEventProducer())
)
if let onesignalId = onesignalId {
user.identityModel.addAliases([OS_ONESIGNAL_ID: onesignalId])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,35 @@ private class Mocks {
let pushModel = OSSubscriptionModel(type: .push, address: "", subscriptionId: nil, reachable: false, isDisabled: false, changeNotifier: OSEventProducer())
return OSUserInternalImpl(identityModel: identityModel, propertiesModel: propertiesModel, pushSubscriptionModel: pushModel)
}

/// The on-new-session self-heal sends its Create through the user manager's subscription executor.
func installSubscriptionExecutor() -> OSSubscriptionOperationExecutor {
let executor = OSSubscriptionOperationExecutor(newRecordsState: newRecordsState)
OneSignalUserManagerImpl.sharedInstance.subscriptionExecutor = executor
return executor
}
}

private let recreatedPushSubId = "recreated-push-sub-id"

final class UserExecutorTests: XCTestCase {

/// Whatever executor the shared manager had before a test installed its own, restored in tearDown.
private var previousSubscriptionExecutor: OSSubscriptionOperationExecutor?

override func setUpWithError() throws {
OneSignalCoreMocks.clearUserDefaults()
OneSignalUserMocks.reset()
// App ID is set because requests have guards against null App ID
OneSignalIdentifiers.currentAppId = "test-app-id"
// Temp. logging to help debug during testing
OneSignalLog.setLogLevel(.LL_VERBOSE)
previousSubscriptionExecutor = OneSignalUserManagerImpl.sharedInstance.subscriptionExecutor
}

override func tearDownWithError() throws { }
override func tearDownWithError() throws {
OneSignalUserManagerImpl.sharedInstance.subscriptionExecutor = previousSubscriptionExecutor
}

func testCreateUser_withPushSubscription_addsToNewRecords() {
/* Setup */
Expand Down Expand Up @@ -263,4 +278,94 @@ final class UserExecutorTests: XCTestCase {
XCTAssertNil(currentUser.identityModel.aliases["stale_label"])
XCTAssertEqual(currentUser.identityModel.externalId, userA_EUID)
}

// MARK: - On-new-session push subscription self-heal

/// Installs a user whose push subscription already has a server id, as after an earlier session.
private func setUpUserWithPushSubscription() -> OSUserInternal {
return OneSignalUserMocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID, pushToken: "push-token")
}

private func stubRecreatedPushSubscription(_ mocks: Mocks) {
mocks.client.setMockResponseForRequest(
request: "<OSRequestCreateSubscription with token: push-token>",
response: ["subscription": MockUserRequests.testDefaultPushSubPayload(id: recreatedPushSubId)]
)
}

private func fetchUserOnNewSession(_ mocks: Mocks, user: OSUserInternal, response: [String: Any]) {
mocks.client.setMockResponseForRequest(
request: "<OSRequestFetchUser with onesignal_id: \(userA_OSID)>",
response: response
)
mocks.userExecutor.fetchUser(aliasLabel: OS_ONESIGNAL_ID, aliasId: userA_OSID, identityModel: user.identityModel, onNewSession: true)
OneSignalCoreMocks.waitUntil("Fetch user request did not complete") {
mocks.client.hasCompletedRequestOfType(OSRequestFetchUser.self)
}
}

/**
A user whose only subscription was deleted server-side comes back with no "subscriptions" key at all.
The self-heal must still notice this device's push subscription is gone and re-create it.
*/
func testFetchUser_onNewSession_recreatesPushSubscription_whenResponseHasNoSubscriptionsKey() {
/* Setup */
let mocks = Mocks()
let subscriptionExecutor = mocks.installSubscriptionExecutor()
let user = setUpUserWithPushSubscription()
stubRecreatedPushSubscription(mocks)

/* When */
fetchUserOnNewSession(mocks, user: user, response: MockUserRequests.testIdentityPayload(onesignalId: userA_OSID, externalId: userA_EUID))
subscriptionExecutor.processDeltaQueue(inBackground: false)
OneSignalCoreMocks.waitUntil("Push subscription was not re-created") {
user.pushSubscriptionModel.subscriptionId == recreatedPushSubId
}

/* Then */
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self, expectedCount: 1))
XCTAssertEqual(user.pushSubscriptionModel.subscriptionId, recreatedPushSubId)
}

/**
The response lists other subscriptions but not this device's push subscription, so it is re-created.
*/
func testFetchUser_onNewSession_recreatesPushSubscription_whenResponseOmitsThisDevice() {
/* Setup */
let mocks = Mocks()
let subscriptionExecutor = mocks.installSubscriptionExecutor()
let user = setUpUserWithPushSubscription()
stubRecreatedPushSubscription(mocks)
var response: [String: Any] = MockUserRequests.testIdentityPayload(onesignalId: userA_OSID, externalId: userA_EUID)
response["subscriptions"] = [["type": "Email", "id": "remote_email_id", "token": "remote_email@example.com"]]

/* When */
fetchUserOnNewSession(mocks, user: user, response: response)
subscriptionExecutor.processDeltaQueue(inBackground: false)
OneSignalCoreMocks.waitUntil("Push subscription was not re-created") {
user.pushSubscriptionModel.subscriptionId == recreatedPushSubId
}

/* Then */
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self, expectedCount: 1))
XCTAssertEqual(user.pushSubscriptionModel.subscriptionId, recreatedPushSubId)
}

/**
The response still lists this device's push subscription, so nothing is re-created.
*/
func testFetchUser_onNewSession_keepsPushSubscription_whenResponseContainsIt() {
/* Setup */
let mocks = Mocks()
let user = setUpUserWithPushSubscription()
var response: [String: Any] = MockUserRequests.testIdentityPayload(onesignalId: userA_OSID, externalId: userA_EUID)
response["subscriptions"] = [MockUserRequests.testDefaultPushSubPayload(id: testPushSubId)]

/* When */
fetchUserOnNewSession(mocks, user: user, response: response)

/* Then */
// The self-heal clears the id before queuing its Create, so an unchanged id proves it did not run.
XCTAssertEqual(user.pushSubscriptionModel.subscriptionId, testPushSubId)
}
}
Loading