Files
mobile-ios/RosettaTests/SearchParityTests.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)
}
}
}