Signal-iOS/Signal/test/util/GroupAndContactStreamTest.swift
2021-10-22 19:09:44 -03:00

419 lines
18 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import XCTest
import Contacts
class GroupAndContactStreamTest: SignalBaseTest {
// MARK: - Test Life Cycle
override func setUp() {
super.setUp()
// ensure local client has necessary "registered" state
let localE164Identifier = "+13235551234"
let localUUID = UUID(uuidString: "83DF967A-5CBD-43EB-AE9A-571A930F78D6")!
tsAccountManager.registerForTests(withLocalNumber: localE164Identifier, uuid: localUUID)
}
// MARK: -
let outputContactSyncData = "GwoMKzEzMjMxMTExMTExEgdBbGljZS0xQABYADMSB0FsaWNlLTJAAEokMzFDRTE0MTItOUEyOC00RTZGLUI0RUUtMjIyMjIyMjIyMjIyWABBCgwrMTMyMTMzMzMzMzMSB0FsaWNlLTNAAEokMUQ0QUIwNDUtODhGQi00QzRFLTlGNkEtMzMzMzMzMzMzMzMzWAA="
func test_writeContactSync() throws {
let signalAccounts = [
SignalAccount(address: .init(phoneNumber: "+13231111111")),
SignalAccount(address: .init(uuidString: "31ce1412-9a28-4e6f-b4ee-222222222222")),
SignalAccount(address: .init(uuidString: "1d4ab045-88fb-4c4e-9f6a-333333333333", phoneNumber: "+13213333333"))
]
let streamData = try buildContactSyncData(signalAccounts: signalAccounts)
XCTAssertEqual(streamData.base64EncodedString(), outputContactSyncData)
}
func test_readContactSync() throws {
var contacts: [ContactDetails] = []
let data = Data(base64Encoded: outputContactSyncData)!
try data.withUnsafeBytes { bufferPtr in
if let baseAddress = bufferPtr.baseAddress, bufferPtr.count > 0 {
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
let inputStream = ChunkedInputStream(forReadingFrom: pointer, count: bufferPtr.count)
let contactStream = ContactsInputStream(inputStream: inputStream)
while let nextContact = try contactStream.decodeContact() {
contacts.append(nextContact)
}
}
}
guard contacts.count == 3 else {
XCTFail("unexpected contact count: \(contacts.count)")
return
}
do {
let contact = contacts[0]
XCTAssertEqual("+13231111111", contact.address.phoneNumber)
XCTAssertNil(contact.address.uuid)
XCTAssertEqual("Alice-1", contact.name)
XCTAssertNil(contact.verifiedProto)
XCTAssertNil(contact.profileKey)
XCTAssertEqual(false, contact.isBlocked)
XCTAssertEqual(0, contact.expireTimer)
XCTAssertEqual(false, contact.isArchived)
XCTAssertNil(contact.inboxSortOrder)
}
do {
let contact = contacts[1]
XCTAssertNil(contact.address.phoneNumber)
XCTAssertEqual("31CE1412-9A28-4E6F-B4EE-222222222222", contact.address.uuid?.uuidString)
XCTAssertEqual("Alice-2", contact.name)
XCTAssertNil(contact.verifiedProto)
XCTAssertNil(contact.profileKey)
XCTAssertEqual(false, contact.isBlocked)
XCTAssertEqual(0, contact.expireTimer)
XCTAssertEqual(false, contact.isArchived)
XCTAssertNil(contact.inboxSortOrder)
}
do {
let contact = contacts[2]
XCTAssertEqual("+13213333333", contact.address.phoneNumber)
XCTAssertEqual("1D4AB045-88FB-4C4E-9F6A-333333333333", contact.address.uuid?.uuidString)
XCTAssertEqual("Alice-3", contact.name)
XCTAssertNil(contact.verifiedProto)
XCTAssertNil(contact.profileKey)
XCTAssertEqual(false, contact.isBlocked)
XCTAssertEqual(0, contact.expireTimer)
XCTAssertEqual(false, contact.isArchived)
XCTAssertNil(contact.inboxSortOrder)
}
}
let outputGroupSyncData = "ZAoQc111Fz2xlVb3YbtcfwN0SBoMKzEzMjEzMjE0MzIxGgwrMTMyMTMyMTQzMjMiDgoJaW1hZ2UvcG5nEKMBMABKDhIMKzEzMjEzMjE0MzIxSg4SDCsxMzIxMzIxNDMyM1ABWACJUE5HDQoaCgAAAA1JSERSAAAAAQAAAAEIBgAAAB8VxIkAAAABc1JHQgCuzhzpAAAARGVYSWZNTQAqAAAACAABh2kABAAAAAEAAAAaAAAAAAADoAEAAwAAAAEAAQAAoAIABAAAAAEAAAABoAMABAAAAAEAAAABAAAAAPkinf4AAAANSURBVAgdY/jPwPAfAAUAAf/ID2IWAAAAAElFTkSuQmCCbwoQc222Fz2xlVb3YbtcfwN0SBIJQm9vayBDbHViGgwrMTMyMTMyMTQzMjEaDCsxNTU1MzIxNDMyMyIOCglpbWFnZS9wbmcQowEwAEoOEgwrMTMyMTMyMTQzMjFKDhIMKzE1NTUzMjE0MzIzUAJYAYlQTkcNChoKAAAADUlIRFIAAAABAAAAAQgGAAAAHxXEiQAAAAFzUkdCAK7OHOkAAABEZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAOgAQADAAAAAQABAACgAgAEAAAAAQAAAAGgAwAEAAAAAQAAAAEAAAAA+SKd/gAAAA1JREFUCB1jYGD4/x8AAwIB/6fhVKUAAAAASUVORK5CYIKNAQoQc333Fz2xlVb3YbtcfwN0SBIJQ29vayBCbHViGgwrMTMyMTMyMTMzMzMaDCsxMzIzNTU1MTIzNBoMKzE1NTUzMjEyMjIyIg4KCWltYWdlL3BuZxCjATAASg4SDCsxMzIxMzIxMzMzM0oOEgwrMTMyMzU1NTEyMzRKDhIMKzE1NTUzMjEyMjIyUABYAYlQTkcNChoKAAAADUlIRFIAAAABAAAAAQgGAAAAHxXEiQAAAAFzUkdCAK7OHOkAAABEZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAOgAQADAAAAAQABAACgAgAEAAAAAQAAAAGgAwAEAAAAAQAAAAEAAAAA+SKd/gAAAA1JREFUCB1jYPjP8B8ABAEB/zB9GO4AAAAASUVORK5CYII="
let groupImageData1 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAAD5Ip3+AAAADUlEQVQIHWP4z8DwHwAFAAH/yA9iFgAAAABJRU5ErkJggg=="
let groupImageData2 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAAD5Ip3+AAAADUlEQVQIHWNgYPj/HwADAgH/p+FUpQAAAABJRU5ErkJggg=="
let groupImageData3 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAAD5Ip3+AAAADUlEQVQIHWNg+M/wHwAEAQH/MH0Y7gAAAABJRU5ErkJggg=="
func test_writeGroupSync() throws {
let groupAvatarData1 = Data(base64Encoded: groupImageData1)!
let groupAvatarData2 = Data(base64Encoded: groupImageData2)!
let groupAvatarData3 = Data(base64Encoded: groupImageData3)!
let group1: TSGroupThread = {
let groupId = Data(base64Encoded: "c111Fz2xlVb3YbtcfwN0SA==")!
let groupMembers: [SignalServiceAddress] = [
.init(phoneNumber: "+13213214321"),
.init(uuidString: "1d4ab045-88fb-4c4e-9f6a-f921124bd529", phoneNumber: "+13213214323")
]
var thread: TSGroupThread!
write { transaction in
thread = try! GroupManager.createGroupForTests(members: groupMembers,
avatarData: groupAvatarData1,
groupId: groupId,
transaction: transaction)
}
return thread
}()
let group2: TSGroupThread = {
let groupId = Data(base64Encoded: "c222Fz2xlVb3YbtcfwN0SA==")!
let groupMembers: [SignalServiceAddress] = [
.init(phoneNumber: "+13213214321"),
.init(uuidString: "55555555-88fb-4c4e-9f6a-f921124bd529", phoneNumber: "+15553214323")
]
var thread: TSGroupThread!
write {
thread = try! GroupManager.createGroupForTests(members: groupMembers,
name: "Book Club",
avatarData: groupAvatarData2,
groupId: groupId,
transaction: $0)
thread.shouldThreadBeVisible = true
thread.anyOverwritingUpdate(transaction: $0)
ThreadAssociatedData.fetchOrDefault(
for: thread,
transaction: $0
).updateWith(
isArchived: true,
updateStorageService: false,
transaction: $0
)
}
return thread
}()
let group3: TSGroupThread = {
let groupId = Data(base64Encoded: "c333Fz2xlVb3YbtcfwN0SA==")!
let groupMembers: [SignalServiceAddress] = [
.init(phoneNumber: "+13213213333"),
tsAccountManager.localAddress!,
.init(uuidString: "55555555-88fb-4c4e-9f6a-222222222222", phoneNumber: "+15553212222")
]
var thread: TSGroupThread!
write { transaction in
thread = try! GroupManager.createGroupForTests(members: groupMembers,
name: "Cook Blub",
avatarData: groupAvatarData3,
groupId: groupId,
transaction: transaction)
thread.shouldThreadBeVisible = true
thread.anyOverwritingUpdate(transaction: transaction)
let messageFactory = OutgoingMessageFactory()
messageFactory.threadCreator = { _ in thread }
_ = messageFactory.create(transaction: transaction)
ThreadAssociatedData.fetchOrDefault(
for: thread,
transaction: transaction
).updateWith(
isArchived: true,
updateStorageService: false,
transaction: transaction
)
}
return thread
}()
let streamData = try buildGroupSyncData(groupThreads: [group1, group2, group3])
XCTAssertEqual(streamData.base64EncodedString(), outputGroupSyncData)
}
func test_readGroupSync() throws {
let groupAvatarData1 = Data(base64Encoded: groupImageData1)!
let groupAvatarData2 = Data(base64Encoded: groupImageData2)!
let groupAvatarData3 = Data(base64Encoded: groupImageData3)!
var groups: [GroupDetails] = []
let data = Data(base64Encoded: outputGroupSyncData)!
try data.withUnsafeBytes { bufferPtr in
if let baseAddress = bufferPtr.baseAddress, bufferPtr.count > 0 {
let pointer = baseAddress.assumingMemoryBound(to: UInt8.self)
let inputStream = ChunkedInputStream(forReadingFrom: pointer, count: bufferPtr.count)
let groupStream = GroupsInputStream(inputStream: inputStream)
while let nextGroup = try groupStream.decodeGroup() {
groups.append(nextGroup)
}
}
}
guard groups.count == 3 else {
XCTFail("unexpected group count: \(groups.count)")
return
}
do {
let group = groups[0]
XCTAssertEqual(group.groupId, Data(base64Encoded: "c111Fz2xlVb3YbtcfwN0SA==")!)
XCTAssertEqual(group.name, nil)
XCTAssertEqual(group.memberAddresses, [
SignalServiceAddress(phoneNumber: "+13213214321"),
SignalServiceAddress(uuidString: "1d4ab045-88fb-4c4e-9f6a-f921124bd529", phoneNumber: "+13213214323")
])
XCTAssertEqual(group.isBlocked, false)
XCTAssertEqual(group.expireTimer, 0)
XCTAssertEqual(group.avatarData, groupAvatarData1)
XCTAssertEqual(false, group.isArchived)
XCTAssertEqual(1, group.inboxSortOrder)
}
do {
let group = groups[1]
XCTAssertEqual(group.groupId, Data(base64Encoded: "c222Fz2xlVb3YbtcfwN0SA==")!)
XCTAssertEqual(group.name, "Book Club")
XCTAssertEqual(group.memberAddresses, [
SignalServiceAddress(phoneNumber: "+13213214321"),
SignalServiceAddress(uuidString: "55555555-88fb-4c4e-9f6a-f921124bd529", phoneNumber: "+15553214323")
])
XCTAssertEqual(group.isBlocked, false)
XCTAssertEqual(group.expireTimer, 0)
XCTAssertEqual(group.avatarData, groupAvatarData2)
XCTAssertEqual(true, group.isArchived)
XCTAssertEqual(2, group.inboxSortOrder)
}
do {
let group = groups[2]
XCTAssertEqual(group.groupId, Data(base64Encoded: "c333Fz2xlVb3YbtcfwN0SA==")!)
XCTAssertEqual(group.name, "Cook Blub")
XCTAssertEqual(group.memberAddresses, [
SignalServiceAddress(phoneNumber: "+13213213333"),
tsAccountManager.localAddress!,
SignalServiceAddress(uuidString: "55555555-88FB-4C4E-9F6A-222222222222", phoneNumber: "+15553212222")
])
XCTAssertEqual(group.isBlocked, false)
XCTAssertEqual(group.expireTimer, 0)
XCTAssertEqual(group.avatarData, groupAvatarData3)
XCTAssertEqual(true, group.isArchived)
XCTAssertEqual(0, group.inboxSortOrder)
}
}
func buildContactSyncData(signalAccounts: [SignalAccount]) throws -> Data {
let contactsManager = TestContactsManager()
let dataOutputStream = OutputStream(toMemory: ())
dataOutputStream.open()
let contactsOutputStream = OWSContactsOutputStream(outputStream: dataOutputStream)
for signalAccount in signalAccounts {
let contactFactory = ContactFactory()
contactFactory.fullNameBuilder = {
"Alice-\(signalAccount.recipientAddress.serviceIdentifier!.suffix(1))"
}
contactFactory.cnContactIdBuilder = { "123" }
contactFactory.uniqueIdBuilder = { "123" }
signalAccount.replaceContactForTests(try contactFactory.build())
contactsOutputStream.write(signalAccount,
recipientIdentity: nil,
profileKeyData: nil,
contactsManager: contactsManager,
disappearingMessagesConfiguration: nil,
isArchived: false,
inboxPosition: nil)
}
dataOutputStream.close()
guard !contactsOutputStream.hasError else {
throw OWSAssertionError("contactsOutputStream.hasError")
}
return dataOutputStream.property(forKey: .dataWrittenToMemoryStreamKey) as! Data
}
func buildGroupSyncData(groupThreads: [TSGroupThread]) throws -> Data {
let dataOutputStream = OutputStream(toMemory: ())
dataOutputStream.open()
let groupsOutputStream = OWSGroupsOutputStream(outputStream: dataOutputStream)
read { transaction in
for groupThread in groupThreads {
groupsOutputStream.writeGroup(groupThread, transaction: transaction)
}
}
dataOutputStream.close()
guard !groupsOutputStream.hasError else {
throw OWSAssertionError("contactsOutputStream.hasError")
}
return dataOutputStream.property(forKey: .dataWrittenToMemoryStreamKey) as! Data
}
}
class TestContactsManager: NSObject, ContactsManagerProtocol {
func isSystemContactWithSignalAccount(_ address: SignalServiceAddress) -> Bool {
false
}
func isSystemContactWithSignalAccount(_ address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> Bool {
false
}
func hasNameInSystemContacts(for address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> Bool {
false
}
func comparableName(for address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> String {
self.displayName(for: address)
}
func comparableName(for signalAccount: SignalAccount, transaction: SDSAnyReadTransaction) -> String {
signalAccount.recipientAddress.stringForDisplay
}
func displayName(for address: SignalServiceAddress) -> String {
address.stringForDisplay
}
func displayName(for address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> String {
address.stringForDisplay
}
func displayName(for signalAccount: SignalAccount) -> String {
signalAccount.recipientAddress.stringForDisplay
}
func displayName(for thread: TSThread, transaction: SDSAnyReadTransaction) -> String {
"Fake Name"
}
func displayNameWithSneakyTransaction(thread: TSThread) -> String {
"Fake Name"
}
func shortDisplayName(for address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> String {
address.stringForDisplay
}
func nameComponents(for address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> PersonNameComponents? {
PersonNameComponents()
}
func signalAccounts() -> [SignalAccount] {
[]
}
func isSystemContactWithSneakyTransaction(phoneNumber: String) -> Bool {
return true
}
func isSystemContact(phoneNumber: String, transaction: SDSAnyReadTransaction) -> Bool {
return true
}
func isSystemContactWithSneakyTransaction(address: SignalServiceAddress) -> Bool {
return true
}
func isSystemContact(address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> Bool {
return true
}
func isSystemContact(withSignalAccount phoneNumber: String) -> Bool {
true
}
func isSystemContact(withSignalAccount phoneNumber: String, transaction: SDSAnyReadTransaction) -> Bool {
true
}
func compare(signalAccount left: SignalAccount, with right: SignalAccount) -> ComparisonResult {
.orderedSame
}
public func sortSignalServiceAddresses(_ addresses: [SignalServiceAddress],
transaction: SDSAnyReadTransaction) -> [SignalServiceAddress] {
addresses
}
func cnContact(withId contactId: String?) -> CNContact? {
nil
}
func avatarData(forCNContactId contactId: String?) -> Data? {
nil
}
func avatarImage(forCNContactId contactId: String?) -> UIImage? {
nil
}
var unknownUserLabel: String = "unknown"
}