chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import CodexBarCore
|
||||
|
||||
@Suite(.serialized)
|
||||
struct ClaudeOAuthCredentialsStoreTemporaryKeychainCacheTests {
|
||||
private struct WrongCacheEntry: Codable {
|
||||
let value: String
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
@Test
|
||||
func `credentials file invalidation preserves keychain cache when temporarily unavailable`() throws {
|
||||
let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)"
|
||||
try KeychainCacheStore.withServiceOverrideForTesting(service) {
|
||||
KeychainCacheStore.setTestStoreForTesting(true)
|
||||
defer { KeychainCacheStore.setTestStoreForTesting(false) }
|
||||
|
||||
try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {
|
||||
try ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {
|
||||
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 firstFile = self.makeCredentialsData(
|
||||
accessToken: "first-file",
|
||||
expiresAt: Date(timeIntervalSinceNow: 3600))
|
||||
try firstFile.write(to: fileURL)
|
||||
#expect(ClaudeOAuthCredentialsStore.invalidateCacheIfCredentialsFileChanged())
|
||||
|
||||
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(),
|
||||
owner: .claudeCLI))
|
||||
defer { KeychainCacheStore.clear(key: cacheKey) }
|
||||
|
||||
let updatedFile = self.makeCredentialsData(
|
||||
accessToken: "updated-file-token-longer",
|
||||
expiresAt: Date(timeIntervalSinceNow: 3600))
|
||||
try updatedFile.write(to: fileURL)
|
||||
|
||||
KeychainCacheStore.withLoadFailureStatusOverrideForTesting(errSecInteractionNotAllowed) {
|
||||
#expect(ClaudeOAuthCredentialsStore.invalidateCacheIfCredentialsFileChanged())
|
||||
}
|
||||
|
||||
switch KeychainCacheStore.load(
|
||||
key: cacheKey,
|
||||
as: ClaudeOAuthCredentialsStore.CacheEntry.self)
|
||||
{
|
||||
case let .found(entry):
|
||||
let parsed = try ClaudeOAuthCredentials.parse(data: entry.data)
|
||||
#expect(parsed.accessToken == "cached-token")
|
||||
case .missing, .temporarilyUnavailable, .invalid:
|
||||
#expect(Bool(false), "Expected temporary unavailability not to clear Claude cache")
|
||||
}
|
||||
|
||||
#expect(ClaudeOAuthCredentialsStore.invalidateCacheIfCredentialsFileChanged())
|
||||
|
||||
switch KeychainCacheStore.load(
|
||||
key: cacheKey,
|
||||
as: ClaudeOAuthCredentialsStore.CacheEntry.self)
|
||||
{
|
||||
case .missing:
|
||||
#expect(true)
|
||||
case .found, .temporarilyUnavailable, .invalid:
|
||||
#expect(Bool(false), "Expected pending invalidation to clear stale Claude cache")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `temporary keychain cache unavailability does not overwrite cache from credentials file fallback`() 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 {
|
||||
try ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {
|
||||
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 fileData = self.makeCredentialsData(
|
||||
accessToken: "file-fallback-token",
|
||||
expiresAt: Date(timeIntervalSinceNow: 3600))
|
||||
try fileData.write(to: fileURL)
|
||||
|
||||
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(),
|
||||
owner: .claudeCLI))
|
||||
defer { KeychainCacheStore.clear(key: cacheKey) }
|
||||
|
||||
let loaded = try KeychainCacheStore.withLoadFailureStatusOverrideForTesting(
|
||||
errSecInteractionNotAllowed)
|
||||
{
|
||||
try ClaudeOAuthCredentialsStore.load(environment: [:], allowKeychainPrompt: false)
|
||||
}
|
||||
#expect(loaded.accessToken == "file-fallback-token")
|
||||
|
||||
switch KeychainCacheStore.load(
|
||||
key: cacheKey,
|
||||
as: ClaudeOAuthCredentialsStore.CacheEntry.self)
|
||||
{
|
||||
case let .found(entry):
|
||||
let parsed = try ClaudeOAuthCredentials.parse(data: entry.data)
|
||||
#expect(parsed.accessToken == "cached-token")
|
||||
case .missing, .temporarilyUnavailable, .invalid:
|
||||
#expect(Bool(false), "Expected file fallback not to overwrite unavailable cache")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `has cached credentials treats temporary keychain cache unavailability as present`() {
|
||||
let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)"
|
||||
KeychainCacheStore.withServiceOverrideForTesting(service) {
|
||||
KeychainCacheStore.setTestStoreForTesting(true)
|
||||
defer { KeychainCacheStore.setTestStoreForTesting(false) }
|
||||
|
||||
ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {
|
||||
ClaudeOAuthCredentialsStore.invalidateCache()
|
||||
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()))
|
||||
defer { KeychainCacheStore.clear(key: cacheKey) }
|
||||
|
||||
let hasCached = KeychainCacheStore.withLoadFailureStatusOverrideForTesting(
|
||||
errSecInteractionNotAllowed)
|
||||
{
|
||||
ClaudeOAuthCredentialsStore.hasCachedCredentials(environment: [:])
|
||||
}
|
||||
|
||||
#expect(hasCached == true)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@Test
|
||||
func `invalid keychain cache is cleared by load`() 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._resetCredentialsFileTrackingForTesting()
|
||||
defer { 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 cacheKey = KeychainCacheStore.Key.oauth(provider: .claude)
|
||||
KeychainCacheStore.store(key: cacheKey, entry: WrongCacheEntry(value: "wrong-shape"))
|
||||
|
||||
do {
|
||||
_ = 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
|
||||
}
|
||||
}
|
||||
|
||||
switch KeychainCacheStore.load(
|
||||
key: cacheKey,
|
||||
as: ClaudeOAuthCredentialsStore.CacheEntry.self)
|
||||
{
|
||||
case .missing:
|
||||
#expect(true)
|
||||
case .found, .temporarilyUnavailable, .invalid:
|
||||
#expect(Bool(false), "Expected invalid Claude cache to be cleared")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user