110 lines
5.1 KiB
Swift
110 lines
5.1 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
@Suite(.serialized)
|
|
struct ClaudeOAuthCredentialsStoreSecurityCLIFallbackPolicyTests {
|
|
private func makeCredentialsData(accessToken: String, expiresAt: Date) -> Data {
|
|
let millis = Int(expiresAt.timeIntervalSince1970 * 1000)
|
|
let json = """
|
|
{
|
|
"claudeAiOauth": {
|
|
"accessToken": "\(accessToken)",
|
|
"expiresAt": \(millis),
|
|
"scopes": ["user:profile"]
|
|
}
|
|
}
|
|
"""
|
|
return Data(json.utf8)
|
|
}
|
|
|
|
@Test
|
|
func `experimental reader blocks background fallback per stored policy`() {
|
|
let fallbackData = self.makeCredentialsData(
|
|
accessToken: "fallback-should-be-blocked",
|
|
expiresAt: Date(timeIntervalSinceNow: 3600))
|
|
|
|
let hasCredentials = KeychainAccessGate.withTaskOverrideForTesting(false) {
|
|
ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
|
|
.securityCLIExperimental,
|
|
operation: {
|
|
ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
|
|
.onlyOnUserAction,
|
|
operation: {
|
|
ProviderInteractionContext.$current.withValue(.background) {
|
|
ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
|
|
data: fallbackData,
|
|
fingerprint: nil)
|
|
{
|
|
ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(
|
|
.nonZeroExit)
|
|
{
|
|
ClaudeOAuthCredentialsStore.hasClaudeKeychainCredentialsWithoutPrompt()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
#expect(hasCredentials == false)
|
|
}
|
|
|
|
@Test
|
|
func `experimental reader sync from claude keychain without prompt background fallback blocked by stored policy`() {
|
|
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 fallbackData = self.makeCredentialsData(
|
|
accessToken: "sync-fallback-should-be-blocked",
|
|
expiresAt: Date(timeIntervalSinceNow: 3600))
|
|
final class Counter: @unchecked Sendable {
|
|
var value = 0
|
|
}
|
|
let preflightCalls = Counter()
|
|
let preflightOverride: (String, String?) -> KeychainAccessPreflight.Outcome = { _, _ in
|
|
preflightCalls.value += 1
|
|
return .allowed
|
|
}
|
|
|
|
let synced = KeychainAccessPreflight.withCheckGenericPasswordOverrideForTesting(
|
|
preflightOverride,
|
|
operation: {
|
|
ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
|
|
.securityCLIExperimental,
|
|
operation: {
|
|
ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
|
|
.onlyOnUserAction,
|
|
operation: {
|
|
ProviderInteractionContext.$current.withValue(.background) {
|
|
ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
|
|
data: fallbackData,
|
|
fingerprint: nil)
|
|
{
|
|
ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(
|
|
.timedOut)
|
|
{
|
|
ClaudeOAuthCredentialsStore
|
|
.syncFromClaudeKeychainWithoutPrompt(now: Date())
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
#expect(synced == false)
|
|
#expect(preflightCalls.value == 0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|