mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +00:00
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:
@@ -407,9 +407,7 @@ class BluetoothMeshService: NSObject {
|
||||
advertisementData = [
|
||||
CBAdvertisementDataServiceUUIDsKey: [BluetoothMeshService.serviceUUID],
|
||||
// Use only peer ID without any identifying prefix
|
||||
CBAdvertisementDataLocalNameKey: myPeerID,
|
||||
// Request max TX power for better range (system may adjust)
|
||||
CBAdvertisementDataTxPowerLevelKey: NSNumber(value: 1)
|
||||
CBAdvertisementDataLocalNameKey: myPeerID
|
||||
]
|
||||
|
||||
isAdvertising = true
|
||||
@@ -960,22 +958,17 @@ class BluetoothMeshService: NSObject {
|
||||
if !messageIDsToRemove.isEmpty {
|
||||
self.messageQueue.async(flags: .barrier) {
|
||||
// Remove only the messages we sent to this specific peer
|
||||
let beforeCount = self.messageCache.count
|
||||
self.messageCache.removeAll { message in
|
||||
messageIDsToRemove.contains(message.messageID)
|
||||
}
|
||||
let afterCount = self.messageCache.count
|
||||
|
||||
// Also remove from favorite queue if any
|
||||
if var favoriteQueue = self.favoriteMessageQueue[peerID] {
|
||||
let beforeFavCount = favoriteQueue.count
|
||||
favoriteQueue.removeAll { message in
|
||||
messageIDsToRemove.contains(message.messageID)
|
||||
}
|
||||
self.favoriteMessageQueue[peerID] = favoriteQueue.isEmpty ? nil : favoriteQueue
|
||||
// Removed from favorite queue
|
||||
}
|
||||
// Removed from cache
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,35 +99,47 @@ struct ContentView: View {
|
||||
|
||||
// Autocomplete overlay
|
||||
if viewModel.showAutocomplete && !viewModel.autocompleteSuggestions.isEmpty {
|
||||
VStack {
|
||||
Spacer()
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(Array(viewModel.autocompleteSuggestions.enumerated()), id: \.element) { index, suggestion in
|
||||
Button(action: {
|
||||
_ = viewModel.completeNickname(suggestion, in: &messageText)
|
||||
}) {
|
||||
HStack {
|
||||
Text("@\(suggestion)")
|
||||
.font(.system(size: 12, design: .monospaced))
|
||||
.foregroundColor(index == viewModel.selectedAutocompleteIndex ? backgroundColor : textColor)
|
||||
Spacer()
|
||||
GeometryReader { geometry in
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
// Calculate approximate position based on nickname length and @ position
|
||||
let nicknameWidth: CGFloat = viewModel.selectedPrivateChatPeer != nil ? 90 : 80
|
||||
let charWidth: CGFloat = 8.5 // Approximate width of monospace character
|
||||
let atPosition = viewModel.autocompleteRange?.location ?? 0
|
||||
let offsetX = nicknameWidth + (CGFloat(atPosition) * charWidth)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(Array(viewModel.autocompleteSuggestions.enumerated()), id: \.element) { index, suggestion in
|
||||
Button(action: {
|
||||
_ = viewModel.completeNickname(suggestion, in: &messageText)
|
||||
}) {
|
||||
HStack {
|
||||
Text("@\(suggestion)")
|
||||
.font(.system(size: 12, design: .monospaced))
|
||||
.foregroundColor(index == viewModel.selectedAutocompleteIndex ? backgroundColor : textColor)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(index == viewModel.selectedAutocompleteIndex ? textColor : Color.clear)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(index == viewModel.selectedAutocompleteIndex ? textColor : Color.clear)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.background(backgroundColor)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.stroke(secondaryTextColor.opacity(0.5), lineWidth: 1)
|
||||
)
|
||||
.frame(width: 150, alignment: .leading)
|
||||
.offset(x: min(offsetX, geometry.size.width - 180)) // Prevent going off-screen
|
||||
.padding(.bottom, 45) // Position just above input
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.background(backgroundColor)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.stroke(secondaryTextColor.opacity(0.5), lineWidth: 1)
|
||||
)
|
||||
.frame(maxWidth: 200, alignment: .leading)
|
||||
.offset(x: 100) // Align with input field
|
||||
.padding(.bottom, 45) // Position just above input
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -47,4 +47,15 @@ targets:
|
||||
CODE_SIGNING_REQUIRED: YES
|
||||
CODE_SIGNING_ALLOWED: YES
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: YES
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: YES
|
||||
|
||||
bitchatTests:
|
||||
type: bundle.unit-test
|
||||
platform: [iOS, macOS]
|
||||
sources:
|
||||
- bitchatTests
|
||||
dependencies:
|
||||
- target: bitchat
|
||||
settings:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.bitchat.tests
|
||||
SWIFT_VERSION: 5.0
|
||||
Reference in New Issue
Block a user