Signal-iOS/Signal/test/contact/OWSContactsManagerTest.swift
George Nachman cf19dcd5b2 Add cachedContactNames(for:, transaction:)
This stacks on top of PR #4054.

The purpose of this PR is to fetch cached contact
names in a batch. We can fetch their accounts in a
single SQL statement and in the case where many
contacts don't have SignalAccounts, this allows us
to fetch their phone numbers in on SQL statement
as well. There are three cases to consider:

1. There is a signal account: then we just use the
   full name on the account.
2. There is a contact we can find by phone number.
   Then use the contact's full name.
3. Fail. Other steps will be needed to determine
   the full name, such as fetching profiles.
2022-03-17 12:52:45 -07:00

344 lines
15 KiB
Swift

//
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
//
import XCTest
@testable import Signal
@testable import SignalMessaging
@testable import SignalServiceKit
class OWSContactsManagerTest: SignalBaseTest {
private lazy var localAddress = CommonGenerator.address()
override func setUp() {
super.setUp()
// Replace the fake contacts manager with the real one just for this test.
(MockSSKEnvironment.shared as! MockSSKEnvironment).setContactsManagerForMock(OWSContactsManager())
// Create local account.
tsAccountManager.registerForTests(withLocalNumber: localAddress.phoneNumber!,
uuid: localAddress.uuid!)
}
override func tearDown() {
super.tearDown()
(MockSSKEnvironment.shared as! MockSSKEnvironment).setContactsManagerForMock(FakeContactsManager())
let fakeProfileManager = (self.profileManager as! OWSFakeProfileManager)
fakeProfileManager.fakeDisplayNames = nil
fakeProfileManager.fakeUsernames = nil
}
private func createRecipientsAndAccounts(_ tuples: [(SignalServiceAddress, SignalAccount)]) {
// Create recipients and accounts.
write { transaction in
for (address, _) in tuples {
SignalRecipient.mark(asRegisteredAndGet: address,
trustLevel: .high,
transaction: transaction)
}
for (_, account) in tuples {
account.anyInsert(transaction: transaction)
}
}
}
// MARK: - getPhoneNumber(s)
func testGetPhoneNumberFromAddress() {
read { transaction in
let contactsManager = self.contactsManager as! OWSContactsManager
let actual = contactsManager.phoneNumber(for: SignalServiceAddress(phoneNumber: "+17035559901"),
transaction: transaction)
XCTAssertEqual(actual, "+17035559901")
}
}
func testGetPhoneNumberFromProfile() {
let address = SignalServiceAddress(uuid: UUID())
let account = SignalAccount(contact: nil,
contactAvatarHash: nil,
multipleAccountLabelText: "home",
recipientPhoneNumber: "+17035559901",
recipientUUID: address.uuid?.uuidString)
let contactsManager = self.contactsManager as! OWSContactsManager
createRecipientsAndAccounts([(address, account)])
read { transaction in
let actual = contactsManager.phoneNumber(for: address, transaction: transaction)
XCTAssertEqual(actual, "+17035559901")
}
}
func testGetPhoneNumbersFromAddresses() {
let addresses = [SignalServiceAddress(phoneNumber: "+17035559901"),
SignalServiceAddress(phoneNumber: "+17035559902"),
SignalServiceAddress(uuid: UUID())]
read { transaction in
let contactsManager = self.contactsManager as! OWSContactsManager
let actual = contactsManager.phoneNumbers(for: addresses, transaction: transaction)
let expected = ["+17035559901", "+17035559902", nil]
XCTAssertEqual(actual, expected)
}
}
private func makeAccount(address: SignalServiceAddress,
phoneNumber: String?,
name: String? = nil) -> SignalAccount {
let contact = name.map { name -> Contact in
let parts = name.components(separatedBy: " ")
return Contact(uniqueId: UUID().uuidString,
cnContactId: nil,
firstName: parts[0],
lastName: parts.count > 1 ? parts[1] : nil,
nickname: nil,
fullName: name,
userTextPhoneNumbers: [],
phoneNumberNameMap: [:],
parsedPhoneNumbers: [],
emails: [])
}
return SignalAccount(contact: contact,
contactAvatarHash: nil,
multipleAccountLabelText: "home",
recipientPhoneNumber: phoneNumber,
recipientUUID: address.uuid!.uuidString)
}
func testGetPhoneNumbersFromProfiles() {
let aliceAddress = SignalServiceAddress(uuid: UUID())
let aliceAccount = makeAccount(address: aliceAddress, phoneNumber: "+17035559901")
let bobAddress = SignalServiceAddress(uuid: UUID())
let bobAccount = makeAccount(address: bobAddress, phoneNumber: "+17035559902")
let bogusAddress = SignalServiceAddress(uuid: UUID())
createRecipientsAndAccounts([(aliceAddress, aliceAccount),
(bobAddress, bobAccount)])
read { transaction in
let contactsManager = self.contactsManager as! OWSContactsManager
let actual = contactsManager.phoneNumbers(for: [aliceAddress,
bogusAddress,
bobAddress],
transaction: transaction)
let expected = [aliceAccount.recipientPhoneNumber,
nil,
bobAccount.recipientPhoneNumber]
XCTAssertEqual(actual, expected)
}
}
// MARK: - Display Names
func testGetDisplayNamesWithCachedContactNames() {
let addresses = [SignalServiceAddress(uuid: UUID()),
SignalServiceAddress(uuid: UUID())]
let accounts = [makeAccount(address: addresses[0], phoneNumber: nil, name: "Alice Aliceson"),
makeAccount(address: addresses[1], phoneNumber: nil, name: "Bob Bobson")]
createRecipientsAndAccounts(Array(zip(addresses, accounts)))
read { transaction in
let contactsManager = self.contactsManager as! OWSContactsManager
let actual = contactsManager.displayNames(for: addresses, transaction: transaction)
let expected = ["Alice Aliceson (home)", "Bob Bobson (home)"]
XCTAssertEqual(actual, expected)
}
}
func testGetDisplayNamesWithProfileFullNames() {
let addresses = [SignalServiceAddress(uuid: UUID()),
SignalServiceAddress(uuid: UUID())]
(self.profileManager as! OWSFakeProfileManager).fakeDisplayNames = [
addresses[0]: "Alice Aliceson",
addresses[1]: "Bob Bobson"
]
read { transaction in
let contactsManager = self.contactsManager as! OWSContactsManager
let actual = contactsManager.displayNames(for: addresses, transaction: transaction)
let expected = ["Alice Aliceson",
"Bob Bobson"]
XCTAssertEqual(actual, expected)
}
}
func testGetDisplayNamesWithPhoneNumbers() {
let addresses = [SignalServiceAddress(phoneNumber: "+17035559900"),
SignalServiceAddress(phoneNumber: "+17035559901")]
// Prevent default fake name from being used.
(self.profileManager as! OWSFakeProfileManager).fakeDisplayNames = [:]
read { transaction in
let contactsManager = self.contactsManager as! OWSContactsManager
let actual = contactsManager.displayNames(for: addresses, transaction: transaction)
let expected = ["+17035559900",
"+17035559901"]
XCTAssertEqual(actual, expected)
}
}
func testGetDisplayNamesWithUserNames() {
let addresses = [SignalServiceAddress(uuid: UUID()),
SignalServiceAddress(uuid: UUID())]
let fakeProfileManager = (self.profileManager as! OWSFakeProfileManager)
fakeProfileManager.fakeUsernames = [addresses[0]: "alice",
addresses[1]: "bob"]
// Prevent default fake name from being used.
fakeProfileManager.fakeDisplayNames = [:]
read { transaction in
let contactsManager = self.contactsManager as! OWSContactsManager
let actual = contactsManager.displayNames(for: addresses, transaction: transaction)
let expected = ["@alice",
"@bob"]
XCTAssertEqual(actual, expected)
}
}
func testGetDisplayNamesUnknown() {
let addresses = [SignalServiceAddress(uuid: UUID()),
SignalServiceAddress(uuid: UUID())]
let fakeProfileManager = (self.profileManager as! OWSFakeProfileManager)
// Prevent default fake name from being used.
fakeProfileManager.fakeDisplayNames = [:]
read { transaction in
let contactsManager = self.contactsManager as! OWSContactsManager
let actual = contactsManager.displayNames(for: addresses, transaction: transaction)
let expected = ["Unknown",
"Unknown"]
XCTAssertEqual(actual, expected)
}
}
func testGetDisplayNamesMixed() {
let aliceAddress = SignalServiceAddress(uuid: UUID())
let aliceAccount = makeAccount(address: aliceAddress, phoneNumber: nil, name: "Alice Aliceson")
createRecipientsAndAccounts([(aliceAddress, aliceAccount)])
let bobAddress = SignalServiceAddress(uuid: UUID())
(self.profileManager as! OWSFakeProfileManager).fakeDisplayNames = [bobAddress: "Bob Bobson"]
let carolAddress = SignalServiceAddress(phoneNumber: "+17035559900")
let daveAddress = SignalServiceAddress(uuid: UUID())
let fakeProfileManager = (self.profileManager as! OWSFakeProfileManager)
fakeProfileManager.fakeUsernames = [daveAddress: "dave"]
let eveAddress = SignalServiceAddress(uuid: UUID())
read { transaction in
let contactsManager = self.contactsManager as! OWSContactsManager
let addresses = [aliceAddress,
bobAddress,
carolAddress,
daveAddress,
eveAddress]
let actual = contactsManager.displayNames(for: addresses, transaction: transaction)
let expected = ["Alice Aliceson (home)",
"Bob Bobson",
"+17035559900",
"@dave",
"Unknown"]
XCTAssertEqual(actual, expected)
}
}
func testSinglePartName() {
let addresses = [SignalServiceAddress(uuid: UUID()),
SignalServiceAddress(uuid: UUID())]
let accounts = [makeAccount(address: addresses[0], phoneNumber: nil, name: "Alice"),
makeAccount(address: addresses[1], phoneNumber: nil, name: "Bob")]
createRecipientsAndAccounts(Array(zip(addresses, accounts)))
read { transaction in
let contactsManager = self.contactsManager as! OWSContactsManager
let actual = contactsManager.displayNames(for: addresses, transaction: transaction)
let expected = ["Alice (home)", "Bob (home)"]
XCTAssertEqual(actual, expected)
}
}
// MARK: - Cached Contact Names
func testCachedContactNamesWithAccounts() {
let addresses = [SignalServiceAddress(uuid: UUID()),
SignalServiceAddress(uuid: UUID())]
let accounts = [makeAccount(address: addresses[0], phoneNumber: nil, name: "Alice Aliceson"),
makeAccount(address: addresses[1], phoneNumber: nil, name: "Bob Bobson")]
let contactsManager = OWSContactsManager()
createRecipientsAndAccounts(Array(zip(addresses, accounts)))
read { transaction in
let actual = contactsManager.cachedContactNames(for: AnySequence(addresses), transaction: transaction)
let expected = ["Alice Aliceson (home)", "Bob Bobson (home)"]
XCTAssertEqual(actual, expected)
}
}
private func contact(name: String, phoneNumber: String?) -> Contact {
Contact(uniqueId: String(name.data(using: .utf8)!.hashValue),
cnContactId: nil,
firstName: nil,
lastName: nil,
nickname: nil,
fullName: name,
userTextPhoneNumbers: [phoneNumber].compactMap { $0 },
phoneNumberNameMap: [:],
parsedPhoneNumbers: [phoneNumber.map { PhoneNumber(fromE164: $0)! }].compactMap { $0 },
emails: [])
}
private func saveContacts(_ contacts: [Contact]) {
let phoneNumberStore = SDSKeyValueStore(collection: "ContactsManagerCache.phoneNumberStore")
write { transaction in
for contact in contacts {
let data = try! NSKeyedArchiver.archivedData(withRootObject: contact, requiringSecureCoding: false)
phoneNumberStore.setData(data, key: contact.parsedPhoneNumbers[0].toE164(), transaction: transaction)
}
}
}
func testCachedContactNamesWithNonSignalContacts() {
let alicePhoneNumber = "+17035550000"
let bobPhoneNumber = "+17035550001"
let addresses = [SignalServiceAddress(phoneNumber: alicePhoneNumber),
SignalServiceAddress(phoneNumber: bobPhoneNumber)]
saveContacts([contact(name: "Alice Aliceson", phoneNumber: alicePhoneNumber),
contact(name: "Bob Bobson", phoneNumber: bobPhoneNumber)])
read { transaction in
let contactsManager = OWSContactsManager()
let actual = contactsManager.cachedContactNames(for: AnySequence(addresses), transaction: transaction)
let expected = ["Alice Aliceson", "Bob Bobson"]
XCTAssertEqual(actual, expected)
}
}
func testCachedContactNameWithNonSignalContactsLackingPhoneNumbers() {
let addresses = [SignalServiceAddress(uuid: UUID()),
SignalServiceAddress(uuid: UUID())]
let contactsManager = OWSContactsManager()
read { transaction in
let actual = contactsManager.cachedContactNames(for: AnySequence(addresses), transaction: transaction)
XCTAssertEqual(actual, [nil, nil])
}
}
func testCachedContactNameMixed() {
// Register alice with an account that has a full name.
let aliceAddress = SignalServiceAddress(uuid: UUID())
let aliceAccount = makeAccount(address: aliceAddress, phoneNumber: nil, name: "Alice Aliceson")
createRecipientsAndAccounts(Array(zip([aliceAddress], [aliceAccount])))
// Register bob as a non-Signal contact.
let bobPhoneNumber = "+17035550001"
let bobAddress = SignalServiceAddress(phoneNumber: bobPhoneNumber)
saveContacts([contact(name: "Bob Bobson", phoneNumber: bobPhoneNumber)])
// Who the heck is Chuck?
let chuckAddress = SignalServiceAddress(uuid: UUID())
read { transaction in
let addresses = [aliceAddress, bobAddress, chuckAddress]
let contactsManager = OWSContactsManager()
let actual = contactsManager.cachedContactNames(for: AnySequence(addresses), transaction: transaction)
let expected = ["Alice Aliceson (home)", "Bob Bobson", nil]
XCTAssertEqual(actual, expected)
}
}
}