From 986106fea4722808bfa475884b45cfcdb6f532de Mon Sep 17 00:00:00 2001 From: jack Date: Fri, 4 Jul 2025 15:40:47 +0200 Subject: [PATCH] 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 --- bitchat/Services/BluetoothMeshService.swift | 9 +- bitchat/Views/ContentView.swift | 62 +++++---- bitchatTests/BinaryProtocolTests.swift | 103 +++++++++++++++ bitchatTests/BitchatMessageTests.swift | 131 ++++++++++++++++++++ bitchatTests/BloomFilterTests.swift | 89 +++++++++++++ bitchatTests/MessagePaddingTests.swift | 83 +++++++++++++ project.yml | 13 +- 7 files changed, 456 insertions(+), 34 deletions(-) create mode 100644 bitchatTests/BinaryProtocolTests.swift create mode 100644 bitchatTests/BitchatMessageTests.swift create mode 100644 bitchatTests/BloomFilterTests.swift create mode 100644 bitchatTests/MessagePaddingTests.swift diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index 6a9cc449..b0412279 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -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 } } } diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 595427fe..aab45d0a 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -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) } } } diff --git a/bitchatTests/BinaryProtocolTests.swift b/bitchatTests/BinaryProtocolTests.swift new file mode 100644 index 00000000..3af8661e --- /dev/null +++ b/bitchatTests/BinaryProtocolTests.swift @@ -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)) + } +} \ No newline at end of file diff --git a/bitchatTests/BitchatMessageTests.swift b/bitchatTests/BitchatMessageTests.swift new file mode 100644 index 00000000..d7e16e8c --- /dev/null +++ b/bitchatTests/BitchatMessageTests.swift @@ -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) + } +} \ No newline at end of file diff --git a/bitchatTests/BloomFilterTests.swift b/bitchatTests/BloomFilterTests.swift new file mode 100644 index 00000000..0e6da401 --- /dev/null +++ b/bitchatTests/BloomFilterTests.swift @@ -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..