743 lines
42 KiB
Swift
743 lines
42 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
@Suite(.serialized)
|
|
struct ClaudeOAuthCredentialsStorePromptPolicyTests {
|
|
@Test
|
|
func `keychain prompt notify preserves its void function signature`() {
|
|
let notify: (KeychainPromptContext) -> Void = KeychainPromptHandler.notify
|
|
_ = notify
|
|
}
|
|
|
|
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 `does not read claude keychain in background when prompt mode 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) }
|
|
|
|
ClaudeOAuthCredentialsStore.invalidateCache()
|
|
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
|
|
defer {
|
|
ClaudeOAuthCredentialsStore.invalidateCache()
|
|
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
|
|
ClaudeOAuthCredentialsStore._resetClaudeKeychainChangeTrackingForTesting()
|
|
}
|
|
|
|
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._resetClaudeKeychainChangeTrackingForTesting()
|
|
|
|
let fingerprint = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprint(
|
|
modifiedAt: 1,
|
|
createdAt: 1,
|
|
persistentRefHash: "ref1")
|
|
let keychainData = self.makeCredentialsData(
|
|
accessToken: "keychain-token",
|
|
expiresAt: Date(timeIntervalSinceNow: 3600))
|
|
|
|
do {
|
|
_ = try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) {
|
|
try ProviderInteractionContext.$current.withValue(.background) {
|
|
try ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
|
|
data: keychainData,
|
|
fingerprint: fingerprint)
|
|
{
|
|
try ClaudeOAuthCredentialsStore.load(environment: [:], allowKeychainPrompt: false)
|
|
}
|
|
}
|
|
}
|
|
Issue.record("Expected ClaudeOAuthCredentialsError.notFound")
|
|
} catch let error as ClaudeOAuthCredentialsError {
|
|
guard case .notFound = error else {
|
|
Issue.record("Expected .notFound, got \(error)")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `can read claude keychain on user action when prompt mode 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) }
|
|
|
|
ClaudeOAuthCredentialsStore.invalidateCache()
|
|
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
|
|
defer {
|
|
ClaudeOAuthCredentialsStore.invalidateCache()
|
|
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
|
|
ClaudeOAuthCredentialsStore._resetClaudeKeychainChangeTrackingForTesting()
|
|
}
|
|
|
|
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._resetClaudeKeychainChangeTrackingForTesting()
|
|
|
|
let fingerprint = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprint(
|
|
modifiedAt: 1,
|
|
createdAt: 1,
|
|
persistentRefHash: "ref1")
|
|
let keychainData = self.makeCredentialsData(
|
|
accessToken: "keychain-token",
|
|
expiresAt: Date(timeIntervalSinceNow: 3600))
|
|
|
|
let creds = try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) {
|
|
try ProviderInteractionContext.$current.withValue(.userInitiated) {
|
|
try ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
|
|
data: keychainData,
|
|
fingerprint: fingerprint)
|
|
{
|
|
try ClaudeOAuthCredentialsStore.load(environment: [:], allowKeychainPrompt: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
#expect(creds.accessToken == "keychain-token")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `user initiated claude keychain reads respect pre alert acknowledgement cooldown`() 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 {
|
|
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 keychainData = self.makeCredentialsData(
|
|
accessToken: "keychain-token",
|
|
expiresAt: Date(timeIntervalSinceNow: 3600))
|
|
|
|
var preAlertHits = 0
|
|
let preflightOverride: (String, String?) -> KeychainAccessPreflight.Outcome = { _, _ in
|
|
.allowed
|
|
}
|
|
let promptHandler: (KeychainPromptContext) -> Void = { _ in
|
|
preAlertHits += 1
|
|
}
|
|
let credentials = try KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting(
|
|
preflightOverride,
|
|
operation: {
|
|
try KeychainPromptHandler.withHandlerForTesting(promptHandler, operation: {
|
|
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
|
|
.onlyOnUserAction)
|
|
{
|
|
try ProviderInteractionContext.$current.withValue(.userInitiated) {
|
|
try ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
|
|
data: keychainData,
|
|
fingerprint: nil)
|
|
{
|
|
let first = try ClaudeOAuthCredentialsStore.load(
|
|
environment: [:],
|
|
allowKeychainPrompt: true)
|
|
ClaudeOAuthCredentialsStore.invalidateCache()
|
|
let second = try ClaudeOAuthCredentialsStore.load(
|
|
environment: [:],
|
|
allowKeychainPrompt: true)
|
|
return (first, second)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
#expect(credentials.0.accessToken == "keychain-token")
|
|
#expect(credentials.1.accessToken == "keychain-token")
|
|
#expect(preAlertHits == 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `shows pre alert when claude keychain likely 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 {
|
|
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 keychainData = self.makeCredentialsData(
|
|
accessToken: "keychain-token",
|
|
expiresAt: Date(timeIntervalSinceNow: 3600))
|
|
|
|
var preAlertHits = 0
|
|
let preflightOverride: (String, String?) -> KeychainAccessPreflight.Outcome = { _, _ in
|
|
.interactionRequired
|
|
}
|
|
let promptHandler: (KeychainPromptContext) -> Void = { _ in
|
|
preAlertHits += 1
|
|
}
|
|
let creds = try KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting(
|
|
preflightOverride,
|
|
operation: {
|
|
try KeychainPromptHandler.withHandlerForTesting(promptHandler, operation: {
|
|
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
|
|
.onlyOnUserAction)
|
|
{
|
|
try ProviderInteractionContext.$current.withValue(.userInitiated) {
|
|
try ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
|
|
data: keychainData,
|
|
fingerprint: nil)
|
|
{
|
|
try ClaudeOAuthCredentialsStore.load(
|
|
environment: [:],
|
|
allowKeychainPrompt: true)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
#expect(creds.accessToken == "keychain-token")
|
|
#expect(preAlertHits == 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `shows pre alert when claude keychain preflight fails`() 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 {
|
|
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 keychainData = self.makeCredentialsData(
|
|
accessToken: "keychain-token",
|
|
expiresAt: Date(timeIntervalSinceNow: 3600))
|
|
|
|
var preAlertHits = 0
|
|
let preflightOverride: (String, String?) -> KeychainAccessPreflight.Outcome = { _, _ in
|
|
.failure(-1)
|
|
}
|
|
let promptHandler: (KeychainPromptContext) -> Void = { _ in
|
|
preAlertHits += 1
|
|
}
|
|
let creds = try KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting(
|
|
preflightOverride,
|
|
operation: {
|
|
try KeychainPromptHandler.withHandlerForTesting(promptHandler, operation: {
|
|
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
|
|
.onlyOnUserAction)
|
|
{
|
|
try ProviderInteractionContext.$current.withValue(.userInitiated) {
|
|
try ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
|
|
data: keychainData,
|
|
fingerprint: nil)
|
|
{
|
|
try ClaudeOAuthCredentialsStore.load(
|
|
environment: [:],
|
|
allowKeychainPrompt: true)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
#expect(creds.accessToken == "keychain-token")
|
|
#expect(preAlertHits == 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `experimental reader skips pre alert when security CLI read succeeds`() 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 {
|
|
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-token",
|
|
expiresAt: Date(timeIntervalSinceNow: 3600))
|
|
|
|
var preAlertHits = 0
|
|
let preflightOverride: (String, String?) -> KeychainAccessPreflight.Outcome = { _, _ in
|
|
.interactionRequired
|
|
}
|
|
let promptHandler: (KeychainPromptContext) -> Void = { _ in
|
|
preAlertHits += 1
|
|
}
|
|
let creds = try KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting(
|
|
preflightOverride,
|
|
operation: {
|
|
try KeychainPromptHandler.withHandlerForTesting(promptHandler, operation: {
|
|
try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
|
|
.securityCLIExperimental)
|
|
{
|
|
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.always) {
|
|
try ProviderInteractionContext.$current.withValue(.userInitiated) {
|
|
try ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(
|
|
.data(securityData))
|
|
{
|
|
try ClaudeOAuthCredentialsStore.load(
|
|
environment: [:],
|
|
allowKeychainPrompt: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
#expect(creds.accessToken == "security-token")
|
|
#expect(preAlertHits == 0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `experimental reader shows pre alert when security CLI fails and fallback needs 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 {
|
|
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 fallbackData = self.makeCredentialsData(
|
|
accessToken: "fallback-token",
|
|
expiresAt: Date(timeIntervalSinceNow: 3600))
|
|
|
|
var preAlertHits = 0
|
|
let preflightOverride: (String, String?) -> KeychainAccessPreflight.Outcome = { _, _ in
|
|
.interactionRequired
|
|
}
|
|
let promptHandler: (KeychainPromptContext) -> Void = { _ in
|
|
preAlertHits += 1
|
|
}
|
|
let creds = try KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting(
|
|
preflightOverride,
|
|
operation: {
|
|
try KeychainPromptHandler.withHandlerForTesting(promptHandler, operation: {
|
|
try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
|
|
.securityCLIExperimental)
|
|
{
|
|
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.always) {
|
|
try ProviderInteractionContext.$current.withValue(.userInitiated) {
|
|
try ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
|
|
data: fallbackData,
|
|
fingerprint: nil)
|
|
{
|
|
try ClaudeOAuthCredentialsStore
|
|
.withSecurityCLIReadOverrideForTesting(.timedOut) {
|
|
try ClaudeOAuthCredentialsStore.load(
|
|
environment: [:],
|
|
allowKeychainPrompt: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
#expect(creds.accessToken == "fallback-token")
|
|
#expect(preAlertHits == 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `experimental reader does not fallback in background when stored mode 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 {
|
|
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 fallbackData = self.makeCredentialsData(
|
|
accessToken: "fallback-token",
|
|
expiresAt: Date(timeIntervalSinceNow: 3600))
|
|
|
|
var preAlertHits = 0
|
|
let preflightOverride: (String, String?) -> KeychainAccessPreflight.Outcome = { _, _ in
|
|
.interactionRequired
|
|
}
|
|
let promptHandler: (KeychainPromptContext) -> Void = { _ in
|
|
preAlertHits += 1
|
|
}
|
|
|
|
do {
|
|
_ = try KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting(
|
|
preflightOverride,
|
|
operation: {
|
|
try KeychainPromptHandler.withHandlerForTesting(promptHandler, operation: {
|
|
try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
|
|
.securityCLIExperimental)
|
|
{
|
|
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
|
|
.onlyOnUserAction)
|
|
{
|
|
try ProviderInteractionContext.$current.withValue(.background) {
|
|
try ClaudeOAuthCredentialsStore
|
|
.withClaudeKeychainOverridesForTesting(
|
|
data: fallbackData,
|
|
fingerprint: nil)
|
|
{
|
|
try ClaudeOAuthCredentialsStore
|
|
.withSecurityCLIReadOverrideForTesting(.timedOut) {
|
|
try ClaudeOAuthCredentialsStore.load(
|
|
environment: [:],
|
|
allowKeychainPrompt: true,
|
|
respectKeychainPromptCooldown: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
Issue.record("Expected ClaudeOAuthCredentialsError.notFound")
|
|
} catch let error as ClaudeOAuthCredentialsError {
|
|
guard case .notFound = error else {
|
|
Issue.record("Expected .notFound, got \(error)")
|
|
return
|
|
}
|
|
}
|
|
|
|
#expect(preAlertHits == 0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `experimental reader does not fallback when stored mode never`() 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 {
|
|
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 fallbackData = self.makeCredentialsData(
|
|
accessToken: "fallback-token",
|
|
expiresAt: Date(timeIntervalSinceNow: 3600))
|
|
|
|
var preAlertHits = 0
|
|
let preflightOverride: (String, String?) -> KeychainAccessPreflight.Outcome = { _, _ in
|
|
.interactionRequired
|
|
}
|
|
let promptHandler: (KeychainPromptContext) -> Void = { _ in
|
|
preAlertHits += 1
|
|
}
|
|
|
|
do {
|
|
_ = try KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting(
|
|
preflightOverride,
|
|
operation: {
|
|
try KeychainPromptHandler.withHandlerForTesting(promptHandler, operation: {
|
|
try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
|
|
.securityCLIExperimental)
|
|
{
|
|
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.never) {
|
|
try ProviderInteractionContext.$current.withValue(.userInitiated) {
|
|
try ClaudeOAuthCredentialsStore
|
|
.withClaudeKeychainOverridesForTesting(
|
|
data: fallbackData,
|
|
fingerprint: nil)
|
|
{
|
|
try ClaudeOAuthCredentialsStore
|
|
.withSecurityCLIReadOverrideForTesting(.timedOut) {
|
|
try ClaudeOAuthCredentialsStore.load(
|
|
environment: [:],
|
|
allowKeychainPrompt: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
Issue.record("Expected ClaudeOAuthCredentialsError.notFound")
|
|
} catch let error as ClaudeOAuthCredentialsError {
|
|
guard case .notFound = error else {
|
|
Issue.record("Expected .notFound, got \(error)")
|
|
return
|
|
}
|
|
}
|
|
|
|
#expect(preAlertHits == 0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `experimental reader non interactive fallback blocked in background when stored mode 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 {
|
|
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 fallbackData = self.makeCredentialsData(
|
|
accessToken: "fallback-token-only-on-user-action",
|
|
expiresAt: Date(timeIntervalSinceNow: 3600))
|
|
let preflightOverride: (String, String?) -> KeychainAccessPreflight.Outcome = { _, _ in
|
|
.allowed
|
|
}
|
|
|
|
do {
|
|
_ = try KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting(
|
|
preflightOverride,
|
|
operation: {
|
|
try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
|
|
.securityCLIExperimental)
|
|
{
|
|
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
|
|
.onlyOnUserAction)
|
|
{
|
|
try ProviderInteractionContext.$current.withValue(.background) {
|
|
try ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
|
|
data: fallbackData,
|
|
fingerprint: nil)
|
|
{
|
|
try ClaudeOAuthCredentialsStore
|
|
.withSecurityCLIReadOverrideForTesting(.timedOut) {
|
|
try ClaudeOAuthCredentialsStore.load(
|
|
environment: [:],
|
|
allowKeychainPrompt: false,
|
|
respectKeychainPromptCooldown: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
Issue.record("Expected ClaudeOAuthCredentialsError.notFound")
|
|
} catch let error as ClaudeOAuthCredentialsError {
|
|
guard case .notFound = error else {
|
|
Issue.record("Expected .notFound, got \(error)")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `experimental reader allows fallback in background when stored mode always`() 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 {
|
|
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 fallbackData = self.makeCredentialsData(
|
|
accessToken: "fallback-token",
|
|
expiresAt: Date(timeIntervalSinceNow: 3600))
|
|
|
|
var preAlertHits = 0
|
|
let preflightOverride: (String, String?) -> KeychainAccessPreflight.Outcome = { _, _ in
|
|
.interactionRequired
|
|
}
|
|
let promptHandler: (KeychainPromptContext) -> Void = { _ in
|
|
preAlertHits += 1
|
|
}
|
|
|
|
let creds = try KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting(
|
|
preflightOverride,
|
|
operation: {
|
|
try KeychainPromptHandler.withHandlerForTesting(promptHandler, operation: {
|
|
try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
|
|
.securityCLIExperimental)
|
|
{
|
|
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.always) {
|
|
try ProviderInteractionContext.$current.withValue(.background) {
|
|
try ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
|
|
data: fallbackData,
|
|
fingerprint: nil)
|
|
{
|
|
try ClaudeOAuthCredentialsStore
|
|
.withSecurityCLIReadOverrideForTesting(.timedOut) {
|
|
try ClaudeOAuthCredentialsStore.load(
|
|
environment: [:],
|
|
allowKeychainPrompt: true,
|
|
respectKeychainPromptCooldown: false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
#expect(creds.accessToken == "fallback-token")
|
|
#expect(preAlertHits == 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|