mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 10:45:20 +00:00
Expand Nostr and identity coverage (#1059)
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
@@ -3,6 +3,57 @@ import XCTest
|
||||
@testable import bitchat
|
||||
|
||||
final class SecureIdentityStateManagerTests: XCTestCase {
|
||||
func test_upsertCryptographicIdentity_withoutClaimedNicknameDoesNotCreateSocialIdentity() async {
|
||||
let manager = SecureIdentityStateManager(MockKeychain())
|
||||
let fingerprint = String(repeating: "aa", count: 32)
|
||||
let peerID = PeerID(str: String(fingerprint.prefix(16)))
|
||||
|
||||
manager.upsertCryptographicIdentity(
|
||||
fingerprint: fingerprint,
|
||||
noisePublicKey: Data(repeating: 0x11, count: 32),
|
||||
signingPublicKey: Data(repeating: 0x22, count: 32),
|
||||
claimedNickname: nil
|
||||
)
|
||||
|
||||
let inserted = await waitUntil {
|
||||
manager.getCryptoIdentitiesByPeerIDPrefix(peerID).count == 1
|
||||
}
|
||||
XCTAssertTrue(inserted)
|
||||
XCTAssertNil(manager.getSocialIdentity(for: fingerprint))
|
||||
}
|
||||
|
||||
func test_upsertCryptographicIdentity_updatesExistingKeyAndPreservesSigningKey() async {
|
||||
let manager = SecureIdentityStateManager(MockKeychain())
|
||||
let fingerprint = String(repeating: "ab", count: 32)
|
||||
let peerID = PeerID(str: String(fingerprint.prefix(16)))
|
||||
let originalNoiseKey = Data(repeating: 0x11, count: 32)
|
||||
let updatedNoiseKey = Data(repeating: 0x33, count: 32)
|
||||
let signingKey = Data(repeating: 0x22, count: 32)
|
||||
|
||||
manager.upsertCryptographicIdentity(
|
||||
fingerprint: fingerprint,
|
||||
noisePublicKey: originalNoiseKey,
|
||||
signingPublicKey: signingKey,
|
||||
claimedNickname: nil
|
||||
)
|
||||
_ = await waitUntil {
|
||||
manager.getCryptoIdentitiesByPeerIDPrefix(peerID).first?.publicKey == originalNoiseKey
|
||||
}
|
||||
|
||||
manager.upsertCryptographicIdentity(
|
||||
fingerprint: fingerprint,
|
||||
noisePublicKey: updatedNoiseKey,
|
||||
signingPublicKey: nil,
|
||||
claimedNickname: nil
|
||||
)
|
||||
|
||||
let updated = await waitUntil {
|
||||
guard let identity = manager.getCryptoIdentitiesByPeerIDPrefix(peerID).first else { return false }
|
||||
return identity.publicKey == updatedNoiseKey && identity.signingPublicKey == signingKey
|
||||
}
|
||||
XCTAssertTrue(updated)
|
||||
}
|
||||
|
||||
func test_upsertCryptographicIdentity_tracksByPeerIDPrefixAndClaimedNickname() async {
|
||||
let manager = SecureIdentityStateManager(MockKeychain())
|
||||
let noisePublicKey = Data(repeating: 0x11, count: 32)
|
||||
@@ -43,6 +94,12 @@ final class SecureIdentityStateManagerTests: XCTestCase {
|
||||
XCTAssertEqual(manager.getSocialIdentity(for: fingerprint)?.claimedNickname, "Unknown")
|
||||
}
|
||||
|
||||
func test_isBlocked_unknownFingerprintReturnsFalse() {
|
||||
let manager = SecureIdentityStateManager(MockKeychain())
|
||||
|
||||
XCTAssertFalse(manager.isBlocked(fingerprint: String(repeating: "ff", count: 32)))
|
||||
}
|
||||
|
||||
func test_setVerified_updatesTrustLevelAndVerifiedSet() async {
|
||||
let manager = SecureIdentityStateManager(MockKeychain())
|
||||
let fingerprint = String(repeating: "cd", count: 32)
|
||||
@@ -71,6 +128,209 @@ final class SecureIdentityStateManagerTests: XCTestCase {
|
||||
XCTAssertTrue(reloaded.isFavorite(fingerprint: fingerprint))
|
||||
}
|
||||
|
||||
func test_updateSocialIdentity_reindexesClaimedNickname() async {
|
||||
let manager = SecureIdentityStateManager(MockKeychain())
|
||||
let fingerprint = String(repeating: "34", count: 32)
|
||||
|
||||
manager.updateSocialIdentity(
|
||||
SocialIdentity(
|
||||
fingerprint: fingerprint,
|
||||
localPetname: nil,
|
||||
claimedNickname: "Alice",
|
||||
trustLevel: .unknown,
|
||||
isFavorite: false,
|
||||
isBlocked: false,
|
||||
notes: nil
|
||||
)
|
||||
)
|
||||
let initialIndexed = await waitUntil {
|
||||
manager.debugNicknameIndex["Alice"]?.contains(fingerprint) == true
|
||||
}
|
||||
XCTAssertTrue(initialIndexed)
|
||||
|
||||
manager.updateSocialIdentity(
|
||||
SocialIdentity(
|
||||
fingerprint: fingerprint,
|
||||
localPetname: "Friend",
|
||||
claimedNickname: "Bob",
|
||||
trustLevel: .trusted,
|
||||
isFavorite: true,
|
||||
isBlocked: false,
|
||||
notes: "updated"
|
||||
)
|
||||
)
|
||||
|
||||
let reindexed = await waitUntil {
|
||||
manager.debugNicknameIndex["Alice"]?.contains(fingerprint) != true &&
|
||||
manager.debugNicknameIndex["Bob"]?.contains(fingerprint) == true
|
||||
}
|
||||
XCTAssertTrue(reindexed)
|
||||
XCTAssertEqual(manager.getSocialIdentity(for: fingerprint)?.claimedNickname, "Bob")
|
||||
}
|
||||
|
||||
func test_upsertCryptographicIdentity_sameClaimedNicknamePreservesExistingSocialIdentity() async {
|
||||
let manager = SecureIdentityStateManager(MockKeychain())
|
||||
let fingerprint = String(repeating: "35", count: 32)
|
||||
|
||||
manager.updateSocialIdentity(
|
||||
SocialIdentity(
|
||||
fingerprint: fingerprint,
|
||||
localPetname: "Pal",
|
||||
claimedNickname: "Alice",
|
||||
trustLevel: .trusted,
|
||||
isFavorite: true,
|
||||
isBlocked: false,
|
||||
notes: "keep me"
|
||||
)
|
||||
)
|
||||
_ = await waitUntil { manager.getSocialIdentity(for: fingerprint) != nil }
|
||||
|
||||
manager.upsertCryptographicIdentity(
|
||||
fingerprint: fingerprint,
|
||||
noisePublicKey: Data(repeating: 0x11, count: 32),
|
||||
signingPublicKey: Data(repeating: 0x22, count: 32),
|
||||
claimedNickname: "Alice"
|
||||
)
|
||||
|
||||
let inserted = await waitUntil {
|
||||
manager.getCryptoIdentitiesByPeerIDPrefix(PeerID(str: String(fingerprint.prefix(16)))).count == 1
|
||||
}
|
||||
XCTAssertTrue(inserted)
|
||||
XCTAssertEqual(manager.getSocialIdentity(for: fingerprint)?.localPetname, "Pal")
|
||||
XCTAssertEqual(manager.getSocialIdentity(for: fingerprint)?.notes, "keep me")
|
||||
XCTAssertTrue(manager.getSocialIdentity(for: fingerprint)?.isFavorite == true)
|
||||
}
|
||||
|
||||
func test_getFavorites_returnsOnlyFavoritedFingerprints() async {
|
||||
let manager = SecureIdentityStateManager(MockKeychain())
|
||||
let favoriteOne = String(repeating: "45", count: 32)
|
||||
let favoriteTwo = String(repeating: "56", count: 32)
|
||||
let other = String(repeating: "67", count: 32)
|
||||
|
||||
manager.setFavorite(favoriteOne, isFavorite: true)
|
||||
manager.setFavorite(favoriteTwo, isFavorite: true)
|
||||
manager.setFavorite(other, isFavorite: false)
|
||||
|
||||
let favoritesLoaded = await waitUntil {
|
||||
manager.getFavorites() == Set([favoriteOne, favoriteTwo])
|
||||
}
|
||||
XCTAssertTrue(favoritesLoaded)
|
||||
}
|
||||
|
||||
func test_setFavorite_existingIdentityCanBeClearedWithoutChangingNickname() async {
|
||||
let manager = SecureIdentityStateManager(MockKeychain())
|
||||
let fingerprint = String(repeating: "68", count: 32)
|
||||
|
||||
manager.updateSocialIdentity(
|
||||
SocialIdentity(
|
||||
fingerprint: fingerprint,
|
||||
localPetname: nil,
|
||||
claimedNickname: "Alice",
|
||||
trustLevel: .trusted,
|
||||
isFavorite: false,
|
||||
isBlocked: false,
|
||||
notes: nil
|
||||
)
|
||||
)
|
||||
_ = await waitUntil { manager.getSocialIdentity(for: fingerprint) != nil }
|
||||
|
||||
manager.setFavorite(fingerprint, isFavorite: true)
|
||||
_ = await waitUntil { manager.isFavorite(fingerprint: fingerprint) }
|
||||
|
||||
manager.setFavorite(fingerprint, isFavorite: false)
|
||||
let cleared = await waitUntil {
|
||||
!manager.isFavorite(fingerprint: fingerprint) &&
|
||||
manager.getSocialIdentity(for: fingerprint)?.claimedNickname == "Alice" &&
|
||||
manager.getSocialIdentity(for: fingerprint)?.trustLevel == .trusted
|
||||
}
|
||||
XCTAssertTrue(cleared)
|
||||
}
|
||||
|
||||
func test_setBlocked_createsIdentityAndCanLaterUnblock() async {
|
||||
let manager = SecureIdentityStateManager(MockKeychain())
|
||||
let fingerprint = String(repeating: "78", count: 32)
|
||||
|
||||
manager.setBlocked(fingerprint, isBlocked: true)
|
||||
let blocked = await waitUntil {
|
||||
manager.isBlocked(fingerprint: fingerprint)
|
||||
}
|
||||
XCTAssertTrue(blocked)
|
||||
XCTAssertEqual(manager.getSocialIdentity(for: fingerprint)?.claimedNickname, "Unknown")
|
||||
|
||||
manager.setBlocked(fingerprint, isBlocked: false)
|
||||
let unblocked = await waitUntil {
|
||||
!manager.isBlocked(fingerprint: fingerprint)
|
||||
}
|
||||
XCTAssertTrue(unblocked)
|
||||
}
|
||||
|
||||
func test_setVerified_false_downgradesTrustLevelToCasual() async {
|
||||
let manager = SecureIdentityStateManager(MockKeychain())
|
||||
let fingerprint = String(repeating: "89", count: 32)
|
||||
|
||||
manager.updateSocialIdentity(
|
||||
SocialIdentity(
|
||||
fingerprint: fingerprint,
|
||||
localPetname: nil,
|
||||
claimedNickname: "Verifier",
|
||||
trustLevel: .trusted,
|
||||
isFavorite: false,
|
||||
isBlocked: false,
|
||||
notes: nil
|
||||
)
|
||||
)
|
||||
_ = await waitUntil { manager.getSocialIdentity(for: fingerprint) != nil }
|
||||
|
||||
manager.setVerified(fingerprint: fingerprint, verified: true)
|
||||
_ = await waitUntil { manager.isVerified(fingerprint: fingerprint) }
|
||||
|
||||
manager.setVerified(fingerprint: fingerprint, verified: false)
|
||||
let downgraded = await waitUntil {
|
||||
!manager.isVerified(fingerprint: fingerprint) &&
|
||||
manager.getSocialIdentity(for: fingerprint)?.trustLevel == .casual
|
||||
}
|
||||
XCTAssertTrue(downgraded)
|
||||
}
|
||||
|
||||
func test_ephemeralSessionLifecycle_tracksHandshakeProgressAndLastInteraction() async {
|
||||
let manager = SecureIdentityStateManager(MockKeychain())
|
||||
let peerID = PeerID(str: "1234567890abcdef")
|
||||
let fingerprint = String(repeating: "90", count: 32)
|
||||
|
||||
manager.registerEphemeralSession(peerID: peerID, handshakeState: .initiated)
|
||||
let registered = await waitUntil {
|
||||
if case .initiated? = manager.debugEphemeralSession(for: peerID)?.handshakeState {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
XCTAssertTrue(registered)
|
||||
|
||||
manager.updateHandshakeState(peerID: peerID, state: .inProgress)
|
||||
let progressed = await waitUntil {
|
||||
if case .inProgress? = manager.debugEphemeralSession(for: peerID)?.handshakeState {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
XCTAssertTrue(progressed)
|
||||
|
||||
manager.updateHandshakeState(peerID: peerID, state: .completed(fingerprint: fingerprint))
|
||||
let completed = await waitUntil {
|
||||
if case .completed(let completedFingerprint)? = manager.debugEphemeralSession(for: peerID)?.handshakeState {
|
||||
return completedFingerprint == fingerprint && manager.debugLastInteraction(for: fingerprint) != nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
XCTAssertTrue(completed)
|
||||
|
||||
manager.removeEphemeralSession(peerID: peerID)
|
||||
let removed = await waitUntil {
|
||||
manager.debugEphemeralSession(for: peerID) == nil
|
||||
}
|
||||
XCTAssertTrue(removed)
|
||||
}
|
||||
|
||||
func test_setNostrBlocked_normalizesToLowercaseAndPersists() async {
|
||||
let keychain = MockKeychain()
|
||||
let manager = SecureIdentityStateManager(keychain)
|
||||
@@ -88,6 +348,21 @@ final class SecureIdentityStateManagerTests: XCTestCase {
|
||||
XCTAssertTrue(reloaded.isNostrBlocked(pubkeyHexLowercased: pubkey))
|
||||
}
|
||||
|
||||
func test_setNostrBlocked_falseRemovesExistingKey() async {
|
||||
let manager = SecureIdentityStateManager(MockKeychain())
|
||||
let pubkey = "ABCDEF1234"
|
||||
|
||||
manager.setNostrBlocked(pubkey, isBlocked: true)
|
||||
_ = await waitUntil { manager.isNostrBlocked(pubkeyHexLowercased: pubkey) }
|
||||
|
||||
manager.setNostrBlocked(pubkey, isBlocked: false)
|
||||
let cleared = await waitUntil {
|
||||
!manager.isNostrBlocked(pubkeyHexLowercased: pubkey) &&
|
||||
manager.getBlockedNostrPubkeys().isEmpty
|
||||
}
|
||||
XCTAssertTrue(cleared)
|
||||
}
|
||||
|
||||
func test_corruptPersistedCache_fallsBackToEmptyState() {
|
||||
let keychain = MockKeychain()
|
||||
_ = keychain.saveIdentityKey(Data(repeating: 0x01, count: 32), forKey: "identityCacheEncryptionKey")
|
||||
@@ -122,6 +397,21 @@ final class SecureIdentityStateManagerTests: XCTestCase {
|
||||
XCTAssertTrue(cleared)
|
||||
}
|
||||
|
||||
func test_forceSave_withFailingCacheWriteDoesNotPersistCache() async {
|
||||
let keychain = FailingCacheSaveKeychain()
|
||||
let manager = SecureIdentityStateManager(keychain)
|
||||
let fingerprint = String(repeating: "de", count: 32)
|
||||
|
||||
manager.setFavorite(fingerprint, isFavorite: true)
|
||||
let primed = await waitUntil { manager.isFavorite(fingerprint: fingerprint) }
|
||||
XCTAssertTrue(primed)
|
||||
|
||||
manager.forceSave()
|
||||
|
||||
let reloaded = SecureIdentityStateManager(keychain)
|
||||
XCTAssertFalse(reloaded.isFavorite(fingerprint: fingerprint))
|
||||
}
|
||||
|
||||
private func waitUntil(
|
||||
timeout: TimeInterval = 1.0,
|
||||
condition: @escaping () -> Bool
|
||||
@@ -136,3 +426,72 @@ final class SecureIdentityStateManagerTests: XCTestCase {
|
||||
return condition()
|
||||
}
|
||||
}
|
||||
|
||||
private final class FailingCacheSaveKeychain: KeychainManagerProtocol {
|
||||
private var storage: [String: Data] = [:]
|
||||
private var serviceStorage: [String: [String: Data]] = [:]
|
||||
|
||||
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
|
||||
if key == "bitchat.identityCache.v2" {
|
||||
return false
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func getIdentityKeyWithResult(forKey key: String) -> KeychainReadResult {
|
||||
if let data = storage[key] {
|
||||
return .success(data)
|
||||
}
|
||||
return .itemNotFound
|
||||
}
|
||||
|
||||
func saveIdentityKeyWithResult(_ keyData: Data, forKey key: String) -> KeychainSaveResult {
|
||||
if saveIdentityKey(keyData, forKey: key) {
|
||||
return .success
|
||||
}
|
||||
return .otherError(OSStatus(-1))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user