Signal-iOS/SignalServiceKit/tests/Contacts/SignalRecipientTest.swift
2025-09-10 18:00:05 -05:00

877 lines
38 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import GRDB
import LibSignalClient
import XCTest
@testable import SignalServiceKit
class SignalRecipientTest: SSKBaseTest {
private lazy var localAci = Aci.randomForTesting()
private lazy var localPhoneNumber = E164("+16505550199")!
private lazy var localIdentifiers = LocalIdentifiers(
aci: localAci,
pni: Pni.randomForTesting(),
phoneNumber: localPhoneNumber.stringValue
)
override func setUp() {
super.setUp()
SSKEnvironment.shared.databaseStorageRef.write { tx in
(DependenciesBridge.shared.registrationStateChangeManager as! RegistrationStateChangeManagerImpl).registerForTests(
localIdentifiers: localIdentifiers,
tx: tx
)
}
}
func testSelfRecipientWithExistingRecord() {
write { transaction in
mergeHighTrust(aci: localAci, phoneNumber: localPhoneNumber, transaction: transaction)
XCTAssertNotNil(fetchRecipient(aci: localAci, transaction: transaction))
XCTAssertNotNil(fetchRecipient(phoneNumber: localPhoneNumber, transaction: transaction))
}
}
func testRecipientWithExistingRecord() {
let aci = Aci.randomForTesting()
let phoneNumber = E164("+16505550101")!
write { transaction in
mergeHighTrust(aci: aci, phoneNumber: phoneNumber, transaction: transaction)
XCTAssertNotNil(fetchRecipient(aci: aci, transaction: transaction))
XCTAssertNotNil(fetchRecipient(phoneNumber: phoneNumber, transaction: transaction))
}
}
// MARK: - Low Trust
func testLowTrustPhoneNumberOnly() {
// Phone number only recipients are recorded
write { tx in
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
let phoneNumber = E164(CommonGenerator.e164())!
_ = recipientFetcher.fetchOrCreate(phoneNumber: phoneNumber, tx: tx)
XCTAssertNotNil(fetchRecipient(phoneNumber: phoneNumber, transaction: tx))
}
}
func testLowTrustUUIDOnly() {
// UUID only recipients are recorded
write { tx in
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
let aci = Aci.randomForTesting()
_ = recipientFetcher.fetchOrCreate(serviceId: aci, tx: tx)
XCTAssertNotNil(fetchRecipient(aci: aci, transaction: tx))
}
}
// MARK: - High Trust
func testHighTrustUUIDOnly() {
// UUID only recipients are recorded
write { transaction in
let aci = Aci.randomForTesting()
_ = mergeHighTrust(aci: aci, phoneNumber: nil, transaction: transaction)
XCTAssertNotNil(fetchRecipient(aci: aci, transaction: transaction))
}
}
func testHighTrustFullyQualified() {
// Fully qualified addresses are recorded in their entirety
let aci = Aci.randomForTesting()
let phoneNumber = E164("+16505550101")!
let addressToBeUpdated = SignalServiceAddress(phoneNumber: phoneNumber.stringValue)
XCTAssertNil(addressToBeUpdated.serviceId)
write { transaction in
let aciProfile = OWSUserProfile.getOrBuildUserProfile(
for: .otherUser(aci),
userProfileWriter: .tests,
tx: transaction
)
aciProfile.anyInsert(transaction: transaction)
aciProfile.update(
isPhoneNumberShared: .setTo(true),
userProfileWriter: .tests,
transaction: transaction
)
let recipient = mergeHighTrust(aci: aci, phoneNumber: phoneNumber, transaction: transaction)
XCTAssertEqual(recipient.aci, aci)
XCTAssertEqual(recipient.phoneNumber?.stringValue, phoneNumber.stringValue)
// The incomplete address is automatically filled after marking the
// complete address as registered.
XCTAssertEqual(addressToBeUpdated.serviceId, aci)
XCTAssertNotNil(fetchRecipient(aci: aci, transaction: transaction))
XCTAssertNotNil(fetchRecipient(phoneNumber: phoneNumber, transaction: transaction))
}
}
func testHighTrustMergeWithInvestedPhoneNumber() {
let recipientDatabaseTable = DependenciesBridge.shared.recipientDatabaseTable
// If there is a UUID-only contact and a phone number-only contact, and if
// we later find out they are the same user, we must merge them.
let aci = Aci.randomForTesting()
let phoneNumber = E164("+16505550101")!
write { tx in
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
let uuidRecipient = recipientFetcher.fetchOrCreate(serviceId: aci, tx: tx)
let phoneNumberRecipient = recipientFetcher.fetchOrCreate(phoneNumber: phoneNumber, tx: tx)
let mergedRecipient = mergeHighTrust(aci: aci, phoneNumber: phoneNumber, transaction: tx)
XCTAssertEqual(mergedRecipient.uniqueId, uuidRecipient.uniqueId)
XCTAssertNil(recipientDatabaseTable.fetchRecipient(uniqueId: phoneNumberRecipient.uniqueId, tx: tx))
}
}
func testHighTrustPhoneNumberChange() {
let aci = Aci.randomForTesting()
let oldPhoneNumber = E164("+16505550101")!
let newPhoneNumber = E164("+16505550102")!
let oldAddress = SignalServiceAddress(serviceId: aci, phoneNumber: oldPhoneNumber.stringValue)
// Do this because of SignalServiceAddressTest.test_hashStability2().
_ = SignalServiceAddress(serviceId: aci, phoneNumber: newPhoneNumber.stringValue)
write { transaction in
let oldThread = TSContactThread.getOrCreateThread(
withContactAddress: oldAddress,
transaction: transaction
)
let messageBuilder: TSIncomingMessageBuilder = .withDefaultValues(
thread: oldThread,
authorAci: aci,
messageBody: AttachmentContentValidatorMock.mockValidatedBody("Test 123")
)
let oldMessage = messageBuilder.build()
oldMessage.anyInsert(transaction: transaction)
let oldPhoneNumberProfile = OWSUserProfile.getOrBuildUserProfile(
for: .otherUser(aci),
userProfileWriter: .tests,
tx: transaction
)
oldPhoneNumberProfile.anyInsert(transaction: transaction)
oldPhoneNumberProfile.update(
isPhoneNumberShared: .setTo(true),
userProfileWriter: .tests,
transaction: transaction
)
let newPhoneNumberProfile = OWSUserProfile(
address: .otherUser(SignalServiceAddress(phoneNumber: newPhoneNumber.stringValue))
)
newPhoneNumberProfile.anyInsert(transaction: transaction)
let oldAccount = SignalAccount(phoneNumber: oldAddress.phoneNumber!)
oldAccount.anyInsert(transaction: transaction)
mergeHighTrust(aci: aci, phoneNumber: oldPhoneNumber, transaction: transaction)
mergeHighTrust(aci: aci, phoneNumber: newPhoneNumber, transaction: transaction)
let newAddress = SignalServiceAddress(serviceId: aci, phoneNumber: newPhoneNumber.stringValue)
let newThread = TSContactThread.getOrCreateThread(
withContactAddress: newAddress,
transaction: transaction
)
let newMessage = TSIncomingMessage.anyFetchIncomingMessage(
uniqueId: oldMessage.uniqueId,
transaction: transaction
)!
let newProfile = OWSUserProfile.getOrBuildUserProfile(
for: .otherUser(aci),
userProfileWriter: .tests,
tx: transaction
)
let newAccount = SignalAccount.anyFetch(
uniqueId: oldAccount.uniqueId,
transaction: transaction
)
// We maintain the same thread, profile, interactions, etc.
// after the phone number change. They are updated to reflect
// the new address.
XCTAssertEqual(oldAddress.phoneNumber, newAddress.phoneNumber)
XCTAssertEqual(oldAddress.serviceId, newAddress.serviceId)
XCTAssertEqual(oldThread.uniqueId, newThread.uniqueId)
XCTAssertNil(oldThread.contactPhoneNumber)
XCTAssertNil(newThread.contactPhoneNumber)
XCTAssertEqual(newAddress, newThread.contactAddress)
XCTAssertEqual(oldMessage.uniqueId, newMessage.uniqueId)
XCTAssertEqual(newAddress, newMessage.authorAddress)
XCTAssertNil(oldMessage.authorPhoneNumber)
XCTAssertNil(newMessage.authorPhoneNumber)
XCTAssertEqual(newProfile.uniqueId, oldPhoneNumberProfile.uniqueId)
XCTAssertNil(newProfile.phoneNumber)
XCTAssertEqual(newProfile.serviceIdString, aci.serviceIdUppercaseString)
XCTAssertNil(OWSUserProfile.anyFetch(uniqueId: newPhoneNumberProfile.uniqueId, transaction: transaction))
XCTAssertNil(newAccount)
}
}
func testHighTrustUUIDChange() throws {
let oldAci = Aci.randomForTesting()
let newAci = Aci.randomForTesting()
let phoneNumber = E164("+16505550101")!
let oldAddress = SignalServiceAddress(serviceId: oldAci, phoneNumber: phoneNumber.stringValue)
try write { transaction in
var oldThread = TSContactThread.getOrCreateThread(
withContactAddress: oldAddress,
transaction: transaction
)
let messageBuilder: TSIncomingMessageBuilder = .withDefaultValues(
thread: oldThread,
authorAci: oldAci,
messageBody: AttachmentContentValidatorMock.mockValidatedBody("Test 123")
)
let oldMessage = messageBuilder.build()
oldMessage.anyInsert(transaction: transaction)
var oldProfile = OWSUserProfile(address: .otherUser(oldAddress))
oldProfile.anyInsert(transaction: transaction)
oldProfile.update(
isPhoneNumberShared: .setTo(true),
userProfileWriter: .tests,
transaction: transaction
)
let oldAccount = SignalAccount(phoneNumber: oldAddress.phoneNumber!)
oldAccount.anyInsert(transaction: transaction)
mergeHighTrust(aci: oldAci, phoneNumber: phoneNumber, transaction: transaction)
mergeHighTrust(aci: newAci, phoneNumber: phoneNumber, transaction: transaction)
let newAddress = SignalServiceAddress(serviceId: newAci, phoneNumber: phoneNumber.stringValue)
let newThread = TSContactThread.getOrCreateThread(
withContactAddress: newAddress,
transaction: transaction
)
let newMessage = TSIncomingMessage.anyFetchIncomingMessage(
uniqueId: oldMessage.uniqueId,
transaction: transaction
)!
let newProfile = OWSUserProfile.getOrBuildUserProfile(
for: .otherUser(newAci),
userProfileWriter: .tests,
tx: transaction
)
let newAccount = SignalAccount.anyFetch(
uniqueId: oldAccount.uniqueId,
transaction: transaction
)!
// When the UUID changes, we treat it as a new account. Old data
// should remain associated with the old UUID, but have the phone
// number stripped.
XCTAssertNil(oldAddress.phoneNumber)
XCTAssertNotEqual(oldAddress.phoneNumber, newAddress.phoneNumber)
XCTAssertNotEqual(oldAddress.serviceId, newAddress.serviceId)
oldThread = TSContactThread.anyFetchContactThread(uniqueId: oldThread.uniqueId, transaction: transaction)!
XCTAssertNotEqual(oldThread.uniqueId, newThread.uniqueId)
XCTAssertNil(oldThread.contactPhoneNumber)
XCTAssertEqual(newAddress, newThread.contactAddress)
XCTAssertNotEqual(newAddress, oldThread.contactAddress)
XCTAssertEqual(oldMessage.uniqueId, newMessage.uniqueId)
XCTAssertNil(newMessage.authorPhoneNumber)
XCTAssertNotEqual(newAddress, newMessage.authorAddress)
oldProfile = try XCTUnwrap(OWSUserProfile.anyFetch(uniqueId: oldProfile.uniqueId, transaction: transaction))
XCTAssertNotEqual(oldProfile.uniqueId, newProfile.uniqueId)
XCTAssertNil(oldProfile.phoneNumber)
XCTAssertEqual(.otherUser(newAddress), newProfile.internalAddress)
XCTAssertNotEqual(.otherUser(newAddress), oldProfile.internalAddress)
XCTAssertEqual(newAccount.uniqueId, oldAccount.uniqueId)
XCTAssertEqual(newAccount.recipientPhoneNumber, phoneNumber.stringValue)
XCTAssertEqual(newAccount.recipientServiceId, newAci)
}
}
private func createGroupAndThreads(for addresses: [(aci: Aci?, phoneNumber: E164?)]) -> TSGroupThread {
return self.write { (tx) -> TSGroupThread in
// Create a group with all the addresses.
let groupThread = {
return try! GroupManager.createGroupForTests(
members: addresses.map { SignalServiceAddress(serviceId: $0.aci, phoneNumber: $0.phoneNumber?.stringValue) },
transaction: tx
)
}()
// Delete the group members that were created automatically.
try! TSGroupMember.deleteAll(tx.database)
// And construct the TSGroupMember members using the specific identifiers
// that were provided.
for address in addresses {
let groupMember = TSGroupMember(
address: NormalizedDatabaseRecordAddress(
aci: address.aci,
phoneNumber: address.phoneNumber?.stringValue,
pni: nil
)!,
groupThreadId: groupThread.uniqueId,
lastInteractionTimestamp: NSDate.ows_millisecondTimeStamp()
)
groupMember.anyInsert(transaction: tx)
TSContactThread.getOrCreateThread(
withContactAddress: SignalServiceAddress(serviceId: address.aci, phoneNumber: address.phoneNumber?.stringValue),
transaction: tx
)
}
return groupThread
}
}
private func assertEqual(groupMembers: [TSGroupMember], expectedAddresses: [(aci: Aci?, phoneNumber: E164?)]) {
let actualValues = Set(groupMembers.lazy.map {
"\($0.serviceId?.serviceIdUppercaseString ?? "nil")-\($0.phoneNumber ?? "nil")"
})
let expectedValues = Set(expectedAddresses.lazy.map {
"\($0.aci?.serviceIdUppercaseString ?? "nil")-\($0.phoneNumber?.stringValue ?? "nil")"
})
XCTAssertEqual(actualValues, expectedValues)
}
// This tests an edge case around uuid<->phone number mapping changes.
//
// * There _is_ a SignalRecipient with (u1, p1).
// * There _is no_ SignalRecipient with u2 or p2.
// * There is a group g1.
// * There is a TSGroupMember with (u1, p1, g1).
// * There is a TSGroupMember with (u2, p2, g1).
// * p2 becomes associated with u1.
//
// Therefore:
//
// * p2 must be mapped from u2 -> u1, but there is no existing phoneNumberInstance for p2.
// * SignalRecipient.clearDBMappings(forPhoneNumber:) must clean up p2 before
// SignalRecipient.changePhoneNumber() is called on uuidInstance for u1.
// * Otherwise we'll end up with two TSGroupMembers with (p2, g1) which violates
// a uniqueness constraint.
func testDBMappingsEdgeCase1() {
let aci1 = Aci.randomForTesting()
let phoneNumber1 = E164(CommonGenerator.e164())!
let aci2 = Aci.randomForTesting()
let phoneNumber2 = E164(CommonGenerator.e164())!
let groupThread = createGroupAndThreads(for: [
(aci: aci1, phoneNumber: phoneNumber1),
(aci: aci2, phoneNumber: phoneNumber2)
])
write { tx in
mergeHighTrust(aci: aci1, phoneNumber: phoneNumber1, transaction: tx)
// phoneNumber2 becomes associated with aci1.
mergeHighTrust(aci: aci1, phoneNumber: phoneNumber2, transaction: tx)
}
SSKEnvironment.shared.databaseStorageRef.read { tx in
XCTAssertEqual(TSThread.anyCount(transaction: tx), 3)
}
let finalGroupMembers = SSKEnvironment.shared.databaseStorageRef.read { tx in
GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx)
}
// We should still have two group members: (u1, p2) and (u2, nil).
assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
(aci: aci1, phoneNumber: nil),
(aci: aci2, phoneNumber: nil)
])
}
// This tests an edge case around uuid<->phone number mapping changes.
//
// * There _is_ a SignalRecipient with (u1, p1).
// * There _is no_ SignalRecipient with u2 or p2.
// * There is a group g1.
// * There is a TSGroupMember with (u1, p1, g1).
// * There is a TSGroupMember with (nil, p2, g1).
// * p2 becomes associated with u1.
//
// Therefore:
//
// * p2 must be mapped to u1, but there is no existing phoneNumberInstance for p2.
// * SignalRecipient.clearDBMappings(forPhoneNumber:) must clean up p2 before
// SignalRecipient.changePhoneNumber() is called on uuidInstance for u1.
// * Otherwise we'll end up with two TSGroupMembers with (p2, g1) which violates
// a uniqueness constraint.
func testDBMappingsEdgeCase2() {
let aci1 = Aci.randomForTesting()
let phoneNumber1 = E164(CommonGenerator.e164())!
let phoneNumber2 = E164(CommonGenerator.e164())!
let groupThread = createGroupAndThreads(for: [
(aci: aci1, phoneNumber: phoneNumber1),
(aci: nil, phoneNumber: phoneNumber2)
])
write { tx in
mergeHighTrust(aci: aci1, phoneNumber: phoneNumber1, transaction: tx)
// phoneNumber2 becomes associated with aci1.
mergeHighTrust(aci: aci1, phoneNumber: phoneNumber2, transaction: tx)
}
SSKEnvironment.shared.databaseStorageRef.read { tx in
XCTAssertEqual(TSThread.anyCount(transaction: tx), 2)
}
let finalGroupMembers = SSKEnvironment.shared.databaseStorageRef.read { tx in
GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx)
}
// We should now have one group member: (u1, p2).
assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
(aci: aci1, phoneNumber: nil)
])
}
// This tests an edge case around uuid<->phone number mapping changes.
//
// * There _is_ a SignalRecipient with (u1, p1).
// * There _is no_ SignalRecipient with u2 or p2.
// * There is a group g1.
// * There is a TSGroupMember with (u1, p1, g1).
// * There is a TSGroupMember with (u2, p2, g1).
// * p1 becomes associated with u2.
//
// Therefore:
//
// * p1 must be mapped from u1 -> u2, but there is no existing uuidInstance for u2.
// * SignalRecipient.clearDBMappings(forUuid:) must clean up u2 before
// SignalRecipient.changePhoneNumber() is called on phoneNumberInstance for p1.
// * Otherwise we'll end up with two TSGroupMembers with (p1, g1) which violates
// a uniqueness constraint.
func testDBMappingsEdgeCase3() {
let aci1 = Aci.randomForTesting()
let phoneNumber1 = E164(CommonGenerator.e164())!
let aci2 = Aci.randomForTesting()
let phoneNumber2 = E164(CommonGenerator.e164())!
let groupThread = createGroupAndThreads(for: [
(aci: aci1, phoneNumber: phoneNumber1),
(aci: aci2, phoneNumber: phoneNumber2)
])
write { tx in
mergeHighTrust(aci: aci1, phoneNumber: phoneNumber1, transaction: tx)
// phoneNumber1 becomes associated with aci2.
mergeHighTrust(aci: aci2, phoneNumber: phoneNumber1, transaction: tx)
}
SSKEnvironment.shared.databaseStorageRef.read { tx in
XCTAssertEqual(TSThread.anyCount(transaction: tx), 3)
}
let finalGroupMembers = SSKEnvironment.shared.databaseStorageRef.read { tx in
GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx)
}
// We should still have two group members: (u2, p1) and (u1, nil).
assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
(aci: aci2, phoneNumber: nil),
(aci: aci1, phoneNumber: nil)
])
}
// This tests an edge case around uuid<->phone number mapping changes.
//
// * There _is_ a SignalRecipient with (u1, p1).
// * There _is no_ SignalRecipient with u2 or p2.
// * There is a group g1.
// * There is a TSGroupMember with (u1, p1, g1).
// * There is a TSGroupMember with (u2, nil, g1).
// * p1 becomes associated with u2.
//
// Therefore:
//
// * p1 must be mapped from u1 -> u2, but there is no existing uuidInstance for u2.
// * SignalRecipient.clearDBMappings(forUuid:) must clean up u2 before
// SignalRecipient.changePhoneNumber() is called on phoneNumberInstance for p1.
// * Otherwise we'll end up with two TSGroupMembers with (p1, g1) which violates
// a uniqueness constraint.
func testDBMappingsEdgeCase4() {
let aci1 = Aci.randomForTesting()
let phoneNumber1 = E164(CommonGenerator.e164())!
let aci2 = Aci.randomForTesting()
let groupThread = createGroupAndThreads(for: [
(aci: aci1, phoneNumber: phoneNumber1),
(aci: aci2, phoneNumber: nil)
])
write { tx in
mergeHighTrust(aci: aci1, phoneNumber: phoneNumber1, transaction: tx)
// phoneNumber1 becomes associated with aci2.
mergeHighTrust(aci: aci2, phoneNumber: phoneNumber1, transaction: tx)
}
SSKEnvironment.shared.databaseStorageRef.read { tx in
XCTAssertEqual(TSThread.anyCount(transaction: tx), 3)
}
let finalGroupMembers = SSKEnvironment.shared.databaseStorageRef.read { tx in
GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx)
}
// We should now have two group members: (u2, p1), (u1, nil).
assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
(aci: aci1, phoneNumber: nil),
(aci: aci2, phoneNumber: nil)
])
}
/// This tests an edge case around groups & merging contacts.
///
/// If a phone number and UUID are both part of a group, and if we later
/// learn that they refer to the same account, we'll end up with a
/// "duplicate" group member.
///
/// This should only be possible in GV1 groups -- in GV2, every member
/// should have a UUID, and that requirement would put us back in
/// testDBMappingsEdgeCase4 territory.
func testDBMappingsEdgeCase5() {
let aci1 = Aci.randomForTesting()
let phoneNumber1 = E164(CommonGenerator.e164())!
let groupThread = createGroupAndThreads(for: [
(aci: aci1, phoneNumber: nil),
(aci: nil, phoneNumber: phoneNumber1)
])
write { tx in
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
_ = recipientFetcher.fetchOrCreate(phoneNumber: phoneNumber1, tx: tx)
mergeHighTrust(aci: aci1, phoneNumber: nil, transaction: tx)
mergeHighTrust(aci: aci1, phoneNumber: phoneNumber1, transaction: tx)
}
SSKEnvironment.shared.databaseStorageRef.read { tx in
XCTAssertEqual(TSThread.anyCount(transaction: tx), 2)
}
let finalGroupMembers = SSKEnvironment.shared.databaseStorageRef.read { tx in
GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx)
}
assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
(aci: aci1, phoneNumber: nil)
])
}
/// This tests an edge case around groups & merging contacts.
///
/// If we merge an ACI & E164 into a single recipient, and then if another
/// account claims that phone number, we should ensure that the original ACI
/// is still in the group but the new ACI is not.
func testDBMappingsEdgeCase6() {
let aci1 = Aci.randomForTesting()
let phoneNumber1 = E164(CommonGenerator.e164())!
let aci2 = Aci.randomForTesting()
let groupThread = createGroupAndThreads(for: [
(aci: aci1, phoneNumber: nil),
(aci: nil, phoneNumber: phoneNumber1)
])
write { tx in
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
_ = recipientFetcher.fetchOrCreate(phoneNumber: phoneNumber1, tx: tx)
mergeHighTrust(aci: aci1, phoneNumber: nil, transaction: tx)
mergeHighTrust(aci: aci1, phoneNumber: phoneNumber1, transaction: tx)
mergeHighTrust(aci: aci2, phoneNumber: phoneNumber1, transaction: tx)
}
SSKEnvironment.shared.databaseStorageRef.read { tx in
XCTAssertEqual(TSThread.anyCount(transaction: tx), 2)
}
let finalGroupMembers = SSKEnvironment.shared.databaseStorageRef.read { tx in
GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx)
}
assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
(aci: aci1, phoneNumber: nil)
])
}
// MARK: - Helpers
@discardableResult
private func mergeHighTrust(aci: Aci, phoneNumber: E164?, transaction tx: DBWriteTransaction) -> SignalRecipient {
let recipientMerger = DependenciesBridge.shared.recipientMerger
return recipientMerger.applyMergeFromContactSync(
localIdentifiers: localIdentifiers,
aci: aci,
phoneNumber: phoneNumber,
tx: tx
)
}
private func fetchRecipient(aci: Aci, transaction tx: DBReadTransaction) -> SignalRecipient? {
return DependenciesBridge.shared.recipientDatabaseTable
.fetchRecipient(serviceId: aci, transaction: tx)
}
private func fetchRecipient(phoneNumber: E164, transaction tx: DBReadTransaction) -> SignalRecipient? {
return DependenciesBridge.shared.recipientDatabaseTable
.fetchRecipient(phoneNumber: phoneNumber.stringValue, transaction: tx)
}
}
final class SignalRecipient2Test: XCTestCase {
private enum Constants {
static let emptyDevices = ""
}
func testDecodeStableRow() throws {
let inMemoryDB = InMemoryDB()
try inMemoryDB.write { tx in
try tx.database.execute(sql: """
INSERT INTO "model_SignalRecipient" (
"id", "recordType", "uniqueId", "devices", "recipientPhoneNumber", "recipientUUID", "unregisteredAtTimestamp"
) VALUES (
18,
31,
'00000000-0000-4000-8000-00000000000A',
X'0102050406',
'+16505550100',
'00000000-0000-4000-8000-000000000000',
NULL
),
(
21,
31,
'00000000-0000-4000-8000-00000000000B',
X'\(Constants.emptyDevices)',
'+16505550101',
'00000000-0000-4000-8000-000000000001',
1683679214631
);
""")
}
inMemoryDB.read { tx in
let signalRecipients = try! SignalRecipient.fetchAll(tx.database)
XCTAssertEqual(signalRecipients.count, 2)
XCTAssertEqual(signalRecipients[0].id, 18)
XCTAssertEqual(signalRecipients[0].uniqueId, "00000000-0000-4000-8000-00000000000A")
XCTAssertEqual(signalRecipients[0].deviceIds, [1, 2, 5, 4, 6].map { DeviceId(validating: $0)! })
XCTAssertEqual(signalRecipients[0].phoneNumber?.stringValue, "+16505550100")
XCTAssertEqual(signalRecipients[0].phoneNumber?.isDiscoverable, false)
XCTAssertEqual(signalRecipients[0].aciString, "00000000-0000-4000-8000-000000000000")
XCTAssertEqual(signalRecipients[0].unregisteredAtTimestamp, nil)
XCTAssertEqual(signalRecipients[1].id, 21)
XCTAssertEqual(signalRecipients[1].uniqueId, "00000000-0000-4000-8000-00000000000B")
XCTAssertEqual(signalRecipients[1].deviceIds, [])
XCTAssertEqual(signalRecipients[1].phoneNumber?.stringValue, "+16505550101")
XCTAssertEqual(signalRecipients[1].phoneNumber?.isDiscoverable, false)
XCTAssertEqual(signalRecipients[1].aciString, "00000000-0000-4000-8000-000000000001")
XCTAssertEqual(signalRecipients[1].unregisteredAtTimestamp, 1683679214631)
}
}
func testDecodePni() throws {
let inMemoryDB = InMemoryDB()
try inMemoryDB.write { tx in
try tx.database.execute(sql: """
INSERT INTO "model_SignalRecipient" (
"id", "recordType", "uniqueId", "devices", "pni"
) VALUES (
1,
31,
'00000000-0000-4000-8000-000000000000',
X'\(Constants.emptyDevices)',
'PNI:10000000-2000-4000-8000-300000000004'
);
""")
}
inMemoryDB.read { tx in
let signalRecipients = try! SignalRecipient.fetchAll(tx.database)
XCTAssertEqual(signalRecipients.count, 1)
XCTAssertEqual(signalRecipients[0].id, 1)
XCTAssertEqual(signalRecipients[0].uniqueId, "00000000-0000-4000-8000-000000000000")
XCTAssertEqual(signalRecipients[0].deviceIds, [])
XCTAssertEqual(signalRecipients[0].pni, Pni.constantForTesting("PNI:10000000-2000-4000-8000-300000000004"))
}
}
func testEncodePni() throws {
let inMemoryDB = InMemoryDB()
let pni = Pni.constantForTesting("PNI:30000000-5000-4000-8000-3000000000A9")
inMemoryDB.insert(record: SignalRecipient(aci: nil, pni: pni, phoneNumber: nil))
inMemoryDB.read { tx in
let db = tx.database
let rawPniValue = try! String.fetchOne(db, sql: #"SELECT "pni" FROM "model_SignalRecipient""#)!
XCTAssertEqual(rawPniValue, pni.serviceIdUppercaseString)
}
}
func testEqualityAndHashing() {
let someRecipient = SignalRecipient(aci: Aci.randomForTesting(), pni: nil, phoneNumber: nil, deviceIds: [1, 2].map { DeviceId(validating: $0)! })
let copiedRecipient = someRecipient.copyRecipient()
XCTAssertEqual(copiedRecipient, someRecipient)
XCTAssertEqual(copiedRecipient.hashValue, someRecipient.hashValue)
XCTAssertEqual(Set([someRecipient, copiedRecipient]).count, 1)
}
func testUnregisteredTimestamps() {
let aci = Aci.randomForTesting()
let mockDb = InMemoryDB()
let recipientTable = RecipientDatabaseTable()
let recipientFetcher = RecipientFetcherImpl(
recipientDatabaseTable: recipientTable,
searchableNameIndexer: MockSearchableNameIndexer(),
)
let recipientManager = SignalRecipientManagerImpl(
phoneNumberVisibilityFetcher: MockPhoneNumberVisibilityFetcher(),
recipientDatabaseTable: recipientTable,
storageServiceManager: FakeStorageServiceManager()
)
mockDb.write { tx in
let recipient = recipientFetcher.fetchOrCreate(serviceId: aci, tx: tx)
XCTAssertNotNil(recipient.unregisteredAtTimestamp)
recipientManager.markAsRegisteredAndSave(recipient, shouldUpdateStorageService: false, tx: tx)
XCTAssertNil(recipientTable.fetchRecipient(serviceId: aci, transaction: tx)!.unregisteredAtTimestamp)
recipientManager.markAsUnregisteredAndSave(recipient, unregisteredAt: .now, shouldUpdateStorageService: false, tx: tx)
XCTAssertGreaterThan(recipientTable.fetchRecipient(serviceId: aci, transaction: tx)!.unregisteredAtTimestamp!, 0)
recipientManager.markAsRegisteredAndSave(recipient, shouldUpdateStorageService: false, tx: tx)
XCTAssertNil(recipientTable.fetchRecipient(serviceId: aci, transaction: tx)!.unregisteredAtTimestamp)
}
}
func testMarkAsRegistered() {
struct TestCase {
var initialDeviceIds: Set<UInt32>
var addedDeviceId: UInt32
var expectedDeviceIds: Set<UInt32>
}
let testCases: [TestCase] = [
TestCase(initialDeviceIds: [], addedDeviceId: 1, expectedDeviceIds: [1]),
TestCase(initialDeviceIds: [], addedDeviceId: 2, expectedDeviceIds: [1, 2]),
TestCase(initialDeviceIds: [1], addedDeviceId: 1, expectedDeviceIds: [1]),
TestCase(initialDeviceIds: [1], addedDeviceId: 2, expectedDeviceIds: [1, 2]),
TestCase(initialDeviceIds: [2], addedDeviceId: 1, expectedDeviceIds: [1, 2]),
TestCase(initialDeviceIds: [2], addedDeviceId: 2, expectedDeviceIds: [1, 2]),
TestCase(initialDeviceIds: [3], addedDeviceId: 1, expectedDeviceIds: [1, 3]),
TestCase(initialDeviceIds: [3], addedDeviceId: 2, expectedDeviceIds: [1, 2, 3]),
TestCase(initialDeviceIds: [1, 2], addedDeviceId: 1, expectedDeviceIds: [1, 2]),
TestCase(initialDeviceIds: [1, 2], addedDeviceId: 2, expectedDeviceIds: [1, 2]),
TestCase(initialDeviceIds: [1, 2, 3], addedDeviceId: 1, expectedDeviceIds: [1, 2, 3]),
TestCase(initialDeviceIds: [1, 2, 3], addedDeviceId: 2, expectedDeviceIds: [1, 2, 3])
]
let mockDb = InMemoryDB()
let recipientTable = RecipientDatabaseTable()
let recipientFetcher = RecipientFetcherImpl(
recipientDatabaseTable: recipientTable,
searchableNameIndexer: MockSearchableNameIndexer(),
)
let recipientManager = SignalRecipientManagerImpl(
phoneNumberVisibilityFetcher: MockPhoneNumberVisibilityFetcher(),
recipientDatabaseTable: recipientTable,
storageServiceManager: FakeStorageServiceManager()
)
mockDb.write { tx in
for testCase in testCases {
let recipient = recipientFetcher.fetchOrCreate(serviceId: Aci.randomForTesting(), tx: tx)
recipientManager.setDeviceIds(Set(testCase.initialDeviceIds.map { DeviceId(validating: $0)! }), for: recipient, shouldUpdateStorageService: false)
recipientManager.markAsRegisteredAndSave(recipient, deviceId: DeviceId(validating: testCase.addedDeviceId)!, shouldUpdateStorageService: false, tx: tx)
XCTAssertEqual(Set(recipient.deviceIds), Set(testCase.expectedDeviceIds.map { DeviceId(validating: $0)! }), "\(testCase)")
}
}
}
func testDeDupe() throws {
let databaseQueue = DatabaseQueue()
try databaseQueue.write { db in
try db.execute(
sql: """
CREATE
TABLE
IF NOT EXISTS "model_SignalRecipient" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
,"recipientPhoneNumber" TEXT
,"recipientUUID" TEXT
)
;
"""
)
let aci1 = Aci.constantForTesting("00000000-0000-4000-8000-00000000000A")
let aci2 = Aci.constantForTesting("00000000-0000-4000-8000-00000000000B")
let aci3 = Aci.constantForTesting("00000000-0000-4000-8000-00000000000C")
let phoneNumber1 = "+16505550101"
let phoneNumber2 = "+16505550102"
let phoneNumber3 = "+16505550103"
try db.execute(
sql: "INSERT INTO model_SignalRecipient (recipientUUID) VALUES (?)",
arguments: [aci1.serviceIdUppercaseString],
)
try db.execute(
sql: "INSERT INTO model_SignalRecipient (recipientUUID, recipientPhoneNumber) VALUES (?, ?)",
arguments: [aci1.serviceIdUppercaseString, phoneNumber1],
)
try db.execute(
sql: "INSERT INTO model_SignalRecipient (recipientPhoneNumber) VALUES (?)",
arguments: [phoneNumber1],
)
try db.execute(
sql: "INSERT INTO model_SignalRecipient (recipientUUID, recipientPhoneNumber) VALUES (?, ?)",
arguments: [aci2.serviceIdUppercaseString, phoneNumber2],
)
try db.execute(
sql: "INSERT INTO model_SignalRecipient (recipientUUID) VALUES (?)",
arguments: [aci2.serviceIdUppercaseString],
)
try db.execute(
sql: "INSERT INTO model_SignalRecipient (recipientUUID, recipientPhoneNumber) VALUES (?, ?)",
arguments: [aci3.serviceIdUppercaseString, phoneNumber2],
)
try db.execute(
sql: "INSERT INTO model_SignalRecipient (recipientPhoneNumber) VALUES (?)",
arguments: [phoneNumber3],
)
try db.execute(
sql: "INSERT INTO model_SignalRecipient (recipientPhoneNumber) VALUES (?)",
arguments: [phoneNumber3],
)
do {
let tx = DBWriteTransaction(database: db)
defer { tx.finalizeTransaction() }
try GRDBSchemaMigrator.dedupeSignalRecipients(tx: tx)
}
let recipientIds = try Int64.fetchAll(db, sql: "SELECT id FROM model_SignalRecipient")
XCTAssertEqual(recipientIds, [1, 4, 6, 7])
let phoneNumbers = try (String?).fetchAll(db, sql: "SELECT recipientPhoneNumber FROM model_SignalRecipient ORDER BY id")
XCTAssertEqual(phoneNumbers, [nil, phoneNumber2, nil, phoneNumber3])
let aciStrings = try (String?).fetchAll(db, sql: "SELECT recipientUUID FROM model_SignalRecipient ORDER BY id")
XCTAssertEqual(aciStrings, [aci1.serviceIdUppercaseString, aci2.serviceIdUppercaseString, aci3.serviceIdUppercaseString, nil])
}
}
}