Fix warnings and improve autocomplete positioning, add unit tests

- Remove unused variables (beforeCount, beforeFavCount)
- Remove Tx Power Level from advertising (not allowed)
- Fix autocomplete popup to appear near cursor position
- Calculate position based on nickname width and @ location
- Add comprehensive unit tests for:
  - Binary protocol encoding/decoding
  - Message padding for privacy
  - Bloom filter duplicate detection
  - BitchatMessage serialization
- Add test target to project.yml
This commit is contained in:
jack
2025-07-04 15:40:47 +02:00
parent e3cc469bf8
commit 986106fea4
7 changed files with 456 additions and 34 deletions
+103
View File
@@ -0,0 +1,103 @@
import XCTest
@testable import bitchat
class BinaryProtocolTests: XCTestCase {
func testPacketEncodingDecoding() {
// Test basic packet
let packet = BitchatPacket(
version: 1,
type: MessageType.message.rawValue,
senderID: Data("testuser".utf8),
recipientID: Data("recipient".utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("Hello, World!".utf8),
signature: nil,
ttl: 5
)
// Encode
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode packet")
return
}
// Decode
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode packet")
return
}
// Verify
XCTAssertEqual(decoded.version, packet.version)
XCTAssertEqual(decoded.type, packet.type)
XCTAssertEqual(decoded.ttl, packet.ttl)
XCTAssertEqual(decoded.timestamp, packet.timestamp)
XCTAssertEqual(decoded.payload, packet.payload)
}
func testBroadcastPacket() {
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("sender".utf8),
recipientID: SpecialRecipients.broadcast,
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("Broadcast message".utf8),
signature: nil,
ttl: 3
)
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode broadcast packet")
return
}
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode broadcast packet")
return
}
// Verify broadcast recipient
XCTAssertEqual(decoded.recipientID, SpecialRecipients.broadcast)
}
func testPacketWithSignature() {
let signature = Data(repeating: 0xAB, count: 64)
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: Data("sender".utf8),
recipientID: Data("recipient".utf8),
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
payload: Data("Signed message".utf8),
signature: signature,
ttl: 5
)
guard let encoded = packet.toBinaryData() else {
XCTFail("Failed to encode signed packet")
return
}
guard let decoded = BitchatPacket.from(encoded) else {
XCTFail("Failed to decode signed packet")
return
}
XCTAssertNotNil(decoded.signature)
XCTAssertEqual(decoded.signature, signature)
}
func testInvalidPacketHandling() {
// Test empty data
XCTAssertNil(BitchatPacket.from(Data()))
// Test truncated data
let truncated = Data(repeating: 0, count: 10)
XCTAssertNil(BitchatPacket.from(truncated))
// Test invalid version
var invalidVersion = Data(repeating: 0, count: 100)
invalidVersion[0] = 99 // Invalid version
XCTAssertNil(BitchatPacket.from(invalidVersion))
}
}
+131
View File
@@ -0,0 +1,131 @@
import XCTest
@testable import bitchat
class BitchatMessageTests: XCTestCase {
func testMessageEncodingDecoding() {
let message = BitchatMessage(
sender: "testuser",
content: "Hello, World!",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: false,
recipientNickname: nil,
senderPeerID: "peer123",
mentions: ["alice", "bob"]
)
guard let encoded = message.toBinaryPayload() else {
XCTFail("Failed to encode message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode message")
return
}
XCTAssertEqual(decoded.sender, message.sender)
XCTAssertEqual(decoded.content, message.content)
XCTAssertEqual(decoded.isPrivate, message.isPrivate)
XCTAssertEqual(decoded.mentions?.count, 2)
XCTAssertTrue(decoded.mentions?.contains("alice") ?? false)
XCTAssertTrue(decoded.mentions?.contains("bob") ?? false)
}
func testPrivateMessage() {
let privateMessage = BitchatMessage(
sender: "alice",
content: "This is private",
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: "bob",
senderPeerID: "alicePeer"
)
guard let encoded = privateMessage.toBinaryPayload() else {
XCTFail("Failed to encode private message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode private message")
return
}
XCTAssertTrue(decoded.isPrivate)
XCTAssertEqual(decoded.recipientNickname, "bob")
}
func testRelayMessage() {
let relayMessage = BitchatMessage(
sender: "charlie",
content: "Relayed message",
timestamp: Date(),
isRelay: true,
originalSender: "alice",
isPrivate: false
)
guard let encoded = relayMessage.toBinaryPayload() else {
XCTFail("Failed to encode relay message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode relay message")
return
}
XCTAssertTrue(decoded.isRelay)
XCTAssertEqual(decoded.originalSender, "alice")
}
func testEmptyContent() {
let emptyMessage = BitchatMessage(
sender: "user",
content: "",
timestamp: Date(),
isRelay: false,
originalSender: nil
)
guard let encoded = emptyMessage.toBinaryPayload() else {
XCTFail("Failed to encode empty message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode empty message")
return
}
XCTAssertEqual(decoded.content, "")
}
func testLongContent() {
let longContent = String(repeating: "A", count: 1000)
let longMessage = BitchatMessage(
sender: "user",
content: longContent,
timestamp: Date(),
isRelay: false,
originalSender: nil
)
guard let encoded = longMessage.toBinaryPayload() else {
XCTFail("Failed to encode long message")
return
}
guard let decoded = BitchatMessage.fromBinaryPayload(encoded) else {
XCTFail("Failed to decode long message")
return
}
XCTAssertEqual(decoded.content, longContent)
}
}
+89
View File
@@ -0,0 +1,89 @@
import XCTest
@testable import bitchat
class BloomFilterTests: XCTestCase {
func testBasicBloomFilter() {
let filter = BloomFilter(size: 1024, hashCount: 3)
// Test insertion and lookup
let testStrings = ["message1", "message2", "message3", "test123"]
for str in testStrings {
XCTAssertFalse(filter.contains(str))
filter.insert(str)
XCTAssertTrue(filter.contains(str))
}
}
func testFalsePositiveRate() {
let filter = BloomFilter(size: 4096, hashCount: 3)
let itemCount = 100
// Insert items
for i in 0..<itemCount {
filter.insert("item\(i)")
}
// Check false positive rate
var falsePositives = 0
let testCount = 1000
for i in itemCount..<(itemCount + testCount) {
if filter.contains("item\(i)") {
falsePositives += 1
}
}
let falsePositiveRate = Double(falsePositives) / Double(testCount)
// With 4096 bits and 3 hash functions, for 100 items,
// false positive rate should be around 0.05% (very low)
XCTAssertLessThan(falsePositiveRate, 0.05)
}
func testReset() {
let filter = BloomFilter(size: 1024, hashCount: 3)
// Insert some items
filter.insert("test1")
filter.insert("test2")
filter.insert("test3")
XCTAssertTrue(filter.contains("test1"))
XCTAssertTrue(filter.contains("test2"))
XCTAssertTrue(filter.contains("test3"))
// Reset
filter.reset()
// Should no longer contain items
XCTAssertFalse(filter.contains("test1"))
XCTAssertFalse(filter.contains("test2"))
XCTAssertFalse(filter.contains("test3"))
}
func testHashDistribution() {
let filter = BloomFilter(size: 4096, hashCount: 3)
// Insert many items and check bit distribution
for i in 0..<500 {
filter.insert("message-\(i)")
}
// Count set bits
var setBits = 0
for i in 0..<filter.bitArray.count {
setBits += filter.bitArray[i].nonzeroBitCount
}
// Should have reasonable distribution (not all bits set)
let totalBits = filter.bitArray.count * 64
let utilization = Double(setBits) / Double(totalBits)
// With 500 items, 3 hashes each, we expect around 1500 bits set
// In a 4096 bit filter, that's about 37% utilization
XCTAssertGreaterThan(utilization, 0.2)
XCTAssertLessThan(utilization, 0.6)
}
}
+83
View File
@@ -0,0 +1,83 @@
import XCTest
@testable import bitchat
class MessagePaddingTests: XCTestCase {
func testBasicPadding() {
let originalData = Data("Hello".utf8)
let targetSize = 256
let padded = MessagePadding.pad(originalData, toSize: targetSize)
XCTAssertEqual(padded.count, targetSize)
let unpadded = MessagePadding.unpad(padded)
XCTAssertEqual(unpadded, originalData)
}
func testMultipleBlockSizes() {
let testMessages = [
"Hi",
"This is a longer message",
"This is an even longer message that should require a larger block size",
String(repeating: "A", count: 500)
]
for message in testMessages {
let data = Data(message.utf8)
let blockSize = MessagePadding.optimalBlockSize(for: data.count)
// Block size should be reasonable
XCTAssertGreaterThan(blockSize, data.count)
XCTAssertTrue(MessagePadding.blockSizes.contains(blockSize) || blockSize == data.count)
let padded = MessagePadding.pad(data, toSize: blockSize)
let unpadded = MessagePadding.unpad(padded)
XCTAssertEqual(unpadded, data)
}
}
func testPaddingWithLargeData() {
let largeData = Data(repeating: 0xFF, count: 1500)
let blockSize = MessagePadding.optimalBlockSize(for: largeData.count)
// Should use 2048 block
XCTAssertEqual(blockSize, 2048)
let padded = MessagePadding.pad(largeData, toSize: blockSize)
XCTAssertEqual(padded.count, blockSize)
let unpadded = MessagePadding.unpad(padded)
XCTAssertEqual(unpadded, largeData)
}
func testInvalidPadding() {
// Test empty data
let empty = Data()
let unpaddedEmpty = MessagePadding.unpad(empty)
XCTAssertEqual(unpaddedEmpty, empty)
// Test data with invalid padding length
var invalidPadding = Data(repeating: 0x00, count: 100)
invalidPadding[99] = 255 // Invalid padding length
let result = MessagePadding.unpad(invalidPadding)
XCTAssertEqual(result, invalidPadding) // Should return original if invalid
}
func testPaddingRandomness() {
// Ensure padding bytes are random (not predictable)
let data = Data("Test".utf8)
let padded1 = MessagePadding.pad(data, toSize: 256)
let padded2 = MessagePadding.pad(data, toSize: 256)
// Same size
XCTAssertEqual(padded1.count, padded2.count)
// But different padding bytes (with very high probability)
XCTAssertNotEqual(padded1, padded2)
// Both should unpad to same data
XCTAssertEqual(MessagePadding.unpad(padded1), data)
XCTAssertEqual(MessagePadding.unpad(padded2), data)
}
}