fix(account): harden session refresh recovery
Persist refresh operation identity across failures and validate revoked Apple credentials so clients recover without unsafe token rotation.
This commit is contained in:
@@ -462,6 +462,36 @@ final class AccountCenterViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(managedGatewayClearCount, 1)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testRevokedAppleCredentialClearsSignedInAccountState() async {
|
||||
let account = AccountSession(
|
||||
accountID: UUID(),
|
||||
createdAtEpochSeconds: 1_700_000_000
|
||||
)
|
||||
let service = AccountServiceSpy(
|
||||
restoredSession: account,
|
||||
snapshot: makeSnapshot(account: account),
|
||||
appleCredentialState: .revoked
|
||||
)
|
||||
let coordinator = AccountSessionCoordinator(
|
||||
dependencies: AccountDependencies(
|
||||
sessionService: service,
|
||||
centerService: service
|
||||
),
|
||||
pendingReferralStore: InMemoryPendingReferralStore()
|
||||
)
|
||||
await coordinator.restoreIfNeeded()
|
||||
XCTAssertTrue(coordinator.isSignedIn)
|
||||
|
||||
await coordinator.validateAppleCredentialState()
|
||||
|
||||
XCTAssertEqual(coordinator.sessionPhase, .signedOut)
|
||||
XCTAssertEqual(coordinator.snapshotPhase, .idle)
|
||||
XCTAssertEqual(coordinator.operationErrorKey, "account.error.sessionExpired")
|
||||
let signOutCount = await service.signOutCount()
|
||||
XCTAssertEqual(signOutCount, 1)
|
||||
}
|
||||
|
||||
private func makeReferral(status: AccountReferralStatus) -> AccountReferral {
|
||||
AccountReferral(
|
||||
id: UUID(),
|
||||
@@ -535,6 +565,7 @@ private actor AccountServiceSpy: AccountSessionServicing, AccountCenterServicing
|
||||
private let shouldFailAccountRefresh: Bool
|
||||
private let signOutDelayNanoseconds: UInt64
|
||||
private let accountLoadDelayNanoseconds: UInt64
|
||||
private let storedAppleCredentialState: AccountAppleCredentialState
|
||||
private var redeemed: [String] = []
|
||||
private var centerLoadCount = 0
|
||||
private var logoutCount = 0
|
||||
@@ -550,7 +581,8 @@ private actor AccountServiceSpy: AccountSessionServicing, AccountCenterServicing
|
||||
shouldFailRedemption: Bool = false,
|
||||
shouldFailAccountRefresh: Bool = false,
|
||||
signOutDelayNanoseconds: UInt64 = 0,
|
||||
accountLoadDelayNanoseconds: UInt64 = 0
|
||||
accountLoadDelayNanoseconds: UInt64 = 0,
|
||||
appleCredentialState: AccountAppleCredentialState = .unknown
|
||||
) {
|
||||
restored = restoredSession
|
||||
centerSnapshot = snapshot
|
||||
@@ -559,6 +591,7 @@ private actor AccountServiceSpy: AccountSessionServicing, AccountCenterServicing
|
||||
self.shouldFailAccountRefresh = shouldFailAccountRefresh
|
||||
self.signOutDelayNanoseconds = signOutDelayNanoseconds
|
||||
self.accountLoadDelayNanoseconds = accountLoadDelayNanoseconds
|
||||
self.storedAppleCredentialState = appleCredentialState
|
||||
}
|
||||
|
||||
func restoreSession() async throws -> AccountSession? {
|
||||
@@ -582,6 +615,10 @@ private actor AccountServiceSpy: AccountSessionServicing, AccountCenterServicing
|
||||
accountDeleteCount += 1
|
||||
}
|
||||
|
||||
func appleCredentialState() async -> AccountAppleCredentialState {
|
||||
storedAppleCredentialState
|
||||
}
|
||||
|
||||
func clearManagedGateway() async {
|
||||
gatewayClearCount += 1
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user