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
428 lines
15 KiB
Swift
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
|
|
}
|
|
}
|