Files
steipete--codexbar/Tests/CodexBarTests/ClaudeOAuthCredentialsStoreSecurityCLITests.swift
2026-07-13 12:22:33 +08:00

883 lines
48 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
@Suite(.serialized)
struct ClaudeOAuthCredentialsStoreSecurityCLITests {
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 `experimental reader prefers security CLI for non interactive load`() 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) }
ClaudeOAuthCredentialsStore.invalidateCache()
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
defer {
ClaudeOAuthCredentialsStore.invalidateCache()
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
ClaudeOAuthCredentialsStore.setClaudeKeychainDataOverrideForTesting(nil)
ClaudeOAuthCredentialsStore.setClaudeKeychainFingerprintOverrideForTesting(nil)
}
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 securityData = self.makeCredentialsData(
accessToken: "security-token",
expiresAt: Date(timeIntervalSinceNow: 3600),
refreshToken: "security-refresh")
let creds = try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
.onlyOnUserAction,
operation: {
try ProviderInteractionContext.$current.withValue(.userInitiated) {
try ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(
.data(securityData))
{
try ClaudeOAuthCredentialsStore.load(
environment: [:],
allowKeychainPrompt: false)
}
}
})
})
#expect(creds.accessToken == "security-token")
#expect(creds.refreshToken == "security-refresh")
#expect(creds.scopes.contains("user:profile"))
}
}
}
}
@Test
func `experimental reader non interactive background load still executes security CLI read`() 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) }
ClaudeOAuthCredentialsStore.invalidateCache()
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
defer {
ClaudeOAuthCredentialsStore.invalidateCache()
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
ClaudeOAuthCredentialsStore.setClaudeKeychainDataOverrideForTesting(nil)
ClaudeOAuthCredentialsStore.setClaudeKeychainFingerprintOverrideForTesting(nil)
}
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 securityData = self.makeCredentialsData(
accessToken: "security-token-background",
expiresAt: Date(timeIntervalSinceNow: 3600),
refreshToken: "security-refresh-background")
final class ReadCounter: @unchecked Sendable {
var count = 0
}
let securityReadCalls = ReadCounter()
let creds = try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
.onlyOnUserAction,
operation: {
try ProviderInteractionContext.$current.withValue(.background) {
try ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(
.dynamic { _ in
securityReadCalls.count += 1
return securityData
}) {
try ClaudeOAuthCredentialsStore.load(
environment: [:],
allowKeychainPrompt: false)
}
}
})
})
#expect(creds.accessToken == "security-token-background")
#expect(securityReadCalls.count == 1)
}
}
}
}
@Test
func `experimental reader falls back when security CLI throws`() 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) }
ClaudeOAuthCredentialsStore.invalidateCache()
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
defer {
ClaudeOAuthCredentialsStore.invalidateCache()
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
ClaudeOAuthCredentialsStore.setClaudeKeychainDataOverrideForTesting(nil)
ClaudeOAuthCredentialsStore.setClaudeKeychainFingerprintOverrideForTesting(nil)
}
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 fallbackData = self.makeCredentialsData(
accessToken: "fallback-token",
expiresAt: Date(timeIntervalSinceNow: 3600),
refreshToken: "fallback-refresh")
let creds = try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
.onlyOnUserAction,
operation: {
try ProviderInteractionContext.$current.withValue(.userInitiated) {
try ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
data: fallbackData,
fingerprint: nil)
{
try ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(
.timedOut)
{
try ClaudeOAuthCredentialsStore.load(
environment: [:],
allowKeychainPrompt: false)
}
}
}
})
})
#expect(creds.accessToken == "fallback-token")
}
}
}
}
@Test
func `experimental reader falls back when security CLI output malformed`() 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) }
ClaudeOAuthCredentialsStore.invalidateCache()
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
defer {
ClaudeOAuthCredentialsStore.invalidateCache()
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
ClaudeOAuthCredentialsStore.setClaudeKeychainDataOverrideForTesting(nil)
ClaudeOAuthCredentialsStore.setClaudeKeychainFingerprintOverrideForTesting(nil)
}
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 fallbackData = self.makeCredentialsData(
accessToken: "fallback-token",
expiresAt: Date(timeIntervalSinceNow: 3600))
let creds = try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
.onlyOnUserAction,
operation: {
try ProviderInteractionContext.$current.withValue(.userInitiated) {
try ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
data: fallbackData,
fingerprint: nil)
{
try ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(
.data(Data("not-json".utf8)))
{
try ClaudeOAuthCredentialsStore.load(
environment: [:],
allowKeychainPrompt: false)
}
}
}
})
})
#expect(creds.accessToken == "fallback-token")
}
}
}
}
@Test
func `experimental reader load from claude keychain uses security CLI`() throws {
let securityData = self.makeCredentialsData(
accessToken: "security-direct",
expiresAt: Date(timeIntervalSinceNow: 3600),
refreshToken: "security-refresh")
let fingerprintStore = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprintStore()
let sentinelFingerprint = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprint(
modifiedAt: 200,
createdAt: 199,
persistentRefHash: "sentinel")
let loaded = try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
.always,
operation: {
try ProviderInteractionContext.$current.withValue(.userInitiated) {
try ClaudeOAuthCredentialsStore.withClaudeKeychainFingerprintStoreOverrideForTesting(
fingerprintStore)
{
try ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
data: nil,
fingerprint: sentinelFingerprint)
{
try ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(
.data(securityData))
{
try ClaudeOAuthCredentialsStore.loadFromClaudeKeychain()
}
}
}
}
})
})
let creds = try ClaudeOAuthCredentials.parse(data: loaded)
#expect(creds.accessToken == "security-direct")
#expect(creds.refreshToken == "security-refresh")
#expect(fingerprintStore.fingerprint == nil)
}
@Test
func `experimental reader has claude keychain credentials without prompt uses security CLI`() {
let securityData = self.makeCredentialsData(
accessToken: "security-available",
expiresAt: Date(timeIntervalSinceNow: 3600))
let hasCredentials = ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
.always,
operation: {
ProviderInteractionContext.$current.withValue(.userInitiated) {
ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(
.data(securityData))
{
ClaudeOAuthCredentialsStore.hasClaudeKeychainCredentialsWithoutPrompt()
}
}
})
})
#expect(hasCredentials == true)
}
@Test
func `experimental reader has claude keychain credentials without prompt falls back when security CLI fails`() {
let fallbackData = self.makeCredentialsData(
accessToken: "fallback-available",
expiresAt: Date(timeIntervalSinceNow: 3600))
let hasCredentials = ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
.always,
operation: {
ProviderInteractionContext.$current.withValue(.userInitiated) {
ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
data: fallbackData,
fingerprint: nil)
{
ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(
.nonZeroExit)
{
ClaudeOAuthCredentialsStore.hasClaudeKeychainCredentialsWithoutPrompt()
}
}
}
})
})
#expect(hasCredentials == true)
}
@Test
func `experimental reader ignores prompt policy and cooldown for background silent check`() {
let securityData = self.makeCredentialsData(
accessToken: "security-background",
expiresAt: Date(timeIntervalSinceNow: 3600))
let hasCredentials = KeychainAccessGate.withTaskOverrideForTesting(false) {
ClaudeOAuthKeychainAccessGate.withShouldAllowPromptOverrideForTesting(false) {
ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
.never,
operation: {
ProviderInteractionContext.$current.withValue(.background) {
ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(
.data(securityData))
{
ClaudeOAuthCredentialsStore.hasClaudeKeychainCredentialsWithoutPrompt()
}
}
})
})
}
}
#expect(hasCredentials == true)
}
@Test
func `experimental reader load from claude keychain fallback blocked when stored mode never`() throws {
var threwNotFound = false
do {
_ = try KeychainAccessGate.withTaskOverrideForTesting(false) {
try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
.never,
operation: {
try ProviderInteractionContext.$current.withValue(.background) {
try ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(
.nonZeroExit)
{
try ClaudeOAuthCredentialsStore.loadFromClaudeKeychain()
}
}
})
})
}
} catch let error as ClaudeOAuthCredentialsError {
if case .notFound = error {
threwNotFound = true
}
}
#expect(threwNotFound == true)
}
@Test
func `experimental reader security CLI read pins preferred account when available`() throws {
let securityData = self.makeCredentialsData(
accessToken: "security-account-pinned",
expiresAt: Date(timeIntervalSinceNow: 3600))
final class AccountBox: @unchecked Sendable {
var value: String?
}
let pinnedAccount = AccountBox()
let loaded = try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
try ClaudeOAuthCredentialsStore.withSecurityCLIReadAccountOverrideForTesting("new-account") {
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
.always,
operation: {
try ProviderInteractionContext.$current.withValue(.userInitiated) {
try ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(
.dynamic { request in
pinnedAccount.value = request.account
return securityData
}) {
try ClaudeOAuthCredentialsStore.loadFromClaudeKeychain()
}
}
})
}
})
let creds = try ClaudeOAuthCredentials.parse(data: loaded)
#expect(pinnedAccount.value == "new-account")
#expect(creds.accessToken == "security-account-pinned")
}
@Test
func `experimental reader security CLI read does not pin account in background`() throws {
let securityData = self.makeCredentialsData(
accessToken: "security-account-not-pinned",
expiresAt: Date(timeIntervalSinceNow: 3600))
final class AccountBox: @unchecked Sendable {
var value: String?
}
let pinnedAccount = AccountBox()
let loaded = try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
try ClaudeOAuthCredentialsStore.withSecurityCLIReadAccountOverrideForTesting("new-account") {
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
.always,
operation: {
try ProviderInteractionContext.$current.withValue(.background) {
try ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(
.dynamic { request in
pinnedAccount.value = request.account
return securityData
}) {
try ClaudeOAuthCredentialsStore.loadFromClaudeKeychain()
}
}
})
}
})
let creds = try ClaudeOAuthCredentials.parse(data: loaded)
#expect(pinnedAccount.value == nil)
#expect(creds.accessToken == "security-account-not-pinned")
}
@Test
func `experimental reader freshness sync skips security CLI when preflight requires interaction`() 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) }
try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {
try ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {
ClaudeOAuthCredentialsStore.invalidateCache()
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
defer {
ClaudeOAuthCredentialsStore.invalidateCache()
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 securityData = self.makeCredentialsData(
accessToken: "security-sync",
expiresAt: Date(timeIntervalSinceNow: 3600))
final class ReadCounter: @unchecked Sendable {
var count = 0
}
let securityReadCalls = ReadCounter()
func loadWithPreflight(
_ outcome: KeychainAccessPreflight.Outcome) throws -> ClaudeOAuthCredentials
{
let preflightOverride: (String, String?) -> KeychainAccessPreflight.Outcome = { _, _ in
outcome
}
return try KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting(
preflightOverride,
operation: {
try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental)
{
try ClaudeOAuthKeychainPromptPreference
.withTaskOverrideForTesting(.always) {
try ProviderInteractionContext.$current.withValue(.background) {
try ClaudeOAuthCredentialsStore
.withSecurityCLIReadOverrideForTesting(
.dynamic { _ in
securityReadCalls.count += 1
return securityData
}) {
try ClaudeOAuthCredentialsStore.load(
environment: [:],
allowKeychainPrompt: false,
respectKeychainPromptCooldown: true)
}
}
}
}
})
}
let first = try loadWithPreflight(.allowed)
#expect(first.accessToken == "security-sync")
#expect(securityReadCalls.count == 1)
let second = try loadWithPreflight(.interactionRequired)
#expect(second.accessToken == "security-sync")
#expect(securityReadCalls.count == 1)
}
}
}
}
}
}
@Test
func `experimental reader freshness sync background respects stored only on user action`() 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) }
try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {
try ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {
ClaudeOAuthCredentialsStore.invalidateCache()
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
defer {
ClaudeOAuthCredentialsStore.invalidateCache()
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 securityData = self.makeCredentialsData(
accessToken: "security-sync-only-on-user-action",
expiresAt: Date(timeIntervalSinceNow: 3600))
final class ReadCounter: @unchecked Sendable {
var count = 0
}
let securityReadCalls = ReadCounter()
let preflightOverride: (String, String?) -> KeychainAccessPreflight.Outcome = { _, _ in
.allowed
}
func load(_ interaction: ProviderInteraction) throws -> ClaudeOAuthCredentials {
try KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting(
preflightOverride,
operation: {
try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental)
{
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
.onlyOnUserAction)
{
try ProviderInteractionContext.$current.withValue(interaction) {
try ClaudeOAuthCredentialsStore
.withSecurityCLIReadOverrideForTesting(
.dynamic { _ in
securityReadCalls.count += 1
return securityData
}) {
try ClaudeOAuthCredentialsStore.load(
environment: [:],
allowKeychainPrompt: false,
respectKeychainPromptCooldown: true)
}
}
}
}
})
}
let first = try load(.userInitiated)
#expect(first.accessToken == "security-sync-only-on-user-action")
#expect(securityReadCalls.count == 1)
let second = try load(.background)
#expect(second.accessToken == "security-sync-only-on-user-action")
#expect(securityReadCalls.count == 1)
}
}
}
}
}
}
@Test
func `experimental reader sync skips fingerprint probe after security CLI read`() {
let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)"
KeychainCacheStore.withServiceOverrideForTesting(service) {
KeychainAccessGate.withTaskOverrideForTesting(false) {
KeychainCacheStore.setTestStoreForTesting(true)
defer { KeychainCacheStore.setTestStoreForTesting(false) }
ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {
ClaudeOAuthCredentialsStore.invalidateCache()
defer { ClaudeOAuthCredentialsStore.invalidateCache() }
let securityData = self.makeCredentialsData(
accessToken: "security-sync-no-fingerprint-probe",
expiresAt: Date(timeIntervalSinceNow: 3600))
let fingerprintStore = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprintStore()
let sentinelFingerprint = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprint(
modifiedAt: 123,
createdAt: 122,
persistentRefHash: "sentinel")
let synced = ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.always) {
ProviderInteractionContext.$current.withValue(.background) {
ClaudeOAuthCredentialsStore.withClaudeKeychainFingerprintStoreOverrideForTesting(
fingerprintStore)
{
ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
data: nil,
fingerprint: sentinelFingerprint)
{
ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(
.data(securityData))
{
ClaudeOAuthCredentialsStore.syncFromClaudeKeychainWithoutPrompt(
now: Date())
}
}
}
}
}
})
#expect(synced == true)
#expect(fingerprintStore.fingerprint == nil)
}
}
}
}
@Test
func `experimental reader no prompt repair skips fingerprint probe after security CLI success`() 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) }
ClaudeOAuthCredentialsStore.invalidateCache()
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
defer {
ClaudeOAuthCredentialsStore.invalidateCache()
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
}
try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {
try ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {
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 securityData = self.makeCredentialsData(
accessToken: "security-repair-no-fingerprint-probe",
expiresAt: Date(timeIntervalSinceNow: 3600))
let fingerprintStore = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprintStore()
let sentinelFingerprint = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprint(
modifiedAt: 456,
createdAt: 455,
persistentRefHash: "sentinel")
let record = try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.always) {
try ProviderInteractionContext.$current.withValue(.background) {
try ClaudeOAuthCredentialsStore
.withClaudeKeychainFingerprintStoreOverrideForTesting(
fingerprintStore)
{
try ClaudeOAuthCredentialsStore
.withClaudeKeychainOverridesForTesting(
data: nil,
fingerprint: sentinelFingerprint)
{
try ClaudeOAuthCredentialsStore
.withSecurityCLIReadOverrideForTesting(
.data(securityData))
{
try ClaudeOAuthCredentialsStore.loadRecord(
environment: [:],
allowKeychainPrompt: false,
respectKeychainPromptCooldown: true)
}
}
}
}
}
})
#expect(record.credentials.accessToken == "security-repair-no-fingerprint-probe")
#expect(record.source == .claudeKeychain)
#expect(fingerprintStore.fingerprint == nil)
}
}
}
}
}
}
@Test
func `experimental reader load with prompt skips fingerprint probe after security CLI success`() 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) }
ClaudeOAuthCredentialsStore.invalidateCache()
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
defer {
ClaudeOAuthCredentialsStore.invalidateCache()
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
}
try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {
try ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {
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 securityData = self.makeCredentialsData(
accessToken: "security-load-with-prompt",
expiresAt: Date(timeIntervalSinceNow: 3600))
let fingerprintStore = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprintStore()
let sentinelFingerprint = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprint(
modifiedAt: 321,
createdAt: 320,
persistentRefHash: "sentinel")
let creds = try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.always) {
try ProviderInteractionContext.$current.withValue(.userInitiated) {
try ClaudeOAuthCredentialsStore
.withClaudeKeychainFingerprintStoreOverrideForTesting(
fingerprintStore)
{
try ClaudeOAuthCredentialsStore
.withClaudeKeychainOverridesForTesting(
data: nil,
fingerprint: sentinelFingerprint)
{
try ClaudeOAuthCredentialsStore
.withSecurityCLIReadOverrideForTesting(
.data(securityData))
{
try ClaudeOAuthCredentialsStore.load(
environment: [:],
allowKeychainPrompt: true,
respectKeychainPromptCooldown: false)
}
}
}
}
}
})
#expect(creds.accessToken == "security-load-with-prompt")
#expect(fingerprintStore.fingerprint == nil)
}
}
}
}
}
}
@Test
func `experimental reader load with prompt does not read when global keychain disabled`() 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) }
ClaudeOAuthCredentialsStore.invalidateCache()
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
defer {
ClaudeOAuthCredentialsStore.invalidateCache()
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 securityData = self.makeCredentialsData(
accessToken: "security-should-not-read",
expiresAt: Date(timeIntervalSinceNow: 3600))
var threwNotFound = false
final class ReadCounter: @unchecked Sendable {
var count = 0
}
let securityReadCalls = ReadCounter()
do {
_ = try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.always) {
try ProviderInteractionContext.$current.withValue(.userInitiated) {
try ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(
.dynamic { _ in
securityReadCalls.count += 1
return securityData
}) {
try ClaudeOAuthCredentialsStore.load(
environment: [:],
allowKeychainPrompt: true,
respectKeychainPromptCooldown: false)
}
}
}
})
} catch let error as ClaudeOAuthCredentialsError {
if case .notFound = error {
threwNotFound = true
} else {
throw error
}
}
#expect(threwNotFound == true)
#expect(securityReadCalls.count < 1)
}
}
}
}
}