import Foundationimport Testing@testable import CodexBarCore @Suite(.serialized)struct ClaudeOAuthCredentialsStoreTests { private func makeCredentialsData(accessToken: String, expiresAt: Date, refreshToken: String? = nil) -> Data { let millis = Int(expiresAt.timeIntervalSince1970 * 1000) let refreshField: String = { guard let refreshToken else { return "" } return ",\n \"refreshToken\": \"\(refreshToken)\"" }() let json = """ { "claudeAiOauth": { "accessToken": "\(accessToken)", "expiresAt": \(millis), "scopes": ["user:profile"]\(refreshField) } } """ return Data(json.utf8) } @Test func `loads from keychain cache before expired file`() throws { let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)" try ProviderInteractionContext.$current.withValue(.background) { try KeychainCacheStore.withServiceOverrideForTesting(service) { try KeychainAccessGate.withTaskOverrideForTesting(false) { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } try ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting { try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting { let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") try ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { let expiredData = self.makeCredentialsData( accessToken: "expired", expiresAt: Date(timeIntervalSinceNow: -3600)) try expiredData.write(to: fileURL) let cachedData = self.makeCredentialsData( accessToken: "cached", expiresAt: Date(timeIntervalSinceNow: 3600)) let cacheEntry = ClaudeOAuthCredentialsStore.CacheEntry( data: cachedData, storedAt: Date()) let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) ClaudeOAuthCredentialsStore.invalidateCache() KeychainCacheStore.store(key: cacheKey, entry: cacheEntry) defer { KeychainCacheStore.clear(key: cacheKey) } _ = try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting( .securityFramework) { try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting( .onlyOnUserAction) { try ClaudeOAuthCredentialsStore.load( environment: [:], allowKeychainPrompt: false) } } // Re-store to cache after file check has marked file as "seen" KeychainCacheStore.store(key: cacheKey, entry: cacheEntry) let creds = try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting( .securityFramework) { try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting( .onlyOnUserAction) { try ClaudeOAuthCredentialsStore.load( environment: [:], allowKeychainPrompt: false) } } #expect(creds.accessToken == "cached") #expect(creds.isExpired == false) } } } } } } } @Test func `load record non interactive repair can be disabled`() throws { let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)" try KeychainCacheStore.withServiceOverrideForTesting(service) { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } try ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting { try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting { // Ensure file-based lookup doesn't interfere (and avoid touching ~/.claude). let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") try ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { ClaudeOAuthCredentialsStore.invalidateCache() let keychainData = self.makeCredentialsData( accessToken: "claude-keychain", expiresAt: Date(timeIntervalSinceNow: 3600)) // Simulate Claude Keychain containing creds, without querying the real Keychain. try ProviderInteractionContext.$current.withValue(.userInitiated) { try ClaudeOAuthCredentialsStore .withClaudeKeychainOverridesForTesting(data: keychainData, fingerprint: nil) { // When repair is disabled, non-interactive loads should not consult Claude's // keychain data. do { _ = try ClaudeOAuthCredentialsStore.loadRecord( environment: [:], allowKeychainPrompt: false, respectKeychainPromptCooldown: true, allowClaudeKeychainRepairWithoutPrompt: false) Issue.record("Expected ClaudeOAuthCredentialsError.notFound") } catch let error as ClaudeOAuthCredentialsError { guard case .notFound = error else { Issue.record("Expected .notFound, got \(error)") return } } // With repair enabled, we should be able to seed from the "Claude keychain" // override. let record = try ClaudeOAuthCredentialsStore.loadRecord( environment: [:], allowKeychainPrompt: false, respectKeychainPromptCooldown: true, allowClaudeKeychainRepairWithoutPrompt: true) #expect(record.credentials.accessToken == "claude-keychain") } } } } } } } @Test func `invalidates cache when credentials file changes`() throws { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } // Avoid interacting with the real Keychain in unit tests. try ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(true) { try ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting { try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting { let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") try ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { let first = self.makeCredentialsData( accessToken: "first", expiresAt: Date(timeIntervalSinceNow: 3600)) try first.write(to: fileURL) let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) let cacheEntry = ClaudeOAuthCredentialsStore.CacheEntry(data: first, storedAt: Date()) KeychainCacheStore.store(key: cacheKey, entry: cacheEntry) _ = try ClaudeOAuthCredentialsStore.load(environment: [:]) let updated = self.makeCredentialsData( accessToken: "second", expiresAt: Date(timeIntervalSinceNow: 3600)) try updated.write(to: fileURL) #expect(ClaudeOAuthCredentialsStore.invalidateCacheIfCredentialsFileChanged()) KeychainCacheStore.clear(key: cacheKey) let creds = try ClaudeOAuthCredentialsStore.load(environment: [:]) #expect(creds.accessToken == "second") } } } } } @Test func `returns expired file when no other sources`() throws { let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)" try KeychainCacheStore.withServiceOverrideForTesting(service) { try KeychainAccessGate.withTaskOverrideForTesting(true) { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") try ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { let expiredData = self.makeCredentialsData( accessToken: "expired-only", expiresAt: Date(timeIntervalSinceNow: -3600)) try expiredData.write(to: fileURL) try ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(true) { ClaudeOAuthCredentialsStore.invalidateCache() let creds = try ClaudeOAuthCredentialsStore.load(environment: [:]) #expect(creds.accessToken == "expired-only") #expect(creds.isExpired == true) } } } } } } @Test func `load with auto refresh expired claude CLI owner throws delegated refresh`() async throws { let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)" try await KeychainCacheStore.withServiceOverrideForTesting(service) { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { await ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(true) { ClaudeOAuthCredentialsStore.invalidateCache() let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) defer { KeychainCacheStore.clear(key: cacheKey) } let expiredData = self.makeCredentialsData( accessToken: "expired-claude-cli-owner", expiresAt: Date(timeIntervalSinceNow: -3600), refreshToken: "refresh-token") KeychainCacheStore.store( key: cacheKey, entry: ClaudeOAuthCredentialsStore.CacheEntry( data: expiredData, storedAt: Date(), owner: .claudeCLI)) do { _ = try await ClaudeOAuthCredentialsStore.loadWithAutoRefresh( environment: [:], allowKeychainPrompt: false, respectKeychainPromptCooldown: true) Issue.record("Expected delegated refresh error for Claude CLI-owned credentials") } catch let error as ClaudeOAuthCredentialsError { guard case .refreshDelegatedToClaudeCLI = error else { Issue.record("Expected .refreshDelegatedToClaudeCLI, got \(error)") return } } catch { Issue.record("Expected ClaudeOAuthCredentialsError, got \(error)") } } } } } @Test func `load with auto refresh expired codexbar owner uses direct refresh path`() async throws { let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)" try await KeychainCacheStore.withServiceOverrideForTesting(service) { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { await ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(true) { ClaudeOAuthCredentialsStore.invalidateCache() let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) defer { KeychainCacheStore.clear(key: cacheKey) } let expiredData = self.makeCredentialsData( accessToken: "expired-codexbar-owner", expiresAt: Date(timeIntervalSinceNow: -3600), refreshToken: "refresh-token") KeychainCacheStore.store( key: cacheKey, entry: ClaudeOAuthCredentialsStore.CacheEntry( data: expiredData, storedAt: Date(), owner: .codexbar)) await ClaudeOAuthRefreshFailureGate.$shouldAttemptOverride.withValue(false) { do { _ = try await ClaudeOAuthCredentialsStore.loadWithAutoRefresh( environment: [:], allowKeychainPrompt: false, respectKeychainPromptCooldown: true) Issue.record("Expected refresh failure for CodexBar-owned direct refresh path") } catch let error as ClaudeOAuthCredentialsError { guard case .refreshFailed = error else { Issue.record("Expected .refreshFailed, got \(error)") return } } catch { Issue.record("Expected ClaudeOAuthCredentialsError, got \(error)") } } } } } } @Test func `load record legacy cache entry without owner defaults to claude CLI owner`() throws { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting { try ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { try ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(true) { ClaudeOAuthCredentialsStore.invalidateCache() let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) defer { KeychainCacheStore.clear(key: cacheKey) } let validData = self.makeCredentialsData( accessToken: "legacy-owner", expiresAt: Date(timeIntervalSinceNow: 3600), refreshToken: "refresh-token") KeychainCacheStore.store( key: cacheKey, entry: ClaudeOAuthCredentialsStore.CacheEntry( data: validData, storedAt: Date())) let record = try ClaudeOAuthCredentialsStore.loadRecord( environment: [:], allowKeychainPrompt: false, respectKeychainPromptCooldown: true) #expect(record.owner == .claudeCLI) #expect(record.source == .cacheKeychain) } } } } @Test func `has cached credentials returns false for expired unrefreshable codexbar cache entry`() throws { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { ClaudeOAuthCredentialsStore.invalidateCache() let expiredData = self.makeCredentialsData( accessToken: "expired-no-refresh", expiresAt: Date(timeIntervalSinceNow: -3600), refreshToken: nil) let cacheEntry = ClaudeOAuthCredentialsStore.CacheEntry( data: expiredData, storedAt: Date(), owner: .codexbar) let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) KeychainCacheStore.store(key: cacheKey, entry: cacheEntry) #expect(ClaudeOAuthCredentialsStore.hasCachedCredentials() == false) } } @Test func `has cached credentials returns true for expired refreshable cache entry`() throws { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { ClaudeOAuthCredentialsStore.invalidateCache() let expiredData = self.makeCredentialsData( accessToken: "expired-refreshable", expiresAt: Date(timeIntervalSinceNow: -3600), refreshToken: "refresh") let cacheEntry = ClaudeOAuthCredentialsStore.CacheEntry(data: expiredData, storedAt: Date()) let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) KeychainCacheStore.store(key: cacheKey, entry: cacheEntry) #expect(ClaudeOAuthCredentialsStore.hasCachedCredentials() == true) } } @Test func `has cached credentials returns true for expired claude CLI backed credentials file`() throws { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") try ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { ClaudeOAuthCredentialsStore.invalidateCache() let expiredData = self.makeCredentialsData( accessToken: "expired-file-no-refresh", expiresAt: Date(timeIntervalSinceNow: -3600), refreshToken: nil) try expiredData.write(to: fileURL) #expect(ClaudeOAuthCredentialsStore.hasCachedCredentials() == true) } } @Test func `syncs cache when claude keychain fingerprint changes and token differs`() throws { let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)" try KeychainCacheStore.withServiceOverrideForTesting(service) { try KeychainAccessGate.withTaskOverrideForTesting(false) { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthKeychainAccessGate.resetForTesting() defer { ClaudeOAuthKeychainAccessGate.resetForTesting() } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") try ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { ClaudeOAuthCredentialsStore.invalidateCache() ClaudeOAuthCredentialsStore._resetClaudeKeychainChangeTrackingForTesting() defer { ClaudeOAuthCredentialsStore.invalidateCache() ClaudeOAuthCredentialsStore._resetClaudeKeychainChangeTrackingForTesting() ClaudeOAuthCredentialsStore.setClaudeKeychainDataOverrideForTesting(nil) ClaudeOAuthCredentialsStore.setClaudeKeychainFingerprintOverrideForTesting(nil) } // Avoid cross-suite interference from UserDefaults fingerprint persistence. let fingerprintStore = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprintStore() let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) let cachedData = self.makeCredentialsData( accessToken: "cached-token", expiresAt: Date(timeIntervalSinceNow: 3600)) KeychainCacheStore.store( key: cacheKey, entry: ClaudeOAuthCredentialsStore.CacheEntry(data: cachedData, storedAt: Date())) let fingerprint1 = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprint( modifiedAt: 1, createdAt: 1, persistentRefHash: "ref1") let first = try ProviderInteractionContext.$current.withValue(.userInitiated) {Lines 1–500 of 889. Load more
import Foundation import Testing @testable import CodexBarCore @Suite(.serialized) struct ClaudeOAuthCredentialsStoreTests { private func makeCredentialsData(accessToken: String, expiresAt: Date, refreshToken: String? = nil) -> Data { let millis = Int(expiresAt.timeIntervalSince1970 * 1000) let refreshField: String = { guard let refreshToken else { return "" } return ",\n \"refreshToken\": \"\(refreshToken)\"" }() let json = """ { "claudeAiOauth": { "accessToken": "\(accessToken)", "expiresAt": \(millis), "scopes": ["user:profile"]\(refreshField) } } """ return Data(json.utf8) } @Test func `loads from keychain cache before expired file`() throws { let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)" try ProviderInteractionContext.$current.withValue(.background) { try KeychainCacheStore.withServiceOverrideForTesting(service) { try KeychainAccessGate.withTaskOverrideForTesting(false) { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } try ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting { try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting { let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") try ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { let expiredData = self.makeCredentialsData( accessToken: "expired", expiresAt: Date(timeIntervalSinceNow: -3600)) try expiredData.write(to: fileURL) let cachedData = self.makeCredentialsData( accessToken: "cached", expiresAt: Date(timeIntervalSinceNow: 3600)) let cacheEntry = ClaudeOAuthCredentialsStore.CacheEntry( data: cachedData, storedAt: Date()) let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) ClaudeOAuthCredentialsStore.invalidateCache() KeychainCacheStore.store(key: cacheKey, entry: cacheEntry) defer { KeychainCacheStore.clear(key: cacheKey) } _ = try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting( .securityFramework) { try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting( .onlyOnUserAction) { try ClaudeOAuthCredentialsStore.load( environment: [:], allowKeychainPrompt: false) } } // Re-store to cache after file check has marked file as "seen" KeychainCacheStore.store(key: cacheKey, entry: cacheEntry) let creds = try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting( .securityFramework) { try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting( .onlyOnUserAction) { try ClaudeOAuthCredentialsStore.load( environment: [:], allowKeychainPrompt: false) } } #expect(creds.accessToken == "cached") #expect(creds.isExpired == false) } } } } } } } @Test func `load record non interactive repair can be disabled`() throws { let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)" try KeychainCacheStore.withServiceOverrideForTesting(service) { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } try ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting { try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting { // Ensure file-based lookup doesn't interfere (and avoid touching ~/.claude). let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") try ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { ClaudeOAuthCredentialsStore.invalidateCache() let keychainData = self.makeCredentialsData( accessToken: "claude-keychain", expiresAt: Date(timeIntervalSinceNow: 3600)) // Simulate Claude Keychain containing creds, without querying the real Keychain. try ProviderInteractionContext.$current.withValue(.userInitiated) { try ClaudeOAuthCredentialsStore .withClaudeKeychainOverridesForTesting(data: keychainData, fingerprint: nil) { // When repair is disabled, non-interactive loads should not consult Claude's // keychain data. do { _ = try ClaudeOAuthCredentialsStore.loadRecord( environment: [:], allowKeychainPrompt: false, respectKeychainPromptCooldown: true, allowClaudeKeychainRepairWithoutPrompt: false) Issue.record("Expected ClaudeOAuthCredentialsError.notFound") } catch let error as ClaudeOAuthCredentialsError { guard case .notFound = error else { Issue.record("Expected .notFound, got \(error)") return } } // With repair enabled, we should be able to seed from the "Claude keychain" // override. let record = try ClaudeOAuthCredentialsStore.loadRecord( environment: [:], allowKeychainPrompt: false, respectKeychainPromptCooldown: true, allowClaudeKeychainRepairWithoutPrompt: true) #expect(record.credentials.accessToken == "claude-keychain") } } } } } } } @Test func `invalidates cache when credentials file changes`() throws { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } // Avoid interacting with the real Keychain in unit tests. try ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(true) { try ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting { try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting { let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") try ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { let first = self.makeCredentialsData( accessToken: "first", expiresAt: Date(timeIntervalSinceNow: 3600)) try first.write(to: fileURL) let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) let cacheEntry = ClaudeOAuthCredentialsStore.CacheEntry(data: first, storedAt: Date()) KeychainCacheStore.store(key: cacheKey, entry: cacheEntry) _ = try ClaudeOAuthCredentialsStore.load(environment: [:]) let updated = self.makeCredentialsData( accessToken: "second", expiresAt: Date(timeIntervalSinceNow: 3600)) try updated.write(to: fileURL) #expect(ClaudeOAuthCredentialsStore.invalidateCacheIfCredentialsFileChanged()) KeychainCacheStore.clear(key: cacheKey) let creds = try ClaudeOAuthCredentialsStore.load(environment: [:]) #expect(creds.accessToken == "second") } } } } } @Test func `returns expired file when no other sources`() throws { let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)" try KeychainCacheStore.withServiceOverrideForTesting(service) { try KeychainAccessGate.withTaskOverrideForTesting(true) { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") try ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { let expiredData = self.makeCredentialsData( accessToken: "expired-only", expiresAt: Date(timeIntervalSinceNow: -3600)) try expiredData.write(to: fileURL) try ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(true) { ClaudeOAuthCredentialsStore.invalidateCache() let creds = try ClaudeOAuthCredentialsStore.load(environment: [:]) #expect(creds.accessToken == "expired-only") #expect(creds.isExpired == true) } } } } } } @Test func `load with auto refresh expired claude CLI owner throws delegated refresh`() async throws { let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)" try await KeychainCacheStore.withServiceOverrideForTesting(service) { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { await ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(true) { ClaudeOAuthCredentialsStore.invalidateCache() let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) defer { KeychainCacheStore.clear(key: cacheKey) } let expiredData = self.makeCredentialsData( accessToken: "expired-claude-cli-owner", expiresAt: Date(timeIntervalSinceNow: -3600), refreshToken: "refresh-token") KeychainCacheStore.store( key: cacheKey, entry: ClaudeOAuthCredentialsStore.CacheEntry( data: expiredData, storedAt: Date(), owner: .claudeCLI)) do { _ = try await ClaudeOAuthCredentialsStore.loadWithAutoRefresh( environment: [:], allowKeychainPrompt: false, respectKeychainPromptCooldown: true) Issue.record("Expected delegated refresh error for Claude CLI-owned credentials") } catch let error as ClaudeOAuthCredentialsError { guard case .refreshDelegatedToClaudeCLI = error else { Issue.record("Expected .refreshDelegatedToClaudeCLI, got \(error)") return } } catch { Issue.record("Expected ClaudeOAuthCredentialsError, got \(error)") } } } } } @Test func `load with auto refresh expired codexbar owner uses direct refresh path`() async throws { let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)" try await KeychainCacheStore.withServiceOverrideForTesting(service) { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { await ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(true) { ClaudeOAuthCredentialsStore.invalidateCache() let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) defer { KeychainCacheStore.clear(key: cacheKey) } let expiredData = self.makeCredentialsData( accessToken: "expired-codexbar-owner", expiresAt: Date(timeIntervalSinceNow: -3600), refreshToken: "refresh-token") KeychainCacheStore.store( key: cacheKey, entry: ClaudeOAuthCredentialsStore.CacheEntry( data: expiredData, storedAt: Date(), owner: .codexbar)) await ClaudeOAuthRefreshFailureGate.$shouldAttemptOverride.withValue(false) { do { _ = try await ClaudeOAuthCredentialsStore.loadWithAutoRefresh( environment: [:], allowKeychainPrompt: false, respectKeychainPromptCooldown: true) Issue.record("Expected refresh failure for CodexBar-owned direct refresh path") } catch let error as ClaudeOAuthCredentialsError { guard case .refreshFailed = error else { Issue.record("Expected .refreshFailed, got \(error)") return } } catch { Issue.record("Expected ClaudeOAuthCredentialsError, got \(error)") } } } } } } @Test func `load record legacy cache entry without owner defaults to claude CLI owner`() throws { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting { try ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { try ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(true) { ClaudeOAuthCredentialsStore.invalidateCache() let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) defer { KeychainCacheStore.clear(key: cacheKey) } let validData = self.makeCredentialsData( accessToken: "legacy-owner", expiresAt: Date(timeIntervalSinceNow: 3600), refreshToken: "refresh-token") KeychainCacheStore.store( key: cacheKey, entry: ClaudeOAuthCredentialsStore.CacheEntry( data: validData, storedAt: Date())) let record = try ClaudeOAuthCredentialsStore.loadRecord( environment: [:], allowKeychainPrompt: false, respectKeychainPromptCooldown: true) #expect(record.owner == .claudeCLI) #expect(record.source == .cacheKeychain) } } } } @Test func `has cached credentials returns false for expired unrefreshable codexbar cache entry`() throws { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { ClaudeOAuthCredentialsStore.invalidateCache() let expiredData = self.makeCredentialsData( accessToken: "expired-no-refresh", expiresAt: Date(timeIntervalSinceNow: -3600), refreshToken: nil) let cacheEntry = ClaudeOAuthCredentialsStore.CacheEntry( data: expiredData, storedAt: Date(), owner: .codexbar) let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) KeychainCacheStore.store(key: cacheKey, entry: cacheEntry) #expect(ClaudeOAuthCredentialsStore.hasCachedCredentials() == false) } } @Test func `has cached credentials returns true for expired refreshable cache entry`() throws { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { ClaudeOAuthCredentialsStore.invalidateCache() let expiredData = self.makeCredentialsData( accessToken: "expired-refreshable", expiresAt: Date(timeIntervalSinceNow: -3600), refreshToken: "refresh") let cacheEntry = ClaudeOAuthCredentialsStore.CacheEntry(data: expiredData, storedAt: Date()) let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) KeychainCacheStore.store(key: cacheKey, entry: cacheEntry) #expect(ClaudeOAuthCredentialsStore.hasCachedCredentials() == true) } } @Test func `has cached credentials returns true for expired claude CLI backed credentials file`() throws { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") try ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { ClaudeOAuthCredentialsStore.invalidateCache() let expiredData = self.makeCredentialsData( accessToken: "expired-file-no-refresh", expiresAt: Date(timeIntervalSinceNow: -3600), refreshToken: nil) try expiredData.write(to: fileURL) #expect(ClaudeOAuthCredentialsStore.hasCachedCredentials() == true) } } @Test func `syncs cache when claude keychain fingerprint changes and token differs`() throws { let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)" try KeychainCacheStore.withServiceOverrideForTesting(service) { try KeychainAccessGate.withTaskOverrideForTesting(false) { KeychainCacheStore.setTestStoreForTesting(true) defer { KeychainCacheStore.setTestStoreForTesting(false) } ClaudeOAuthKeychainAccessGate.resetForTesting() defer { ClaudeOAuthKeychainAccessGate.resetForTesting() } ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() } let tempDir = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) let fileURL = tempDir.appendingPathComponent("credentials.json") try ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) { ClaudeOAuthCredentialsStore.invalidateCache() ClaudeOAuthCredentialsStore._resetClaudeKeychainChangeTrackingForTesting() defer { ClaudeOAuthCredentialsStore.invalidateCache() ClaudeOAuthCredentialsStore._resetClaudeKeychainChangeTrackingForTesting() ClaudeOAuthCredentialsStore.setClaudeKeychainDataOverrideForTesting(nil) ClaudeOAuthCredentialsStore.setClaudeKeychainFingerprintOverrideForTesting(nil) } // Avoid cross-suite interference from UserDefaults fingerprint persistence. let fingerprintStore = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprintStore() let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) let cachedData = self.makeCredentialsData( accessToken: "cached-token", expiresAt: Date(timeIntervalSinceNow: 3600)) KeychainCacheStore.store( key: cacheKey, entry: ClaudeOAuthCredentialsStore.CacheEntry(data: cachedData, storedAt: Date())) let fingerprint1 = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprint( modifiedAt: 1, createdAt: 1, persistentRefHash: "ref1") let first = try ProviderInteractionContext.$current.withValue(.userInitiated) {