mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 21:22:53 +00:00
Revalidate a tokenless credential scope on account change
Fourth review pass, on a hole the previous commit opened. Making a nil ubiquity token mean "unresolved" fixed the shared-local-scope collision, but left a scope resolved from CloudKit alone with nothing local to watch: with iCloud Drive off the token stays nil across a switch from account A to B, so NSUbiquityIdentityDidChange never fires and the scope kept pointing at A - and its retained credentials with it - until relaunch. Two additions. CKAccountChanged is now observed alongside the ubiquity notification, and a tokenless scope is dropped outright when either fires because there is no fingerprint to re-derive it from. Foreground activation also revalidates a tokenless scope against CloudKit, covering a switch whose notification never arrived; it compares before replacing so the common case never blanks out credentials, and an unreachable account leaves the scope standing rather than flapping on every transient failure. A token-derived scope is untouched by all of this - it changes only via the notification, and a test pins that down.
This commit is contained in:
@@ -392,6 +392,8 @@ struct CredentialIdentityScopeTests {
|
||||
var resultsByFingerprint: [String: String] = [:]
|
||||
var failuresRemaining = 0
|
||||
var availability: CloudKitIdentity.AccountAvailability = .available
|
||||
/// User record returned when there is no ubiquity token to key on.
|
||||
var tokenlessRecord: String?
|
||||
var onResolveStart: (() -> Void)?
|
||||
var switchTask: Task<Void, Never>?
|
||||
|
||||
@@ -525,6 +527,77 @@ struct CredentialIdentityScopeTests {
|
||||
#expect(scope.current == CredentialIdentityScope.localScope)
|
||||
}
|
||||
|
||||
@Test("A tokenless scope is re-pointed when the account changes silently")
|
||||
func tokenlessScopeIsRevalidated() async {
|
||||
// iCloud Drive off: the token stays nil across the switch, so nothing
|
||||
// local changes and no ubiquity notification fires.
|
||||
let harness = Harness()
|
||||
harness.fingerprint = nil
|
||||
harness.availability = .available
|
||||
|
||||
harness.tokenlessRecord = "_recordA"
|
||||
let scope = CredentialIdentityScope(
|
||||
defaults: makeDefaults(),
|
||||
fingerprintProvider: { harness.fingerprint },
|
||||
resolveUserRecordID: { harness.tokenlessRecord },
|
||||
accountAvailabilityProvider: { harness.availability },
|
||||
retryDelay: { _ in }
|
||||
)
|
||||
|
||||
await scope.resolve()
|
||||
#expect(scope.current == "_recordA")
|
||||
|
||||
// The user switches to account B and returns to the app.
|
||||
harness.tokenlessRecord = "_recordB"
|
||||
await scope.resolveIfNeeded()
|
||||
#expect(scope.current == "_recordB")
|
||||
}
|
||||
|
||||
@Test("A tokenless scope falls back to local once the account is gone")
|
||||
func tokenlessScopeFallsBackOnSignOut() async {
|
||||
let harness = Harness()
|
||||
harness.fingerprint = nil
|
||||
harness.availability = .available
|
||||
|
||||
let scope = CredentialIdentityScope(
|
||||
defaults: makeDefaults(),
|
||||
fingerprintProvider: { harness.fingerprint },
|
||||
resolveUserRecordID: { "_recordA" },
|
||||
accountAvailabilityProvider: { harness.availability },
|
||||
retryDelay: { _ in }
|
||||
)
|
||||
|
||||
await scope.resolve()
|
||||
#expect(scope.current == "_recordA")
|
||||
|
||||
harness.availability = .noAccount
|
||||
await scope.resolveIfNeeded()
|
||||
#expect(scope.current == CredentialIdentityScope.localScope)
|
||||
}
|
||||
|
||||
@Test("A token-derived scope is not re-checked on every activation")
|
||||
func tokenDerivedScopeIsStable() async {
|
||||
let harness = Harness()
|
||||
harness.resultsByFingerprint = ["identity-A": "_recordA"]
|
||||
|
||||
let scope = CredentialIdentityScope(
|
||||
defaults: makeDefaults(),
|
||||
fingerprintProvider: { harness.fingerprint },
|
||||
resolveUserRecordID: { await harness.resolve() },
|
||||
accountAvailabilityProvider: { harness.availability },
|
||||
retryDelay: { _ in }
|
||||
)
|
||||
|
||||
await scope.resolve()
|
||||
#expect(scope.current == "_recordA")
|
||||
|
||||
// A token-backed scope changes only via the notification, so a
|
||||
// foreground pass must leave it exactly as it was.
|
||||
harness.resultsByFingerprint = ["identity-A": "_somethingElse"]
|
||||
await scope.resolveIfNeeded()
|
||||
#expect(scope.current == "_recordA")
|
||||
}
|
||||
|
||||
@Test("An undeterminable account status leaves the scope unresolved")
|
||||
func indeterminateStatusStaysUnresolved() async {
|
||||
let harness = Harness()
|
||||
|
||||
Reference in New Issue
Block a user