Signal-iOS/SignalServiceKit/tests/Contacts/BlockingManagerTests.swift
2024-05-06 09:57:21 -07:00

233 lines
11 KiB
Swift

//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import LibSignalClient
import XCTest
@testable import SignalServiceKit
class BlockingManagerTests: SSKBaseTest {
// Some tests will use this to simulate the state as seen by another process
// If working correctly, state should be reloaded
var remoteState = BlockingManager.State._testing_createEmpty()
override func setUp() {
super.setUp()
databaseStorage.read { remoteState.reloadIfNecessary($0) }
}
override func tearDown() {
super.tearDown()
BlockingManager.TestingFlags.optimisticallyCommitSyncToken = false
}
func testAddBlockedAddress() {
// Setup
let generatedAddresses = generateAddresses(count: 30)
let generatedThread = ContactThreadFactory().create()
// Test
expectation(forNotification: BlockingManager.blockListDidChange, object: nil)
databaseStorage.write { writeTx in
blockingManager.addBlockedThread(generatedThread, blockMode: .localShouldNotLeaveGroups, transaction: writeTx)
generatedAddresses.forEach {
blockingManager.addBlockedAddress($0, blockMode: .localShouldNotLeaveGroups, transaction: writeTx)
}
}
// Verify
databaseStorage.read { readTx in
// First, query the whole set of blocked addresses:
// Because these are made up generated addresses, a ACI+e164 that goes in may be low trust
// If that happens, the addresses that come out will be two separate SignalServiceAddresses
// To work around this, we compactMap the result sets to e164/ACI to compare apples to apples
let allFetchedBlockedAddresses = blockingManager.blockedAddresses(transaction: readTx)
let allExpectedBlockedAddresses = generatedAddresses.union([generatedThread.contactAddress])
XCTAssertEqual(
Set(allFetchedBlockedAddresses.compactMap { $0.phoneNumber }),
Set(allExpectedBlockedAddresses.compactMap { $0.phoneNumber })
)
XCTAssertEqual(
Set(allFetchedBlockedAddresses.compactMap { $0.aci }),
Set(allExpectedBlockedAddresses.compactMap { $0.aci })
)
// Next, ensure that querying an individual address or thread works properly
generatedAddresses.forEach {
XCTAssertTrue(blockingManager.isAddressBlocked($0, transaction: readTx))
}
XCTAssertTrue(blockingManager.isAddressBlocked(generatedThread.contactAddress, transaction: readTx))
XCTAssertTrue(blockingManager.isThreadBlocked(generatedThread, transaction: readTx))
// Since this was a local change, we expect to need a sync message
XCTAssertTrue(blockingManager._testingOnly_needsSyncMessage(readTx))
// Reload the remote state and ensure it sees the up-to-date block list.
let oldToken = remoteState.changeToken
remoteState.reloadIfNecessary(readTx)
let newToken = remoteState.changeToken
XCTAssertNotEqual(oldToken, newToken)
XCTAssertEqual(remoteState.blockedPhoneNumbers, Set(allExpectedBlockedAddresses.compactMap { $0.phoneNumber }))
XCTAssertEqual(remoteState.blockedAcis, Set(allExpectedBlockedAddresses.compactMap { $0.aci }))
XCTAssertEqual(remoteState.blockedGroupMap, [:])
}
waitForExpectations(timeout: 3)
}
func testRemoveBlockedAddress() {
// Setup
let blockedContact = CommonGenerator.address()
let unblockedContact = CommonGenerator.address()
let blockedThread = ContactThreadFactory().create()
let unblockedThread = ContactThreadFactory().create()
databaseStorage.write {
blockingManager.addBlockedAddress(blockedContact, blockMode: .localShouldLeaveGroups, transaction: $0)
blockingManager.addBlockedAddress(blockedContact, blockMode: .localShouldLeaveGroups, transaction: $0)
blockingManager.addBlockedThread(blockedThread, blockMode: .localShouldLeaveGroups, transaction: $0)
blockingManager.addBlockedThread(unblockedThread, blockMode: .localShouldLeaveGroups, transaction: $0)
}
databaseStorage.write {
remoteState.reloadIfNecessary($0)
blockingManager._testingOnly_clearNeedsSyncMessage($0)
}
// Test
databaseStorage.write {
blockingManager.removeBlockedAddress(unblockedContact, wasLocallyInitiated: true, transaction: $0)
blockingManager.removeBlockedThread(unblockedThread, wasLocallyInitiated: true, transaction: $0)
}
// Verify
databaseStorage.read { readTx in
XCTAssertTrue(blockingManager.isAddressBlocked(blockedContact, transaction: readTx))
XCTAssertTrue(blockingManager.isThreadBlocked(blockedThread, transaction: readTx))
XCTAssertFalse(blockingManager.isAddressBlocked(unblockedContact, transaction: readTx))
XCTAssertFalse(blockingManager.isThreadBlocked(unblockedThread, transaction: readTx))
// Since this was a local change, we expect to need a sync message
XCTAssertTrue(blockingManager._testingOnly_needsSyncMessage(readTx))
// Reload the remote state and ensure it sees the up-to-date block list.
let oldToken = remoteState.changeToken
remoteState.reloadIfNecessary(readTx)
let newToken = remoteState.changeToken
XCTAssertNotEqual(oldToken, newToken)
let expectedRemainingBlocks = [blockedContact, blockedThread.contactAddress]
XCTAssertEqual(remoteState.blockedAcis, Set(expectedRemainingBlocks.compactMap { $0.aci }))
XCTAssertEqual(remoteState.blockedPhoneNumbers, Set(expectedRemainingBlocks.compactMap { $0.phoneNumber }))
XCTAssertEqual(remoteState.blockedGroupMap, [:])
}
}
func testIncomingSyncMessage() {
// Setup
let victimBlockedAddress = CommonGenerator.address()
let victimGroupId = TSGroupModel.generateRandomV1GroupId()
let blockedAcis = Set((0..<10).map { _ in Aci.randomForTesting() })
let blockedE164s = Set((0..<10).map { _ in CommonGenerator.e164() })
let blockedGroupIds = Set((0..<10).map { _ in TSGroupModel.generateRandomV1GroupId() })
databaseStorage.write { writeTx in
// For our initial state, we insert the victims that we expect to see removed
// and a single item out of our block sets that we expect to see persisted.
blockingManager.addBlockedAddress(
victimBlockedAddress,
blockMode: .localShouldNotLeaveGroups,
transaction: writeTx)
blockingManager.addBlockedGroup(
groupId: victimGroupId,
blockMode: .localShouldNotLeaveGroups,
transaction: writeTx)
blockingManager.addBlockedGroup(
groupId: blockedGroupIds.randomElement()!,
blockMode: .localShouldNotLeaveGroups,
transaction: writeTx)
blockingManager.addBlockedAddress(
SignalServiceAddress(blockedAcis.randomElement()!),
blockMode: .localShouldNotLeaveGroups,
transaction: writeTx)
blockingManager.addBlockedAddress(
SignalServiceAddress(phoneNumber: blockedE164s.randomElement()!),
blockMode: .localShouldNotLeaveGroups,
transaction: writeTx)
}
// Test
databaseStorage.write { writeTx in
blockingManager.processIncomingSync(
blockedPhoneNumbers: blockedE164s,
blockedAcis: Set(blockedAcis),
blockedGroupIds: blockedGroupIds,
tx: writeTx)
}
// Verify
databaseStorage.read { readTx in
// First, our incoming sync message should've cleared our "NeedsSync" flag
XCTAssertFalse(blockingManager._testingOnly_needsSyncMessage(readTx))
// Verify our victims aren't blocked anymore
XCTAssertFalse(blockingManager.isAddressBlocked(victimBlockedAddress, transaction: readTx))
XCTAssertFalse(blockingManager.isGroupIdBlocked(victimGroupId, transaction: readTx))
// Verify everything that came through our sync message is now blocked
XCTAssertTrue(blockedE164s
.map { SignalServiceAddress(phoneNumber: $0) }
.allSatisfy { blockingManager.isAddressBlocked($0, transaction: readTx) })
XCTAssertTrue(blockedAcis
.map { SignalServiceAddress($0) }
.allSatisfy { blockingManager.isAddressBlocked($0, transaction: readTx) })
XCTAssertTrue(blockedGroupIds
.allSatisfy { blockingManager.isGroupIdBlocked($0, transaction: readTx) })
// Finally, verify that any remote state agrees
remoteState.reloadIfNecessary(readTx)
XCTAssertEqual(Set(remoteState.blockedGroupMap.keys), blockedGroupIds)
XCTAssertEqual(remoteState.blockedAcis, Set(blockedAcis))
XCTAssertEqual(remoteState.blockedPhoneNumbers, blockedE164s)
}
}
func testSendSyncMessage() {
// Setup
// ensure local client has necessary "registered" state
let identityManager = DependenciesBridge.shared.identityManager
identityManager.generateAndPersistNewIdentityKey(for: .aci)
Self.databaseStorage.write { tx in
(DependenciesBridge.shared.registrationStateChangeManager as! RegistrationStateChangeManagerImpl).registerForTests(
localIdentifiers: .forUnitTests,
tx: tx.asV2Write
)
}
BlockingManager.TestingFlags.optimisticallyCommitSyncToken = true
// Test
expectation(forNotification: BlockingManager.blockListDidChange, object: nil)
let neededSyncMessage = databaseStorage.write { writeTx -> Bool in
blockingManager.addBlockedAddress(CommonGenerator.address(), blockMode: .localShouldLeaveGroups, transaction: writeTx)
return blockingManager._testingOnly_needsSyncMessage(writeTx)
}
waitForExpectations(timeout: 3)
// Verify
databaseStorage.read {
XCTAssertTrue(neededSyncMessage)
XCTAssertFalse(blockingManager._testingOnly_needsSyncMessage($0))
}
}
// MARK: - Helpers
func generateAddresses(count: UInt) -> Set<SignalServiceAddress> {
Set((0..<count).map { _ in
let hasPhoneNumber = Int.random(in: 0...2) == 0
let hasAci = !hasPhoneNumber || Bool.random()
return CommonGenerator.address(hasAci: hasAci, hasPhoneNumber: hasPhoneNumber)
})
}
}