From 264a95b61a3a8e62bd19b6a421f90d6cf06440a8 Mon Sep 17 00:00:00 2001 From: jack <212554440+jackjackbits@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:55:41 -1000 Subject: [PATCH] Raise protocol coverage to 99 percent (#1058) * Expand protocol coverage with edge-case tests * Stabilize read receipt transport test * Stabilize BLE duplicate packet test --------- Co-authored-by: jack --- bitchatTests/BLEServiceCoreTests.swift | 14 +- bitchatTests/LocationChannelsTests.swift | 8 ++ .../Protocol/BinaryProtocolTests.swift | 32 +++++ bitchatTests/ProtocolContractTests.swift | 1 + .../Protocols/BinaryEncodingUtilsTests.swift | 21 +++ .../Protocols/BitchatFilePacketTests.swift | 29 ++++ .../Protocols/LocationChannelTests.swift | 56 ++++++++ bitchatTests/Protocols/PacketsTests.swift | 125 ++++++++++++++++++ .../Services/NostrTransportTests.swift | 11 +- 9 files changed, 288 insertions(+), 9 deletions(-) create mode 100644 bitchatTests/Protocols/LocationChannelTests.swift create mode 100644 bitchatTests/Protocols/PacketsTests.swift diff --git a/bitchatTests/BLEServiceCoreTests.swift b/bitchatTests/BLEServiceCoreTests.swift index 5aa3dde9..e43f35cf 100644 --- a/bitchatTests/BLEServiceCoreTests.swift +++ b/bitchatTests/BLEServiceCoreTests.swift @@ -23,10 +23,18 @@ struct BLEServiceCoreTests { let packet = makePublicPacket(content: "Hello", sender: sender, timestamp: timestamp) ble._test_handlePacket(packet, fromPeerID: sender) - ble._test_handlePacket(packet, fromPeerID: sender) + let receivedFirst = await TestHelpers.waitUntil( + { delegate.publicMessagesSnapshot().count == 1 }, + timeout: TestConstants.defaultTimeout + ) + #expect(receivedFirst) - _ = await TestHelpers.waitUntil({ delegate.publicMessagesSnapshot().count == 1 }, - timeout: TestConstants.shortTimeout) + ble._test_handlePacket(packet, fromPeerID: sender) + let receivedDuplicate = await TestHelpers.waitUntil( + { delegate.publicMessagesSnapshot().count > 1 }, + timeout: TestConstants.shortTimeout + ) + #expect(!receivedDuplicate) let messages = delegate.publicMessagesSnapshot() #expect(messages.count == 1) diff --git a/bitchatTests/LocationChannelsTests.swift b/bitchatTests/LocationChannelsTests.swift index 433f28d9..7f5651a9 100644 --- a/bitchatTests/LocationChannelsTests.swift +++ b/bitchatTests/LocationChannelsTests.swift @@ -44,4 +44,12 @@ struct LocationChannelsTests { let id2 = try idBridge.deriveIdentity(forGeohash: gh) #expect(id1.publicKeyHex == id2.publicKeyHex) } + + @Test func geohashNeighborsNearPoleSkipOutOfBoundsCells() { + let nearPole = Geohash.encode(latitude: 89.9999, longitude: 0.0, precision: 8) + let neighbors = Geohash.neighbors(of: nearPole) + + #expect(neighbors.isEmpty == false) + #expect(neighbors.count < 8) + } } diff --git a/bitchatTests/Protocol/BinaryProtocolTests.swift b/bitchatTests/Protocol/BinaryProtocolTests.swift index eb4582eb..2758bcd2 100644 --- a/bitchatTests/Protocol/BinaryProtocolTests.swift +++ b/bitchatTests/Protocol/BinaryProtocolTests.swift @@ -31,6 +31,11 @@ struct BinaryProtocolTests { let decodedSenderID = decodedPacket.senderID.trimmingNullBytes() #expect(decodedSenderID == originalSenderID) } + + @Test func trimmingNullBytesReturnsOriginalDataWhenNoNullsPresent() { + let raw = Data([0x41, 0x42, 0x43]) + #expect(raw.trimmingNullBytes() == raw) + } @Test func packetWithRecipient() throws { let recipientID = PeerID(str: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789") @@ -683,6 +688,33 @@ struct BinaryProtocolTests { let result = BinaryProtocol.decode(malformedData) #expect(result == nil, "Compressed packet with invalid original size should return nil, not crash") } + + @Test("Test compressed packet with suspicious compression ratio") + func compressedPacketWithSuspiciousCompressionRatio() { + var malformedData = Data() + + malformedData.append(1) // version + malformedData.append(1) // type + malformedData.append(10) // ttl + + for _ in 0..<8 { + malformedData.append(0) + } + + malformedData.append(0x04) // isCompressed + malformedData.append(0x00) + malformedData.append(0x03) // payloadLength = 3 (2 original-size bytes + 1 compressed byte) + + for _ in 0..<8 { + malformedData.append(0x01) + } + + malformedData.append(0xFF) + malformedData.append(0xFF) // originalSize = 65535 + malformedData.append(0x99) // compressed payload length = 1 => ratio > 50_000 + + #expect(BinaryProtocol.decode(malformedData) == nil) + } @Test("Test packet designed to cause integer overflow") func maliciousPacketWithIntegerOverflow() throws { diff --git a/bitchatTests/ProtocolContractTests.swift b/bitchatTests/ProtocolContractTests.swift index 416ba62e..4d38b196 100644 --- a/bitchatTests/ProtocolContractTests.swift +++ b/bitchatTests/ProtocolContractTests.swift @@ -68,6 +68,7 @@ struct ProtocolContractTests { #expect(MessageType.requestSync.description == "requestSync") #expect(NoisePayloadType.verifyResponse.description == "verifyResponse") #expect(DeliveryStatus.sending.displayText == "Sending...") + #expect(DeliveryStatus.sent.displayText == "Sent") #expect(DeliveryStatus.delivered(to: "Alice", at: Date()).displayText == "Delivered to Alice") #expect(DeliveryStatus.read(by: "Bob", at: Date()).displayText == "Read by Bob") #expect(DeliveryStatus.failed(reason: "oops").displayText == "Failed: oops") diff --git a/bitchatTests/Protocols/BinaryEncodingUtilsTests.swift b/bitchatTests/Protocols/BinaryEncodingUtilsTests.swift index 7da9b00c..0d5fa442 100644 --- a/bitchatTests/Protocols/BinaryEncodingUtilsTests.swift +++ b/bitchatTests/Protocols/BinaryEncodingUtilsTests.swift @@ -69,4 +69,25 @@ final class BinaryEncodingUtilsTests: XCTestCase { XCTAssertNil(shortData.readFixedBytes(at: &offset, count: 2)) XCTAssertEqual(offset, 0) } + + func test_sha256Hex_andExtendedLengthStringRoundTrip() throws { + XCTAssertEqual( + Data("abc".utf8).sha256Hex(), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" + ) + + var data = Data() + data.appendString("hello", maxLength: 300) + + var offset = 0 + XCTAssertEqual(data.readString(at: &offset, maxLength: 300), "hello") + } + + func test_readString_returnsNilForInvalidUTF8ExtendedPayload() { + let invalidUTF8 = Data([0x00, 0x02, 0xFF, 0xFF]) + var offset = 0 + + XCTAssertNil(invalidUTF8.readString(at: &offset, maxLength: 300)) + XCTAssertEqual(offset, invalidUTF8.count) + } } diff --git a/bitchatTests/Protocols/BitchatFilePacketTests.swift b/bitchatTests/Protocols/BitchatFilePacketTests.swift index 51801fe5..849eda47 100644 --- a/bitchatTests/Protocols/BitchatFilePacketTests.swift +++ b/bitchatTests/Protocols/BitchatFilePacketTests.swift @@ -44,4 +44,33 @@ final class BitchatFilePacketTests: XCTestCase { XCTAssertEqual(decoded.fileSize, UInt64(content.count)) XCTAssertEqual(decoded.content, content) } + + func testDecodeSupportsLegacyEightByteFileSizeTLV() throws { + let content = Data([0x01, 0x02, 0x03, 0x04]) + var data = Data() + + data.append(0x02) + data.append(contentsOf: [0x00, 0x08]) + data.append(contentsOf: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00]) + data.append(0x04) + data.append(contentsOf: [0x00, 0x00, 0x00, 0x04]) + data.append(content) + + let decoded = try XCTUnwrap(BitchatFilePacket.decode(data)) + XCTAssertEqual(decoded.fileSize, 256) + XCTAssertEqual(decoded.content, content) + } + + func testDecodeUsesContentCountWhenFileSizeTLVIsMissing() throws { + let content = Data([0xAA, 0xBB, 0xCC]) + var data = Data() + + data.append(0x04) + data.append(contentsOf: [0x00, 0x00, 0x00, 0x03]) + data.append(content) + + let decoded = try XCTUnwrap(BitchatFilePacket.decode(data)) + XCTAssertEqual(decoded.fileSize, UInt64(content.count)) + XCTAssertEqual(decoded.content, content) + } } diff --git a/bitchatTests/Protocols/LocationChannelTests.swift b/bitchatTests/Protocols/LocationChannelTests.swift new file mode 100644 index 00000000..bdd32c1e --- /dev/null +++ b/bitchatTests/Protocols/LocationChannelTests.swift @@ -0,0 +1,56 @@ +import Foundation +import Testing + +@testable import bitchat + +struct LocationChannelTests { + @Test + func geohashChannelLevelDisplayNamesAndLegacyDecoding() throws { + for level in GeohashChannelLevel.allCases { + #expect(level.displayName.isEmpty == false) + } + + #expect(try decodeLevel(from: "\"building\"") == .building) + #expect(try decodeLevel(from: "\"block\"") == .block) + #expect(try decodeLevel(from: "\"neighborhood\"") == .neighborhood) + #expect(try decodeLevel(from: "\"city\"") == .city) + #expect(try decodeLevel(from: "\"province\"") == .province) + #expect(try decodeLevel(from: "\"region\"") == .province) + #expect(try decodeLevel(from: "\"country\"") == .region) + #expect(try decodeLevel(from: "\"unknown\"") == .block) + #expect(try decodeLevel(from: "8") == .building) + #expect(try decodeLevel(from: "7") == .block) + #expect(try decodeLevel(from: "6") == .neighborhood) + #expect(try decodeLevel(from: "5") == .city) + #expect(try decodeLevel(from: "4") == .province) + #expect(try decodeLevel(from: "3") == .region) + #expect(try decodeLevel(from: "0") == .region) + #expect(try decodeLevel(from: "99") == .block) + #expect(try decodeLevel(from: "true") == .block) + } + + @Test + func geohashChannelAndChannelIDExposeStableAccessors() { + let channel = GeohashChannel(level: .city, geohash: "u4pru") + + #expect(channel.id == "city-u4pru") + #expect(channel.displayName.contains("u4pru")) + #expect(channel.displayName.contains(channel.level.displayName)) + + let mesh = ChannelID.mesh + #expect(mesh.displayName == "Mesh") + #expect(mesh.nostrGeohashTag == nil) + #expect(mesh.isMesh) + #expect(mesh.isLocation == false) + + let location = ChannelID.location(channel) + #expect(location.displayName == channel.displayName) + #expect(location.nostrGeohashTag == "u4pru") + #expect(location.isMesh == false) + #expect(location.isLocation) + } + + private func decodeLevel(from json: String) throws -> GeohashChannelLevel { + try JSONDecoder().decode(GeohashChannelLevel.self, from: Data(json.utf8)) + } +} diff --git a/bitchatTests/Protocols/PacketsTests.swift b/bitchatTests/Protocols/PacketsTests.swift new file mode 100644 index 00000000..78c244de --- /dev/null +++ b/bitchatTests/Protocols/PacketsTests.swift @@ -0,0 +1,125 @@ +import Foundation +import Testing + +@testable import bitchat + +struct PacketsTests { + @Test + func announcementPacketRoundTripsNeighborsAndSkipsUnknownTLVs() throws { + let neighbors = (0..<12).map { index in + Data(repeating: UInt8(index), count: 8) + } + let packet = AnnouncementPacket( + nickname: "alice", + noisePublicKey: Data(repeating: 0x11, count: 32), + signingPublicKey: Data(repeating: 0x22, count: 32), + directNeighbors: neighbors + ) + + var encoded = try #require(packet.encode()) + encoded.append(makeTLV(type: 0xFF, value: Data([0xAB]))) + + let decoded = try #require(AnnouncementPacket.decode(from: encoded)) + #expect(decoded.nickname == "alice") + #expect(decoded.noisePublicKey == Data(repeating: 0x11, count: 32)) + #expect(decoded.signingPublicKey == Data(repeating: 0x22, count: 32)) + #expect(decoded.directNeighbors?.count == 10) + #expect(decoded.directNeighbors?.first == neighbors.first) + #expect(decoded.directNeighbors?.last == neighbors[9]) + } + + @Test + func announcementPacketEncodeRejectsOversizedFieldsAndInvalidNeighborGroups() { + let oversizedNickname = String(repeating: "a", count: 256) + let validKey = Data(repeating: 0x44, count: 32) + + #expect( + AnnouncementPacket( + nickname: oversizedNickname, + noisePublicKey: validKey, + signingPublicKey: validKey, + directNeighbors: nil + ).encode() == nil + ) + + #expect( + AnnouncementPacket( + nickname: "alice", + noisePublicKey: Data(repeating: 0x55, count: 256), + signingPublicKey: validKey, + directNeighbors: nil + ).encode() == nil + ) + + #expect( + AnnouncementPacket( + nickname: "alice", + noisePublicKey: validKey, + signingPublicKey: Data(repeating: 0x66, count: 256), + directNeighbors: nil + ).encode() == nil + ) + + let invalidNeighborPacket = AnnouncementPacket( + nickname: "alice", + noisePublicKey: validKey, + signingPublicKey: validKey, + directNeighbors: [Data([0x01, 0x02, 0x03])] + ) + let encodedWithoutNeighbors = AnnouncementPacket( + nickname: "alice", + noisePublicKey: validKey, + signingPublicKey: validKey, + directNeighbors: nil + ).encode() + #expect(invalidNeighborPacket.encode() == encodedWithoutNeighbors) + } + + @Test + func announcementPacketDecodeRejectsMissingFieldsAndTruncation() throws { + let missingSigningKey = makeTLV(type: 0x01, value: Data("alice".utf8)) + + makeTLV(type: 0x02, value: Data(repeating: 0x11, count: 32)) + #expect(AnnouncementPacket.decode(from: missingSigningKey) == nil) + + let validPacket = try #require( + AnnouncementPacket( + nickname: "alice", + noisePublicKey: Data(repeating: 0x11, count: 32), + signingPublicKey: Data(repeating: 0x22, count: 32), + directNeighbors: nil + ).encode() + ) + #expect(AnnouncementPacket.decode(from: validPacket.dropLast()) == nil) + } + + @Test + func announcementPacketDecodeIgnoresInvalidNeighborLengths() throws { + var encoded = try #require( + AnnouncementPacket( + nickname: "alice", + noisePublicKey: Data(repeating: 0x11, count: 32), + signingPublicKey: Data(repeating: 0x22, count: 32), + directNeighbors: nil + ).encode() + ) + encoded.append(makeTLV(type: 0x04, value: Data(repeating: 0x99, count: 7))) + + let decoded = try #require(AnnouncementPacket.decode(from: encoded)) + #expect(decoded.directNeighbors == nil) + } + + @Test + func privateMessagePacketRejectsUnknownTypeAndTruncation() { + let unknownTLV = Data([0x7F, 0x01, 0x41]) + #expect(PrivateMessagePacket.decode(from: unknownTLV) == nil) + + let truncated = Data([0x00, 0x05, 0x61]) + #expect(PrivateMessagePacket.decode(from: truncated) == nil) + } + + private func makeTLV(type: UInt8, value: Data) -> Data { + var data = Data([type, UInt8(value.count)]) + data.append(value) + return data + } +} diff --git a/bitchatTests/Services/NostrTransportTests.swift b/bitchatTests/Services/NostrTransportTests.swift index 4f1475a1..e88be16c 100644 --- a/bitchatTests/Services/NostrTransportTests.swift +++ b/bitchatTests/Services/NostrTransportTests.swift @@ -288,11 +288,10 @@ struct NostrTransportTests { transport.sendReadReceipt(first, to: fullPeerID) transport.sendReadReceipt(second, to: fullPeerID) - let sentFirst = await TestHelpers.waitUntil( - { probe.sentEvents.count == 1 && probe.scheduledActionCount == 1 }, - timeout: 0.5 - ) - try #require(sentFirst, "Expected first read receipt and throttle action to be queued") + let sentFirst = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: 1.5) + try #require(sentFirst, "Expected first queued read receipt event") + let scheduledThrottle = await TestHelpers.waitUntil({ probe.scheduledActionCount == 1 }, timeout: 1.5) + try #require(scheduledThrottle, "Expected queued throttle action after first read receipt") let firstEvent = try #require(probe.sentEvents.first, "Expected first queued read receipt event") let firstPayload = try decodeEmbeddedPayload(from: firstEvent, recipient: recipient).payload #expect(firstPayload.type == .readReceipt) @@ -300,7 +299,7 @@ struct NostrTransportTests { try #require(probe.runNextScheduledAction(), "Expected queued throttle action after first read receipt") - let sentSecond = await TestHelpers.waitUntil({ probe.sentEvents.count == 2 }, timeout: 0.5) + let sentSecond = await TestHelpers.waitUntil({ probe.sentEvents.count == 2 }, timeout: 1.5) try #require(sentSecond, "Expected second read receipt after running throttle action") let secondEvent = try #require(probe.sentEvents.last, "Expected second queued read receipt event") let secondPayload = try decodeEmbeddedPayload(from: secondEvent, recipient: recipient).payload