Refactor/robustness (#446)

* Refactor BitChat for improved robustness and performance

Major refactoring to simplify architecture and fix critical issues:

Architecture Improvements:
- Replace complex BluetoothMeshService with simplified SimplifiedBluetoothService
- Consolidate message routing and peer management into unified services
- Remove redundant caching layers and optimize performance

Bug Fixes:
- Fix critical BLE peer mapping corruption in mesh networks
- Fix encrypted message routing failures in multi-peer scenarios
- Fix app freezes and Main Thread Checker warnings
- Fix BLE message delivery in dual-role connections
- Fix favorite toggle UI not updating instantly
- Fix Nostr offline messaging with 24-hour message filtering

Features:
- Add command processor for chat commands
- Add autocomplete service for mentions and commands
- Improve private chat management with dedicated service
- Add unified peer service for consistent state management

Performance:
- Optimize BLE reconnection speed
- Reduce excessive logging throughout codebase
- Improve message deduplication efficiency
- Optimize UI updates and state management

* Improvements refactor robust (#441)

* remove unused code

* remove more

* TLV for announcement

* restore

* restore

* restore?

* messages tlv too (#442)

* Fix Nostr notification and read receipt issues, add TLV encoding, cleanup unused code

## Notification & Read Receipt Fixes
- Fixed toolbar notification icon appearing incorrectly on app restart for already-read messages
- Fixed read receipts being incorrectly deleted on startup when privateChats was empty
- Fixed messages not being marked as read when opening chat for first time
- Fixed senderPeerID not being updated during message consolidation
- Added startup phase logic to block old messages (>30s) while allowing recent ones
- Fixed unread status checking across all storage locations (ephemeral, stable Noise keys, temporary Nostr IDs)

## TLV Encoding Implementation
- Implemented Type-Length-Value encoding for private message payloads
- Added PrivateMessagePacket struct with TLV encode/decode methods
- Enhanced message structure for better extensibility and robustness

## Code Cleanup
- Removed unused PeerStateManager class and related dependencies
- Removed dead protocol types (DeliveryAck, ProtocolAck/Nack, NoiseIdentityAnnouncement)
- Cleaned up BitchatDelegate by removing unused methods
- Removed excessive debug logging throughout ChatViewModel
- Added test-only ProtocolNack helper for integration tests

## Technical Details
- Messages stored under three ID types: ephemeral peer IDs, stable Noise key hexes, temporary Nostr IDs
- Fixed cleanupOldReadReceipts() to skip when privateChats is empty or during startup
- Updated message consolidation to properly update senderPeerID
- Restored NIP-17 timestamp randomization (±15 minutes) for privacy

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
jack
2025-08-17 01:51:54 +02:00
committed by GitHub
co-authored by jack callebtc
parent 47b0829685
commit 845ffc601b
38 changed files with 6423 additions and 12246 deletions
+9 -246
View File
@@ -16,9 +16,6 @@ final class PrivateChatE2ETests: XCTestCase {
var bob: MockBluetoothMeshService!
var charlie: MockBluetoothMeshService!
var deliveryTracker: DeliveryTracker!
var retryService: MessageRetryService!
override func setUp() {
super.setUp()
@@ -27,22 +24,13 @@ final class PrivateChatE2ETests: XCTestCase {
bob = createMockService(peerID: TestConstants.testPeerID2, nickname: TestConstants.testNickname2)
charlie = createMockService(peerID: TestConstants.testPeerID3, nickname: TestConstants.testNickname3)
// Setup delivery tracking
deliveryTracker = DeliveryTracker.shared
retryService = MessageRetryService.shared
retryService.meshService = alice
// Clear any existing state
deliveryTracker.clearDeliveryStatus(for: "")
retryService.clearRetryQueue()
// Delivery tracking is now handled internally by SimplifiedBluetoothService
}
override func tearDown() {
alice = nil
bob = nil
charlie = nil
deliveryTracker = nil
retryService = nil
super.tearDown()
}
@@ -104,227 +92,16 @@ final class PrivateChatE2ETests: XCTestCase {
// MARK: - Delivery Acknowledgment Tests
func testDeliveryAckGeneration() {
simulateConnection(alice, bob)
let messageID = UUID().uuidString
let expectation = XCTestExpectation(description: "Delivery status updated")
// Monitor delivery status
let cancellable = deliveryTracker.deliveryStatusUpdated.sink { update in
if update.messageID == messageID {
switch update.status {
case .delivered(let recipient, _):
XCTAssertEqual(recipient, TestConstants.testNickname2)
expectation.fulfill()
default:
break
}
}
}
// Setup Bob to generate ACK
bob.packetDeliveryHandler = { packet in
if let message = BitchatMessage.fromBinaryPayload(packet.payload),
message.isPrivate {
// Generate ACK
if let ack = self.deliveryTracker.generateAck(
for: message,
myPeerID: TestConstants.testPeerID2,
myNickname: TestConstants.testNickname2,
hopCount: 1
) {
// Send ACK back
let ackData = ack.encode()!
let ackPacket = TestHelpers.createTestPacket(
type: 0x03,
senderID: TestConstants.testPeerID2,
recipientID: TestConstants.testPeerID1,
payload: ackData
)
self.alice.simulateIncomingPacket(ackPacket)
}
}
}
// Setup Alice to process ACK
alice.packetDeliveryHandler = { packet in
if packet.type == 0x03 {
if let ack = DeliveryAck.decode(from: packet.payload) {
self.deliveryTracker.processDeliveryAck(ack)
}
}
}
// Track the message
let trackedMessage = BitchatMessage(
id: messageID,
sender: TestConstants.testNickname1,
content: TestConstants.testMessage1,
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: TestConstants.testNickname2,
senderPeerID: TestConstants.testPeerID1,
mentions: nil
)
deliveryTracker.trackMessage(
trackedMessage,
recipientID: TestConstants.testPeerID2,
recipientNickname: TestConstants.testNickname2
)
// Send the message
alice.sendPrivateMessage(
TestConstants.testMessage1,
to: TestConstants.testPeerID2,
recipientNickname: TestConstants.testNickname2,
messageID: messageID
)
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
cancellable.cancel()
}
// NOTE: DeliveryTracker has been removed in SimplifiedBluetoothService.
// Delivery tracking is now handled internally.
func testDeliveryTimeout() {
// Don't connect peers - message should timeout
let messageID = UUID().uuidString
let expectation = XCTestExpectation(description: "Delivery failed due to timeout")
// Use shared instance (can't create new one due to private init)
let shortTimeoutTracker = DeliveryTracker.shared
let cancellable = shortTimeoutTracker.deliveryStatusUpdated.sink { update in
if update.messageID == messageID {
switch update.status {
case .failed(let reason):
XCTAssertTrue(reason.contains("not delivered"))
expectation.fulfill()
default:
break
}
}
}
let trackedMessage = BitchatMessage(
id: messageID,
sender: TestConstants.testNickname1,
content: TestConstants.testMessage1,
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: TestConstants.testNickname2,
senderPeerID: TestConstants.testPeerID1,
mentions: nil
)
// Track with short timeout (will use default 30s for private messages)
shortTimeoutTracker.trackMessage(
trackedMessage,
recipientID: TestConstants.testPeerID2,
recipientNickname: TestConstants.testNickname2
)
// Don't actually send - let it timeout
// For testing, we'll manually trigger timeout
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
shortTimeoutTracker.clearDeliveryStatus(for: messageID)
shortTimeoutTracker.deliveryStatusUpdated.send((messageID: messageID, status: .failed(reason: "Message not delivered")))
}
wait(for: [expectation], timeout: TestConstants.shortTimeout)
cancellable.cancel()
}
// MARK: - Message Retry Tests
func testMessageRetryOnFailure() {
let messageContent = "Retry test message"
let expectation = XCTestExpectation(description: "Message retried")
var sendCount = 0
// Override send to count attempts
alice.messageDeliveryHandler = { message in
if message.content == messageContent {
sendCount += 1
if sendCount == 2 { // Original + 1 retry
expectation.fulfill()
}
}
}
// Add to retry queue
retryService.addMessageForRetry(
content: messageContent,
isPrivate: true,
recipientPeerID: TestConstants.testPeerID2,
recipientNickname: TestConstants.testNickname2
)
// Simulate connection after delay to trigger retry
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
self.simulateConnection(self.alice, self.bob)
}
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
XCTAssertGreaterThanOrEqual(sendCount, 1)
}
// NOTE: MessageRetryService has been removed in SimplifiedBluetoothService.
// Retry logic is now handled internally.
func testRetryQueueOrdering() {
// Add multiple messages to retry queue
let messages = [
(content: "First", timestamp: Date().addingTimeInterval(-10)),
(content: "Second", timestamp: Date().addingTimeInterval(-5)),
(content: "Third", timestamp: Date())
]
for msg in messages {
retryService.addMessageForRetry(
content: msg.content,
isPrivate: true,
recipientPeerID: TestConstants.testPeerID2,
recipientNickname: TestConstants.testNickname2,
originalTimestamp: msg.timestamp
)
}
XCTAssertEqual(retryService.getRetryQueueCount(), 3)
var receivedOrder: [String] = []
let expectation = XCTestExpectation(description: "Messages received in order")
bob.messageDeliveryHandler = { message in
receivedOrder.append(message.content)
if receivedOrder.count == 3 {
expectation.fulfill()
}
}
// Connect to trigger retry
simulateConnection(alice, bob)
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
// Verify order maintained
XCTAssertEqual(receivedOrder, ["First", "Second", "Third"])
}
func testRetryQueueMaxSize() {
// Try to add more than max queue size
for i in 0..<60 {
retryService.addMessageForRetry(
content: "Message \(i)",
recipientPeerID: TestConstants.testPeerID2
)
}
// Should not exceed max size (50)
XCTAssertLessThanOrEqual(retryService.getRetryQueueCount(), 50)
}
// MARK: - End-to-End Encryption Tests
@@ -491,24 +268,7 @@ final class PrivateChatE2ETests: XCTestCase {
// MARK: - Error Handling Tests
func testPrivateMessageToUnknownPeer() {
// Alice not connected to anyone
let expectation = XCTestExpectation(description: "Message added to retry queue")
retryService.addMessageForRetry(
content: TestConstants.testMessage1,
isPrivate: true,
recipientPeerID: TestConstants.testPeerID2,
recipientNickname: TestConstants.testNickname2
)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
XCTAssertEqual(self.retryService.getRetryQueueCount(), 1)
expectation.fulfill()
}
wait(for: [expectation], timeout: TestConstants.shortTimeout)
}
// NOTE: This test relied on MessageRetryService which has been removed
func testDuplicateAckPrevention() {
simulateConnection(alice, bob)
@@ -537,6 +297,8 @@ final class PrivateChatE2ETests: XCTestCase {
)
// Generate multiple ACKs for same message
// NOTE: DeliveryTracker has been removed - this test is no longer applicable
/*
for _ in 0..<3 {
if let ack = deliveryTracker.generateAck(
for: message,
@@ -554,6 +316,7 @@ final class PrivateChatE2ETests: XCTestCase {
alice.simulateIncomingPacket(ackPacket)
}
}
*/
// Should only generate one ACK
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+12 -12
View File
@@ -287,7 +287,7 @@ final class IntegrationTests: XCTestCase {
// Track identity announcements
nodes["Bob"]!.packetDeliveryHandler = { packet in
if packet.type == MessageType.noiseIdentityAnnounce.rawValue {
if packet.type == 0x04 { // noiseIdentityAnnounce was removed
bobReceivedIdentityAnnounce = true
if aliceReceivedIdentityAnnounce {
expectation.fulfill()
@@ -296,7 +296,7 @@ final class IntegrationTests: XCTestCase {
}
nodes["Alice"]!.packetDeliveryHandler = { packet in
if packet.type == MessageType.noiseIdentityAnnounce.rawValue {
if packet.type == 0x04 { // noiseIdentityAnnounce was removed
aliceReceivedIdentityAnnounce = true
if bobReceivedIdentityAnnounce {
expectation.fulfill()
@@ -347,7 +347,7 @@ final class IntegrationTests: XCTestCase {
let handshakeExpectation = XCTestExpectation(description: "New handshake completed")
nodes["Bob"]!.packetDeliveryHandler = { packet in
if packet.type == MessageType.noiseHandshakeInit.rawValue {
if packet.type == 0x05 { // noiseHandshakeInit was removed
// Bob initiates new handshake after restart
do {
let response = try self.noiseManagers["Alice"]!.handleIncomingHandshake(
@@ -357,7 +357,7 @@ final class IntegrationTests: XCTestCase {
if let resp = response {
// Send response back to Bob
let responsePacket = TestHelpers.createTestPacket(
type: MessageType.noiseHandshakeResp.rawValue,
type: 0x06, // noiseHandshakeResp was removed
payload: resp
)
self.nodes["Bob"]!.simulateIncomingPacket(responsePacket)
@@ -365,7 +365,7 @@ final class IntegrationTests: XCTestCase {
} catch {
XCTFail("Handshake handling failed: \(error)")
}
} else if packet.type == MessageType.noiseHandshakeResp.rawValue && packet.senderID.hexEncodedString() == TestConstants.testPeerID1 {
} else if packet.type == 0x06 // noiseHandshakeResp was removed && packet.senderID.hexEncodedString() == TestConstants.testPeerID1 {
// Final handshake message (message 3 in XX pattern)
handshakeExpectation.fulfill()
}
@@ -552,7 +552,7 @@ final class IntegrationTests: XCTestCase {
let nackData = nack.toBinaryData()
let nackPacket = TestHelpers.createTestPacket(
type: MessageType.protocolNack.rawValue,
type: 0x08, // protocolNack was removed
payload: nackData
)
self.nodes["Alice"]!.simulateIncomingPacket(nackPacket)
@@ -560,7 +560,7 @@ final class IntegrationTests: XCTestCase {
// Bob clears session
self.noiseManagers["Bob"]!.removeSession(for: TestConstants.testPeerID1)
}
} else if packet.type == MessageType.noiseHandshakeInit.rawValue {
} else if packet.type == 0x05 { // noiseHandshakeInit was removed
// Bob receives handshake init from Alice after NACK
do {
let response = try self.noiseManagers["Bob"]!.handleIncomingHandshake(
@@ -569,7 +569,7 @@ final class IntegrationTests: XCTestCase {
)
if let resp = response {
let responsePacket = TestHelpers.createTestPacket(
type: MessageType.noiseHandshakeResp.rawValue,
type: 0x06, // noiseHandshakeResp was removed
payload: resp
)
self.nodes["Alice"]!.simulateIncomingPacket(responsePacket)
@@ -582,7 +582,7 @@ final class IntegrationTests: XCTestCase {
// Setup Alice's handler to clear session on NACK and initiate handshake
nodes["Alice"]!.packetDeliveryHandler = { packet in
if packet.type == MessageType.protocolNack.rawValue {
if packet.type == 0x08 { // protocolNack was removed
// Alice receives NACK - clear session
self.noiseManagers["Alice"]!.removeSession(for: TestConstants.testPeerID2)
@@ -590,14 +590,14 @@ final class IntegrationTests: XCTestCase {
do {
let handshakeInit = try self.noiseManagers["Alice"]!.initiateHandshake(with: TestConstants.testPeerID2)
let handshakePacket = TestHelpers.createTestPacket(
type: MessageType.noiseHandshakeInit.rawValue,
type: 0x05, // noiseHandshakeInit was removed
payload: handshakeInit
)
self.nodes["Bob"]!.simulateIncomingPacket(handshakePacket)
} catch {
XCTFail("Alice failed to initiate handshake: \(error)")
}
} else if packet.type == MessageType.noiseHandshakeResp.rawValue {
} else if packet.type == 0x06 // noiseHandshakeResp was removed {
// Complete handshake
do {
let final = try self.noiseManagers["Alice"]!.handleIncomingHandshake(
@@ -606,7 +606,7 @@ final class IntegrationTests: XCTestCase {
)
if let finalMsg = final {
let finalPacket = TestHelpers.createTestPacket(
type: MessageType.noiseHandshakeResp.rawValue,
type: 0x06, // noiseHandshakeResp was removed
payload: finalMsg
)
self.nodes["Bob"]!.simulateIncomingPacket(finalPacket)
@@ -7,139 +7,8 @@
//
import Foundation
import MultipeerConnectivity
import CoreBluetooth
@testable import bitchat
class MockBluetoothMeshService: BluetoothMeshService {
var sentMessages: [(message: BitchatMessage, packet: BitchatPacket)] = []
var sentPackets: [BitchatPacket] = []
var connectedPeers: Set<String> = []
var messageDeliveryHandler: ((BitchatMessage) -> Void)?
var packetDeliveryHandler: ((BitchatPacket) -> Void)?
// Override these properties
var mockNickname: String = "MockUser"
override var myPeerID: String {
didSet {
// Update when changed
}
}
var nickname: String {
return mockNickname
}
var peerID: String {
return myPeerID
}
override init() {
super.init()
self.myPeerID = "MOCK1234"
}
func simulateConnectedPeer(_ peerID: String) {
connectedPeers.insert(peerID)
delegate?.didConnectToPeer(peerID)
delegate?.didUpdatePeerList(Array(connectedPeers))
}
func simulateDisconnectedPeer(_ peerID: String) {
connectedPeers.remove(peerID)
delegate?.didDisconnectFromPeer(peerID)
delegate?.didUpdatePeerList(Array(connectedPeers))
}
override func sendMessage(_ content: String, mentions: [String], to room: String? = nil, messageID: String? = nil, timestamp: Date? = nil) {
let message = BitchatMessage(
id: messageID ?? UUID().uuidString,
sender: mockNickname,
content: content,
timestamp: timestamp ?? Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: myPeerID,
mentions: mentions.isEmpty ? nil : mentions
)
if let payload = message.toBinaryPayload() {
let packet = BitchatPacket(
type: 0x01,
senderID: myPeerID.data(using: .utf8)!,
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: payload,
signature: nil,
ttl: 3
)
sentMessages.append((message, packet))
sentPackets.append(packet)
// Simulate local echo
DispatchQueue.main.async { [weak self] in
self?.delegate?.didReceiveMessage(message)
}
// Call delivery handler if set
messageDeliveryHandler?(message)
}
}
override func sendPrivateMessage(_ content: String, to recipientPeerID: String, recipientNickname: String, messageID: String? = nil) {
let message = BitchatMessage(
id: messageID ?? UUID().uuidString,
sender: mockNickname,
content: content,
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: recipientNickname,
senderPeerID: myPeerID,
mentions: nil
)
if let payload = message.toBinaryPayload() {
let packet = BitchatPacket(
type: 0x01,
senderID: myPeerID.data(using: .utf8)!,
recipientID: recipientPeerID.data(using: .utf8)!,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: payload,
signature: nil,
ttl: 3
)
sentMessages.append((message, packet))
sentPackets.append(packet)
// Simulate local echo
DispatchQueue.main.async { [weak self] in
self?.delegate?.didReceiveMessage(message)
}
// Call delivery handler if set
messageDeliveryHandler?(message)
}
}
func simulateIncomingMessage(_ message: BitchatMessage) {
delegate?.didReceiveMessage(message)
}
func simulateIncomingPacket(_ packet: BitchatPacket) {
// Process through the actual handling logic
if let message = BitchatMessage.fromBinaryPayload(packet.payload) {
delegate?.didReceiveMessage(message)
}
packetDeliveryHandler?(packet)
}
func getConnectedPeers() -> [String] {
return Array(connectedPeers)
}
}
// Compatibility wrapper for old tests - please use MockSimplifiedBluetoothService directly
typealias MockBluetoothMeshService = MockSimplifiedBluetoothService
@@ -0,0 +1,227 @@
//
// MockSimplifiedBluetoothService.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
import CoreBluetooth
@testable import bitchat
// Mock implementation that mimics SimplifiedBluetoothService behavior
class MockSimplifiedBluetoothService: NSObject {
// MARK: - Properties matching SimplifiedBluetoothService
weak var delegate: BitchatDelegate?
var myPeerID: String = "MOCK1234"
var myNickname: String = "MockUser"
// Test-specific properties
var sentMessages: [(message: BitchatMessage, packet: BitchatPacket)] = []
var sentPackets: [BitchatPacket] = []
var connectedPeers: Set<String> = []
var messageDeliveryHandler: ((BitchatMessage) -> Void)?
var packetDeliveryHandler: ((BitchatPacket) -> Void)?
// Compatibility properties for old tests
var mockNickname: String {
get { return myNickname }
set { myNickname = newValue }
}
var nickname: String {
return myNickname
}
var peerID: String {
return myPeerID
}
// MARK: - Initialization
override init() {
super.init()
}
// MARK: - Methods matching SimplifiedBluetoothService
func setNickname(_ nickname: String) {
self.myNickname = nickname
}
func startServices() {
// Mock implementation - do nothing
}
func stopServices() {
// Mock implementation - do nothing
}
func isPeerConnected(_ peerID: String) -> Bool {
return connectedPeers.contains(peerID)
}
func getPeerNicknames() -> [String: String] {
var nicknames: [String: String] = [:]
for peer in connectedPeers {
nicknames[peer] = "MockPeer_\(peer)"
}
return nicknames
}
func getPeers() -> [String: String] {
return getPeerNicknames()
}
func sendMessage(_ content: String, mentions: [String] = [], to recipientID: String? = nil, messageID: String? = nil, timestamp: Date? = nil) {
let message = BitchatMessage(
id: messageID ?? UUID().uuidString,
sender: myNickname,
content: content,
timestamp: timestamp ?? Date(),
isRelay: false,
originalSender: nil,
isPrivate: recipientID != nil,
recipientNickname: nil,
senderPeerID: myPeerID,
mentions: mentions.isEmpty ? nil : mentions
)
if let payload = message.toBinaryPayload() {
let packet = BitchatPacket(
type: 0x01,
senderID: myPeerID.data(using: .utf8)!,
recipientID: recipientID?.data(using: .utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: payload,
signature: nil,
ttl: 3
)
sentMessages.append((message, packet))
sentPackets.append(packet)
// Simulate local echo
DispatchQueue.main.async { [weak self] in
self?.delegate?.didReceiveMessage(message)
}
// Call delivery handler if set
messageDeliveryHandler?(message)
}
}
func sendPrivateMessage(_ content: String, to recipientPeerID: String, recipientNickname: String, messageID: String) {
let message = BitchatMessage(
id: messageID,
sender: myNickname,
content: content,
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: recipientNickname,
senderPeerID: myPeerID,
mentions: nil
)
if let payload = message.toBinaryPayload() {
let packet = BitchatPacket(
type: 0x01,
senderID: myPeerID.data(using: .utf8)!,
recipientID: recipientPeerID.data(using: .utf8)!,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: payload,
signature: nil,
ttl: 3
)
sentMessages.append((message, packet))
sentPackets.append(packet)
// Simulate local echo
DispatchQueue.main.async { [weak self] in
self?.delegate?.didReceiveMessage(message)
}
// Call delivery handler if set
messageDeliveryHandler?(message)
}
}
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
// Mock implementation
}
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String) {
// Mock implementation
}
func sendBroadcastAnnounce() {
// Mock implementation
}
func getPeerFingerprint(_ peerID: String) -> String? {
return nil
}
func getNoiseSessionState(for peerID: String) -> LazyHandshakeState {
return .none
}
func triggerHandshake(with peerID: String) {
// Mock implementation
}
func emergencyDisconnectAll() {
connectedPeers.removeAll()
delegate?.didUpdatePeerList([])
}
func getNoiseService() -> NoiseEncryptionService {
return NoiseEncryptionService()
}
func getFingerprint(for peerID: String) -> String? {
return nil
}
// MARK: - Test Helper Methods
func simulateConnectedPeer(_ peerID: String) {
connectedPeers.insert(peerID)
delegate?.didConnectToPeer(peerID)
delegate?.didUpdatePeerList(Array(connectedPeers))
}
func simulateDisconnectedPeer(_ peerID: String) {
connectedPeers.remove(peerID)
delegate?.didDisconnectFromPeer(peerID)
delegate?.didUpdatePeerList(Array(connectedPeers))
}
func simulateIncomingMessage(_ message: BitchatMessage) {
delegate?.didReceiveMessage(message)
}
func simulateIncomingPacket(_ packet: BitchatPacket) {
// Process through the actual handling logic
if let message = BitchatMessage.fromBinaryPayload(packet.payload) {
delegate?.didReceiveMessage(message)
}
packetDeliveryHandler?(packet)
}
func getConnectedPeers() -> [String] {
return Array(connectedPeers)
}
// MARK: - Compatibility methods for old tests
func sendPrivateMessage(_ content: String, to recipientPeerID: String, recipientNickname: String, messageID: String? = nil) {
sendPrivateMessage(content, to: recipientPeerID, recipientNickname: recipientNickname, messageID: messageID ?? UUID().uuidString)
}
}
+10 -6
View File
@@ -13,8 +13,7 @@ final class NostrProtocolTests: XCTestCase {
override func setUp() {
super.setUp()
// Enable secure logging for tests
SecureLogger.enableLogging()
// SecureLogger is always enabled
}
func testNIP17MessageRoundTrip() throws {
@@ -39,7 +38,7 @@ final class NostrProtocolTests: XCTestCase {
print("Gift wrap pubkey: \(giftWrap.pubkey)")
// Decrypt the gift wrap
let (decryptedContent, senderPubkey) = try NostrProtocol.decryptPrivateMessage(
let (decryptedContent, senderPubkey, timestamp) = try NostrProtocol.decryptPrivateMessage(
giftWrap: giftWrap,
recipientIdentity: recipient
)
@@ -48,7 +47,12 @@ final class NostrProtocolTests: XCTestCase {
XCTAssertEqual(decryptedContent, originalContent)
XCTAssertEqual(senderPubkey, sender.publicKeyHex)
print("✅ Successfully decrypted message: '\(decryptedContent)' from \(senderPubkey)")
// Verify timestamp is reasonable (within last minute)
let messageDate = Date(timeIntervalSince1970: TimeInterval(timestamp))
let timeDiff = abs(messageDate.timeIntervalSinceNow)
XCTAssertLessThan(timeDiff, 60, "Message timestamp should be recent")
print("✅ Successfully decrypted message: '\(decryptedContent)' from \(senderPubkey) at \(messageDate)")
}
func testGiftWrapUsesUniqueEphemeralKeys() throws {
@@ -75,11 +79,11 @@ final class NostrProtocolTests: XCTestCase {
print("Message 2 gift wrap pubkey: \(message2.pubkey)")
// Both should decrypt successfully
let (content1, _) = try NostrProtocol.decryptPrivateMessage(
let (content1, _, _) = try NostrProtocol.decryptPrivateMessage(
giftWrap: message1,
recipientIdentity: recipient
)
let (content2, _) = try NostrProtocol.decryptPrivateMessage(
let (content2, _, _) = try NostrProtocol.decryptPrivateMessage(
giftWrap: message2,
recipientIdentity: recipient
)
@@ -0,0 +1,282 @@
//
// SimplifiedBluetoothServiceTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import XCTest
import CoreBluetooth
@testable import bitchat
final class SimplifiedBluetoothServiceTests: XCTestCase {
var service: MockSimplifiedBluetoothService!
override func setUp() {
super.setUp()
service = MockSimplifiedBluetoothService()
service.myPeerID = "TEST1234"
service.mockNickname = "TestUser"
}
override func tearDown() {
service = nil
super.tearDown()
}
// MARK: - Basic Functionality Tests
func testServiceInitialization() {
XCTAssertNotNil(service)
XCTAssertEqual(service.myPeerID, "TEST1234")
XCTAssertEqual(service.myNickname, "TestUser")
}
func testPeerConnection() {
// Test connecting a peer
service.simulateConnectedPeer("PEER5678")
XCTAssertTrue(service.isPeerConnected("PEER5678"))
XCTAssertEqual(service.getConnectedPeers().count, 1)
// Test disconnecting a peer
service.simulateDisconnectedPeer("PEER5678")
XCTAssertFalse(service.isPeerConnected("PEER5678"))
XCTAssertEqual(service.getConnectedPeers().count, 0)
}
func testMultiplePeerConnections() {
service.simulateConnectedPeer("PEER1")
service.simulateConnectedPeer("PEER2")
service.simulateConnectedPeer("PEER3")
XCTAssertEqual(service.getConnectedPeers().count, 3)
XCTAssertTrue(service.isPeerConnected("PEER1"))
XCTAssertTrue(service.isPeerConnected("PEER2"))
XCTAssertTrue(service.isPeerConnected("PEER3"))
service.simulateDisconnectedPeer("PEER2")
XCTAssertEqual(service.getConnectedPeers().count, 2)
XCTAssertFalse(service.isPeerConnected("PEER2"))
}
// MARK: - Message Sending Tests
func testSendPublicMessage() {
let expectation = XCTestExpectation(description: "Message sent")
service.delegate = MockBitchatDelegate { message in
XCTAssertEqual(message.content, "Hello, world!")
XCTAssertEqual(message.sender, "TestUser")
XCTAssertFalse(message.isPrivate)
expectation.fulfill()
}
service.sendMessage("Hello, world!")
wait(for: [expectation], timeout: 1.0)
XCTAssertEqual(service.sentMessages.count, 1)
}
func testSendPrivateMessage() {
let expectation = XCTestExpectation(description: "Private message sent")
service.delegate = MockBitchatDelegate { message in
XCTAssertEqual(message.content, "Secret message")
XCTAssertEqual(message.sender, "TestUser")
XCTAssertTrue(message.isPrivate)
XCTAssertEqual(message.recipientNickname, "Bob")
expectation.fulfill()
}
service.sendPrivateMessage("Secret message", to: "PEER5678", recipientNickname: "Bob", messageID: "MSG123")
wait(for: [expectation], timeout: 1.0)
XCTAssertEqual(service.sentMessages.count, 1)
}
func testSendMessageWithMentions() {
let expectation = XCTestExpectation(description: "Message with mentions sent")
service.delegate = MockBitchatDelegate { message in
XCTAssertEqual(message.content, "@alice @bob check this out")
XCTAssertEqual(message.mentions, ["alice", "bob"])
expectation.fulfill()
}
service.sendMessage("@alice @bob check this out", mentions: ["alice", "bob"])
wait(for: [expectation], timeout: 1.0)
}
// MARK: - Message Reception Tests
func testSimulateIncomingMessage() {
let expectation = XCTestExpectation(description: "Message received")
service.delegate = MockBitchatDelegate { message in
XCTAssertEqual(message.content, "Incoming message")
XCTAssertEqual(message.sender, "RemoteUser")
expectation.fulfill()
}
let incomingMessage = BitchatMessage(
id: "MSG456",
sender: "RemoteUser",
content: "Incoming message",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "REMOTE123",
mentions: nil
)
service.simulateIncomingMessage(incomingMessage)
wait(for: [expectation], timeout: 1.0)
}
func testSimulateIncomingPacket() {
let expectation = XCTestExpectation(description: "Packet processed")
service.delegate = MockBitchatDelegate { message in
XCTAssertEqual(message.content, "Packet message")
expectation.fulfill()
}
let message = BitchatMessage(
id: "MSG789",
sender: "PacketSender",
content: "Packet message",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "PACKET123",
mentions: nil
)
guard let payload = message.toBinaryPayload() else {
XCTFail("Failed to create binary payload")
return
}
let packet = BitchatPacket(
type: 0x01,
senderID: "PACKET123".data(using: .utf8)!,
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: payload,
signature: nil,
ttl: 3
)
service.simulateIncomingPacket(packet)
wait(for: [expectation], timeout: 1.0)
}
// MARK: - Peer Nickname Tests
func testGetPeerNicknames() {
service.simulateConnectedPeer("PEER1")
service.simulateConnectedPeer("PEER2")
let nicknames = service.getPeerNicknames()
XCTAssertEqual(nicknames.count, 2)
XCTAssertEqual(nicknames["PEER1"], "MockPeer_PEER1")
XCTAssertEqual(nicknames["PEER2"], "MockPeer_PEER2")
}
// MARK: - Service State Tests
func testStartStopServices() {
// These are mock implementations, just ensure they don't crash
service.startServices()
service.stopServices()
// Service should still be functional after start/stop
service.simulateConnectedPeer("PEER999")
XCTAssertTrue(service.isPeerConnected("PEER999"))
}
// MARK: - Message Delivery Handler Tests
func testMessageDeliveryHandler() {
let expectation = XCTestExpectation(description: "Delivery handler called")
service.messageDeliveryHandler = { message in
XCTAssertEqual(message.content, "Test delivery")
expectation.fulfill()
}
service.sendMessage("Test delivery")
wait(for: [expectation], timeout: 1.0)
}
func testPacketDeliveryHandler() {
let expectation = XCTestExpectation(description: "Packet handler called")
service.packetDeliveryHandler = { packet in
XCTAssertEqual(packet.type, 0x01)
expectation.fulfill()
}
let message = BitchatMessage(
id: "PKT123",
sender: "TestSender",
content: "Test packet",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "TEST123",
mentions: nil
)
guard let payload = message.toBinaryPayload() else {
XCTFail("Failed to create payload")
return
}
let packet = BitchatPacket(
type: 0x01,
senderID: "TEST123".data(using: .utf8)!,
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: payload,
signature: nil,
ttl: 3
)
service.simulateIncomingPacket(packet)
wait(for: [expectation], timeout: 1.0)
}
}
// MARK: - Mock Delegate Helper
private class MockBitchatDelegate: BitchatDelegate {
private let messageHandler: (BitchatMessage) -> Void
init(_ handler: @escaping (BitchatMessage) -> Void) {
self.messageHandler = handler
}
func didReceiveMessage(_ message: BitchatMessage) {
messageHandler(message)
}
func didConnectToPeer(_ peerID: String) {}
func didDisconnectFromPeer(_ peerID: String) {}
func didUpdatePeerList(_ peers: [String]) {}
func isFavorite(fingerprint: String) -> Bool { return false }
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {}
}
@@ -0,0 +1,45 @@
//
// LegacyTestProtocolTypes.swift
// bitchatTests
//
// Minimal legacy protocol types used only by tests to simulate old flows.
// These are not part of production code anymore.
import Foundation
struct ProtocolNack {
let originalPacketID: String
let nackID: String
let senderID: String
let receiverID: String
let packetType: UInt8
let reason: String
let errorCode: UInt8
enum ErrorCode: UInt8 {
case unknown = 0
case decryptionFailed = 2
}
init(originalPacketID: String, senderID: String, receiverID: String, packetType: UInt8, reason: String, errorCode: ErrorCode = .unknown) {
self.originalPacketID = originalPacketID
self.nackID = UUID().uuidString
self.senderID = senderID
self.receiverID = receiverID
self.packetType = packetType
self.reason = reason
self.errorCode = errorCode.rawValue
}
func toBinaryData() -> Data {
// Tests don't parse the payload; return a compact encoding for completeness
var data = Data()
data.appendUUID(originalPacketID)
data.appendUUID(nackID)
data.append(UInt8(packetType))
data.append(UInt8(errorCode))
data.appendString(reason)
return data
}
}