mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			432 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			432 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Swift
		
	
| //
 | |
| //  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| import XCTest
 | |
| @testable import Signal
 | |
| @testable import SignalMessaging
 | |
| 
 | |
| // TODO: We might be able to merge this with OWSFakeContactsManager.
 | |
| @objc
 | |
| class ConversationSearcherContactsManager: NSObject, ContactsManagerProtocol {
 | |
|     func displayName(forPhoneIdentifier recipientId: String?, transaction: YapDatabaseReadTransaction) -> String {
 | |
|         return self.displayName(forPhoneIdentifier: recipientId)
 | |
|     }
 | |
| 
 | |
|     func displayName(forPhoneIdentifier phoneNumber: String?) -> String {
 | |
|         if phoneNumber == aliceRecipientId {
 | |
|             return "Alice"
 | |
|         } else if phoneNumber == bobRecipientId {
 | |
|             return "Bob Barker"
 | |
|         } else {
 | |
|             return ""
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     func signalAccounts() -> [SignalAccount] {
 | |
|         return []
 | |
|     }
 | |
| 
 | |
|     func isSystemContact(_ recipientId: String) -> Bool {
 | |
|         return true
 | |
|     }
 | |
| 
 | |
|     func isSystemContact(withSignalAccount recipientId: String) -> Bool {
 | |
|         return true
 | |
|     }
 | |
| 
 | |
|     func compare(signalAccount left: SignalAccount, with right: SignalAccount) -> ComparisonResult {
 | |
|         owsFailDebug("if this method ends up being used by the tests, we should provide a better implementation.")
 | |
| 
 | |
|         return .orderedAscending
 | |
|     }
 | |
| 
 | |
|     func cnContact(withId contactId: String?) -> CNContact? {
 | |
|         return nil
 | |
|     }
 | |
| 
 | |
|     func avatarData(forCNContactId contactId: String?) -> Data? {
 | |
|         return nil
 | |
|     }
 | |
| 
 | |
|     func avatarImage(forCNContactId contactId: String?) -> UIImage? {
 | |
|         return nil
 | |
|     }
 | |
| }
 | |
| 
 | |
| private let bobRecipientId = "+49030183000"
 | |
| private let aliceRecipientId = "+12345678900"
 | |
| 
 | |
| class ConversationSearcherTest: SignalBaseTest {
 | |
| 
 | |
|     // MARK: - Dependencies
 | |
|     var searcher: ConversationSearcher {
 | |
|         return ConversationSearcher.shared
 | |
|     }
 | |
| 
 | |
|     var dbConnection: YapDatabaseConnection {
 | |
|         return OWSPrimaryStorage.shared().dbReadWriteConnection
 | |
|     }
 | |
| 
 | |
|     // MARK: - Test Life Cycle
 | |
| 
 | |
|     override func tearDown() {
 | |
|         super.tearDown()
 | |
|     }
 | |
| 
 | |
|     override func setUp() {
 | |
|         super.setUp()
 | |
| 
 | |
|         FullTextSearchFinder.ensureDatabaseExtensionRegistered(storage: OWSPrimaryStorage.shared())
 | |
| 
 | |
|         // Replace this singleton.
 | |
|         SSKEnvironment.shared.contactsManager = ConversationSearcherContactsManager()
 | |
| 
 | |
|         self.dbConnection.readWrite { transaction in
 | |
|             let bookModel = TSGroupModel(title: "Book Club", memberIds: [aliceRecipientId, bobRecipientId], image: nil, groupId: Randomness.generateRandomBytes(kGroupIdLength))
 | |
|             let bookClubGroupThread = TSGroupThread.getOrCreateThread(with: bookModel, transaction: transaction)
 | |
|             self.bookClubThread = ThreadViewModel(thread: bookClubGroupThread, transaction: transaction)
 | |
| 
 | |
|             let snackModel = TSGroupModel(title: "Snack Club", memberIds: [aliceRecipientId], image: nil, groupId: Randomness.generateRandomBytes(kGroupIdLength))
 | |
|             let snackClubGroupThread = TSGroupThread.getOrCreateThread(with: snackModel, transaction: transaction)
 | |
|             self.snackClubThread = ThreadViewModel(thread: snackClubGroupThread, transaction: transaction)
 | |
| 
 | |
|             let aliceContactThread = TSContactThread.getOrCreateThread(withContactId: aliceRecipientId, transaction: transaction)
 | |
|             self.aliceThread = ThreadViewModel(thread: aliceContactThread, transaction: transaction)
 | |
| 
 | |
|             let bobContactThread = TSContactThread.getOrCreateThread(withContactId: bobRecipientId, transaction: transaction)
 | |
|             self.bobEmptyThread = ThreadViewModel(thread: bobContactThread, transaction: transaction)
 | |
| 
 | |
|             let helloAlice = TSOutgoingMessage(in: aliceContactThread, messageBody: "Hello Alice", attachmentId: nil)
 | |
|             helloAlice.save(with: transaction)
 | |
| 
 | |
|             let goodbyeAlice = TSOutgoingMessage(in: aliceContactThread, messageBody: "Goodbye Alice", attachmentId: nil)
 | |
|             goodbyeAlice.save(with: transaction)
 | |
| 
 | |
|             let helloBookClub = TSOutgoingMessage(in: bookClubGroupThread, messageBody: "Hello Book Club", attachmentId: nil)
 | |
|             helloBookClub.save(with: transaction)
 | |
| 
 | |
|             let goodbyeBookClub = TSOutgoingMessage(in: bookClubGroupThread, messageBody: "Goodbye Book Club", attachmentId: nil)
 | |
|             goodbyeBookClub.save(with: transaction)
 | |
| 
 | |
|             let bobsPhoneNumber = TSOutgoingMessage(in: bookClubGroupThread, messageBody: "My phone number is: 321-321-4321", attachmentId: nil)
 | |
|             bobsPhoneNumber.save(with: transaction)
 | |
| 
 | |
|             let bobsFaxNumber = TSOutgoingMessage(in: bookClubGroupThread, messageBody: "My fax is: 222-333-4444", attachmentId: nil)
 | |
|             bobsFaxNumber.save(with: transaction)
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // MARK: - Fixtures
 | |
| 
 | |
|     var bookClubThread: ThreadViewModel!
 | |
|     var snackClubThread: ThreadViewModel!
 | |
| 
 | |
|     var aliceThread: ThreadViewModel!
 | |
|     var bobEmptyThread: ThreadViewModel!
 | |
| 
 | |
|     // MARK: Tests
 | |
| 
 | |
|     func testSearchByGroupName() {
 | |
|         var threads: [ThreadViewModel] = []
 | |
| 
 | |
|         // No Match
 | |
|         threads = searchConversations(searchText: "asdasdasd")
 | |
|         XCTAssert(threads.isEmpty)
 | |
| 
 | |
|         // Partial Match
 | |
|         threads = searchConversations(searchText: "Book")
 | |
|         XCTAssertEqual(1, threads.count)
 | |
|         XCTAssertEqual([bookClubThread], threads)
 | |
| 
 | |
|         threads = searchConversations(searchText: "Snack")
 | |
|         XCTAssertEqual(1, threads.count)
 | |
|         XCTAssertEqual([snackClubThread], threads)
 | |
| 
 | |
|         // Multiple Partial Matches
 | |
|         threads = searchConversations(searchText: "Club")
 | |
|         XCTAssertEqual(2, threads.count)
 | |
|         XCTAssertEqual([bookClubThread, snackClubThread], threads)
 | |
| 
 | |
|         // Match Name Exactly
 | |
|         threads = searchConversations(searchText: "Book Club")
 | |
|         XCTAssertEqual(1, threads.count)
 | |
|         XCTAssertEqual([bookClubThread], threads)
 | |
|     }
 | |
| 
 | |
|     func testSearchContactByNumber() {
 | |
|         var threads: [ThreadViewModel] = []
 | |
| 
 | |
|         // No match
 | |
|         threads = searchConversations(searchText: "+5551239999")
 | |
|         XCTAssertEqual(0, threads.count)
 | |
| 
 | |
|         // Exact match
 | |
|         threads = searchConversations(searchText: aliceRecipientId)
 | |
|         XCTAssertEqual(3, threads.count)
 | |
|         XCTAssertEqual([bookClubThread, aliceThread, snackClubThread], threads)
 | |
| 
 | |
|         // Partial match
 | |
|         threads = searchConversations(searchText: "+123456")
 | |
|         XCTAssertEqual(3, threads.count)
 | |
|         XCTAssertEqual([bookClubThread, aliceThread, snackClubThread], threads)
 | |
| 
 | |
|         // Prefixes
 | |
|         threads = searchConversations(searchText: "12345678900")
 | |
|         XCTAssertEqual(3, threads.count)
 | |
|         XCTAssertEqual([bookClubThread, aliceThread, snackClubThread], threads)
 | |
| 
 | |
|         threads = searchConversations(searchText: "49")
 | |
|         XCTAssertEqual(1, threads.count)
 | |
|         XCTAssertEqual([bookClubThread], threads)
 | |
| 
 | |
|         threads = searchConversations(searchText: "1-234-56")
 | |
|         XCTAssertEqual(3, threads.count)
 | |
|         XCTAssertEqual([bookClubThread, aliceThread, snackClubThread], threads)
 | |
| 
 | |
|         threads = searchConversations(searchText: "123456")
 | |
|         XCTAssertEqual(3, threads.count)
 | |
|         XCTAssertEqual([bookClubThread, aliceThread, snackClubThread], threads)
 | |
| 
 | |
|         threads = searchConversations(searchText: "1.234.56")
 | |
|         XCTAssertEqual(3, threads.count)
 | |
|         XCTAssertEqual([bookClubThread, aliceThread, snackClubThread], threads)
 | |
| 
 | |
|         threads = searchConversations(searchText: "1 234 56")
 | |
|         XCTAssertEqual(3, threads.count)
 | |
|         XCTAssertEqual([bookClubThread, aliceThread, snackClubThread], threads)
 | |
|     }
 | |
| 
 | |
|     func testSearchContactByNumberWithoutCountryCode() {
 | |
|         var threads: [ThreadViewModel] = []
 | |
|         // Phone Number formatting should be forgiving
 | |
|         threads = searchConversations(searchText: "234.56")
 | |
|         XCTAssertEqual(3, threads.count)
 | |
|         XCTAssertEqual([bookClubThread, aliceThread, snackClubThread], threads)
 | |
| 
 | |
|         threads = searchConversations(searchText: "234 56")
 | |
|         XCTAssertEqual(3, threads.count)
 | |
|         XCTAssertEqual([bookClubThread, aliceThread, snackClubThread], threads)
 | |
|     }
 | |
| 
 | |
|     func testSearchConversationByContactByName() {
 | |
|         var threads: [ThreadViewModel] = []
 | |
| 
 | |
|         threads = searchConversations(searchText: "Alice")
 | |
|         XCTAssertEqual(3, threads.count)
 | |
|         XCTAssertEqual([bookClubThread, aliceThread, snackClubThread], threads)
 | |
| 
 | |
|         threads = searchConversations(searchText: "Bob")
 | |
|         XCTAssertEqual(1, threads.count)
 | |
|         XCTAssertEqual([bookClubThread], threads)
 | |
| 
 | |
|         threads = searchConversations(searchText: "Barker")
 | |
|         XCTAssertEqual(1, threads.count)
 | |
|         XCTAssertEqual([bookClubThread], threads)
 | |
| 
 | |
|         threads = searchConversations(searchText: "Bob B")
 | |
|         XCTAssertEqual(1, threads.count)
 | |
|         XCTAssertEqual([bookClubThread], threads)
 | |
|     }
 | |
| 
 | |
|     func testSearchMessageByBodyContent() {
 | |
|         var resultSet: SearchResultSet = .empty
 | |
| 
 | |
|         resultSet = getResultSet(searchText: "Hello Alice")
 | |
|         XCTAssertEqual(1, resultSet.messages.count)
 | |
|         XCTAssertEqual(aliceThread, resultSet.messages.first?.thread)
 | |
| 
 | |
|         resultSet = getResultSet(searchText: "Hello")
 | |
|         XCTAssertEqual(2, resultSet.messages.count)
 | |
|         XCTAssert(resultSet.messages.map { $0.thread }.contains(aliceThread))
 | |
|         XCTAssert(resultSet.messages.map { $0.thread }.contains(bookClubThread))
 | |
|     }
 | |
| 
 | |
|     func testSearchEdgeCases() {
 | |
|         var resultSet: SearchResultSet = .empty
 | |
| 
 | |
|         resultSet = getResultSet(searchText: "Hello Alice")
 | |
|         XCTAssertEqual(1, resultSet.messages.count)
 | |
|         XCTAssertEqual(["Hello Alice"], bodies(forMessageResults: resultSet.messages))
 | |
| 
 | |
|         resultSet = getResultSet(searchText: "hello alice")
 | |
|         XCTAssertEqual(1, resultSet.messages.count)
 | |
|         XCTAssertEqual(["Hello Alice"], bodies(forMessageResults: resultSet.messages))
 | |
| 
 | |
|         resultSet = getResultSet(searchText: "Hel")
 | |
|         XCTAssertEqual(2, resultSet.messages.count)
 | |
|         XCTAssertEqual(["Hello Alice", "Hello Book Club"], bodies(forMessageResults: resultSet.messages))
 | |
| 
 | |
|         resultSet = getResultSet(searchText: "Hel Ali")
 | |
|         XCTAssertEqual(1, resultSet.messages.count)
 | |
|         XCTAssertEqual(["Hello Alice"], bodies(forMessageResults: resultSet.messages))
 | |
| 
 | |
|         resultSet = getResultSet(searchText: "Hel Ali Alic")
 | |
|         XCTAssertEqual(1, resultSet.messages.count)
 | |
|         XCTAssertEqual(["Hello Alice"], bodies(forMessageResults: resultSet.messages))
 | |
| 
 | |
|         resultSet = getResultSet(searchText: "Ali Hel")
 | |
|         XCTAssertEqual(1, resultSet.messages.count)
 | |
|         XCTAssertEqual(["Hello Alice"], bodies(forMessageResults: resultSet.messages))
 | |
| 
 | |
|         resultSet = getResultSet(searchText: "CLU")
 | |
|         XCTAssertEqual(2, resultSet.messages.count)
 | |
|         XCTAssertEqual(["Goodbye Book Club", "Hello Book Club"], bodies(forMessageResults: resultSet.messages))
 | |
| 
 | |
|         resultSet = getResultSet(searchText: "hello !@##!@#!$^@!@#! alice")
 | |
|         XCTAssertEqual(1, resultSet.messages.count)
 | |
|         XCTAssertEqual(["Hello Alice"], bodies(forMessageResults: resultSet.messages))
 | |
| 
 | |
|         resultSet = getResultSet(searchText: "3213 phone")
 | |
|         XCTAssertEqual(1, resultSet.messages.count)
 | |
|         XCTAssertEqual(["My phone number is: 321-321-4321"], bodies(forMessageResults: resultSet.messages))
 | |
| 
 | |
|         resultSet = getResultSet(searchText: "PHO 3213")
 | |
|         XCTAssertEqual(1, resultSet.messages.count)
 | |
|         XCTAssertEqual(["My phone number is: 321-321-4321"], bodies(forMessageResults: resultSet.messages))
 | |
| 
 | |
|         resultSet = getResultSet(searchText: "fax")
 | |
|         XCTAssertEqual(1, resultSet.messages.count)
 | |
|         XCTAssertEqual(["My fax is: 222-333-4444"], bodies(forMessageResults: resultSet.messages))
 | |
| 
 | |
|         resultSet = getResultSet(searchText: "fax 2223")
 | |
|         XCTAssertEqual(1, resultSet.messages.count)
 | |
|         XCTAssertEqual(["My fax is: 222-333-4444"], bodies(forMessageResults: resultSet.messages))
 | |
|     }
 | |
| 
 | |
|     // MARK: Helpers
 | |
| 
 | |
|     func bodies<T>(forMessageResults messageResults: [ConversationSearchResult<T>]) -> [String] {
 | |
|         var result = [String]()
 | |
| 
 | |
|         self.dbConnection.read { transaction in
 | |
|             for messageResult in messageResults {
 | |
|                 guard let messageId = messageResult.messageId else {
 | |
|                     owsFailDebug("message result missing message id")
 | |
|                     continue
 | |
|                 }
 | |
|                 guard let interaction = TSInteraction.fetch(uniqueId: messageId, transaction: transaction) else {
 | |
|                     owsFailDebug("couldn't load interaction for message result")
 | |
|                     continue
 | |
|                 }
 | |
|                 guard let message = interaction as? TSMessage else {
 | |
|                     owsFailDebug("invalid message for message result")
 | |
|                     continue
 | |
|                 }
 | |
|                 guard let messageBody = message.body else {
 | |
|                     owsFailDebug("message result missing message body")
 | |
|                     continue
 | |
|                 }
 | |
|                 result.append(messageBody)
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return result.sorted()
 | |
|     }
 | |
| 
 | |
|     private func searchConversations(searchText: String) -> [ThreadViewModel] {
 | |
|         let results = getResultSet(searchText: searchText)
 | |
|         return results.conversations.map { $0.thread }
 | |
|     }
 | |
| 
 | |
|     private func getResultSet(searchText: String) -> SearchResultSet {
 | |
|         var results: SearchResultSet!
 | |
|         self.dbConnection.read { transaction in
 | |
|             results = self.searcher.results(searchText: searchText, transaction: transaction, contactsManager: SSKEnvironment.shared.contactsManager)
 | |
|         }
 | |
|         return results
 | |
|     }
 | |
| }
 | |
| 
 | |
| class SearcherTest: SignalBaseTest {
 | |
| 
 | |
|     struct TestCharacter {
 | |
|         let name: String
 | |
|         let description: String
 | |
|         let phoneNumber: String?
 | |
|     }
 | |
| 
 | |
|     let smerdyakov = TestCharacter(name: "Pavel Fyodorovich Smerdyakov", description: "A rusty hue in the sky", phoneNumber: nil)
 | |
|     let stinkingLizaveta = TestCharacter(name: "Stinking Lizaveta", description: "object of pity", phoneNumber: "+13235555555")
 | |
|     let regularLizaveta = TestCharacter(name: "Lizaveta", description: "", phoneNumber: "1 (415) 555-5555")
 | |
| 
 | |
|     let indexer = { (character: TestCharacter) in
 | |
|         return "\(character.name) \(character.description) \(character.phoneNumber ?? "")"
 | |
|     }
 | |
| 
 | |
|     var searcher: Searcher<TestCharacter> {
 | |
|         return Searcher(indexer: indexer)
 | |
|     }
 | |
| 
 | |
|     override func setUp() {
 | |
|         super.setUp()
 | |
|         // Put setup code here. This method is called before the invocation of each test method in the class.
 | |
|     }
 | |
| 
 | |
|     override func tearDown() {
 | |
|         // Put teardown code here. This method is called after the invocation of each test method in the class.
 | |
|         super.tearDown()
 | |
|     }
 | |
| 
 | |
|     func testSimple() {
 | |
|         XCTAssert(searcher.matches(item: smerdyakov, query: "Pavel"))
 | |
|         XCTAssert(searcher.matches(item: smerdyakov, query: "pavel"))
 | |
|         XCTAssertFalse(searcher.matches(item: smerdyakov, query: "asdf"))
 | |
|         XCTAssertFalse(searcher.matches(item: smerdyakov, query: ""))
 | |
|         XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Pity"))
 | |
|     }
 | |
| 
 | |
|     func testRepeats() {
 | |
|         XCTAssert(searcher.matches(item: smerdyakov, query: "pavel pavel"))
 | |
|         XCTAssertFalse(searcher.matches(item: smerdyakov, query: "pavelpavel"))
 | |
|     }
 | |
| 
 | |
|     func testSplitWords() {
 | |
|         XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Lizaveta"))
 | |
|         XCTAssert(searcher.matches(item: regularLizaveta, query: "Lizaveta"))
 | |
| 
 | |
|         XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Stinking Lizaveta"))
 | |
|         XCTAssertFalse(searcher.matches(item: regularLizaveta, query: "Stinking Lizaveta"))
 | |
| 
 | |
|         XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Lizaveta Stinking"))
 | |
|         XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Lizaveta St"))
 | |
|         XCTAssert(searcher.matches(item: stinkingLizaveta, query: "  Lizaveta St "))
 | |
|     }
 | |
| 
 | |
|     func testFormattingChars() {
 | |
|         XCTAssert(searcher.matches(item: stinkingLizaveta, query: "323"))
 | |
|         XCTAssert(searcher.matches(item: stinkingLizaveta, query: "1-323-555-5555"))
 | |
|         XCTAssert(searcher.matches(item: stinkingLizaveta, query: "13235555555"))
 | |
|         XCTAssert(searcher.matches(item: stinkingLizaveta, query: "+1-323"))
 | |
|         XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Liza +1-323"))
 | |
| 
 | |
|         // Sanity check, match both by names
 | |
|         XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Liza"))
 | |
|         XCTAssert(searcher.matches(item: regularLizaveta, query: "Liza"))
 | |
| 
 | |
|         // Disambiguate the two Liza's by area code
 | |
|         XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Liza 323"))
 | |
|         XCTAssertFalse(searcher.matches(item: regularLizaveta, query: "Liza 323"))
 | |
|     }
 | |
| 
 | |
|     func testSearchQuery() {
 | |
|         XCTAssertEqual(FullTextSearchFinder.query(searchText: "Liza"), "\"Liza\"*")
 | |
|         XCTAssertEqual(FullTextSearchFinder.query(searchText: "Liza +1-323"), "\"1323\"* \"Liza\"*")
 | |
|         XCTAssertEqual(FullTextSearchFinder.query(searchText: "\"\\ `~!@#$%^&*()_+-={}|[]:;'<>?,./Liza +1-323"), "\"1323\"* \"Liza\"*")
 | |
|         XCTAssertEqual(FullTextSearchFinder.query(searchText: "renaldo RENALDO reñaldo REÑALDO"), "\"RENALDO\"* \"REÑALDO\"* \"renaldo\"* \"reñaldo\"*")
 | |
|         XCTAssertEqual(FullTextSearchFinder.query(searchText: "😏"), "\"😏\"*")
 | |
|         XCTAssertEqual(FullTextSearchFinder.query(searchText: "alice 123 bob 456"), "\"123456\"* \"alice\"* \"bob\"*")
 | |
|         XCTAssertEqual(FullTextSearchFinder.query(searchText: "Li!za"), "\"Liza\"*")
 | |
|         XCTAssertEqual(FullTextSearchFinder.query(searchText: "Liza Liza"), "\"Liza\"*")
 | |
|         XCTAssertEqual(FullTextSearchFinder.query(searchText: "Liza liza"), "\"Liza\"* \"liza\"*")
 | |
|     }
 | |
| 
 | |
|     func testTextNormalization() {
 | |
|         XCTAssertEqual(FullTextSearchFinder.normalize(text: "Liza"), "Liza")
 | |
|         XCTAssertEqual(FullTextSearchFinder.normalize(text: "Liza +1-323"), "Liza 1323")
 | |
|         XCTAssertEqual(FullTextSearchFinder.normalize(text: "\"\\ `~!@#$%^&*()_+-={}|[]:;'<>?,./Liza +1-323"), "Liza 1323")
 | |
|         XCTAssertEqual(FullTextSearchFinder.normalize(text: "renaldo RENALDO reñaldo REÑALDO"), "renaldo RENALDO reñaldo REÑALDO")
 | |
|         XCTAssertEqual(FullTextSearchFinder.normalize(text: "😏"), "😏")
 | |
|     }
 | |
| }
 |