235 lines
7.9 KiB
Swift
235 lines
7.9 KiB
Swift
import XCTest
|
|
@testable import Rosetta
|
|
|
|
@MainActor
|
|
final class SearchParityTests: XCTestCase {
|
|
func testSearchViewModelAndChatListUseSameQueryNormalization() async {
|
|
let searchDispatcher = MockSearchDispatcher()
|
|
let chatDispatcher = MockSearchDispatcher()
|
|
let searchVM = SearchViewModel(searchDispatcher: searchDispatcher)
|
|
let chatVM = ChatListViewModel(searchDispatcher: chatDispatcher)
|
|
|
|
searchVM.setSearchQuery(" @Alice ")
|
|
chatVM.setSearchQuery(" @Alice ")
|
|
|
|
try? await Task.sleep(for: .milliseconds(1200))
|
|
|
|
XCTAssertEqual(searchDispatcher.sentQueries, ["alice"])
|
|
XCTAssertEqual(chatDispatcher.sentQueries, ["alice"])
|
|
}
|
|
|
|
func testSavedAliasesAndExactPublicKeyFallback() throws {
|
|
let ownPair = try Self.makeKeyPair()
|
|
let peerPair = try Self.makeKeyPair()
|
|
|
|
let dialog = Self.makeDialog(
|
|
account: ownPair.publicKeyHex,
|
|
opponentKey: peerPair.publicKeyHex,
|
|
username: "peer_user",
|
|
title: "Peer User"
|
|
)
|
|
|
|
let saved = SearchParityPolicy.localAugmentedUsers(
|
|
query: "Saved Messages",
|
|
currentPublicKey: ownPair.publicKeyHex,
|
|
dialogs: [dialog]
|
|
)
|
|
XCTAssertEqual(saved.count, 1)
|
|
XCTAssertEqual(saved.first?.publicKey, ownPair.publicKeyHex)
|
|
XCTAssertEqual(saved.first?.title, "Saved Messages")
|
|
|
|
let exactPeer = SearchParityPolicy.localAugmentedUsers(
|
|
query: "0x" + peerPair.publicKeyHex,
|
|
currentPublicKey: ownPair.publicKeyHex,
|
|
dialogs: [dialog]
|
|
)
|
|
XCTAssertEqual(exactPeer.count, 1)
|
|
XCTAssertEqual(exactPeer.first?.publicKey, peerPair.publicKeyHex)
|
|
XCTAssertEqual(exactPeer.first?.username, "peer_user")
|
|
}
|
|
|
|
func testServerAndLocalMergeDedupesByPublicKeyWithServerPriority() {
|
|
let key = "021111111111111111111111111111111111111111111111111111111111111111"
|
|
let localOnlyKey = "022222222222222222222222222222222222222222222222222222222222222222"
|
|
|
|
let server = [
|
|
SearchUser(username: "server_u", title: "Server Name", publicKey: key, verified: 2, online: 0),
|
|
]
|
|
let local = [
|
|
SearchUser(username: "local_u", title: "Local Name", publicKey: key, verified: 0, online: 1),
|
|
SearchUser(username: "local_only", title: "Local Only", publicKey: localOnlyKey, verified: 0, online: 1),
|
|
]
|
|
|
|
let merged = SearchParityPolicy.mergeServerAndLocal(server: server, local: local)
|
|
XCTAssertEqual(merged.count, 2)
|
|
XCTAssertEqual(merged[0].publicKey, key)
|
|
XCTAssertEqual(merged[0].title, "Server Name")
|
|
XCTAssertEqual(merged[1].publicKey, localOnlyKey)
|
|
}
|
|
|
|
func testActiveUiSearchIgnoresBackgroundUserInfoPackets() async {
|
|
let bus = ChannelSearchBus()
|
|
let uiChannel = SearchPacketChannel.ui(UUID())
|
|
let uiDispatcher = ChannelBusSearchDispatcher(bus: bus, channel: uiChannel)
|
|
let userInfoDispatcher = ChannelBusSearchDispatcher(bus: bus, channel: .userInfo)
|
|
let vm = SearchViewModel(searchDispatcher: uiDispatcher)
|
|
|
|
var backgroundRequest = PacketSearch()
|
|
backgroundRequest.search = "02aa"
|
|
userInfoDispatcher.sendSearchPacket(backgroundRequest)
|
|
|
|
vm.setSearchQuery("alice")
|
|
try? await Task.sleep(for: .milliseconds(1200))
|
|
XCTAssertEqual(uiDispatcher.sentQueries, ["alice"])
|
|
|
|
var backgroundPacket = PacketSearch()
|
|
backgroundPacket.users = [
|
|
SearchUser(username: "random_user", title: "Random User", publicKey: "02aa", verified: 0, online: 0),
|
|
]
|
|
userInfoDispatcher.emit(backgroundPacket)
|
|
try? await Task.sleep(for: .milliseconds(80))
|
|
|
|
XCTAssertTrue(vm.searchResults.isEmpty)
|
|
XCTAssertTrue(vm.isSearching)
|
|
|
|
var uiPacket = PacketSearch()
|
|
uiPacket.users = [
|
|
SearchUser(username: "alice_user", title: "Alice", publicKey: "02bb", verified: 1, online: 0),
|
|
]
|
|
uiDispatcher.emit(uiPacket)
|
|
try? await Task.sleep(for: .milliseconds(80))
|
|
|
|
XCTAssertEqual(vm.searchResults.count, 1)
|
|
XCTAssertEqual(vm.searchResults.first?.username, "alice_user")
|
|
XCTAssertFalse(vm.isSearching)
|
|
}
|
|
}
|
|
|
|
private extension SearchParityTests {
|
|
static func makeKeyPair() throws -> (privateKeyHex: String, publicKeyHex: String) {
|
|
let mnemonic = try CryptoManager.shared.generateMnemonic()
|
|
let pair = try CryptoManager.shared.deriveKeyPair(from: mnemonic)
|
|
return (pair.privateKey.hexString, pair.publicKey.hexString)
|
|
}
|
|
|
|
static func makeDialog(
|
|
account: String,
|
|
opponentKey: String,
|
|
username: String,
|
|
title: String
|
|
) -> Dialog {
|
|
Dialog(
|
|
id: UUID().uuidString,
|
|
account: account,
|
|
opponentKey: opponentKey,
|
|
opponentTitle: title,
|
|
opponentUsername: username,
|
|
lastMessage: "",
|
|
lastMessageTimestamp: 0,
|
|
unreadCount: 0,
|
|
isOnline: true,
|
|
lastSeen: 0,
|
|
verified: 0,
|
|
iHaveSent: true,
|
|
isPinned: false,
|
|
isMuted: false,
|
|
lastMessageFromMe: false,
|
|
lastMessageDelivered: .delivered,
|
|
lastMessageRead: true
|
|
)
|
|
}
|
|
}
|
|
|
|
private final class MockSearchDispatcher: SearchResultDispatching {
|
|
var connectionState: ConnectionState = .authenticated
|
|
var privateHash: String? = "mock-private-hash"
|
|
private(set) var sentQueries: [String] = []
|
|
private var handlers: [UUID: (PacketSearch) -> Void] = [:]
|
|
|
|
func sendSearchPacket(_ packet: PacketSearch) {
|
|
sentQueries.append(packet.search)
|
|
}
|
|
|
|
@discardableResult
|
|
func addSearchResultHandler(_ handler: @escaping (PacketSearch) -> Void) -> UUID {
|
|
let id = UUID()
|
|
handlers[id] = handler
|
|
return id
|
|
}
|
|
|
|
func removeSearchResultHandler(_ id: UUID) {
|
|
handlers.removeValue(forKey: id)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class ChannelBusSearchDispatcher: SearchResultDispatching {
|
|
let connectionState: ConnectionState = .authenticated
|
|
let privateHash: String? = "mock-private-hash"
|
|
private(set) var sentQueries: [String] = []
|
|
|
|
private let bus: ChannelSearchBus
|
|
private let channel: SearchPacketChannel
|
|
|
|
init(bus: ChannelSearchBus, channel: SearchPacketChannel) {
|
|
self.bus = bus
|
|
self.channel = channel
|
|
}
|
|
|
|
func sendSearchPacket(_ packet: PacketSearch) {
|
|
sentQueries.append(packet.search)
|
|
bus.enqueue(channel: channel)
|
|
}
|
|
|
|
@discardableResult
|
|
func addSearchResultHandler(_ handler: @escaping (PacketSearch) -> Void) -> UUID {
|
|
bus.addHandler(channel: channel, handler)
|
|
}
|
|
|
|
func removeSearchResultHandler(_ id: UUID) {
|
|
bus.removeHandler(id)
|
|
}
|
|
|
|
func emit(_ packet: PacketSearch) {
|
|
bus.dispatch(packet)
|
|
}
|
|
}
|
|
|
|
private final class ChannelSearchBus {
|
|
private struct HandlerEntry {
|
|
let channel: SearchPacketChannel
|
|
let handler: (PacketSearch) -> Void
|
|
}
|
|
|
|
private var handlers: [UUID: HandlerEntry] = [:]
|
|
private var pendingChannels: [SearchPacketChannel] = []
|
|
|
|
@discardableResult
|
|
func addHandler(
|
|
channel: SearchPacketChannel,
|
|
_ handler: @escaping (PacketSearch) -> Void
|
|
) -> UUID {
|
|
let id = UUID()
|
|
handlers[id] = HandlerEntry(channel: channel, handler: handler)
|
|
return id
|
|
}
|
|
|
|
func removeHandler(_ id: UUID) {
|
|
handlers.removeValue(forKey: id)
|
|
}
|
|
|
|
func enqueue(channel: SearchPacketChannel) {
|
|
pendingChannels.append(channel)
|
|
}
|
|
|
|
func dispatch(_ packet: PacketSearch) {
|
|
let routed = pendingChannels.isEmpty ? SearchPacketChannel.unscoped : pendingChannels.removeFirst()
|
|
let callbacks = handlers.values
|
|
.filter { $0.channel == .unscoped || $0.channel == routed }
|
|
.map(\.handler)
|
|
for callback in callbacks {
|
|
callback(packet)
|
|
}
|
|
}
|
|
}
|