218 lines
8.6 KiB
Swift
218 lines
8.6 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
@Suite(.serialized)
|
|
struct KeychainCacheStoreTests {
|
|
struct TestEntry: Codable, Equatable {
|
|
let value: String
|
|
let storedAt: Date
|
|
}
|
|
|
|
@Test
|
|
func `tests suppress real keychain access by default`() {
|
|
guard ProcessInfo.processInfo.environment["CODEXBAR_ALLOW_TEST_KEYCHAIN_ACCESS"] != "1" else { return }
|
|
|
|
#expect(KeychainCacheStore.canUseRealKeychainForTesting == false)
|
|
let key = KeychainCacheStore.Key(category: "test", identifier: UUID().uuidString)
|
|
let entry = TestEntry(value: "implicit", storedAt: Date(timeIntervalSince1970: 0))
|
|
|
|
KeychainCacheStore.store(key: key, entry: entry)
|
|
defer { KeychainCacheStore.clear(key: key) }
|
|
|
|
switch KeychainCacheStore.load(key: key, as: TestEntry.self) {
|
|
case let .found(loaded):
|
|
#expect(loaded == entry)
|
|
case .missing, .temporarilyUnavailable, .invalid:
|
|
#expect(Bool(false), "Expected implicit test cache entry")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `background interaction keeps real keychain cache available for no UI reads writes and deletes`() {
|
|
KeychainAccessGate.withTaskOverrideForTesting(false) {
|
|
ProviderInteractionContext.$current.withValue(.background) {
|
|
#expect(KeychainCacheStore.canUseRealKeychainForTesting == true)
|
|
#expect(KeychainCacheStore.canEnumerateOrDeleteRealKeychainForTesting == true)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `stores and loads entry`() {
|
|
KeychainCacheStore.setTestStoreForTesting(true)
|
|
defer { KeychainCacheStore.setTestStoreForTesting(false) }
|
|
|
|
let key = KeychainCacheStore.Key(category: "test", identifier: UUID().uuidString)
|
|
let storedAt = Date(timeIntervalSince1970: 0)
|
|
let entry = TestEntry(value: "alpha", storedAt: storedAt)
|
|
|
|
KeychainCacheStore.store(key: key, entry: entry)
|
|
defer { KeychainCacheStore.clear(key: key) }
|
|
|
|
switch KeychainCacheStore.load(key: key, as: TestEntry.self) {
|
|
case let .found(loaded):
|
|
#expect(loaded == entry)
|
|
case .missing, .temporarilyUnavailable, .invalid:
|
|
#expect(Bool(false), "Expected keychain cache entry")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `overwrites existing entry`() {
|
|
KeychainCacheStore.setTestStoreForTesting(true)
|
|
defer { KeychainCacheStore.setTestStoreForTesting(false) }
|
|
|
|
let key = KeychainCacheStore.Key(category: "test", identifier: UUID().uuidString)
|
|
let first = TestEntry(value: "first", storedAt: Date(timeIntervalSince1970: 1))
|
|
let second = TestEntry(value: "second", storedAt: Date(timeIntervalSince1970: 2))
|
|
|
|
KeychainCacheStore.store(key: key, entry: first)
|
|
KeychainCacheStore.store(key: key, entry: second)
|
|
defer { KeychainCacheStore.clear(key: key) }
|
|
|
|
switch KeychainCacheStore.load(key: key, as: TestEntry.self) {
|
|
case let .found(loaded):
|
|
#expect(loaded == second)
|
|
case .missing, .temporarilyUnavailable, .invalid:
|
|
#expect(Bool(false), "Expected overwritten keychain cache entry")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `clear removes entry`() {
|
|
KeychainCacheStore.setTestStoreForTesting(true)
|
|
defer { KeychainCacheStore.setTestStoreForTesting(false) }
|
|
|
|
let key = KeychainCacheStore.Key(category: "test", identifier: UUID().uuidString)
|
|
let entry = TestEntry(value: "gone", storedAt: Date(timeIntervalSince1970: 0))
|
|
|
|
KeychainCacheStore.store(key: key, entry: entry)
|
|
KeychainCacheStore.clear(key: key)
|
|
|
|
switch KeychainCacheStore.load(key: key, as: TestEntry.self) {
|
|
case .missing:
|
|
#expect(true)
|
|
case .found, .temporarilyUnavailable, .invalid:
|
|
#expect(Bool(false), "Expected keychain cache entry to be cleared")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `clear reports whether an entry was removed`() {
|
|
KeychainCacheStore.setTestStoreForTesting(true)
|
|
defer { KeychainCacheStore.setTestStoreForTesting(false) }
|
|
|
|
let key = KeychainCacheStore.Key(category: "test", identifier: UUID().uuidString)
|
|
let entry = TestEntry(value: "gone", storedAt: Date(timeIntervalSince1970: 0))
|
|
KeychainCacheStore.store(key: key, entry: entry)
|
|
|
|
#expect(KeychainCacheStore.clear(key: key) == true)
|
|
#expect(KeychainCacheStore.clear(key: key) == false)
|
|
}
|
|
|
|
@Test
|
|
func `keys lists only matching category for current service`() {
|
|
KeychainCacheStore.setTestStoreForTesting(true)
|
|
defer { KeychainCacheStore.setTestStoreForTesting(false) }
|
|
|
|
let serviceA = "cache-keys-a-\(UUID().uuidString)"
|
|
let serviceB = "cache-keys-b-\(UUID().uuidString)"
|
|
let cookieA = KeychainCacheStore.Key(category: "cookie", identifier: "codex")
|
|
let scopedCookieA = KeychainCacheStore.Key(category: "cookie", identifier: "codex.managed.account")
|
|
let oauthA = KeychainCacheStore.Key(category: "oauth", identifier: "codex")
|
|
let cookieB = KeychainCacheStore.Key(category: "cookie", identifier: "claude")
|
|
let entry = TestEntry(value: "value", storedAt: Date(timeIntervalSince1970: 0))
|
|
|
|
KeychainCacheStore.withServiceOverrideForTesting(serviceA) {
|
|
KeychainCacheStore.store(key: cookieA, entry: entry)
|
|
KeychainCacheStore.store(key: scopedCookieA, entry: entry)
|
|
KeychainCacheStore.store(key: oauthA, entry: entry)
|
|
}
|
|
KeychainCacheStore.withServiceOverrideForTesting(serviceB) {
|
|
KeychainCacheStore.store(key: cookieB, entry: entry)
|
|
}
|
|
|
|
let keys = KeychainCacheStore.withServiceOverrideForTesting(serviceA) {
|
|
KeychainCacheStore.keys(category: "cookie")
|
|
}
|
|
|
|
#expect(keys == [cookieA, scopedCookieA])
|
|
}
|
|
|
|
#if os(macOS)
|
|
@Test
|
|
func `interaction not allowed is treated as temporarily unavailable`() {
|
|
let key = KeychainCacheStore.Key(category: "test", identifier: UUID().uuidString)
|
|
let result: KeychainCacheStore.LoadResult<TestEntry> = KeychainCacheStore.loadResultForKeychainReadFailure(
|
|
status: errSecInteractionNotAllowed,
|
|
key: key)
|
|
|
|
switch result {
|
|
case .temporarilyUnavailable:
|
|
#expect(true)
|
|
case .found, .missing, .invalid:
|
|
#expect(Bool(false), "Expected temporary keychain lock to be retry-later")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `delete interaction not allowed is non fatal`() {
|
|
let key = KeychainCacheStore.Key(category: "test", identifier: UUID().uuidString)
|
|
#expect(KeychainCacheStore.clearResultForKeychainDeleteStatus(
|
|
errSecInteractionNotAllowed,
|
|
key: key) == .failed)
|
|
}
|
|
|
|
@Test
|
|
func `load failure override bypasses test store without affecting store or clear`() {
|
|
KeychainCacheStore.setTestStoreForTesting(true)
|
|
defer { KeychainCacheStore.setTestStoreForTesting(false) }
|
|
|
|
let key = KeychainCacheStore.Key(category: "test", identifier: UUID().uuidString)
|
|
let entry = TestEntry(value: "stored", storedAt: Date(timeIntervalSince1970: 0))
|
|
KeychainCacheStore.store(key: key, entry: entry)
|
|
defer { KeychainCacheStore.clear(key: key) }
|
|
|
|
KeychainCacheStore.withLoadFailureStatusOverrideForTesting(errSecInteractionNotAllowed) {
|
|
switch KeychainCacheStore.load(key: key, as: TestEntry.self) {
|
|
case .temporarilyUnavailable:
|
|
#expect(true)
|
|
case .found, .missing, .invalid:
|
|
#expect(Bool(false), "Expected override to run before test store")
|
|
}
|
|
}
|
|
|
|
switch KeychainCacheStore.load(key: key, as: TestEntry.self) {
|
|
case let .found(loaded):
|
|
#expect(loaded == entry)
|
|
case .missing, .temporarilyUnavailable, .invalid:
|
|
#expect(Bool(false), "Expected override not to mutate test store")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `cache ACL trusts bundled app and CLI helper`() {
|
|
let root = URL(fileURLWithPath: "/Applications/CodexBar.app")
|
|
let executable = root.appendingPathComponent("Contents/MacOS/CodexBar")
|
|
let helper = root.appendingPathComponent("Contents/Helpers/CodexBarCLI")
|
|
let existing = Set([
|
|
root.path,
|
|
executable.path,
|
|
helper.path,
|
|
])
|
|
|
|
let paths = KeychainCacheStore.trustedApplicationPathsForCacheAccess(
|
|
bundleURL: root,
|
|
executableURL: executable,
|
|
fileExists: { existing.contains($0) })
|
|
|
|
#expect(paths == [
|
|
root.path,
|
|
helper.path,
|
|
executable.path,
|
|
])
|
|
}
|
|
#endif
|
|
}
|