Files
bitchat/bitchatTests/Services/UnifiedPeerServiceTests.swift
T
ef848857b7 Remove dead code found by full Periphery audit; add scan config + advisory CI (#1410)
Periphery 3.7.4 audit of both schemes (macOS + iOS, intersected so
platform-specific code is never touched), with test targets indexed and
the share extension built. 277 dead declarations removed or demoted:
dead forwarding wrappers (ChatViewModel+Nostr/+PrivateChat), removed-
feature remnants (autocomplete command suggestions, back-swipe tuning,
MediaSendError, GeohashParticipantTracker), unused Tor dormancy
bindings, assign-only properties, unused parameters (renamed to _), and
redundant public accessibility. 13 orphaned localization keys deleted
across all 29 locales (old pre-#1392 location-notes UI, app_info
warnings).

Two real tests were flagged as unused because they never ran: Swift
Testing methods missing @Test (NostrProtocolTests.
testAckRoundTripNIP44V2_Delivered, NotificationStreamAssemblerTests.
testAssemblesCompressedLargeFrame). Re-armed both; they pass.

Deliberately kept, now recorded in .periphery.baseline.json: iOS-only
code invisible to the CI macOS scan, C FFI signatures, keep-alive
NWPathMonitor reference, InboundEventKey.eventID (dedup semantics),
wifiBulk capability bit (reserved for Wi-Fi bulk work, used by
BitFoundation package tests), and the String secureClear cluster
(exercised by package tests).

New: .periphery.yml config and an advisory Dead Code CI job (mirrors
the SwiftLint precedent from #1361) that fails on findings not in the
committed baseline.

Verified: full macOS app suite, BitFoundation (119) and BitLogger (13)
package tests green; periphery scan --strict exits clean.

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 11:24:19 +02:00

311 lines
11 KiB
Swift

//
// UnifiedPeerServiceTests.swift
// bitchatTests
//
// Tests for UnifiedPeerService fingerprint and block resolution.
//
import Testing
import Foundation
import BitFoundation
@testable import bitchat
struct UnifiedPeerServiceTests {
@Test @MainActor
func getFingerprint_prefersMeshService() async {
let transport = MockTransport()
let identity = TestIdentityManager()
let idBridge = NostrIdentityBridge(keychain: MockKeychainHelper())
let service = UnifiedPeerService(meshService: transport, idBridge: idBridge, identityManager: identity)
let peerID = PeerID(str: "00000000000000CC")
transport.peerFingerprints[peerID] = "fp-1"
let fingerprint = service.getFingerprint(for: peerID)
#expect(fingerprint == "fp-1")
}
@Test @MainActor
func isBlocked_usesSocialIdentity() async {
let transport = MockTransport()
let identity = TestIdentityManager()
let idBridge = NostrIdentityBridge(keychain: MockKeychainHelper())
let service = UnifiedPeerService(meshService: transport, idBridge: idBridge, identityManager: identity)
let peerID = PeerID(str: "00000000000000DD")
let fingerprint = "fp-blocked"
transport.peerFingerprints[peerID] = fingerprint
identity.setBlocked(fingerprint, isBlocked: true)
#expect(service.isBlocked(peerID))
}
@Test @MainActor
func setBlocked_persistsByFingerprintAndToggles() async {
let transport = MockTransport()
let identity = TestIdentityManager()
let idBridge = NostrIdentityBridge(keychain: MockKeychainHelper())
let service = UnifiedPeerService(meshService: transport, idBridge: idBridge, identityManager: identity)
let peerID = PeerID(str: "00000000000000EE")
let fingerprint = "fp-target"
transport.peerFingerprints[peerID] = fingerprint
// Blocking resolves and persists by the peer's fingerprint.
let resolved = service.setBlocked(peerID, blocked: true)
#expect(resolved == fingerprint)
#expect(identity.isBlocked(fingerprint: fingerprint))
#expect(service.isBlocked(peerID))
// Unblocking clears it against the same identity.
let unresolved = service.setBlocked(peerID, blocked: false)
#expect(unresolved == fingerprint)
#expect(!identity.isBlocked(fingerprint: fingerprint))
#expect(!service.isBlocked(peerID))
}
// MARK: - Offline-favorite dedup (updatePeers phase 2)
/// A mutual favorite that is also on the mesh must collapse to a single
/// row keyed by the short mesh ID — even when the announced nickname no
/// longer matches the one stored with the favorite.
@Test @MainActor
func updatePeers_mutualFavoriteOnMeshYieldsSingleRow() async {
let favoritesService = FavoritesPersistenceService.shared
let transport = MockTransport()
let idBridge = NostrIdentityBridge(keychain: MockKeychainHelper())
let service = UnifiedPeerService(meshService: transport, idBridge: idBridge, identityManager: TestIdentityManager())
let noiseKey = Data(repeating: 0xAB, count: 32)
favoritesService.addFavorite(peerNoisePublicKey: noiseKey, peerNickname: "alice")
favoritesService.updatePeerFavoritedUs(peerNoisePublicKey: noiseKey, favorited: true)
defer {
favoritesService.updatePeerFavoritedUs(peerNoisePublicKey: noiseKey, favorited: false)
favoritesService.removeFavorite(peerNoisePublicKey: noiseKey)
}
let meshID = PeerID(publicKey: noiseKey)
let snapshots = [TransportPeerSnapshot(
peerID: meshID,
nickname: "alice-renamed",
isConnected: true,
noisePublicKey: noiseKey,
lastSeen: Date()
)]
transport.updatePeerSnapshots(snapshots)
service.didUpdatePeerSnapshots(snapshots)
let rows = service.peers.filter { $0.noisePublicKey == noiseKey }
#expect(rows.count == 1)
#expect(rows.first?.peerID == meshID)
#expect(rows.first?.isMutualFavorite == true)
#expect(service.favorites.filter { $0.noisePublicKey == noiseKey }.count == 1)
}
/// Same collapse must hold for a reachable-but-not-connected favorite
/// (relayed peers linger as "reachable" after their link drops).
@Test @MainActor
func updatePeers_reachableMutualFavoriteYieldsSingleRow() async {
let favoritesService = FavoritesPersistenceService.shared
let transport = MockTransport()
let idBridge = NostrIdentityBridge(keychain: MockKeychainHelper())
let service = UnifiedPeerService(meshService: transport, idBridge: idBridge, identityManager: TestIdentityManager())
let noiseKey = Data(repeating: 0xCD, count: 32)
favoritesService.addFavorite(peerNoisePublicKey: noiseKey, peerNickname: "bob")
favoritesService.updatePeerFavoritedUs(peerNoisePublicKey: noiseKey, favorited: true)
defer {
favoritesService.updatePeerFavoritedUs(peerNoisePublicKey: noiseKey, favorited: false)
favoritesService.removeFavorite(peerNoisePublicKey: noiseKey)
}
let otherKey = Data(repeating: 0x11, count: 32)
let snapshots = [
// A live link is required for anyone to count as reachable.
TransportPeerSnapshot(
peerID: PeerID(publicKey: otherKey),
nickname: "carol",
isConnected: true,
noisePublicKey: otherKey,
lastSeen: Date()
),
TransportPeerSnapshot(
peerID: PeerID(publicKey: noiseKey),
nickname: "bob",
isConnected: false,
noisePublicKey: noiseKey,
lastSeen: Date()
)
]
transport.updatePeerSnapshots(snapshots)
service.didUpdatePeerSnapshots(snapshots)
let bobRows = service.peers.filter { $0.noisePublicKey == noiseKey }
#expect(bobRows.count == 1)
#expect(bobRows.first?.peerID == PeerID(publicKey: noiseKey))
#expect(bobRows.first?.isReachable == true)
}
/// A mutual favorite with no mesh presence still gets its offline row,
/// keyed by the full noise-key PeerID.
@Test @MainActor
func updatePeers_offlineMutualFavoriteGetsOfflineRow() async {
let favoritesService = FavoritesPersistenceService.shared
let transport = MockTransport()
let idBridge = NostrIdentityBridge(keychain: MockKeychainHelper())
let service = UnifiedPeerService(meshService: transport, idBridge: idBridge, identityManager: TestIdentityManager())
let noiseKey = Data(repeating: 0xEF, count: 32)
favoritesService.addFavorite(peerNoisePublicKey: noiseKey, peerNickname: "dave")
favoritesService.updatePeerFavoritedUs(peerNoisePublicKey: noiseKey, favorited: true)
defer {
favoritesService.updatePeerFavoritedUs(peerNoisePublicKey: noiseKey, favorited: false)
favoritesService.removeFavorite(peerNoisePublicKey: noiseKey)
}
transport.updatePeerSnapshots([])
service.didUpdatePeerSnapshots([])
let rows = service.peers.filter { $0.noisePublicKey == noiseKey }
#expect(rows.count == 1)
#expect(rows.first?.peerID == PeerID(hexData: noiseKey))
#expect(rows.first?.isMutualFavorite == true)
}
@Test @MainActor
func setBlocked_unknownIdentityReturnsNil() async {
let transport = MockTransport()
let identity = TestIdentityManager()
let idBridge = NostrIdentityBridge(keychain: MockKeychainHelper())
let service = UnifiedPeerService(meshService: transport, idBridge: idBridge, identityManager: identity)
// No fingerprint resolvable for this peer (offline & unknown).
let peerID = PeerID(str: "00000000000000FF")
#expect(service.setBlocked(peerID, blocked: true) == nil)
#expect(!service.isBlocked(peerID))
}
}
private final class TestIdentityManager: SecureIdentityStateManagerProtocol {
private var socialIdentities: [String: SocialIdentity] = [:]
private var favorites: Set<String> = []
private var blockedNostr: Set<String> = []
private var verified: Set<String> = []
func forceSave() {}
func getSocialIdentity(for fingerprint: String) -> SocialIdentity? {
socialIdentities[fingerprint]
}
func upsertCryptographicIdentity(fingerprint: String, noisePublicKey: Data, signingPublicKey: Data?, claimedNickname: String?) {}
func getCryptoIdentitiesByPeerIDPrefix(_ peerID: PeerID) -> [CryptographicIdentity] {
[]
}
func updateSocialIdentity(_ identity: SocialIdentity) {
socialIdentities[identity.fingerprint] = identity
}
func isFavorite(fingerprint: String) -> Bool {
favorites.contains(fingerprint)
}
func isBlocked(fingerprint: String) -> Bool {
socialIdentities[fingerprint]?.isBlocked ?? false
}
func setBlocked(_ fingerprint: String, isBlocked: Bool) {
var identity = socialIdentities[fingerprint] ?? SocialIdentity(
fingerprint: fingerprint,
localPetname: nil,
claimedNickname: "",
trustLevel: .unknown,
isFavorite: false,
isBlocked: false,
notes: nil
)
identity.isBlocked = isBlocked
socialIdentities[fingerprint] = identity
}
func isNostrBlocked(pubkeyHexLowercased: String) -> Bool {
blockedNostr.contains(pubkeyHexLowercased)
}
func setNostrBlocked(_ pubkeyHexLowercased: String, isBlocked: Bool) {
if isBlocked {
blockedNostr.insert(pubkeyHexLowercased)
} else {
blockedNostr.remove(pubkeyHexLowercased)
}
}
func getBlockedNostrPubkeys() -> Set<String> {
blockedNostr
}
func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState) {}
func clearAllIdentityData() {
socialIdentities.removeAll()
favorites.removeAll()
blockedNostr.removeAll()
verified.removeAll()
}
func removeEphemeralSession(peerID: PeerID) {}
func setVerified(fingerprint: String, verified: Bool) {
if verified {
self.verified.insert(fingerprint)
} else {
self.verified.remove(fingerprint)
}
}
func isVerified(fingerprint: String) -> Bool {
verified.contains(fingerprint)
}
func getVerifiedFingerprints() -> Set<String> {
verified
}
// MARK: Vouching (unused by these tests)
@discardableResult
func recordVouch(voucheeFingerprint: String, voucherFingerprint: String, timestamp: Date) -> Bool {
false
}
func validVouchers(for fingerprint: String) -> [VouchRecord] {
[]
}
func isVouched(fingerprint: String) -> Bool {
false
}
func lastVouchBatchSent(to fingerprint: String) -> Date? {
nil
}
func markVouchBatchSent(to fingerprint: String, at date: Date) {}
func signingPublicKey(forFingerprint fingerprint: String) -> Data? {
nil
}
func mostRecentlyVerifiedFingerprints(limit: Int, excluding fingerprint: String) -> [String] {
[]
}
}