Skip to content
Open
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
25 changes: 19 additions & 6 deletions swift-sdk/Internal/AuthManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,16 @@ class AuthManager: IterableAuthManagerProtocol {
}

private func shouldPauseRetry(_ shouldIgnoreRetryPolicy: Bool) -> Bool {
return (!shouldIgnoreRetryPolicy && pauseAuthRetry) ||
(retryCount >= authRetryPolicy.maxRetry && !shouldIgnoreRetryPolicy)
if pauseAuthRetry {
return true
}

// Scheduled refresh should never bypass retry safety limits.
if isInScheduledRefreshCallback {
return retryCount >= authRetryPolicy.maxRetry
}

return retryCount >= authRetryPolicy.maxRetry && !shouldIgnoreRetryPolicy
}

private func shouldUseLastValidToken(_ shouldIgnoreRetryPolicy: Bool) -> Bool {
Expand Down Expand Up @@ -113,6 +121,7 @@ class AuthManager: IterableAuthManagerProtocol {
private var isLastAuthTokenValid: Bool = false
private var pauseAuthRetry: Bool = false
private var isTimerScheduled: Bool = false
private var isInScheduledRefreshCallback: Bool = false

private var pendingSuccessCallbacks: [AuthTokenRetrievalHandler] = []
private let callbackQueue = DispatchQueue(label: "com.iterable.authCallbackQueue")
Expand Down Expand Up @@ -205,6 +214,7 @@ class AuthManager: IterableAuthManagerProtocol {
let timeIntervalToRefresh = TimeInterval(expirationDate) - dateProvider.currentDate.timeIntervalSince1970 - expirationRefreshPeriod
if timeIntervalToRefresh > 0 {
scheduleAuthTokenRefreshTimer(interval: timeIntervalToRefresh, isScheduledRefresh: true, successCallback: onSuccess)
resetRetryCount()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Schedule wont necessarily guarantees successful retrieval of token. Should we reset the count here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that since the schedule is fired, we can reset the retries. regardless of its success.

return true // Only return true when we successfully queue a refresh
}
return false
Expand All @@ -228,14 +238,17 @@ class AuthManager: IterableAuthManagerProtocol {
addPendingCallback(successCallback)

expirationRefreshTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in
self?.isTimerScheduled = false
if self?.localStorage.email != nil || self?.localStorage.userId != nil {
self?.requestNewAuthToken(hasFailedPriorAuth: false, onSuccess: { [weak self] token in
guard let self else { return }
self.isTimerScheduled = false
if self.localStorage.email != nil || self.localStorage.userId != nil {
self.isInScheduledRefreshCallback = isScheduledRefresh
self.requestNewAuthToken(hasFailedPriorAuth: false, onSuccess: { [weak self] token in
self?.invokePendingCallbacks(with: token)
}, shouldIgnoreRetryPolicy: isScheduledRefresh)
self.isInScheduledRefreshCallback = false
} else {
ITBDebug("Email or userId is not available. Skipping token refresh")
self?.clearPendingCallbacks()
self.clearPendingCallbacks()
}
}

Expand Down
77 changes: 75 additions & 2 deletions tests/unit-tests/AuthTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,9 @@ class AuthTests: XCTestCase {

let expirationRefreshPeriod: TimeInterval = 0
let waitTime: TimeInterval = 1.0
let expirationTimeSinceEpoch = Date(timeIntervalSinceNow: expirationRefreshPeriod + waitTime).timeIntervalSince1970
let mockDateProvider = MockDateProvider()
mockDateProvider.currentDate = Date(timeIntervalSince1970: 1_000_000_000)
let expirationTimeSinceEpoch = mockDateProvider.currentDate.timeIntervalSince1970 + expirationRefreshPeriod + waitTime
let mockEncodedPayload = createMockEncodedPayload(exp: Int(expirationTimeSinceEpoch))

let localStorage = MockLocalStorage()
Expand All @@ -437,12 +439,83 @@ class AuthTests: XCTestCase {
authRetryPolicy: RetryPolicy(maxRetry: 1, retryInterval: 0, retryBackoff: .linear),
expirationRefreshPeriod: expirationRefreshPeriod,
localStorage: localStorage,
dateProvider: MockDateProvider())
dateProvider: mockDateProvider)

let _ = authManager

wait(for: [condition1], timeout: testExpectationTimeout)
}

func testPauseAuthRetriesBlocksScheduledRefresh() {
let callbackNotCalledExpectation = expectation(description: "\(#function) - callback got called when it shouldn't while paused")
callbackNotCalledExpectation.isInverted = true

let authDelegate = createAuthDelegate({
callbackNotCalledExpectation.fulfill()
return nil
})

let expirationRefreshPeriod: TimeInterval = 0
let waitTime: TimeInterval = 1.0

let mockDateProvider = MockDateProvider()
mockDateProvider.currentDate = Date(timeIntervalSince1970: 1_000_000_000)
let expirationTimeSinceEpoch = mockDateProvider.currentDate.timeIntervalSince1970 + expirationRefreshPeriod + waitTime
let mockEncodedPayload = createMockEncodedPayload(exp: Int(expirationTimeSinceEpoch))

let localStorage = MockLocalStorage()
localStorage.authToken = mockEncodedPayload
localStorage.userId = AuthTests.userId

let authManager = AuthManager(delegate: authDelegate,
authRetryPolicy: RetryPolicy(maxRetry: 1, retryInterval: 0, retryBackoff: .linear),
expirationRefreshPeriod: expirationRefreshPeriod,
localStorage: localStorage,
dateProvider: mockDateProvider)

authManager.pauseAuthRetries(true)

wait(for: [callbackNotCalledExpectation], timeout: waitTime + 1.0)
}

func testMaxRetryLimitRespectedForScheduledRefresh() {
let secondCallbackNotCalledExpectation = expectation(description: "\(#function) - scheduled refresh requested auth token beyond maxRetry")
secondCallbackNotCalledExpectation.isInverted = true

let expirationRefreshPeriod: TimeInterval = 0
let mockDateProvider = MockDateProvider()
mockDateProvider.currentDate = Date(timeIntervalSince1970: 1_000_000_000)

let expiredExp = Int(mockDateProvider.currentDate.timeIntervalSince1970 - 10)
let expiredToken = createMockEncodedPayload(exp: expiredExp)

var callbackCount = 0
let authDelegate = createAuthDelegate({
callbackCount += 1
if callbackCount > 1 {
secondCallbackNotCalledExpectation.fulfill()
}
return expiredToken
})

let localStorage = MockLocalStorage()
localStorage.userId = AuthTests.userId

let authManager = AuthManager(delegate: authDelegate,
authRetryPolicy: RetryPolicy(maxRetry: 1, retryInterval: 0, retryBackoff: .linear),
expirationRefreshPeriod: expirationRefreshPeriod,
localStorage: localStorage,
dateProvider: mockDateProvider)

// 1st attempt increments retryCount to maxRetry
authManager.requestNewAuthToken(hasFailedPriorAuth: false, onSuccess: nil, shouldIgnoreRetryPolicy: true)

// Scheduled refresh attempt must respect maxRetry even when shouldIgnoreRetryPolicy is true.
authManager.scheduleAuthTokenRefreshTimer(interval: 0.01, isScheduledRefresh: true, successCallback: nil)

wait(for: [secondCallbackNotCalledExpectation], timeout: 1.0)
XCTAssertEqual(callbackCount, 1)
}

func testAuthTokenRefreshOnInit() {
let condition1 = expectation(description: "\(#function) - callback didn't get called when refresh was fired")
Expand Down
Loading