106 lines
5.5 KiB
Swift
106 lines
5.5 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
#if os(macOS)
|
|
@Suite(.serialized)
|
|
struct ClaudeOAuthCredentialsStoreMCPOnlyGuardTests {
|
|
@Test
|
|
func `standard reader blocks expired CLI owner in background but preserves user refresh`() async throws {
|
|
let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)"
|
|
let mcpOAuthOnly = Data(#"{"mcpOAuth":{"plugin:test":{"accessToken":"synthetic"}}}"#.utf8)
|
|
let tempDir = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: tempDir) }
|
|
let credentialsURL = tempDir.appendingPathComponent("credentials.json")
|
|
|
|
await KeychainCacheStore.withServiceOverrideForTesting(service) {
|
|
KeychainCacheStore.setTestStoreForTesting(true)
|
|
defer { KeychainCacheStore.setTestStoreForTesting(false) }
|
|
|
|
ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting()
|
|
defer { ClaudeOAuthCredentialsStore._resetCredentialsFileTrackingForTesting() }
|
|
|
|
await KeychainAccessGate.withTaskOverrideForTesting(false) {
|
|
await ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(.securityFramework) {
|
|
await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) {
|
|
await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(credentialsURL) {
|
|
await ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
|
|
data: mcpOAuthOnly,
|
|
fingerprint: nil)
|
|
{
|
|
let isMcpOnly = ProviderInteractionContext.$current.withValue(.background) {
|
|
ClaudeOAuthCredentialsStore.isMcpOAuthOnlyClaudeKeychainPayloadPresent(
|
|
interaction: .background,
|
|
readStrategy: .securityFramework,
|
|
keychainAccessDisabled: false,
|
|
environment: [:])
|
|
}
|
|
#expect(isMcpOnly)
|
|
|
|
ClaudeOAuthCredentialsStore.invalidateCache()
|
|
let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude)
|
|
defer { KeychainCacheStore.clear(key: cacheKey) }
|
|
KeychainCacheStore.store(
|
|
key: cacheKey,
|
|
entry: ClaudeOAuthCredentialsStore.CacheEntry(
|
|
data: self.expiredCredentialsData,
|
|
storedAt: Date(),
|
|
owner: .claudeCLI))
|
|
|
|
do {
|
|
_ = try await ClaudeOAuthCredentialsStore.loadWithAutoRefresh(
|
|
environment: [:],
|
|
allowKeychainPrompt: false,
|
|
respectKeychainPromptCooldown: true)
|
|
Issue.record("Expected MCP-only keychain failure")
|
|
} catch let error as ClaudeOAuthCredentialsError {
|
|
guard case .mcpOAuthOnlyKeychain = error else {
|
|
Issue.record("Expected .mcpOAuthOnlyKeychain, got \(error)")
|
|
return
|
|
}
|
|
} catch {
|
|
Issue.record("Expected ClaudeOAuthCredentialsError, got \(error)")
|
|
}
|
|
|
|
do {
|
|
_ = try await ProviderInteractionContext.$current.withValue(.userInitiated) {
|
|
try await ClaudeOAuthCredentialsStore.loadWithAutoRefresh(
|
|
environment: [:],
|
|
allowKeychainPrompt: false,
|
|
respectKeychainPromptCooldown: true)
|
|
}
|
|
Issue.record("Expected explicit user Refresh to delegate")
|
|
} catch let error as ClaudeOAuthCredentialsError {
|
|
guard case .refreshDelegatedToClaudeCLI = error else {
|
|
Issue.record("Expected .refreshDelegatedToClaudeCLI, got \(error)")
|
|
return
|
|
}
|
|
} catch {
|
|
Issue.record("Expected ClaudeOAuthCredentialsError, got \(error)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var expiredCredentialsData: Data {
|
|
let json = #"""
|
|
{
|
|
"claudeAiOauth": {
|
|
"accessToken": "expired",
|
|
"refreshToken": "refresh",
|
|
"expiresAt": 1000,
|
|
"scopes": ["user:profile"]
|
|
}
|
|
}
|
|
"""#
|
|
return Data(json.utf8)
|
|
}
|
|
}
|
|
#endif
|