import Foundation import Testing @testable import ClientTLS // C-iOS · The keychain-replace helper backs the rotation scheduler's identity // updates. Its one job: a crash/failure mid-write must NEVER leave zero installed // items (which would lock the device out of its own mTLS identity until a manual // re-enroll). It enforces ADD-new-BEFORE-DELETE-old ordering — the inverse of the // delete-then-add sequence whose window has zero items installed. /// A tiny in-memory stand-in for the keychain: the current set of installed item /// ids plus the smallest count ever observed, so a test can assert the invariant /// that the installed set is NEVER empty at any step of the replace. private final class FakeKeychain { private(set) var items: Set private(set) var log: [String] = [] private(set) var minCount: Int init(seed: Set) { items = seed minCount = seed.count } func add(_ id: String) { items.insert(id) log.append("add(\(id))") minCount = min(minCount, items.count) } func delete(_ id: String) { items.remove(id) log.append("delete(\(id))") minCount = min(minCount, items.count) } } @Test("replace ADDS the new item before DELETING the old — never a zero-item window") func replaceAddsBeforeDeletes() throws { let keychain = FakeKeychain(seed: ["old"]) try replaceKeychainItemAtomically( addNew: { keychain.add("new") }, deleteOld: { keychain.delete("old") } ) #expect(keychain.items == ["new"]) #expect(keychain.log == ["add(new)", "delete(old)"]) // add strictly precedes delete #expect(keychain.minCount >= 1) // at least one identity installed at every step } @Test("a failed add leaves the prior item intact and never runs the delete") func replaceAddFailureKeepsOld() { struct Boom: Error {} let keychain = FakeKeychain(seed: ["old"]) #expect(throws: Boom.self) { try replaceKeychainItemAtomically( addNew: { throw Boom() }, deleteOld: { keychain.delete("old") } ) } #expect(keychain.items == ["old"]) // old preserved — a working identity remains #expect(keychain.log.contains("delete(old)") == false) #expect(keychain.minCount >= 1) } @Test("a failed delete-of-old is non-fatal — the new item stays installed and the fault is surfaced") func replaceDeleteFailureKeepsNew() throws { struct Boom: Error {} let keychain = FakeKeychain(seed: ["old"]) var reported: Error? try replaceKeychainItemAtomically( addNew: { keychain.add("new") }, deleteOld: { throw Boom() }, reportStaleDeleteFailure: { reported = $0 } ) #expect(keychain.items.contains("new")) // renewal succeeded despite the stale-delete fault #expect(reported is Boom) // surfaced (not silently swallowed) #expect(keychain.minCount >= 1) }