mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:25:19 +00:00
Add comprehensive test suite for bitchat
- Created test utilities and helpers for common test operations - Implemented Binary Protocol tests covering encoding/decoding, compression, and padding - Added Noise Protocol tests for handshake, encryption, and session management - Created Public Chat E2E tests for broadcasting, routing, TTL, and mesh topologies - Implemented Private Chat E2E tests for direct messaging, delivery ACKs, and retry logic - Added Integration tests for multi-peer scenarios, network resilience, and mixed traffic patterns - Created mock implementations for BluetoothMeshService and NoiseSession Test coverage includes: - Protocol layer (binary encoding, message serialization) - Security layer (Noise handshake, encryption/decryption) - Application layer (public/private messaging, delivery tracking) - Network scenarios (mesh topology, partitions, churn) - Performance and stress testing
This commit is contained in:
@@ -12,6 +12,22 @@
|
||||
04636BBC2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */; };
|
||||
04636BBF2E2FCA8A00FBCFA8 /* SecureLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */; };
|
||||
04636BC02E2FCA8A00FBCFA8 /* SecureLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */; };
|
||||
04636BD12E30BE5100FBCFA8 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BCC2E30BE5100FBCFA8 /* BinaryProtocolTests.swift */; };
|
||||
04636BD22E30BE5100FBCFA8 /* MockNoiseSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BC82E30BE5100FBCFA8 /* MockNoiseSession.swift */; };
|
||||
04636BD32E30BE5100FBCFA8 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BC42E30BE5100FBCFA8 /* PrivateChatE2ETests.swift */; };
|
||||
04636BD42E30BE5100FBCFA8 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BCE2E30BE5100FBCFA8 /* TestConstants.swift */; };
|
||||
04636BD52E30BE5100FBCFA8 /* MockBluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BC72E30BE5100FBCFA8 /* MockBluetoothMeshService.swift */; };
|
||||
04636BD62E30BE5100FBCFA8 /* NoiseProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BCA2E30BE5100FBCFA8 /* NoiseProtocolTests.swift */; };
|
||||
04636BD72E30BE5100FBCFA8 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BCF2E30BE5100FBCFA8 /* TestHelpers.swift */; };
|
||||
04636BD82E30BE5100FBCFA8 /* PublicChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BC52E30BE5100FBCFA8 /* PublicChatE2ETests.swift */; };
|
||||
04636BD92E30BE5100FBCFA8 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BCC2E30BE5100FBCFA8 /* BinaryProtocolTests.swift */; };
|
||||
04636BDA2E30BE5100FBCFA8 /* MockNoiseSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BC82E30BE5100FBCFA8 /* MockNoiseSession.swift */; };
|
||||
04636BDB2E30BE5100FBCFA8 /* PrivateChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BC42E30BE5100FBCFA8 /* PrivateChatE2ETests.swift */; };
|
||||
04636BDC2E30BE5100FBCFA8 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BCE2E30BE5100FBCFA8 /* TestConstants.swift */; };
|
||||
04636BDD2E30BE5100FBCFA8 /* MockBluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BC72E30BE5100FBCFA8 /* MockBluetoothMeshService.swift */; };
|
||||
04636BDE2E30BE5100FBCFA8 /* NoiseProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BCA2E30BE5100FBCFA8 /* NoiseProtocolTests.swift */; };
|
||||
04636BDF2E30BE5100FBCFA8 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BCF2E30BE5100FBCFA8 /* TestHelpers.swift */; };
|
||||
04636BE02E30BE5100FBCFA8 /* PublicChatE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BC52E30BE5100FBCFA8 /* PublicChatE2ETests.swift */; };
|
||||
04891CA92E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; };
|
||||
04891CAA2E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; };
|
||||
04AD0B4E2E25B9580002A40A /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2446380E7A44E49A35B664 /* IdentityModels.swift */; };
|
||||
@@ -106,6 +122,14 @@
|
||||
03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_macOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryEncodingUtils.swift; sourceTree = "<group>"; };
|
||||
04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureLogger.swift; sourceTree = "<group>"; };
|
||||
04636BC42E30BE5100FBCFA8 /* PrivateChatE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateChatE2ETests.swift; sourceTree = "<group>"; };
|
||||
04636BC52E30BE5100FBCFA8 /* PublicChatE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicChatE2ETests.swift; sourceTree = "<group>"; };
|
||||
04636BC72E30BE5100FBCFA8 /* MockBluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBluetoothMeshService.swift; sourceTree = "<group>"; };
|
||||
04636BC82E30BE5100FBCFA8 /* MockNoiseSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNoiseSession.swift; sourceTree = "<group>"; };
|
||||
04636BCA2E30BE5100FBCFA8 /* NoiseProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseProtocolTests.swift; sourceTree = "<group>"; };
|
||||
04636BCC2E30BE5100FBCFA8 /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = "<group>"; };
|
||||
04636BCE2E30BE5100FBCFA8 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = "<group>"; };
|
||||
04636BCF2E30BE5100FBCFA8 /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = "<group>"; };
|
||||
04891CA82E22971E0064A111 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
|
||||
04B6BA412E2035530090FE39 /* NoiseProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseProtocol.swift; sourceTree = "<group>"; };
|
||||
04B6BA422E2035530090FE39 /* NoiseSecurityConsiderations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSecurityConsiderations.swift; sourceTree = "<group>"; };
|
||||
@@ -145,6 +169,49 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
04636BC62E30BE5100FBCFA8 /* EndToEnd */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
04636BC42E30BE5100FBCFA8 /* PrivateChatE2ETests.swift */,
|
||||
04636BC52E30BE5100FBCFA8 /* PublicChatE2ETests.swift */,
|
||||
);
|
||||
path = EndToEnd;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
04636BC92E30BE5100FBCFA8 /* Mocks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
04636BC72E30BE5100FBCFA8 /* MockBluetoothMeshService.swift */,
|
||||
04636BC82E30BE5100FBCFA8 /* MockNoiseSession.swift */,
|
||||
);
|
||||
path = Mocks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
04636BCB2E30BE5100FBCFA8 /* Noise */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
04636BCA2E30BE5100FBCFA8 /* NoiseProtocolTests.swift */,
|
||||
);
|
||||
path = Noise;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
04636BCD2E30BE5100FBCFA8 /* Protocol */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
04636BCC2E30BE5100FBCFA8 /* BinaryProtocolTests.swift */,
|
||||
);
|
||||
path = Protocol;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
04636BD02E30BE5100FBCFA8 /* TestUtilities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
04636BCE2E30BE5100FBCFA8 /* TestConstants.swift */,
|
||||
04636BCF2E30BE5100FBCFA8 /* TestHelpers.swift */,
|
||||
);
|
||||
path = TestUtilities;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
04B6BA442E2035530090FE39 /* Noise */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -260,6 +327,11 @@
|
||||
C3D98EB3E1B455E321F519F4 /* bitchatTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
04636BC62E30BE5100FBCFA8 /* EndToEnd */,
|
||||
04636BC92E30BE5100FBCFA8 /* Mocks */,
|
||||
04636BCB2E30BE5100FBCFA8 /* Noise */,
|
||||
04636BCD2E30BE5100FBCFA8 /* Protocol */,
|
||||
04636BD02E30BE5100FBCFA8 /* TestUtilities */,
|
||||
D69A18D27F9A565FD6041E12 /* Info.plist */,
|
||||
);
|
||||
path = bitchatTests;
|
||||
@@ -522,6 +594,14 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
04636BD92E30BE5100FBCFA8 /* BinaryProtocolTests.swift in Sources */,
|
||||
04636BDA2E30BE5100FBCFA8 /* MockNoiseSession.swift in Sources */,
|
||||
04636BDB2E30BE5100FBCFA8 /* PrivateChatE2ETests.swift in Sources */,
|
||||
04636BDC2E30BE5100FBCFA8 /* TestConstants.swift in Sources */,
|
||||
04636BDD2E30BE5100FBCFA8 /* MockBluetoothMeshService.swift in Sources */,
|
||||
04636BDE2E30BE5100FBCFA8 /* NoiseProtocolTests.swift in Sources */,
|
||||
04636BDF2E30BE5100FBCFA8 /* TestHelpers.swift in Sources */,
|
||||
04636BE02E30BE5100FBCFA8 /* PublicChatE2ETests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -529,6 +609,14 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
04636BD12E30BE5100FBCFA8 /* BinaryProtocolTests.swift in Sources */,
|
||||
04636BD22E30BE5100FBCFA8 /* MockNoiseSession.swift in Sources */,
|
||||
04636BD32E30BE5100FBCFA8 /* PrivateChatE2ETests.swift in Sources */,
|
||||
04636BD42E30BE5100FBCFA8 /* TestConstants.swift in Sources */,
|
||||
04636BD52E30BE5100FBCFA8 /* MockBluetoothMeshService.swift in Sources */,
|
||||
04636BD62E30BE5100FBCFA8 /* NoiseProtocolTests.swift in Sources */,
|
||||
04636BD72E30BE5100FBCFA8 /* TestHelpers.swift in Sources */,
|
||||
04636BD82E30BE5100FBCFA8 /* PublicChatE2ETests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,560 @@
|
||||
//
|
||||
// PrivateChatE2ETests.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import bitchat
|
||||
|
||||
final class PrivateChatE2ETests: XCTestCase {
|
||||
|
||||
var alice: MockBluetoothMeshService!
|
||||
var bob: MockBluetoothMeshService!
|
||||
var charlie: MockBluetoothMeshService!
|
||||
|
||||
var deliveryTracker: DeliveryTracker!
|
||||
var retryService: MessageRetryService!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
// Create services
|
||||
alice = createMockService(peerID: TestConstants.testPeerID1, nickname: TestConstants.testNickname1)
|
||||
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()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
alice = nil
|
||||
bob = nil
|
||||
charlie = nil
|
||||
deliveryTracker = nil
|
||||
retryService = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - Basic Private Messaging Tests
|
||||
|
||||
func testSimplePrivateMessage() {
|
||||
simulateConnection(alice, bob)
|
||||
|
||||
let expectation = XCTestExpectation(description: "Bob receives private message")
|
||||
|
||||
bob.messageDeliveryHandler = { message in
|
||||
if message.content == TestConstants.testMessage1 &&
|
||||
message.isPrivate &&
|
||||
message.sender == TestConstants.testNickname1 {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
// Alice sends private message to Bob
|
||||
alice.sendPrivateMessage(
|
||||
TestConstants.testMessage1,
|
||||
to: TestConstants.testPeerID2,
|
||||
recipientNickname: TestConstants.testNickname2
|
||||
)
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.shortTimeout)
|
||||
}
|
||||
|
||||
func testPrivateMessageNotReceivedByOthers() {
|
||||
// Connect all three
|
||||
simulateConnection(alice, bob)
|
||||
simulateConnection(alice, charlie)
|
||||
|
||||
let bobExpectation = XCTestExpectation(description: "Bob receives private message")
|
||||
let charlieExpectation = XCTestExpectation(description: "Charlie should not receive")
|
||||
charlieExpectation.isInverted = true
|
||||
|
||||
bob.messageDeliveryHandler = { message in
|
||||
if message.content == TestConstants.testMessage1 && message.isPrivate {
|
||||
bobExpectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
charlie.messageDeliveryHandler = { message in
|
||||
if message.content == TestConstants.testMessage1 {
|
||||
charlieExpectation.fulfill() // Should not happen
|
||||
}
|
||||
}
|
||||
|
||||
// Alice sends private message to Bob only
|
||||
alice.sendPrivateMessage(
|
||||
TestConstants.testMessage1,
|
||||
to: TestConstants.testPeerID2,
|
||||
recipientNickname: TestConstants.testNickname2
|
||||
)
|
||||
|
||||
wait(for: [bobExpectation, charlieExpectation], timeout: TestConstants.shortTimeout)
|
||||
}
|
||||
|
||||
// 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.serialize()
|
||||
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.deserialize(from: packet.payload) {
|
||||
self.deliveryTracker.processDeliveryAck(ack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track the message
|
||||
let message = TestHelpers.createTestMessage(
|
||||
content: TestConstants.testMessage1,
|
||||
isPrivate: true,
|
||||
recipientNickname: TestConstants.testNickname2
|
||||
)
|
||||
var trackedMessage = message
|
||||
trackedMessage.id = messageID
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
func testDeliveryTimeout() {
|
||||
// Don't connect peers - message should timeout
|
||||
let messageID = UUID().uuidString
|
||||
let expectation = XCTestExpectation(description: "Delivery failed due to timeout")
|
||||
|
||||
// Use shorter timeout for testing
|
||||
let shortTimeoutTracker = DeliveryTracker()
|
||||
|
||||
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 message = TestHelpers.createTestMessage(
|
||||
content: TestConstants.testMessage1,
|
||||
isPrivate: true,
|
||||
recipientNickname: TestConstants.testNickname2
|
||||
)
|
||||
var trackedMessage = message
|
||||
trackedMessage.id = messageID
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
func testPrivateMessageEncryption() {
|
||||
simulateConnection(alice, bob)
|
||||
|
||||
// Setup Noise sessions
|
||||
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
|
||||
let bobKey = Curve25519.KeyAgreement.PrivateKey()
|
||||
|
||||
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
|
||||
let bobManager = NoiseSessionManager(localStaticKey: bobKey)
|
||||
|
||||
// Establish encrypted session
|
||||
do {
|
||||
let handshake1 = try aliceManager.initiateHandshake(with: TestConstants.testPeerID2)
|
||||
let handshake2 = try bobManager.handleIncomingHandshake(from: TestConstants.testPeerID1, message: handshake1)!
|
||||
let handshake3 = try aliceManager.handleIncomingHandshake(from: TestConstants.testPeerID2, message: handshake2)!
|
||||
_ = try bobManager.handleIncomingHandshake(from: TestConstants.testPeerID1, message: handshake3)
|
||||
} catch {
|
||||
XCTFail("Failed to establish Noise session: \(error)")
|
||||
}
|
||||
|
||||
let expectation = XCTestExpectation(description: "Encrypted message received")
|
||||
|
||||
// Setup packet handlers for encryption
|
||||
alice.packetDeliveryHandler = { packet in
|
||||
// Encrypt outgoing private messages
|
||||
if packet.type == 0x01,
|
||||
let message = BitchatMessage.fromBinaryPayload(packet.payload),
|
||||
message.isPrivate {
|
||||
do {
|
||||
let encrypted = try aliceManager.encrypt(packet.payload, for: TestConstants.testPeerID2)
|
||||
var encryptedPacket = packet
|
||||
encryptedPacket.type = 0x02 // Encrypted message type
|
||||
encryptedPacket.payload = encrypted
|
||||
self.bob.simulateIncomingPacket(encryptedPacket)
|
||||
} catch {
|
||||
XCTFail("Encryption failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bob.packetDeliveryHandler = { packet in
|
||||
// Decrypt incoming encrypted messages
|
||||
if packet.type == 0x02 {
|
||||
do {
|
||||
let decrypted = try bobManager.decrypt(packet.payload, from: TestConstants.testPeerID1)
|
||||
if let message = BitchatMessage.fromBinaryPayload(decrypted) {
|
||||
XCTAssertEqual(message.content, TestConstants.testMessage1)
|
||||
XCTAssertTrue(message.isPrivate)
|
||||
expectation.fulfill()
|
||||
}
|
||||
} catch {
|
||||
XCTFail("Decryption failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send encrypted private message
|
||||
alice.sendPrivateMessage(
|
||||
TestConstants.testMessage1,
|
||||
to: TestConstants.testPeerID2,
|
||||
recipientNickname: TestConstants.testNickname2
|
||||
)
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
}
|
||||
|
||||
// MARK: - Multi-hop Private Message Tests
|
||||
|
||||
func testPrivateMessageRelay() {
|
||||
// Setup: Alice -> Bob -> Charlie
|
||||
simulateConnection(alice, bob)
|
||||
simulateConnection(bob, charlie)
|
||||
|
||||
let expectation = XCTestExpectation(description: "Private message relayed to Charlie")
|
||||
|
||||
// Bob relays private messages for Charlie
|
||||
bob.packetDeliveryHandler = { packet in
|
||||
if let recipientID = packet.recipientID,
|
||||
String(data: recipientID, encoding: .utf8) == TestConstants.testPeerID3 {
|
||||
// Relay to Charlie
|
||||
var relayPacket = packet
|
||||
relayPacket.ttl = packet.ttl - 1
|
||||
self.charlie.simulateIncomingPacket(relayPacket)
|
||||
}
|
||||
}
|
||||
|
||||
charlie.messageDeliveryHandler = { message in
|
||||
if message.content == TestConstants.testMessage1 &&
|
||||
message.isPrivate &&
|
||||
message.recipientNickname == TestConstants.testNickname3 {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
// Alice sends private message to Charlie (through Bob)
|
||||
alice.sendPrivateMessage(
|
||||
TestConstants.testMessage1,
|
||||
to: TestConstants.testPeerID3,
|
||||
recipientNickname: TestConstants.testNickname3
|
||||
)
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
}
|
||||
|
||||
// MARK: - Performance Tests
|
||||
|
||||
func testPrivateMessageThroughput() {
|
||||
simulateConnection(alice, bob)
|
||||
|
||||
let messageCount = 100
|
||||
var receivedCount = 0
|
||||
let expectation = XCTestExpectation(description: "All private messages received")
|
||||
|
||||
bob.messageDeliveryHandler = { message in
|
||||
if message.isPrivate && message.sender == TestConstants.testNickname1 {
|
||||
receivedCount += 1
|
||||
if receivedCount == messageCount {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send many private messages
|
||||
for i in 0..<messageCount {
|
||||
alice.sendPrivateMessage(
|
||||
"Private message \(i)",
|
||||
to: TestConstants.testPeerID2,
|
||||
recipientNickname: TestConstants.testNickname2
|
||||
)
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.longTimeout)
|
||||
XCTAssertEqual(receivedCount, messageCount)
|
||||
}
|
||||
|
||||
func testLargePrivateMessage() {
|
||||
simulateConnection(alice, bob)
|
||||
|
||||
let expectation = XCTestExpectation(description: "Large private message received")
|
||||
|
||||
bob.messageDeliveryHandler = { message in
|
||||
if message.content == TestConstants.testLongMessage && message.isPrivate {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
alice.sendPrivateMessage(
|
||||
TestConstants.testLongMessage,
|
||||
to: TestConstants.testPeerID2,
|
||||
recipientNickname: TestConstants.testNickname2
|
||||
)
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func testDuplicateAckPrevention() {
|
||||
simulateConnection(alice, bob)
|
||||
|
||||
let messageID = UUID().uuidString
|
||||
var ackCount = 0
|
||||
|
||||
alice.packetDeliveryHandler = { packet in
|
||||
if packet.type == 0x03 {
|
||||
ackCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
// Create message
|
||||
let message = 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
|
||||
)
|
||||
|
||||
// Generate multiple ACKs for same message
|
||||
for _ in 0..<3 {
|
||||
if let ack = deliveryTracker.generateAck(
|
||||
for: message,
|
||||
myPeerID: TestConstants.testPeerID2,
|
||||
myNickname: TestConstants.testNickname2,
|
||||
hopCount: 1
|
||||
) {
|
||||
let ackData = ack.serialize()
|
||||
let ackPacket = TestHelpers.createTestPacket(
|
||||
type: 0x03,
|
||||
senderID: TestConstants.testPeerID2,
|
||||
recipientID: TestConstants.testPeerID1,
|
||||
payload: ackData
|
||||
)
|
||||
alice.simulateIncomingPacket(ackPacket)
|
||||
}
|
||||
}
|
||||
|
||||
// Should only generate one ACK
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
XCTAssertEqual(ackCount, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func createMockService(peerID: String, nickname: String) -> MockBluetoothMeshService {
|
||||
let service = MockBluetoothMeshService()
|
||||
service.peerID = peerID
|
||||
service.nickname = nickname
|
||||
return service
|
||||
}
|
||||
|
||||
private func simulateConnection(_ peer1: MockBluetoothMeshService, _ peer2: MockBluetoothMeshService) {
|
||||
peer1.simulateConnectedPeer(peer2.peerID)
|
||||
peer2.simulateConnectedPeer(peer1.peerID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
//
|
||||
// PublicChatE2ETests.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import bitchat
|
||||
|
||||
final class PublicChatE2ETests: XCTestCase {
|
||||
|
||||
var alice: MockBluetoothMeshService!
|
||||
var bob: MockBluetoothMeshService!
|
||||
var charlie: MockBluetoothMeshService!
|
||||
var david: MockBluetoothMeshService!
|
||||
|
||||
var receivedMessages: [String: [BitchatMessage]] = [:]
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
// Create mock services
|
||||
alice = createMockService(peerID: TestConstants.testPeerID1, nickname: TestConstants.testNickname1)
|
||||
bob = createMockService(peerID: TestConstants.testPeerID2, nickname: TestConstants.testNickname2)
|
||||
charlie = createMockService(peerID: TestConstants.testPeerID3, nickname: TestConstants.testNickname3)
|
||||
david = createMockService(peerID: TestConstants.testPeerID4, nickname: TestConstants.testNickname4)
|
||||
|
||||
// Clear received messages
|
||||
receivedMessages.removeAll()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
alice = nil
|
||||
bob = nil
|
||||
charlie = nil
|
||||
david = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - Basic Broadcasting Tests
|
||||
|
||||
func testSimplePublicMessage() {
|
||||
// Connect Alice and Bob
|
||||
simulateConnection(alice, bob)
|
||||
|
||||
let expectation = XCTestExpectation(description: "Bob receives message")
|
||||
|
||||
bob.messageDeliveryHandler = { message in
|
||||
if message.content == TestConstants.testMessage1 && message.sender == TestConstants.testNickname1 {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
// Alice sends public message
|
||||
alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil)
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.shortTimeout)
|
||||
}
|
||||
|
||||
func testMultiRecipientBroadcast() {
|
||||
// Connect Alice to Bob and Charlie
|
||||
simulateConnection(alice, bob)
|
||||
simulateConnection(alice, charlie)
|
||||
|
||||
let bobExpectation = XCTestExpectation(description: "Bob receives message")
|
||||
let charlieExpectation = XCTestExpectation(description: "Charlie receives message")
|
||||
|
||||
bob.messageDeliveryHandler = { message in
|
||||
if message.content == TestConstants.testMessage1 {
|
||||
bobExpectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
charlie.messageDeliveryHandler = { message in
|
||||
if message.content == TestConstants.testMessage1 {
|
||||
charlieExpectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
// Alice broadcasts
|
||||
alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil)
|
||||
|
||||
wait(for: [bobExpectation, charlieExpectation], timeout: TestConstants.shortTimeout)
|
||||
}
|
||||
|
||||
// MARK: - Message Routing and Relay Tests
|
||||
|
||||
func testMessageRelayChain() {
|
||||
// Linear topology: Alice -> Bob -> Charlie
|
||||
simulateConnection(alice, bob)
|
||||
simulateConnection(bob, charlie)
|
||||
|
||||
let expectation = XCTestExpectation(description: "Charlie receives relayed message")
|
||||
|
||||
// Set up relay in Bob
|
||||
bob.packetDeliveryHandler = { packet in
|
||||
// Bob should relay to Charlie
|
||||
if let message = BitchatMessage.fromBinaryPayload(packet.payload),
|
||||
message.sender == TestConstants.testNickname1 {
|
||||
|
||||
// Create relay message
|
||||
var relayMessage = message
|
||||
relayMessage.isRelay = true
|
||||
relayMessage.originalSender = message.sender
|
||||
|
||||
if let relayPayload = relayMessage.toBinaryPayload() {
|
||||
var relayPacket = packet
|
||||
relayPacket.payload = relayPayload
|
||||
relayPacket.ttl = packet.ttl - 1
|
||||
|
||||
// Simulate relay to Charlie
|
||||
self.charlie.simulateIncomingPacket(relayPacket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
charlie.messageDeliveryHandler = { message in
|
||||
if message.content == TestConstants.testMessage1 &&
|
||||
message.originalSender == TestConstants.testNickname1 &&
|
||||
message.isRelay {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
// Alice sends message
|
||||
alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil)
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
}
|
||||
|
||||
func testMultiHopRelay() {
|
||||
// Topology: Alice -> Bob -> Charlie -> David
|
||||
simulateConnection(alice, bob)
|
||||
simulateConnection(bob, charlie)
|
||||
simulateConnection(charlie, david)
|
||||
|
||||
let expectation = XCTestExpectation(description: "David receives multi-hop message")
|
||||
|
||||
// Set up relay chain
|
||||
setupRelayHandler(bob, nextHops: [charlie])
|
||||
setupRelayHandler(charlie, nextHops: [david])
|
||||
|
||||
david.messageDeliveryHandler = { message in
|
||||
if message.content == TestConstants.testMessage1 &&
|
||||
message.originalSender == TestConstants.testNickname1 &&
|
||||
message.isRelay {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
// Alice sends message
|
||||
alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil)
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
}
|
||||
|
||||
// MARK: - TTL (Time To Live) Tests
|
||||
|
||||
func testTTLDecrement() {
|
||||
// Create a chain longer than TTL
|
||||
let nodes = [alice!, bob!, charlie!, david!]
|
||||
|
||||
// Connect in chain
|
||||
for i in 0..<nodes.count-1 {
|
||||
simulateConnection(nodes[i], nodes[i+1])
|
||||
if i > 0 && i < nodes.count-1 {
|
||||
setupRelayHandler(nodes[i], nextHops: [nodes[i+1]])
|
||||
}
|
||||
}
|
||||
|
||||
let expectation = XCTestExpectation(description: "Message dropped due to TTL")
|
||||
expectation.isInverted = true // Should NOT be fulfilled
|
||||
|
||||
david.messageDeliveryHandler = { message in
|
||||
if message.content == TestConstants.testMessage1 {
|
||||
expectation.fulfill() // This should not happen
|
||||
}
|
||||
}
|
||||
|
||||
// Send message with TTL=2 (should reach Charlie but not David)
|
||||
alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil)
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.shortTimeout)
|
||||
}
|
||||
|
||||
func testZeroTTLNotRelayed() {
|
||||
simulateConnection(alice, bob)
|
||||
simulateConnection(bob, charlie)
|
||||
|
||||
let expectation = XCTestExpectation(description: "Zero TTL message not relayed")
|
||||
expectation.isInverted = true
|
||||
|
||||
charlie.messageDeliveryHandler = { message in
|
||||
if message.content == "Zero TTL message" {
|
||||
expectation.fulfill() // Should not happen
|
||||
}
|
||||
}
|
||||
|
||||
// Create packet with TTL=0
|
||||
let message = TestHelpers.createTestMessage(content: "Zero TTL message")
|
||||
if let payload = message.toBinaryPayload() {
|
||||
let packet = TestHelpers.createTestPacket(payload: payload, ttl: 0)
|
||||
alice.simulateIncomingPacket(packet)
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.shortTimeout)
|
||||
}
|
||||
|
||||
// MARK: - Duplicate Detection Tests
|
||||
|
||||
func testDuplicateMessagePrevention() {
|
||||
simulateConnection(alice, bob)
|
||||
|
||||
var messageCount = 0
|
||||
let expectation = XCTestExpectation(description: "Only one message received")
|
||||
|
||||
bob.messageDeliveryHandler = { message in
|
||||
if message.content == TestConstants.testMessage1 {
|
||||
messageCount += 1
|
||||
if messageCount == 1 {
|
||||
// Send duplicate after small delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
self.alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil, messageID: message.id)
|
||||
}
|
||||
} else {
|
||||
XCTFail("Duplicate message was not filtered")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send original message
|
||||
alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil)
|
||||
|
||||
// Wait to ensure duplicate would have been received
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
XCTAssertEqual(messageCount, 1)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
}
|
||||
|
||||
// MARK: - Mention Tests
|
||||
|
||||
func testMessageWithMentions() {
|
||||
simulateConnection(alice, bob)
|
||||
simulateConnection(alice, charlie)
|
||||
|
||||
let expectation = XCTestExpectation(description: "Mentioned users receive notification")
|
||||
|
||||
var mentionedUsers: Set<String> = []
|
||||
|
||||
bob.messageDeliveryHandler = { message in
|
||||
if let mentions = message.mentions, mentions.contains(TestConstants.testNickname2) {
|
||||
mentionedUsers.insert(TestConstants.testNickname2)
|
||||
}
|
||||
}
|
||||
|
||||
charlie.messageDeliveryHandler = { message in
|
||||
if let mentions = message.mentions, mentions.contains(TestConstants.testNickname3) {
|
||||
mentionedUsers.insert(TestConstants.testNickname3)
|
||||
}
|
||||
}
|
||||
|
||||
// Alice mentions Bob and Charlie
|
||||
alice.sendMessage(
|
||||
"Hey @\(TestConstants.testNickname2) and @\(TestConstants.testNickname3)!",
|
||||
mentions: [TestConstants.testNickname2, TestConstants.testNickname3],
|
||||
to: nil
|
||||
)
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
XCTAssertEqual(mentionedUsers, [TestConstants.testNickname2, TestConstants.testNickname3])
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
}
|
||||
|
||||
// MARK: - Network Topology Tests
|
||||
|
||||
func testMeshTopologyBroadcast() {
|
||||
// Create mesh: Everyone connected to everyone
|
||||
let nodes = [alice!, bob!, charlie!, david!]
|
||||
for i in 0..<nodes.count {
|
||||
for j in i+1..<nodes.count {
|
||||
simulateConnection(nodes[i], nodes[j])
|
||||
}
|
||||
}
|
||||
|
||||
var receivedCount = 0
|
||||
let expectation = XCTestExpectation(description: "All nodes receive message")
|
||||
|
||||
for (index, node) in nodes.enumerated() where index > 0 {
|
||||
node.messageDeliveryHandler = { message in
|
||||
if message.content == TestConstants.testMessage1 {
|
||||
receivedCount += 1
|
||||
if receivedCount == 3 { // Bob, Charlie, David
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alice broadcasts
|
||||
alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil)
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
}
|
||||
|
||||
func testPartialMeshRelay() {
|
||||
// Partial mesh: Alice -> Bob, Bob -> Charlie, Charlie -> David, David -> Alice
|
||||
simulateConnection(alice, bob)
|
||||
simulateConnection(bob, charlie)
|
||||
simulateConnection(charlie, david)
|
||||
simulateConnection(david, alice)
|
||||
|
||||
// Setup relay handlers
|
||||
setupRelayHandler(bob, nextHops: [charlie])
|
||||
setupRelayHandler(charlie, nextHops: [david])
|
||||
setupRelayHandler(david, nextHops: [alice])
|
||||
|
||||
var messageIDs = Set<String>()
|
||||
let expectation = XCTestExpectation(description: "Message reaches all nodes once")
|
||||
|
||||
let checkCompletion = {
|
||||
if messageIDs.count == 3 { // Bob, Charlie, David should receive
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
for node in [bob!, charlie!, david!] {
|
||||
node.messageDeliveryHandler = { message in
|
||||
if message.content == TestConstants.testMessage1 {
|
||||
messageIDs.insert(message.id)
|
||||
checkCompletion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alice broadcasts
|
||||
alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil)
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
}
|
||||
|
||||
// MARK: - Performance and Stress Tests
|
||||
|
||||
func testHighVolumeMessaging() {
|
||||
simulateConnection(alice, bob)
|
||||
|
||||
let messageCount = 100
|
||||
var receivedCount = 0
|
||||
let expectation = XCTestExpectation(description: "All messages received")
|
||||
|
||||
bob.messageDeliveryHandler = { message in
|
||||
if message.sender == TestConstants.testNickname1 {
|
||||
receivedCount += 1
|
||||
if receivedCount == messageCount {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send many messages rapidly
|
||||
for i in 0..<messageCount {
|
||||
alice.sendMessage("Message \(i)", mentions: [], to: nil)
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.longTimeout)
|
||||
XCTAssertEqual(receivedCount, messageCount)
|
||||
}
|
||||
|
||||
func testLargeMessageBroadcast() {
|
||||
simulateConnection(alice, bob)
|
||||
|
||||
let expectation = XCTestExpectation(description: "Large message received")
|
||||
|
||||
bob.messageDeliveryHandler = { message in
|
||||
if message.content == TestConstants.testLongMessage {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
// Send large message
|
||||
alice.sendMessage(TestConstants.testLongMessage, mentions: [], to: nil)
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func createMockService(peerID: String, nickname: String) -> MockBluetoothMeshService {
|
||||
let service = MockBluetoothMeshService()
|
||||
service.peerID = peerID
|
||||
service.nickname = nickname
|
||||
return service
|
||||
}
|
||||
|
||||
private func simulateConnection(_ peer1: MockBluetoothMeshService, _ peer2: MockBluetoothMeshService) {
|
||||
peer1.simulateConnectedPeer(peer2.peerID)
|
||||
peer2.simulateConnectedPeer(peer1.peerID)
|
||||
}
|
||||
|
||||
private func setupRelayHandler(_ node: MockBluetoothMeshService, nextHops: [MockBluetoothMeshService]) {
|
||||
node.packetDeliveryHandler = { packet in
|
||||
// Check if should relay
|
||||
guard packet.ttl > 1 else { return }
|
||||
|
||||
if let message = BitchatMessage.fromBinaryPayload(packet.payload) {
|
||||
// Don't relay own messages
|
||||
guard message.senderPeerID != node.peerID else { return }
|
||||
|
||||
// Create relay message
|
||||
var relayMessage = message
|
||||
if !relayMessage.isRelay {
|
||||
relayMessage.isRelay = true
|
||||
relayMessage.originalSender = message.sender
|
||||
}
|
||||
|
||||
if let relayPayload = relayMessage.toBinaryPayload() {
|
||||
var relayPacket = packet
|
||||
relayPacket.payload = relayPayload
|
||||
relayPacket.ttl = packet.ttl - 1
|
||||
relayPacket.senderID = node.peerID.data(using: .utf8)!
|
||||
|
||||
// Relay to next hops
|
||||
for nextHop in nextHops {
|
||||
nextHop.simulateIncomingPacket(relayPacket)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
//
|
||||
// IntegrationTests.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import bitchat
|
||||
|
||||
final class IntegrationTests: XCTestCase {
|
||||
|
||||
var nodes: [String: MockBluetoothMeshService] = [:]
|
||||
var noiseManagers: [String: NoiseSessionManager] = [:]
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
// Create a network of nodes
|
||||
createNode("Alice", peerID: TestConstants.testPeerID1)
|
||||
createNode("Bob", peerID: TestConstants.testPeerID2)
|
||||
createNode("Charlie", peerID: TestConstants.testPeerID3)
|
||||
createNode("David", peerID: TestConstants.testPeerID4)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
nodes.removeAll()
|
||||
noiseManagers.removeAll()
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - Multi-Peer Scenarios
|
||||
|
||||
func testFullMeshCommunication() {
|
||||
// Create full mesh - everyone connected to everyone
|
||||
connectFullMesh()
|
||||
|
||||
let expectation = XCTestExpectation(description: "All nodes communicate")
|
||||
var messageMatrix: [String: Set<String>] = [:]
|
||||
|
||||
// Each node should receive messages from all others
|
||||
for (senderName, sender) in nodes {
|
||||
messageMatrix[senderName] = []
|
||||
|
||||
for (receiverName, receiver) in nodes where receiverName != senderName {
|
||||
receiver.messageDeliveryHandler = { message in
|
||||
if message.content.contains("from \(senderName)") {
|
||||
messageMatrix[message.content.components(separatedBy: " ").last!]?.insert(receiverName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Each node sends a message
|
||||
for (name, node) in nodes {
|
||||
node.sendMessage("Hello from \(name)", mentions: [], to: nil)
|
||||
}
|
||||
|
||||
// Wait and verify
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
// Each sender should have reached all other nodes
|
||||
for (sender, receivers) in messageMatrix {
|
||||
let expectedReceivers = Set(self.nodes.keys.filter { $0 != sender })
|
||||
XCTAssertEqual(receivers, expectedReceivers, "\(sender) didn't reach all nodes")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
}
|
||||
|
||||
func testDynamicTopologyChanges() {
|
||||
// Start with Alice -> Bob -> Charlie
|
||||
connect("Alice", "Bob")
|
||||
connect("Bob", "Charlie")
|
||||
|
||||
let expectation = XCTestExpectation(description: "Topology changes handled")
|
||||
var phase = 1
|
||||
|
||||
// Phase 1: Test initial topology
|
||||
nodes["Charlie"]!.messageDeliveryHandler = { message in
|
||||
if phase == 1 && message.sender == "Alice" {
|
||||
// Now change topology: disconnect Bob, connect Alice-Charlie
|
||||
self.disconnect("Alice", "Bob")
|
||||
self.disconnect("Bob", "Charlie")
|
||||
self.connect("Alice", "Charlie")
|
||||
phase = 2
|
||||
|
||||
// Send another message
|
||||
self.nodes["Alice"]!.sendMessage("Direct message", mentions: [], to: nil)
|
||||
} else if phase == 2 && message.content == "Direct message" {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
// Initial message through relay
|
||||
nodes["Alice"]!.sendMessage("Relayed message", mentions: [], to: nil)
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
}
|
||||
|
||||
func testNetworkPartitionRecovery() {
|
||||
// Create two partitions
|
||||
connect("Alice", "Bob")
|
||||
connect("Charlie", "David")
|
||||
|
||||
let expectation = XCTestExpectation(description: "Partitions merge and communicate")
|
||||
var messagesBeforeMerge = 0
|
||||
var messagesAfterMerge = 0
|
||||
|
||||
// Monitor cross-partition messages
|
||||
nodes["David"]!.messageDeliveryHandler = { message in
|
||||
if message.sender == "Alice" {
|
||||
messagesAfterMerge += 1
|
||||
if messagesAfterMerge == 1 {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to send across partition (should fail)
|
||||
nodes["Alice"]!.sendMessage("Before merge", mentions: [], to: nil)
|
||||
|
||||
// Merge partitions after delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
// Connect partitions
|
||||
self.connect("Bob", "Charlie")
|
||||
|
||||
// Enable relay
|
||||
self.setupRelay("Bob", nextHops: ["Charlie"])
|
||||
self.setupRelay("Charlie", nextHops: ["David"])
|
||||
|
||||
// Send message across merged network
|
||||
self.nodes["Alice"]!.sendMessage("After merge", mentions: [], to: nil)
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
XCTAssertEqual(messagesBeforeMerge, 0)
|
||||
XCTAssertEqual(messagesAfterMerge, 1)
|
||||
}
|
||||
|
||||
// MARK: - Mixed Message Type Scenarios
|
||||
|
||||
func testMixedPublicPrivateMessages() throws {
|
||||
connectFullMesh()
|
||||
|
||||
let expectation = XCTestExpectation(description: "Mixed messages handled correctly")
|
||||
var publicCount = 0
|
||||
var privateCount = 0
|
||||
|
||||
// Bob monitors messages
|
||||
nodes["Bob"]!.messageDeliveryHandler = { message in
|
||||
if message.isPrivate && message.recipientNickname == "Bob" {
|
||||
privateCount += 1
|
||||
} else if !message.isPrivate {
|
||||
publicCount += 1
|
||||
}
|
||||
|
||||
if publicCount == 2 && privateCount == 1 {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
// Alice sends mixed messages
|
||||
nodes["Alice"]!.sendMessage("Public 1", mentions: [], to: nil)
|
||||
nodes["Alice"]!.sendPrivateMessage("Private to Bob", to: TestConstants.testPeerID2, recipientNickname: "Bob")
|
||||
nodes["Alice"]!.sendMessage("Public 2", mentions: [], to: nil)
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
XCTAssertEqual(publicCount, 2)
|
||||
XCTAssertEqual(privateCount, 1)
|
||||
}
|
||||
|
||||
func testEncryptedAndUnencryptedMix() throws {
|
||||
connect("Alice", "Bob")
|
||||
|
||||
// Setup Noise session
|
||||
try establishNoiseSession("Alice", "Bob")
|
||||
|
||||
let expectation = XCTestExpectation(description: "Both encrypted and plain messages work")
|
||||
var plainCount = 0
|
||||
var encryptedCount = 0
|
||||
|
||||
// Setup handlers
|
||||
nodes["Alice"]!.packetDeliveryHandler = { packet in
|
||||
if packet.type == 0x01 { // Plain message
|
||||
self.nodes["Bob"]!.simulateIncomingPacket(packet)
|
||||
} else if packet.type == 0x02 { // Would be encrypted
|
||||
// Simulate encryption
|
||||
if let encrypted = try? self.noiseManagers["Alice"]!.encrypt(packet.payload, for: TestConstants.testPeerID2) {
|
||||
var encPacket = packet
|
||||
encPacket.payload = encrypted
|
||||
self.nodes["Bob"]!.simulateIncomingPacket(encPacket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nodes["Bob"]!.packetDeliveryHandler = { packet in
|
||||
if packet.type == 0x01 {
|
||||
plainCount += 1
|
||||
} else if packet.type == 0x02 {
|
||||
if let decrypted = try? self.noiseManagers["Bob"]!.decrypt(packet.payload, from: TestConstants.testPeerID1) {
|
||||
encryptedCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
if plainCount == 1 && encryptedCount == 1 {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
// Send both types
|
||||
nodes["Alice"]!.sendMessage("Plain message", mentions: [], to: nil)
|
||||
|
||||
// Send "encrypted" message
|
||||
let encMessage = TestHelpers.createTestMessage(content: "Encrypted message")
|
||||
if let payload = encMessage.toBinaryPayload() {
|
||||
let packet = TestHelpers.createTestPacket(type: 0x02, payload: payload)
|
||||
nodes["Alice"]!.simulateIncomingPacket(packet)
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
}
|
||||
|
||||
// MARK: - Network Resilience Tests
|
||||
|
||||
func testMessageDeliveryUnderChurn() {
|
||||
// Start with stable network
|
||||
connectFullMesh()
|
||||
|
||||
let expectation = XCTestExpectation(description: "Messages delivered despite churn")
|
||||
var receivedMessages = Set<String>()
|
||||
let totalMessages = 10
|
||||
|
||||
// David tracks received messages
|
||||
nodes["David"]!.messageDeliveryHandler = { message in
|
||||
receivedMessages.insert(message.content)
|
||||
if receivedMessages.count == totalMessages {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
// Send messages while churning network
|
||||
for i in 0..<totalMessages {
|
||||
nodes["Alice"]!.sendMessage("Message \(i)", mentions: [], to: nil)
|
||||
|
||||
// Simulate churn
|
||||
if i % 3 == 0 {
|
||||
// Disconnect and reconnect random connection
|
||||
let pairs = [("Alice", "Bob"), ("Bob", "Charlie"), ("Charlie", "David")]
|
||||
let randomPair = pairs.randomElement()!
|
||||
disconnect(randomPair.0, randomPair.1)
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
self.connect(randomPair.0, randomPair.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.longTimeout)
|
||||
XCTAssertEqual(receivedMessages.count, totalMessages)
|
||||
}
|
||||
|
||||
func testLargeScaleNetwork() {
|
||||
// Create larger network
|
||||
for i in 5...10 {
|
||||
createNode("Node\(i)", peerID: "PEER\(i)")
|
||||
}
|
||||
|
||||
// Connect in ring topology with cross-connections
|
||||
let allNodes = Array(nodes.keys).sorted()
|
||||
for i in 0..<allNodes.count {
|
||||
// Ring connection
|
||||
connect(allNodes[i], allNodes[(i + 1) % allNodes.count])
|
||||
|
||||
// Cross connection
|
||||
if i + 3 < allNodes.count {
|
||||
connect(allNodes[i], allNodes[i + 3])
|
||||
}
|
||||
}
|
||||
|
||||
let expectation = XCTestExpectation(description: "Large network handles broadcast")
|
||||
var nodesReached = Set<String>()
|
||||
|
||||
// All nodes except Alice listen
|
||||
for (name, node) in nodes where name != "Alice" {
|
||||
node.messageDeliveryHandler = { message in
|
||||
if message.content == "Broadcast test" {
|
||||
nodesReached.insert(name)
|
||||
if nodesReached.count == self.nodes.count - 1 {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alice broadcasts
|
||||
nodes["Alice"]!.sendMessage("Broadcast test", mentions: [], to: nil)
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.longTimeout)
|
||||
XCTAssertEqual(nodesReached.count, nodes.count - 1)
|
||||
}
|
||||
|
||||
// MARK: - Stress Tests
|
||||
|
||||
func testHighLoadScenario() {
|
||||
connectFullMesh()
|
||||
|
||||
let messagesPerNode = 25
|
||||
let expectedTotal = messagesPerNode * nodes.count * (nodes.count - 1)
|
||||
var receivedTotal = 0
|
||||
let expectation = XCTestExpectation(description: "High load handled")
|
||||
|
||||
// Each node tracks messages
|
||||
for (_, node) in nodes {
|
||||
node.messageDeliveryHandler = { _ in
|
||||
receivedTotal += 1
|
||||
if receivedTotal == expectedTotal {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All nodes send many messages simultaneously
|
||||
DispatchQueue.concurrentPerform(iterations: nodes.count) { index in
|
||||
let nodeName = Array(nodes.keys).sorted()[index]
|
||||
for i in 0..<messagesPerNode {
|
||||
nodes[nodeName]!.sendMessage("\(nodeName) message \(i)", mentions: [], to: nil)
|
||||
}
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.longTimeout)
|
||||
XCTAssertEqual(receivedTotal, expectedTotal)
|
||||
}
|
||||
|
||||
func testMixedTrafficPatterns() {
|
||||
connectFullMesh()
|
||||
|
||||
let expectation = XCTestExpectation(description: "Mixed traffic handled")
|
||||
var metrics = [
|
||||
"public": 0,
|
||||
"private": 0,
|
||||
"mentions": 0,
|
||||
"relayed": 0
|
||||
]
|
||||
|
||||
// Setup complex handlers
|
||||
for (name, node) in nodes {
|
||||
node.messageDeliveryHandler = { message in
|
||||
if message.isPrivate {
|
||||
metrics["private"]! += 1
|
||||
} else {
|
||||
metrics["public"]! += 1
|
||||
}
|
||||
|
||||
if message.mentions?.contains(name) ?? false {
|
||||
metrics["mentions"]! += 1
|
||||
}
|
||||
|
||||
if message.isRelay {
|
||||
metrics["relayed"]! += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate mixed traffic
|
||||
nodes["Alice"]!.sendMessage("Public broadcast", mentions: [], to: nil)
|
||||
nodes["Alice"]!.sendPrivateMessage("Private to Bob", to: TestConstants.testPeerID2, recipientNickname: "Bob")
|
||||
nodes["Bob"]!.sendMessage("Mentioning @Charlie", mentions: ["Charlie"], to: nil)
|
||||
|
||||
// Disconnect to force relay
|
||||
disconnect("Alice", "David")
|
||||
nodes["Alice"]!.sendMessage("Needs relay to David", mentions: [], to: nil)
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
XCTAssertGreaterThan(metrics["public"]!, 0)
|
||||
XCTAssertGreaterThan(metrics["private"]!, 0)
|
||||
XCTAssertGreaterThan(metrics["mentions"]!, 0)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
}
|
||||
|
||||
// MARK: - Security Integration Tests
|
||||
|
||||
func testEndToEndSecurityScenario() throws {
|
||||
connect("Alice", "Bob")
|
||||
connect("Bob", "Charlie") // Charlie will try to eavesdrop
|
||||
|
||||
// Establish secure session between Alice and Bob only
|
||||
try establishNoiseSession("Alice", "Bob")
|
||||
|
||||
let expectation = XCTestExpectation(description: "Secure communication maintained")
|
||||
var bobDecrypted = false
|
||||
var charlieIntercepted = false
|
||||
|
||||
// Setup encryption at Alice
|
||||
nodes["Alice"]!.packetDeliveryHandler = { packet in
|
||||
if packet.type == 0x01,
|
||||
let message = BitchatMessage.fromBinaryPayload(packet.payload),
|
||||
message.isPrivate && packet.recipientID != nil {
|
||||
// Encrypt private messages
|
||||
if let encrypted = try? self.noiseManagers["Alice"]!.encrypt(packet.payload, for: TestConstants.testPeerID2) {
|
||||
var encPacket = packet
|
||||
encPacket.type = 0x02
|
||||
encPacket.payload = encrypted
|
||||
self.nodes["Bob"]!.simulateIncomingPacket(encPacket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bob can decrypt
|
||||
nodes["Bob"]!.packetDeliveryHandler = { packet in
|
||||
if packet.type == 0x02 {
|
||||
if let decrypted = try? self.noiseManagers["Bob"]!.decrypt(packet.payload, from: TestConstants.testPeerID1),
|
||||
let message = BitchatMessage.fromBinaryPayload(decrypted) {
|
||||
bobDecrypted = message.content == "Secret message"
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
// Relay encrypted packet to Charlie
|
||||
self.nodes["Charlie"]!.simulateIncomingPacket(packet)
|
||||
}
|
||||
}
|
||||
|
||||
// Charlie cannot decrypt
|
||||
nodes["Charlie"]!.packetDeliveryHandler = { packet in
|
||||
if packet.type == 0x02 {
|
||||
charlieIntercepted = true
|
||||
// Try to decrypt (should fail)
|
||||
do {
|
||||
_ = try self.noiseManagers["Charlie"]?.decrypt(packet.payload, from: TestConstants.testPeerID1)
|
||||
XCTFail("Charlie should not be able to decrypt")
|
||||
} catch {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send encrypted private message
|
||||
nodes["Alice"]!.sendPrivateMessage("Secret message", to: TestConstants.testPeerID2, recipientNickname: "Bob")
|
||||
|
||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
||||
XCTAssertTrue(bobDecrypted)
|
||||
XCTAssertTrue(charlieIntercepted)
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func createNode(_ name: String, peerID: String) {
|
||||
let node = MockBluetoothMeshService()
|
||||
node.peerID = peerID
|
||||
node.nickname = name
|
||||
nodes[name] = node
|
||||
|
||||
// Create Noise manager
|
||||
let key = Curve25519.KeyAgreement.PrivateKey()
|
||||
noiseManagers[name] = NoiseSessionManager(localStaticKey: key)
|
||||
}
|
||||
|
||||
private func connect(_ node1: String, _ node2: String) {
|
||||
guard let n1 = nodes[node1], let n2 = nodes[node2] else { return }
|
||||
n1.simulateConnectedPeer(n2.peerID)
|
||||
n2.simulateConnectedPeer(n1.peerID)
|
||||
}
|
||||
|
||||
private func disconnect(_ node1: String, _ node2: String) {
|
||||
guard let n1 = nodes[node1], let n2 = nodes[node2] else { return }
|
||||
n1.simulateDisconnectedPeer(n2.peerID)
|
||||
n2.simulateDisconnectedPeer(n1.peerID)
|
||||
}
|
||||
|
||||
private func connectFullMesh() {
|
||||
let nodeNames = Array(nodes.keys)
|
||||
for i in 0..<nodeNames.count {
|
||||
for j in i+1..<nodeNames.count {
|
||||
connect(nodeNames[i], nodeNames[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupRelay(_ nodeName: String, nextHops: [String]) {
|
||||
guard let node = nodes[nodeName] else { return }
|
||||
|
||||
node.packetDeliveryHandler = { packet in
|
||||
guard packet.ttl > 1 else { return }
|
||||
|
||||
if let message = BitchatMessage.fromBinaryPayload(packet.payload) {
|
||||
guard message.senderPeerID != node.peerID else { return }
|
||||
|
||||
var relayMessage = message
|
||||
if !relayMessage.isRelay {
|
||||
relayMessage.isRelay = true
|
||||
relayMessage.originalSender = message.sender
|
||||
}
|
||||
|
||||
if let relayPayload = relayMessage.toBinaryPayload() {
|
||||
var relayPacket = packet
|
||||
relayPacket.payload = relayPayload
|
||||
relayPacket.ttl = packet.ttl - 1
|
||||
|
||||
for hop in nextHops {
|
||||
self.nodes[hop]?.simulateIncomingPacket(relayPacket)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func establishNoiseSession(_ node1: String, _ node2: String) throws {
|
||||
guard let manager1 = noiseManagers[node1],
|
||||
let manager2 = noiseManagers[node2],
|
||||
let peer1ID = nodes[node1]?.peerID,
|
||||
let peer2ID = nodes[node2]?.peerID else { return }
|
||||
|
||||
let msg1 = try manager1.initiateHandshake(with: peer2ID)
|
||||
let msg2 = try manager2.handleIncomingHandshake(from: peer1ID, message: msg1)!
|
||||
let msg3 = try manager1.handleIncomingHandshake(from: peer2ID, message: msg2)!
|
||||
_ = try manager2.handleIncomingHandshake(from: peer1ID, message: msg3)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//
|
||||
// MockBluetoothMeshService.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MultipeerConnectivity
|
||||
@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 init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
func simulateConnectedPeer(_ peerID: String) {
|
||||
connectedPeers.insert(peerID)
|
||||
delegate?.bluetoothMeshService(self, didConnectToPeer: peerID, peerInfo: PeerInfo(
|
||||
mcPeerID: MCPeerID(displayName: peerID),
|
||||
peerID: peerID,
|
||||
nickname: "Test User",
|
||||
publicKey: nil,
|
||||
capabilities: PeerCapabilities(supportedProtocolVersions: [1])
|
||||
))
|
||||
}
|
||||
|
||||
func simulateDisconnectedPeer(_ peerID: String) {
|
||||
connectedPeers.remove(peerID)
|
||||
delegate?.bluetoothMeshService(self, didDisconnectFromPeer: peerID)
|
||||
}
|
||||
|
||||
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: nickname,
|
||||
content: content,
|
||||
timestamp: timestamp ?? Date(),
|
||||
isRelay: false,
|
||||
originalSender: nil,
|
||||
isPrivate: false,
|
||||
recipientNickname: nil,
|
||||
senderPeerID: peerID,
|
||||
mentions: mentions.isEmpty ? nil : mentions
|
||||
)
|
||||
|
||||
if let payload = message.toBinaryPayload() {
|
||||
let packet = BitchatPacket(
|
||||
type: 0x01,
|
||||
senderID: peerID.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?.bluetoothMeshService(self!, 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: nickname,
|
||||
content: content,
|
||||
timestamp: Date(),
|
||||
isRelay: false,
|
||||
originalSender: nil,
|
||||
isPrivate: true,
|
||||
recipientNickname: recipientNickname,
|
||||
senderPeerID: peerID,
|
||||
mentions: nil
|
||||
)
|
||||
|
||||
if let payload = message.toBinaryPayload() {
|
||||
let packet = BitchatPacket(
|
||||
type: 0x01,
|
||||
senderID: peerID.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?.bluetoothMeshService(self!, didReceiveMessage: message)
|
||||
}
|
||||
|
||||
// Call delivery handler if set
|
||||
messageDeliveryHandler?(message)
|
||||
}
|
||||
}
|
||||
|
||||
func simulateIncomingMessage(_ message: BitchatMessage) {
|
||||
delegate?.bluetoothMeshService(self, didReceiveMessage: message)
|
||||
}
|
||||
|
||||
func simulateIncomingPacket(_ packet: BitchatPacket) {
|
||||
// Process through the actual handling logic
|
||||
if let message = BitchatMessage.fromBinaryPayload(packet.payload) {
|
||||
delegate?.bluetoothMeshService(self, didReceiveMessage: message)
|
||||
}
|
||||
packetDeliveryHandler?(packet)
|
||||
}
|
||||
|
||||
override func getConnectedPeers() -> [String] {
|
||||
return Array(connectedPeers)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// MockNoiseSession.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
@testable import bitchat
|
||||
|
||||
class MockNoiseSession: NoiseSession {
|
||||
var mockState: NoiseSessionState = .uninitialized
|
||||
var shouldFailHandshake = false
|
||||
var shouldFailEncryption = false
|
||||
var handshakeMessages: [Data] = []
|
||||
var encryptedData: [Data] = []
|
||||
var decryptedData: [Data] = []
|
||||
|
||||
override func getState() -> NoiseSessionState {
|
||||
return mockState
|
||||
}
|
||||
|
||||
override func isEstablished() -> Bool {
|
||||
return mockState == .established
|
||||
}
|
||||
|
||||
override func startHandshake() throws -> Data {
|
||||
if shouldFailHandshake {
|
||||
mockState = .failed(NoiseSessionError.handshakeFailed(TestError.testFailure("Mock handshake failure")))
|
||||
throw NoiseSessionError.handshakeFailed(TestError.testFailure("Mock handshake failure"))
|
||||
}
|
||||
|
||||
mockState = .handshaking
|
||||
let handshakeData = TestHelpers.generateRandomData(length: 32)
|
||||
handshakeMessages.append(handshakeData)
|
||||
return handshakeData
|
||||
}
|
||||
|
||||
override func processHandshakeMessage(_ message: Data) throws -> Data? {
|
||||
if shouldFailHandshake {
|
||||
mockState = .failed(NoiseSessionError.handshakeFailed(TestError.testFailure("Mock handshake failure")))
|
||||
throw NoiseSessionError.handshakeFailed(TestError.testFailure("Mock handshake failure"))
|
||||
}
|
||||
|
||||
handshakeMessages.append(message)
|
||||
|
||||
// Simulate handshake completion after 2 messages
|
||||
if handshakeMessages.count >= 2 {
|
||||
mockState = .established
|
||||
return nil
|
||||
} else {
|
||||
let response = TestHelpers.generateRandomData(length: 48)
|
||||
handshakeMessages.append(response)
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
override func encrypt(_ plaintext: Data) throws -> Data {
|
||||
if shouldFailEncryption {
|
||||
throw NoiseSessionError.notEstablished
|
||||
}
|
||||
|
||||
guard mockState == .established else {
|
||||
throw NoiseSessionError.notEstablished
|
||||
}
|
||||
|
||||
// Simple mock encryption: prepend magic bytes and append the data
|
||||
var encrypted = Data([0xDE, 0xAD, 0xBE, 0xEF])
|
||||
encrypted.append(plaintext)
|
||||
encryptedData.append(encrypted)
|
||||
return encrypted
|
||||
}
|
||||
|
||||
override func decrypt(_ ciphertext: Data) throws -> Data {
|
||||
if shouldFailEncryption {
|
||||
throw NoiseSessionError.notEstablished
|
||||
}
|
||||
|
||||
guard mockState == .established else {
|
||||
throw NoiseSessionError.notEstablished
|
||||
}
|
||||
|
||||
// Simple mock decryption: remove magic bytes
|
||||
guard ciphertext.count > 4 else {
|
||||
throw TestError.testFailure("Invalid ciphertext")
|
||||
}
|
||||
|
||||
let plaintext = ciphertext.dropFirst(4)
|
||||
decryptedData.append(plaintext)
|
||||
return plaintext
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
mockState = .uninitialized
|
||||
handshakeMessages.removeAll()
|
||||
encryptedData.removeAll()
|
||||
decryptedData.removeAll()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
//
|
||||
// NoiseProtocolTests.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import CryptoKit
|
||||
@testable import bitchat
|
||||
|
||||
final class NoiseProtocolTests: XCTestCase {
|
||||
|
||||
var aliceKey: Curve25519.KeyAgreement.PrivateKey!
|
||||
var bobKey: Curve25519.KeyAgreement.PrivateKey!
|
||||
var aliceSession: NoiseSession!
|
||||
var bobSession: NoiseSession!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
aliceKey = Curve25519.KeyAgreement.PrivateKey()
|
||||
bobKey = Curve25519.KeyAgreement.PrivateKey()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
aliceSession = nil
|
||||
bobSession = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - Basic Handshake Tests
|
||||
|
||||
func testXXPatternHandshake() throws {
|
||||
// Create sessions
|
||||
aliceSession = NoiseSession(
|
||||
peerID: TestConstants.testPeerID2,
|
||||
role: .initiator,
|
||||
localStaticKey: aliceKey
|
||||
)
|
||||
|
||||
bobSession = NoiseSession(
|
||||
peerID: TestConstants.testPeerID1,
|
||||
role: .responder,
|
||||
localStaticKey: bobKey
|
||||
)
|
||||
|
||||
// Alice starts handshake (message 1)
|
||||
let message1 = try aliceSession.startHandshake()
|
||||
XCTAssertFalse(message1.isEmpty)
|
||||
XCTAssertEqual(aliceSession.getState(), .handshaking)
|
||||
|
||||
// Bob processes message 1 and creates message 2
|
||||
let message2 = try bobSession.processHandshakeMessage(message1)
|
||||
XCTAssertNotNil(message2)
|
||||
XCTAssertFalse(message2!.isEmpty)
|
||||
XCTAssertEqual(bobSession.getState(), .handshaking)
|
||||
|
||||
// Alice processes message 2 and creates message 3
|
||||
let message3 = try aliceSession.processHandshakeMessage(message2!)
|
||||
XCTAssertNotNil(message3)
|
||||
XCTAssertFalse(message3!.isEmpty)
|
||||
XCTAssertEqual(aliceSession.getState(), .established)
|
||||
|
||||
// Bob processes message 3 and completes handshake
|
||||
let finalMessage = try bobSession.processHandshakeMessage(message3!)
|
||||
XCTAssertNil(finalMessage) // No more messages needed
|
||||
XCTAssertEqual(bobSession.getState(), .established)
|
||||
|
||||
// Verify both sessions are established
|
||||
XCTAssertTrue(aliceSession.isEstablished())
|
||||
XCTAssertTrue(bobSession.isEstablished())
|
||||
|
||||
// Verify they have each other's static keys
|
||||
XCTAssertEqual(aliceSession.getRemoteStaticPublicKey()?.rawRepresentation, bobKey.publicKey.rawRepresentation)
|
||||
XCTAssertEqual(bobSession.getRemoteStaticPublicKey()?.rawRepresentation, aliceKey.publicKey.rawRepresentation)
|
||||
}
|
||||
|
||||
func testHandshakeStateValidation() throws {
|
||||
aliceSession = NoiseSession(
|
||||
peerID: TestConstants.testPeerID2,
|
||||
role: .initiator,
|
||||
localStaticKey: aliceKey
|
||||
)
|
||||
|
||||
// Cannot process message before starting handshake
|
||||
XCTAssertThrows(try aliceSession.processHandshakeMessage(Data()))
|
||||
|
||||
// Start handshake
|
||||
_ = try aliceSession.startHandshake()
|
||||
|
||||
// Cannot start handshake twice
|
||||
XCTAssertThrows(try aliceSession.startHandshake())
|
||||
}
|
||||
|
||||
// MARK: - Encryption/Decryption Tests
|
||||
|
||||
func testBasicEncryptionDecryption() throws {
|
||||
// Establish sessions
|
||||
try establishSessions()
|
||||
|
||||
let plaintext = "Hello, Bob!".data(using: .utf8)!
|
||||
|
||||
// Alice encrypts
|
||||
let ciphertext = try aliceSession.encrypt(plaintext)
|
||||
XCTAssertNotEqual(ciphertext, plaintext)
|
||||
XCTAssertGreaterThan(ciphertext.count, plaintext.count) // Should have overhead
|
||||
|
||||
// Bob decrypts
|
||||
let decrypted = try bobSession.decrypt(ciphertext)
|
||||
XCTAssertEqual(decrypted, plaintext)
|
||||
}
|
||||
|
||||
func testBidirectionalEncryption() throws {
|
||||
try establishSessions()
|
||||
|
||||
// Alice -> Bob
|
||||
let aliceMessage = "Hello from Alice".data(using: .utf8)!
|
||||
let aliceCiphertext = try aliceSession.encrypt(aliceMessage)
|
||||
let bobReceived = try bobSession.decrypt(aliceCiphertext)
|
||||
XCTAssertEqual(bobReceived, aliceMessage)
|
||||
|
||||
// Bob -> Alice
|
||||
let bobMessage = "Hello from Bob".data(using: .utf8)!
|
||||
let bobCiphertext = try bobSession.encrypt(bobMessage)
|
||||
let aliceReceived = try aliceSession.decrypt(bobCiphertext)
|
||||
XCTAssertEqual(aliceReceived, bobMessage)
|
||||
}
|
||||
|
||||
func testLargeMessageEncryption() throws {
|
||||
try establishSessions()
|
||||
|
||||
// Create a large message
|
||||
let largeMessage = TestHelpers.generateRandomData(length: 100_000)
|
||||
|
||||
// Encrypt and decrypt
|
||||
let ciphertext = try aliceSession.encrypt(largeMessage)
|
||||
let decrypted = try bobSession.decrypt(ciphertext)
|
||||
|
||||
XCTAssertEqual(decrypted, largeMessage)
|
||||
}
|
||||
|
||||
func testEncryptionBeforeHandshake() {
|
||||
aliceSession = NoiseSession(
|
||||
peerID: TestConstants.testPeerID2,
|
||||
role: .initiator,
|
||||
localStaticKey: aliceKey
|
||||
)
|
||||
|
||||
let plaintext = "test".data(using: .utf8)!
|
||||
|
||||
// Should throw when not established
|
||||
XCTAssertThrows(try aliceSession.encrypt(plaintext))
|
||||
XCTAssertThrows(try aliceSession.decrypt(plaintext))
|
||||
}
|
||||
|
||||
// MARK: - Session Manager Tests
|
||||
|
||||
func testSessionManagerBasicOperations() throws {
|
||||
let manager = NoiseSessionManager(localStaticKey: aliceKey)
|
||||
|
||||
// Create session
|
||||
let session = manager.createSession(for: TestConstants.testPeerID2, role: .initiator)
|
||||
XCTAssertNotNil(session)
|
||||
|
||||
// Get session
|
||||
let retrieved = manager.getSession(for: TestConstants.testPeerID2)
|
||||
XCTAssertNotNil(retrieved)
|
||||
XCTAssertTrue(session === retrieved)
|
||||
|
||||
// Remove session
|
||||
manager.removeSession(for: TestConstants.testPeerID2)
|
||||
XCTAssertNil(manager.getSession(for: TestConstants.testPeerID2))
|
||||
}
|
||||
|
||||
func testSessionManagerHandshakeInitiation() throws {
|
||||
let manager = NoiseSessionManager(localStaticKey: aliceKey)
|
||||
|
||||
// Initiate handshake
|
||||
let handshakeData = try manager.initiateHandshake(with: TestConstants.testPeerID2)
|
||||
XCTAssertFalse(handshakeData.isEmpty)
|
||||
|
||||
// Session should exist
|
||||
let session = manager.getSession(for: TestConstants.testPeerID2)
|
||||
XCTAssertNotNil(session)
|
||||
XCTAssertEqual(session?.getState(), .handshaking)
|
||||
}
|
||||
|
||||
func testSessionManagerIncomingHandshake() throws {
|
||||
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
|
||||
let bobManager = NoiseSessionManager(localStaticKey: bobKey)
|
||||
|
||||
// Alice initiates
|
||||
let message1 = try aliceManager.initiateHandshake(with: TestConstants.testPeerID2)
|
||||
|
||||
// Bob responds
|
||||
let message2 = try bobManager.handleIncomingHandshake(from: TestConstants.testPeerID1, message: message1)
|
||||
XCTAssertNotNil(message2)
|
||||
|
||||
// Continue handshake
|
||||
let message3 = try aliceManager.handleIncomingHandshake(from: TestConstants.testPeerID2, message: message2!)
|
||||
XCTAssertNotNil(message3)
|
||||
|
||||
// Complete handshake
|
||||
let finalMessage = try bobManager.handleIncomingHandshake(from: TestConstants.testPeerID1, message: message3!)
|
||||
XCTAssertNil(finalMessage)
|
||||
|
||||
// Both should have established sessions
|
||||
XCTAssertTrue(aliceManager.getSession(for: TestConstants.testPeerID2)?.isEstablished() ?? false)
|
||||
XCTAssertTrue(bobManager.getSession(for: TestConstants.testPeerID1)?.isEstablished() ?? false)
|
||||
}
|
||||
|
||||
func testSessionManagerEncryptionDecryption() throws {
|
||||
let aliceManager = NoiseSessionManager(localStaticKey: aliceKey)
|
||||
let bobManager = NoiseSessionManager(localStaticKey: bobKey)
|
||||
|
||||
// Establish sessions
|
||||
try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager)
|
||||
|
||||
// Encrypt with manager
|
||||
let plaintext = "Test message".data(using: .utf8)!
|
||||
let ciphertext = try aliceManager.encrypt(plaintext, for: TestConstants.testPeerID2)
|
||||
|
||||
// Decrypt with manager
|
||||
let decrypted = try bobManager.decrypt(ciphertext, from: TestConstants.testPeerID1)
|
||||
XCTAssertEqual(decrypted, plaintext)
|
||||
}
|
||||
|
||||
func testSessionMigration() throws {
|
||||
let manager = NoiseSessionManager(localStaticKey: aliceKey)
|
||||
|
||||
// Create and establish a session
|
||||
_ = try manager.initiateHandshake(with: TestConstants.testPeerID2)
|
||||
|
||||
// Migrate to new peer ID
|
||||
let newPeerID = TestConstants.testPeerID3
|
||||
manager.migrateSession(from: TestConstants.testPeerID2, to: newPeerID)
|
||||
|
||||
// Old peer ID should not have session
|
||||
XCTAssertNil(manager.getSession(for: TestConstants.testPeerID2))
|
||||
|
||||
// New peer ID should have the session
|
||||
XCTAssertNotNil(manager.getSession(for: newPeerID))
|
||||
}
|
||||
|
||||
// MARK: - Security Tests
|
||||
|
||||
func testTamperedCiphertextDetection() throws {
|
||||
try establishSessions()
|
||||
|
||||
let plaintext = "Secret message".data(using: .utf8)!
|
||||
var ciphertext = try aliceSession.encrypt(plaintext)
|
||||
|
||||
// Tamper with ciphertext
|
||||
ciphertext[ciphertext.count / 2] ^= 0xFF
|
||||
|
||||
// Decryption should fail
|
||||
XCTAssertThrows(try bobSession.decrypt(ciphertext))
|
||||
}
|
||||
|
||||
func testReplayPrevention() throws {
|
||||
try establishSessions()
|
||||
|
||||
let plaintext = "Test message".data(using: .utf8)!
|
||||
let ciphertext = try aliceSession.encrypt(plaintext)
|
||||
|
||||
// First decryption should succeed
|
||||
_ = try bobSession.decrypt(ciphertext)
|
||||
|
||||
// Replaying the same ciphertext should fail
|
||||
XCTAssertThrows(try bobSession.decrypt(ciphertext))
|
||||
}
|
||||
|
||||
func testSessionIsolation() throws {
|
||||
// Create two separate session pairs
|
||||
let aliceSession1 = NoiseSession(peerID: "peer1", role: .initiator, localStaticKey: aliceKey)
|
||||
let bobSession1 = NoiseSession(peerID: "alice1", role: .responder, localStaticKey: bobKey)
|
||||
|
||||
let aliceSession2 = NoiseSession(peerID: "peer2", role: .initiator, localStaticKey: aliceKey)
|
||||
let bobSession2 = NoiseSession(peerID: "alice2", role: .responder, localStaticKey: bobKey)
|
||||
|
||||
// Establish both pairs
|
||||
try performHandshake(initiator: aliceSession1, responder: bobSession1)
|
||||
try performHandshake(initiator: aliceSession2, responder: bobSession2)
|
||||
|
||||
// Encrypt with session 1
|
||||
let plaintext = "Secret".data(using: .utf8)!
|
||||
let ciphertext1 = try aliceSession1.encrypt(plaintext)
|
||||
|
||||
// Should not be able to decrypt with session 2
|
||||
XCTAssertThrows(try bobSession2.decrypt(ciphertext1))
|
||||
|
||||
// But should work with correct session
|
||||
let decrypted = try bobSession1.decrypt(ciphertext1)
|
||||
XCTAssertEqual(decrypted, plaintext)
|
||||
}
|
||||
|
||||
// MARK: - Performance Tests
|
||||
|
||||
func testHandshakePerformance() throws {
|
||||
measure {
|
||||
do {
|
||||
let alice = NoiseSession(peerID: "bob", role: .initiator, localStaticKey: aliceKey)
|
||||
let bob = NoiseSession(peerID: "alice", role: .responder, localStaticKey: bobKey)
|
||||
try performHandshake(initiator: alice, responder: bob)
|
||||
} catch {
|
||||
XCTFail("Handshake failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testEncryptionPerformance() throws {
|
||||
try establishSessions()
|
||||
let message = TestHelpers.generateRandomData(length: 1024)
|
||||
|
||||
measure {
|
||||
do {
|
||||
for _ in 0..<100 {
|
||||
let ciphertext = try aliceSession.encrypt(message)
|
||||
_ = try bobSession.decrypt(ciphertext)
|
||||
}
|
||||
} catch {
|
||||
XCTFail("Encryption/decryption failed: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func establishSessions() throws {
|
||||
aliceSession = NoiseSession(
|
||||
peerID: TestConstants.testPeerID2,
|
||||
role: .initiator,
|
||||
localStaticKey: aliceKey
|
||||
)
|
||||
|
||||
bobSession = NoiseSession(
|
||||
peerID: TestConstants.testPeerID1,
|
||||
role: .responder,
|
||||
localStaticKey: bobKey
|
||||
)
|
||||
|
||||
try performHandshake(initiator: aliceSession, responder: bobSession)
|
||||
}
|
||||
|
||||
private func performHandshake(initiator: NoiseSession, responder: NoiseSession) throws {
|
||||
let msg1 = try initiator.startHandshake()
|
||||
let msg2 = try responder.processHandshakeMessage(msg1)!
|
||||
let msg3 = try initiator.processHandshakeMessage(msg2)!
|
||||
_ = try responder.processHandshakeMessage(msg3)
|
||||
}
|
||||
|
||||
private func establishManagerSessions(aliceManager: NoiseSessionManager, bobManager: NoiseSessionManager) throws {
|
||||
let msg1 = try aliceManager.initiateHandshake(with: TestConstants.testPeerID2)
|
||||
let msg2 = try bobManager.handleIncomingHandshake(from: TestConstants.testPeerID1, message: msg1)!
|
||||
let msg3 = try aliceManager.handleIncomingHandshake(from: TestConstants.testPeerID2, message: msg2)!
|
||||
_ = try bobManager.handleIncomingHandshake(from: TestConstants.testPeerID1, message: msg3)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
//
|
||||
// BinaryProtocolTests.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import bitchat
|
||||
|
||||
final class BinaryProtocolTests: XCTestCase {
|
||||
|
||||
// MARK: - Basic Encoding/Decoding Tests
|
||||
|
||||
func testBasicPacketEncodingDecoding() throws {
|
||||
let originalPacket = TestHelpers.createTestPacket()
|
||||
|
||||
// Encode
|
||||
guard let encodedData = BinaryProtocol.encode(originalPacket) else {
|
||||
XCTFail("Failed to encode packet")
|
||||
return
|
||||
}
|
||||
|
||||
// Decode
|
||||
guard let decodedPacket = BinaryProtocol.decode(encodedData) else {
|
||||
XCTFail("Failed to decode packet")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify
|
||||
XCTAssertEqual(decodedPacket.type, originalPacket.type)
|
||||
XCTAssertEqual(decodedPacket.ttl, originalPacket.ttl)
|
||||
XCTAssertEqual(decodedPacket.timestamp, originalPacket.timestamp)
|
||||
XCTAssertEqual(decodedPacket.payload, originalPacket.payload)
|
||||
|
||||
// Sender ID should match (accounting for padding)
|
||||
let originalSenderID = originalPacket.senderID.prefix(BinaryProtocol.senderIDSize)
|
||||
let decodedSenderID = decodedPacket.senderID.trimmingNullBytes()
|
||||
XCTAssertEqual(decodedSenderID, originalSenderID)
|
||||
}
|
||||
|
||||
func testPacketWithRecipient() throws {
|
||||
let recipientID = TestConstants.testPeerID2
|
||||
let packet = TestHelpers.createTestPacket(recipientID: recipientID)
|
||||
|
||||
// Encode and decode
|
||||
guard let encodedData = BinaryProtocol.encode(packet),
|
||||
let decodedPacket = BinaryProtocol.decode(encodedData) else {
|
||||
XCTFail("Failed to encode/decode packet with recipient")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify recipient
|
||||
XCTAssertNotNil(decodedPacket.recipientID)
|
||||
let decodedRecipientID = decodedPacket.recipientID?.trimmingNullBytes()
|
||||
XCTAssertEqual(String(data: decodedRecipientID!, encoding: .utf8), recipientID)
|
||||
}
|
||||
|
||||
func testPacketWithSignature() throws {
|
||||
var packet = TestHelpers.createTestPacket()
|
||||
packet.signature = TestConstants.testSignature
|
||||
|
||||
// Encode and decode
|
||||
guard let encodedData = BinaryProtocol.encode(packet),
|
||||
let decodedPacket = BinaryProtocol.decode(encodedData) else {
|
||||
XCTFail("Failed to encode/decode packet with signature")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
XCTAssertNotNil(decodedPacket.signature)
|
||||
XCTAssertEqual(decodedPacket.signature, TestConstants.testSignature)
|
||||
}
|
||||
|
||||
// MARK: - Compression Tests
|
||||
|
||||
func testPayloadCompression() throws {
|
||||
// Create a large, compressible payload
|
||||
let repeatedString = String(repeating: "This is a test message. ", count: 50)
|
||||
let largePayload = repeatedString.data(using: .utf8)!
|
||||
|
||||
let packet = TestHelpers.createTestPacket(payload: largePayload)
|
||||
|
||||
// Encode (should compress)
|
||||
guard let encodedData = BinaryProtocol.encode(packet) else {
|
||||
XCTFail("Failed to encode packet with large payload")
|
||||
return
|
||||
}
|
||||
|
||||
// The encoded size should be smaller than uncompressed due to compression
|
||||
let uncompressedSize = BinaryProtocol.headerSize + BinaryProtocol.senderIDSize + largePayload.count
|
||||
XCTAssertLessThan(encodedData.count, uncompressedSize)
|
||||
|
||||
// Decode and verify
|
||||
guard let decodedPacket = BinaryProtocol.decode(encodedData) else {
|
||||
XCTFail("Failed to decode compressed packet")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(decodedPacket.payload, largePayload)
|
||||
}
|
||||
|
||||
func testSmallPayloadNoCompression() throws {
|
||||
// Small payloads should not be compressed
|
||||
let smallPayload = "Hi".data(using: .utf8)!
|
||||
let packet = TestHelpers.createTestPacket(payload: smallPayload)
|
||||
|
||||
guard let encodedData = BinaryProtocol.encode(packet),
|
||||
let decodedPacket = BinaryProtocol.decode(encodedData) else {
|
||||
XCTFail("Failed to encode/decode small packet")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(decodedPacket.payload, smallPayload)
|
||||
}
|
||||
|
||||
// MARK: - Message Padding Tests
|
||||
|
||||
func testMessagePadding() throws {
|
||||
let payloads = [
|
||||
"Short",
|
||||
"This is a medium length message for testing",
|
||||
TestConstants.testLongMessage
|
||||
]
|
||||
|
||||
var encodedSizes = Set<Int>()
|
||||
|
||||
for payload in payloads {
|
||||
let packet = TestHelpers.createTestPacket(payload: payload.data(using: .utf8)!)
|
||||
|
||||
guard let encodedData = BinaryProtocol.encode(packet) else {
|
||||
XCTFail("Failed to encode packet")
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify padding creates standard block sizes
|
||||
let blockSizes = [256, 512, 1024, 2048, 4096]
|
||||
XCTAssertTrue(blockSizes.contains(encodedData.count), "Encoded size \(encodedData.count) is not a standard block size")
|
||||
|
||||
encodedSizes.insert(encodedData.count)
|
||||
|
||||
// Verify decoding works
|
||||
guard let decodedPacket = BinaryProtocol.decode(encodedData) else {
|
||||
XCTFail("Failed to decode padded packet")
|
||||
continue
|
||||
}
|
||||
|
||||
XCTAssertEqual(String(data: decodedPacket.payload, encoding: .utf8), payload)
|
||||
}
|
||||
|
||||
// Different payload sizes should result in different padded sizes
|
||||
XCTAssertGreaterThan(encodedSizes.count, 1)
|
||||
}
|
||||
|
||||
// MARK: - Message Encoding/Decoding Tests
|
||||
|
||||
func testMessageEncodingDecoding() throws {
|
||||
let message = TestHelpers.createTestMessage()
|
||||
|
||||
guard let payload = message.toBinaryPayload() else {
|
||||
XCTFail("Failed to encode message to binary")
|
||||
return
|
||||
}
|
||||
|
||||
guard let decodedMessage = BitchatMessage.fromBinaryPayload(payload) else {
|
||||
XCTFail("Failed to decode message from binary")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(decodedMessage.content, message.content)
|
||||
XCTAssertEqual(decodedMessage.sender, message.sender)
|
||||
XCTAssertEqual(decodedMessage.senderPeerID, message.senderPeerID)
|
||||
XCTAssertEqual(decodedMessage.isPrivate, message.isPrivate)
|
||||
|
||||
// Timestamp should be close (within 1 second due to conversion)
|
||||
let timeDiff = abs(decodedMessage.timestamp.timeIntervalSince(message.timestamp))
|
||||
XCTAssertLessThan(timeDiff, 1.0)
|
||||
}
|
||||
|
||||
func testPrivateMessageEncoding() throws {
|
||||
let message = TestHelpers.createTestMessage(
|
||||
isPrivate: true,
|
||||
recipientNickname: TestConstants.testNickname2
|
||||
)
|
||||
|
||||
guard let payload = message.toBinaryPayload(),
|
||||
let decodedMessage = BitchatMessage.fromBinaryPayload(payload) else {
|
||||
XCTFail("Failed to encode/decode private message")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertTrue(decodedMessage.isPrivate)
|
||||
XCTAssertEqual(decodedMessage.recipientNickname, TestConstants.testNickname2)
|
||||
}
|
||||
|
||||
func testMessageWithMentions() throws {
|
||||
let mentions = [TestConstants.testNickname2, TestConstants.testNickname3]
|
||||
let message = TestHelpers.createTestMessage(mentions: mentions)
|
||||
|
||||
guard let payload = message.toBinaryPayload(),
|
||||
let decodedMessage = BitchatMessage.fromBinaryPayload(payload) else {
|
||||
XCTFail("Failed to encode/decode message with mentions")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(decodedMessage.mentions, mentions)
|
||||
}
|
||||
|
||||
func testRelayMessageEncoding() throws {
|
||||
var message = TestHelpers.createTestMessage()
|
||||
message.isRelay = true
|
||||
message.originalSender = TestConstants.testNickname3
|
||||
|
||||
guard let payload = message.toBinaryPayload(),
|
||||
let decodedMessage = BitchatMessage.fromBinaryPayload(payload) else {
|
||||
XCTFail("Failed to encode/decode relay message")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertTrue(decodedMessage.isRelay)
|
||||
XCTAssertEqual(decodedMessage.originalSender, TestConstants.testNickname3)
|
||||
}
|
||||
|
||||
// MARK: - Edge Cases and Error Handling
|
||||
|
||||
func testInvalidDataDecoding() {
|
||||
// Too small data
|
||||
let tooSmall = Data(repeating: 0, count: 5)
|
||||
XCTAssertNil(BinaryProtocol.decode(tooSmall))
|
||||
|
||||
// Random data
|
||||
let random = TestHelpers.generateRandomData(length: 100)
|
||||
XCTAssertNil(BinaryProtocol.decode(random))
|
||||
|
||||
// Corrupted header
|
||||
var packet = TestHelpers.createTestPacket()
|
||||
guard var encoded = BinaryProtocol.encode(packet) else {
|
||||
XCTFail("Failed to encode test packet")
|
||||
return
|
||||
}
|
||||
|
||||
// Corrupt the version byte
|
||||
encoded[0] = 0xFF
|
||||
XCTAssertNil(BinaryProtocol.decode(encoded))
|
||||
}
|
||||
|
||||
func testLargeMessageHandling() throws {
|
||||
// Test maximum size handling
|
||||
let largeContent = String(repeating: "X", count: 65535) // Max uint16
|
||||
let message = TestHelpers.createTestMessage(content: largeContent)
|
||||
|
||||
guard let payload = message.toBinaryPayload(),
|
||||
let decodedMessage = BitchatMessage.fromBinaryPayload(payload) else {
|
||||
XCTFail("Failed to handle large message")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(decodedMessage.content, largeContent)
|
||||
}
|
||||
|
||||
func testEmptyFieldsHandling() throws {
|
||||
// Test message with empty content
|
||||
let emptyMessage = TestHelpers.createTestMessage(content: "")
|
||||
|
||||
guard let payload = emptyMessage.toBinaryPayload(),
|
||||
let decodedMessage = BitchatMessage.fromBinaryPayload(payload) else {
|
||||
XCTFail("Failed to handle empty message")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(decodedMessage.content, "")
|
||||
}
|
||||
|
||||
// MARK: - Protocol Version Tests
|
||||
|
||||
func testProtocolVersionHandling() throws {
|
||||
// Test with supported version
|
||||
let packet = BitchatPacket(
|
||||
version: 1,
|
||||
type: 0x01,
|
||||
senderID: TestConstants.testPeerID1.data(using: .utf8)!,
|
||||
recipientID: nil,
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
payload: "test".data(using: .utf8)!,
|
||||
signature: nil,
|
||||
ttl: 3
|
||||
)
|
||||
|
||||
guard let encoded = BinaryProtocol.encode(packet),
|
||||
let decoded = BinaryProtocol.decode(encoded) else {
|
||||
XCTFail("Failed to encode/decode packet with version")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(decoded.version, 1)
|
||||
}
|
||||
|
||||
func testUnsupportedProtocolVersion() throws {
|
||||
// Create packet with unsupported version
|
||||
var packet = TestHelpers.createTestPacket()
|
||||
packet.version = 99 // Unsupported version
|
||||
|
||||
guard let encoded = BinaryProtocol.encode(packet) else {
|
||||
XCTFail("Failed to encode packet")
|
||||
return
|
||||
}
|
||||
|
||||
// Should fail to decode
|
||||
XCTAssertNil(BinaryProtocol.decode(encoded))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// TestConstants.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TestConstants {
|
||||
static let defaultTimeout: TimeInterval = 5.0
|
||||
static let shortTimeout: TimeInterval = 1.0
|
||||
static let longTimeout: TimeInterval = 10.0
|
||||
|
||||
static let testPeerID1 = "PEER1234"
|
||||
static let testPeerID2 = "PEER5678"
|
||||
static let testPeerID3 = "PEER9012"
|
||||
static let testPeerID4 = "PEER3456"
|
||||
|
||||
static let testNickname1 = "Alice"
|
||||
static let testNickname2 = "Bob"
|
||||
static let testNickname3 = "Charlie"
|
||||
static let testNickname4 = "David"
|
||||
|
||||
static let testMessage1 = "Hello, World!"
|
||||
static let testMessage2 = "How are you?"
|
||||
static let testMessage3 = "This is a test message"
|
||||
static let testLongMessage = String(repeating: "This is a long message. ", count: 100)
|
||||
|
||||
static let testRoomID = "test-room"
|
||||
static let testSignature = Data(repeating: 0xAB, count: 64)
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
//
|
||||
// TestHelpers.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
@testable import bitchat
|
||||
|
||||
class TestHelpers {
|
||||
|
||||
// MARK: - Key Generation
|
||||
|
||||
static func generateTestKeyPair() -> (privateKey: Curve25519.KeyAgreement.PrivateKey, publicKey: Curve25519.KeyAgreement.PublicKey) {
|
||||
let privateKey = Curve25519.KeyAgreement.PrivateKey()
|
||||
let publicKey = privateKey.publicKey
|
||||
return (privateKey, publicKey)
|
||||
}
|
||||
|
||||
static func generateTestIdentity(peerID: String, nickname: String) -> UserProfile {
|
||||
let (privateKey, publicKey) = generateTestKeyPair()
|
||||
return UserProfile(
|
||||
nickname: nickname,
|
||||
peerID: peerID,
|
||||
publicKey: publicKey.rawRepresentation,
|
||||
privateKey: privateKey.rawRepresentation
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Message Creation
|
||||
|
||||
static func createTestMessage(
|
||||
content: String = TestConstants.testMessage1,
|
||||
sender: String = TestConstants.testNickname1,
|
||||
senderPeerID: String = TestConstants.testPeerID1,
|
||||
isPrivate: Bool = false,
|
||||
recipientNickname: String? = nil,
|
||||
mentions: [String]? = nil
|
||||
) -> BitchatMessage {
|
||||
return BitchatMessage(
|
||||
id: UUID().uuidString,
|
||||
sender: sender,
|
||||
content: content,
|
||||
timestamp: Date(),
|
||||
isRelay: false,
|
||||
originalSender: nil,
|
||||
isPrivate: isPrivate,
|
||||
recipientNickname: recipientNickname,
|
||||
senderPeerID: senderPeerID,
|
||||
mentions: mentions
|
||||
)
|
||||
}
|
||||
|
||||
static func createTestPacket(
|
||||
type: UInt8 = 0x01,
|
||||
senderID: String = TestConstants.testPeerID1,
|
||||
recipientID: String? = nil,
|
||||
payload: Data = "test payload".data(using: .utf8)!,
|
||||
ttl: UInt8 = 3
|
||||
) -> BitchatPacket {
|
||||
return BitchatPacket(
|
||||
type: type,
|
||||
senderID: senderID.data(using: .utf8)!,
|
||||
recipientID: recipientID?.data(using: .utf8),
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
payload: payload,
|
||||
signature: nil,
|
||||
ttl: ttl
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Data Generation
|
||||
|
||||
static func generateRandomData(length: Int) -> Data {
|
||||
var data = Data(count: length)
|
||||
_ = data.withUnsafeMutableBytes { bytes in
|
||||
SecRandomCopyBytes(kSecRandomDefault, length, bytes.baseAddress!)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
static func generateTestPeerID() -> String {
|
||||
return "PEER" + UUID().uuidString.prefix(8)
|
||||
}
|
||||
|
||||
// MARK: - Async Helpers
|
||||
|
||||
static func waitFor(_ condition: @escaping () -> Bool, timeout: TimeInterval = TestConstants.defaultTimeout) async throws {
|
||||
let start = Date()
|
||||
while !condition() {
|
||||
if Date().timeIntervalSince(start) > timeout {
|
||||
throw TestError.timeout
|
||||
}
|
||||
try await Task.sleep(nanoseconds: 10_000_000) // 10ms
|
||||
}
|
||||
}
|
||||
|
||||
static func expectAsync<T>(
|
||||
timeout: TimeInterval = TestConstants.defaultTimeout,
|
||||
operation: @escaping () async throws -> T
|
||||
) async throws -> T {
|
||||
return try await withThrowingTaskGroup(of: T.self) { group in
|
||||
group.addTask {
|
||||
return try await operation()
|
||||
}
|
||||
|
||||
group.addTask {
|
||||
try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
|
||||
throw TestError.timeout
|
||||
}
|
||||
|
||||
let result = try await group.next()!
|
||||
group.cancelAll()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TestError: Error {
|
||||
case timeout
|
||||
case unexpectedValue
|
||||
case testFailure(String)
|
||||
}
|
||||
Reference in New Issue
Block a user