From 3d914dcf469cdf074f8e094167d16999b03a66a3 Mon Sep 17 00:00:00 2001 From: Islam <2553451+qalandarov@users.noreply.github.com> Date: Wed, 15 Oct 2025 00:04:01 +0100 Subject: [PATCH] Convert the remaining tests to Swift Testing (#781) * SwiftTesting: NoiseProtocolTests + BinaryProtocolPaddingTests * SwiftTesting: `NotificationStreamAssemblerTests` * SwiftTesting: `NostrProtocolTests` * SwiftTesting: `BinaryProtocolTests` * SwiftTesting: `PeerIDTests` * SwiftTesting: `BLEServiceTests` * SwiftTesting: `CommandProcessorTests` * SwiftTesting: `GCSFilterTests` * SwiftTesting: `GeohashBookmarksStoreTests` * Remove `peerID` test constants * Remove PeerID + String interop from tests * Refactor IntegrationTests to extract state management * Refactor global state management of MockBLEService * NoiseProtocolSwiftTests: `actor` -> `struct` * Remove measurement tests w/ no benchmark * `NoiseProtocolSwiftTests` -> `NoiseProtocolTests` * SwiftTesting: `LocationChannelsTests` * SwiftTesting: `GossipSyncManagerTests` * SwiftTesting: `LocationNotesManagerTests` * Global `sleep` function for tests * SwiftTesting: `IntegrationTests` --------- Co-authored-by: jack --- bitchat/Noise/NoiseSessionError.swift | 3 +- bitchat/Services/GeohashBookmarksStore.swift | 11 - bitchatTests/BLEServiceTests.swift | 416 +++++----- bitchatTests/CommandProcessorTests.swift | 38 +- .../EndToEnd/PrivateChatE2ETests.swift | 20 +- .../EndToEnd/PublicChatE2ETests.swift | 10 +- .../Fragmentation/FragmentationTests.swift | 28 +- bitchatTests/GCSFilterTests.swift | 17 +- bitchatTests/GeohashBookmarksStoreTests.swift | 50 +- bitchatTests/GossipSyncManagerTests.swift | 77 +- .../Integration/IntegrationTests.swift | 766 +++++++----------- .../Integration/TestNetworkHelper.swift | 123 +++ bitchatTests/LocationChannelsTests.swift | 35 +- bitchatTests/LocationNotesManagerTests.swift | 47 +- bitchatTests/Mocks/MockBLEBus.swift | 57 ++ bitchatTests/Mocks/MockBLEService.swift | 66 +- bitchatTests/Noise/NoiseProtocolTests.swift | 469 +++++------ bitchatTests/NostrProtocolTests.swift | 123 ++- .../NotificationStreamAssemblerTests.swift | 95 +-- .../Protocol/BinaryProtocolPaddingTests.swift | 35 +- .../Protocol/BinaryProtocolTests.swift | 329 +++----- .../TestUtilities/TestConstants.swift | 5 - bitchatTests/TestUtilities/TestHelpers.swift | 20 +- bitchatTests/Utils/PeerIDTests.swift | 371 +++++---- 24 files changed, 1504 insertions(+), 1707 deletions(-) create mode 100644 bitchatTests/Integration/TestNetworkHelper.swift create mode 100644 bitchatTests/Mocks/MockBLEBus.swift diff --git a/bitchat/Noise/NoiseSessionError.swift b/bitchat/Noise/NoiseSessionError.swift index 34a2a3f7..24e172e7 100644 --- a/bitchat/Noise/NoiseSessionError.swift +++ b/bitchat/Noise/NoiseSessionError.swift @@ -6,10 +6,9 @@ // For more information, see // -enum NoiseSessionError: Error { +enum NoiseSessionError: Error, Equatable { case invalidState case notEstablished case sessionNotFound - case handshakeFailed(Error) case alreadyEstablished } diff --git a/bitchat/Services/GeohashBookmarksStore.swift b/bitchat/Services/GeohashBookmarksStore.swift index 9d6144f3..37033965 100644 --- a/bitchat/Services/GeohashBookmarksStore.swift +++ b/bitchat/Services/GeohashBookmarksStore.swift @@ -216,15 +216,4 @@ final class GeohashBookmarksStore: ObservableObject { } } #endif - - #if DEBUG - /// Testing-only reset helper - func _resetForTesting() { - bookmarks.removeAll() - membership.removeAll() - bookmarkNames.removeAll() - persist() - persistNames() - } - #endif } diff --git a/bitchatTests/BLEServiceTests.swift b/bitchatTests/BLEServiceTests.swift index 9ae29284..0facccb7 100644 --- a/bitchatTests/BLEServiceTests.swift +++ b/bitchatTests/BLEServiceTests.swift @@ -6,265 +6,275 @@ // For more information, see // -import XCTest +import Testing import CoreBluetooth @testable import bitchat -final class BLEServiceTests: XCTestCase { +struct BLEServiceTests { + private let service: MockBLEService + private let myUUID = UUID() + private let bus = MockBLEBus() - var service: MockBLEService! - - override func setUp() { - super.setUp() - service = MockBLEService() - service.myPeerID = "TEST1234" + init() { + service = MockBLEService.init(bus: bus) + service.myPeerID = PeerID(str: myUUID.uuidString) service.mockNickname = "TestUser" } - override func tearDown() { - service = nil - super.tearDown() - } - // MARK: - Basic Functionality Tests - func testServiceInitialization() { - XCTAssertNotNil(service) - XCTAssertEqual(service.myPeerID, "TEST1234") - XCTAssertEqual(service.myNickname, "TestUser") + @Test func serviceInitialization() { + #expect(service.myPeerID == PeerID(str: myUUID.uuidString)) + #expect(service.myNickname == "TestUser") } - func testPeerConnection() { - // Test connecting a peer - service.simulateConnectedPeer("PEER5678") - XCTAssertTrue(service.isPeerConnected("PEER5678")) - XCTAssertEqual(service.getConnectedPeers().count, 1) + @Test func peerConnection() { + let somePeerID = PeerID(str: UUID().uuidString) - // Test disconnecting a peer - service.simulateDisconnectedPeer("PEER5678") - XCTAssertFalse(service.isPeerConnected("PEER5678")) - XCTAssertEqual(service.getConnectedPeers().count, 0) + service.simulateConnectedPeer(somePeerID) + #expect(service.isPeerConnected(somePeerID)) + #expect(service.getConnectedPeers().count == 1) + + service.simulateDisconnectedPeer(somePeerID) + #expect(!service.isPeerConnected(somePeerID)) + #expect(service.getConnectedPeers().count == 0) } - func testMultiplePeerConnections() { - service.simulateConnectedPeer("PEER1") - service.simulateConnectedPeer("PEER2") - service.simulateConnectedPeer("PEER3") + @Test func multiplePeerConnections() { + let peerID1 = PeerID(str: UUID().uuidString) + let peerID2 = PeerID(str: UUID().uuidString) + let peerID3 = PeerID(str: UUID().uuidString) + + service.simulateConnectedPeer(peerID1) + service.simulateConnectedPeer(peerID2) + service.simulateConnectedPeer(peerID3) - XCTAssertEqual(service.getConnectedPeers().count, 3) - XCTAssertTrue(service.isPeerConnected("PEER1")) - XCTAssertTrue(service.isPeerConnected("PEER2")) - XCTAssertTrue(service.isPeerConnected("PEER3")) + #expect(service.getConnectedPeers().count == 3) + #expect(service.isPeerConnected(peerID1)) + #expect(service.isPeerConnected(peerID2)) + #expect(service.isPeerConnected(peerID3)) - service.simulateDisconnectedPeer("PEER2") - XCTAssertEqual(service.getConnectedPeers().count, 2) - XCTAssertFalse(service.isPeerConnected("PEER2")) + service.simulateDisconnectedPeer(peerID2) + #expect(service.getConnectedPeers().count == 2) + #expect(!service.isPeerConnected(peerID2)) } // MARK: - Message Sending Tests - func testSendPublicMessage() { - let expectation = XCTestExpectation(description: "Message sent") - - let delegate = MockBitchatDelegate { message in - XCTAssertEqual(message.content, "Hello, world!") - XCTAssertEqual(message.sender, "TestUser") - XCTAssertFalse(message.isPrivate) - expectation.fulfill() + @Test func sendPublicMessage() async throws { + try await confirmation { receivedPublicMessage in + let delegate = MockBitchatDelegate { message in + #expect(message.content == "Hello, world!") + #expect(message.sender == "TestUser") + #expect(!message.isPrivate) + receivedPublicMessage() + } + service.delegate = delegate + service.sendMessage("Hello, world!") + + // Allow async processing + try await sleep(0.5) } - service.delegate = delegate - - service.sendMessage("Hello, world!") - - wait(for: [expectation], timeout: 1.0) - XCTAssertEqual(service.sentMessages.count, 1) + #expect(service.sentMessages.count == 1) } - func testSendPrivateMessage() { - let expectation = XCTestExpectation(description: "Private message sent") - - let delegate = MockBitchatDelegate { message in - XCTAssertEqual(message.content, "Secret message") - XCTAssertEqual(message.sender, "TestUser") - XCTAssertTrue(message.isPrivate) - XCTAssertEqual(message.recipientNickname, "Bob") - expectation.fulfill() + @Test func sendPrivateMessage() async throws { + try await confirmation { receivedPrivateMessage in + let delegate = MockBitchatDelegate { message in + #expect(message.content == "Secret message") + #expect(message.sender == "TestUser") + #expect(message.senderPeerID == PeerID(str: myUUID.uuidString)) + #expect(message.isPrivate) + #expect(message.recipientNickname == "Bob") + receivedPrivateMessage() + } + service.delegate = delegate + service.sendPrivateMessage( + "Secret message", + to: PeerID(str: UUID().uuidString), + recipientNickname: "Bob", + messageID: "MSG123" + ) + + // Allow async processing + try await sleep(0.5) } - service.delegate = delegate - - service.sendPrivateMessage("Secret message", to: "PEER5678", recipientNickname: "Bob", messageID: "MSG123") - - wait(for: [expectation], timeout: 1.0) - XCTAssertEqual(service.sentMessages.count, 1) + #expect(service.sentMessages.count == 1) } - func testSendMessageWithMentions() { - let expectation = XCTestExpectation(description: "Message with mentions sent") - - let delegate = MockBitchatDelegate { message in - XCTAssertEqual(message.content, "@alice @bob check this out") - XCTAssertEqual(message.mentions, ["alice", "bob"]) - expectation.fulfill() + @Test func sendMessageWithMentions() async throws { + try await confirmation { receivedMessageWithMentions in + let delegate = MockBitchatDelegate { message in + #expect(message.content == "@alice @bob check this out") + #expect(message.mentions == ["alice", "bob"]) + receivedMessageWithMentions() + } + service.delegate = delegate + service.sendMessage("@alice @bob check this out", mentions: ["alice", "bob"]) + + // Allow async processing + try await sleep(0.5) } - service.delegate = delegate - - service.sendMessage("@alice @bob check this out", mentions: ["alice", "bob"]) - - wait(for: [expectation], timeout: 1.0) } // MARK: - Message Reception Tests - func testSimulateIncomingMessage() { - let expectation = XCTestExpectation(description: "Message received") - - let delegate = MockBitchatDelegate { message in - XCTAssertEqual(message.content, "Incoming message") - XCTAssertEqual(message.sender, "RemoteUser") - expectation.fulfill() + @Test func simulateIncomingMessage() async throws { + try await confirmation { receiveMessage in + let peerID = PeerID(str: UUID().uuidString) + + let delegate = MockBitchatDelegate { message in + #expect(message.content == "Incoming message") + #expect(message.sender == "RemoteUser") + #expect(message.senderPeerID == peerID) + receiveMessage() + } + service.delegate = delegate + + let incomingMessage = BitchatMessage( + id: "MSG456", + sender: "RemoteUser", + content: "Incoming message", + timestamp: Date(), + isRelay: false, + originalSender: nil, + isPrivate: false, + recipientNickname: nil, + senderPeerID: peerID, + mentions: nil + ) + service.simulateIncomingMessage(incomingMessage) + + // Allow async processing + try await sleep(0.5) } - service.delegate = delegate - - let incomingMessage = BitchatMessage( - id: "MSG456", - sender: "RemoteUser", - content: "Incoming message", - timestamp: Date(), - isRelay: false, - originalSender: nil, - isPrivate: false, - recipientNickname: nil, - senderPeerID: "REMOTE123", - mentions: nil - ) - - service.simulateIncomingMessage(incomingMessage) - - wait(for: [expectation], timeout: 1.0) } - func testSimulateIncomingPacket() { - let expectation = XCTestExpectation(description: "Packet processed") - - let delegate = MockBitchatDelegate { message in - XCTAssertEqual(message.content, "Packet message") - expectation.fulfill() + @Test func simulateIncomingPacket() async throws { + try await confirmation { processPacket in + let peerID = PeerID(str: UUID().uuidString) + + let delegate = MockBitchatDelegate { message in + #expect(message.content == "Packet message") + #expect(message.senderPeerID == peerID) + processPacket() + } + service.delegate = delegate + + let message = BitchatMessage( + id: "MSG789", + sender: "PacketSender", + content: "Packet message", + timestamp: Date(), + isRelay: false, + originalSender: nil, + isPrivate: false, + recipientNickname: nil, + senderPeerID: peerID, + mentions: nil + ) + + let payload = try #require(message.toBinaryPayload(), "Failed to create binary payload") + + let packet = BitchatPacket( + type: 0x01, + senderID: peerID.id.data(using: .utf8)!, + recipientID: nil, + timestamp: UInt64(Date().timeIntervalSince1970 * 1000), + payload: payload, + signature: nil, + ttl: 3 + ) + + service.simulateIncomingPacket(packet) + + // Allow async processing + try await sleep(0.5) } - service.delegate = delegate - - let message = BitchatMessage( - id: "MSG789", - sender: "PacketSender", - content: "Packet message", - timestamp: Date(), - isRelay: false, - originalSender: nil, - isPrivate: false, - recipientNickname: nil, - senderPeerID: "PACKET123", - mentions: nil - ) - - guard let payload = message.toBinaryPayload() else { - XCTFail("Failed to create binary payload") - return - } - - let packet = BitchatPacket( - type: 0x01, - senderID: "PACKET123".data(using: .utf8)!, - recipientID: nil, - timestamp: UInt64(Date().timeIntervalSince1970 * 1000), - payload: payload, - signature: nil, - ttl: 3 - ) - - service.simulateIncomingPacket(packet) - - wait(for: [expectation], timeout: 1.0) } // MARK: - Peer Nickname Tests - func testGetPeerNicknames() { - service.simulateConnectedPeer("PEER1") - service.simulateConnectedPeer("PEER2") + @Test func getPeerNicknames() { + let peerID1 = PeerID(str: UUID().uuidString) + let peerID2 = PeerID(str: UUID().uuidString) + + service.simulateConnectedPeer(peerID1) + service.simulateConnectedPeer(peerID2) let nicknames = service.getPeerNicknames() - XCTAssertEqual(nicknames.count, 2) - XCTAssertEqual(nicknames["PEER1"], "MockPeer_PEER1") - XCTAssertEqual(nicknames["PEER2"], "MockPeer_PEER2") + #expect(nicknames.count == 2) + #expect(nicknames[peerID1] == "MockPeer_\(peerID1)") + #expect(nicknames[peerID2] == "MockPeer_\(peerID2)") } // MARK: - Service State Tests - func testStartStopServices() { - // These are mock implementations, just ensure they don't crash + @Test func startStopServices() { service.startServices() service.stopServices() - - // Service should still be functional after start/stop - service.simulateConnectedPeer("PEER999") - XCTAssertTrue(service.isPeerConnected("PEER999")) + let somePeerID = PeerID(str: UUID().uuidString) + service.simulateConnectedPeer(somePeerID) + #expect(service.isPeerConnected(somePeerID)) } // MARK: - Message Delivery Handler Tests - func testMessageDeliveryHandler() { - let expectation = XCTestExpectation(description: "Delivery handler called") - - service.packetDeliveryHandler = { packet in - if let msg = BitchatMessage(packet.payload) { - XCTAssertEqual(msg.content, "Test delivery") - expectation.fulfill() + @Test func messageDeliveryHandler() async throws { + try await confirmation { deliveryHandler in + service.packetDeliveryHandler = { packet in + if let msg = BitchatMessage(packet.payload) { + #expect(msg.content == "Test delivery") + deliveryHandler() + } } + service.sendMessage("Test delivery") + + // Allow async processing + try await sleep(0.5) } - - service.sendMessage("Test delivery") - - wait(for: [expectation], timeout: 1.0) } - func testPacketDeliveryHandler() { - let expectation = XCTestExpectation(description: "Packet handler called") - - service.packetDeliveryHandler = { packet in - XCTAssertEqual(packet.type, 0x01) - expectation.fulfill() + @Test func packetDeliveryHandler() async throws { + try await confirmation("Packet handler called") { packetHandler in + let peerID = PeerID(str: UUID().uuidString) + + service.packetDeliveryHandler = { packet in + #expect(packet.type == 0x01) + #expect(packet.senderID == Data(peerID.id.utf8)) + packetHandler() + } + + let message = BitchatMessage( + id: "PKT123", + sender: "TestSender", + content: "Test packet", + timestamp: Date(), + isRelay: false, + originalSender: nil, + isPrivate: false, + recipientNickname: nil, + senderPeerID: peerID, + mentions: nil + ) + + let payload = try #require(message.toBinaryPayload(), "Failed to create payload") + + let packet = BitchatPacket( + type: 0x01, + senderID: peerID.id.data(using: .utf8)!, + recipientID: nil, + timestamp: UInt64(Date().timeIntervalSince1970 * 1000), + payload: payload, + signature: nil, + ttl: 3 + ) + + service.simulateIncomingPacket(packet) + + // Allow async processing + try await sleep(0.5) } - - let message = BitchatMessage( - id: "PKT123", - sender: "TestSender", - content: "Test packet", - timestamp: Date(), - isRelay: false, - originalSender: nil, - isPrivate: false, - recipientNickname: nil, - senderPeerID: "TEST123", - mentions: nil - ) - - guard let payload = message.toBinaryPayload() else { - XCTFail("Failed to create payload") - return - } - - let packet = BitchatPacket( - type: 0x01, - senderID: "TEST123".data(using: .utf8)!, - recipientID: nil, - timestamp: UInt64(Date().timeIntervalSince1970 * 1000), - payload: payload, - signature: nil, - ttl: 3 - ) - - service.simulateIncomingPacket(packet) - - wait(for: [expectation], timeout: 1.0) } } diff --git a/bitchatTests/CommandProcessorTests.swift b/bitchatTests/CommandProcessorTests.swift index 071dad96..343fecef 100644 --- a/bitchatTests/CommandProcessorTests.swift +++ b/bitchatTests/CommandProcessorTests.swift @@ -1,54 +1,42 @@ -import XCTest +import Testing @testable import bitchat -final class CommandProcessorTests: XCTestCase { - - var identityManager: MockIdentityManager! - - override func setUp() { - super.setUp() - // Provide a minimal identity manager for commands that query identity/block lists - identityManager = MockIdentityManager(MockKeychain()) - } - - override func tearDown() { - identityManager = nil - super.tearDown() - } +struct CommandProcessorTests { + private var identityManager = MockIdentityManager(MockKeychain()) @MainActor - func test_slap_notFoundGrammar() { + @Test func slapNotFoundGrammar() { let processor = CommandProcessor(chatViewModel: nil, meshService: nil, identityManager: identityManager) let result = processor.process("/slap @system") switch result { case .error(let message): - XCTAssertEqual(message, "cannot slap system: not found") + #expect(message == "cannot slap system: not found") default: - XCTFail("Expected error result") + Issue.record("Expected error result") } } @MainActor - func test_hug_notFoundGrammar() { + @Test func hugNotFoundGrammar() { let processor = CommandProcessor(chatViewModel: nil, meshService: nil, identityManager: identityManager) let result = processor.process("/hug @system") switch result { case .error(let message): - XCTAssertEqual(message, "cannot hug system: not found") + #expect(message == "cannot hug system: not found") default: - XCTFail("Expected error result") + Issue.record("Expected error result") } } - + @MainActor - func test_slap_usageMessage() { + @Test func slapUsageMessage() { let processor = CommandProcessor(chatViewModel: nil, meshService: nil, identityManager: identityManager) let result = processor.process("/slap") switch result { case .error(let message): - XCTAssertEqual(message, "usage: /slap ") + #expect(message == "usage: /slap ") default: - XCTFail("Expected error result for usage message") + Issue.record("Expected error result for usage message") } } } diff --git a/bitchatTests/EndToEnd/PrivateChatE2ETests.swift b/bitchatTests/EndToEnd/PrivateChatE2ETests.swift index 47d72dd4..2f0d45c3 100644 --- a/bitchatTests/EndToEnd/PrivateChatE2ETests.swift +++ b/bitchatTests/EndToEnd/PrivateChatE2ETests.swift @@ -11,21 +11,19 @@ import CryptoKit import struct Foundation.UUID @testable import bitchat -// TODO: Remove once MockBLEService is refactored to fix race condition -@Suite(.serialized) struct PrivateChatE2ETests { private let alice: MockBLEService private let bob: MockBLEService private let charlie: MockBLEService - private let mockKeychain: MockKeychain + private let mockKeychain = MockKeychain() + private let bus = MockBLEBus() init() { // Create services with unique peer IDs to avoid any collision - alice = MockBLEService(peerID: PeerID(str: UUID().uuidString), nickname: TestConstants.testNickname1) - bob = MockBLEService(peerID: PeerID(str: UUID().uuidString), nickname: TestConstants.testNickname2) - charlie = MockBLEService(peerID: PeerID(str: UUID().uuidString), nickname: TestConstants.testNickname3) - mockKeychain = MockKeychain() + alice = MockBLEService(peerID: PeerID(str: UUID().uuidString), nickname: TestConstants.testNickname1, bus: bus) + bob = MockBLEService(peerID: PeerID(str: UUID().uuidString), nickname: TestConstants.testNickname2, bus: bus) + charlie = MockBLEService(peerID: PeerID(str: UUID().uuidString), nickname: TestConstants.testNickname3, bus: bus) } // MARK: - Basic Private Messaging Tests @@ -53,7 +51,7 @@ struct PrivateChatE2ETests { ) // Wait a bit to ensure message would have been delivered if it was going to be - try? await Task.sleep(nanoseconds: UInt64(TestConstants.shortTimeout * 1_000_000_000)) + try? await sleep(0.1) } #expect(!bobReceivedMessage, "Bob should not have received the message") @@ -171,7 +169,7 @@ struct PrivateChatE2ETests { // Send encrypted private message alice.sendPrivateMessage( TestConstants.testMessage1, - to: TestConstants.testPeerID2, + to: bob.peerID, recipientNickname: TestConstants.testNickname2 ) } @@ -235,7 +233,7 @@ struct PrivateChatE2ETests { for i in 0..= 1) let decoded = GCSFilter.decodeToSortedSet(p: params.p, m: params.m, data: params.data) - XCTAssertLessThanOrEqual(decoded.count, 1) + #expect(decoded.count <= 1) } - func testBucketAvoidsZeroCandidate() { + @Test func bucketAvoidsZeroCandidate() { let id = Data(repeating: 0x01, count: 16) let bucket = GCSFilter.bucket(for: id, modulus: 2) - XCTAssertNotEqual(bucket, 0) - XCTAssertLessThan(bucket, 2) + #expect(bucket != 0) + #expect(bucket < 2) } } diff --git a/bitchatTests/GeohashBookmarksStoreTests.swift b/bitchatTests/GeohashBookmarksStoreTests.swift index a7804ef9..0979dec4 100644 --- a/bitchatTests/GeohashBookmarksStoreTests.swift +++ b/bitchatTests/GeohashBookmarksStoreTests.swift @@ -1,52 +1,38 @@ -import XCTest +import Testing +import Foundation @testable import bitchat -final class GeohashBookmarksStoreTests: XCTestCase { - let storeKey = "locationChannel.bookmarks" - var storage: UserDefaults! - var store: GeohashBookmarksStore! +struct GeohashBookmarksStoreTests { + private let storeKey = "locationChannel.bookmarks" + private let storage = UserDefaults(suiteName: UUID().uuidString)! + private let store: GeohashBookmarksStore - override func setUp() { - super.setUp() - // Unique instance for each test to avoid race condition - storage = UserDefaults(suiteName: UUID().uuidString) - store = GeohashBookmarksStore(storage: storage!) + init() { + store = GeohashBookmarksStore(storage: storage) } - override func tearDown() { - storage.removeObject(forKey: storeKey) - store._resetForTesting() - store = nil - storage = nil - super.tearDown() - } - - func testToggleAndNormalize() { + @Test func toggleAndNormalize() { // Start clean - XCTAssertTrue(store.bookmarks.isEmpty) + #expect(store.bookmarks.isEmpty) // Add with mixed case and hash prefix store.toggle("#U4PRUY") - XCTAssertTrue(store.isBookmarked("u4pruy")) - XCTAssertEqual(store.bookmarks.first, "u4pruy") + #expect(store.isBookmarked("u4pruy")) + #expect(store.bookmarks.first == "u4pruy") // Toggling again removes store.toggle("u4pruy") - XCTAssertFalse(store.isBookmarked("u4pruy")) - XCTAssertTrue(store.bookmarks.isEmpty) + #expect(!store.isBookmarked("u4pruy")) + #expect(store.bookmarks.isEmpty) } - func testPersistenceWritten() throws { + @Test func persistenceWritten() throws { store.toggle("ezs42") store.toggle("u4pruy") - // Verify persisted JSON contains both (order not enforced here) - guard let data = storage.data(forKey: storeKey) else { - XCTFail("No persisted data found") - return - } + let data = try #require(storage.data(forKey: storeKey), "No persisted data found") let arr = try JSONDecoder().decode([String].self, from: data) - XCTAssertTrue(arr.contains("ezs42")) - XCTAssertTrue(arr.contains("u4pruy")) + #expect(arr.contains("ezs42")) + #expect(arr.contains("u4pruy")) } } diff --git a/bitchatTests/GossipSyncManagerTests.swift b/bitchatTests/GossipSyncManagerTests.swift index c37610e7..a9ca9cf2 100644 --- a/bitchatTests/GossipSyncManagerTests.swift +++ b/bitchatTests/GossipSyncManagerTests.swift @@ -1,24 +1,28 @@ import Foundation -import XCTest +import Testing @testable import bitchat -final class GossipSyncManagerTests: XCTestCase { - func testConcurrentPacketIntakeAndSyncRequest() { - let manager = GossipSyncManager(myPeerID: "0102030405060708") +struct GossipSyncManagerTests { + + private let myPeerID = PeerID(str: "0102030405060708") + + @Test func concurrentPacketIntakeAndSyncRequest() async throws { + let manager = GossipSyncManager(myPeerID: myPeerID) let delegate = RecordingDelegate() - let sendExpectation = expectation(description: "sync request sent") - delegate.onSend = { sendExpectation.fulfill() } manager.delegate = delegate - let iterations = 200 - let group = DispatchGroup() + try await confirmation("sync request sent") { sent in + delegate.onSend = { + sent() + } - for i in 0.. // -import XCTest +import Foundation import CryptoKit +import Testing @testable import bitchat -final class IntegrationTests: XCTestCase { +struct IntegrationTests { - var nodes: [String: MockBLEService] = [:] - var noiseManagers: [String: NoiseSessionManager] = [:] - private var mockKeychain: MockKeychain! + private var helper = TestNetworkHelper() - override func setUp() { - super.setUp() - // Use the in-memory test bus with autoFlood enabled to simulate - // broadcast propagation across a larger mesh. Integration-only. - MockBLEService.resetTestBus() - MockBLEService.autoFloodEnabled = true - mockKeychain = MockKeychain() - - // 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() { - // Disable flooding to avoid cross-test interference - MockBLEService.autoFloodEnabled = false - nodes.removeAll() - noiseManagers.removeAll() - mockKeychain = nil - super.tearDown() + init() { + helper.createNode("Alice", peerID: PeerID(str: UUID().uuidString)) + helper.createNode("Bob", peerID: PeerID(str: UUID().uuidString)) + helper.createNode("Charlie", peerID: PeerID(str: UUID().uuidString)) + helper.createNode("David", peerID: PeerID(str: UUID().uuidString)) } // MARK: - Multi-Peer Scenarios - func testFullMeshCommunication() { - // Create full mesh - everyone connected to everyone - connectFullMesh() + @Test func fullMeshCommunication() async throws { + helper.connectFullMesh() - let expectation = XCTestExpectation(description: "All nodes communicate") var messageMatrix: [String: Set] = [:] + for (senderName, _) in helper.nodes { messageMatrix[senderName] = [] } - // Track all receivers; parse sender name from message content "Hello from " - for (senderName, _) in nodes { messageMatrix[senderName] = [] } - for (receiverName, receiver) in nodes { + for (receiverName, receiver) in helper.nodes { receiver.messageDeliveryHandler = { message in let parts = message.content.components(separatedBy: " ") if let last = parts.last, message.content.contains("Hello from") { @@ -62,370 +41,336 @@ final class IntegrationTests: XCTestCase { } } - // Each node sends a message - for (name, node) in nodes { - node.sendMessage("Hello from \(name)", mentions: [], to: nil) + for (name, node) in helper.nodes { + node.sendMessage("Hello from \(name)") } - // 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() + // Each sender should have reached all other nodes + for (sender, receivers) in messageMatrix { + let expectedReceivers = Set(helper.nodes.keys.filter { $0 != sender }) + #expect(receivers == expectedReceivers, "\(sender) didn't reach all nodes") } - - wait(for: [expectation], timeout: TestConstants.defaultTimeout) } - func testDynamicTopologyChanges() { + @Test func dynamicTopologyChanges() async throws { // Start with Alice -> Bob -> Charlie - connect("Alice", "Bob") - connect("Bob", "Charlie") + helper.connect("Alice", "Bob") + helper.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() + try await confirmation("Topology changes handled") { receiveMessage in + var phase = 1 + + helper.nodes["Charlie"]!.messageDeliveryHandler = { message in + if phase == 1 && message.sender == "Alice" { + // Now change topology: disconnect Bob, connect Alice-Charlie + helper.disconnect("Alice", "Bob") + helper.disconnect("Bob", "Charlie") + helper.connect("Alice", "Charlie") + phase = 2 + + // Send another message + helper.nodes["Alice"]!.sendMessage("Direct message") + } else if phase == 2 && message.content == "Direct message" { + receiveMessage() + } } + + // Allow relay handler to be set before first send + try await sleep(0.05) + helper.nodes["Alice"]!.sendMessage("Relayed message") } - - // Initial message through relay - // Allow relay handler to be set before first send - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { - self.nodes["Alice"]!.sendMessage("Relayed message", mentions: [], to: nil) - } - - wait(for: [expectation], timeout: TestConstants.defaultTimeout) } - func testNetworkPartitionRecovery() { + @Test func networkPartitionRecovery() async throws { // Create two partitions - connect("Alice", "Bob") - connect("Charlie", "David") + helper.connect("Alice", "Bob") + helper.connect("Charlie", "David") - let expectation = XCTestExpectation(description: "Partitions merge and communicate") let 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 await confirmation("Partitions merge and communicate") { receiveMessage in + // Monitor cross-partition messages + helper.nodes["David"]!.messageDeliveryHandler = { message in + if message.sender == "Alice" { + messagesAfterMerge += 1 + if messagesAfterMerge == 1 { + receiveMessage() + } } } - } - - // 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) { + + // Try to send across partition (should fail) + helper.nodes["Alice"]!.sendMessage("Before merge") + + // Merge partitions after delay + try await sleep(0.05) // Connect partitions - self.connect("Bob", "Charlie") + helper.connect("Bob", "Charlie") // Enable relay - self.setupRelay("Bob", nextHops: ["Charlie"]) - self.setupRelay("Charlie", nextHops: ["David"]) + helper.setupRelay("Bob", nextHops: ["Charlie"]) + helper.setupRelay("Charlie", nextHops: ["David"]) // Send message across merged network - self.nodes["Alice"]!.sendMessage("After merge", mentions: [], to: nil) + helper.nodes["Alice"]!.sendMessage("After merge") } - wait(for: [expectation], timeout: TestConstants.defaultTimeout) - XCTAssertEqual(messagesBeforeMerge, 0) - XCTAssertEqual(messagesAfterMerge, 1) + #expect(messagesBeforeMerge == 0) + #expect(messagesAfterMerge == 1) } // MARK: - Mixed Message Type Scenarios - func testMixedPublicPrivateMessages() throws { - connectFullMesh() + @Test func mixedPublicPrivateMessages() async throws { + helper.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 + await confirmation("Mixed messages handled correctly") { completion in + // Bob monitors messages + helper.nodes["Bob"]!.messageDeliveryHandler = { message in + if message.isPrivate && message.recipientNickname == "Bob" { + privateCount += 1 + } else if !message.isPrivate { + publicCount += 1 + } + + if publicCount == 2 && privateCount == 1 { + completion() + } } - if publicCount == 2 && privateCount == 1 { - expectation.fulfill() - } + // Alice sends mixed messages + helper.nodes["Alice"]!.sendMessage("Public 1") + helper.nodes["Alice"]!.sendPrivateMessage("Private to Bob", to: helper.nodes["Bob"]!.peerID, recipientNickname: "Bob") + helper.nodes["Alice"]!.sendMessage("Public 2") } - // 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) + #expect(publicCount == 2) + #expect(privateCount == 1) } - func testEncryptedAndUnencryptedMix() throws { - connect("Alice", "Bob") + @Test func encryptedAndUnencryptedMix() async throws { + helper.connect("Alice", "Bob") // Setup Noise session - try establishNoiseSession("Alice", "Bob") + try helper.establishNoiseSession("Alice", "Bob") - let expectation = XCTestExpectation(description: "Both encrypted and plain messages work") var plainCount = 0 var encryptedCount = 0 - // Setup handlers - // Plain path: send public message and count at Bob - nodes["Bob"]!.messageDeliveryHandler = { message in - if message.content == "Plain message" { plainCount += 1 } - if plainCount == 1 && encryptedCount == 1 { expectation.fulfill() } - } - - // Encrypted path: use NoiseSessionManager explicitly - let plaintext = "Encrypted message".data(using: .utf8)! - let ciphertext = try noiseManagers["Alice"]!.encrypt(plaintext, for: TestConstants.testPeerID2) - nodes["Bob"]!.packetDeliveryHandler = { packet in - if packet.type == MessageType.noiseEncrypted.rawValue { - if let data = try? self.noiseManagers["Bob"]!.decrypt(ciphertext, from: TestConstants.testPeerID1), - data == plaintext { - encryptedCount = 1 - if plainCount == 1 { expectation.fulfill() } + try await confirmation("Both encrypted and plain messages work") { completion in + // Plain path: send public message and count at Bob + helper.nodes["Bob"]!.messageDeliveryHandler = { message in + if message.content == "Plain message" { + plainCount += 1 + } + if plainCount == 1 && encryptedCount == 1 { + completion() } } + + // Encrypted path: use NoiseSessionManager explicitly + let plaintext = "Encrypted message".data(using: .utf8)! + let ciphertext = try helper.noiseManagers["Alice"]!.encrypt(plaintext, for: helper.nodes["Bob"]!.peerID) + + helper.nodes["Bob"]!.packetDeliveryHandler = { packet in + if packet.type == MessageType.noiseEncrypted.rawValue { + if let data = try? helper.noiseManagers["Bob"]!.decrypt(ciphertext, from: helper.nodes["Alice"]!.peerID), + data == plaintext { + encryptedCount = 1 + if plainCount == 1 { + completion() + } + } + } + } + + helper.nodes["Alice"]!.sendMessage("Plain message") + // Deliver encrypted packet directly + let encPacket = TestHelpers.createTestPacket(type: MessageType.noiseEncrypted.rawValue, payload: ciphertext) + helper.nodes["Bob"]!.simulateIncomingPacket(encPacket) } - - nodes["Alice"]!.sendMessage("Plain message", mentions: [], to: nil) - // Deliver encrypted packet directly - let encPacket = TestHelpers.createTestPacket(type: MessageType.noiseEncrypted.rawValue, payload: ciphertext) - nodes["Bob"]!.simulateIncomingPacket(encPacket) - - wait(for: [expectation], timeout: TestConstants.defaultTimeout) } // MARK: - Network Resilience Tests - func testMessageDeliveryUnderChurn() { + @Test func messageDeliveryUnderChurn() async throws { // Start with stable network - connectFullMesh() + helper.connectFullMesh() - let expectation = XCTestExpectation(description: "Messages delivered despite churn") - var receivedMessages = Set() let totalMessages = 10 - // David tracks received messages - nodes["David"]!.messageDeliveryHandler = { message in - receivedMessages.insert(message.content) - if receivedMessages.count == totalMessages { - expectation.fulfill() + try await confirmation("Messages delivered despite churn", expectedCount: totalMessages) { completion in + // David tracks received messages + helper.nodes["David"]!.messageDeliveryHandler = { message in + completion() } - } - - // Send messages while churning network - for i in 0..() - - // 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() + await confirmation("Large network handles broadcast", expectedCount: helper.nodes.count - 1) { nodeReaced in + // All nodes except Alice listen + for (name, node) in helper.nodes where name != "Alice" { + node.messageDeliveryHandler = { message in + if message.content == "Broadcast test" { + nodeReaced() } } } + + // Alice broadcasts + helper.nodes["Alice"]!.sendMessage("Broadcast test") } - - // 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() + @Test func highLoadScenario() async throws { + helper.connectFullMesh() let messagesPerNode = 25 - let expectedTotal = messagesPerNode * nodes.count * (nodes.count - 1) - var receivedTotal = 0 - let expectation = XCTestExpectation(description: "High load handled") + let expectedTotal = messagesPerNode * helper.nodes.count * (helper.nodes.count - 1) - // Each node tracks messages - for (_, node) in nodes { - node.messageDeliveryHandler = { _ in - receivedTotal += 1 - if receivedTotal >= (expectedTotal - 2) { - expectation.fulfill() + await confirmation("High load handled", expectedCount: expectedTotal) { received in + // Each node tracks messages + for (_, node) in helper.nodes { + node.messageDeliveryHandler = { _ in + received() } } - } - - // All nodes send many messages simultaneously - DispatchQueue.concurrentPerform(iterations: nodes.count) { index in - let nodeName = Array(nodes.keys).sorted()[index] - for i in 0.. 0) + #expect(metrics["private", default: 0] > 0) + #expect(metrics["mentions", default: 0] > 0) } // MARK: - Security Integration Tests // Replacement for the legacy NACK test: verifies that after a // decryption failure, peers can rehandshake via NoiseSessionManager // and resume secure communication. - func testRehandshakeAfterDecryptionFailure() throws { + @Test func rehandshakeAfterDecryptionFailure() throws { // Alice <-> Bob connected - connect("Alice", "Bob") - + helper.connect("Alice", "Bob") + // Establish initial Noise session - try establishNoiseSession("Alice", "Bob") - - guard let aliceManager = noiseManagers["Alice"], - let bobManager = noiseManagers["Bob"], - let alicePeerID = nodes["Alice"]?.peerID, - let bobPeerID = nodes["Bob"]?.peerID else { - return XCTFail("Missing managers or peer IDs") + try helper.establishNoiseSession("Alice", "Bob") + + guard let aliceManager = helper.noiseManagers["Alice"], + let bobManager = helper.noiseManagers["Bob"], + let alicePeerID = helper.nodes["Alice"]?.peerID, + let bobPeerID = helper.nodes["Bob"]?.peerID + else { + Issue.record("Missing managers or peer IDs") + return } - + // Baseline: encrypt from Alice, decrypt at Bob let plaintext1 = Data("hello-secure".utf8) let encrypted1 = try aliceManager.encrypt(plaintext1, for: bobPeerID) let decrypted1 = try bobManager.decrypt(encrypted1, from: alicePeerID) - XCTAssertEqual(decrypted1, plaintext1) - + #expect(decrypted1 == plaintext1) + // Simulate decryption failure by corrupting ciphertext - var corrupted = encrypted1 - if !corrupted.isEmpty { corrupted[corrupted.count - 1] ^= 0xFF } - do { + let corrupted = encrypted1.prefix(15) + #expect(throws: NoiseError.invalidCiphertext) { _ = try bobManager.decrypt(corrupted, from: alicePeerID) - XCTFail("Corrupted ciphertext should not decrypt") - } catch { - // Expected: treat as session desync and rehandshake } - + // Bob initiates a new handshake; clear Bob's session first so initiateHandshake won't throw bobManager.removeSession(for: alicePeerID) - try establishNoiseSession("Bob", "Alice") - + try helper.establishNoiseSession("Bob", "Alice") + // After rehandshake, encryption/decryption works again let plaintext2 = Data("hello-again".utf8) let encrypted2 = try aliceManager.encrypt(plaintext2, for: bobPeerID) let decrypted2 = try bobManager.decrypt(encrypted2, from: alicePeerID) - XCTAssertEqual(decrypted2, plaintext2) + #expect(decrypted2 == plaintext2) } - - func testEndToEndSecurityScenario() throws { - connect("Alice", "Bob") - connect("Bob", "Charlie") // Charlie will try to eavesdrop + @Test func endToEndSecurityScenario() async throws { + helper.connect("Alice", "Bob") + helper.connect("Bob", "Charlie") // Charlie will try to eavesdrop // Establish secure session between Alice and Bob only - try establishNoiseSession("Alice", "Bob") + try helper.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(packet.payload), - message.isPrivate && packet.recipientID != nil { - // Encrypt private messages - if let encrypted = try? self.noiseManagers["Alice"]!.encrypt(packet.payload, for: TestConstants.testPeerID2) { - let encPacket = BitchatPacket( - type: 0x02, - senderID: packet.senderID, - recipientID: packet.recipientID, - timestamp: packet.timestamp, - payload: encrypted, - signature: packet.signature, - ttl: packet.ttl - ) - 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(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: PeerID) { - let node = MockBLEService() - node.myPeerID = peerID - node.mockNickname = name - nodes[name] = node - - // Create Noise manager - let key = Curve25519.KeyAgreement.PrivateKey() - noiseManagers[name] = NoiseSessionManager(localStaticKey: key, keychain: mockKeychain) - } - - 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.. 1 else { return } + await confirmation("Secure communication maintained", expectedCount: 2) { receivedPacket in - if let message = BitchatMessage(packet.payload) { - guard message.senderPeerID != node.peerID else { return } - - let relayMessage = BitchatMessage( - id: message.id, - sender: message.sender, - content: message.content, - timestamp: message.timestamp, - isRelay: true, - originalSender: message.isRelay ? message.originalSender : message.sender, - isPrivate: message.isPrivate, - recipientNickname: message.recipientNickname, - senderPeerID: message.senderPeerID, - mentions: message.mentions - ) - - if let relayPayload = relayMessage.toBinaryPayload() { - let relayPacket = BitchatPacket( - type: packet.type, - senderID: packet.senderID, - recipientID: packet.recipientID, - timestamp: packet.timestamp, - payload: relayPayload, - signature: packet.signature, - ttl: packet.ttl - 1 - ) - - for hop in nextHops { - self.nodes[hop]?.simulateIncomingPacket(relayPacket) + // Setup encryption at Alice + helper.nodes["Alice"]!.packetDeliveryHandler = { packet in + if packet.type == 0x01, + let message = BitchatMessage(packet.payload), + message.isPrivate && packet.recipientID != nil { + // Encrypt private messages + if let encrypted = try? helper.noiseManagers["Alice"]!.encrypt(packet.payload, for: helper.nodes["Bob"]!.peerID) { + let encPacket = BitchatPacket( + type: 0x02, + senderID: packet.senderID, + recipientID: packet.recipientID, + timestamp: packet.timestamp, + payload: encrypted, + signature: packet.signature, + ttl: packet.ttl + ) + helper.nodes["Bob"]!.simulateIncomingPacket(encPacket) } } } + + // Bob can decrypt + helper.nodes["Bob"]!.packetDeliveryHandler = { packet in + if packet.type == 0x02 { + receivedPacket() + if let decrypted = try? helper.noiseManagers["Bob"]!.decrypt(packet.payload, from: helper.nodes["Alice"]!.peerID) { + #expect(BitchatMessage(decrypted)?.content == "Secret message") + } else { + Issue.record("Bob was unable to decrypt the message") + } + + // Relay encrypted packet to Charlie + helper.nodes["Charlie"]!.simulateIncomingPacket(packet) + } + } + + // Charlie cannot decrypt + helper.nodes["Charlie"]!.packetDeliveryHandler = { packet in + if packet.type == 0x02 { + receivedPacket() + #expect(throws: NoiseSessionError.sessionNotFound, "Charlie should not be able to decrypt") { + _ = try helper.noiseManagers["Charlie"]?.decrypt(packet.payload, from: helper.nodes["Alice"]!.peerID) + } + } + } + + // Send encrypted private message + helper.nodes["Alice"]!.sendPrivateMessage("Secret message", to: helper.nodes["Bob"]!.peerID, recipientNickname: "Bob") } } - - 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) - } } diff --git a/bitchatTests/Integration/TestNetworkHelper.swift b/bitchatTests/Integration/TestNetworkHelper.swift new file mode 100644 index 00000000..eb3eeb62 --- /dev/null +++ b/bitchatTests/Integration/TestNetworkHelper.swift @@ -0,0 +1,123 @@ +// +// TestNetworkHelper.swift +// bitchatTests +// +// Extracted shared, mutable integration state for nodes and noise sessions. +// Keeps test containers nonmutating (Swift Testing-friendly). +// + +import Foundation +import CryptoKit +@testable import bitchat + +final class TestNetworkHelper { + // Public, read-only views for tests; mutation only through methods + var nodes: [String: MockBLEService] = [:] + var noiseManagers: [String: NoiseSessionManager] = [:] + let mockKeychain = MockKeychain() + private let bus = MockBLEBus(autoFloodEnabled: true) + + // MARK: - Node/Manager management + + @discardableResult + func createNode(_ name: String, peerID: PeerID) -> MockBLEService { + let node = MockBLEService(bus: bus) + node.myPeerID = peerID + node.mockNickname = name + nodes[name] = node + + // Create/replace Noise manager for this node + let key = Curve25519.KeyAgreement.PrivateKey() + noiseManagers[name] = NoiseSessionManager(localStaticKey: key, keychain: mockKeychain) + return node + } + + func getNode(_ name: String) -> MockBLEService? { + nodes[name] + } + + func getManager(_ name: String) -> NoiseSessionManager? { + noiseManagers[name] + } + + // MARK: - Topology + + func connect(_ a: String, _ b: String) { + guard let n1 = nodes[a], let n2 = nodes[b] else { return } + n1.simulateConnectedPeer(n2.peerID) + n2.simulateConnectedPeer(n1.peerID) + } + + func disconnect(_ a: String, _ b: String) { + guard let n1 = nodes[a], let n2 = nodes[b] else { return } + n1.simulateDisconnectedPeer(n2.peerID) + n2.simulateDisconnectedPeer(n1.peerID) + } + + func connectFullMesh() { + let names = Array(nodes.keys) + for i in 0.. 1 else { return } + + if let message = BitchatMessage(packet.payload) { + guard message.senderPeerID != node.peerID else { return } + + let relayMessage = BitchatMessage( + id: message.id, + sender: message.sender, + content: message.content, + timestamp: message.timestamp, + isRelay: true, + originalSender: message.isRelay ? message.originalSender : message.sender, + isPrivate: message.isPrivate, + recipientNickname: message.recipientNickname, + senderPeerID: message.senderPeerID, + mentions: message.mentions + ) + + if let relayPayload = relayMessage.toBinaryPayload() { + let relayPacket = BitchatPacket( + type: packet.type, + senderID: packet.senderID, + recipientID: packet.recipientID, + timestamp: packet.timestamp, + payload: relayPayload, + signature: packet.signature, + ttl: packet.ttl - 1 + ) + + for hop in nextHops { + self.nodes[hop]?.simulateIncomingPacket(relayPacket) + } + } + } + } + } + + // MARK: - Noise sessions + + 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) + } +} + diff --git a/bitchatTests/LocationChannelsTests.swift b/bitchatTests/LocationChannelsTests.swift index f3954eb5..433f28d9 100644 --- a/bitchatTests/LocationChannelsTests.swift +++ b/bitchatTests/LocationChannelsTests.swift @@ -1,8 +1,9 @@ -import XCTest +import Testing +import Foundation @testable import bitchat -final class LocationChannelsTests: XCTestCase { - func testGeohashEncoderPrecisionMapping() { +struct LocationChannelsTests { + @Test func geohashEncoderPrecisionMapping() { // Sanity: known coords (Statue of Liberty approx) let lat = 40.6892 let lon = -74.0445 @@ -12,35 +13,35 @@ final class LocationChannelsTests: XCTestCase { let region = Geohash.encode(latitude: lat, longitude: lon, precision: GeohashChannelLevel.province.precision) let country = Geohash.encode(latitude: lat, longitude: lon, precision: GeohashChannelLevel.region.precision) - XCTAssertEqual(block.count, 7) - XCTAssertEqual(neighborhood.count, 6) - XCTAssertEqual(city.count, 5) - XCTAssertEqual(region.count, 4) - XCTAssertEqual(country.count, 2) + #expect(block.count == 7) + #expect(neighborhood.count == 6) + #expect(city.count == 5) + #expect(region.count == 4) + #expect(country.count == 2) // All prefixes must match progressively - XCTAssertTrue(block.hasPrefix(neighborhood)) - XCTAssertTrue(neighborhood.hasPrefix(city)) - XCTAssertTrue(city.hasPrefix(region)) - XCTAssertTrue(region.hasPrefix(country)) + #expect(block.hasPrefix(neighborhood)) + #expect(neighborhood.hasPrefix(city)) + #expect(city.hasPrefix(region)) + #expect(region.hasPrefix(country)) } - func testNostrGeohashFilterEncoding() throws { + @Test func nostrGeohashFilterEncoding() throws { let gh = "u4pruy" let filter = NostrFilter.geohashEphemeral(gh) let data = try JSONEncoder().encode(filter) let json = String(data: data, encoding: .utf8) ?? "" // Expect kinds includes 20000 and tag filter '#g':[gh] - XCTAssertTrue(json.contains("20000")) - XCTAssertTrue(json.contains("\"#g\":[\"\(gh)\"]")) + #expect(json.contains("20000")) + #expect(json.contains("\"#g\":[\"\(gh)\"]")) } - func testPerGeohashIdentityDeterministic() throws { + @Test func perGeohashIdentityDeterministic() throws { // Derive twice for same geohash; should be identical let idBridge = NostrIdentityBridge(keychain: MockKeychainHelper()) let gh = "u4pruy" let id1 = try idBridge.deriveIdentity(forGeohash: gh) let id2 = try idBridge.deriveIdentity(forGeohash: gh) - XCTAssertEqual(id1.publicKeyHex, id2.publicKeyHex) + #expect(id1.publicKeyHex == id2.publicKeyHex) } } diff --git a/bitchatTests/LocationNotesManagerTests.swift b/bitchatTests/LocationNotesManagerTests.swift index e34fd023..fcf3254e 100644 --- a/bitchatTests/LocationNotesManagerTests.swift +++ b/bitchatTests/LocationNotesManagerTests.swift @@ -1,8 +1,9 @@ -import XCTest +import Testing +import Foundation @testable import bitchat @MainActor -final class LocationNotesManagerTests: XCTestCase { +struct LocationNotesManagerTests { // func testSubscribeWithoutRelaysSetsNoRelaysState() { // var subscribeCalled = false // let deps = LocationNotesDependencies( @@ -47,15 +48,15 @@ final class LocationNotesManagerTests: XCTestCase { // XCTAssertNotEqual(manager.errorMessage, "location_notes.error.no_relays") // } - func testSubscribeUsesGeoRelaysAndAppendsNotes() { + @Test func subscribeUsesGeoRelaysAndAppendsNotes() { var relaysCaptured: [String] = [] var storedHandler: ((NostrEvent) -> Void)? var storedEOSE: (() -> Void)? let deps = LocationNotesDependencies( relayLookup: { _, _ in ["wss://relay.one"] }, subscribe: { filter, id, relays, handler, eose in - XCTAssertEqual(filter.kinds, [1]) - XCTAssertFalse(id.isEmpty) + #expect(filter.kinds == [1]) + #expect(!id.isEmpty) relaysCaptured = relays storedHandler = handler storedEOSE = eose @@ -67,8 +68,8 @@ final class LocationNotesManagerTests: XCTestCase { ) let manager = LocationNotesManager(geohash: "u4pruydq", dependencies: deps) - XCTAssertEqual(relaysCaptured, ["wss://relay.one"]) - XCTAssertEqual(manager.state, .loading) + #expect(relaysCaptured == ["wss://relay.one"]) + #expect(manager.state == .loading) var event = NostrEvent( pubkey: "pub", @@ -81,9 +82,9 @@ final class LocationNotesManagerTests: XCTestCase { storedHandler?(event) storedEOSE?() - XCTAssertEqual(manager.state, .ready) - XCTAssertEqual(manager.notes.count, 1) - XCTAssertEqual(manager.notes.first?.content, "hi") + #expect(manager.state == .ready) + #expect(manager.notes.count == 1) + #expect(manager.notes.first?.content == "hi") } private enum TestError: Error { @@ -92,8 +93,8 @@ final class LocationNotesManagerTests: XCTestCase { } @MainActor -final class LocationNotesCounterTests: XCTestCase { - func testSubscribeWithoutRelaysMarksUnavailable() { +struct LocationNotesCounterTests { + @Test func subscribeWithoutRelaysMarksUnavailable() { var subscribeCalled = false let deps = LocationNotesCounterDependencies( relayLookup: { _, _ in [] }, @@ -104,21 +105,21 @@ final class LocationNotesCounterTests: XCTestCase { let counter = LocationNotesCounter(testDependencies: deps) counter.subscribe(geohash: "u4pruydq") - XCTAssertFalse(subscribeCalled) - XCTAssertFalse(counter.relayAvailable) - XCTAssertTrue(counter.initialLoadComplete) - XCTAssertEqual(counter.count, 0) + #expect(!subscribeCalled) + #expect(!counter.relayAvailable) + #expect(counter.initialLoadComplete) + #expect(counter.count == 0) } - func testSubscribeCountsUniqueNotes() { + @Test func subscribeCountsUniqueNotes() { var storedHandler: ((NostrEvent) -> Void)? var storedEOSE: (() -> Void)? let deps = LocationNotesCounterDependencies( relayLookup: { _, _ in ["wss://relay.geo"] }, subscribe: { filter, id, relays, handler, eose in - XCTAssertEqual(relays, ["wss://relay.geo"]) - XCTAssertEqual(filter.kinds, [1]) - XCTAssertFalse(id.isEmpty) + #expect(relays == ["wss://relay.geo"]) + #expect(filter.kinds == [1]) + #expect(!id.isEmpty) storedHandler = handler storedEOSE = eose }, @@ -143,8 +144,8 @@ final class LocationNotesCounterTests: XCTestCase { storedEOSE?() - XCTAssertTrue(counter.relayAvailable) - XCTAssertEqual(counter.count, 1) - XCTAssertTrue(counter.initialLoadComplete) + #expect(counter.relayAvailable) + #expect(counter.count == 1) + #expect(counter.initialLoadComplete) } } diff --git a/bitchatTests/Mocks/MockBLEBus.swift b/bitchatTests/Mocks/MockBLEBus.swift new file mode 100644 index 00000000..8199ec1a --- /dev/null +++ b/bitchatTests/Mocks/MockBLEBus.swift @@ -0,0 +1,57 @@ +// +// MockBLEBus.swift +// bitchatTests +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import Foundation +@testable import bitchat + +final class MockBLEBus { + private var registry: [PeerID: MockBLEService] = [:] + private var adjacency: [PeerID: Set] = [:] + + // Enable automatic flooding for public messages in integration tests only + let autoFloodEnabled: Bool + + init(autoFloodEnabled: Bool = false) { + self.autoFloodEnabled = autoFloodEnabled + } + + func register(_ service: MockBLEService, for peerID: PeerID) { + registry[peerID] = service + if adjacency[peerID] == nil { adjacency[peerID] = [] } + } + + func connect(_ a: PeerID, _ b: PeerID) { + var setA = adjacency[a] ?? [] + setA.insert(b) + adjacency[a] = setA + var setB = adjacency[b] ?? [] + setB.insert(a) + adjacency[b] = setB + } + + func disconnect(_ a: PeerID, _ b: PeerID) { + if var setA = adjacency[a] { setA.remove(b); adjacency[a] = setA } + if var setB = adjacency[b] { setB.remove(a); adjacency[b] = setB } + } + + func neighbors(of peerID: PeerID) -> [MockBLEService] { + let ids = adjacency[peerID] ?? [] + let result = ids.compactMap { registry[$0] } + return result + } + + func isDirectNeighbor(_ a: PeerID, _ b: PeerID) -> Bool { + let res = adjacency[a]?.contains(b) ?? false + return res + } + + func service(for peerID: PeerID) -> MockBLEService? { + let svc = registry[peerID] + return svc + } +} diff --git a/bitchatTests/Mocks/MockBLEService.swift b/bitchatTests/Mocks/MockBLEService.swift index 8f40550f..746ccf84 100644 --- a/bitchatTests/Mocks/MockBLEService.swift +++ b/bitchatTests/Mocks/MockBLEService.swift @@ -26,13 +26,12 @@ import CoreBluetooth /// simulate broadcast propagation across the mesh. E2E tests keep it off and perform explicit /// relays when needed. final class MockBLEService: NSObject { - // Enable automatic flooding for public messages in integration tests only - static var autoFloodEnabled: Bool = false + private let bus: MockBLEBus // MARK: - Properties matching BLEService weak var delegate: BitchatDelegate? - var myPeerID: PeerID = "MOCK1234" + var myPeerID = PeerID(str: "MOCK1234") var myNickname: String = "MockUser" private let mockKeychain = MockKeychain() @@ -60,8 +59,8 @@ final class MockBLEService: NSObject { // MARK: - Initialization - override init() { - super.init() + init(bus: MockBLEBus) { + self.bus = bus } // MARK: - Methods matching BLEService @@ -71,42 +70,15 @@ final class MockBLEService: NSObject { } // MARK: - In-memory test bus (for E2E/Integration) - /// Global per-process bus for deterministic routing in tests. - private static var registry: [PeerID: MockBLEService] = [:] - private static var adjacency: [PeerID: Set] = [:] - - /// Clears global bus state. Call from test `setUp()`. - static func resetTestBus() { - registry.removeAll() - adjacency.removeAll() - } /// Registers this instance on first use. private func registerIfNeeded() { - MockBLEService.registry[myPeerID] = self - if MockBLEService.adjacency[myPeerID] == nil { MockBLEService.adjacency[myPeerID] = [] } + bus.register(self, for: myPeerID) } /// Returns adjacent neighbors based on the current simulated topology. private func neighbors() -> [MockBLEService] { - guard let ids = MockBLEService.adjacency[myPeerID] else { return [] } - return ids.compactMap { MockBLEService.registry[$0] } - } - - /// Adds an undirected edge between two peerIDs. - private static func connectPeers(_ a: PeerID, _ b: PeerID) { - var setA = adjacency[a] ?? [] - setA.insert(b) - adjacency[a] = setA - var setB = adjacency[b] ?? [] - setB.insert(a) - adjacency[b] = setB - } - - /// Removes an undirected edge between two peerIDs. - private static func disconnectPeers(_ a: PeerID, _ b: PeerID) { - if var setA = adjacency[a] { setA.remove(b); adjacency[a] = setA } - if var setB = adjacency[b] { setB.remove(a); adjacency[b] = setB } + bus.neighbors(of: myPeerID) } func startServices() { @@ -173,7 +145,7 @@ final class MockBLEService: NSObject { // Surface raw packet to tests that intercept/relay/encrypt packetDeliveryHandler?(packet) - // Deliver public messages to adjacent peers via test bus + // Deliver public messages to adjacent peers via bus if recipientID == nil { for neighbor in neighbors() { neighbor.simulateIncomingPacket(packet) @@ -219,20 +191,16 @@ final class MockBLEService: NSObject { packetDeliveryHandler?(packet) // If directly connected to recipient, deliver only to them. - if let neighbors = MockBLEService.adjacency[myPeerID], neighbors.contains(recipientPeerID), - let target = MockBLEService.registry[recipientPeerID] { + if bus.isDirectNeighbor(myPeerID, recipientPeerID), + let target = bus.service(for: recipientPeerID) { target.simulateIncomingPacket(packet) } else { // Not directly connected: deliver to neighbors for relay; also deliver directly if target is known - if let target = MockBLEService.registry[recipientPeerID] { + if let target = bus.service(for: recipientPeerID) { target.simulateIncomingPacket(packet) } - if let neighbors = MockBLEService.adjacency[myPeerID] { - for peer in neighbors where peer != recipientPeerID { - if let neighbor = MockBLEService.registry[peer] { - neighbor.simulateIncomingPacket(packet) - } - } + for neighbor in neighbors() where neighbor.peerID != recipientPeerID { + neighbor.simulateIncomingPacket(packet) } } } @@ -279,14 +247,14 @@ final class MockBLEService: NSObject { func simulateConnectedPeer(_ peerID: PeerID) { registerIfNeeded() - MockBLEService.connectPeers(myPeerID, peerID) + bus.connect(myPeerID, peerID) connectedPeers.insert(peerID) delegate?.didConnectToPeer(peerID) delegate?.didUpdatePeerList(Array(connectedPeers)) } func simulateDisconnectedPeer(_ peerID: PeerID) { - MockBLEService.disconnectPeers(myPeerID, peerID) + bus.disconnect(myPeerID, peerID) connectedPeers.remove(peerID) delegate?.didDisconnectFromPeer(peerID) delegate?.didUpdatePeerList(Array(connectedPeers)) @@ -319,7 +287,7 @@ final class MockBLEService: NSObject { // When enabled, propagate a public broadcast across the entire connected // component regardless of the original TTL to better emulate large-network // broadcast expectations. De-duplication via seenMessageIDs prevents loops. - if MockBLEService.autoFloodEnabled, + if bus.autoFloodEnabled, packet.recipientID == nil, !message.isPrivate { let nextTTL = packet.ttl > 0 ? packet.ttl - 1 : 0 @@ -353,8 +321,8 @@ typealias MockSimplifiedBluetoothService = MockBLEService // MARK: - Helpers extension MockBLEService { - convenience init(peerID: PeerID, nickname: String) { - self.init() + convenience init(peerID: PeerID, nickname: String, bus: MockBLEBus) { + self.init(bus: bus) myPeerID = peerID mockNickname = nickname } diff --git a/bitchatTests/Noise/NoiseProtocolTests.swift b/bitchatTests/Noise/NoiseProtocolTests.swift index 4a22588b..be4b15a3 100644 --- a/bitchatTests/Noise/NoiseProtocolTests.swift +++ b/bitchatTests/Noise/NoiseProtocolTests.swift @@ -6,135 +6,123 @@ // For more information, see // -import XCTest +import Testing import CryptoKit +import Foundation @testable import bitchat -final class NoiseProtocolTests: XCTestCase { +struct NoiseProtocolTests { - var aliceKey: Curve25519.KeyAgreement.PrivateKey! - var bobKey: Curve25519.KeyAgreement.PrivateKey! - var aliceSession: NoiseSession! - var bobSession: NoiseSession! - private var mockKeychain: MockKeychain! + private let aliceKey = Curve25519.KeyAgreement.PrivateKey() + private let bobKey = Curve25519.KeyAgreement.PrivateKey() + private let mockKeychain = MockKeychain() - override func setUp() { - super.setUp() - aliceKey = Curve25519.KeyAgreement.PrivateKey() - bobKey = Curve25519.KeyAgreement.PrivateKey() - mockKeychain = MockKeychain() - } + private let alicePeerID = PeerID(str: UUID().uuidString) + private let bobPeerID = PeerID(str: UUID().uuidString) - override func tearDown() { - aliceSession = nil - bobSession = nil - mockKeychain = nil - super.tearDown() - } + private let aliceSession: NoiseSession + private let bobSession: NoiseSession - // MARK: - Basic Handshake Tests - - func testXXPatternHandshake() throws { - // Create sessions + init() { aliceSession = NoiseSession( - peerID: TestConstants.testPeerID2, + peerID: alicePeerID, role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey ) bobSession = NoiseSession( - peerID: TestConstants.testPeerID1, + peerID: bobPeerID, role: .responder, keychain: mockKeychain, localStaticKey: bobKey ) - + } + + // MARK: - Basic Handshake Tests + + @Test func xxPatternHandshake() throws { // Alice starts handshake (message 1) let message1 = try aliceSession.startHandshake() - XCTAssertFalse(message1.isEmpty) - XCTAssertEqual(aliceSession.getState(), .handshaking) + #expect(!message1.isEmpty) + #expect(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) + #expect(message2 != nil) + #expect(!message2!.isEmpty) + #expect(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) + #expect(message3 != nil) + #expect(!message3!.isEmpty) + #expect(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) + #expect(finalMessage == nil) // No more messages needed + #expect(bobSession.getState() == .established) // Verify both sessions are established - XCTAssertTrue(aliceSession.isEstablished()) - XCTAssertTrue(bobSession.isEstablished()) + #expect(aliceSession.isEstablished()) + #expect(bobSession.isEstablished()) // Verify they have each other's static keys - XCTAssertEqual(aliceSession.getRemoteStaticPublicKey()?.rawRepresentation, bobKey.publicKey.rawRepresentation) - XCTAssertEqual(bobSession.getRemoteStaticPublicKey()?.rawRepresentation, aliceKey.publicKey.rawRepresentation) + #expect(aliceSession.getRemoteStaticPublicKey()?.rawRepresentation == bobKey.publicKey.rawRepresentation) + #expect(bobSession.getRemoteStaticPublicKey()?.rawRepresentation == aliceKey.publicKey.rawRepresentation) } - func testHandshakeStateValidation() throws { - aliceSession = NoiseSession( - peerID: TestConstants.testPeerID2, - role: .initiator, - keychain: mockKeychain, - localStaticKey: aliceKey - ) - + @Test func handshakeStateValidation() throws { // Cannot process message before starting handshake - XCTAssertThrowsError(try aliceSession.processHandshakeMessage(Data())) + #expect(throws: NoiseSessionError.invalidState) { + try aliceSession.processHandshakeMessage(Data()) + } // Start handshake _ = try aliceSession.startHandshake() // Cannot start handshake twice - XCTAssertThrowsError(try aliceSession.startHandshake()) + #expect(throws: NoiseSessionError.invalidState) { + try aliceSession.startHandshake() + } } // MARK: - Encryption/Decryption Tests - func testBasicEncryptionDecryption() throws { - // Establish sessions - try establishSessions() + @Test func basicEncryptionDecryption() throws { + try performHandshake(initiator: aliceSession, responder: bobSession) 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 + #expect(ciphertext != plaintext) + #expect(ciphertext.count > plaintext.count) // Should have overhead // Bob decrypts let decrypted = try bobSession.decrypt(ciphertext) - XCTAssertEqual(decrypted, plaintext) + #expect(decrypted == plaintext) } - func testBidirectionalEncryption() throws { - try establishSessions() + @Test func bidirectionalEncryption() throws { + try performHandshake(initiator: aliceSession, responder: bobSession) // 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) + #expect(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) + #expect(aliceReceived == bobMessage) } - func testLargeMessageEncryption() throws { - try establishSessions() + @Test func largeMessageEncryption() throws { + try performHandshake(initiator: aliceSession, responder: bobSession) // Create a large message let largeMessage = TestHelpers.generateRandomData(length: 100_000) @@ -143,81 +131,78 @@ final class NoiseProtocolTests: XCTestCase { let ciphertext = try aliceSession.encrypt(largeMessage) let decrypted = try bobSession.decrypt(ciphertext) - XCTAssertEqual(decrypted, largeMessage) + #expect(decrypted == largeMessage) } - func testEncryptionBeforeHandshake() { - aliceSession = NoiseSession( - peerID: TestConstants.testPeerID2, - role: .initiator, - keychain: mockKeychain, - localStaticKey: aliceKey - ) - + @Test func encryptionBeforeHandshake() { let plaintext = "test".data(using: .utf8)! - // Should throw when not established - XCTAssertThrowsError(try aliceSession.encrypt(plaintext)) - XCTAssertThrowsError(try aliceSession.decrypt(plaintext)) + #expect(throws: NoiseSessionError.notEstablished) { + try aliceSession.encrypt(plaintext) + } + + #expect(throws: NoiseSessionError.notEstablished) { + try aliceSession.decrypt(plaintext) + } } // MARK: - Session Manager Tests - func testSessionManagerBasicOperations() throws { + @Test func sessionManagerBasicOperations() throws { let manager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) - - XCTAssertNil(manager.getSession(for: TestConstants.testPeerID2)) - - _ = try manager.initiateHandshake(with: TestConstants.testPeerID2) - XCTAssertNotNil(manager.getSession(for: TestConstants.testPeerID2)) - + + #expect(manager.getSession(for: alicePeerID) == nil) + + _ = try manager.initiateHandshake(with: alicePeerID) + #expect(manager.getSession(for: alicePeerID) != nil) + // Get session - let retrieved = manager.getSession(for: TestConstants.testPeerID2) - XCTAssertNotNil(retrieved) - + let retrieved = manager.getSession(for: alicePeerID) + #expect(retrieved != nil) + // Remove session - manager.removeSession(for: TestConstants.testPeerID2) - XCTAssertNil(manager.getSession(for: TestConstants.testPeerID2)) + manager.removeSession(for: alicePeerID) + #expect(manager.getSession(for: alicePeerID) == nil) } - func testSessionManagerHandshakeInitiation() throws { + @Test func sessionManagerHandshakeInitiation() throws { let manager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) // Initiate handshake - let handshakeData = try manager.initiateHandshake(with: TestConstants.testPeerID2) - XCTAssertFalse(handshakeData.isEmpty) + let handshakeData = try manager.initiateHandshake(with: alicePeerID) + #expect(!handshakeData.isEmpty) // Session should exist - let session = manager.getSession(for: TestConstants.testPeerID2) - XCTAssertNotNil(session) - XCTAssertEqual(session?.getState(), .handshaking) + let session = manager.getSession(for: alicePeerID) + #expect(session != nil) + #expect(session?.getState() == .handshaking) } - func testSessionManagerIncomingHandshake() throws { + @Test func sessionManagerIncomingHandshake() throws { let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) // Alice initiates - let message1 = try aliceManager.initiateHandshake(with: TestConstants.testPeerID2) + let message1 = try aliceManager.initiateHandshake(with: alicePeerID) // Bob responds - let message2 = try bobManager.handleIncomingHandshake(from: TestConstants.testPeerID1, message: message1) - XCTAssertNotNil(message2) + let message2 = try bobManager.handleIncomingHandshake(from: bobPeerID, message: message1) + #expect(message2 != nil) // Continue handshake - let message3 = try aliceManager.handleIncomingHandshake(from: TestConstants.testPeerID2, message: message2!) - XCTAssertNotNil(message3) + let message3 = try aliceManager.handleIncomingHandshake(from: alicePeerID, message: message2!) + #expect(message3 != nil) // Complete handshake - let finalMessage = try bobManager.handleIncomingHandshake(from: TestConstants.testPeerID1, message: message3!) - XCTAssertNil(finalMessage) + let finalMessage = try bobManager.handleIncomingHandshake(from: bobPeerID, message: message3!) + #expect(finalMessage == nil) // Both should have established sessions - XCTAssertTrue(aliceManager.getSession(for: TestConstants.testPeerID2)?.isEstablished() ?? false) - XCTAssertTrue(bobManager.getSession(for: TestConstants.testPeerID1)?.isEstablished() ?? false) + #expect(aliceManager.getSession(for: alicePeerID)?.isEstablished() == true) + #expect(bobManager.getSession(for: bobPeerID)?.isEstablished() == true) } - func testSessionManagerEncryptionDecryption() throws { + @Test func sessionManagerEncryptionDecryption() throws { let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) @@ -226,17 +211,17 @@ final class NoiseProtocolTests: XCTestCase { // Encrypt with manager let plaintext = "Test message".data(using: .utf8)! - let ciphertext = try aliceManager.encrypt(plaintext, for: TestConstants.testPeerID2) + let ciphertext = try aliceManager.encrypt(plaintext, for: alicePeerID) // Decrypt with manager - let decrypted = try bobManager.decrypt(ciphertext, from: TestConstants.testPeerID1) - XCTAssertEqual(decrypted, plaintext) + let decrypted = try bobManager.decrypt(ciphertext, from: bobPeerID) + #expect(decrypted == plaintext) } // MARK: - Security Tests - func testTamperedCiphertextDetection() throws { - try establishSessions() + @Test func tamperedCiphertextDetection() throws { + try performHandshake(initiator: aliceSession, responder: bobSession) let plaintext = "Secret message".data(using: .utf8)! var ciphertext = try aliceSession.encrypt(plaintext) @@ -245,11 +230,19 @@ final class NoiseProtocolTests: XCTestCase { ciphertext[ciphertext.count / 2] ^= 0xFF // Decryption should fail - XCTAssertThrowsError(try bobSession.decrypt(ciphertext)) + if #available(macOS 14.4, iOS 17.4, *) { + #expect(throws: CryptoKitError.authenticationFailure) { + try bobSession.decrypt(ciphertext) + } + } else { + #expect(throws: (any Error).self) { + try bobSession.decrypt(ciphertext) + } + } } - func testReplayPrevention() throws { - try establishSessions() + @Test func replayPrevention() throws { + try performHandshake(initiator: aliceSession, responder: bobSession) let plaintext = "Test message".data(using: .utf8)! let ciphertext = try aliceSession.encrypt(plaintext) @@ -258,16 +251,18 @@ final class NoiseProtocolTests: XCTestCase { _ = try bobSession.decrypt(ciphertext) // Replaying the same ciphertext should fail - XCTAssertThrowsError(try bobSession.decrypt(ciphertext)) + #expect(throws: NoiseError.replayDetected) { + try bobSession.decrypt(ciphertext) + } } - func testSessionIsolation() throws { + @Test func sessionIsolation() throws { // Create two separate session pairs - let aliceSession1 = NoiseSession(peerID: "peer1", role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey) - let bobSession1 = NoiseSession(peerID: "alice1", role: .responder, keychain: mockKeychain, localStaticKey: bobKey) + let aliceSession1 = NoiseSession(peerID: PeerID(str: "peer1"), role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey) + let bobSession1 = NoiseSession(peerID: PeerID(str: "alice1"), role: .responder, keychain: mockKeychain, localStaticKey: bobKey) - let aliceSession2 = NoiseSession(peerID: "peer2", role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey) - let bobSession2 = NoiseSession(peerID: "alice2", role: .responder, keychain: mockKeychain, localStaticKey: bobKey) + let aliceSession2 = NoiseSession(peerID: PeerID(str: "peer2"), role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey) + let bobSession2 = NoiseSession(peerID: PeerID(str: "alice2"), role: .responder, keychain: mockKeychain, localStaticKey: bobKey) // Establish both pairs try performHandshake(initiator: aliceSession1, responder: bobSession1) @@ -278,16 +273,24 @@ final class NoiseProtocolTests: XCTestCase { let ciphertext1 = try aliceSession1.encrypt(plaintext) // Should not be able to decrypt with session 2 - XCTAssertThrowsError(try bobSession2.decrypt(ciphertext1)) + if #available(macOS 14.4, iOS 17.4, *) { + #expect(throws: CryptoKitError.authenticationFailure) { + try bobSession2.decrypt(ciphertext1) + } + } else { + #expect(throws: (any Error).self) { + try bobSession2.decrypt(ciphertext1) + } + } // But should work with correct session let decrypted = try bobSession1.decrypt(ciphertext1) - XCTAssertEqual(decrypted, plaintext) + #expect(decrypted == plaintext) } // MARK: - Session Recovery Tests - func testPeerRestartDetection() throws { + @Test func peerRestartDetection() throws { // Establish initial sessions let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) @@ -295,38 +298,38 @@ final class NoiseProtocolTests: XCTestCase { try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager) // Exchange some messages to establish nonce state - let message1 = try aliceManager.encrypt("Hello".data(using: .utf8)!, for: TestConstants.testPeerID2) - _ = try bobManager.decrypt(message1, from: TestConstants.testPeerID1) + let message1 = try aliceManager.encrypt("Hello".data(using: .utf8)!, for: alicePeerID) + _ = try bobManager.decrypt(message1, from: bobPeerID) - let message2 = try bobManager.encrypt("World".data(using: .utf8)!, for: TestConstants.testPeerID1) - _ = try aliceManager.decrypt(message2, from: TestConstants.testPeerID2) + let message2 = try bobManager.encrypt("World".data(using: .utf8)!, for: bobPeerID) + _ = try aliceManager.decrypt(message2, from: alicePeerID) // Simulate Bob restart by creating new manager with same key let bobManagerRestarted = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) // Bob initiates new handshake after restart - let newHandshake1 = try bobManagerRestarted.initiateHandshake(with: TestConstants.testPeerID1) + let newHandshake1 = try bobManagerRestarted.initiateHandshake(with: bobPeerID) // Alice should accept the new handshake (clearing old session) - let newHandshake2 = try aliceManager.handleIncomingHandshake(from: TestConstants.testPeerID2, message: newHandshake1) - XCTAssertNotNil(newHandshake2) + let newHandshake2 = try aliceManager.handleIncomingHandshake(from: alicePeerID, message: newHandshake1) + #expect(newHandshake2 != nil) // Complete the new handshake - let newHandshake3 = try bobManagerRestarted.handleIncomingHandshake(from: TestConstants.testPeerID1, message: newHandshake2!) - XCTAssertNotNil(newHandshake3) - _ = try aliceManager.handleIncomingHandshake(from: TestConstants.testPeerID2, message: newHandshake3!) + let newHandshake3 = try bobManagerRestarted.handleIncomingHandshake(from: bobPeerID, message: newHandshake2!) + #expect(newHandshake3 != nil) + _ = try aliceManager.handleIncomingHandshake(from: alicePeerID, message: newHandshake3!) // Should be able to exchange messages with new sessions let testMessage = "After restart".data(using: .utf8)! - let encrypted = try bobManagerRestarted.encrypt(testMessage, for: TestConstants.testPeerID1) - let decrypted = try aliceManager.decrypt(encrypted, from: TestConstants.testPeerID2) - XCTAssertEqual(decrypted, testMessage) + let encrypted = try bobManagerRestarted.encrypt(testMessage, for: bobPeerID) + let decrypted = try aliceManager.decrypt(encrypted, from: alicePeerID) + #expect(decrypted == testMessage) } - func testNonceDesynchronizationRecovery() throws { + @Test func nonceDesynchronizationRecovery() throws { // Create two sessions - aliceSession = NoiseSession(peerID: TestConstants.testPeerID2, role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey) - bobSession = NoiseSession(peerID: TestConstants.testPeerID1, role: .responder, keychain: mockKeychain, localStaticKey: bobKey) + let aliceSession = NoiseSession(peerID: alicePeerID, role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey) + let bobSession = NoiseSession(peerID: bobPeerID, role: .responder, keychain: mockKeychain, localStaticKey: bobKey) // Establish sessions try performHandshake(initiator: aliceSession, responder: bobSession) @@ -344,10 +347,12 @@ final class NoiseProtocolTests: XCTestCase { // With per-packet nonce carried, decryption should not throw here let desyncMessage = try aliceSession.encrypt("This now succeeds".data(using: .utf8)!) - XCTAssertNoThrow(try bobSession.decrypt(desyncMessage)) + #expect(throws: Never.self) { + try bobSession.decrypt(desyncMessage) + } } - func testConcurrentEncryption() throws { + @Test func concurrentEncryption() async throws { // Test thread safety of encryption operations let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) @@ -355,37 +360,35 @@ final class NoiseProtocolTests: XCTestCase { try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager) let messageCount = 100 - let expectation = XCTestExpectation(description: "All messages encrypted and decrypted") - expectation.expectedFulfillmentCount = messageCount - - var encryptedMessages: [Int: Data] = [:] - // Encrypt messages sequentially to avoid nonce races in manager - for i in 0.. BitchatPacket { let sender = Data([0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77]) return BitchatPacket( @@ -15,60 +16,51 @@ final class NotificationStreamAssemblerTests: XCTestCase { ) } - func testAssemblesSingleFrameAcrossChunks() { + @Test func assemblesSingleFrameAcrossChunks() throws { var assembler = NotificationStreamAssembler() let packet = makePacket() - guard let frame = packet.toBinaryData(padding: false) else { - return XCTFail("Failed to encode packet") - } - XCTAssertNotNil(BinaryProtocol.decode(frame)) + let frame = try #require(packet.toBinaryData(padding: false), "Failed to encode packet") + + #expect(BinaryProtocol.decode(frame) != nil) let payloadLen = (Int(frame[12]) << 8) | Int(frame[13]) - XCTAssertEqual(payloadLen, packet.payload.count) + #expect(payloadLen == packet.payload.count) let splitIndex = min(20, max(1, frame.count / 2)) let first = frame.prefix(splitIndex) let second = frame.suffix(from: splitIndex) - XCTAssertEqual(first.count + second.count, frame.count) + #expect(first.count + second.count == frame.count) var result = assembler.append(first) - XCTAssertTrue(result.frames.isEmpty) - XCTAssertTrue(result.droppedPrefixes.isEmpty) - XCTAssertFalse(result.reset) + #expect(result.frames.isEmpty) + #expect(result.droppedPrefixes.isEmpty) + #expect(!result.reset) result = assembler.append(second) - XCTAssertEqual(result.frames.count, 1) - XCTAssertTrue(result.droppedPrefixes.isEmpty) - XCTAssertFalse(result.reset) + #expect(result.frames.count == 1) + #expect(result.droppedPrefixes.isEmpty) + #expect(!result.reset) - guard let frameData = result.frames.first else { - return XCTFail("Missing frame data") - } - if frameData.count != frame.count { - XCTFail("Frame size mismatch: expected \(frame.count) got \(frameData.count)\nframe=\(Array(frame))\nassembled=\(Array(frameData))") - return - } - guard let decoded = BinaryProtocol.decode(frameData) else { - return XCTFail("Failed to decode frame") - } - XCTAssertEqual(decoded.type, packet.type) - XCTAssertEqual(decoded.payload, packet.payload) - XCTAssertEqual(decoded.senderID, packet.senderID) - XCTAssertEqual(decoded.timestamp, packet.timestamp) + let frameData = try #require(result.frames.first, "Missing frame data") + #expect(frameData.count == frame.count) + + let decoded = try #require(BinaryProtocol.decode(frameData), "Failed to decode frame") + #expect(decoded.type == packet.type) + #expect(decoded.payload == packet.payload) + #expect(decoded.senderID == packet.senderID) + #expect(decoded.timestamp == packet.timestamp) var directAssembler = NotificationStreamAssembler() let directResult = directAssembler.append(frame) - XCTAssertEqual(directResult.frames.first?.count, frame.count) + #expect(directResult.frames.first?.count == frame.count) } - func testAssemblesMultipleFramesSequentially() { + @Test func assemblesMultipleFramesSequentially() throws { var assembler = NotificationStreamAssembler() let packet1 = makePacket(timestamp: 0xABC) let packet2 = makePacket(timestamp: 0xDEF) - guard let frame1 = packet1.toBinaryData(padding: false), - let frame2 = packet2.toBinaryData(padding: false) else { - return XCTFail("Failed to encode packets") - } + let frame1 = try #require(packet1.toBinaryData(padding: false), "Failed to encode packet") + let frame2 = try #require(packet2.toBinaryData(padding: false), "Failed to encode packet") var combined = Data() combined.append(frame1) @@ -77,35 +69,30 @@ final class NotificationStreamAssemblerTests: XCTestCase { let secondChunk = combined.suffix(from: 20) var result = assembler.append(firstChunk) - XCTAssertTrue(result.frames.isEmpty) + #expect(result.frames.isEmpty) result = assembler.append(secondChunk) - XCTAssertEqual(result.frames.count, 2) - guard let decoded1 = BinaryProtocol.decode(result.frames[0]), - let decoded2 = BinaryProtocol.decode(result.frames[1]) else { - return XCTFail("Failed to decode frames") - } - XCTAssertEqual(decoded1.timestamp, packet1.timestamp) - XCTAssertEqual(decoded2.timestamp, packet2.timestamp) + #expect(result.frames.count == 2) + + let decoded1 = try #require(BinaryProtocol.decode(result.frames[0]), "Failed to decode frame") + let decoded2 = try #require(BinaryProtocol.decode(result.frames[1]), "Failed to decode frame") + #expect(decoded1.timestamp == packet1.timestamp) + #expect(decoded2.timestamp == packet2.timestamp) } - func testDropsInvalidPrefixByte() { + @Test func dropsInvalidPrefixByte() throws { var assembler = NotificationStreamAssembler() let packet = makePacket(timestamp: 0xF00) - guard let frame = packet.toBinaryData(padding: false) else { - return XCTFail("Failed to encode packet") - } + let frame = try #require(packet.toBinaryData(padding: false), "Failed to encode packet") var noisyFrame = Data([0x00]) noisyFrame.append(frame) let result = assembler.append(noisyFrame) - XCTAssertEqual(result.droppedPrefixes, [0x00]) - XCTAssertEqual(result.frames.count, 1) - XCTAssertFalse(result.reset) + #expect(result.droppedPrefixes == [0x00]) + #expect(result.frames.count == 1) + #expect(result.reset == false) - guard let decoded = BinaryProtocol.decode(result.frames[0]) else { - return XCTFail("Failed to decode frame after drop") - } - XCTAssertEqual(decoded.timestamp, packet.timestamp) + let decoded = try #require(BinaryProtocol.decode(result.frames[0]), "Failed to decode frame after drop") + #expect(decoded.timestamp == packet.timestamp) } } diff --git a/bitchatTests/Protocol/BinaryProtocolPaddingTests.swift b/bitchatTests/Protocol/BinaryProtocolPaddingTests.swift index 0edabf5d..a5e3f44f 100644 --- a/bitchatTests/Protocol/BinaryProtocolPaddingTests.swift +++ b/bitchatTests/Protocol/BinaryProtocolPaddingTests.swift @@ -5,30 +5,29 @@ // This is free and unencumbered software released into the public domain. // -import XCTest +import Testing @testable import bitchat -final class BinaryProtocolPaddingTests: XCTestCase { - func test_padded_vs_unpadded_length() throws { +struct BinaryProtocolPaddingTests { + @Test func padded_vs_unpadded_length() throws { // Use helper to create a small test packet let packet = TestHelpers.createTestPacket() - guard let padded = BinaryProtocol.encode(packet, padding: true) else { return XCTFail("encode padded") } - guard let unpadded = BinaryProtocol.encode(packet, padding: false) else { return XCTFail("encode unpadded") } - XCTAssertGreaterThanOrEqual(padded.count, unpadded.count, "Padded frame should be >= unpadded") + let padded = try #require(BinaryProtocol.encode(packet, padding: true), "encode padded") + let unpadded = try #require(BinaryProtocol.encode(packet, padding: false), "encode unpadded") + #expect(padded.count >= unpadded.count, "Padded frame should be >= unpadded") } - func test_decode_padded_and_unpadded_round_trip() throws { + @Test func decode_padded_and_unpadded_round_trip() throws { let packet = TestHelpers.createTestPacket() - // Padded - guard let padded = BinaryProtocol.encode(packet, padding: true) else { return XCTFail("encode padded") } - guard let dec1 = BinaryProtocol.decode(padded) else { return XCTFail("decode padded") } - XCTAssertEqual(dec1.type, packet.type) - XCTAssertEqual(dec1.payload, packet.payload) - // Unpadded - guard let unpadded = BinaryProtocol.encode(packet, padding: false) else { return XCTFail("encode unpadded") } - guard let dec2 = BinaryProtocol.decode(unpadded) else { return XCTFail("decode unpadded") } - XCTAssertEqual(dec2.type, packet.type) - XCTAssertEqual(dec2.payload, packet.payload) + + let padded = try #require(BinaryProtocol.encode(packet, padding: true), "encode padded") + let dec1 = try #require(BinaryProtocol.decode(padded), "decode padded") + #expect(dec1.type == packet.type) + #expect(dec1.payload == packet.payload) + + let unpadded = try #require(BinaryProtocol.encode(packet, padding: false), "encode unpadded") + let dec2 = try #require(BinaryProtocol.decode(unpadded), "decode unpadded") + #expect(dec2.type == packet.type) + #expect(dec2.payload == packet.payload) } } - diff --git a/bitchatTests/Protocol/BinaryProtocolTests.swift b/bitchatTests/Protocol/BinaryProtocolTests.swift index eef9cbc2..0c7f5751 100644 --- a/bitchatTests/Protocol/BinaryProtocolTests.swift +++ b/bitchatTests/Protocol/BinaryProtocolTests.swift @@ -6,119 +6,89 @@ // For more information, see // -import XCTest +import Testing +import Foundation @testable import bitchat -final class BinaryProtocolTests: XCTestCase { +struct BinaryProtocolTests { // MARK: - Basic Encoding/Decoding Tests - func testBasicPacketEncodingDecoding() throws { + @Test func basicPacketEncodingDecoding() 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 - } + let encodedData = try #require(BinaryProtocol.encode(originalPacket), "Failed to encode packet") + let decodedPacket = try #require(BinaryProtocol.decode(encodedData), "Failed to decode packet") // Verify - XCTAssertEqual(decodedPacket.type, originalPacket.type) - XCTAssertEqual(decodedPacket.ttl, originalPacket.ttl) - XCTAssertEqual(decodedPacket.timestamp, originalPacket.timestamp) - XCTAssertEqual(decodedPacket.payload, originalPacket.payload) + #expect(decodedPacket.type == originalPacket.type) + #expect(decodedPacket.ttl == originalPacket.ttl) + #expect(decodedPacket.timestamp == originalPacket.timestamp) + #expect(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) + #expect(decodedSenderID == originalSenderID) } - func testPacketWithRecipient() throws { - let recipientID = TestConstants.testPeerID2 + @Test func packetWithRecipient() throws { + let recipientID = PeerID(str: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789") 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 - } + let encodedData = try #require(BinaryProtocol.encode(packet), "Failed to encode packet with recipient") + let decodedPacket = try #require(BinaryProtocol.decode(encodedData), "Failed to decode packet with recipient") // Verify recipient - XCTAssertNotNil(decodedPacket.recipientID) + #expect(decodedPacket.recipientID != nil) let decodedRecipientID = decodedPacket.recipientID?.trimmingNullBytes() - XCTAssertTrue(String(data: decodedRecipientID!, encoding: .utf8) == recipientID) + // TODO: Check if this is intended that the decoding only gets the first 8 + #expect(String(data: decodedRecipientID!, encoding: .utf8) == "abcdef01") } - func testPacketWithSignature() throws { - let packet = TestHelpers.createTestPacket( - 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 - } + @Test func packetWithSignature() throws { + let packet = TestHelpers.createTestPacket(signature: TestConstants.testSignature) + let encodedData = try #require(BinaryProtocol.encode(packet), "Failed to encode packet with signature") + let decodedPacket = try #require(BinaryProtocol.decode(encodedData), "Failed to decode packet with signature") // Verify signature - XCTAssertNotNil(decodedPacket.signature) - XCTAssertEqual(decodedPacket.signature, TestConstants.testSignature) + #expect(decodedPacket.signature != nil) + #expect(decodedPacket.signature == TestConstants.testSignature) } // MARK: - Compression Tests - func testPayloadCompression() throws { - // Create a large, compressible payload above current threshold (2048B) + @Test("Create a large, compressible payload above current threshold (2048B)") + func payloadCompression() throws { let repeatedString = String(repeating: "This is a test message. ", count: 200) 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 - } + let encodedData = try #require(BinaryProtocol.encode(packet), "Failed to encode packet with large payload") // The encoded size should be smaller than uncompressed due to compression let uncompressedSize = BinaryProtocol.headerSize + BinaryProtocol.senderIDSize + largePayload.count - XCTAssertLessThan(encodedData.count, uncompressedSize) + #expect(encodedData.count < uncompressedSize) // Decode and verify - guard let decodedPacket = BinaryProtocol.decode(encodedData) else { - XCTFail("Failed to decode compressed packet") - return - } + let decodedPacket = try #require(BinaryProtocol.decode(encodedData), "Failed to decode compressed packet") - XCTAssertEqual(decodedPacket.payload, largePayload) + #expect(decodedPacket.payload == largePayload) } - func testSmallPayloadNoCompression() throws { - // Small payloads should not be compressed + @Test("Small payloads should not be compressed") + func smallPayloadNoCompression() throws { 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) + let encodedData = try #require(BinaryProtocol.encode(packet), "Failed to encode small packet") + let decodedPacket = try #require(BinaryProtocol.decode(encodedData), "Failed to decode small packet") + #expect(decodedPacket.payload == smallPayload) } // MARK: - Message Padding Tests - func testMessagePadding() throws { + @Test func messagePadding() throws { let payloads = [ "Short", String(repeating: "Medium length message content ", count: 10), // ~300 bytes @@ -130,43 +100,32 @@ final class BinaryProtocolTests: XCTestCase { 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 - } + let encodedData = try #require(BinaryProtocol.encode(packet), "Failed to encode packet") // Verify padding creates standard block sizes up to configured limit (no 4096 bucket currently) let blockSizes = [256, 512, 1024, 2048] if encodedData.count <= 2048 { - XCTAssertTrue(blockSizes.contains(encodedData.count), "Encoded size \(encodedData.count) is not a standard block size") + #expect(blockSizes.contains(encodedData.count), "Encoded size \(encodedData.count) is not a standard block size") } else { // For very large payloads we expect no additional padding beyond raw size - XCTAssertGreaterThan(encodedData.count, 2048) + #expect(encodedData.count > 2048) } 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) + let decodedPacket = try #require(BinaryProtocol.decode(encodedData), "Failed to decode padded packet") + #expect(String(data: decodedPacket.payload, encoding: .utf8) == payload) } // Different payload sizes (within <=2048) may map to the same bucket depending on compression. // Require at least one padded size to be present. - XCTAssertGreaterThanOrEqual(encodedSizes.filter { $0 <= 2048 }.count, 1, "Expected at least one padded size up to 2048, got \(encodedSizes)") + #expect(encodedSizes.filter { $0 <= 2048 }.count >= 1, "Expected at least one padded size up to 2048, got \(encodedSizes)") } - func testInvalidPKCS7PaddingIsRejected() throws { + @Test func invalidPKCS7PaddingIsRejected() throws { let pkt = TestHelpers.createTestPacket(payload: Data(repeating: 0x41, count: 50)) // small - guard let enc0 = BinaryProtocol.encode(pkt) else { - XCTFail("encode failed") - return - } + let enc0 = try #require(BinaryProtocol.encode(pkt), "encode failed") // Force padding to known block for test stability var enc = MessagePadding.pad(enc0, toSize: 256) let unpadded = MessagePadding.unpad(enc) @@ -177,39 +136,33 @@ final class BinaryProtocolTests: XCTestCase { let maybe = BinaryProtocol.decode(enc) // If decode still succeeds (nested pad edge case), at least ensure payload integrity if let pkt2 = maybe { - XCTAssertEqual(pkt2.payload, pkt.payload) + #expect(pkt2.payload == pkt.payload) } else { - XCTAssertNil(maybe) + #expect(maybe == nil) } } else { // If no padding was applied, just assert decode succeeds (nothing to test) - XCTAssertNotNil(BinaryProtocol.decode(enc)) + #expect(BinaryProtocol.decode(enc) != nil) } } // MARK: - Message Encoding/Decoding Tests - func testMessageEncodingDecoding() throws { + @Test func messageEncodingDecoding() throws { let message = TestHelpers.createTestMessage() - guard let payload = message.toBinaryPayload() else { - XCTFail("Failed to encode message to binary") - return - } + let payload = try #require(message.toBinaryPayload(), "Failed to encode message to binary") - guard let decodedMessage = BitchatMessage(payload) else { - XCTFail("Failed to decode message from binary") - return - } + let decodedMessage = try #require(BitchatMessage(payload), "Failed to decode message from binary") - XCTAssertEqual(decodedMessage.content, message.content) - XCTAssertEqual(decodedMessage.sender, message.sender) - XCTAssertEqual(decodedMessage.senderPeerID, message.senderPeerID) - XCTAssertEqual(decodedMessage.isPrivate, message.isPrivate) + #expect(decodedMessage.content == message.content) + #expect(decodedMessage.sender == message.sender) + #expect(decodedMessage.senderPeerID == message.senderPeerID) + #expect(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) + #expect(timeDiff < 1) } func testPrivateMessageEncoding() throws { @@ -218,30 +171,22 @@ final class BinaryProtocolTests: XCTestCase { recipientNickname: TestConstants.testNickname2 ) - guard let payload = message.toBinaryPayload(), - let decodedMessage = BitchatMessage(payload) else { - XCTFail("Failed to encode/decode private message") - return - } + let payload = try #require(message.toBinaryPayload(), "Failed to encode private message") + let decodedMessage = try #require(BitchatMessage(payload), "Failed to decode private message") - XCTAssertTrue(decodedMessage.isPrivate) - XCTAssertEqual(decodedMessage.recipientNickname, TestConstants.testNickname2) + #expect(decodedMessage.isPrivate) + #expect(decodedMessage.recipientNickname == TestConstants.testNickname2) } - func testMessageWithMentions() throws { + @Test func messageWithMentions() throws { let mentions = [TestConstants.testNickname2, TestConstants.testNickname3] let message = TestHelpers.createTestMessage(mentions: mentions) - - guard let payload = message.toBinaryPayload(), - let decodedMessage = BitchatMessage(payload) else { - XCTFail("Failed to encode/decode message with mentions") - return - } - - XCTAssertEqual(decodedMessage.mentions, mentions) + let payload = try #require(message.toBinaryPayload(), "Failed to encode message with mentions") + let decodedMessage = try #require(BitchatMessage(payload), "Failed to decode message with mentions") + #expect(decodedMessage.mentions == mentions) } - func testRelayMessageEncoding() throws { + @Test func relayMessageEncoding() throws { let message = BitchatMessage( id: UUID().uuidString, sender: TestConstants.testNickname1, @@ -251,105 +196,77 @@ final class BinaryProtocolTests: XCTestCase { originalSender: TestConstants.testNickname3, isPrivate: false, recipientNickname: nil, - senderPeerID: TestConstants.testPeerID1, mentions: nil ) - - guard let payload = message.toBinaryPayload(), - let decodedMessage = BitchatMessage(payload) else { - XCTFail("Failed to encode/decode relay message") - return - } - - XCTAssertTrue(decodedMessage.isRelay) - XCTAssertEqual(decodedMessage.originalSender, TestConstants.testNickname3) + let payload = try #require(message.toBinaryPayload(), "Failed to encode relay message") + let decodedMessage = try #require(BitchatMessage(payload), "Failed to decode relay message") + #expect(decodedMessage.isRelay) + #expect(decodedMessage.originalSender == TestConstants.testNickname3) } // MARK: - Edge Cases and Error Handling - func testInvalidDataDecoding() { - // Too small data + @Test("Too small data") + func invalidDataDecoding() throws { let tooSmall = Data(repeating: 0, count: 5) - XCTAssertNil(BinaryProtocol.decode(tooSmall)) + #expect(BinaryProtocol.decode(tooSmall) == nil) // Random data let random = TestHelpers.generateRandomData(length: 100) - XCTAssertNil(BinaryProtocol.decode(random)) + #expect(BinaryProtocol.decode(random) == nil) // Corrupted header let packet = TestHelpers.createTestPacket() - guard var encoded = BinaryProtocol.encode(packet) else { - XCTFail("Failed to encode test packet") - return - } + var encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode test packet") // Corrupt the version byte encoded[0] = 0xFF - XCTAssertNil(BinaryProtocol.decode(encoded)) + #expect(BinaryProtocol.decode(encoded) == nil) } - func testLargeMessageHandling() throws { - // Test maximum size handling + @Test("Test maximum size handling") + func largeMessageHandling() throws { let largeContent = String(repeating: "X", count: 65535) // Max uint16 let message = TestHelpers.createTestMessage(content: largeContent) - - guard let payload = message.toBinaryPayload(), - let decodedMessage = BitchatMessage(payload) else { - XCTFail("Failed to handle large message") - return - } - - XCTAssertEqual(decodedMessage.content, largeContent) + let payload = try #require(message.toBinaryPayload(), "Failed to handle large message") + let decodedMessage = try #require(BitchatMessage(payload), "Failed to handle large message") + #expect(decodedMessage.content == largeContent) } - func testEmptyFieldsHandling() throws { - // Test message with empty content + @Test("Test message with empty content") + func emptyFieldsHandling() throws { let emptyMessage = TestHelpers.createTestMessage(content: "") - - guard let payload = emptyMessage.toBinaryPayload(), - let decodedMessage = BitchatMessage(payload) else { - XCTFail("Failed to handle empty message") - return - } - - XCTAssertEqual(decodedMessage.content, "") + let payload = try #require(emptyMessage.toBinaryPayload(), "Failed to handle empty message") + let decodedMessage = try #require(BitchatMessage(payload), "Failed to handle empty message") + #expect(decodedMessage.content.isEmpty) } // MARK: - Protocol Version Tests - func testProtocolVersionHandling() throws { - // Test with supported version (version is always 1 in init) + @Test("Test with supported version (version is always 1 in init)") + func protocolVersionHandling() throws { let packet = TestHelpers.createTestPacket() - - 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) + let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode packet with version") + let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode packet with version") + #expect(decoded.version == 1) } - func testUnsupportedProtocolVersion() throws { - // Create packet data with unsupported version + @Test("Create packet data with unsupported version") + func unsupportedProtocolVersion() throws { let packet = TestHelpers.createTestPacket() - - guard var encoded = BinaryProtocol.encode(packet) else { - XCTFail("Failed to encode packet") - return - } + var encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode packet") // Manually change version byte to unsupported value encoded[0] = 99 // Unsupported version // Should fail to decode - XCTAssertNil(BinaryProtocol.decode(encoded)) + #expect(BinaryProtocol.decode(encoded) == nil) } // MARK: - Bounds Checking Tests (Crash Prevention) - func testMalformedPacketWithInvalidPayloadLength() throws { - // Test the specific crash scenario: payloadLength = 193 (0xc1) but only 30 bytes available + @Test("Test the specific crash scenario: payloadLength = 193 (0xc1) but only 30 bytes available") + func malformedPacketWithInvalidPayloadLength() throws { var malformedData = Data() // Valid header (13 bytes) @@ -379,20 +296,17 @@ final class BinaryProtocolTests: XCTestCase { } // Total data is now 30 bytes, but payloadLength claims 193 - XCTAssertEqual(malformedData.count, 30) + #expect(malformedData.count == 30) // This should not crash - should return nil gracefully let result = BinaryProtocol.decode(malformedData) - XCTAssertNil(result, "Malformed packet with invalid payload length should return nil, not crash") + #expect(result == nil, "Malformed packet with invalid payload length should return nil, not crash") } - func testTruncatedPacketHandling() throws { - // Test various truncation scenarios + @Test("Test various truncation scenarios") + func truncatedPacketHandling() throws { let packet = TestHelpers.createTestPacket() - guard let validEncoded = BinaryProtocol.encode(packet) else { - XCTFail("Failed to encode test packet") - return - } + let validEncoded = try #require(BinaryProtocol.encode(packet), "Failed to encode test packet") // Test truncation at various points let truncationPoints = [0, 5, 10, 15, 20, 25] @@ -400,12 +314,12 @@ final class BinaryProtocolTests: XCTestCase { for point in truncationPoints { let truncated = validEncoded.prefix(point) let result = BinaryProtocol.decode(truncated) - XCTAssertNil(result, "Truncated packet at \(point) bytes should return nil, not crash") + #expect(result == nil, "Truncated packet at \(point) bytes should return nil, not crash") } } - func testMalformedCompressedPacket() throws { - // Test compressed packet with invalid original size + @Test("Test compressed packet with invalid original size") + func malformedCompressedPacket() throws { var malformedData = Data() // Valid header @@ -434,11 +348,11 @@ final class BinaryProtocolTests: XCTestCase { // Should handle this gracefully let result = BinaryProtocol.decode(malformedData) - XCTAssertNil(result, "Malformed compressed packet should return nil, not crash") + #expect(result == nil, "Malformed compressed packet should return nil, not crash") } - func testExcessivelyLargePayloadLength() throws { - // Test packet claiming extremely large payload + @Test("Test packet claiming extremely large payload") + func excessivelyLargePayloadLength() throws { var malformedData = Data() // Valid header @@ -467,11 +381,11 @@ final class BinaryProtocolTests: XCTestCase { // Should handle this gracefully without trying to allocate massive amounts of memory let result = BinaryProtocol.decode(malformedData) - XCTAssertNil(result, "Packet with excessive payload length should return nil, not crash") + #expect(result == nil, "Packet with excessive payload length should return nil, not crash") } - func testCompressedPacketWithInvalidOriginalSize() throws { - // Test compressed packet with unreasonable original size + @Test("Test compressed packet with unreasonable original size") + func compressedPacketWithInvalidOriginalSize() throws { var malformedData = Data() // Valid header @@ -509,11 +423,11 @@ final class BinaryProtocolTests: XCTestCase { } let result = BinaryProtocol.decode(malformedData) - XCTAssertNil(result, "Compressed packet with invalid original size should return nil, not crash") + #expect(result == nil, "Compressed packet with invalid original size should return nil, not crash") } - func testMaliciousPacketWithIntegerOverflow() throws { - // Test packet designed to cause integer overflow + @Test("Test packet designed to cause integer overflow") + func maliciousPacketWithIntegerOverflow() throws { var maliciousData = Data() // Valid header @@ -548,27 +462,24 @@ final class BinaryProtocolTests: XCTestCase { // Should handle gracefully without integer overflow issues let result = BinaryProtocol.decode(maliciousData) - XCTAssertNil(result, "Malicious packet designed for integer overflow should return nil, not crash") + #expect(result == nil, "Malicious packet designed for integer overflow should return nil, not crash") } - func testPartialHeaderData() throws { - // Test packets with incomplete headers + @Test("Test packets with incomplete headers") + func partialHeaderData() throws { let headerSizes = [0, 1, 5, 10, 12] // Various incomplete header sizes for size in headerSizes { let partialData = Data(repeating: 0x01, count: size) let result = BinaryProtocol.decode(partialData) - XCTAssertNil(result, "Partial header data (\(size) bytes) should return nil, not crash") + #expect(result == nil, "Partial header data (\(size) bytes) should return nil, not crash") } } - func testBoundaryConditions() throws { - // Test exact boundary conditions + @Test("Test exact boundary conditions") + func boundaryConditions() throws { let packet = TestHelpers.createTestPacket() - guard let validEncoded = BinaryProtocol.encode(packet) else { - XCTFail("Failed to encode test packet") - return - } + let validEncoded = try #require(BinaryProtocol.encode(packet), "Failed to encode test packet") // If truncation only removes padding, decode may still succeed. Compute unpadded size. let unpadded = MessagePadding.unpad(validEncoded) @@ -576,7 +487,7 @@ final class BinaryProtocolTests: XCTestCase { let cut = max(1, unpadded.count - 10) let truncatedCore = unpadded.prefix(cut) let result = BinaryProtocol.decode(truncatedCore) - XCTAssertNil(result, "Truncated core frame should return nil, not crash") + #expect(result == nil, "Truncated core frame should return nil, not crash") // Test minimum valid size - create a valid minimal packet var minData = Data() diff --git a/bitchatTests/TestUtilities/TestConstants.swift b/bitchatTests/TestUtilities/TestConstants.swift index 4ae436bb..25437d9f 100644 --- a/bitchatTests/TestUtilities/TestConstants.swift +++ b/bitchatTests/TestUtilities/TestConstants.swift @@ -14,11 +14,6 @@ struct TestConstants { static let shortTimeout: TimeInterval = 1.0 static let longTimeout: TimeInterval = 10.0 - static let testPeerID1: PeerID = "PEER1234" - static let testPeerID2: PeerID = "PEER5678" - static let testPeerID3: PeerID = "PEER9012" - static let testPeerID4: PeerID = "PEER3456" - static let testNickname1 = "Alice" static let testNickname2 = "Bob" static let testNickname3 = "Charlie" diff --git a/bitchatTests/TestUtilities/TestHelpers.swift b/bitchatTests/TestUtilities/TestHelpers.swift index c22fb871..b6236629 100644 --- a/bitchatTests/TestUtilities/TestHelpers.swift +++ b/bitchatTests/TestUtilities/TestHelpers.swift @@ -30,7 +30,7 @@ final class TestHelpers { static func createTestMessage( content: String = TestConstants.testMessage1, sender: String = TestConstants.testNickname1, - senderPeerID: PeerID = TestConstants.testPeerID1, + senderPeerID: PeerID = PeerID(str: UUID().uuidString), isPrivate: Bool = false, recipientNickname: String? = nil, mentions: [String]? = nil @@ -51,7 +51,7 @@ final class TestHelpers { static func createTestPacket( type: UInt8 = 0x01, - senderID: PeerID = TestConstants.testPeerID1, + senderID: PeerID = PeerID(str: UUID().uuidString), recipientID: PeerID? = nil, payload: Data = "test payload".data(using: .utf8)!, signature: Data? = nil, @@ -90,7 +90,7 @@ final class TestHelpers { if Date().timeIntervalSince(start) > timeout { throw TestError.timeout } - try await Task.sleep(nanoseconds: 10_000_000) // 10ms + try await sleep(0.01) } } @@ -104,7 +104,7 @@ final class TestHelpers { } group.addTask { - try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) + try await sleep(1) throw TestError.timeout } @@ -121,14 +121,6 @@ enum TestError: Error { case testFailure(String) } -// MARK: - PeerID String Helpers - -/// Raw String can be passed as PeerID -extension PeerID: @retroactive ExpressibleByStringLiteral { - public init(stringLiteral value: String) { - self.init(str: value) - } +func sleep(_ seconds: TimeInterval) async throws { + try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) } - -/// Interpolated String can be passed as PeerID -extension PeerID: @retroactive ExpressibleByStringInterpolation {} diff --git a/bitchatTests/Utils/PeerIDTests.swift b/bitchatTests/Utils/PeerIDTests.swift index d1b75839..9df7e83a 100644 --- a/bitchatTests/Utils/PeerIDTests.swift +++ b/bitchatTests/Utils/PeerIDTests.swift @@ -6,11 +6,11 @@ // For more information, see // -import XCTest +import Testing +import Foundation @testable import bitchat -final class PeerIDTests: XCTestCase { - +struct PeerIDTests { private let hex16 = "0011223344556677" private let hex64 = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff" @@ -22,212 +22,205 @@ final class PeerIDTests: XCTestCase { // MARK: - Empty prefix - func test_init_empty_prefix_with16() { + @Test func empty_prefix_with16() { let peerID = PeerID(str: hex16) - XCTAssertEqual(peerID.id, hex16) - XCTAssertEqual(peerID.bare, hex16) - XCTAssertEqual(peerID.prefix, .empty) + #expect(peerID.id == hex16) + #expect(peerID.bare == hex16) + #expect(peerID.prefix == .empty) } - func test_init_empty_prefix_with64() { + @Test func empty_prefix_with64() { let peerID = PeerID(str: hex64) - XCTAssertEqual(peerID.id, hex64) - XCTAssertEqual(peerID.bare, hex64) - XCTAssertEqual(peerID.prefix, .empty) + #expect(peerID.id == hex64) + #expect(peerID.bare == hex64) + #expect(peerID.prefix == .empty) } // MARK: - Mesh prefix - func test_init_mesh_prefix_with16() { + @Test func mesh_prefix_with16() { let str = "mesh:" + hex16 let peerID = PeerID(str: str) - XCTAssertEqual(peerID.id, str) - XCTAssertEqual(peerID.bare, hex16) - XCTAssertEqual(peerID.prefix, .mesh) + #expect(peerID.id == str) + #expect(peerID.bare == hex16) + #expect(peerID.prefix == .mesh) } - func test_init_mesh_prefix_with64() { + @Test func mesh_prefix_with64() { let str = "mesh:" + hex64 let peerID = PeerID(str: str) - XCTAssertEqual(peerID.id, str) - XCTAssertEqual(peerID.bare, hex64) - XCTAssertEqual(peerID.prefix, .mesh) + #expect(peerID.id == str) + #expect(peerID.bare == hex64) + #expect(peerID.prefix == .mesh) } // MARK: - Name prefix - func test_init_name_prefix() { + @Test func name_prefix() { let str = "name:some_name" let peerID = PeerID(str: str) - XCTAssertEqual(peerID.id, str) - XCTAssertEqual(peerID.bare, "some_name") - XCTAssertEqual(peerID.prefix, .name) + #expect(peerID.id == str) + #expect(peerID.bare == "some_name") + #expect(peerID.prefix == .name) } // MARK: - Noise prefix - func test_init_noise_prefix_with16() { + @Test func noise_prefix_with16() { let str = "noise:" + hex16 let peerID = PeerID(str: str) - XCTAssertEqual(peerID.id, str) - XCTAssertEqual(peerID.bare, hex16) - XCTAssertEqual(peerID.prefix, .noise) + #expect(peerID.id == str) + #expect(peerID.bare == hex16) + #expect(peerID.prefix == .noise) } - func test_init_noise_prefix_with64() { + @Test func noise_prefix_with64() { let str = "noise:" + hex64 let peerID = PeerID(str: str) - XCTAssertEqual(peerID.id, str) - XCTAssertEqual(peerID.bare, hex64) - XCTAssertEqual(peerID.prefix, .noise) + #expect(peerID.id == str) + #expect(peerID.bare == hex64) + #expect(peerID.prefix == .noise) } // MARK: - GeoDM prefix - func test_init_geoDM_prefix_with16() { + @Test func geoDM_prefix_with16() { let str = "nostr_" + hex16 let peerID = PeerID(str: str) - XCTAssertEqual(peerID.id, str) - XCTAssertEqual(peerID.bare, hex16) - XCTAssertEqual(peerID.prefix, .geoDM) + #expect(peerID.id == str) + #expect(peerID.bare == hex16) + #expect(peerID.prefix == .geoDM) } - func test_init_geoDM_prefix_with64() { + @Test func geoDM_prefix_with64() { let str = "nostr_" + hex64 let peerID = PeerID(str: str) - XCTAssertEqual(peerID.id, str) - XCTAssertEqual(peerID.bare, hex64) - XCTAssertEqual(peerID.prefix, .geoDM) + #expect(peerID.id == str) + #expect(peerID.bare == hex64) + #expect(peerID.prefix == .geoDM) } // MARK: - GeoChat prefix - func test_init_geoChat_prefix_with16() { + @Test func geoChat_prefix_with16() { let str = "nostr:" + hex16 let peerID = PeerID(str: str) - XCTAssertEqual(peerID.id, str) - XCTAssertEqual(peerID.bare, hex16) - XCTAssertEqual(peerID.prefix, .geoChat) + #expect(peerID.id == str) + #expect(peerID.bare == hex16) + #expect(peerID.prefix == .geoChat) } - func test_init_geoChat_prefix_with64() { + @Test func geoChat_prefix_with64() { let str = "nostr:" + hex64 let peerID = PeerID(str: str) - XCTAssertEqual(peerID.id, str) - XCTAssertEqual(peerID.bare, hex64) - XCTAssertEqual(peerID.prefix, .geoChat) + #expect(peerID.id == str) + #expect(peerID.bare == hex64) + #expect(peerID.prefix == .geoChat) } // MARK: - Edge cases - func test_init_with_unknown_prefix() { + @Test func with_unknown_prefix() { let str = "unknown:" + hex16 let peerID = PeerID(str: str) // Falls back to .empty - XCTAssertEqual(peerID.id, str) - XCTAssertEqual(peerID.bare, str) - XCTAssertEqual(peerID.prefix, .empty) + #expect(peerID.id == str) + #expect(peerID.bare == str) + #expect(peerID.prefix == .empty) } - func test_init_with_only_prefix_no_bare() { + @Test func with_only_prefix_no_bare() { let str = "mesh:" let peerID = PeerID(str: str) - XCTAssertEqual(peerID.id, str) - XCTAssertEqual(peerID.bare, "") - XCTAssertEqual(peerID.prefix, .mesh) + #expect(peerID.id == str) + #expect(peerID.bare == "") + #expect(peerID.prefix == .mesh) } // MARK: - init?(data:) - func test_init_data_valid_utf8() { + @Test func data_valid_utf8() { let peerID = PeerID(data: Data(hex16.utf8)) - XCTAssertNotNil(peerID) - XCTAssertEqual(peerID?.bare, hex16) - XCTAssertEqual(peerID?.prefix, .empty) + #expect(peerID != nil) + #expect(peerID?.bare == hex16) + #expect(peerID?.prefix == .empty) } - func test_init_data_invalid_utf8() { + @Test func data_invalid_utf8() { // Random invalid UTF8 let bytes: [UInt8] = [0xFF, 0xFE, 0xFA] let peerID = PeerID(data: Data(bytes)) - XCTAssertNil(peerID) + #expect(peerID == nil) } // MARK: - init(str: Substring) - func test_init_substring() { + @Test func substring() { let substring = hex64.prefix(16) let peerID = PeerID(str: substring) - XCTAssertEqual(peerID.id, String(substring)) - XCTAssertEqual(peerID.bare, String(substring)) - XCTAssertEqual(peerID.prefix, .empty) + #expect(peerID.id == String(substring)) + #expect(peerID.bare == String(substring)) + #expect(peerID.prefix == .empty) } // MARK: - init(nostr_ pubKey:) - func test_init_nostrUnderscore_pubKey() { + @Test func nostrUnderscore_pubKey() { let pubKey = hex64 let peerID = PeerID(nostr_: pubKey) - XCTAssertEqual(peerID.id, "nostr_\(pubKey.prefix(TransportConfig.nostrConvKeyPrefixLength))") - XCTAssertEqual(peerID.bare, String(pubKey.prefix(TransportConfig.nostrConvKeyPrefixLength))) - XCTAssertEqual(peerID.prefix, .geoDM) + #expect(peerID.id == "nostr_\(pubKey.prefix(TransportConfig.nostrConvKeyPrefixLength))") + #expect(peerID.bare == String(pubKey.prefix(TransportConfig.nostrConvKeyPrefixLength))) + #expect(peerID.prefix == .geoDM) } // MARK: - init(nostr pubKey:) - func test_init_nostr_pubKey() { + @Test func nostr_pubKey() { let pubKey = hex64 let peerID = PeerID(nostr: pubKey) - XCTAssertEqual(peerID.id, "nostr:\(pubKey.prefix(TransportConfig.nostrShortKeyDisplayLength))") - XCTAssertEqual(peerID.bare, String(pubKey.prefix(TransportConfig.nostrShortKeyDisplayLength))) - XCTAssertEqual(peerID.prefix, .geoChat) + #expect(peerID.id == "nostr:\(pubKey.prefix(TransportConfig.nostrShortKeyDisplayLength))") + #expect(peerID.bare == String(pubKey.prefix(TransportConfig.nostrShortKeyDisplayLength))) + #expect(peerID.prefix == .geoChat) } // MARK: - init(publicKey:) - func test_init_publicKey_derivesFingerprint() { + @Test func publicKey_derivesFingerprint() { let publicKey = Data(hex64.utf8) let expected = publicKey.sha256Fingerprint().prefix(16) let peerID = PeerID(publicKey: publicKey) - XCTAssertEqual(peerID.bare, String(expected)) - XCTAssertEqual(peerID.prefix, .empty) + #expect(peerID.bare == String(expected)) + #expect(peerID.prefix == .empty) } // MARK: - toShort() - func test_toShort_whenNoiseKeyExists() { + @Test func toShort_whenNoiseKeyExists() { let peerID = PeerID(str: hex64) let short = peerID.toShort() - - // `toShort()` should derive 16-hex peerID let expected = Data(hexString: hex64)!.sha256Fingerprint().prefix(16) - - XCTAssertEqual(short.bare, String(expected)) - XCTAssertEqual(short.prefix, .empty) + #expect(short.bare == String(expected)) + #expect(short.prefix == .empty) } - func test_toShort_whenNoiseKeyExists_withNoisePrefix() { + @Test func toShort_whenNoiseKeyExists_withNoisePrefix() { let peerID = PeerID(str: "noise:" + hex64) let short = peerID.toShort() - - // `toShort()` should derive 16-hex peerID let expected = Data(hexString: hex64)!.sha256Fingerprint().prefix(16) - - XCTAssertEqual(short.bare, String(expected)) - XCTAssertEqual(short.prefix, .empty) - XCTAssertEqual(peerID.prefix, .noise) + #expect(short.bare == String(expected)) + #expect(short.prefix == .empty) + #expect(peerID.prefix == .noise) } - func test_toShort_whenNoNoiseKey() { + @Test func toShort_whenNoNoiseKey() { let peerID = PeerID(str: "some_random_key") let short = peerID.toShort() - XCTAssertEqual(short, peerID) // unchanged + #expect(short == peerID) } - // MARK: - Codable - func test_codable_emptyPrefix() throws { + @Test func codable_emptyPrefix() throws { struct Dummy: Codable, Equatable { let name: String let peerID: PeerID @@ -237,13 +230,13 @@ final class PeerIDTests: XCTestCase { let jsonString = "{\"name\":\"some name\",\"peerID\":\"\(str)\"}" let decoded = try JSONDecoder().decode(Dummy.self, from: Data(jsonString.utf8)) - XCTAssertEqual(decoded.peerID, PeerID(str: str)) + #expect(decoded.peerID == PeerID(str: str)) let encoded = try encoder.encode(decoded) - XCTAssertEqual(String(data: encoded, encoding: .utf8), jsonString) + #expect(String(data: encoded, encoding: .utf8) == jsonString) } - func test_codable_withPrefix() throws { + @Test func codable_withPrefix() throws { struct Dummy: Codable, Equatable { let peerID: PeerID } @@ -252,193 +245,191 @@ final class PeerIDTests: XCTestCase { let jsonString = "{\"peerID\":\"\(str)\"}" let decoded = try JSONDecoder().decode(Dummy.self, from: Data(jsonString.utf8)) - XCTAssertEqual(decoded.peerID, PeerID(str: str)) - XCTAssertEqual(decoded.peerID.bare, hex16) - XCTAssertEqual(decoded.peerID.prefix, .geoDM) + #expect(decoded.peerID == PeerID(str: str)) + #expect(decoded.peerID.bare == hex16) + #expect(decoded.peerID.prefix == .geoDM) let encoded = try encoder.encode(decoded) - XCTAssertEqual(String(data: encoded, encoding: .utf8), jsonString) + #expect(String(data: encoded, encoding: .utf8) == jsonString) } - func test_codable_multiplePrefixes() throws { + @Test func codable_multiplePrefixes() throws { // Loop across all Prefix cases (except .empty since already tested) for prefix in PeerID.Prefix.allCases where prefix != .empty { let bare = hex16 let str = prefix.rawValue + bare let decoded = try JSONDecoder().decode(PeerID.self, from: Data("\"\(str)\"".utf8)) - XCTAssertEqual(decoded.prefix, prefix) - XCTAssertEqual(decoded.bare, bare) + #expect(decoded.prefix == prefix) + #expect(decoded.bare == bare) let encoded = try encoder.encode(decoded) - XCTAssertEqual(String(data: encoded, encoding: .utf8), "\"\(str)\"") + #expect(String(data: encoded, encoding: .utf8) == "\"\(str)\"") } } // MARK: - Comparable - func test_comparable_sorting_and_equality() { + @Test func comparable_sorting_and_equality() { let p1 = PeerID(str: "aaa") let p2 = PeerID(str: "bbb") let p3 = PeerID(str: "bbb") - XCTAssertTrue(p1 < p2) - XCTAssertFalse(p2 < p1) - XCTAssertEqual(p2, p3) + #expect(p1 < p2) + #expect(p2 >= p1) + #expect(p2 == p3) let sorted = [p2, p1].sorted() - XCTAssertEqual(sorted, [p1, p2]) + #expect(sorted == [p1, p2]) } - func test_equality() { + @Test func equality() { let string = "aaa" let peerID = PeerID(str: string) let badString = "bbb" // PeerID == String - XCTAssertTrue(peerID == string) - XCTAssertTrue(peerID == Optional(string)) - XCTAssertTrue(Optional(peerID) == string) - XCTAssertTrue(Optional(peerID) == Optional(string)) + #expect(peerID == string) + #expect(peerID == Optional(string)) + #expect(Optional(peerID) == string) + #expect(Optional(peerID) == Optional(string)) // PeerID != String - XCTAssertTrue(peerID != badString) - XCTAssertTrue(peerID != Optional(badString)) - XCTAssertTrue(Optional(peerID) != badString) - XCTAssertTrue(Optional(peerID) != Optional(badString)) + #expect(peerID != badString) + #expect(peerID != Optional(badString)) + #expect(Optional(peerID) != badString) + #expect(Optional(peerID) != Optional(badString)) // String == PeerID - XCTAssertTrue(string == peerID) - XCTAssertTrue(Optional(string) == peerID) - XCTAssertTrue(string == Optional(peerID)) - XCTAssertTrue(Optional(string) == Optional(peerID)) + #expect(string == peerID) + #expect(Optional(string) == peerID) + #expect(string == Optional(peerID)) + #expect(Optional(string) == Optional(peerID)) // String != PeerID - XCTAssertTrue(badString != peerID) - XCTAssertTrue(Optional(badString) != peerID) - XCTAssertTrue(badString != Optional(peerID)) - XCTAssertTrue(Optional(badString) != Optional(peerID)) + #expect(badString != peerID) + #expect(Optional(badString) != peerID) + #expect(badString != Optional(peerID)) + #expect(Optional(badString) != Optional(peerID)) + // Regular PeerID <> PeerID + #expect(peerID == PeerID(str: "aaa")) + #expect(peerID == Optional(PeerID(str: "aaa"))) + #expect(PeerID(str: "aaa") == peerID) + #expect(Optional(PeerID(str: "aaa")) == Optional(peerID)) - // Make sure the regular PeerID <> PeerID is not broken - XCTAssertTrue(peerID == PeerID(str: "aaa")) - XCTAssertTrue(peerID == Optional(PeerID(str: "aaa"))) - XCTAssertTrue(PeerID(str: "aaa") == peerID) - XCTAssertTrue(Optional(PeerID(str: "aaa")) == Optional(peerID)) - - XCTAssertTrue(peerID != PeerID(str: "bbb")) - XCTAssertTrue(peerID != Optional(PeerID(str: "bbb"))) - XCTAssertTrue(PeerID(str: "bbb") != peerID) - XCTAssertTrue(Optional(PeerID(str: "bbb")) != Optional(peerID)) + #expect(peerID != PeerID(str: "bbb")) + #expect(peerID != Optional(PeerID(str: "bbb"))) + #expect(PeerID(str: "bbb") != peerID) + #expect(Optional(PeerID(str: "bbb")) != Optional(peerID)) } // MARK: - Computed properties - func test_isEmpty_true_and_false() { - XCTAssertTrue(PeerID(str: "").isEmpty) - XCTAssertFalse(PeerID(str: "abc").isEmpty) + @Test func isEmpty_true_and_false() { + #expect(PeerID(str: "").isEmpty) + #expect(!PeerID(str: "abc").isEmpty) } - func test_isGeoChat() { - XCTAssertTrue(PeerID(str: "nostr:abcdef").isGeoChat) - XCTAssertFalse(PeerID(str: "nostr_abcdef").isGeoChat) // different prefix + @Test func isGeoChat() { + #expect(PeerID(str: "nostr:abcdef").isGeoChat) + #expect(!PeerID(str: "nostr_abcdef").isGeoChat) } - func test_isGeoDM() { - XCTAssertTrue(PeerID(str: "nostr_abcdef").isGeoDM) - XCTAssertFalse(PeerID(str: "nostr:abcdef").isGeoDM) + @Test func isGeoDM() { + #expect(PeerID(str: "nostr_abcdef").isGeoDM) + #expect(!PeerID(str: "nostr:abcdef").isGeoDM) } - func test_toPercentEncoded() { + @Test func toPercentEncoded() { let peerID = PeerID(str: "name:some value/with spaces?") let encoded = peerID.toPercentEncoded() // spaces and ? should be percent-encoded in urlPathAllowed - XCTAssertEqual(encoded, "name%3Asome%20value/with%20spaces%3F") + #expect(encoded == "name%3Asome%20value/with%20spaces%3F") } // MARK: - Validation - func test_accepts_short_hex_peer_id() { - XCTAssertTrue(PeerID(str: "0011223344556677").isValid) - XCTAssertTrue(PeerID(str: "aabbccddeeff0011").isValid) + @Test func accepts_short_hex_peer_id() { + #expect(PeerID(str: "0011223344556677").isValid) + #expect(PeerID(str: "aabbccddeeff0011").isValid) } - func test_accepts_full_noise_key_hex() { + @Test func accepts_full_noise_key_hex() { let hex64 = String(repeating: "ab", count: 32) // 64 hex chars - XCTAssertTrue(PeerID(str: hex64).isValid) + #expect(PeerID(str: hex64).isValid) } - func test_accepts_internal_alnum_dash_underscore() { - XCTAssertTrue(PeerID(str: "peer_123-ABC").isValid) - XCTAssertTrue(PeerID(str: "nostr_user_01").isValid) + @Test func accepts_internal_alnum_dash_underscore() { + #expect(PeerID(str: "peer_123-ABC").isValid) + #expect(PeerID(str: "nostr_user_01").isValid) } - func test_rejects_invalid_characters() { - XCTAssertFalse(PeerID(str: "peer!@#").isValid) - XCTAssertFalse(PeerID(str: "gggggggggggggggg").isValid) // not hex for short form + @Test func rejects_invalid_characters() { + #expect(!PeerID(str: "peer!@#").isValid) + #expect(!PeerID(str: "gggggggggggggggg").isValid) // not hex for short form } - func test_rejects_too_long() { + @Test func rejects_too_long() { let tooLong = String(repeating: "a", count: 65) - XCTAssertFalse(PeerID(str: tooLong).isValid) + #expect(!PeerID(str: tooLong).isValid) } - func test_isShort() { - XCTAssertTrue(PeerID(str: hex16).isShort) - XCTAssertFalse(PeerID(str: "abcd").isShort) // wrong length + @Test func isShort() { + #expect(PeerID(str: hex16).isShort) + #expect(!PeerID(str: "abcd").isShort) // wrong length } - func test_isNoiseKeyHex_and_noiseKey() { + @Test func isNoiseKeyHex_and_noiseKey() { let hex64 = String(repeating: "ab", count: 32) // 64 chars valid hex let peerID = PeerID(str: hex64) - XCTAssertTrue(peerID.isNoiseKeyHex) - XCTAssertNotNil(peerID.noiseKey) + #expect(peerID.isNoiseKeyHex) + #expect(peerID.noiseKey != nil) let prefixedPeerID = PeerID(str: "noise:" + hex64) - XCTAssertTrue(prefixedPeerID.isNoiseKeyHex) - XCTAssertNotNil(prefixedPeerID.noiseKey) + #expect(prefixedPeerID.isNoiseKeyHex) + #expect(prefixedPeerID.noiseKey != nil) let bad = String(repeating: "z", count: 64) // invalid hex let badPeerID = PeerID(str: bad) - XCTAssertFalse(badPeerID.isNoiseKeyHex) - XCTAssertNil(badPeerID.noiseKey) + #expect(!badPeerID.isNoiseKeyHex) + #expect(badPeerID.noiseKey == nil) } - func test_prefixes() { + @Test func prefixes() { let hex64 = String(repeating: "a", count: 64) - XCTAssertTrue(PeerID(str: "noise:\(hex64)").isValid) - XCTAssertTrue(PeerID(str: "nostr:\(hex64)").isValid) - XCTAssertTrue(PeerID(str: "nostr_\(hex64)").isValid) + #expect(PeerID(str: "noise:\(hex64)").isValid) + #expect(PeerID(str: "nostr:\(hex64)").isValid) + #expect(PeerID(str: "nostr_\(hex64)").isValid) let hex63 = String(repeating: "a", count: 63) - XCTAssertTrue(PeerID(str: "noise:\(hex63)").isValid) - XCTAssertTrue(PeerID(str: "nostr:\(hex63)").isValid) - XCTAssertTrue(PeerID(str: "nostr_\(hex63)").isValid) + #expect(PeerID(str: "noise:\(hex63)").isValid) + #expect(PeerID(str: "nostr:\(hex63)").isValid) + #expect(PeerID(str: "nostr_\(hex63)").isValid) let hex16 = String(repeating: "a", count: 16) - XCTAssertTrue(PeerID(str: "noise:\(hex16)").isValid) - XCTAssertTrue(PeerID(str: "nostr:\(hex16)").isValid) - XCTAssertTrue(PeerID(str: "nostr_\(hex16)").isValid) + #expect(PeerID(str: "noise:\(hex16)").isValid) + #expect(PeerID(str: "nostr:\(hex16)").isValid) + #expect(PeerID(str: "nostr_\(hex16)").isValid) let hex8 = String(repeating: "a", count: 8) - XCTAssertTrue(PeerID(str: "noise:\(hex8)").isValid) - XCTAssertTrue(PeerID(str: "nostr:\(hex8)").isValid) - XCTAssertTrue(PeerID(str: "nostr_\(hex8)").isValid) + #expect(PeerID(str: "noise:\(hex8)").isValid) + #expect(PeerID(str: "nostr:\(hex8)").isValid) + #expect(PeerID(str: "nostr_\(hex8)").isValid) let mesh = "mesh:abcdefg" - XCTAssertTrue(PeerID(str: "name:\(mesh)").isValid) + #expect(PeerID(str: "name:\(mesh)").isValid) let name = "name:some_name" - XCTAssertTrue(PeerID(str: "name:\(name)").isValid) + #expect(PeerID(str: "name:\(name)").isValid) let badName = "name:bad:name" - XCTAssertFalse(PeerID(str: "name:\(badName)").isValid) + #expect(!PeerID(str: "name:\(badName)").isValid) // Too long let hex65 = String(repeating: "a", count: 65) - XCTAssertFalse(PeerID(str: "noise:\(hex65)").isValid) - XCTAssertFalse(PeerID(str: "nostr:\(hex65)").isValid) - XCTAssertFalse(PeerID(str: "nostr_\(hex65)").isValid) + #expect(!PeerID(str: "noise:\(hex65)").isValid) + #expect(!PeerID(str: "nostr:\(hex65)").isValid) + #expect(!PeerID(str: "nostr_\(hex65)").isValid) } } -