TM-SGNL-iOS/SignalServiceKit/tests/Contacts/Discovery/ContactDiscoveryManagerTest.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

149 lines
6.6 KiB
Swift

//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import LibSignalClient
import XCTest
@testable import SignalServiceKit
final class ContactDiscoveryManagerTest: XCTestCase {
private class MockContactDiscoveryTaskQueue: ContactDiscoveryTaskQueue {
var onPerform: ((Set<String>, ContactDiscoveryMode) async throws -> Set<SignalRecipient>)?
func perform(for phoneNumbers: Set<String>, mode: ContactDiscoveryMode) async throws -> Set<SignalRecipient> {
return try await onPerform!(phoneNumbers, mode)
}
static func foundResponse(for phoneNumbers: Set<String>) -> Set<SignalRecipient> {
Set(phoneNumbers.lazy.map {
SignalRecipient(aci: Aci.randomForTesting(), pni: Pni.randomForTesting(), phoneNumber: E164($0)!, deviceIds: [1])
})
}
}
private lazy var taskQueue = MockContactDiscoveryTaskQueue()
private lazy var manager = ContactDiscoveryManagerImpl(contactDiscoveryTaskQueue: taskQueue)
func testQueueing() async throws {
// Start the first stateful request, but don't resolve it yet.
let initialRequest = CancellableContinuation<CheckedContinuation<Set<SignalRecipient>, any Error>>()
taskQueue.onPerform = { phoneNumbers, mode in
return try await withCheckedThrowingContinuation { continuation in
initialRequest.resume(with: .success(continuation))
}
}
async let _ = manager.lookUp(phoneNumbers: ["+16505550100"], mode: .contactIntersection)
let initialContinuation = try await initialRequest.wait()
// Schedule the next stateful request, which will be queued.
taskQueue.onPerform = { phoneNumbers, mode in
return MockContactDiscoveryTaskQueue.foundResponse(for: phoneNumbers)
}
async let queuedResult = manager.lookUp(phoneNumbers: ["+16505550101"], mode: .contactIntersection)
// Finish the initial request, which should unblock the queued request.
initialContinuation.resume(returning: [])
let queuedResults = try await queuedResult.map { $0.phoneNumber!.stringValue }
XCTAssertEqual(queuedResults, ["+16505550101"])
}
func testRateLimit() async throws {
let retryDate1 = Date(timeIntervalSinceNow: 30)
let retryDate2 = Date(timeIntervalSinceNow: 60)
// Step 1: Contact intersection fails with a rate limit error.
taskQueue.onPerform = { phoneNumbers, mode in
throw ContactDiscoveryError.rateLimit(retryAfter: retryDate1)
}
let result1 = try await lookUpAndReturnRateLimitDate(phoneNumbers: ["+16505550100"], mode: .contactIntersection)
XCTAssertEqual(result1, retryDate1)
// Step 2: One-off requests should still be possible, despite the earlier error.
taskQueue.onPerform = { phoneNumbers, mode in
throw ContactDiscoveryError.rateLimit(retryAfter: retryDate2)
}
let result2 = try await lookUpAndReturnRateLimitDate(phoneNumbers: ["+16505550100"], mode: .oneOffUserRequest)
XCTAssertEqual(result2, retryDate2)
// Step 3: Contact intersection should now be stuck behind the one-off retry date.
taskQueue.onPerform = nil
let result3 = try await lookUpAndReturnRateLimitDate(phoneNumbers: ["+16505550100"], mode: .contactIntersection)
XCTAssertEqual(result3, retryDate2)
}
func testUndiscoverableCache() async throws {
let phoneNumber1 = "+16505550101"
let phoneNumber2 = "+16505550102"
let phoneNumber3 = "+16505550103"
let phoneNumber4 = "+16505550104"
// Populate the cache with empty phone numbers.
taskQueue.onPerform = { phoneNumbers, mode in
if phoneNumbers == [phoneNumber1, phoneNumber2, phoneNumber3] {
return []
}
throw OWSGenericError("Invalid request.")
}
let result1 = try await lookUpAndReturnResult(phoneNumbers: [phoneNumber1, phoneNumber2, phoneNumber3], mode: .outgoingMessage)
XCTAssertEqual(result1, [])
// Send a request for some of the same numbers -- these should be de-duped.
taskQueue.onPerform = { phoneNumbers, mode in
if phoneNumbers == [] {
return []
}
throw OWSGenericError("Invalid request.")
}
let result2 = try await lookUpAndReturnResult(phoneNumbers: [phoneNumber1, phoneNumber2], mode: .outgoingMessage)
XCTAssertEqual(result2, [])
// Send another request, but include an unknown number to force a request.
taskQueue.onPerform = { phoneNumbers, mode in
if phoneNumbers == [phoneNumber1, phoneNumber4] {
return MockContactDiscoveryTaskQueue.foundResponse(for: [phoneNumber4])
}
throw OWSGenericError("Invalid request.")
}
let result3 = try await lookUpAndReturnResult(phoneNumbers: [phoneNumber1, phoneNumber4], mode: .outgoingMessage)
XCTAssertEqual(result3, [phoneNumber4])
}
private func lookUpAndReturnResult(phoneNumbers: Set<String>, mode: ContactDiscoveryMode) async throws -> Set<String> {
let phoneNumbers = try await manager.lookUp(phoneNumbers: phoneNumbers, mode: mode).map {
$0.phoneNumber!.stringValue
}
return Set(phoneNumbers)
}
private func lookUpAndReturnRateLimitDate(phoneNumbers: Set<String>, mode: ContactDiscoveryMode) async throws -> Date? {
do {
_ = try await manager.lookUp(phoneNumbers: phoneNumbers, mode: mode)
return nil
} catch ContactDiscoveryError.rateLimit(let retryAfter) {
return retryAfter
}
}
/// Ensures that all modes are included in `allCasesOrderedByRateLimitPriority.`
///
/// This test is written weirdly so that the compiler will complain if you
/// add a new mode without also updating this test. If you add a new mode &
/// update this test but don't add it to the list sorted by priority, you'll
/// get a test failure.
func testModeRateLimitPriority() {
let allCases = ContactDiscoveryMode.allCasesOrderedByRateLimitPriority
let uniqueCases = Set(allCases)
XCTAssertEqual(allCases.count, uniqueCases.count) // no duplicates
var caseCount = 0
for mode in Set(ContactDiscoveryMode.allCasesOrderedByRateLimitPriority) {
switch mode {
case .oneOffUserRequest, .outgoingMessage, .contactIntersection:
caseCount += 1
}
}
XCTAssertEqual(caseCount, 3) // every case appears
}
}