Signal-iOS/SignalServiceKit/tests/Contacts/SignalRecipientTest.swift
2023-04-24 18:45:18 -07:00

655 lines
28 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import XCTest
@testable import SignalServiceKit
class SignalRecipientTest: SSKBaseTestSwift {
private lazy var localAci = ServiceId(UUID())
private lazy var localPhoneNumber = E164("+16505550199")!
private lazy var localIdentifiers = LocalIdentifiers(
aci: localAci,
pni: ServiceId(UUID()),
phoneNumber: localPhoneNumber.stringValue
)
override func setUp() {
super.setUp()
tsAccountManager.registerForTests(withLocalNumber: localIdentifiers.phoneNumber, uuid: localIdentifiers.aci.uuidValue)
}
func testSelfRecipientWithExistingRecord() {
write { transaction in
mergeHighTrust(serviceId: localAci, phoneNumber: localPhoneNumber, transaction: transaction)
XCTAssertNotNil(fetchRecipient(serviceId: localAci, transaction: transaction))
XCTAssertNotNil(fetchRecipient(phoneNumber: localPhoneNumber, transaction: transaction))
}
}
func testRecipientWithExistingRecord() {
let serviceId = ServiceId(UUID())
let phoneNumber = E164("+16505550101")!
write { transaction in
mergeHighTrust(serviceId: serviceId, phoneNumber: phoneNumber, transaction: transaction)
XCTAssertNotNil(fetchRecipient(serviceId: serviceId, 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.asV2Write)
XCTAssertNotNil(fetchRecipient(phoneNumber: phoneNumber, transaction: tx))
}
}
func testLowTrustUUIDOnly() {
// UUID only recipients are recorded
write { tx in
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
let serviceId = ServiceId(UUID())
_ = recipientFetcher.fetchOrCreate(serviceId: serviceId, tx: tx.asV2Write)
XCTAssertNotNil(fetchRecipient(serviceId: serviceId, transaction: tx))
}
}
// MARK: - High Trust
func testHighTrustUUIDOnly() {
// UUID only recipients are recorded
write { transaction in
let serviceId = ServiceId(UUID())
_ = mergeHighTrust(serviceId: serviceId, phoneNumber: nil, transaction: transaction)
XCTAssertNotNil(fetchRecipient(serviceId: serviceId, transaction: transaction))
}
}
func testHighTrustFullyQualified() {
// Fully qualified addresses are recorded in their entirety
let serviceId = ServiceId(UUID())
let phoneNumber = E164("+16505550101")!
let addressToBeUpdated = SignalServiceAddress(phoneNumber: phoneNumber.stringValue)
XCTAssertNil(addressToBeUpdated.serviceId)
write { transaction in
let recipient = mergeHighTrust(serviceId: serviceId, phoneNumber: phoneNumber, transaction: transaction)
XCTAssertEqual(recipient.recipientUUID, serviceId.uuidValue.uuidString)
XCTAssertEqual(recipient.recipientPhoneNumber, phoneNumber.stringValue)
// The incomplete address is automatically filled after marking the
// complete address as registered.
XCTAssertEqual(addressToBeUpdated.serviceId, serviceId)
XCTAssertNotNil(fetchRecipient(serviceId: serviceId, transaction: transaction))
XCTAssertNotNil(fetchRecipient(phoneNumber: phoneNumber, transaction: transaction))
}
}
func testHighTrustMergeWithInvestedPhoneNumber() {
// 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 serviceId = ServiceId(UUID())
let phoneNumber = E164("+16505550101")!
write { tx in
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
let uuidRecipient = recipientFetcher.fetchOrCreate(serviceId: serviceId, tx: tx.asV2Write)
let phoneNumberRecipient = recipientFetcher.fetchOrCreate(phoneNumber: phoneNumber, tx: tx.asV2Write)
let mergedRecipient = mergeHighTrust(serviceId: serviceId, phoneNumber: phoneNumber, transaction: tx)
XCTAssertEqual(mergedRecipient.uniqueId, uuidRecipient.uniqueId)
XCTAssertNil(SignalRecipient.anyFetch(uniqueId: phoneNumberRecipient.uniqueId, transaction: tx))
}
}
func testHighTrustPhoneNumberChange() {
let aci = ServiceId(UUID())
let oldPhoneNumber = E164("+16505550101")!
let newPhoneNumber = E164("+16505550102")!
let oldAddress = SignalServiceAddress(uuid: aci.uuidValue, e164: oldPhoneNumber)
write { transaction in
let oldThread = TSContactThread.getOrCreateThread(
withContactAddress: oldAddress,
transaction: transaction
)
let messageBuilder = TSIncomingMessageBuilder(
thread: oldThread,
authorAddress: oldAddress,
messageBody: "Test 123"
)
let oldMessage = messageBuilder.build()
oldMessage.anyInsert(transaction: transaction)
let oldProfile = OWSUserProfile.getOrBuild(
for: oldAddress,
authedAccount: .implicit(),
transaction: transaction
)
oldProfile.anyInsert(transaction: transaction)
let oldAccount = SignalAccount(address: oldAddress)
oldAccount.anyInsert(transaction: transaction)
mergeHighTrust(serviceId: aci, phoneNumber: oldPhoneNumber, transaction: transaction)
mergeHighTrust(serviceId: aci, phoneNumber: newPhoneNumber, transaction: transaction)
let newAddress = SignalServiceAddress(uuid: aci.uuidValue, e164: newPhoneNumber)
let newThread = TSContactThread.getOrCreateThread(
withContactAddress: newAddress,
transaction: transaction
)
let newMessage = TSIncomingMessage.anyFetchIncomingMessage(
uniqueId: oldMessage.uniqueId,
transaction: transaction
)!
let newProfile = OWSUserProfile.getOrBuild(
for: newAddress,
authedAccount: .implicit(),
transaction: 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.uuid, newAddress.uuid)
XCTAssertEqual(oldThread.uniqueId, newThread.uniqueId)
XCTAssertNotEqual(oldThread.contactPhoneNumber, newThread.contactPhoneNumber)
XCTAssertEqual(newAddress, newThread.contactAddress)
XCTAssertEqual(oldMessage.uniqueId, newMessage.uniqueId)
XCTAssertNotEqual(oldMessage.authorPhoneNumber, newMessage.authorPhoneNumber)
XCTAssertEqual(newAddress, newMessage.authorAddress)
XCTAssertEqual(oldProfile.uniqueId, newProfile.uniqueId)
XCTAssertNotEqual(oldProfile.recipientPhoneNumber, newProfile.recipientPhoneNumber)
XCTAssertEqual(newAddress, newProfile.address)
XCTAssertEqual(oldAccount.uniqueId, newAccount.uniqueId)
XCTAssertNotEqual(oldAccount.recipientPhoneNumber, newAccount.recipientPhoneNumber)
XCTAssertEqual(newAddress, newAccount.recipientAddress)
}
}
func testHighTrustUUIDChange() {
let oldAci = ServiceId(UUID())
let newAci = ServiceId(UUID())
let phoneNumber = E164("+16505550101")!
let oldAddress = SignalServiceAddress(uuid: oldAci.uuidValue, e164: phoneNumber)
write { transaction in
let oldThread = TSContactThread.getOrCreateThread(
withContactAddress: oldAddress,
transaction: transaction
)
let messageBuilder = TSIncomingMessageBuilder(
thread: oldThread,
authorAddress: oldAddress,
messageBody: "Test 123"
)
let oldMessage = messageBuilder.build()
oldMessage.anyInsert(transaction: transaction)
let oldProfile = OWSUserProfile.getOrBuild(
for: oldAddress,
authedAccount: .implicit(),
transaction: transaction
)
oldProfile.anyInsert(transaction: transaction)
let oldAccount = SignalAccount(address: oldAddress)
oldAccount.anyInsert(transaction: transaction)
mergeHighTrust(serviceId: oldAci, phoneNumber: phoneNumber, transaction: transaction)
mergeHighTrust(serviceId: newAci, phoneNumber: phoneNumber, transaction: transaction)
let newAddress = SignalServiceAddress(uuid: newAci.uuidValue, e164: phoneNumber)
let newThread = TSContactThread.getOrCreateThread(
withContactAddress: newAddress,
transaction: transaction
)
let newMessage = TSIncomingMessage.anyFetchIncomingMessage(
uniqueId: oldMessage.uniqueId,
transaction: transaction
)!
let newProfile = OWSUserProfile.getOrBuild(
for: newAddress,
authedAccount: .implicit(),
transaction: 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.uuid, newAddress.uuid)
oldThread.anyReload(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.anyReload(transaction: transaction)
XCTAssertNotEqual(oldProfile.uniqueId, newProfile.uniqueId)
XCTAssertNil(oldProfile.recipientPhoneNumber)
XCTAssertEqual(newAddress, newProfile.address)
XCTAssertNotEqual(newAddress, oldProfile.address)
XCTAssertEqual(oldAccount.uniqueId, newAccount.uniqueId)
XCTAssertNil(newAccount.recipientPhoneNumber)
XCTAssertNotEqual(newAddress, newAccount.recipientAddress)
}
}
private func createGroupAndThreads(for addresses: [(aci: ServiceId?, phoneNumber: E164?)]) -> TSGroupThread {
return self.write { (tx) -> TSGroupThread in
// Create a group with all the addresses.
let groupThread = {
let factory = GroupThreadFactory()
factory.memberAddressesBuilder = {
return addresses.map { SignalServiceAddress(uuid: $0.aci?.uuidValue, e164: $0.phoneNumber) }
}
return factory.create(transaction: tx)
}()
// Delete the group members that were created automatically.
TSGroupMember.anyRemoveAllWithInstantiation(transaction: tx)
// And construct the TSGroupMember members using the specific identifiers
// that were provided.
for address in addresses {
let groupMember = TSGroupMember(
serviceId: address.aci,
phoneNumber: address.phoneNumber?.stringValue,
groupThreadId: groupThread.uniqueId,
lastInteractionTimestamp: NSDate.ows_millisecondTimeStamp()
)
groupMember.anyInsert(transaction: tx)
TSContactThread.getOrCreateThread(
withContactAddress: SignalServiceAddress(uuid: address.aci?.uuidValue, e164: address.phoneNumber),
transaction: tx
)
}
return groupThread
}
}
private func assertEqual(groupMembers: [TSGroupMember], expectedAddresses: [(aci: ServiceId?, phoneNumber: E164?)]) {
let actualValues = Set(groupMembers.lazy.map {
"\($0.serviceId?.uuidValue.uuidString ?? "nil")-\($0.phoneNumber ?? "nil")"
})
let expectedValues = Set(expectedAddresses.lazy.map {
"\($0.aci?.uuidValue.uuidString ?? "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 uuid1 = ServiceId(UUID())
let phoneNumber1 = E164(CommonGenerator.e164())!
let uuid2 = ServiceId(UUID())
let phoneNumber2 = E164(CommonGenerator.e164())!
let groupThread = createGroupAndThreads(for: [
(aci: uuid1, phoneNumber: phoneNumber1),
(aci: uuid2, phoneNumber: phoneNumber2)
])
write { tx in
mergeHighTrust(serviceId: uuid1, phoneNumber: phoneNumber1, transaction: tx)
// phoneNumber2 becomes associated with uuid1.
mergeHighTrust(serviceId: uuid1, phoneNumber: phoneNumber2, transaction: tx)
}
databaseStorage.read { tx in
XCTAssertEqual(TSThread.anyCount(transaction: tx), 3)
}
let finalGroupMembers = databaseStorage.read { tx in
GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx.asV2Read)
}
// We should still have two group members: (u1, p2) and (u2, nil).
assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
(aci: uuid1, phoneNumber: phoneNumber2),
(aci: uuid2, 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 uuid1 = ServiceId(UUID())
let phoneNumber1 = E164(CommonGenerator.e164())!
let phoneNumber2 = E164(CommonGenerator.e164())!
let groupThread = createGroupAndThreads(for: [
(aci: uuid1, phoneNumber: phoneNumber1),
(aci: nil, phoneNumber: phoneNumber2)
])
write { tx in
mergeHighTrust(serviceId: uuid1, phoneNumber: phoneNumber1, transaction: tx)
// phoneNumber2 becomes associated with uuid1.
mergeHighTrust(serviceId: uuid1, phoneNumber: phoneNumber2, transaction: tx)
}
databaseStorage.read { tx in
XCTAssertEqual(TSThread.anyCount(transaction: tx), 3)
}
let finalGroupMembers = databaseStorage.read { tx in
GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx.asV2Read)
}
// We should now have one group member: (u1, p2).
assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
(aci: uuid1, phoneNumber: phoneNumber2)
])
}
// 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 uuid1 = ServiceId(UUID())
let phoneNumber1 = E164(CommonGenerator.e164())!
let uuid2 = ServiceId(UUID())
let phoneNumber2 = E164(CommonGenerator.e164())!
let groupThread = createGroupAndThreads(for: [
(aci: uuid1, phoneNumber: phoneNumber1),
(aci: uuid2, phoneNumber: phoneNumber2)
])
write { tx in
mergeHighTrust(serviceId: uuid1, phoneNumber: phoneNumber1, transaction: tx)
// phoneNumber1 becomes associated with uuid2.
mergeHighTrust(serviceId: uuid2, phoneNumber: phoneNumber1, transaction: tx)
}
databaseStorage.read { tx in
XCTAssertEqual(TSThread.anyCount(transaction: tx), 3)
}
let finalGroupMembers = databaseStorage.read { tx in
GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx.asV2Read)
}
// We should still have two group members: (u2, p1) and (u1, nil).
assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
(aci: uuid2, phoneNumber: phoneNumber1),
(aci: uuid1, 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 uuid1 = ServiceId(UUID())
let phoneNumber1 = E164(CommonGenerator.e164())!
let uuid2 = ServiceId(UUID())
let groupThread = createGroupAndThreads(for: [
(aci: uuid1, phoneNumber: phoneNumber1),
(aci: uuid2, phoneNumber: nil)
])
write { tx in
mergeHighTrust(serviceId: uuid1, phoneNumber: phoneNumber1, transaction: tx)
// phoneNumber1 becomes associated with uuid2.
mergeHighTrust(serviceId: uuid2, phoneNumber: phoneNumber1, transaction: tx)
}
databaseStorage.read { tx in
XCTAssertEqual(TSThread.anyCount(transaction: tx), 3)
}
let finalGroupMembers = databaseStorage.read { tx in
GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx.asV2Read)
}
// We should now have two group members: (u2, p1), (u1, nil).
assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
(aci: uuid1, phoneNumber: nil),
(aci: uuid2, phoneNumber: phoneNumber1)
])
}
/// 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 uuid1 = ServiceId(UUID())
let phoneNumber1 = E164(CommonGenerator.e164())!
let groupThread = createGroupAndThreads(for: [
(aci: uuid1, phoneNumber: nil),
(aci: nil, phoneNumber: phoneNumber1)
])
write { tx in
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
_ = recipientFetcher.fetchOrCreate(phoneNumber: phoneNumber1, tx: tx.asV2Write)
mergeHighTrust(serviceId: uuid1, phoneNumber: nil, transaction: tx)
mergeHighTrust(serviceId: uuid1, phoneNumber: phoneNumber1, transaction: tx)
}
databaseStorage.read { tx in
XCTAssertEqual(TSThread.anyCount(transaction: tx), 3)
}
let finalGroupMembers = databaseStorage.read { tx in
GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx.asV2Read)
}
assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
(aci: uuid1, phoneNumber: phoneNumber1)
])
}
/// 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 uuid1 = ServiceId(UUID())
let phoneNumber1 = E164(CommonGenerator.e164())!
let uuid2 = ServiceId(UUID())
let groupThread = createGroupAndThreads(for: [
(aci: uuid1, phoneNumber: nil),
(aci: nil, phoneNumber: phoneNumber1)
])
write { tx in
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
_ = recipientFetcher.fetchOrCreate(phoneNumber: phoneNumber1, tx: tx.asV2Write)
mergeHighTrust(serviceId: uuid1, phoneNumber: nil, transaction: tx)
mergeHighTrust(serviceId: uuid1, phoneNumber: phoneNumber1, transaction: tx)
mergeHighTrust(serviceId: uuid2, phoneNumber: phoneNumber1, transaction: tx)
}
databaseStorage.read { tx in
XCTAssertEqual(TSThread.anyCount(transaction: tx), 3)
}
let finalGroupMembers = databaseStorage.read { tx in
GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx.asV2Read)
}
assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
(aci: uuid1, phoneNumber: nil)
])
}
func testUnregisteredTimestamps() {
let serviceId = ServiceId(UUID())
write { tx in
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
let recipient = recipientFetcher.fetchOrCreate(serviceId: serviceId, tx: tx.asV2Write)
XCTAssertNotNil(recipient.unregisteredAtTimestamp)
recipient.markAsRegistered(transaction: tx)
XCTAssertNil(fetchRecipient(serviceId: serviceId, transaction: tx)!.unregisteredAtTimestamp)
recipient.markAsUnregistered(transaction: tx)
XCTAssertGreaterThan(fetchRecipient(serviceId: serviceId, transaction: tx)!.unregisteredAtTimestamp!.doubleValue, 0)
recipient.markAsRegistered(transaction: tx)
XCTAssertNil(fetchRecipient(serviceId: serviceId, 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])
]
write { tx in
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
for testCase in testCases {
let recipient = recipientFetcher.fetchOrCreate(serviceId: ServiceId(UUID()), tx: tx.asV2Write)
if !testCase.initialDeviceIds.isEmpty {
recipient.anyUpdate(transaction: tx) {
$0.addDevices(Set(testCase.initialDeviceIds.map { NSNumber(value: $0) }), source: .local)
}
}
recipient.markAsRegistered(deviceId: testCase.addedDeviceId, transaction: tx)
XCTAssertEqual(recipient.deviceIds.map { Set($0) }, testCase.expectedDeviceIds, "\(testCase)")
}
}
}
// MARK: - Helpers
@discardableResult
private func mergeHighTrust(serviceId: ServiceId, phoneNumber: E164?, transaction tx: SDSAnyWriteTransaction) -> SignalRecipient {
let recipientMerger = DependenciesBridge.shared.recipientMerger
return recipientMerger.applyMergeFromLinkedDevice(
localIdentifiers: localIdentifiers,
serviceId: serviceId,
phoneNumber: phoneNumber,
tx: tx.asV2Write
)
}
private func fetchRecipient(serviceId: ServiceId, transaction: SDSAnyReadTransaction) -> SignalRecipient? {
SignalRecipient.get(address: SignalServiceAddress(serviceId), mustHaveDevices: false, transaction: transaction)
}
private func fetchRecipient(phoneNumber: E164, transaction: SDSAnyReadTransaction) -> SignalRecipient? {
SignalRecipient.get(address: SignalServiceAddress(phoneNumber), mustHaveDevices: false, transaction: transaction)
}
}