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

448 lines
14 KiB
Swift

//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import GRDB
import LibSignalClient
import XCTest
@testable import SignalServiceKit
class StoryManagerTest: SSKBaseTest {
override func setUp() {
super.setUp()
databaseStorage.write { tx in
(DependenciesBridge.shared.registrationStateChangeManager as! RegistrationStateChangeManagerImpl).registerForTests(
localIdentifiers: .forUnitTests,
tx: tx.asV2Write
)
}
}
// MARK: - Message Creation
func testProcessIncomingStoryMessage_createsPrivateStoryWithWhitelistedAuthor() throws {
let timestamp = Date().ows_millisecondsSince1970
let author = Aci.randomForTesting()
let storyMessage = try Self.makePrivateStory()
try write {
profileManager.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0
)
try StoryManager.processIncomingStoryMessage(
storyMessage,
timestamp: timestamp,
author: author,
localIdentifiers: .forUnitTests,
transaction: $0
)
// Message should have been created.
let message = StoryFinder.story(
timestamp: timestamp,
author: author,
transaction: $0
)
XCTAssertNotNil(message)
}
}
func testProcessIncomingStoryMessage_dropsPrivateStoryWithUnwhitelistedAuthor() throws {
let timestamp = Date().ows_millisecondsSince1970
let author = Aci.randomForTesting()
let storyMessage = try Self.makePrivateStory()
try write {
try StoryManager.processIncomingStoryMessage(
storyMessage,
timestamp: timestamp,
author: author,
localIdentifiers: .forUnitTests,
transaction: $0
)
// Message should not have been created.
let message = StoryFinder.story(
timestamp: timestamp,
author: author,
transaction: $0
)
XCTAssertNil(message)
}
}
func testProcessIncomingStoryMessage_dropsStoryWithBlockedAuthor() throws {
let timestamp = Date().ows_millisecondsSince1970
let author = Aci.randomForTesting()
let privateStoryMessage = try Self.makePrivateStory()
let groupStoryMessage = try Self.makeGroupStory()
try write {
profileManager.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0
)
blockingManager.addBlockedAddress(
SignalServiceAddress(author),
blockMode: .localShouldNotLeaveGroups,
transaction: $0
)
try StoryManager.processIncomingStoryMessage(
privateStoryMessage,
timestamp: timestamp,
author: author,
localIdentifiers: .forUnitTests,
transaction: $0
)
try StoryManager.processIncomingStoryMessage(
groupStoryMessage,
timestamp: timestamp,
author: author,
localIdentifiers: .forUnitTests,
transaction: $0
)
// Message should not have been created.
let message = StoryFinder.story(
timestamp: timestamp,
author: author,
transaction: $0
)
XCTAssertNil(message)
}
}
func testProcessIncomingStoryMessage_dropsStoryWithBlockedGroup() throws {
let timestamp = Date().ows_millisecondsSince1970
let author = Aci.randomForTesting()
let storyMessage = try Self.makeGroupStory()
let groupId = try groupsV2.groupV2ContextInfo(forMasterKeyData: storyMessage.group!.masterKey!).groupId
try write {
profileManager.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0
)
blockingManager.addBlockedGroup(
groupId: groupId,
blockMode: .localShouldNotLeaveGroups,
transaction: $0
)
try StoryManager.processIncomingStoryMessage(
storyMessage,
timestamp: timestamp,
author: author,
localIdentifiers: .forUnitTests,
transaction: $0
)
// Message should not have been created.
let message = StoryFinder.story(
timestamp: timestamp,
author: author,
transaction: $0
)
XCTAssertNil(message)
}
}
func testProcessIncomingStoryMessage_dropsStoryWhenNotAGroupMember() throws {
let timestamp = Date().ows_millisecondsSince1970
let author = Aci.randomForTesting()
let storyMessage = try Self.makeGroupStory()
let groupId = try groupsV2.groupV2ContextInfo(forMasterKeyData: storyMessage.group!.masterKey!).groupId
try write {
profileManager.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0
)
try Self.makeGroupThread(groupId: groupId, transaction: $0)
try StoryManager.processIncomingStoryMessage(
storyMessage,
timestamp: timestamp,
author: author,
localIdentifiers: .forUnitTests,
transaction: $0
)
// Message should not have been created.
let message = StoryFinder.story(
timestamp: timestamp,
author: author,
transaction: $0
)
XCTAssertNil(message)
}
}
func testProcessIncomingStoryMessage_dropsAnnouncementStoryWhenNotAnAdmin() throws {
let timestamp = Date().ows_millisecondsSince1970
let author = Aci.randomForTesting()
let storyMessage = try Self.makeGroupStory()
let groupId = try groupsV2.groupV2ContextInfo(forMasterKeyData: storyMessage.group!.masterKey!).groupId
try write {
profileManager.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0
)
try Self.makeGroupThread(groupId: groupId, announcementOnly: true, members: [author], transaction: $0)
try StoryManager.processIncomingStoryMessage(
storyMessage,
timestamp: timestamp,
author: author,
localIdentifiers: .forUnitTests,
transaction: $0
)
// Message should not have been created.
let message = StoryFinder.story(
timestamp: timestamp,
author: author,
transaction: $0
)
XCTAssertNil(message)
}
}
func testProcessIncomingStoryMessage_createsAnnouncementStoryWhenAnAdmin() throws {
let timestamp = Date().ows_millisecondsSince1970
let author = Aci.randomForTesting()
let storyMessage = try Self.makeGroupStory()
let groupId = try groupsV2.groupV2ContextInfo(forMasterKeyData: storyMessage.group!.masterKey!).groupId
try write {
profileManager.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0
)
try Self.makeGroupThread(groupId: groupId, announcementOnly: true, admins: [author], transaction: $0)
try StoryManager.processIncomingStoryMessage(
storyMessage,
timestamp: timestamp,
author: author,
localIdentifiers: .forUnitTests,
transaction: $0
)
// Message should have been created.
let message = StoryFinder.story(
timestamp: timestamp,
author: author,
transaction: $0
)
XCTAssertNotNil(message)
}
}
func testProcessIncomingStoryMessage_createsStoryWhenAValidGroupMember() throws {
let timestamp = Date().ows_millisecondsSince1970
let author = Aci.randomForTesting()
let storyMessage = try Self.makeGroupStory()
let groupId = try groupsV2.groupV2ContextInfo(forMasterKeyData: storyMessage.group!.masterKey!).groupId
try write {
profileManager.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0
)
try Self.makeGroupThread(groupId: groupId, members: [author], transaction: $0)
try StoryManager.processIncomingStoryMessage(
storyMessage,
timestamp: timestamp,
author: author,
localIdentifiers: .forUnitTests,
transaction: $0
)
// Message should have been created.
let message = StoryFinder.story(
timestamp: timestamp,
author: author,
transaction: $0
)
XCTAssertNotNil(message)
}
}
func testProcessIncomingStoryMessage_storeAuthorProfileKey() throws {
let timestamp = Date().ows_millisecondsSince1970
let author = Aci.randomForTesting()
let storyMessage = try Self.makePrivateStory()
try write {
profileManager.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0
)
try StoryManager.processIncomingStoryMessage(
storyMessage,
timestamp: timestamp,
author: author,
localIdentifiers: .forUnitTests,
transaction: $0
)
let profileKey = profileManager.profileKeyData(
for: SignalServiceAddress(author),
transaction: $0
)
XCTAssertEqual(profileKey, storyMessage.profileKey)
}
}
func testProcessIncomingStoryMessage_dropMessageThatAlreadyExists() throws {
let timestamp = Date().ows_millisecondsSince1970
let author = Aci.randomForTesting()
let storyMessage = try Self.makePrivateStory()
try write {
try StoryMessage.create(
withIncomingStoryMessage: storyMessage,
timestamp: timestamp,
receivedTimestamp: timestamp,
author: author,
transaction: $0
)
profileManager.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0
)
try StoryManager.processIncomingStoryMessage(
storyMessage,
timestamp: timestamp,
author: author,
localIdentifiers: .forUnitTests,
transaction: $0
)
let count = try StoryMessage
.filter(Column(StoryMessage.columnName(.timestamp)) == timestamp)
.filter(Column(StoryMessage.columnName(.authorAci)) == author.serviceIdUppercaseString)
.fetchCount($0.unwrapGrdbRead.database)
XCTAssertEqual(count, 1)
}
}
// MARK: - Helpers
static func makePrivateStory() throws -> SSKProtoStoryMessage {
let storyMessageBuilder = SSKProtoStoryMessage.builder()
storyMessageBuilder.setFileAttachment(makeImageAttachment())
storyMessageBuilder.setProfileKey(Randomness.generateRandomBytes(32))
return try storyMessageBuilder.build()
}
static func makeImageAttachment() -> SSKProtoAttachmentPointer {
let builder = SSKProtoAttachmentPointer.builder()
builder.setCdnID(1)
builder.setKey(Randomness.generateRandomBytes(32))
builder.setContentType(MimeType.imageJpeg.rawValue)
return builder.buildInfallibly()
}
static func makeGroupStory() throws -> SSKProtoStoryMessage {
let storyMessageBuilder = SSKProtoStoryMessage.builder()
storyMessageBuilder.setFileAttachment(makeImageAttachment())
storyMessageBuilder.setProfileKey(Randomness.generateRandomBytes(32))
let groupContext = makeGroupContext()
let groupInfo = try makeGroupContextInfo(for: groupContext)
(groupsV2 as! MockGroupsV2).groupV2ContextInfos[groupContext.masterKey!] = groupInfo
storyMessageBuilder.setGroup(groupContext)
return try storyMessageBuilder.build()
}
static func makeGroupThread(
groupId: Data,
announcementOnly: Bool = false,
members: [Aci] = [],
admins: [Aci] = [],
transaction: SDSAnyWriteTransaction
) throws {
var membershipBuilder = GroupMembership.Builder()
for member in members {
membershipBuilder.addFullMember(member, role: .normal)
}
for admin in admins {
membershipBuilder.addFullMember(admin, role: .administrator)
}
var modelBuilder = TSGroupModelBuilder()
modelBuilder.groupId = groupId
modelBuilder.groupMembership = membershipBuilder.build()
modelBuilder.isAnnouncementsOnly = announcementOnly
let groupModel = try modelBuilder.buildAsV2()
let thread = TSGroupThread(groupModel: groupModel)
thread.anyInsert(transaction: transaction)
}
static func makeGroupContext() -> SSKProtoGroupContextV2 {
let builder = SSKProtoGroupContextV2.builder()
builder.setMasterKey(Data())
builder.setRevision(1)
return builder.buildInfallibly()
}
static func makeGroupContextInfo(for context: SSKProtoGroupContextV2) throws -> GroupV2ContextInfo {
let bytes: [UInt8] = .init(repeating: 1, count: Int(kGroupIdLengthV2))
return GroupV2ContextInfo(
masterKeyData: context.masterKey!,
groupSecretParamsData: Data(),
groupId: Data(bytes)
)
}
}