mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:45:18 +00:00
* Isolate tests from the developer's real login keychain Every test run prompted for the login keychain password (repeatedly, since the xctest runner's code signature changes each build, so "Always Allow" can never stick) and silently deleted the developer's real Nostr identity via panic-mode tests. Two holes let tests reach the real keychain: - NostrIdentityBridge() defaulted to the real KeychainManager. Tests inject mocks, but app-side constructions with no injection point (LocationNotesManager's static bridge, GeohashPresenceService, BoardManager, AppRuntime) read the real chat.bitchat.nostr item when exercised under test. - clearAllAssociations() used raw SecItem* calls that bypassed the injected keychain entirely, so panicClearAllData tests wiped the real Nostr identity items on every run. Fix: centralize FavoritesPersistenceService's test-guarded in-memory default as KeychainManager.makeDefault() and use it for all default keychain parameters, and add deleteAll(service:) to KeychainManagerProtocol so clearAllAssociations() goes through the injected keychain like every other operation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Share one in-memory test keychain per process (Codex P2) A fresh store per makeDefault() call diverges from production, where separate default-constructed bridges share chat.bitchat.nostr — BoardManager's publish and NIP-09 delete paths would derive different geohash identities under test. PreviewKeychainManager gains a lock since the shared instance is reached from arbitrary threads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
210 lines
5.6 KiB
Swift
210 lines
5.6 KiB
Swift
//
|
|
// MockKeychain.swift
|
|
// bitchat
|
|
//
|
|
// This is free and unencumbered software released into the public domain.
|
|
// For more information, see <https://unlicense.org>
|
|
//
|
|
|
|
import Foundation
|
|
import BitFoundation
|
|
@testable import bitchat
|
|
|
|
final class MockKeychain: KeychainManagerProtocol {
|
|
private var storage: [String: Data] = [:]
|
|
private var serviceStorage: [String: [String: Data]] = [:]
|
|
|
|
// BCH-01-009: Configurable error simulation for testing
|
|
var simulatedReadError: KeychainReadResult?
|
|
var simulatedSaveError: KeychainSaveResult?
|
|
|
|
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
|
|
storage[key] = keyData
|
|
return true
|
|
}
|
|
|
|
func getIdentityKey(forKey key: String) -> Data? {
|
|
storage[key]
|
|
}
|
|
|
|
func deleteIdentityKey(forKey key: String) -> Bool {
|
|
storage.removeValue(forKey: key)
|
|
return true
|
|
}
|
|
|
|
func deleteAllKeychainData() -> Bool {
|
|
storage.removeAll()
|
|
serviceStorage.removeAll()
|
|
return true
|
|
}
|
|
|
|
func secureClear(_ data: inout Data) {
|
|
data = Data()
|
|
}
|
|
|
|
func secureClear(_ string: inout String) {
|
|
string = ""
|
|
}
|
|
|
|
func verifyIdentityKeyExists() -> Bool {
|
|
storage["identity_noiseStaticKey"] != nil
|
|
}
|
|
|
|
// BCH-01-009: New methods with proper error classification
|
|
func getIdentityKeyWithResult(forKey key: String) -> KeychainReadResult {
|
|
if let simulated = simulatedReadError {
|
|
return simulated
|
|
}
|
|
if let data = storage[key] {
|
|
return .success(data)
|
|
}
|
|
return .itemNotFound
|
|
}
|
|
|
|
func saveIdentityKeyWithResult(_ keyData: Data, forKey key: String) -> KeychainSaveResult {
|
|
if let simulated = simulatedSaveError {
|
|
return simulated
|
|
}
|
|
storage[key] = keyData
|
|
return .success
|
|
}
|
|
|
|
// MARK: - Generic Data Storage (consolidated from KeychainHelper)
|
|
|
|
func save(key: String, data: Data, service: String, accessible: CFString?) {
|
|
if serviceStorage[service] == nil {
|
|
serviceStorage[service] = [:]
|
|
}
|
|
serviceStorage[service]?[key] = data
|
|
}
|
|
|
|
func load(key: String, service: String) -> Data? {
|
|
serviceStorage[service]?[key]
|
|
}
|
|
|
|
func delete(key: String, service: String) {
|
|
serviceStorage[service]?.removeValue(forKey: key)
|
|
}
|
|
|
|
func deleteAll(service: String) {
|
|
serviceStorage.removeValue(forKey: service)
|
|
}
|
|
}
|
|
|
|
/// Typealias for backwards compatibility with tests using MockKeychainHelper
|
|
typealias MockKeychainHelper = MockKeychain
|
|
|
|
/// Mock keychain that tracks secureClear calls for testing DH secret clearing
|
|
final class TrackingMockKeychain: KeychainManagerProtocol {
|
|
private var storage: [String: Data] = [:]
|
|
private var serviceStorage: [String: [String: Data]] = [:]
|
|
|
|
/// Thread-safe counter for secureClear calls
|
|
private let lock = NSLock()
|
|
private var _secureClearDataCallCount = 0
|
|
private var _secureClearStringCallCount = 0
|
|
|
|
// BCH-01-009: Configurable error simulation for testing
|
|
var simulatedReadError: KeychainReadResult?
|
|
var simulatedSaveError: KeychainSaveResult?
|
|
|
|
var secureClearDataCallCount: Int {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
return _secureClearDataCallCount
|
|
}
|
|
|
|
var secureClearStringCallCount: Int {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
return _secureClearStringCallCount
|
|
}
|
|
|
|
var totalSecureClearCallCount: Int {
|
|
return secureClearDataCallCount + secureClearStringCallCount
|
|
}
|
|
|
|
func resetCounts() {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
_secureClearDataCallCount = 0
|
|
_secureClearStringCallCount = 0
|
|
}
|
|
|
|
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
|
|
storage[key] = keyData
|
|
return true
|
|
}
|
|
|
|
func getIdentityKey(forKey key: String) -> Data? {
|
|
storage[key]
|
|
}
|
|
|
|
func deleteIdentityKey(forKey key: String) -> Bool {
|
|
storage.removeValue(forKey: key)
|
|
return true
|
|
}
|
|
|
|
func deleteAllKeychainData() -> Bool {
|
|
storage.removeAll()
|
|
serviceStorage.removeAll()
|
|
return true
|
|
}
|
|
|
|
func secureClear(_ data: inout Data) {
|
|
lock.lock()
|
|
_secureClearDataCallCount += 1
|
|
lock.unlock()
|
|
data = Data()
|
|
}
|
|
|
|
func secureClear(_ string: inout String) {
|
|
lock.lock()
|
|
_secureClearStringCallCount += 1
|
|
lock.unlock()
|
|
string = ""
|
|
}
|
|
|
|
func verifyIdentityKeyExists() -> Bool {
|
|
storage["identity_noiseStaticKey"] != nil
|
|
}
|
|
|
|
// BCH-01-009: New methods with proper error classification
|
|
func getIdentityKeyWithResult(forKey key: String) -> KeychainReadResult {
|
|
if let simulated = simulatedReadError {
|
|
return simulated
|
|
}
|
|
if let data = storage[key] {
|
|
return .success(data)
|
|
}
|
|
return .itemNotFound
|
|
}
|
|
|
|
func saveIdentityKeyWithResult(_ keyData: Data, forKey key: String) -> KeychainSaveResult {
|
|
if let simulated = simulatedSaveError {
|
|
return simulated
|
|
}
|
|
storage[key] = keyData
|
|
return .success
|
|
}
|
|
|
|
func save(key: String, data: Data, service: String, accessible: CFString?) {
|
|
if serviceStorage[service] == nil {
|
|
serviceStorage[service] = [:]
|
|
}
|
|
serviceStorage[service]?[key] = data
|
|
}
|
|
|
|
func load(key: String, service: String) -> Data? {
|
|
serviceStorage[service]?[key]
|
|
}
|
|
|
|
func delete(key: String, service: String) {
|
|
serviceStorage[service]?.removeValue(forKey: key)
|
|
}
|
|
|
|
func deleteAll(service: String) {
|
|
serviceStorage.removeValue(forKey: service)
|
|
}
|
|
}
|