Signal-iOS/SignalServiceKit/tests/Contacts/RecipientMergerTest.swift
2023-07-26 16:56:19 -05:00

177 lines
8.7 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import LibSignalClient
import SignalCoreKit
import XCTest
@testable import SignalServiceKit
private class MockRecipientDataStore: RecipientDataStore {
var nextRowId = 1
var recipientTable: [Int: SignalRecipient] = [:]
func fetchRecipient(serviceId: UntypedServiceId, transaction: DBReadTransaction) -> SignalRecipient? {
copyRecipient(recipientTable.values.first(where: { $0.serviceId == serviceId }) ?? nil)
}
func fetchRecipient(phoneNumber: String, transaction: DBReadTransaction) -> SignalRecipient? {
copyRecipient(recipientTable.values.first(where: { $0.phoneNumber == phoneNumber }) ?? nil)
}
private func copyRecipient(_ signalRecipient: SignalRecipient?) -> SignalRecipient? {
signalRecipient?.copy() as! SignalRecipient?
}
func insertRecipient(_ signalRecipient: SignalRecipient, transaction: DBWriteTransaction) {
precondition(rowId(for: signalRecipient) == nil)
recipientTable[nextRowId] = copyRecipient(signalRecipient)
nextRowId += 1
}
func updateRecipient(_ signalRecipient: SignalRecipient, transaction: DBWriteTransaction) {
let rowId = rowId(for: signalRecipient)!
recipientTable[rowId] = copyRecipient(signalRecipient)
}
func removeRecipient(_ signalRecipient: SignalRecipient, transaction: DBWriteTransaction) {
let rowId = rowId(for: signalRecipient)!
recipientTable[rowId] = nil
}
private func rowId(for signalRecipient: SignalRecipient) -> Int? {
for (rowId, value) in recipientTable {
if value.uniqueId == signalRecipient.uniqueId {
return rowId
}
}
return nil
}
}
private class MockRecipientMergerTemporaryShims: RecipientMergerTemporaryShims {
func didUpdatePhoneNumber(serviceIdString: String, oldPhoneNumber: String?, newPhoneNumber: E164?, transaction: DBWriteTransaction) {}
func hasActiveSignalProtocolSession(recipientId: String, deviceId: Int32, transaction: DBWriteTransaction) -> Bool { false }
}
private class MockStorageServiceManager: StorageServiceManager {
func recordPendingDeletions(deletedGroupV1Ids: [Data]) {}
func recordPendingUpdates(updatedAccountIds: [AccountId]) {}
func recordPendingUpdates(updatedAddresses: [SignalServiceAddress]) {}
func recordPendingUpdates(updatedGroupV1Ids: [Data]) {}
func recordPendingUpdates(updatedGroupV2MasterKeys: [Data]) {}
func recordPendingUpdates(updatedStoryDistributionListIds: [Data]) {}
func recordPendingUpdates(groupModel: TSGroupModel) {}
func recordPendingLocalAccountUpdates() {}
func setLocalIdentifiers(_ localIdentifiers: LocalIdentifiersObjC) {}
func backupPendingChanges(authedAccount: AuthedAccount) {}
func resetLocalData(transaction: DBWriteTransaction) {}
func restoreOrCreateManifestIfNecessary(authedAccount: AuthedAccount) -> AnyPromise {
AnyPromise(Promise<Void>(error: OWSGenericError("Not implemented.")))
}
func waitForPendingRestores() -> AnyPromise {
AnyPromise(Promise<Void>(error: OWSGenericError("Not implemented.")))
}
}
class RecipientMergerTest: XCTestCase {
func testTwoWayMergeCases() {
let aci_A = FutureAci.constantForTesting("00000000-0000-4000-8000-00000000000A")
let aci_B = FutureAci.constantForTesting("00000000-0000-4000-8000-00000000000B")
let aciMe = FutureAci.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: (serviceId: UntypedServiceId?, phoneNumber: E164?),
initialState: [(rowId: Int, serviceId: UntypedServiceId?, phoneNumber: E164?)],
finalState: [(rowId: Int, serviceId: UntypedServiceId?, 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 mockDB = MockDB()
let mockDataStore = MockRecipientDataStore()
let recipientFetcher = RecipientFetcherImpl(recipientStore: mockDataStore)
let recipientMerger = RecipientMergerImpl(
temporaryShims: MockRecipientMergerTemporaryShims(),
observers: [],
recipientFetcher: recipientFetcher,
dataStore: mockDataStore,
storageServiceManager: MockStorageServiceManager()
)
func run(transaction: DBWriteTransaction) {
for initialRecipient in testCase.initialState {
XCTAssertEqual(mockDataStore.nextRowId, initialRecipient.rowId, "\(testCase)")
mockDataStore.insertRecipient(
SignalRecipient(
serviceId: initialRecipient.serviceId,
phoneNumber: initialRecipient.phoneNumber
),
transaction: transaction
)
}
switch (testCase.trustLevel, testCase.mergeRequest.serviceId, testCase.mergeRequest.phoneNumber) {
case (.high, let serviceId?, let phoneNumber?):
_ = recipientMerger.applyMergeFromLinkedDevice(
localIdentifiers: localIdentifiers,
serviceId: serviceId,
phoneNumber: phoneNumber,
tx: transaction
)
case (_, let serviceId?, _):
_ = recipientFetcher.fetchOrCreate(serviceId: serviceId, tx: transaction)
case (_, _, let phoneNumber):
_ = recipientFetcher.fetchOrCreate(phoneNumber: phoneNumber!, tx: transaction)
}
for finalRecipient in testCase.finalState.reversed() {
let signalRecipient = mockDataStore.recipientTable.removeValue(forKey: finalRecipient.rowId)
XCTAssertEqual(signalRecipient?.serviceId, finalRecipient.serviceId, "\(idx)")
XCTAssertEqual(signalRecipient?.phoneNumber, finalRecipient.phoneNumber?.stringValue, "\(idx)")
}
XCTAssertEqual(mockDataStore.recipientTable, [:], "\(idx)")
}
mockDB.write { run(transaction: $0) }
}
}
}