576 lines
28 KiB
Swift
576 lines
28 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import LibSignalClient
|
|
import SignalRingRTC
|
|
import XCTest
|
|
|
|
@testable import SignalServiceKit
|
|
|
|
private class MockStorageServiceManager: StorageServiceManager {
|
|
func setLocalIdentifiers(_ localIdentifiers: LocalIdentifiers) {}
|
|
func currentManifestVersion(tx: DBReadTransaction) -> UInt64 { 0 }
|
|
func currentManifestHasRecordIkm(tx: DBReadTransaction) -> Bool { false }
|
|
func recordPendingUpdates(updatedRecipientUniqueIds: [RecipientUniqueId]) {}
|
|
func recordPendingUpdates(updatedAddresses: [SignalServiceAddress]) {}
|
|
func recordPendingUpdates(updatedGroupV2MasterKeys: [Data]) {}
|
|
func recordPendingUpdates(updatedStoryDistributionListIds: [Data]) {}
|
|
func recordPendingUpdates(callLinkRootKeys: [CallLinkRootKey]) {}
|
|
func recordPendingUpdates(groupModel: TSGroupModel) {}
|
|
func recordPendingLocalAccountUpdates() {}
|
|
func backupPendingChanges(authedDevice: AuthedDevice) {}
|
|
func resetLocalData(transaction: DBWriteTransaction) {}
|
|
func restoreOrCreateManifestIfNecessary(authedDevice: AuthedDevice) -> Promise<Void> { Promise<Void>(error: OWSGenericError("Not implemented.")) }
|
|
func rotateManifest(mode: ManifestRotationMode, authedDevice: AuthedDevice) async throws { throw OWSGenericError("Not implemented.") }
|
|
func waitForPendingRestores() -> Promise<Void> { Promise<Void>(error: OWSGenericError("Not implemented.")) }
|
|
}
|
|
|
|
private class TestDependencies {
|
|
let aciSessionStore: SignalSessionStore
|
|
var aciSessionStoreKeyValueStore: KeyValueStore {
|
|
KeyValueStore(collection: "TSStorageManagerSessionStoreCollection")
|
|
}
|
|
let identityManager: MockIdentityManager
|
|
let mockDB = InMemoryDB()
|
|
let recipientMerger: RecipientMerger
|
|
let recipientDatabaseTable = MockRecipientDatabaseTable()
|
|
let recipientFetcher: RecipientFetcher
|
|
let recipientIdFinder: RecipientIdFinder
|
|
let threadAssociatedDataStore: MockThreadAssociatedDataStore
|
|
let threadStore: MockThreadStore
|
|
let threadMerger: ThreadMerger
|
|
|
|
init(observers: [RecipientMergeObserver] = []) {
|
|
let storageServiceManager = MockStorageServiceManager()
|
|
recipientFetcher = RecipientFetcherImpl(recipientDatabaseTable: recipientDatabaseTable)
|
|
recipientIdFinder = RecipientIdFinder(recipientDatabaseTable: recipientDatabaseTable, recipientFetcher: recipientFetcher)
|
|
aciSessionStore = SSKSessionStore(for: .aci, recipientIdFinder: recipientIdFinder)
|
|
identityManager = MockIdentityManager(recipientIdFinder: recipientIdFinder)
|
|
identityManager.recipientIdentities = [:]
|
|
identityManager.sessionSwitchoverMessages = []
|
|
threadAssociatedDataStore = MockThreadAssociatedDataStore()
|
|
threadStore = MockThreadStore()
|
|
threadMerger = ThreadMerger.forUnitTests(
|
|
threadAssociatedDataStore: threadAssociatedDataStore,
|
|
threadStore: threadStore
|
|
)
|
|
recipientMerger = RecipientMergerImpl(
|
|
aciSessionStore: aciSessionStore,
|
|
blockedRecipientStore: BlockedRecipientStoreImpl(),
|
|
identityManager: identityManager,
|
|
observers: RecipientMergerImpl.Observers(
|
|
preThreadMerger: [],
|
|
threadMerger: threadMerger,
|
|
postThreadMerger: observers
|
|
),
|
|
recipientDatabaseTable: recipientDatabaseTable,
|
|
recipientFetcher: recipientFetcher,
|
|
storageServiceManager: storageServiceManager
|
|
)
|
|
}
|
|
}
|
|
|
|
class RecipientMergerTest: XCTestCase {
|
|
func testTwoWayMergeCases() {
|
|
let aci_A = Aci.constantForTesting("00000000-0000-4000-8000-00000000000A")
|
|
let aci_B = Aci.constantForTesting("00000000-0000-4000-8000-00000000000B")
|
|
let aciMe = Aci.constantForTesting("00000000-0000-4000-8000-00000000000C")
|
|
let e164_A = E164("+16505550101")!
|
|
let e164_B = E164("+16505550102")!
|
|
let e164Me = E164("+16505550103")!
|
|
let localIdentifiers = LocalIdentifiers(aci: aciMe, pni: nil, phoneNumber: e164Me.stringValue)
|
|
|
|
enum TrustLevel {
|
|
case high
|
|
case low
|
|
}
|
|
|
|
// Taken from the "ACI-E164 Merging Test Cases" document.
|
|
let testCases: [(
|
|
trustLevel: TrustLevel,
|
|
mergeRequest: (aci: Aci?, phoneNumber: E164?),
|
|
initialState: [(rowId: Int64, aci: Aci?, phoneNumber: E164?)],
|
|
finalState: [(rowId: Int64, aci: Aci?, phoneNumber: E164?)]
|
|
)] = [
|
|
(.high, (aci_A, nil), [], [(1, aci_A, nil)]),
|
|
(.low, (aci_A, nil), [], [(1, aci_A, nil)]),
|
|
(.high, (nil, e164_A), [], [(1, nil, e164_A)]),
|
|
(.low, (nil, e164_A), [], [(1, nil, e164_A)]),
|
|
(.high, (aci_A, e164_A), [], [(1, aci_A, e164_A)]),
|
|
(.low, (aci_A, e164_A), [], [(1, aci_A, nil)]),
|
|
(.high, (aci_A, e164_A), [(1, aci_A, nil)], [(1, aci_A, e164_A)]),
|
|
(.low, (aci_A, e164_A), [(1, aci_A, nil)], [(1, aci_A, nil)]),
|
|
(.high, (aci_A, e164_B), [(1, aci_A, e164_A)], [(1, aci_A, e164_B)]),
|
|
(.low, (aci_A, e164_B), [(1, aci_A, e164_A)], [(1, aci_A, e164_A)]),
|
|
(.high, (aci_A, e164_A), [(1, nil, e164_A)], [(1, aci_A, e164_A)]),
|
|
(.low, (aci_A, e164_A), [(1, nil, e164_A)], [(1, nil, e164_A), (2, aci_A, nil)]),
|
|
(.high, (aci_B, e164_A), [(1, aci_A, e164_A)], [(1, aci_A, nil), (2, aci_B, e164_A)]),
|
|
(.low, (aci_B, e164_A), [(1, aci_A, e164_A)], [(1, aci_A, e164_A), (2, aci_B, nil)]),
|
|
(.high, (aci_B, e164Me), [(1, aciMe, e164Me)], [(1, aciMe, e164Me), (2, aci_B, nil)]),
|
|
(.low, (aci_B, e164Me), [(1, aciMe, e164Me)], [(1, aciMe, e164Me), (2, aci_B, nil)]),
|
|
(.high, (aci_A, e164_A), [(1, aci_A, e164_A)], [(1, aci_A, e164_A)]),
|
|
(.low, (aci_A, e164_A), [(1, aci_A, e164_A)], [(1, aci_A, e164_A)]),
|
|
(.high, (aci_A, e164_A), [(1, aci_A, nil), (2, nil, e164_A)], [(1, aci_A, e164_A)]),
|
|
(.low, (aci_A, e164_A), [(1, aci_A, nil), (2, nil, e164_A)], [(1, aci_A, nil), (2, nil, e164_A)]),
|
|
(.high, (aci_A, e164_A), [(1, aci_A, e164_B), (2, aci_B, e164_A)], [(1, aci_A, e164_A), (2, aci_B, nil)]),
|
|
(.low, (aci_A, e164_A), [(1, aci_A, e164_B), (2, aci_B, e164_A)], [(1, aci_A, e164_B), (2, aci_B, e164_A)]),
|
|
(.high, (aci_A, e164_A), [(1, aci_A, e164_B), (2, nil, e164_A)], [(1, aci_A, e164_A)]),
|
|
(.high, (aci_A, e164Me), [(1, aciMe, e164Me), (2, aci_A, nil)], [(1, aciMe, e164Me), (2, aci_A, nil)]),
|
|
(.low, (aci_A, e164Me), [(1, aciMe, e164Me), (2, aci_A, nil)], [(1, aciMe, e164Me), (2, aci_A, nil)])
|
|
]
|
|
|
|
for (idx, testCase) in testCases.enumerated() {
|
|
let d = TestDependencies()
|
|
func run(transaction: DBWriteTransaction) {
|
|
for initialRecipient in testCase.initialState {
|
|
XCTAssertEqual(d.recipientDatabaseTable.nextRowId, initialRecipient.rowId, "\(testCase)")
|
|
d.recipientDatabaseTable.insertRecipient(
|
|
SignalRecipient(
|
|
aci: initialRecipient.aci,
|
|
pni: nil,
|
|
phoneNumber: initialRecipient.phoneNumber
|
|
),
|
|
transaction: transaction
|
|
)
|
|
}
|
|
|
|
switch (testCase.trustLevel, testCase.mergeRequest.aci, testCase.mergeRequest.phoneNumber) {
|
|
case (.high, let aci?, let phoneNumber?):
|
|
_ = d.recipientMerger.applyMergeFromContactSync(
|
|
localIdentifiers: localIdentifiers,
|
|
aci: aci,
|
|
phoneNumber: phoneNumber,
|
|
tx: transaction
|
|
)
|
|
case (_, let aci?, _):
|
|
_ = d.recipientFetcher.fetchOrCreate(serviceId: aci, tx: transaction)
|
|
case (_, _, let phoneNumber):
|
|
_ = d.recipientFetcher.fetchOrCreate(phoneNumber: phoneNumber!, tx: transaction)
|
|
}
|
|
|
|
for finalRecipient in testCase.finalState.reversed() {
|
|
let signalRecipient = d.recipientDatabaseTable.recipientTable.removeValue(forKey: finalRecipient.rowId)
|
|
XCTAssertEqual(signalRecipient?.aci, finalRecipient.aci, "\(idx)")
|
|
XCTAssertEqual(signalRecipient?.phoneNumber?.stringValue, finalRecipient.phoneNumber?.stringValue, "\(idx)")
|
|
}
|
|
XCTAssertEqual(d.recipientDatabaseTable.recipientTable, [:], "\(idx)")
|
|
}
|
|
d.mockDB.write { run(transaction: $0) }
|
|
}
|
|
}
|
|
|
|
func testNotifier() {
|
|
let recipientMergeNotifier = RecipientMergeNotifier(scheduler: SyncScheduler())
|
|
let d = TestDependencies(observers: [recipientMergeNotifier])
|
|
|
|
let aci = Aci.constantForTesting("00000000-0000-4000-8000-0000000000a1")
|
|
let phoneNumber = E164("+16505550101")!
|
|
let pni = Pni.constantForTesting("PNI:00000000-0000-4000-8000-0000000000b1")
|
|
|
|
var notificationCount = 0
|
|
let observer = NotificationCenter.default.addObserver(
|
|
forName: .didLearnRecipientAssociation,
|
|
object: recipientMergeNotifier,
|
|
queue: nil,
|
|
using: { note in
|
|
notificationCount += 1
|
|
}
|
|
)
|
|
d.mockDB.write { tx in
|
|
_ = d.recipientMerger.applyMergeFromSealedSender(
|
|
localIdentifiers: .forUnitTests,
|
|
aci: aci,
|
|
phoneNumber: nil,
|
|
tx: tx
|
|
)
|
|
}
|
|
XCTAssertEqual(notificationCount, 0)
|
|
d.mockDB.write { tx in
|
|
_ = d.recipientMerger.applyMergeFromContactDiscovery(
|
|
localIdentifiers: .forUnitTests,
|
|
phoneNumber: phoneNumber,
|
|
pni: pni,
|
|
aci: aci,
|
|
tx: tx
|
|
)
|
|
}
|
|
XCTAssertEqual(notificationCount, 2)
|
|
NotificationCenter.default.removeObserver(observer)
|
|
}
|
|
|
|
func testAciPhoneNumberSafetyNumberChange() {
|
|
let ac1 = Aci.constantForTesting("00000000-0000-4000-8000-00000000000A")
|
|
let ac2 = Aci.constantForTesting("00000000-0000-4000-8000-00000000000B")
|
|
let ik1 = IdentityKey(publicKey: IdentityKeyPair.generate().publicKey)
|
|
let ik2 = IdentityKey(publicKey: IdentityKeyPair.generate().publicKey)
|
|
let pn1 = E164("+16505550101")!
|
|
let pn2 = E164("+16505550102")!
|
|
|
|
let testCases: [(
|
|
initialState: [(aci: Aci?, phoneNumber: E164?, identityKey: IdentityKey?)],
|
|
shouldInsertEvent: Bool
|
|
)] = [
|
|
([(ac1, nil, ik1), (nil, pn1, ik2)], true),
|
|
([(ac1, nil, ik1), (nil, pn1, ik1)], false),
|
|
([(ac1, nil, ik1), (nil, pn1, nil)], false),
|
|
([(ac1, nil, nil), (nil, pn1, ik1)], false),
|
|
([(ac1, pn2, ik1), (nil, pn1, ik1)], false),
|
|
([(ac1, pn2, ik1), (nil, pn1, ik2)], true),
|
|
([(ac1, nil, ik1), (ac2, pn1, ik1)], false),
|
|
([(ac1, nil, ik1), (ac2, pn1, ik2)], false)
|
|
]
|
|
|
|
for testCase in testCases {
|
|
let d = TestDependencies()
|
|
|
|
d.identityManager.identityChangeInfoMessages = []
|
|
|
|
d.mockDB.write { tx in
|
|
for initialState in testCase.initialState {
|
|
let recipient = SignalRecipient(aci: initialState.aci, pni: nil, phoneNumber: initialState.phoneNumber)
|
|
d.recipientDatabaseTable.insertRecipient(recipient, transaction: tx)
|
|
if let identityKey = initialState.identityKey {
|
|
d.identityManager.recipientIdentities[recipient.uniqueId] = OWSRecipientIdentity(
|
|
recipientUniqueId: recipient.uniqueId,
|
|
identityKey: Data(identityKey.publicKey.keyBytes),
|
|
isFirstKnownKey: true,
|
|
createdAt: Date(),
|
|
verificationState: .default
|
|
)
|
|
d.aciSessionStoreKeyValueStore.setData(Data(), key: recipient.uniqueId, transaction: tx)
|
|
}
|
|
}
|
|
|
|
let mergedRecipient = d.recipientMerger.applyMergeFromSealedSender(
|
|
localIdentifiers: .forUnitTests,
|
|
aci: ac1,
|
|
phoneNumber: pn1,
|
|
tx: tx
|
|
)
|
|
|
|
XCTAssertEqual(d.identityManager.identityChangeInfoMessages, testCase.shouldInsertEvent ? [ac1] : [])
|
|
XCTAssertEqual(try! d.identityManager.identityKey(for: ac1, tx: tx), ik1)
|
|
XCTAssertTrue(d.aciSessionStore.mightContainSession(for: mergedRecipient, tx: tx))
|
|
}
|
|
}
|
|
}
|
|
|
|
func testAciPhoneNumberPniMerges() throws {
|
|
let aci1 = Aci.constantForTesting("00000000-0000-4000-8000-0000000000a1")
|
|
let pni1 = Pni.constantForTesting("PNI:00000000-0000-4000-8000-0000000000b1")
|
|
let phone1 = E164("+16505550101")!
|
|
let phone2 = E164("+16505550102")!
|
|
|
|
let testCases: [(
|
|
initialState: [(aci: Aci?, phoneNumber: E164?, pni: Pni?)],
|
|
includeAci: Bool,
|
|
finalState: [(aci: Aci?, phoneNumber: E164?, pni: Pni?)?]
|
|
)] = [
|
|
// If they're already associated, do nothing.
|
|
([(aci1, phone1, pni1)], false, [(aci1, phone1, pni1)]),
|
|
([(aci1, phone1, pni1)], true, [(aci1, phone1, pni1)]),
|
|
([(nil, phone1, pni1)], false, [(nil, phone1, pni1)]),
|
|
([(aci1, phone1, pni1)], true, [(aci1, phone1, pni1)]),
|
|
|
|
// If the PNI doesn't exist anywhere, just add it.
|
|
([(aci1, phone1, nil)], false, [(aci1, phone1, pni1)]),
|
|
([(aci1, phone1, nil)], true, [(aci1, phone1, pni1)]),
|
|
|
|
// If the PNI exists elsewhere, steal it.
|
|
([(nil, phone1, nil), (nil, phone2, pni1)], false, [(nil, phone1, pni1), (nil, phone2, nil)]),
|
|
|
|
// If the PNI exists, steal it if possible.
|
|
([(nil, nil, pni1)], false, [(nil, phone1, pni1)]),
|
|
([(nil, phone2, pni1)], false, [(nil, phone2, nil), (nil, phone1, pni1)]),
|
|
([(aci1, nil, pni1)], false, [(aci1, phone1, pni1)]),
|
|
|
|
// If nothing exists, create it.
|
|
([], false, [(nil, phone1, pni1)])
|
|
]
|
|
|
|
for testCase in testCases {
|
|
let d = TestDependencies()
|
|
let mergedRecipient = d.mockDB.write { tx in
|
|
for initialState in testCase.initialState {
|
|
d.recipientDatabaseTable.insertRecipient(
|
|
SignalRecipient(aci: initialState.aci, pni: initialState.pni, phoneNumber: initialState.phoneNumber),
|
|
transaction: tx
|
|
)
|
|
}
|
|
return d.recipientMerger.applyMergeFromContactDiscovery(
|
|
localIdentifiers: .forUnitTests,
|
|
phoneNumber: phone1,
|
|
pni: pni1,
|
|
aci: testCase.includeAci ? aci1 : nil,
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
// Make sure the returned recipient has the correct details.
|
|
XCTAssertEqual(mergedRecipient?.phoneNumber?.stringValue, phone1.stringValue)
|
|
XCTAssertEqual(mergedRecipient?.pni, pni1)
|
|
if testCase.includeAci { XCTAssertEqual(mergedRecipient?.aci, aci1) }
|
|
|
|
// Make sure all the recipients have been updated properly.
|
|
for (idx, finalState) in testCase.finalState.enumerated() {
|
|
let recipient = try XCTUnwrap(d.recipientDatabaseTable.recipientTable.removeValue(forKey: Int64(idx + 1)))
|
|
XCTAssertEqual(recipient.phoneNumber?.stringValue, finalState?.phoneNumber?.stringValue)
|
|
XCTAssertEqual(recipient.pni, finalState?.pni)
|
|
XCTAssertEqual(recipient.aci, finalState?.aci)
|
|
}
|
|
XCTAssertEqual(d.recipientDatabaseTable.recipientTable, [:])
|
|
}
|
|
}
|
|
|
|
func testSessionSwitchoverEvents() throws {
|
|
let aci1 = Aci.constantForTesting("00000000-0000-4000-8000-0000000000a1")
|
|
let phone1 = E164("+16505550101")!
|
|
let pni1 = Pni.constantForTesting("PNI:00000000-0000-4000-8000-0000000000b1")
|
|
let recipient1 = SignalRecipient(aci: nil, pni: pni1, phoneNumber: phone1)
|
|
|
|
let aci2 = Aci.constantForTesting("00000000-0000-4000-8000-0000000000a2")
|
|
let phone2 = E164("+16505550102")!
|
|
let pni2 = Pni.constantForTesting("PNI:00000000-0000-4000-8000-0000000000b2")
|
|
let recipient2 = SignalRecipient(aci: aci2, pni: pni2, phoneNumber: phone2)
|
|
|
|
struct TestCase {
|
|
let mergeRequest: (aci: Aci?, phoneNumber: E164, pni: Pni)
|
|
let hasSession: [SignalRecipient]
|
|
let needsEvent: [SignalRecipient]
|
|
let lineNumber: Int
|
|
|
|
init(
|
|
_ mergeRequest: (aci: Aci?, phoneNumber: E164, pni: Pni),
|
|
hasSession: [SignalRecipient],
|
|
needsEvent: [SignalRecipient],
|
|
_ lineNumber: Int = #line
|
|
) {
|
|
self.mergeRequest = mergeRequest
|
|
self.hasSession = hasSession
|
|
self.needsEvent = needsEvent
|
|
self.lineNumber = lineNumber
|
|
}
|
|
}
|
|
|
|
let testCases: [TestCase] = [
|
|
// If there's no session, there's no session switchover.
|
|
TestCase((aci1, phone1, pni1), hasSession: [], needsEvent: []),
|
|
// If there's a session, there's a session switchover.
|
|
TestCase((aci1, phone1, pni1), hasSession: [recipient1], needsEvent: [recipient1]),
|
|
// If we're already communicating with the aci, there's no switchover.
|
|
TestCase((aci2, phone2, pni1), hasSession: [recipient2], needsEvent: []),
|
|
// But the source of the pni might need one if it had a session.
|
|
TestCase((aci2, phone2, pni1), hasSession: [recipient1, recipient2], needsEvent: [recipient1]),
|
|
// If we do a thread merge, we can skip the session switchover.
|
|
TestCase((aci2, phone1, pni1), hasSession: [recipient1, recipient2], needsEvent: []),
|
|
]
|
|
|
|
for testCase in testCases {
|
|
Logger.verbose("Starting test case from line \(testCase.lineNumber)")
|
|
defer { Logger.flush() }
|
|
|
|
let d = TestDependencies()
|
|
d.mockDB.write { tx in
|
|
for recipient in [recipient1, recipient2] {
|
|
d.recipientDatabaseTable.insertRecipient(recipient, transaction: tx)
|
|
}
|
|
for recipient in testCase.hasSession {
|
|
d.aciSessionStoreKeyValueStore.setData(Data(), key: recipient.uniqueId, transaction: tx)
|
|
let thread = TSContactThread(contactAddress: SignalServiceAddress(
|
|
serviceId: recipient.aci ?? recipient.pni,
|
|
phoneNumber: recipient.phoneNumber?.stringValue,
|
|
cache: SignalServiceAddressCache()
|
|
))
|
|
thread.shouldThreadBeVisible = true
|
|
d.threadStore.insertThread(thread)
|
|
d.threadAssociatedDataStore.values[thread.uniqueId] = ThreadAssociatedData(threadUniqueId: thread.uniqueId)
|
|
}
|
|
}
|
|
|
|
d.mockDB.write { tx in
|
|
_ = d.recipientMerger.applyMergeFromContactDiscovery(
|
|
localIdentifiers: .forUnitTests,
|
|
phoneNumber: testCase.mergeRequest.phoneNumber,
|
|
pni: testCase.mergeRequest.pni,
|
|
aci: testCase.mergeRequest.aci,
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
for recipientNeedingEvent in testCase.needsEvent {
|
|
let foundRecipient = d.identityManager.sessionSwitchoverMessages.removeFirst(where: { (recipient, _) in
|
|
recipient.uniqueId == recipientNeedingEvent.uniqueId
|
|
})
|
|
XCTAssertNotNil(foundRecipient)
|
|
}
|
|
XCTAssertEqual(d.identityManager.sessionSwitchoverMessages.count, 0)
|
|
}
|
|
}
|
|
|
|
func testPniSignatureMerge() throws {
|
|
let aci = Aci.constantForTesting("00000000-0000-4000-8000-0000000000a1")
|
|
let aciRecipient = SignalRecipient(aci: aci, pni: nil, phoneNumber: nil)
|
|
|
|
let phoneNumber = E164("+16505550101")!
|
|
let pni = Pni.constantForTesting("PNI:00000000-0000-4000-8000-0000000000b1")
|
|
let pniRecipient = SignalRecipient(aci: nil, pni: pni, phoneNumber: phoneNumber)
|
|
|
|
let d = TestDependencies()
|
|
d.mockDB.write { tx in
|
|
for recipient in [aciRecipient, pniRecipient] {
|
|
d.recipientDatabaseTable.insertRecipient(recipient, transaction: tx)
|
|
d.aciSessionStoreKeyValueStore.setData(Data(), key: recipient.uniqueId, transaction: tx)
|
|
}
|
|
}
|
|
|
|
d.mockDB.write { tx in
|
|
d.recipientMerger.applyMergeFromPniSignature(localIdentifiers: .forUnitTests, aci: aci, pni: pni, tx: tx)
|
|
}
|
|
|
|
XCTAssertEqual(d.recipientDatabaseTable.recipientTable.values.map({ $0.uniqueId }), [aciRecipient.uniqueId])
|
|
XCTAssertEqual(d.identityManager.sessionSwitchoverMessages.count, 0)
|
|
}
|
|
|
|
func testStorageServiceMerges() throws {
|
|
let aci1 = Aci.constantForTesting("00000000-0000-4000-8000-0000000000a1")
|
|
let aci2 = Aci.constantForTesting("00000000-0000-4000-8000-0000000000a2")
|
|
let pni1 = Pni.constantForTesting("PNI:00000000-0000-4000-8000-0000000000b1")
|
|
let pni2 = Pni.constantForTesting("PNI:00000000-0000-4000-8000-0000000000b2")
|
|
let phoneNumber1 = E164("+16505550101")!
|
|
let phoneNumber2 = E164("+16505550102")!
|
|
|
|
struct TestCase {
|
|
let isPrimaryDevice: Bool
|
|
let initialState: [(aci: Aci?, phoneNumber: E164?, pni: Pni?)]
|
|
let mergeRequest: (aci: Aci?, phoneNumber: E164?, pni: Pni?)
|
|
let finalState: [(aci: Aci?, phoneNumber: E164?, pni: Pni?, isResult: Bool)]
|
|
let lineNumber: Int
|
|
|
|
init(
|
|
isPrimaryDevice: Bool,
|
|
initialState: [(aci: Aci?, phoneNumber: E164?, pni: Pni?)],
|
|
mergeRequest: (aci: Aci?, phoneNumber: E164?, pni: Pni?),
|
|
finalState: [(aci: Aci?, phoneNumber: E164?, pni: Pni?, isResult: Bool)],
|
|
lineNumber: Int = #line
|
|
) {
|
|
self.isPrimaryDevice = isPrimaryDevice
|
|
self.initialState = initialState
|
|
self.mergeRequest = mergeRequest
|
|
self.finalState = finalState
|
|
self.lineNumber = lineNumber
|
|
}
|
|
}
|
|
|
|
let testCases: [TestCase] = [
|
|
// If we know the ACI/PNI, we can add the phone number.
|
|
TestCase(
|
|
isPrimaryDevice: true,
|
|
initialState: [(aci1, nil, pni1)],
|
|
mergeRequest: (nil, phoneNumber1, pni1),
|
|
finalState: [(aci1, phoneNumber1, pni1, true)]
|
|
),
|
|
// If we're linking a phone number/PNI across recipients, the phone number has precedence.
|
|
TestCase(
|
|
isPrimaryDevice: true,
|
|
initialState: [(aci1, nil, pni1), (aci2, phoneNumber1, nil)],
|
|
mergeRequest: (nil, phoneNumber1, pni1),
|
|
finalState: [(aci1, nil, nil, false), (aci2, phoneNumber1, pni1, true)]
|
|
),
|
|
// If we're trying to re-associate on a primary, that's not allowed/ignored.
|
|
TestCase(
|
|
isPrimaryDevice: true,
|
|
initialState: [(aci1, phoneNumber1, pni1)],
|
|
mergeRequest: (nil, phoneNumber1, pni2),
|
|
finalState: [(aci1, phoneNumber1, pni1, true)]
|
|
),
|
|
TestCase(
|
|
isPrimaryDevice: false,
|
|
initialState: [(aci1, phoneNumber1, pni1)],
|
|
mergeRequest: (nil, phoneNumber1, pni2),
|
|
finalState: [(aci1, phoneNumber1, pni2, true)]
|
|
),
|
|
TestCase(
|
|
isPrimaryDevice: true,
|
|
initialState: [(aci1, phoneNumber1, pni1)],
|
|
mergeRequest: (nil, phoneNumber2, pni1),
|
|
finalState: [(aci1, phoneNumber1, pni1, true)]
|
|
),
|
|
TestCase(
|
|
isPrimaryDevice: false,
|
|
initialState: [(aci1, phoneNumber1, pni1)],
|
|
mergeRequest: (nil, phoneNumber2, pni1),
|
|
finalState: [(aci1, phoneNumber1, nil, false), (nil, phoneNumber2, pni1, true)]
|
|
),
|
|
// If we learn the PNI but not the phone number, we should add the PNI.
|
|
TestCase(
|
|
isPrimaryDevice: true,
|
|
initialState: [(aci1, phoneNumber1, nil)],
|
|
mergeRequest: (aci1, nil, pni1),
|
|
finalState: [(aci1, phoneNumber1, pni1, true)]
|
|
),
|
|
// But not if we already know some other PNI for the phone number.
|
|
TestCase(
|
|
isPrimaryDevice: true,
|
|
initialState: [(aci1, phoneNumber1, pni1)],
|
|
mergeRequest: (aci1, nil, pni2),
|
|
finalState: [(aci1, phoneNumber1, pni1, true)]
|
|
),
|
|
// Unless we're on a linked device.
|
|
TestCase(
|
|
isPrimaryDevice: false,
|
|
initialState: [(aci1, phoneNumber1, pni1)],
|
|
mergeRequest: (aci1, nil, pni2),
|
|
finalState: [(aci1, phoneNumber1, pni2, true)]
|
|
),
|
|
// But we can if we don't know a phone number.
|
|
TestCase(
|
|
isPrimaryDevice: true,
|
|
initialState: [(aci1, nil, pni1)],
|
|
mergeRequest: (aci1, nil, pni2),
|
|
finalState: [(aci1, nil, pni2, true)]
|
|
),
|
|
// If we get an ACI/PNI result, we should infer the phone number.
|
|
TestCase(
|
|
isPrimaryDevice: true,
|
|
initialState: [(aci1, phoneNumber2, pni2), (nil, phoneNumber1, pni1)],
|
|
mergeRequest: (aci1, nil, pni1),
|
|
finalState: [(aci1, phoneNumber1, pni1, true)]
|
|
),
|
|
]
|
|
|
|
for testCase in testCases {
|
|
Logger.verbose("Starting test on line \(testCase.lineNumber)")
|
|
defer { Logger.flush() }
|
|
let d = TestDependencies()
|
|
let mergedRecipient = d.mockDB.write { tx in
|
|
for initialState in testCase.initialState {
|
|
d.recipientDatabaseTable.insertRecipient(
|
|
SignalRecipient(aci: initialState.aci, pni: initialState.pni, phoneNumber: initialState.phoneNumber),
|
|
transaction: tx
|
|
)
|
|
}
|
|
return d.recipientMerger.applyMergeFromStorageService(
|
|
localIdentifiers: .forUnitTests,
|
|
isPrimaryDevice: testCase.isPrimaryDevice,
|
|
serviceIds: AtLeastOneServiceId(aci: testCase.mergeRequest.aci, pni: testCase.mergeRequest.pni)!,
|
|
phoneNumber: testCase.mergeRequest.phoneNumber,
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
// Make sure all the recipients have been updated properly.
|
|
for (idx, finalState) in testCase.finalState.enumerated() {
|
|
let recipient = try XCTUnwrap(d.recipientDatabaseTable.recipientTable.removeValue(forKey: Int64(idx + 1)))
|
|
XCTAssertEqual(recipient.phoneNumber?.stringValue, finalState.phoneNumber?.stringValue)
|
|
XCTAssertEqual(recipient.pni, finalState.pni)
|
|
XCTAssertEqual(recipient.aci, finalState.aci)
|
|
if finalState.isResult {
|
|
XCTAssertEqual(mergedRecipient.uniqueId, recipient.uniqueId)
|
|
}
|
|
}
|
|
XCTAssertEqual(d.recipientDatabaseTable.recipientTable, [:])
|
|
}
|
|
}
|
|
}
|