mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:45:18 +00:00
* Refactor app runtime and view model architecture * Move app ownership into stores and coordinators * Fix smoke test environment injection * Stabilize fragmentation package tests * Fix coordinator build warnings * Clean up chat view model warnings * Fix Nostr relay startup coalescing --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com>
255 lines
8.2 KiB
Swift
255 lines
8.2 KiB
Swift
//
|
|
// ChatViewModelDeliveryStatusTests.swift
|
|
// bitchatTests
|
|
//
|
|
// Tests for ChatViewModel delivery status state machine.
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
import BitFoundation
|
|
@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
|
|
}())
|
|
}
|
|
|
|
@Test @MainActor
|
|
func cleanupOldReadReceipts_removesReceiptIDsWithoutMessages() async {
|
|
let (viewModel, transport) = makeTestableViewModel()
|
|
let peerID = PeerID(str: "0102030405060709")
|
|
|
|
let message = BitchatMessage(
|
|
id: "keep-receipt",
|
|
sender: viewModel.nickname,
|
|
content: "Keep me",
|
|
timestamp: Date(),
|
|
isRelay: false,
|
|
isPrivate: true,
|
|
recipientNickname: "Peer",
|
|
senderPeerID: transport.myPeerID,
|
|
deliveryStatus: .sent
|
|
)
|
|
viewModel.privateChats[peerID] = [message]
|
|
viewModel.sentReadReceipts = ["keep-receipt", "drop-receipt"]
|
|
viewModel.isStartupPhase = false
|
|
|
|
viewModel.cleanupOldReadReceipts()
|
|
|
|
#expect(viewModel.sentReadReceipts == ["keep-receipt"])
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
}
|