Signal-iOS/SignalServiceKit/tests/Contacts/UUIDBackfillTaskTest.swift
Michelle Linington 3f746368cd PR feedback:
- Adjust how clients provide QoS information to ContactDiscoveryTask
- Fix an issue where legacy CDS was reporting a generic server failure
  as a result of network failures.
- ContactDiscoveryTask will no longer error on an empty set
- Remove CDSFeedback operation
2020-08-06 12:57:39 -07:00

525 lines
18 KiB
Swift

//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
//
import Foundation
import XCTest
@testable import SignalServiceKit
/* Most of this code was moved into ContactsUpdater
TODO: IOS-715: Remove UUIDBackfillTask or fix its tests
class UUIDBackfillTaskTest: SSKBaseTestSwift {
private var dut: UUIDBackfillTask! = nil
private var readiness: MockReadiness! = nil
private var persistence: MockPersistence! = nil
private var network: MockNetwork! = nil
override func setUp() {
super.setUp()
let mockEnvironment = SSKEnvironment.shared as! MockSSKEnvironment
readiness = MockReadiness()
persistence = MockPersistence()
network = MockNetwork()
mockEnvironment.signalServiceAddressCache = MockServiceAddressCache()
dut = UUIDBackfillTask(persistence: persistence,
network: network,
readiness: readiness)
dut.testing_shortBackoffInterval = true
dut.testing_skipModernCDSFlagCheck = true
}
// MARK: - Tests
func testWaitsUntilReady() {
// Setup
let didComplete = expectation(description: "Task Completed")
configureMocks(expectingRegistrationFor: [],
expectingUnregistrationFor: [])
readiness.ready = false
var didSetReady = false
var readyStateAtCompletion = false
// Test
dut.perform {
readyStateAtCompletion = didSetReady
didComplete.fulfill()
}
DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) {
didSetReady = true
self.readiness.ready = true
}
// Verify
waitForExpectations(timeout: 3)
XCTAssertTrue(readyStateAtCompletion)
}
func testNoLegacyRecipients() {
// Setup
let didComplete = expectation(description: "Task Completed")
configureMocks(expectingRegistrationFor: [],
expectingUnregistrationFor: [])
// Test
dut.perform {
didComplete.fulfill()
}
// Verify
waitForExpectations(timeout: 3)
persistence.verifySuccess()
network.verify(requestCount: 0)
}
func testOneLegacyRecipient_Found() {
// Setup
let didComplete = expectation(description: "Task Completed")
configureMocks(expectingRegistrationFor: ["+11234567890"],
expectingUnregistrationFor: [])
// Test
dut.perform {
didComplete.fulfill()
}
// Verify
waitForExpectations(timeout: 3)
persistence.verifySuccess()
network.verify(requestCount: 1)
}
func testOneLegacyRecipient_NotFound() {
// Setup
let didComplete = expectation(description: "Task Completed")
configureMocks(expectingRegistrationFor: [],
expectingUnregistrationFor: ["+11234567890"])
// Test
dut.perform {
didComplete.fulfill()
}
// Verify
waitForExpectations(timeout: 3)
persistence.verifySuccess()
network.verify(requestCount: 1)
}
func testManyLegacyRecipients_AllFound() {
// Setup
let didComplete = expectation(description: "Task Completed")
let registrations = (1234567890..<1234567890+1000).map { "+1\($0)" }
configureMocks(expectingRegistrationFor: registrations,
expectingUnregistrationFor: [])
// Test
dut.perform {
didComplete.fulfill()
}
// Verify
waitForExpectations(timeout: 3)
persistence.verifySuccess()
network.verify(requestCount: 1)
}
func testManyLegacyRecipients_SomeFound() {
// Setup
let didComplete = expectation(description: "Task Completed")
let registrations = (1234567890..<1234567890+500).map { "+1\($0)" }
let unregistrations = (2234567890..<2234567890+500).map { "+1\($0)" }
configureMocks(expectingRegistrationFor: registrations,
expectingUnregistrationFor: unregistrations)
// Test
dut.perform {
didComplete.fulfill()
}
// Verify
waitForExpectations(timeout: 3)
persistence.verifySuccess()
network.verify(requestCount: 1)
}
func testManyLegacyRecipients_NoneFound() {
// Setup
let didComplete = expectation(description: "Task Completed")
let unregistrations = (1234567890..<1234567890+1000).map { "+1\($0)" }
configureMocks(expectingRegistrationFor: [],
expectingUnregistrationFor: unregistrations)
// Test
dut.perform {
didComplete.fulfill()
}
// Verify
waitForExpectations(timeout: 3)
persistence.verifySuccess()
network.verify(requestCount: 1)
}
func testNetworkFailure() {
// Setup
let didComplete = expectation(description: "Task Completed")
configureMocks(expectingRegistrationFor: ["+11234567890"],
expectingUnregistrationFor: [],
forcedFailures: [
NSError(domain: TSNetworkManagerErrorDomain, code: 0, userInfo: nil)
])
// Test
dut.perform {
didComplete.fulfill()
}
// Verify
waitForExpectations(timeout: 3)
persistence.verifySuccess()
network.verify(requestCount: 2)
}
func testRepeatedNetworkFailures() {
// Setup
let didComplete = expectation(description: "Task Completed")
configureMocks(expectingRegistrationFor: ["+11234567890"],
expectingUnregistrationFor: [],
forcedFailures: [
NSError(domain: TSNetworkManagerErrorDomain, code: 0, userInfo: nil),
NSError(domain: TSNetworkManagerErrorDomain, code: 0, userInfo: nil),
NSError(domain: TSNetworkManagerErrorDomain, code: 0, userInfo: nil),
NSError(domain: TSNetworkManagerErrorDomain, code: 0, userInfo: nil),
NSError(domain: TSNetworkManagerErrorDomain, code: 0, userInfo: nil),
NSError(domain: TSNetworkManagerErrorDomain, code: 0, userInfo: nil),
NSError(domain: TSNetworkManagerErrorDomain, code: 0, userInfo: nil)
])
// Test
dut.perform {
didComplete.fulfill()
}
// Verify
waitForExpectations(timeout: 10)
persistence.verifySuccess()
network.verify(requestCount: 8)
}
func testUnknownFailures() {
// Setup
let didComplete = expectation(description: "Task Completed")
configureMocks(expectingRegistrationFor: ["+11234567890"],
expectingUnregistrationFor: [],
forcedFailures: [
NSError(domain: "TestDomain", code: 1, userInfo: nil)
])
// Test
dut.perform {
didComplete.fulfill()
}
// Verify
waitForExpectations(timeout: 3)
persistence.verifySuccess()
network.verify(requestCount: 2)
}
func testRepeatedUnknownFailures() {
// Setup
let didComplete = expectation(description: "Task Completed")
configureMocks(expectingRegistrationFor: ["+11234567890"],
expectingUnregistrationFor: [],
forcedFailures: [
NSError(domain: "TestDomain", code: 1, userInfo: nil),
NSError(domain: "TestDomain", code: 2, userInfo: nil),
NSError(domain: "TestDomain", code: 3, userInfo: nil),
NSError(domain: "TestDomain", code: 4, userInfo: nil),
NSError(domain: "TestDomain", code: 5, userInfo: nil),
NSError(domain: "TestDomain", code: 6, userInfo: nil)
])
// Test
dut.perform {
didComplete.fulfill()
}
// Verify
waitForExpectations(timeout: 10)
persistence.verifySuccess()
network.verify(requestCount: 7)
}
func testMixedFailures() {
// Setup
let didComplete = expectation(description: "Task Completed")
configureMocks(expectingRegistrationFor: ["+11234567890"],
expectingUnregistrationFor: [],
forcedFailures: [
NSError(domain: "TestDomain", code: 1, userInfo: nil),
NSError(domain: "TestDomain", code: 2, userInfo: nil),
NSError(domain: TSNetworkManagerErrorDomain, code: 3, userInfo: nil),
NSError(domain: TSNetworkManagerErrorDomain, code: 4, userInfo: nil),
NSError(domain: TSNetworkManagerErrorDomain, code: 5, userInfo: nil),
NSError(domain: TSNetworkManagerErrorDomain, code: 6, userInfo: nil),
NSError(domain: TSNetworkManagerErrorDomain, code: 7, userInfo: nil)
])
// Test
dut.perform {
didComplete.fulfill()
}
// Verify
waitForExpectations(timeout: 10)
persistence.verifySuccess()
network.verify(requestCount: 8)
}
func testBackoffInterval() {
// Setup
dut.testing_shortBackoffInterval = false // reset to normal backoff behavior
// Test + Verify: first attempt has no delay
dut.testing_attemptCount = 0
XCTAssertEqual(dut.testing_backoffInterval, .seconds(0))
// Test + Verify: next few attempts are briefly delayed
(1..<4).forEach {
dut.testing_attemptCount = $0
XCTAssertLessThan(dut.testing_backoffInterval, .seconds(1))
}
// Test + Verify: later attempts are greatly delayed
(10..<13).forEach {
dut.testing_attemptCount = $0
XCTAssertGreaterThan(dut.testing_backoffInterval, .seconds(60))
}
// Test + Verify: delays cap out at 15 minutes
[20, 30, 50, 100, 500, 1000].forEach {
dut.testing_attemptCount = $0
XCTAssertEqual(dut.testing_backoffInterval, .seconds(15 * 60))
}
}
func testUnnormalizedNumbers_nonE164() {
// Setup
let didComplete = expectation(description: "Task Completed")
configureMocks(expectingRegistrationFor: ["1234567890", "3134505219"],
expectingUnregistrationFor: ["123",
"999999999999999999"
/*, "" (works for empty strings but hits a failDebug and stops test execution) */
])
// Test
dut.perform {
didComplete.fulfill()
}
// Verify
waitForExpectations(timeout: 3)
persistence.verifySuccess()
network.verify(requestCount: 1)
}
func testUnnormalizedNumbers_onlyUnregistration() {
// Setup
let didComplete = expectation(description: "Task Completed")
configureMocks(expectingRegistrationFor: [],
expectingUnregistrationFor: ["123", "999999999999999999"])
// Test
dut.perform {
didComplete.fulfill()
}
// Verify
waitForExpectations(timeout: 3)
persistence.verifySuccess()
network.verify(requestCount: 1)
}
}
extension UUIDBackfillTaskTest {
class MockServiceAddressCache: SignalServiceAddressCache {
override func hashAndCache(uuid: UUID?, phoneNumber: String?, trustLevel: SignalRecipientTrustLevel) -> Int {
// If the cache is disabled, we just still return a valid hash to speed up isEqual: checks
// Anything works as long as it's consistent.
if let uuid = uuid {
return uuid.hashValue
} else if let digits = phoneNumber?.filter({ $0.isWholeNumber }) {
return Int(digits) ?? 0
} else {
return 0
}
}
override func uuid(forPhoneNumber phoneNumber: String) -> UUID? { return nil }
override func phoneNumber(forUuid uuid: UUID) -> String? { return nil }
}
class MockReadiness: UUIDBackfillTask.ReadinessProvider {
var queuedItems: [() -> Void] = []
var ready: Bool = true {
didSet {
if ready {
queuedItems.forEach { $0() }
queuedItems.removeAll()
}
}
}
override func runNowOrWhenAppDidBecomeReady(_ workItem: @escaping () -> Void) {
if ready {
workItem()
} else {
queuedItems.append(workItem)
}
}
}
class MockPersistence: UUIDBackfillTask.PersistenceProvider {
var unknownRecipients: Set<SignalRecipient> = Set()
var expectedRegistration: Set<SignalServiceAddress> = Set()
var expectedUnregistration: Set<SignalServiceAddress> = Set()
var registered: Set<SignalServiceAddress> = Set()
var unregistered: Set<SignalServiceAddress> = Set()
var fetchInvocations = 0
var registerInvocations = 0
override func fetchRegisteredRecipientsWithoutUUID() -> [SignalRecipient] {
fetchInvocations += 1
return Array(unknownRecipients)
}
override func updateSignalRecipients(registering addressesToRegister: [SignalServiceAddress],
unregistering addressesToUnregister: [SignalServiceAddress]) {
registerInvocations += 1
unknownRecipients = unknownRecipients.filter { unknown in
let wasRegistered = addressesToRegister
.contains(where: { $0.phoneNumber == unknown.recipientPhoneNumber })
let wasUnregistered = addressesToUnregister
.contains(where: { $0.phoneNumber == unknown.recipientPhoneNumber })
return (!wasRegistered && !wasUnregistered)
}
registered.formUnion(addressesToRegister)
unregistered.formUnion(addressesToUnregister)
}
func verifySuccess() {
XCTAssertEqual(registered, expectedRegistration)
XCTAssertEqual(unregistered, expectedUnregistration)
XCTAssertTrue(unknownRecipients.isEmpty)
XCTAssertEqual(fetchInvocations, 1)
if expectedRegistration.isEmpty && expectedUnregistration.isEmpty {
XCTAssertEqual(registerInvocations, 0)
} else {
XCTAssertEqual(registerInvocations, 1)
}
}
func verifyFailure() {
XCTAssertEqual(registered.count, 0)
XCTAssertEqual(unregistered.count, 0)
XCTAssertEqual(fetchInvocations, 1)
XCTAssertEqual(registerInvocations, 0)
}
}
class MockNetwork: UUIDBackfillTask.NetworkProvider {
var requestCount = 0
var scheduledErrors: [Error] = []
var expectedRequest: Set<String> = Set()
var finalResult: Set<CDSRegisteredContact> = Set()
override func fetchServiceAddress(for phoneNumbers: [String],
completion: @escaping (Set<CDSRegisteredContact>, Error?) -> Void) {
requestCount += 1
XCTAssertEqual(Set(phoneNumbers), expectedRequest)
if let error = scheduledErrors.first {
completion(Set(), error)
scheduledErrors.remove(at: 0)
} else {
let milliseconds = Int.random(in: 0...300)
DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(milliseconds)) {
completion(self.finalResult, nil)
}
}
}
func verify(requestCount expected: Int) {
XCTAssertEqual(requestCount, expected)
}
}
func e164(from number: String) -> String? {
return PhoneNumber.tryParsePhoneNumber(fromUserSpecifiedText: number)?.toE164()
}
func configureMocks(expectingRegistrationFor registeredNumbers: [String],
expectingUnregistrationFor unregisteredNumbers: [String],
forcedFailures: [Error] = []) {
let initialRecipientSet = Set((registeredNumbers + unregisteredNumbers)
.map { SignalRecipient(address: SignalServiceAddress(uuid: nil, phoneNumber: $0)) })
let expectedCDSRequestSet = Set((registeredNumbers + unregisteredNumbers)
.compactMap { e164(from: $0) })
let expectedRegistrationSet = Set(registeredNumbers
.map { SignalServiceAddress(uuid: UUID(), phoneNumber: $0) })
let expectedUnregistrationSet = Set(unregisteredNumbers
.map { SignalServiceAddress(uuid: nil, phoneNumber: $0) })
let finalCDSResultSet = Set(expectedRegistrationSet
.map { CDSRegisteredContact(signalUuid: $0.uuid!, e164PhoneNumber: e164(from: $0.phoneNumber!)!) })
persistence.unknownRecipients = initialRecipientSet
persistence.expectedRegistration = expectedRegistrationSet
persistence.expectedUnregistration = expectedUnregistrationSet
network.expectedRequest = expectedCDSRequestSet
network.scheduledErrors = forcedFailures
network.finalResult = finalCDSResultSet
// Verify no duplicate entries
XCTAssertEqual(registeredNumbers.count + unregisteredNumbers.count,
initialRecipientSet.count, "Invalid test configuration, duplicate numbers")
}
}
extension DispatchTimeInterval: Comparable {
private var normalizedNanoseconds: Int64 {
switch self {
case let .seconds(val):
return Int64(val) * Int64(NSEC_PER_SEC)
case let .milliseconds(val):
return Int64(val) * Int64(NSEC_PER_MSEC)
case let .microseconds(val):
return Int64(val) * Int64(NSEC_PER_USEC)
case let .nanoseconds(val):
return Int64(val)
case .never:
return Int64.max
default:
assertionFailure("welp~")
return 0
}
}
public static func < (lhs: DispatchTimeInterval, rhs: DispatchTimeInterval) -> Bool {
return lhs.normalizedNanoseconds < rhs.normalizedNanoseconds
}
}
*/