Signal-iOS/SignalServiceKit/tests/Contacts/UUIDBackfillTaskTest.swift
Michelle Linington ddb2467ebf Added tests to exercise UUIDBackfillTask
This adds a whole bunch of tests for UUIDBackfillTask. This required a
small change to be made in SignalServiceAddress. Currently,
SignalServiceAddress accesses a singleton cache to force updates to any
existing addresses after a UUID is discovered. This breaks a whole bunch
of test verification.

To workaround this, I've added a testing_ flag to modify the singleton
SignalServiceAddressCache to temporarily disable it. This is going to
cause problems if we ever enable test parallelization, but it's all we
can do for now.

This also adds in incremental backoff logic, changes some of the log
statements and disables the task on production.

Also, adopted some minor feedback Matthew provided in the draft PR,
including:
- Rename: {signal -> registered}RecipientsWithoutUUID
- Move to owsFailDebug instead of assertionFailure
- Adopt `IsNetworkConnectivityFailure` for discerning network failures
- Add some extra assertions around our registered/unregistered sets
2020-07-13 18:20:47 -07:00

428 lines
15 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 persistence: MockPersistence! = nil
private var network: MockNetwork! = nil
override func setUp() {
super.setUp()
// The shared signal service address cache will persist UUIDs as soon as they're created
// This messes with some of the verification checks performed in this test suite
SSKEnvironment.shared.signalServiceAddressCache.testing_disableCache = true
persistence = MockPersistence()
network = MockNetwork()
dut = UUIDBackfillTask(persistence: persistence,
network: network)
dut.testing_shortBackoffInterval = true
}
override func tearDown() {
super.tearDown()
SSKEnvironment.shared.signalServiceAddressCache.testing_disableCache = false
}
// MARK: - Tests
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: ["+1234567890"],
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: ["+1234567890"])
// 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 { "+\($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 { "+\($0)" }
let unregistrations = (2234567890..<2234567890+500).map { "+\($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 { "+\($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: ["+1234567890"],
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: ["+1234567890"],
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: ["+1234567890"],
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: ["+1234567890"],
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),
NSError(domain: "TestDomain", code: 7, userInfo: nil),
NSError(domain: "TestDomain", code: 8, userInfo: nil),
NSError(domain: "TestDomain", code: 9, userInfo: nil)
])
// Test
dut.perform {
didComplete.fulfill()
}
// Verify
waitForExpectations(timeout: 10)
persistence.verifyFailure()
network.verify(requestCount: 5)
}
func testMixedFailures() {
// Setup
let didComplete = expectation(description: "Task Completed")
configureMocks(expectingRegistrationFor: ["+1234567890"],
expectingUnregistrationFor: [],
forcedFailures: [
NSError(domain: "TestDomain", code: 1, userInfo: nil),
NSError(domain: "TestDomain", code: 2, 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)
}
}
extension UUIDBackfillTaskTest {
func configureMocks(expectingRegistrationFor registeredNumbers: [String],
expectingUnregistrationFor unregisteredNumbers: [String],
forcedFailures: [Error] = []) {
// Verify no duplicate entries
XCTAssertEqual(registeredNumbers.count + unregisteredNumbers.count,
Set(registeredNumbers + unregisteredNumbers).count, "Invalid test configuration")
let initialRecipients = (registeredNumbers + unregisteredNumbers).map {
SignalRecipient(address: SignalServiceAddress(uuid: nil, phoneNumber: $0))
}
let expectedRegistration = registeredNumbers.map {
SignalServiceAddress(uuid: UUID(), phoneNumber: $0)
}
let expectedUnregistration = unregisteredNumbers.map {
SignalServiceAddress(uuid: nil, phoneNumber: $0)
}
let finalCDSResult = expectedRegistration.map {
CDSRegisteredContact(signalUuid: $0.uuid!, e164PhoneNumber: $0.phoneNumber!)
}
persistence.unknownRecipients = Set(initialRecipients)
persistence.expectedRegistration = Set(expectedRegistration)
persistence.expectedUnregistration = Set(expectedUnregistration)
network.scheduledErrors = forcedFailures
network.finalResult = Set(finalCDSResult)
}
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))
}
// Verify
}
}
extension UUIDBackfillTaskTest {
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 finalResult: Set<CDSRegisteredContact> = Set()
override func fetchServiceAddress(for phoneNumbers: [String],
completion: @escaping (Set<CDSRegisteredContact>, Error?) -> Void) {
requestCount += 1
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)
}
}
}
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
}
}