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

344 lines
15 KiB
Swift

//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import LibSignalClient
import XCTest
@testable import SignalServiceKit
class BlockingManagerStateTests: SSKBaseTest {
var dut = BlockingManager.State._testing_createEmpty()
override func setUp() {
super.setUp()
SSKEnvironment.shared.databaseStorageRef.read { dut.reloadIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: $0) }
assertInitalState(dut)
}
// MARK: Mutations
func testAddBlockedItems() {
// Setup
let originalChangeToken = dut.changeToken
let blockedGroup = generateRandomGroupModel()
let blockedRecipientId = generateRecipientId()
// Test
// We add everthing twice, only the first pass should return true (i.e. didChange)
XCTAssertTrue(dut.addBlockedGroup(blockedGroup))
XCTAssertTrue(dut.addBlockedRecipientId(blockedRecipientId))
XCTAssertFalse(dut.addBlockedGroup(blockedGroup))
XCTAssertFalse(dut.addBlockedRecipientId(blockedRecipientId))
// Verify All added addresses are contained in each set
XCTAssertEqual(dut.blockedGroupMap[blockedGroup.groupId], blockedGroup)
XCTAssertTrue(dut.blockedRecipientIds.contains(blockedRecipientId))
XCTAssertTrue(dut.isDirty, "Mutations should mark the state as dirty")
XCTAssertTrue(dut.changeToken == originalChangeToken, "Change tokens shouldn't update until we persist")
}
func testRemoveBlockedItems() {
// Setup
let originalChangeToken = dut.changeToken
let victimRecipientId = generateRecipientId()
let victimGroup = generateRandomGroupModel()
dut.addBlockedRecipientId(victimRecipientId)
dut.addBlockedGroup(victimGroup)
for _ in 0..<3 {
dut.addBlockedGroup(generateRandomGroupModel())
dut.addBlockedRecipientId(generateRecipientId())
}
let initialBlockedGroupCount = dut.blockedGroupMap.count
let initialBlockedRecipientCount = dut.blockedRecipientIds.count
// Test
// Remove both a known entry and a (likely) non-entry
XCTAssertNotNil(dut.removeBlockedGroup(victimGroup.groupId))
XCTAssertTrue(dut.removeBlockedRecipientId(victimRecipientId))
XCTAssertNil(dut.removeBlockedGroup(TSGroupModel.generateRandomGroupId(.V2)))
XCTAssertFalse(dut.removeBlockedRecipientId(generateRecipientId()))
// Verify One and only one item in each set should have been removed
XCTAssertEqual(dut.blockedGroupMap.count + 1, initialBlockedGroupCount)
XCTAssertEqual(dut.blockedRecipientIds.count + 1, initialBlockedRecipientCount)
XCTAssertTrue(dut.isDirty, "Mutations should mark the state as dirty")
XCTAssertTrue(dut.changeToken == originalChangeToken, "Change tokens shouldn't update until we persist")
}
func testIncomingSyncReplaces() {
// Setup
var replacementRecipientIds = (0..<2).map { _ in generateRecipientId() }
var replacementGroups = generateGroupMap(count: 2)
func replaceWithCurrentValues() {
dut.replace(
blockedRecipientIds: Set(replacementRecipientIds),
blockedGroups: replacementGroups
)
}
// Test
dut.replace(blockedRecipientIds: Set(), blockedGroups: Dictionary())
let replaceEmptyWithEmpty = dut.isDirty
dut._testingOnly_resetDirtyBit()
replaceWithCurrentValues()
let replaceEmptyWithFull = dut.isDirty
dut._testingOnly_resetDirtyBit()
replaceWithCurrentValues()
let replaceFullWithFull = dut.isDirty
dut._testingOnly_resetDirtyBit()
replacementRecipientIds.append(generateRecipientId())
replaceWithCurrentValues()
let replaceFullWithAnExtraAddress = dut.isDirty
dut._testingOnly_resetDirtyBit()
let newGroup = generateRandomGroupModel()
replacementGroups[newGroup.groupId] = newGroup
replaceWithCurrentValues()
let replaceFullWithAnExtraGroup = dut.isDirty
dut._testingOnly_resetDirtyBit()
replacementRecipientIds.append(generateRecipientId())
replaceWithCurrentValues()
replaceWithCurrentValues()
let replaceFullWithTheSameAddressesTwice = dut.isDirty
dut._testingOnly_resetDirtyBit()
// Verify
XCTAssertFalse(replaceEmptyWithEmpty)
XCTAssertTrue(replaceEmptyWithFull)
XCTAssertFalse(replaceFullWithFull)
XCTAssertTrue(replaceFullWithAnExtraAddress)
XCTAssertTrue(replaceFullWithAnExtraGroup)
XCTAssertTrue(replaceFullWithTheSameAddressesTwice)
}
func testDirtyBitUpdates() {
// Setup
// We apply a sequence of mutations, one after the other, and verify it updates the dirty bit
// as expected
let victimRecipientId = generateRecipientId()
let victimGroup = generateRandomGroupModel()
[
// Insert and remove a bunch of random addresses. Inserts should always mutate. Removes should never mutate.
(generateRecipientId(), false, false),
(generateRecipientId(), true, true),
(generateRecipientId(), true, true),
(generateRecipientId(), false, false),
(generateRandomGroupModel(), false, false),
(generateRandomGroupModel(), true, true),
(generateRandomGroupModel(), true, true),
(generateRecipientId(), false, false),
// Insert and remove the same address/group. Only the first insert or remove should mutate.
(victimRecipientId, false, false),
(victimRecipientId, true, true),
(victimRecipientId, true, false),
(victimRecipientId, true, false),
(victimRecipientId, false, true),
(victimRecipientId, false, false),
(victimGroup, false, false),
(victimGroup, true, true),
(victimGroup, true, false),
(victimGroup, true, false),
(victimGroup, false, true),
(victimGroup, false, false)
].forEach { (changedObject: Any, isInsertion: Bool, expectDirtyBit: Bool) in
// Force reset the dirty bit to test the effect of this single insert/remove
dut._testingOnly_resetDirtyBit()
// Test
let didChange: Bool = {
switch (isInsertion, changedObject) {
case (true, let changedObject as SignalRecipient.RowId):
return dut.addBlockedRecipientId(changedObject)
case (false, let changedObject as SignalRecipient.RowId):
return dut.removeBlockedRecipientId(changedObject)
case (true, let changedObject as TSGroupModel):
return dut.addBlockedGroup(changedObject)
case (false, let changedObject as TSGroupModel):
return dut.removeBlockedGroup(changedObject.groupId) != nil
default:
XCTFail("This case should be impossible")
return false
}
}()
// Verify
XCTAssertEqual(dut.isDirty, expectDirtyBit)
XCTAssertEqual(didChange, expectDirtyBit)
}
}
// MARK: Persistence and Migrations
func testFreshInstall() {
SSKEnvironment.shared.databaseStorageRef.read {
dut.reloadIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: $0)
XCTAssertFalse(dut.needsSync(transaction: $0), "Fresh installs shouldn't need to implicitly sync")
}
}
func testMigrationFromOldKeys() {
typealias Key = BlockingManager.State.PersistenceKey
let storage = BlockingManager.State.keyValueStore
SSKEnvironment.shared.databaseStorageRef.write {
storage.setObject("", key: Key.Legacy.syncedBlockedPhoneNumbersKey.rawValue, transaction: $0.asV2Write)
}
SSKEnvironment.shared.databaseStorageRef.read {
// Test
// We first reset our test object to ensure that it doesn't reuse any cached state.
// A reload would only occur if the change token was updated, which we're not testing here.
dut = BlockingManager.State._testing_createEmpty()
dut.reloadIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: $0)
// Verify
XCTAssertTrue(dut.needsSync(transaction: $0), "Block state requires a sync on first migration")
}
}
func testPersistAndLoad() {
// Setup
let testRecipientId = generateRecipientId()
let testGroup = generateRandomGroupModel()
let initialChangeToken: UInt64 = SSKEnvironment.shared.databaseStorageRef.read {
dut.reloadIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: $0)
return dut.changeToken
}
// Test Add blocked items, persist, reset our local state to force a reload
let changeTokenAfterUpdate: UInt64 = SSKEnvironment.shared.databaseStorageRef.write {
dut.addBlockedRecipientId(testRecipientId)
dut.addBlockedGroup(testGroup)
// Double persist, only the first should be necessary. Dirty bit should be unset.
XCTAssertTrue(dut.isDirty)
XCTAssertTrue(dut.persistIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: $0))
XCTAssertFalse(dut.persistIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: $0))
XCTAssertFalse(dut.isDirty)
return dut.changeToken
}
dut = BlockingManager.State._testing_createEmpty()
// Verify
SSKEnvironment.shared.databaseStorageRef.read {
dut.reloadIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: $0)
XCTAssertEqual(dut.blockedRecipientIds, Set([testRecipientId]))
XCTAssertEqual(dut.blockedGroupMap[testGroup.groupId], testGroup)
XCTAssertEqual(dut.changeToken, changeTokenAfterUpdate)
XCTAssertNotEqual(dut.changeToken, initialChangeToken)
XCTAssertTrue(dut.needsSync(transaction: $0))
}
}
func testSimulatedSyncMessage() {
let recipientId = generateRecipientId()
SSKEnvironment.shared.databaseStorageRef.write {
// Setup
dut.reloadIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: $0)
XCTAssertFalse(dut.needsSync(transaction: $0))
dut.addBlockedRecipientId(recipientId)
XCTAssertTrue(dut.persistIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: $0))
// Test
let needsSyncBefore = dut.needsSync(transaction: $0)
BlockingManager.State.setLastSyncedChangeToken(dut.changeToken, transaction: $0)
let needsSyncAfter = dut.needsSync(transaction: $0)
// Verify
XCTAssertTrue(needsSyncBefore)
XCTAssertFalse(needsSyncAfter)
}
}
func testSimulatedRemoteChange() {
// Setup
// We mutate two different instance of a state object. One lives as an ivar on the test class
// the other lives within the scope of this test. Mutations to one should be reflected in the other
dut = BlockingManager.State._testing_createEmpty()
var remoteState = BlockingManager.State._testing_createEmpty()
SSKEnvironment.shared.databaseStorageRef.read {
dut.reloadIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: $0)
remoteState.reloadIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: $0)
}
let blockedRecipientIds = (0..<3).map { _ in generateRecipientId() }
let blockedGroups = generateGroupMap(count: 3)
let removedBlock = blockedRecipientIds.randomElement()!
// Test #1 Add some items to one state. Ensure it gets reflected in the other state
SSKEnvironment.shared.databaseStorageRef.write { writeTx in
dut.reloadIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: writeTx)
blockedRecipientIds.forEach { dut.addBlockedRecipientId($0) }
blockedGroups.forEach { dut.addBlockedGroup($0.value) }
XCTAssertTrue(dut.persistIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: writeTx))
}
SSKEnvironment.shared.databaseStorageRef.read { readTx in
let oldChangeToken = remoteState.changeToken
remoteState.reloadIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: readTx)
// Verify #1
XCTAssertNotEqual(oldChangeToken, remoteState.changeToken)
XCTAssertEqual(remoteState.blockedRecipientIds, Set(blockedRecipientIds))
XCTAssertEqual(remoteState.blockedGroupMap, blockedGroups)
}
// Test #2 In the opposite direction, remove an item and ensure it gets reflected on the other side
SSKEnvironment.shared.databaseStorageRef.write { writeTx in
remoteState.reloadIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: writeTx)
remoteState.removeBlockedRecipientId(removedBlock)
XCTAssertTrue(remoteState.persistIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: writeTx))
}
SSKEnvironment.shared.databaseStorageRef.read { readTx in
let oldChangeToken = dut.changeToken
dut.reloadIfNecessary(blockedRecipientStore: BlockedRecipientStoreImpl(), tx: readTx)
// Verify #2
XCTAssertNotEqual(oldChangeToken, dut.changeToken)
XCTAssertFalse(dut.blockedRecipientIds.contains(removedBlock))
XCTAssertEqual(dut.blockedGroupMap, blockedGroups)
}
}
// MARK: Helpers
func assertInitalState(_ state: BlockingManager.State) {
XCTAssertEqual(dut.isDirty, false)
XCTAssertEqual(dut.blockedRecipientIds, [])
XCTAssertEqual(dut.blockedGroupMap, [:])
}
func generateRecipientId() -> SignalRecipient.RowId {
return SSKEnvironment.shared.databaseStorageRef.write { tx in
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
return recipientFetcher.fetchOrCreate(serviceId: Aci.randomForTesting(), tx: tx.asV2Write).id!
}
}
func generateGroupMap(count: UInt) -> [Data: TSGroupModel] {
Dictionary(uniqueKeysWithValues: (0..<count).map { _ in
let fakeGroup = generateRandomGroupModel()
return (fakeGroup.groupId, fakeGroup)
})
}
func generateRandomGroupModel() -> TSGroupModel {
GroupManager.fakeGroupModel(groupId: TSGroupModel.generateRandomGroupId(.V2))!
}
}