mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:45:18 +00:00
* Add comprehensive test coverage for ChatViewModel and BLEService This commit adds 14 new tests to improve the safety net for future refactoring of ChatViewModel and BLEService: New test files: - ChatViewModelTorTests: Tor lifecycle notification handlers (8 tests) - ChatViewModelDeliveryStatusTests: Delivery status state machine (6 tests) - ChatViewModelRefactoringTests: Command routing and message handling (4 tests) - BLEServiceCoreTests: Packet deduplication and stale broadcast filtering (2 tests) - PublicMessagePipelineTests: Message ordering and deduplication (4 tests) - MessageRouterTests: Transport selection and outbox behavior (4 tests) - PrivateChatManagerTests: Chat selection and read receipts (2 tests) - UnifiedPeerServiceTests: Fingerprint resolution and blocking (2 tests) - RelayControllerTests: TTL, handshake, and fragment relay logic (4 tests) Modified files: - MockTransport: Added peer snapshot publishing on connect/disconnect - TestHelpers: Added waitUntil polling helper for async tests - MockIdentityManager: Extended for new test scenarios Total: 454 tests across 64 suites (was 440) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix flaky test by using waitUntil instead of fixed sleep Replace fixed 200ms sleep with waitUntil helper for more reliable async assertion in routing tests. This prevents timing issues on CI. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
229 lines
7.4 KiB
Swift
229 lines
7.4 KiB
Swift
//
|
|
// ChatViewModelDeliveryStatusTests.swift
|
|
// bitchatTests
|
|
//
|
|
// Tests for ChatViewModel delivery status state machine.
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
@testable import bitchat
|
|
|
|
// MARK: - Test Helpers
|
|
|
|
@MainActor
|
|
private func makeTestableViewModel() -> (viewModel: ChatViewModel, transport: MockTransport) {
|
|
let keychain = MockKeychain()
|
|
let keychainHelper = MockKeychainHelper()
|
|
let idBridge = NostrIdentityBridge(keychain: keychainHelper)
|
|
let identityManager = MockIdentityManager(keychain)
|
|
let transport = MockTransport()
|
|
|
|
let viewModel = ChatViewModel(
|
|
keychain: keychain,
|
|
idBridge: idBridge,
|
|
identityManager: identityManager,
|
|
transport: transport
|
|
)
|
|
|
|
return (viewModel, transport)
|
|
}
|
|
|
|
// MARK: - Delivery Status Tests
|
|
|
|
struct ChatViewModelDeliveryStatusTests {
|
|
|
|
// MARK: - Status Transition Tests
|
|
|
|
@Test @MainActor
|
|
func deliveryStatus_noDowngrade_readToDelivered() async {
|
|
let (viewModel, transport) = makeTestableViewModel()
|
|
let peerID = PeerID(str: "0102030405060708")
|
|
let messageID = "test-msg-1"
|
|
|
|
// Setup: create a message with .read status
|
|
let message = BitchatMessage(
|
|
id: messageID,
|
|
sender: viewModel.nickname,
|
|
content: "Test message",
|
|
timestamp: Date(),
|
|
isRelay: false,
|
|
isPrivate: true,
|
|
recipientNickname: "Peer",
|
|
senderPeerID: transport.myPeerID,
|
|
deliveryStatus: .read(by: "Peer", at: Date())
|
|
)
|
|
viewModel.privateChats[peerID] = [message]
|
|
|
|
// Action: try to downgrade to .delivered
|
|
viewModel.didUpdateMessageDeliveryStatus(messageID, status: .delivered(to: "Peer", at: Date()))
|
|
|
|
// Assert: status should remain .read (no downgrade)
|
|
let currentStatus = viewModel.privateChats[peerID]?.first?.deliveryStatus
|
|
#expect({
|
|
if case .read = currentStatus { return true }
|
|
return false
|
|
}())
|
|
}
|
|
|
|
@Test @MainActor
|
|
func deliveryStatus_upgrade_sentToDelivered() async {
|
|
let (viewModel, transport) = makeTestableViewModel()
|
|
let peerID = PeerID(str: "0102030405060708")
|
|
let messageID = "test-msg-2"
|
|
|
|
// Setup: create a message with .sent status
|
|
let message = BitchatMessage(
|
|
id: messageID,
|
|
sender: viewModel.nickname,
|
|
content: "Test message",
|
|
timestamp: Date(),
|
|
isRelay: false,
|
|
isPrivate: true,
|
|
recipientNickname: "Peer",
|
|
senderPeerID: transport.myPeerID,
|
|
deliveryStatus: .sent
|
|
)
|
|
viewModel.privateChats[peerID] = [message]
|
|
|
|
// Action: upgrade to .delivered
|
|
viewModel.didUpdateMessageDeliveryStatus(messageID, status: .delivered(to: "Peer", at: Date()))
|
|
|
|
// Assert: status should be .delivered
|
|
let currentStatus = viewModel.privateChats[peerID]?.first?.deliveryStatus
|
|
#expect({
|
|
if case .delivered = currentStatus { return true }
|
|
return false
|
|
}())
|
|
}
|
|
|
|
@Test @MainActor
|
|
func deliveryStatus_upgrade_deliveredToRead() async {
|
|
let (viewModel, transport) = makeTestableViewModel()
|
|
let peerID = PeerID(str: "0102030405060708")
|
|
let messageID = "test-msg-3"
|
|
|
|
// Setup: create a message with .delivered status
|
|
let message = BitchatMessage(
|
|
id: messageID,
|
|
sender: viewModel.nickname,
|
|
content: "Test message",
|
|
timestamp: Date(),
|
|
isRelay: false,
|
|
isPrivate: true,
|
|
recipientNickname: "Peer",
|
|
senderPeerID: transport.myPeerID,
|
|
deliveryStatus: .delivered(to: "Peer", at: Date().addingTimeInterval(-60))
|
|
)
|
|
viewModel.privateChats[peerID] = [message]
|
|
|
|
// Action: upgrade to .read
|
|
viewModel.didUpdateMessageDeliveryStatus(messageID, status: .read(by: "Peer", at: Date()))
|
|
|
|
// Assert: status should be .read
|
|
let currentStatus = viewModel.privateChats[peerID]?.first?.deliveryStatus
|
|
#expect({
|
|
if case .read = currentStatus { return true }
|
|
return false
|
|
}())
|
|
}
|
|
|
|
// MARK: - Read Receipt Handling
|
|
|
|
@Test @MainActor
|
|
func didReceiveReadReceipt_updatesMessageStatus() async {
|
|
let (viewModel, transport) = makeTestableViewModel()
|
|
let peerID = PeerID(str: "0102030405060708")
|
|
let messageID = "test-msg-4"
|
|
|
|
// Setup: create a message with .sent status
|
|
let message = BitchatMessage(
|
|
id: messageID,
|
|
sender: viewModel.nickname,
|
|
content: "Test message",
|
|
timestamp: Date(),
|
|
isRelay: false,
|
|
isPrivate: true,
|
|
recipientNickname: "Peer",
|
|
senderPeerID: transport.myPeerID,
|
|
deliveryStatus: .sent
|
|
)
|
|
viewModel.privateChats[peerID] = [message]
|
|
|
|
// Action: receive read receipt
|
|
let receipt = ReadReceipt(
|
|
originalMessageID: messageID,
|
|
readerID: peerID,
|
|
readerNickname: "Peer"
|
|
)
|
|
viewModel.didReceiveReadReceipt(receipt)
|
|
|
|
// Assert: status should be .read
|
|
let currentStatus = viewModel.privateChats[peerID]?.first?.deliveryStatus
|
|
#expect({
|
|
if case .read = currentStatus { return true }
|
|
return false
|
|
}())
|
|
}
|
|
|
|
// MARK: - Public Timeline Status Tests
|
|
|
|
@Test @MainActor
|
|
func deliveryStatus_publicTimeline_updatesCorrectly() async {
|
|
let (viewModel, _) = makeTestableViewModel()
|
|
let messageID = "public-msg-1"
|
|
|
|
// Setup: add a message to public timeline with .sending status
|
|
let message = BitchatMessage(
|
|
id: messageID,
|
|
sender: viewModel.nickname,
|
|
content: "Public message",
|
|
timestamp: Date(),
|
|
isRelay: false,
|
|
isPrivate: false,
|
|
deliveryStatus: .sending
|
|
)
|
|
viewModel.messages.append(message)
|
|
|
|
// Action: update to .sent
|
|
viewModel.didUpdateMessageDeliveryStatus(messageID, status: .sent)
|
|
|
|
// Assert
|
|
let updatedMessage = viewModel.messages.first(where: { $0.id == messageID })
|
|
#expect({
|
|
if case .sent = updatedMessage?.deliveryStatus { return true }
|
|
return false
|
|
}())
|
|
}
|
|
|
|
// MARK: - Status Rank Tests (for deduplication)
|
|
|
|
@Test @MainActor
|
|
func statusRank_orderingIsCorrect() async {
|
|
// This tests the implicit ordering used in refreshVisibleMessages
|
|
// failed < sending < sent < partiallyDelivered < delivered < read
|
|
|
|
let statuses: [DeliveryStatus] = [
|
|
.failed(reason: "test"),
|
|
.sending,
|
|
.sent,
|
|
.partiallyDelivered(reached: 1, total: 3),
|
|
.delivered(to: "B", at: Date()),
|
|
.read(by: "C", at: Date())
|
|
]
|
|
|
|
// Verify each status has a logical progression
|
|
// This is more of a documentation test to ensure the ranking logic is understood
|
|
for (index, status) in statuses.enumerated() {
|
|
switch status {
|
|
case .failed: #expect(index == 0)
|
|
case .sending: #expect(index == 1)
|
|
case .sent: #expect(index == 2)
|
|
case .partiallyDelivered: #expect(index == 3)
|
|
case .delivered: #expect(index == 4)
|
|
case .read: #expect(index == 5)
|
|
}
|
|
}
|
|
}
|
|
}
|