Signal-iOS/SignalServiceKit/tests/Messages/ThreadFinderTests.swift

355 lines
13 KiB
Swift

//
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import XCTest
import LibSignalClient
@testable import SignalServiceKit
class ThreadFinderTests: XCTestCase {
private var db = InMemoryDB()
private let threadFinder = ThreadFinder()
private var contactThread1: TSContactThread!
private var contactThread2: TSContactThread!
enum ChatListType: CaseIterable {
case inbox
case unread
case archive
}
override func setUp() {
super.setUp()
let testPhone1 = E164("+16505550101")!
let testACI1 = Aci.constantForTesting("00000000-0000-4000-8000-00000000000A")
contactThread1 = TSContactThread(contactAddress: SignalServiceAddress(
serviceId: testACI1,
phoneNumber: testPhone1.stringValue,
cache: SignalServiceAddressCache()
))
let testPhone2 = E164("+16505550100")!
let testACI2 = Aci.constantForTesting("00000000-0000-4000-A000-00000000000B")
contactThread2 = TSContactThread(contactAddress: SignalServiceAddress(
serviceId: testACI2,
phoneNumber: testPhone2.stringValue,
cache: SignalServiceAddressCache()
))
}
func buildThreadRecord(
uniqueID: String,
contactThread: TSContactThread,
draft: String?,
lastInteractionRowID: UInt64,
lastDraftInteractionRowId: UInt64,
lastDraftUpdateTimestamp: UInt64
) -> ThreadRecord {
ThreadRecord(
delegate: contactThread,
recordType: .contactThread,
uniqueId: uniqueID,
conversationColorName: "Obsolete",
creationDate: Date.now.timeIntervalSince1970,
isArchived: false,
lastInteractionRowId: lastInteractionRowID,
messageDraft: draft,
mutedUntilDate: nil,
shouldThreadBeVisible: true,
contactPhoneNumber: nil,
contactUUID: nil,
groupModel: nil,
hasDismissedOffers: false,
isMarkedUnread: false,
lastVisibleSortIdOnScreenPercentage: 0.0,
lastVisibleSortId: 0,
messageDraftBodyRanges: nil,
mentionNotificationMode: 0,
mutedUntilTimestamp: 0,
allowsReplies: nil,
lastSentStoryTimestamp: nil,
name: nil,
addresses: nil,
storyViewMode: 0,
editTargetTimestamp: nil,
lastDraftInteractionRowId: lastDraftInteractionRowId,
lastDraftUpdateTimestamp: lastDraftUpdateTimestamp)
}
func buildThreadAssociatedData(
uniqueID: String,
isMarkedUnread: Bool,
isArchived: Bool
) -> ThreadAssociatedData {
return ThreadAssociatedData(
threadUniqueId: uniqueID,
isArchived: isArchived,
isMarkedUnread: isMarkedUnread,
mutedUntilTimestamp: 0,
audioPlaybackRate: 1
)
}
func newDraftGoesToTop(chatListType: ChatListType) throws {
try db.write { transaction in
let database = transaction.database
// New draft.
try buildThreadRecord(
uniqueID: "UUID1",
contactThread: contactThread1,
draft: "test draft",
lastInteractionRowID: 0,
lastDraftInteractionRowId: 1,
lastDraftUpdateTimestamp: 1
).insert(database)
try buildThreadAssociatedData(
uniqueID: "UUID1",
isMarkedUnread: chatListType == .unread,
isArchived: chatListType == .archive
).insert(database)
// Non-draft that has more recent lastInteractionRowID.
try buildThreadRecord(
uniqueID: "UUID2",
contactThread: contactThread2,
draft: nil,
lastInteractionRowID: 1,
lastDraftInteractionRowId: 0,
lastDraftUpdateTimestamp: 0
).insert(database)
try buildThreadAssociatedData(
uniqueID: "UUID2",
isMarkedUnread: chatListType == .unread,
isArchived: chatListType == .archive
).insert(database)
}
switch chatListType {
case .inbox, .unread:
try db.read { transaction in
let messages = try threadFinder.internal_visibleInboxThreadIds(transaction: transaction)
XCTAssertEqual(messages.count, 2)
XCTAssertTrue(messages.first == "UUID1", "First message should be the draft")
}
case .archive:
try db.read { transaction in
let messages = try threadFinder.internal_visibleArchivedThreadIds(transaction: transaction)
XCTAssertEqual(messages.count, 2)
XCTAssertTrue(messages.first == "UUID1", "First message should be the draft")
}
}
}
func testNewDraftGoesToTop_inbox() throws {
try newDraftGoesToTop(chatListType: .inbox)
}
func testNewDraftGoesToTop_unread() throws {
try newDraftGoesToTop(chatListType: .unread)
}
func testNewDraftGoesToTop_archive() throws {
try newDraftGoesToTop(chatListType: .archive)
}
func ignoreDraftRowIdThatIsNotMostRecent(chatListType: ChatListType) throws {
try db.write { transaction in
let database = transaction.database
// New draft that is not the latest activity on the thread.
try buildThreadRecord(
uniqueID: "UUID1",
contactThread: contactThread1,
draft: "test draft",
lastInteractionRowID: 3,
lastDraftInteractionRowId: 1,
lastDraftUpdateTimestamp: 1
).insert(database)
try buildThreadAssociatedData(
uniqueID: "UUID1",
isMarkedUnread: chatListType == .unread,
isArchived: chatListType == .archive
).insert(database)
// Non-draft that has less recent lastInteractionRowID.
try buildThreadRecord(
uniqueID: "UUID2",
contactThread: contactThread2,
draft: nil,
lastInteractionRowID: 2,
lastDraftInteractionRowId: 0,
lastDraftUpdateTimestamp: 0
).insert(database)
try buildThreadAssociatedData(
uniqueID: "UUID2",
isMarkedUnread: chatListType == .unread,
isArchived: chatListType == .archive
).insert(database)
}
switch chatListType {
case .inbox, .unread:
try db.read { transaction in
let threads = try threadFinder.internal_visibleInboxThreadIds(transaction: transaction)
XCTAssertEqual(threads.count, 2)
XCTAssertTrue(threads.first == "UUID1", "First thread should be the draft thread, even though the draft is not the most recent activity")
}
case .archive:
try db.read { transaction in
let threads = try threadFinder.internal_visibleArchivedThreadIds(transaction: transaction)
XCTAssertEqual(threads.count, 2)
XCTAssertTrue(threads.first == "UUID1", "First thread should be the draft thread, even though the draft is not the most recent activity")
}
}
}
func testIgnoreDraftRowIdThatIsNotMostRecent_inbox() throws {
try ignoreDraftRowIdThatIsNotMostRecent(chatListType: .inbox)
}
func testIgnoreDraftRowIdThatIsNotMostRecent_unread() throws {
try ignoreDraftRowIdThatIsNotMostRecent(chatListType: .unread)
}
func testIgnoreDraftRowIdThatIsNotMostRecent_archive() throws {
try ignoreDraftRowIdThatIsNotMostRecent(chatListType: .archive)
}
func sameDraftRowIdFallsBackToTimestamp(chatListType: ChatListType) throws {
try db.write { transaction in
let database = transaction.database
// Thread 1, has a draft after latest TSInteraction, but less recent than UUID2.
try buildThreadRecord(
uniqueID: "UUID1",
contactThread: contactThread1,
draft: "test draft",
lastInteractionRowID: 2,
lastDraftInteractionRowId: 2,
lastDraftUpdateTimestamp: 1
).insert(database)
try buildThreadAssociatedData(
uniqueID: "UUID1",
isMarkedUnread: chatListType == .unread,
isArchived: chatListType == .archive
).insert(database)
// Thread 2, has a more recent draft based on timestamp.
try buildThreadRecord(
uniqueID: "UUID2",
contactThread: contactThread2,
draft: "test draft",
lastInteractionRowID: 1,
lastDraftInteractionRowId: 2,
lastDraftUpdateTimestamp: 2
).insert(database)
try buildThreadAssociatedData(
uniqueID: "UUID2",
isMarkedUnread: chatListType == .unread,
isArchived: chatListType == .archive
).insert(database)
}
switch chatListType {
case .inbox, .unread:
try db.read { transaction in
let threads = try threadFinder.internal_visibleInboxThreadIds(transaction: transaction)
XCTAssertEqual(threads.count, 2)
XCTAssertTrue(threads.first == "UUID2", "First thread should be the one with latest timestamp")
}
case .archive:
try db.read { transaction in
let threads = try threadFinder.internal_visibleArchivedThreadIds(transaction: transaction)
XCTAssertEqual(threads.count, 2)
XCTAssertTrue(threads.first == "UUID2", "First thread should be the one with latest timestamp")
}
}
}
func testSameDraftRowIdFallsBackToTimestamp_inbox() throws {
try sameDraftRowIdFallsBackToTimestamp(chatListType: .inbox)
}
func testSameDraftRowIdFallsBackToTimestamp_unread() throws {
try sameDraftRowIdFallsBackToTimestamp(chatListType: .unread)
}
func testSameDraftRowIdFallsBackToTimestamp_archive() throws {
try sameDraftRowIdFallsBackToTimestamp(chatListType: .archive)
}
func draftNotMostRecent(chatListType: ChatListType) throws {
try db.write { transaction in
let database = transaction.database
// New draft that is not the latest activity on the thread.
try buildThreadRecord(
uniqueID: "UUID1",
contactThread: contactThread1,
draft: "test draft",
lastInteractionRowID: 1,
lastDraftInteractionRowId: 2,
lastDraftUpdateTimestamp: 100
).insert(database)
try buildThreadAssociatedData(
uniqueID: "UUID1",
isMarkedUnread: chatListType == .unread,
isArchived: chatListType == .archive
).insert(database)
// Non-draft that has less recent lastInteractionRowID.
try buildThreadRecord(
uniqueID: "UUID2",
contactThread: contactThread2,
draft: nil,
lastInteractionRowID: 3,
lastDraftInteractionRowId: 0,
lastDraftUpdateTimestamp: 0
).insert(database)
try buildThreadAssociatedData(
uniqueID: "UUID2",
isMarkedUnread: chatListType == .unread,
isArchived: chatListType == .archive
).insert(database)
}
switch chatListType {
case .inbox, .unread:
try db.read { transaction in
let threads = try threadFinder.internal_visibleInboxThreadIds(transaction: transaction)
XCTAssertEqual(threads.count, 2)
XCTAssertTrue(threads.first == "UUID2", "First thread should be most recent thread, the non-draft")
}
case .archive:
try db.read { transaction in
let threads = try threadFinder.internal_visibleArchivedThreadIds(transaction: transaction)
XCTAssertEqual(threads.count, 2)
XCTAssertTrue(threads.first == "UUID2", "First thread should be most recent thread, the non-draft")
}
}
}
func testDraftNotMostRecent_inbox() throws {
try draftNotMostRecent(chatListType: .inbox)
}
func testDraftNotMostRecent_unread() throws {
try draftNotMostRecent(chatListType: .unread)
}
func testDraftNotMostRecent_archive() throws {
try draftNotMostRecent(chatListType: .archive)
}
}