@@ -17,14 +17,65 @@ protocol KeychainServiceProtocol: Sendable {
1717 func deleteAll( ) throws
1818}
1919
20+ private final class TestKeychainStore : @unchecked Sendable {
21+ static let shared = TestKeychainStore ( )
22+
23+ private let lock = NSLock ( )
24+ private var storage : [ String : [ String : Data ] ] = [ : ]
25+
26+ func save( service: String , key: String , data: Data ) {
27+ lock. lock ( )
28+ defer { lock. unlock ( ) }
29+
30+ var serviceStorage = storage [ service] ?? [ : ]
31+ serviceStorage [ key] = data
32+ storage [ service] = serviceStorage
33+ }
34+
35+ func read( service: String , key: String ) -> Data ? {
36+ lock. lock ( )
37+ defer { lock. unlock ( ) }
38+
39+ return storage [ service] ? [ key]
40+ }
41+
42+ func delete( service: String , key: String ) {
43+ lock. lock ( )
44+ defer { lock. unlock ( ) }
45+
46+ storage [ service] ? [ key] = nil
47+ if storage [ service] ? . isEmpty == true {
48+ storage [ service] = nil
49+ }
50+ }
51+
52+ func deleteAll( service: String ) {
53+ lock. lock ( )
54+ defer { lock. unlock ( ) }
55+
56+ storage [ service] = nil
57+ }
58+ }
59+
2060struct KeychainService : KeychainServiceProtocol , Sendable {
2161 let service : String
62+ private let testStore : TestKeychainStore ?
2263
23- init ( service: String = Constants . Keychain. service) {
64+ init (
65+ service: String = Constants . Keychain. service,
66+ environment: [ String : String ] = ProcessInfo . processInfo. environment
67+ ) {
2468 self . service = service
69+ // Hosted unit tests launch the app target, so avoid touching the login keychain entirely.
70+ self . testStore = environment [ " XCTestConfigurationFilePath " ] == nil ? nil : . shared
2571 }
2672
2773 func save( key: String , data: Data ) throws {
74+ if let testStore {
75+ testStore. save ( service: service, key: key, data: data)
76+ return
77+ }
78+
2879 // Delete existing item first
2980 let deleteQuery : [ String : Any ] = [
3081 kSecClass as String : kSecClassGenericPassword,
@@ -55,6 +106,10 @@ struct KeychainService: KeychainServiceProtocol, Sendable {
55106 }
56107
57108 func read( key: String ) throws -> Data ? {
109+ if let testStore {
110+ return testStore. read ( service: service, key: key)
111+ }
112+
58113 let query : [ String : Any ] = [
59114 kSecClass as String : kSecClassGenericPassword,
60115 kSecAttrService as String : service,
@@ -83,6 +138,11 @@ struct KeychainService: KeychainServiceProtocol, Sendable {
83138 }
84139
85140 func delete( key: String ) throws {
141+ if let testStore {
142+ testStore. delete ( service: service, key: key)
143+ return
144+ }
145+
86146 let query : [ String : Any ] = [
87147 kSecClass as String : kSecClassGenericPassword,
88148 kSecAttrService as String : service,
@@ -96,6 +156,11 @@ struct KeychainService: KeychainServiceProtocol, Sendable {
96156 }
97157
98158 func deleteAll( ) throws {
159+ if let testStore {
160+ testStore. deleteAll ( service: service)
161+ return
162+ }
163+
99164 // Delete known keys individually for reliability across macOS versions
100165 for key in [ Constants . Keychain. accessTokenKey, Constants . Keychain. refreshTokenKey, Constants . Keychain. expirationKey] {
101166 try delete ( key: key)
0 commit comments