The TSGroupMember table is updated after every inserted interaction. It asserts that the sender of the interaction is in the group membership. Before, our tests would construct groups with mock membership and then construct outgoing messages with the local address as the sender. This would fail this new requirement. The fix is to make sure that the local address is in the test group membership if we'd like to "send" any messages.
413 lines
18 KiB
Swift
413 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 = "IQoMKzEzMjMxMTExMTExEgdBbGljZS0xIgRibHVlQABYADkSB0FsaWNlLTIiBGJsdWVAAEokMzFDRTE0MTItOUEyOC00RTZGLUI0RUUtMjIyMjIyMjIyMjIyWABHCgwrMTMyMTMzMzMzMzMSB0FsaWNlLTMiBGJsdWVAAEokMUQ0QUIwNDUtODhGQi00QzRFLTlGNkEtMzMzMzMzMzMzMzMzWAA="
|
|
|
|
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)
|
|
XCTAssertEqual("blue", contact.conversationColorName)
|
|
XCTAssertNil(contact.verifiedProto)
|
|
XCTAssertNil(contact.profileKey)
|
|
XCTAssertEqual(false, contact.isBlocked)
|
|
XCTAssertEqual(0, contact.expireTimer)
|
|
XCTAssertNil(contact.avatarData)
|
|
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)
|
|
XCTAssertEqual("blue", contact.conversationColorName)
|
|
XCTAssertNil(contact.verifiedProto)
|
|
XCTAssertNil(contact.profileKey)
|
|
XCTAssertEqual(false, contact.isBlocked)
|
|
XCTAssertEqual(0, contact.expireTimer)
|
|
XCTAssertNil(contact.avatarData)
|
|
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)
|
|
XCTAssertEqual("blue", contact.conversationColorName)
|
|
XCTAssertNil(contact.verifiedProto)
|
|
XCTAssertNil(contact.profileKey)
|
|
XCTAssertEqual(false, contact.isBlocked)
|
|
XCTAssertEqual(0, contact.expireTimer)
|
|
XCTAssertNil(contact.avatarData)
|
|
XCTAssertEqual(false, contact.isArchived)
|
|
XCTAssertNil(contact.inboxSortOrder)
|
|
}
|
|
}
|
|
|
|
let outputGroupSyncData = "awoQc111Fz2xlVb3YbtcfwN0SBoMKzEzMjEzMjE0MzIxGgwrMTMyMTMyMTQzMjMiDgoJaW1hZ2UvcG5nEKMBMAA6BWJyb3duSg4SDCsxMzIxMzIxNDMyMUoOEgwrMTMyMTMyMTQzMjNQAVgAiVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAAD5Ip3+AAAADUlEQVQIHWP4z8DwHwAFAAH/yA9iFgAAAABJRU5ErkJggnoKEHNtthc9sZVW92G7XH8DdEgSCUJvb2sgQ2x1YhoMKzEzMjEzMjE0MzIxGgwrMTU1NTMyMTQzMjMiDgoJaW1hZ2UvcG5nEKMBMAA6CWJsdWVfZ3JleUoOEgwrMTMyMTMyMTQzMjFKDhIMKzE1NTUzMjE0MzIzUAJYAYlQTkcNChoKAAAADUlIRFIAAAABAAAAAQgGAAAAHxXEiQAAAAFzUkdCAK7OHOkAAABEZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAOgAQADAAAAAQABAACgAgAEAAAAAQAAAAGgAwAEAAAAAQAAAAEAAAAA+SKd/gAAAA1JREFUCB1jYGD4/x8AAwIB/6fhVKUAAAAASUVORK5CYIKTAQoQc333Fz2xlVb3YbtcfwN0SBIJQ29vayBCbHViGgwrMTMyMTMyMTMzMzMaDCsxMzIzNTU1MTIzNBoMKzE1NTUzMjEyMjIyIg4KCWltYWdlL3BuZxCjATAAOgRibHVlSg4SDCsxMzIxMzIxMzMzM0oOEgwrMTMyMzU1NTEyMzRKDhIMKzE1NTUzMjEyMjIyUABYAYlQTkcNChoKAAAADUlIRFIAAAABAAAAAQgGAAAAHxXEiQAAAAFzUkdCAK7OHOkAAABEZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAOgAQADAAAAAQABAACgAgAEAAAAAQAAAAGgAwAEAAAAAQAAAAEAAAAA+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)
|
|
thread.updateConversationColorName(.burlap, 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)
|
|
thread.updateConversationColorName(.taupe, transaction: $0)
|
|
thread.archiveThread(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)
|
|
thread.updateConversationColorName(.blue, transaction: transaction)
|
|
|
|
let messageFactory = OutgoingMessageFactory()
|
|
messageFactory.threadCreator = { _ in thread }
|
|
_ = messageFactory.create(transaction: transaction)
|
|
|
|
thread.archiveThread(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.conversationColorName, ConversationColorName.burlap.rawValue)
|
|
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.conversationColorName, ConversationColorName.taupe.rawValue)
|
|
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.conversationColorName, ConversationColorName.blue.rawValue)
|
|
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,
|
|
conversationColorName: ConversationColorName.blue.rawValue,
|
|
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 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 conversationColorName(for address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> ConversationColorName {
|
|
ConversationColorName.taupe
|
|
}
|
|
|
|
func nameComponents(for address: SignalServiceAddress) -> PersonNameComponents? {
|
|
PersonNameComponents()
|
|
}
|
|
|
|
func nameComponents(for address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> PersonNameComponents? {
|
|
PersonNameComponents()
|
|
}
|
|
|
|
func signalAccounts() -> [SignalAccount] {
|
|
[]
|
|
}
|
|
|
|
func isSystemContact(phoneNumber: String) -> Bool {
|
|
true
|
|
}
|
|
|
|
func isSystemContact(address: SignalServiceAddress) -> Bool {
|
|
true
|
|
}
|
|
|
|
func isSystemContact(withSignalAccount phoneNumber: String) -> Bool {
|
|
true
|
|
}
|
|
|
|
func isSystemContact(withSignalAccount phoneNumber: String, transaction: SDSAnyReadTransaction) -> Bool {
|
|
true
|
|
}
|
|
|
|
func hasNameInSystemContacts(for address: SignalServiceAddress) -> Bool {
|
|
false
|
|
}
|
|
|
|
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"
|
|
}
|