Signal-iOS/SignalServiceKit/tests/Contacts/UUIDBackfillTaskTest.swift
Michelle Linington 2d1b90f443 Pull request feedback: retry indefinitely on CDS failure
After some discussion, we're okay considering that if CDS is down, all
of Signal is down. This will help protect session state and reduce the
risk of divergence.

Also changed the FeatureFlag guarding this code to
useOnlyModernContactDiscovery.
2020-07-13 18:20:47 -07:00

521 lines
18 KiB
Swift

//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
//
import Foundation
import XCTest
@testable import SignalServiceKit
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_skipProductionCheck = 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?) -> 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
}
}