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 @@ -448,14 +448,19 @@ extension OSUserExecutor {
OneSignalCoreImpl.sharedClient().execute(request) { response in
self.removeFromQueue(request)

// A fetch for a user that is no longer current is stale
guard OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModel) else {
self.executePendingRequests()
return
}

if let response = response {
// Clear local data in preparation for hydration
OneSignalUserManagerImpl.sharedInstance.clearUserData()
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 request.onNewSession,
OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModel),
let subId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId,
let subscriptionObjects = self.parseSubscriptionObjectResponse(response) {
var subscriptionExists = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,61 @@ final class UserExecutorTests: XCTestCase {
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self))
XCTAssertTrue(mocks.newRecordsState.records.isEmpty)
}

/**
Regression test for a login race that landed identity (and subsequent user updates) data on the wrong user.

When an on-new-session Fetch User request for a *previous* user (e.g. a cached anonymous user) is still
pending and a `login()` switches the current user, the in-flight Fetch User must NOT clear the new current
user's data.
*/
func testFetchUser_forNonCurrentUser_doesNotClearCurrentUserData() {
/* Setup */
let mocks = Mocks()

// The current user has just logged in with an external_id (userB).
let currentUser = OneSignalUserMocks.setUserManagerInternalUser(externalId: userB_EUID, onesignalId: userB_OSID)

// A stale on-new-session Fetch User is in flight for a different, no-longer-current user (userA),
// and its response only carries an onesignal_id (as an anonymous user's would).
let staleIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID], changeNotifier: OSEventProducer())
mocks.client.setMockResponseForRequest(
request: "<OSRequestFetchUser with onesignal_id: \(userA_OSID)>",
response: MockUserRequests.testIdentityPayload(onesignalId: userA_OSID, externalId: nil)
)

/* When */
mocks.userExecutor.fetchUser(aliasLabel: OS_ONESIGNAL_ID, aliasId: userA_OSID, identityModel: staleIdentityModel, onNewSession: true)
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)

/* Then */
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self))
// The current user's external_id must be intact — the stale fetch must not have cleared it.
XCTAssertEqual(currentUser.identityModel.externalId, userB_EUID)
XCTAssertEqual(OneSignalUserManagerImpl.sharedInstance._user?.identityModel.externalId, userB_EUID)
}

/**
The normal new-session Fetch User for the *current* user must still clear stale local data before hydrating
from the response, so the `isCurrentUser` guard added for the race above does not regress the common path.
*/
func testFetchUser_forCurrentUser_stillClearsStaleData() {
/* Setup */
let mocks = Mocks()
let currentUser = OneSignalUserMocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
// A stale local alias that is not present in the server response and should be cleared by the fetch.
currentUser.identityModel.addAliases(["stale_label": "stale_value"])

MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID)

/* When */
mocks.userExecutor.fetchUser(aliasLabel: OS_ONESIGNAL_ID, aliasId: userA_OSID, identityModel: currentUser.identityModel, onNewSession: false)
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)

/* Then */
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self))
// clearUserData() ran for the current user: the stale alias is gone and server aliases are hydrated.
XCTAssertNil(currentUser.identityModel.aliases["stale_label"])
XCTAssertEqual(currentUser.identityModel.externalId, userA_EUID)
}
}
Loading