mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +00:00
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 <jackjackbits@users.noreply.github.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,11 @@ struct BinaryProtocolTests {
|
||||
#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")
|
||||
let packet = TestHelpers.createTestPacket(recipientID: recipientID)
|
||||
@@ -684,6 +689,33 @@ struct BinaryProtocolTests {
|
||||
#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 {
|
||||
var maliciousData = Data()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user