355 lines
13 KiB
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)
|
|
}
|
|
}
|