Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/ThreemaTests/ManuallyTests/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 86 kB image not shown  

Quelle  DbLoadTests.swift   Sprache: JAVA

 

import XCTest

import FileUtility
import ThreemaEssentials

#if THREEMA_CUSTOMER
    @testable import Threema
#elseif THREEMA_WORK
    @testable import Threema_Work
#elseif THREEMA_ONPREM
    @testable import Threema_OnPrem
#elseif CUSTOM_ONPREM
    @testable import Custom_OnPrem
#elseif THREEMA_GREEN
    @testable import Threema_Green
#elseif THREEMA_BLUE
    @testable import Threema_Blue
#endif
@testable import ThreemaFramework

final class DBLoadTests: XCTestCase {

    override func setUp() async throws {
        AppGroup.setAppID(BundleUtil.threemaAppIdentifier())
        AppGroup.setGroupID(BundleUtil.threemaAppGroupIdentifier())
        
        FileUtility.updateSharedInstance(with: FileUtility())

        // Workaround to ensure remote secret is initialized
        let remoteSecretManager = try await AppLaunchManager.shared.initializeRemoteSecret(
            navigationController: nil,
            onDelete: nil,
            onCancel: nil
        )
        
        AppLaunchManager.shared.setRemoteSecretManager(remoteSecretManager)
    }

    func testTargetUnderTest() {
        #if THREEMA_CUSTOMER
            XCTAssertEqual(AppGroup.groupID(), "group.ch.threema")
            XCTAssertEqual(TargetManager.current, .threema)
        #elseif THREEMA_WORK
            XCTAssertEqual(AppGroup.groupID(), "group.ch.threema.work")
            XCTAssertEqual(TargetManager.current, .work)
        #elseif THREEMA_ONPREM
            XCTAssertEqual(AppGroup.groupID(), "group.ch.threema.onprem")
            XCTAssertEqual(TargetManager.current, .onPrem)
        #elseif CUSTOM_ONPREM
            XCTAssertEqual(AppGroup.groupID(), "group.com.custom.onprem")
            XCTAssertEqual(TargetManager.current, .customOnPrem)
        #elseif THREEMA_GREEN
            XCTAssertEqual(AppGroup.groupID(), "group.ch.threema.red")
            XCTAssertEqual(TargetManager.current, .green)
        #elseif THREEMA_BLUE
            XCTAssertEqual(AppGroup.groupID(), "group.ch.threema.work.red")
            XCTAssertEqual(TargetManager.current, .blue)
        #endif
    }

    /// Print out shell commands to copy database and external data form simulator. To prepare database before, run
    /// methods in this file like 'testDbLoad'.
    func testCopyOldVersionOfDatabase() {
        
        let databasePath = FileUtility.shared.appDataDirectory(appGroupID: AppGroup.groupID())?.path
        
        if databasePath != nil {
            print("\nCopy 'old' version of database for testing DB migration:")
            print("""
                \n
                cd \(databasePath!)
                tar cf ThreemaDataOldVersion.tar.gz .ThreemaData_SUPPORT ThreemaData.sqlite
                mv ThreemaDataOldVersion.tar.gz ~/Documents/
                cd ~/Documents
                mkdir ThreemaDataOldVersion
                tar xf ThreemaDataOldVersion.tar.gz -C ./ThreemaDataOldVersion
                \n
                """)
            print(
                "As last step copy the directory 'ThreemaDataOldVersion' via iTunes into 'applicationDocuments' of Threema.\n"
            )
        }

        XCTAssertNotNil(databasePath)
    }

    /// Generate duplicate contacts of each existing contact:
    /// - create a 1:1 conversation with some messages for the first duplicate contact
    /// - create 10 call history entries for the first duplicate contact
    /// - create a group for all existing identities plus all new duplicates, with some messages
    /// This test is useful to test "Settings - Advanced - Contacts Cleanup"
    ///
    /// Note: the database must contain at least 2 nonduplicate contacts before running this test
    func testGenerateDuplicateContacts() async throws {
        let em = PersistenceManager(
            appGroupID: AppGroup.groupID(),
            userDefaults: AppGroup.userDefaults(),
            remoteSecretManager: RemoteSecretProvider.remoteSecretManager
        ).backgroundEntityManager

        let allContactIdentity: [String] = await em.perform {
            em.entityFetcher.contactEntities()?.map(\.identity) ?? [String]()
        }

        guard allContactIdentity.count >= 2 else {
            print("\nSkipping test, please first add at least 2 contacts\n")
            return
        }

        let newDuplicateContacts = await em.perform {
            var newDuplicateContacts = [ContactEntity]()

            for identity in allContactIdentity {

                if let contactsForIdentity = em.entityFetcher.contactEntities(for: identity),
                   contactsForIdentity.count == 1,
                   let contact = contactsForIdentity.first {

                    print("Create duplicate contact for \(identity)")
                    let duplicateContact = em.entityCreator.contactEntity(
                        identity: identity,
                        publicKey: contact.publicKey,
                        sortOrderFirstName: true
                    )
                    duplicateContact.setIdentity(to: identity, sortOrderFirstName: true)
                    duplicateContact.setFirstName(to: "Duplicate of", sortOrderFirstName: true)
                    duplicateContact.setLastName(to: identity, sortOrderFirstName: true)
                    duplicateContact.publicKey = contact.publicKey
                    duplicateContact.contactVerificationLevel = contact.contactVerificationLevel
                    duplicateContact.workContact = contact.workContact
                    duplicateContact.forwardSecurityState = contact.forwardSecurityState
                    duplicateContact.setFeatureMask(to: Int(truncating: contact.featureMask))
                    duplicateContact.isHidden = contact.isHidden
                    duplicateContact.typingIndicator = contact.typingIndicator
                    duplicateContact.readReceipt = contact.readReceipt
                    duplicateContact.contactImportStatus = contact.contactImportStatus
                    duplicateContact.publicNickname = contact.publicNickname

                    newDuplicateContacts.append(duplicateContact)
                }
            }

            return newDuplicateContacts
        }

        guard !newDuplicateContacts.isEmpty else {
            print("No duplicates created, terminating test.")
            return
        }

        await em.performSave {

            // Create 1:1 conversation for the first duplicated contact
            if let firstDuplicateContact = newDuplicateContacts.first {
                self.addOneToOneTextMessage(
                    "This message was received",
                    isOwn: false,
                    contact: firstDuplicateContact,
                    entityManager: em
                )
                self.addOneToOneTextMessage(
                    "This message was sent",
                    isOwn: true,
                    contact: firstDuplicateContact,
                    entityManager: em
                )
                print(
                    "Created (duplicate) 1:1 conversation with \(firstDuplicateContact.identity) and sent/received messages"
                )
            }

            // Create call history entries for duplicate contact
            if let firstDuplicateContact = newDuplicateContacts.first {
                // inspired from CallHistoryManager / CallHistoryManagerTests
                for _ in 0..<10 {
                    let call = em.entityCreator.callEntity(contactEntity: firstDuplicateContact)
                    call.callID = NSNumber(value: UInt32.random(in: UInt32.min..<UInt32.max))
                    call.date = Date()
                }
                print(
                    "Created 10 (invisible) call history entries for duplicate of \(firstDuplicateContact.identity)"
                )
            }
        }

        // Create group with duplicate contacts
        let group = try await createGroup(
            named: "Duplicate members in this group",
            with: allContactIdentity,
            entityManager: em
        )

        await em.performSave {
            group.conversation.members?.formUnion(Set(newDuplicateContacts))
            for duplicateContact in newDuplicateContacts {
                let mainContact = em.entityFetcher.contactEntity(for: duplicateContact.identity)

                self.addGroupTextMessage("From main contact", sender: mainContact, in: group, entityManager: em)
                self.addGroupTextMessage(
                    "From duplicate contact",
                    sender: duplicateContact,
                    in: group,
                    entityManager: em
                )
            }
            print(
                "Created group conversation '\(group.name ?? "-")' with regular and duplicate members and received messages"
            )

            // Add rejected message at the end of the group
            let message = self.addGroupTextMessage(
                "From myself. Rejected by 50% main contacts and 50% duplicates.",
                in: group,
                entityManager: em
            )
            message.sendFailed = NSNumber(booleanLiteral: true)
            for i in 0..<newDuplicateContacts.count {
                // alternate between regular and duplicate contacts
                let duplicate = newDuplicateContacts[i]
                let contact = i % 2 == 0 ? duplicate : em.entityFetcher.contactEntity(for: duplicate.identity)
                message.addToRejectedBy(contact!)
            }
            print(
                "Added message in group '\(group.name ?? "-")' rejected by various main contacts/duplicates"
            )
        }
    }

    /// Add 10000 messages (./Resources/test_texts.json) to ECHOECHO for testing.
    func testLoadTextMessages() throws {
        let testBundle = Bundle(for: DBLoadTests.self)

        guard let textsPath = testBundle.url(forResource: "test_texts", withExtension: "json") else {
            XCTFail("Cannot find file with test texts")
            return
        }
        
        let texts = try JSONDecoder().decode([String].self, from: Data(contentsOf: textsPath))
        
        var newConversation: ConversationEntity?
        
        _ = createContacts(for: ["ECHOECHO"])
        
        let entityManager = BusinessInjector.ui.entityManager
        entityManager.performAndWaitSave {
            if let contact = entityManager.entityFetcher.contactEntity(for: "ECHOECHO") {
                newConversation = entityManager.conversation(forContact: contact, createIfNotExisting: true)
            }
        }
        
        let conversation = try XCTUnwrap(newConversation)
        
        entityManager.performAndWaitSave {
            for index in 0..<100_000 {
                let calendar = Calendar.current
                let date = calendar.date(byAdding: .hour, value: index, to: Date(timeIntervalSince1970: 0))!
                let message = entityManager.entityCreator.textMessageEntity(
                    text: "\(index) - \(texts[index % texts.count])",
                    in: conversation,
                    setLastUpdate: true
                )
                message.date = date
                message.sender = conversation.contact
                message.sent = true
                message.delivered = true
                message.read = true
                message.remoteSentDate = Date.now
                print("\(index)/100'000")
            }
        }
    }

    func testImportTestLiveIDs() {
        let testIDs = [
            "4SA2FT56", "Y7D72UZ2", "YT2UREKN", "Y2ZS4H4Y", "HV7UKC9B", "S8T7SKVP", "JEAR4C68", "299N6FAF",
            "8JA7ZRHX", "NJ9NYUNU", "3AD82KSN", "8T6Y4MTA", "C2NTU8DS", "ABP323M8", "MSAAJY4R", "CF6BWXYE",
            "A9CDB69Z", "BNKTHA3X", "W9JYWXHM", "MRB4X6NY", "JTPT46YP", "6M59BE5H", "3328D2U6", "PWM5HHTK",
            "D4NVXVYZ", "9UNACVPH", "94SBW52N", "2MCMK2V8", "JBB56TAU", "8PFR4VMC", "V92D9CDN", "38S27RMC",
            "CPUC6WH9", "C97DW8TJ", "7RWVXVY9", "BJJFNHEZ", "8P3H8ADN", "U6JXRHKX", "Z3PFEKWT", "NZETVFTY",
            "ENUA5S5D", "6HJTP8XV", "PBJST426", "ZB48CUSJ", "Z3JJZ436", "ZU5TRUAM", "H6AXSHKC", "5U38HREW",
            "9N2A8689", "NS5EJ4JJ", "95MHAUMK", "UDNEPRMM", "MYJJF7WH", "AX6BBU4K", "RBT83N6W", "P3PM59D7",
            "XWPSAUSE", "DY7HMAKN", "ECP4HFWE", "FXHR5WFX", "XERCUKNS", "NRH4F2JM", "RY4PAMSV", "FD4D5H6H",
            "DH5WZHYK", "HJA2S8HA", "2HC47N7Z", "RNJRZPWF", "TCZ3AZ9U", "78Y2MEV6", "EA3SKCJE", "SCV2RXFD",
            "8V2RMJ28", "C5WF6E79", "669E8U8D", "KZ3CCF88", "RBAS3SBX", "6HSE6VVE", "CXF84N53", "W7KTP29Y",
            "YRE95V2W", "CPXDFB4M", "F35HXBE3", "XRJZTWXP", "5ZDNC5NZ", "7PVKMD6Y", "92KEZ9H9", "98P3ZJFY",
            "ADDRTCNX", "8K43TJ9Y", "JAZR8BHV", "TMZJ3E5P", "N9KS9ZMX", "FS46TYHE", "FNHVBMD2", "X6KN2NKU",
            "ASDUT598", "F4NWU2WY", "2R9TXZMV", "9X7XBJAN", "DFNMZWRZ", "N4PKWSJ5", "NX57R85W", "95RA89WF",
            "57JTE5EK", "TMV4ZCBK", "97NHH47W", "X7P8B8MM", "5AJ6WD6B", "U89FSTN8", "CHANPKUP", "NKPMFP92",
            "Y4T3YKW6", "4H6FD5AM", "AYPJVYRS", "D3YAX8BB", "664NJK2C", "YFUF8T5M", "YV3YD9YH", "E272NS79",
            "DJ8TBMWR", "CH5E8AAF", "R9V9EZ55", "NZ7RSKUB", "5SWJ9KPM", "S4XZPWER", "CZT7WN8C", "J2E9UPY3",
            "7KMUDH6Z", "583ACKJJ", "4MHT43PT", "A322VDCZ", "V44EUFVE", "AJ78UVJD", "JRDDC6J9", "89WPPC28",
            "M3Z7A9NF", "TNPEMDAM", "TJT4Y8HD", "9D58MRVF", "VD59NC8Y", "TPVCT7XB", "RN57D48P", "V7T4NFUF",
            "9S5NZ2MS", "FJUTHMXN", "4N6EKVVB", "22YWHRNT", "N9RJCB6H", "86DWR5FS", "FCU2RFU9", "BUHJVECK",
            "7YC7JRY5", "2H35ZCHN", "YVRKD6K6", "K7D2DVPA", "AH5K4J36", "9E55UB2K", "BVX4NFF5", "J8CKV4C2",
            "F9CTVB88", "RZ6V4K7P", "3BZ66RCS", "YSWVXUZP", "H8P8FSK7", "J4NZ8A9F", "Z3R4JBKY", "C36BBD72",
            "DD52YEPB", "8U88DSJA", "UE9C74SD", "AUBDPXFM", "AW5BE83S", "58NJCKPD", "9877UEYD", "X5CNADHT",
            "F7F8VD53", "4995K37U", "4KBMDY73", "AZ2HKVXZ", "BR9C76FH", "7TADJ5K7", "JBYYFUMY", "2SKTMD5T",
            "TV4TRXYW", "BVNNVD5J", "48B4E2ER", "92WZ8DDH", "EJZJX2MM", "CT8BW584", "BDB72MVX", "BAXV3XUK",
            "45MVVWHB", "7CXV8JUK", "2DET88WJ", "3ENE9X42", "EJEWJ8F7", "8827FF7U", "FEAUXBT3", "MVZ7NYVR",
            "N46FZW8C", "3F87E2D5", "EWTV2K82", "KYK5YW9E", "V3BESYSR", "53YCJ4ZS", "PFAPHCT7", "KNZNMSX5",
            "Y3ER2RH7", "R9ND6T48", "3K6KHP7C", "U4DZ9JCN", "MSR393YW", "A336U2RE", "Z3BVCPA3", "JF96657J",
            "B5CZA8YK", "3ECSYCJD", "X94D97KN", "PV2R9X34", "8XDVA2F7", "RXR58XAS", "7VVC63T5", "AYNJNXKY",
            "AUWNU8H4", "PDE5CCXY", "BUH7VMKZ", "WTJK9VST", "9RXHBE2N", "YZH66YC6", "677Y6YVW", "2D4EVX57",
            "77PW947J", "A3PYY24W", "HZ4EYB6V", "ZPT9VWPD", "3Z3EKUAV", "ASDDX5AE", "76NE2Y2C", "PZSX3XUW",
            "5TA948ZN", "TZ5DFKDP", "8T2EY7WY", "5DW6XRVM", "76UA8N39", "UPH2ENA7", "SZMAK3K7", "VKCCWD3J",
            "9WS3HKB3", "WTY5WTRH", "ZBT98WNS", "TPJSH62P", "EWH275UV", "R9XFZKYV", "CTM4XR2J", "JM5VD3UX",
            "AR9394S3", "RX7Y9N9S", "5PTBNNCW", "2UR8PYN2", "8UCCTRJX", "3KWWDN3Z", "3FMRF8V3", "PFT4Y2DP",
            "2MUJAHE7", "WNPFHRFM", "6AXTPA93", "D5PE9W9X", "B9MX8K69", "S2WYS8JA", "SW35VYZF", "HHDEPX43",
            "CTD47XRK", "PFZS2AP8", "J4DP9N3Z", "RN9495A7", "V88EY5E5", "EW4ERCX6", "H78FPRFS", "P7J6AENR",
            "UU7VHDVW", "PSSF8KAA", "PWDVRCDD", "YAKFAKPX", "SFM6DSVZ", "BB4ZTPM9", "UUZW2D9W", "ABYDNFYS",
            "RSDSENRV", "XV7XWHEM", "JESYRPPR", "8FHKVCA2", "XZ8BTYXE", "UNZRF545", "JSYT4MTK", "MWDJBKYB",
            "7TNKPMDU", "2SRE23ST", "ANEYAN3J", "RB7C3E5H",
        ]

        _ = createContacts(for: testIDs)
    }
    
    /// Add 1000 messages to every contacts conversation with wrong last message and count of unread messages.
    /// To test last message workaround (see `AppLaunchTasks.checkLastMessageOfAllConversations()`)
    func testLoadTextMessagesToAllOneToOneConversations() throws {
        let testBundle = Bundle(for: DBLoadTests.self)

        guard let textsPath = testBundle.url(forResource: "test_texts", withExtension: "json") else {
            XCTFail("Cannot find file with test texts")
            return
        }

        let texts = try JSONDecoder().decode([String].self, from: Data(contentsOf: textsPath))

        let entityManager = BusinessInjector.ui.entityManager
        entityManager.performAndWaitSave {
            for contactEntity in entityManager.entityFetcher.contactEntities() ?? [] {
                guard !contactEntity.isHidden else {
                    continue
                }

                _ = entityManager.conversation(forContact: contactEntity, createIfNotExisting: true)
            }
        }

        entityManager.performAndWait {
            for conversation in entityManager.entityFetcher.conversationEntities() ?? [] {
                var lastMessage: BaseMessageEntity?

                entityManager.performAndWaitSave {
                    for index in 0..<1000 {
                        let calendar = Calendar.current
                        let date = calendar.date(byAdding: .second, value: index, to: Date(timeIntervalSince1970: 0))!
                        let message = entityManager.entityCreator.textMessageEntity(
                            text: "\(index) - \(texts[index % texts.count])",
                            in: conversation,
                            setLastUpdate: true
                        )
                        message.isOwn = index % 4 == 0 ? true : false
                        message.date = date
                        message.sender = conversation.contact
                        message.sent = true
                        message.delivered = true
                        message.read = index % 5 == 0 ? true : false
                        message.remoteSentDate = Date.now

                        if index == 998 {
                            lastMessage = message
                        }

                        print("\(index)/1000")
                    }
                }

                entityManager.performAndWaitSave {
                    // Set wrong last message and count of unread messages deliberately
                    conversation.lastMessage = lastMessage
                    conversation.unreadMessageCount = 0
                }
            }
        }
    }

    func testUnreadMessagesCount() throws {
        let testBundle = Bundle(for: DBLoadTests.self)

        guard let textsPath = testBundle.url(forResource: "test_texts", withExtension: "json") else {
            XCTFail("Cannot find file with test texts")
            return
        }

        let texts = try JSONDecoder().decode([String].self, from: Data(contentsOf: textsPath))

        var newConversation: ConversationEntity?

        let entityManager = BusinessInjector.ui.entityManager

        let senders = ["ECHOECHO"]
        _ = createContacts(for: senders)
        for sender in senders {
            entityManager.performAndWaitSave {
                if let contact = entityManager.entityFetcher.contactEntity(for: sender) {
                    newConversation = entityManager.conversation(forContact: contact, createIfNotExisting: true)
                }
            }
            
            let conversation = try XCTUnwrap(newConversation)

            for index in 0..<5000 {
                entityManager.performAndWaitSave {
                    let calendar = Calendar.current
                    let date = calendar.date(byAdding: .second, value: +index, to: Date())!
                    let message = entityManager.entityCreator.textMessageEntity(
                        text: texts[index % texts.count],
                        in: conversation,
                        setLastUpdate: true
                    )
                    let isOwn = index % 3 == 0 ? false : true
                    message.isOwn = NSNumber(booleanLiteral: isOwn)
                    message.date = date
                    message.sent = true
                    message.delivered = true
                    if !isOwn {
                        message.sender = conversation.contact
                        message.read = index % 5 == 0 ? true : false
                        message.readDate = index % 5 == 0 ? date : nil
                    }
                    message.remoteSentDate = date
                }
            }
        }
    }

    func testFillGroupsWithText() throws {
        let testBundle = Bundle(for: DBLoadTests.self)

        guard let textsPath = testBundle.url(forResource: "test_texts", withExtension: "json") else {
            XCTFail("Cannot find file with test texts")
            return
        }
        
        let texts = try JSONDecoder().decode([String].self, from: Data(contentsOf: textsPath))
        
        let entityManager = BusinessInjector.ui.entityManager

        for num in 0..<1000 {
            for groupConversation in entityManager.entityFetcher.groupConversationEntities() ?? [] {
                entityManager.performAndWaitSave {
                    for contact in entityManager.entityFetcher.contactEntities() ?? [] {
                        let calendar = Calendar.current
                        let date = calendar.date(byAdding: .hour, value: 1, to: Date(timeIntervalSince1970: 0))!
                        let message = entityManager.entityCreator.textMessageEntity(
                            text: "Message \(num) from \(contact.identity) \(texts[num % texts.count])",
                            in: groupConversation,
                            setLastUpdate: true
                        )
                        
                        message.date = date
                        message.sender = contact
                        message.isOwn = NSNumber(booleanLiteral: false)
                        message.remoteSentDate = date
                    }
                }
            }
        }

        if let groupConversations = entityManager.entityFetcher.groupConversationEntities() {
            let unreadMessages = UnreadMessages(messageSender: MessageSenderMock(), entityManager: entityManager)
            unreadMessages.totalCount(doCalcUnreadMessagesCountOf: Set(groupConversations))
        }
    }
    
    /// Adding 160 contacts with Threema ID's (./Resources/test_ids.txt) for testing.
    func testLoadContacts() throws {
        let testBundle = Bundle(for: DBLoadTests.self)
        let filePath = try XCTUnwrap(testBundle.path(forResource: "test_ids", ofType: "txt"))
        
        do {
            var fetchIdentities = [String]()
            
            let ids = try String(contentsOfFile: filePath, encoding: .utf8)
            for id in ids.components(separatedBy: .newlines) {
                if !id.isEmpty {
                    fetchIdentities.append(id)
                }
            }
            
            try addContacts(for: fetchIdentities, entityManager: BusinessInjector.ui.entityManager)
        }
        catch {
            print(error)
        }
    }
    
    // This doesn't seem to work right now as the network calls seem to block infinitely
    // This is probably due to the fact of `semaphore.wait()` blocking the main thread and `fetchBulkIdentityInfo`
    // running on the main thread as well.
    private func addContacts(for ids: [String], entityManager: EntityManager) throws {
        let queue = DispatchQueue.global()
        let semaphore = DispatchSemaphore(value: 0)
        
        var pks = [String: Data]()

        queue.async {
            let api = ServerAPIConnector()
            api.fetchBulkIdentityInfo(ids, onCompletion: { identities, publicKeys, _, _, _ in
                
                for index in 0..<(identities!.count - 1) {
                    pks[identities![index] as! String] = Data(base64Encoded: publicKeys![index] as! String)
                }
                
                semaphore.signal()
            }) { error in
                print(error?.localizedDescription ?? "")
                
                semaphore.signal()
            }
        }
        
        semaphore.wait()
        
        for pk in pks {
            print("Add id: \(pk.key)")
            
            entityManager.performAndWaitSave {
                guard entityManager.entityFetcher.contactEntity(for: pk.key) == nil,
                      MyIdentityStore.shared().identity != pk.key else {
                    return
                }

                let contact = entityManager.entityCreator.contactEntity(
                    identity: pk.key,
                    publicKey: pk.value,
                    sortOrderFirstName: true
                )
                contact.contactVerificationLevel = .unverified
                contact.publicNickname = pk.key
                contact.isHidden = false
                contact.workContact = 0
            }
        }
    }
    
    func testAssignImagesToAllContacts() {
        let entityManager = BusinessInjector.ui.entityManager

        for contact in entityManager.entityFetcher.contactEntities() ?? [] {
            
            let testBundle = Bundle(for: DBLoadTests.self)
            let testImageURL = testBundle.url(forResource: "Bild-1-0", withExtension: "jpg")
            let testImageData = try? Data(contentsOf: testImageURL!)
            let resizedTestImage = MediaConverter.scaleImageData(testImageData!, toMaxSize: 512)!
                .jpegData(compressionQuality: 0.99)!
            
            entityManager.performAndWaitSave {
                let imageData = entityManager.entityCreator.imageDataEntity(
                    data: resizedTestImage,
                    size: CGSize(width: 512, height: 512)
                )
                contact.contactImage = imageData
            }
        }
    }
    
    func testLoadImageFileMessages() {
        var conversation: ConversationEntity?
        
        _ = createContacts(for: ["ECHOECHO"])
        let entityManager = PersistenceManager(
            appGroupID: AppGroup.groupID(),
            userDefaults: AppGroup.userDefaults(),
            remoteSecretManager: RemoteSecretProvider.remoteSecretManager
        ).entityManager
        entityManager.performAndWaitSave {
            if let contact = entityManager.entityFetcher.contactEntity(for: "ECHOECHO") {
                conversation = (entityManager.conversation(forContact: contact, createIfNotExisting: true))!
            }
        }
        
        let imageCount = 100_000
        
        let testBundle = Bundle(for: DBLoadTests.self)
        let testImageURL = testBundle.url(forResource: "Bild-1-0", withExtension: "jpg")
        let testImageData = try? Data(contentsOf: testImageURL!)
        
        for i in 0..<imageCount {
            let log = "Image: \(i) / \(imageCount)"
            print(log)
            
            loadImage(with: testImageData!, conversation!, entityManager, log)
        }
    }
    
    func testLoadImageFileMessagesToAllOneToOneConversations() {
        let imageCount = 100

        let testBundle = Bundle(for: DBLoadTests.self)
        let testImageURL = testBundle.url(forResource: "Bild-1-0", withExtension: "jpg")
        let testImageData = try? Data(contentsOf: testImageURL!)

        let entityManager = PersistenceManager(
            appGroupID: AppGroup.groupID(),
            userDefaults: AppGroup.userDefaults(),
            remoteSecretManager: RemoteSecretProvider.remoteSecretManager
        ).entityManager

        for conversation in entityManager.entityFetcher.conversationEntities() ?? [] {
            for i in 0..<imageCount {
                let log = "Image: \(i) / \(imageCount)"
                print(log)

                loadImage(with: testImageData!, conversation, entityManager, log)
            }
        }
    }

    func loadImage(
        with imageData: Data,
        _ conversation: ConversationEntity,
        _ entityManager: EntityManager,
        _ caption: String
    ) {
        
        entityManager.performAndWaitSave {
            let dbFile: FileDataEntity = entityManager.entityCreator.fileDataEntity(data: imageData)
            
            let thumbnailImage = MediaConverter.getThumbnailFor(UIImage(data: imageData)!)!
            let thumbnailData = thumbnailImage.jpegData(compressionQuality: 1.0)!
            let thumbnailFile: ImageDataEntity = entityManager.entityCreator.imageDataEntity(
                data: thumbnailData,
                size: thumbnailImage.size
            )
         
            let message: FileMessageEntity = entityManager.entityCreator.fileMessageEntity(in: conversation)
            message.data = dbFile
            message.thumbnail = thumbnailFile
            message.fileName = "Bild.jpeg"
            message.fileSize = NSNumber(integerLiteral: dbFile.data!.count)
            message.mimeType = "image/jpeg"
            message.type = NSNumber(integerLiteral: 1)
            message.date = Date(timeIntervalSinceReferenceDate: TimeInterval(-1 * Int.random(in: 0...223_456_789)))
            message.deliveryDate = message.date
            message.sender = conversation.contact
            message.sent = NSNumber(booleanLiteral: true)
            message.delivered = NSNumber(booleanLiteral: true)
            message.read = NSNumber(booleanLiteral: true)
            message.remoteSentDate = Date.now
            message.encryptionKey = NaClCrypto.shared().randomBytes(kBlobKeyLen)
            
            do {
                if let jsonWithoutCaption = FileMessageEncoder.jsonString(for: message),
                   let dataWithoutCaption = jsonWithoutCaption.data(using: .utf8),
                   var dict = try JSONSerialization.jsonObject(with: dataWithoutCaption) as? [String: AnyObject] {
                    dict["d"] = caption as AnyObject
                    let dataWithCaption = try JSONSerialization.data(withJSONObject: dict)
                    message.json = String(data: dataWithCaption, encoding: .utf8)
                }
            }
            catch {
                // do nothing
            }
        }
    }
    
    func testLoadVideoFileMessages() {
        var conversation: ConversationEntity?
        
        _ = createContacts(for: ["ECHOECHO"])
        let entityManager = PersistenceManager(
            appGroupID: AppGroup.groupID(),
            userDefaults: AppGroup.userDefaults(),
            remoteSecretManager: RemoteSecretProvider.remoteSecretManager
        ).entityManager
        entityManager.performAndWaitSave {
            if let contact = entityManager.entityFetcher.contactEntity(for: "ECHOECHO") {
                conversation = (entityManager.conversation(forContact: contact, createIfNotExisting: true))!
            }
        }
        
        let videoCount = 100_000
        
        let testBundle = Bundle(for: DBLoadTests.self)
        let testVideoURL = try? XCTUnwrap(testBundle.url(forResource: "Video-1", withExtension: "mp4"))
        let testVideoData = try? XCTUnwrap(Data(contentsOf: testVideoURL!))
        
        let testThumbnailURL = testBundle.url(forResource: "Bild-1-0", withExtension: "jpg")
        let testThumbnailData = try? Data(contentsOf: testThumbnailURL!)
        
        for i in 0..<videoCount {
            let log = "Video: \(i) / \(videoCount)"
            print(log)
            
            loadVideo(with: testVideoData!, testThumbnailData!, conversation!, entityManager, log)
        }
    }
    
    func loadVideo(
        with videoData: Data,
        _ thumbnailData: Data,
        _ conversation: ConversationEntity,
        _ entityManager: EntityManager,
        _ caption: String
    ) {
        
        entityManager.performAndWaitSave {
            let dbFile: FileDataEntity = entityManager.entityCreator.fileDataEntity(data: videoData)
            
            let thumbnailImage = MediaConverter.getThumbnailFor(UIImage(data: thumbnailData)!)!
            let thumbnailData = thumbnailImage.jpegData(compressionQuality: 1.0)!
            let thumbnailFile: ImageDataEntity = entityManager.entityCreator.imageDataEntity(
                data: thumbnailData,
                size: thumbnailImage.size
            )
            
            let message: FileMessageEntity = entityManager.entityCreator.fileMessageEntity(in: conversation)
            message.data = dbFile
            message.thumbnail = thumbnailFile
            message.fileName = "Video.mp4"
            message.fileSize = NSNumber(integerLiteral: dbFile.data!.count)
            message.mimeType = "video/mp4"
            message.type = NSNumber(integerLiteral: 1)
            message.date = Date(timeIntervalSinceReferenceDate: TimeInterval(-1 * Int.random(in: 0...223_456_789)))
            message.deliveryDate = message.date
            message.sender = conversation.contact
            message.sent = NSNumber(booleanLiteral: true)
            message.delivered = NSNumber(booleanLiteral: true)
            message.read = NSNumber(booleanLiteral: true)
            message.remoteSentDate = Date.now
            message.encryptionKey = NaClCrypto.shared().randomBytes(kBlobKeyLen)
            
            do {
                if let jsonWithoutCaption = FileMessageEncoder.jsonString(for: message),
                   let dataWithoutCaption = jsonWithoutCaption.data(using: .utf8),
                   var dict = try JSONSerialization.jsonObject(with: dataWithoutCaption) as? [String: AnyObject] {
                    dict["d"] = caption as AnyObject
                    let dataWithCaption = try JSONSerialization.data(withJSONObject: dict)
                    message.json = String(data: dataWithCaption, encoding: .utf8)
                }
            }
            catch {
                // do nothing
            }
        }
    }
    
    func testLoadAudioFileMessages() {
        var conversation: ConversationEntity?
        
        _ = createContacts(for: ["ECHOECHO"])
        let entityManager = PersistenceManager(
            appGroupID: AppGroup.groupID(),
            userDefaults: AppGroup.userDefaults(),
            remoteSecretManager: RemoteSecretProvider.remoteSecretManager
        ).entityManager
        entityManager.performAndWaitSave {
            if let contact = entityManager.entityFetcher.contactEntity(for: "ECHOECHO") {
                conversation = (entityManager.conversation(forContact: contact, createIfNotExisting: true))!
            }
        }
        
        let audioCount = 100_000
        
        let testBundle = Bundle(for: DBLoadTests.self)
        let testAudioURL = try? XCTUnwrap(testBundle.url(forResource: "SmallVoice", withExtension: "mp3"))
        let testAudioData = try? XCTUnwrap(Data(contentsOf: testAudioURL!))

        for i in 0..<audioCount {
            let log = "Audio: \(i) / \(audioCount)"
            print(log)
            
            loadAudio(with: testAudioData!, conversation!, entityManager, log)
        }
    }
    
    func loadAudio(
        with audioData: Data,
        _ conversation: ConversationEntity,
        _ entityManager: EntityManager,
        _ caption: String
    ) {
        
        entityManager.performAndWaitSave {
            let dbFile: FileDataEntity = entityManager.entityCreator.fileDataEntity(data: audioData)
                        
            let message: FileMessageEntity = entityManager.entityCreator.fileMessageEntity(in: conversation)
            message.data = dbFile
            message.fileName = "Audio.mp3"
            message.fileSize = NSNumber(integerLiteral: dbFile.data!.count)
            message.mimeType = "audio/mp4"
            message.type = NSNumber(integerLiteral: 1)
            message.date = Date(timeIntervalSinceReferenceDate: TimeInterval(-1 * Int.random(in: 0...223_456_789)))
            message.deliveryDate = message.date
            message.sender = conversation.contact
            message.sent = NSNumber(booleanLiteral: true)
            message.delivered = NSNumber(booleanLiteral: true)
            message.read = NSNumber(booleanLiteral: true)
            message.remoteSentDate = Date.now
            message.encryptionKey = NaClCrypto.shared().randomBytes(kBlobKeyLen)
            
            do {
                if let jsonWithoutCaption = FileMessageEncoder.jsonString(for: message),
                   let dataWithoutCaption = jsonWithoutCaption.data(using: .utf8),
                   var dict = try JSONSerialization.jsonObject(with: dataWithoutCaption) as? [String: AnyObject] {
                    dict["d"] = caption as AnyObject
                    let dataWithCaption = try JSONSerialization.data(withJSONObject: dict)
                    message.json = String(data: dataWithCaption, encoding: .utf8)
                }
            }
            catch {
                // do nothing
            }
        }
    }
    
    func testLoadAudioMessages() throws {
        var conversation: ConversationEntity?
        
        _ = createContacts(for: ["ECHOECHO"])
        let entityManager = PersistenceManager(
            appGroupID: AppGroup.groupID(),
            userDefaults: AppGroup.userDefaults(),
            remoteSecretManager: RemoteSecretProvider.remoteSecretManager
        ).entityManager
        entityManager.performAndWaitSave {
            if let contact = entityManager.entityFetcher.contactEntity(for: "ECHOECHO") {
                conversation = (entityManager.conversation(forContact: contact, createIfNotExisting: true))!
            }
        }
                
        let testBundle = Bundle(for: DBLoadTests.self)
        let testAudioURL = try XCTUnwrap(testBundle.url(forResource: "SmallVoice", withExtension: "mp3"))
        let testAudioData = try XCTUnwrap(Data(contentsOf: testAudioURL))
        
        entityManager.performAndWaitSave {
            let dbFile: AudioDataEntity = entityManager.entityCreator.audioDataEntity(data: testAudioData)
            
            let message: AudioMessageEntity = entityManager.entityCreator.audioMessageEntity(in: conversation!)
            message.audio = dbFile
            message.duration = 2
            message.audioSize = 55
            message.audioBlobID = BytesUtility.generateBlobID()
            message.encryptionKey = BytesUtility.generateBlobEncryptionKey()
            message.progress = 100
        }
    }
    
    func testLoadImageMessage() throws {
        var conversation: ConversationEntity?
        
        _ = createContacts(for: ["ECHOECHO"])
        let entityManager = BusinessInjector.ui.entityManager
        entityManager.performAndWaitSave {
            if let contact = entityManager.entityFetcher.contactEntity(for: "ECHOECHO") {
                conversation = (entityManager.conversation(forContact: contact, createIfNotExisting: true))!
            }
        }
                
        let testBundle = Bundle(for: DBLoadTests.self)
        let testImageURL = try XCTUnwrap(testBundle.url(forResource: "Bild-1-0", withExtension: "jpg"))
        let testImageData = try XCTUnwrap(Data(contentsOf: testImageURL))
        
        entityManager.performAndWaitSave {
            let imageDataEntity: ImageDataEntity = entityManager.entityCreator.imageDataEntity(
                data: testImageData,
                size: CGSize(width: 1837, height: 1379)
            )
            
            let thumbnailImage = MediaConverter.getThumbnailFor(UIImage(data: testImageData)!)!
            let thumbnailData = thumbnailImage.jpegData(compressionQuality: 1.0)!
            let thumbnailDataEntity: ImageDataEntity = entityManager.entityCreator.imageDataEntity(
                data: thumbnailData,
                size: thumbnailImage.size
            )

            let message: ImageMessageEntity = entityManager.entityCreator.imageMessageEntity(in: conversation!)
            message.image = imageDataEntity
            message.thumbnail = thumbnailDataEntity
            
            message.encryptionKey = BytesUtility.generateBlobEncryptionKey()
            message.imageBlobID = BytesUtility.generateBlobID()
            message.imageNonce = BytesUtility.generateMessageNonce()
            message.imageSize = NSNumber(value: imageDataEntity.data.count)
            message.progress = 100
        }
    }
    
    func testLoadVideoMessage() throws {
        var conversation: ConversationEntity?
        
        _ = createContacts(for: ["ECHOECHO"])
        let entityManager = BusinessInjector.ui.entityManager
        entityManager.performAndWaitSave {
            if let contact = entityManager.entityFetcher.contactEntity(for: "ECHOECHO") {
                conversation = (entityManager.conversation(forContact: contact, createIfNotExisting: true))!
            }
        }
                
        let testBundle = Bundle(for: DBLoadTests.self)
        let testVideoURL = try XCTUnwrap(testBundle.url(forResource: "Video-1", withExtension: "mp4"))
        let testVideoData = try XCTUnwrap(Data(contentsOf: testVideoURL))
        
        let testImageURL = try XCTUnwrap(testBundle.url(forResource: "Bild-1-0", withExtension: "jpg"))
        let testImageData = try XCTUnwrap(Data(contentsOf: testImageURL))
        
        entityManager.performAndWaitSave {
            let videoDataEntity: VideoDataEntity = entityManager.entityCreator.videoDataEntity(data: testVideoData)
            
            let thumbnailImage = MediaConverter.getThumbnailFor(UIImage(data: testImageData)!)!
            let thumbnailData = thumbnailImage.jpegData(compressionQuality: 1.0)!
            let thumbnailDataEntity: ImageDataEntity = entityManager.entityCreator.imageDataEntity(
                data: thumbnailData,
                size: thumbnailImage.size
            )

            let message: VideoMessageEntity = entityManager.entityCreator.videoMessageEntity(in: conversation!)
            message.video = videoDataEntity
            message.thumbnail = thumbnailDataEntity
            
            message.encryptionKey = BytesUtility.generateBlobEncryptionKey()
            message.videoBlobID = BytesUtility.generateBlobID()
            message.duration = 10
            message.videoSize = NSNumber(value: videoDataEntity.data.count)
            message.progress = 100
        }
    }
    
    // TODO: Call messages (IOS-3033)
    
    // MARK: - Groups with single set of messages
    
    // MARK: Text messages with quotes

    func testGroupWithQuoteMessages() async throws {
        let numberOfMessagesToAdd = 100
        let alternateEveryXMessage = 5
        
        let testBundle = Bundle(for: DBLoadTests.self)
        let entityManager = BusinessInjector.ui.entityManager
        
        // Create group
        let group = try await createGroup(named: "Quote Messages", with: [], entityManager: entityManager)

        // Sender
        _ = createContacts(for: ["ECHOECHO"])
        let senderContact = try XCTUnwrap(entityManager.entityFetcher.contactEntity(for: "ECHOECHO"))
        
        // Load texts
        let testTextsURL = try XCTUnwrap(testBundle.url(forResource: "test_texts", withExtension: "json"))
        let texts = try JSONDecoder().decode([String].self, from: Data(contentsOf: testTextsURL))
        
        // Create set of messages to quote
        
        var quotableMessages = [BaseMessageEntity]()
        
        // Text messages
        entityManager.performAndWaitSave {
            let outgoingMessage = entityManager.entityCreator.textMessageEntity(
                text: texts[0],
                in: group.conversation,
                setLastUpdate: true
            )
            outgoingMessage.date = Date()
            outgoingMessage.sent = true
            outgoingMessage.delivered = true
            outgoingMessage.remoteSentDate = Date.now
            outgoingMessage.deliveryDate = Date()
            outgoingMessage.isOwn = true
            quotableMessages.append(outgoingMessage)
            
            let incomingMessage = entityManager.entityCreator.textMessageEntity(
                text: texts[1],
                in: group.conversation,
                setLastUpdate: true
            )
            incomingMessage.date = Date()
            incomingMessage.sent = true
            incomingMessage.delivered = true
            incomingMessage.remoteSentDate = Date.now
            incomingMessage.deliveryDate = Date()
            incomingMessage.isOwn = false
            incomingMessage.sender = senderContact
            quotableMessages.append(incomingMessage)
        }
        
        // File messages
        
        var senderItems = [URLSenderItem]()
        
        // Load image
        let testImageURL = try XCTUnwrap(testBundle.url(forResource: "Bild-1-0", withExtension: "jpg"))
        let imageSenderItem = try XCTUnwrap(ImageURLSenderItemCreator().senderItem(from: testImageURL))
        senderItems.append(imageSenderItem)
        
        // Load sticker
        let testStickerURL = try XCTUnwrap(testBundle.url(forResource: "Sticker-sine_wave", withExtension: "png"))
        let stickerSenderItem = try XCTUnwrap(URLSenderItem(
            url: testStickerURL,
            type: UTType.png.identifier,
            renderType: 2,
            sendAsFile: false
        ))
        senderItems.append(stickerSenderItem)
        
        // Load animated image
        let testAnimatedImageURL = try XCTUnwrap(testBundle.url(
            forResource: "Animated_two_spur_gears_1_2",
            withExtension: "gif"
        ))
        let animatedImageSenderItem = try XCTUnwrap(ImageURLSenderItemCreator().senderItem(from: testAnimatedImageURL))
        senderItems.append(animatedImageSenderItem)
        
        // Load animated sticker
        let testAnimatedStickerURL = try XCTUnwrap(testBundle.url(
            forResource: "Animated_sticker-sine_wave",
            withExtension: "gif"
        ))
        let animatedStickerSenderItem = try XCTUnwrap(URLSenderItem(
            url: testAnimatedStickerURL,
            type: UTType.gif.identifier,
            renderType: 2,
            sendAsFile: false
        ))
        senderItems.append(animatedStickerSenderItem)
        
        // Load video
        let testVideoURL = try XCTUnwrap(testBundle.url(forResource: "Video-1", withExtension: "mp4"))
        let videoSenderItem = try XCTUnwrap(VideoURLSenderItemCreator().senderItem(from: testVideoURL))
        senderItems.append(videoSenderItem)
        
        // Load voice message
        let testVoiceURL = try XCTUnwrap(testBundle.url(forResource: "audioAnalyzerTest", withExtension: "m4a"))
        let voiceSenderItem = try XCTUnwrap(URLSenderItem(
            url: testVoiceURL,
            type: UTType.mpeg4Audio.identifier,
            renderType: 1,
            sendAsFile: false
        ))
        senderItems.append(voiceSenderItem)
        
        // Load document
        let testFileURL = try XCTUnwrap(testBundle.url(forResource: "Test", withExtension: "pdf"))
        let fileSenderItem = try XCTUnwrap(URLSenderItem(
            url: testFileURL,
            type: UTType.pdf.identifier,
            renderType: 0,
            sendAsFile: false
        ))
        senderItems.append(fileSenderItem)
        
        // Add quotable file messages
        
        var fileMessageCreationError: Error?
        
        entityManager.performAndWaitSave {
            do {
                for senderItem in senderItems {
                    let ownFileMessageEntity = try self.createFileMessageEntity(
                        for: senderItem,
                        in: group.conversation,
                        with: entityManager
                    )
                    ownFileMessageEntity.isOwn = true
                    quotableMessages.append(ownFileMessageEntity)
                    
                    let otherFileMessageEntity = try self.createFileMessageEntity(
                        for: senderItem,
                        in: group.conversation,
                        with: entityManager
                    )
                    otherFileMessageEntity.isOwn = false
                    otherFileMessageEntity.sender = senderContact
                    quotableMessages.append(otherFileMessageEntity)
                    
                    // This needs to be last otherwise the `renderType` of stickers will be 1 instead of 2
                    senderItem.caption = texts.randomElement() ?? "Caption"
                    let captionFileMessageEntity = try self.createFileMessageEntity(
                        for: senderItem,
                        in: group.conversation,
                        with: entityManager
                    )
                    captionFileMessageEntity.isOwn = false
                    captionFileMessageEntity.sender = senderContact
                    quotableMessages.append(captionFileMessageEntity)
                }
            }
            catch {
                fileMessageCreationError = error
            }
        }
        
        if let fileMessageCreationError {
            XCTFail(fileMessageCreationError.localizedDescription)
        }
        
        // Location messages
        
        let testLocationsURL = try XCTUnwrap(testBundle.url(forResource: "test_locations", withExtension: "json"))
        let locations = try JSONDecoder().decode([Location].self, from: Data(contentsOf: testLocationsURL))
        
        entityManager.performAndWaitSave {
            let ownLocationMessage = entityManager.entityCreator.locationMessageEntity(
                latitude: locations[0].latitude,
                longitude: locations[0].longitude,
                accuracy: locations[0].accuracy,
                poiName: locations[0].name,
                poiAddress: locations[0].address,
                in: group.conversation,
                setLastUpdate: true
            )
            ownLocationMessage.isOwn = true
            quotableMessages.append(ownLocationMessage)
            
            let otherLocationMessage = entityManager.entityCreator.locationMessageEntity(
                latitude: locations[2].latitude,
                longitude: locations[2].longitude,
                accuracy: locations[2].accuracy,
                poiName: locations[2].name,
                poiAddress: locations[2].address,
                in: group.conversation,
                setLastUpdate: true
            )
            otherLocationMessage.isOwn = false
            otherLocationMessage.sender = senderContact
            quotableMessages.append(otherLocationMessage)

            let anotherLocationMessage = entityManager.entityCreator.locationMessageEntity(
                latitude: locations[3].latitude,
                longitude: locations[3].longitude,
                accuracy: locations[3].accuracy,
                poiName: locations[3].name,
                poiAddress: locations[3].address,
                in: group.conversation,
                setLastUpdate: true
            )
            anotherLocationMessage.isOwn = false
            anotherLocationMessage.sender = senderContact
            quotableMessages.append(anotherLocationMessage)
        }
        
        // TODO: Add quotable ballots to `quotableMessages` (IOS-3033)
        
        // Create quote messages
        
        entityManager.performAndWaitSave {
            for index in 0..<numberOfMessagesToAdd {
                let message = entityManager.entityCreator.textMessageEntity(
                    text: "\(index) - \(texts[index % texts.count])",
                    in: group.conversation,
                    setLastUpdate: true
                )
                
                message.sent = true
                message.delivered = true
                message.remoteSentDate = Date.now
                message.deliveryDate = Date()
                
                // Don't add a quote to every forth message
                if index % 4 > 0 {
                    message.quotedMessageID = quotableMessages[index % quotableMessages.count].id
                }
                
                if index % (alternateEveryXMessage * 2) >= alternateEveryXMessage {
                    // incoming message
                    message.isOwn = false
                    message.sender = senderContact
                }
                else {
                    // outgoing message
                    message.isOwn = true
                }
            }
        }
    }
    
    // MARK: Image file messages

    func testGroupWithImageFileMessages() async throws {
        let numberOfMessagesToAdd = 100
        let alternateEveryXMessage = 5
        
        let entityManager = BusinessInjector.ui.entityManager
        
        // Create group
        let group = try await createGroup(named: "Image File Messages", with: [], entityManager: entityManager)

        // Load image
        let testBundle = Bundle(for: DBLoadTests.self)
        let testImageURL = try XCTUnwrap(testBundle.url(forResource: "Bild-1-0", withExtension: "jpg"))
        let imageSenderItem = try XCTUnwrap(ImageURLSenderItemCreator().senderItem(from: testImageURL))
        
        // Create messages
        try add(
            senderItem: imageSenderItem,
            to: group,
            numberOfMessagesToAdd,
            timesAndAlternateEvery: alternateEveryXMessage,
            entityManager: entityManager
        )
    }
    
    // MARK: Sticker file messages

    func testGroupWithStickerFileMessages() async throws {
        let numberOfMessagesToAdd = 100
        let alternateEveryXMessage = 5
        
        let entityManager = BusinessInjector.ui.entityManager
        
        // Create group
        let group = try await createGroup(named: "Sticker File Messages", with: [], entityManager: entityManager)

        // Load image
        let testBundle = Bundle(for: DBLoadTests.self)
        let testStickerURL = try XCTUnwrap(testBundle.url(forResource: "Sticker-sine_wave", withExtension: "png"))
        let stickerSenderItem = try XCTUnwrap(URLSenderItem(
            url: testStickerURL,
            type: UTType.png.identifier,
            renderType: 2,
            sendAsFile: false
        ))
        
        // Create messages
        try add(
            senderItem: stickerSenderItem,
            showCaptions: false,
            to: group,
            numberOfMessagesToAdd,
            timesAndAlternateEvery: alternateEveryXMessage,
            entityManager: entityManager
        )
    }
    
    // MARK: Animated image file messages

    func testGroupWithAnimatedImageFileMessages() async throws {
        let numberOfMessagesToAdd = 100
        let alternateEveryXMessage = 5
        
        let entityManager = BusinessInjector.ui.entityManager
        
        // Create group
        let group = try await createGroup(named: "Animated Image File Messages", with: [], entityManager: entityManager)

        // Load image
        let testBundle = Bundle(for: DBLoadTests.self)
        let testAnimatedImageURL = try XCTUnwrap(testBundle.url(
            forResource: "Animated_two_spur_gears_1_2",
            withExtension: "gif"
        ))
        let animatedImageSenderItem = try XCTUnwrap(ImageURLSenderItemCreator().senderItem(from: testAnimatedImageURL))
        
        // Create messages
        try add(
            senderItem: animatedImageSenderItem,
            to: group,
            numberOfMessagesToAdd,
            timesAndAlternateEvery: alternateEveryXMessage,
            entityManager: entityManager
        )
    }
    
    // MARK: Animated sticker file messages
    
    func testGroupWithAnimatedStickerFileMessages() async throws {
        let numberOfMessagesToAdd = 100
        let alternateEveryXMessage = 5
        
        let entityManager = BusinessInjector.ui.entityManager
        
        // Create group
        let group = try await createGroup(
            named: "Animated Sticker File Messages",
            with: [],
            entityManager: entityManager
        )

        // Load image
        let testBundle = Bundle(for: DBLoadTests.self)
        let testAnimatedStickerURL = try XCTUnwrap(testBundle.url(
            forResource: "Animated_sticker-sine_wave",
            withExtension: "gif"
        ))
        let animatedStickerSenderItem = try XCTUnwrap(URLSenderItem(
            url: testAnimatedStickerURL,
            type: UTType.gif.identifier,
            renderType: 2,
            sendAsFile: false
        ))
        
        // Create messages
        try add(
            senderItem: animatedStickerSenderItem,
            showCaptions: false,
            to: group,
            numberOfMessagesToAdd,
            timesAndAlternateEvery: alternateEveryXMessage,
            entityManager: entityManager
        )
    }
    
    // MARK: Video file messages

    func testGroupWithVideoFileMessages() async throws {
        let numberOfMessagesToAdd = 100
        let alternateEveryXMessage = 5
        
        let entityManager = BusinessInjector.ui.entityManager
        
        // Create group
        let group = try await createGroup(named: "Video File Messages", with: [], entityManager: entityManager)

        // Load image
        let testBundle = Bundle(for: DBLoadTests.self)
        let testVideoURL = try XCTUnwrap(testBundle.url(forResource: "Video-1", withExtension: "mp4"))
        let videoSenderItem = try XCTUnwrap(VideoURLSenderItemCreator().senderItem(from: testVideoURL))
        
        // Create messages
        try add(
            senderItem: videoSenderItem,
            to: group,
            numberOfMessagesToAdd,
            timesAndAlternateEvery: alternateEveryXMessage,
            entityManager: entityManager
        )
    }
    
    // MARK: Voice file messages

    func testGroupWithVoiceFileMessages() async throws {
        let numberOfMessagesToAdd = 100
        let alternateEveryXMessage = 5
        
        let entityManager = BusinessInjector.ui.entityManager
        
        // Create group
        let group = try await createGroup(named: "Voice File Messages", with: [], entityManager: entityManager)

        // Load image
        let testBundle = Bundle(for: DBLoadTests.self)
        let testVoiceURL = try XCTUnwrap(testBundle.url(forResource: "audioAnalyzerTest", withExtension: "m4a"))
        let voiceSenderItem = try XCTUnwrap(URLSenderItem(
            url: testVoiceURL,
            type: UTType.mpeg4Audio.identifier,
            renderType: 1,
            sendAsFile: false
        ))
        
        // Create messages
        try add(
            senderItem: voiceSenderItem,
            to: group,
            numberOfMessagesToAdd,
            timesAndAlternateEvery: alternateEveryXMessage,
            entityManager: entityManager
        )
    }
    
    // MARK: File file messages

    func testGroupWithFileFileMessages() async throws {
        let numberOfMessagesToAdd = 100
        let alternateEveryXMessage = 5
        
        let entityManager = BusinessInjector.ui.entityManager
        
        // Create group
        let group = try await createGroup(named: "File File Messages", with: [], entityManager: entityManager)

        // Load image
        let testBundle = Bundle(for: DBLoadTests.self)
        let testFileURL = try XCTUnwrap(testBundle.url(forResource: "Test", withExtension: "pdf"))
        let fileSenderItem = try XCTUnwrap(URLSenderItem(
            url: testFileURL,
            type: UTType.pdf.identifier,
            renderType: 0,
            sendAsFile: false
        ))
        
        // Create messages
        try add(
            senderItem: fileSenderItem,
            to: group,
            numberOfMessagesToAdd,
            timesAndAlternateEvery: alternateEveryXMessage,
            entityManager: entityManager
        )
    }
    
    // MARK: Call Status Message
    
    func testCallStatusMessages() {
        let numberOfMessagesToAdd = 500
        let alternateEveryXMessage = 10
        let identity = "ECHOECHO"
        
        _ = createContacts(for: [identity])
        
        let entityManager = BusinessInjector.ui.entityManager
        
        // Load conversation
        var conversation: ConversationEntity!
        
        entityManager.performAndWaitSave {
            if let contact = entityManager.entityFetcher.contactEntity(for: identity) {
                conversation = entityManager.conversation(forContact: contact, createIfNotExisting: true)
            }
        }
        
        let incomingCallStates: [SystemMessageEntity.SystemMessageEntityType] = [
            .callMissed,
            .callRejected,
            .callRejectedBusy,
            .callRejectedTimeout,
            .callEnded,
            .callRejectedDisabled,
            .callRejectedUnknown,
        ]
        
        let outgoingCallStates: [SystemMessageEntity.SystemMessageEntityType] = [
            .callRejected,
            .callRejectedBusy,
            .callRejectedTimeout,
            .callEnded,
            .callRejectedDisabled,
            .callRejectedUnknown,
        ]
        
        for index in 0..<numberOfMessagesToAdd {
            let isIncoming = index % (alternateEveryXMessage * 2) >= alternateEveryXMessage
            let callType = isIncoming ? incomingCallStates[index % incomingCallStates.count] :
                outgoingCallStates[index % outgoingCallStates.count]
            
            entityManager.performAndWaitSave {
                let systemMessage = entityManager.entityCreator.systemMessageEntity(
                    for: callType,
                    in: conversation
                )

                systemMessage.remoteSentDate = Date.now
                
                var callInfo = [
                    "DateString": DateFormatter.shortStyleTimeNoDate(Date()),
                    "CallInitiator": NSNumber(booleanLiteral: isIncoming),
                ] as [String: Any]
                
                if index % (incomingCallStates.count + outgoingCallStates.count) == 0,
                   callType == .callEnded {
                    callInfo["CallTime"] = DateFormatter.timeFormatted(Int.random(in: 1..<60 * 60 * 48))
                }
                
                do {
                    let callInfoData = try JSONSerialization.data(withJSONObject: callInfo, options: .prettyPrinted)
                    systemMessage.arg = callInfoData
                    systemMessage.isOwn = NSNumber(booleanLiteral: isIncoming)
                    systemMessage.conversation = conversation
                    conversation.lastMessage = systemMessage
                    conversation.lastUpdate = Date()
                }
                catch {
                    XCTFail(error.localizedDescription)
                }
            }
        }
    }
    
    // MARK: Ballot Messages
    
    func testGroupWithBallotMessages() async throws {
        // Configuration
        // There is more configuration in the loop below
        let numberOfMessagesToAdd = 100
        let alternateEveryXMessage = 20
        let choicesPerBallot = 2..<15
        let totalVotes = 0..<256
        
        // Load test data
        let testBundle = Bundle(for: DBLoadTests.self)

        guard let textsPath = testBundle.url(forResource: "test_ballot_titles", withExtension: "json") else {
            XCTFail("Cannot find file with test texts")
            return
        }
        let ballotTitles = try XCTUnwrap(JSONDecoder().decode([String].self, from: Data(contentsOf: textsPath)))
        
        guard let textsPath = testBundle.url(forResource: "test_ballot_options", withExtension: "json") else {
            XCTFail("Cannot find file with test texts")
            return
        }
        let ballotOptions = try XCTUnwrap(JSONDecoder().decode([String].self, from: Data(contentsOf: textsPath)))
        
        let filePath = try XCTUnwrap(testBundle.path(forResource: "test_ids", ofType: "txt"))
        
        // Prepare Group
        // Load test identities
        var fetchIdentities = ["ECHOECHO"]
        
        let ids = try XCTUnwrap(String(contentsOfFile: filePath, encoding: .utf8))
        for id in ids.components(separatedBy: .newlines) {
            if !id.isEmpty {
                fetchIdentities.append(id)
            }
        }
        
        var participants = createContacts(for: fetchIdentities.reversed())
        
        let entityManager = BusinessInjector.ui.entityManager
        
        let myIdentityStoreMock = MyIdentityStoreMock()
        
        participants.append(myIdentityStoreMock.identity)
        
        // Create group
        let group = try await createGroup(
            named: "Ballot Messages (\(DateFormatter.accessibilityDateTime(Date())))",
            with: participants,
            entityManager: entityManager
        )
        let conversation = group.conversation
        
        for index in 0..<numberOfMessagesToAdd {
            let isIncoming = index % (alternateEveryXMessage * 2) >= alternateEveryXMessage
            let isClosed = index % 2 == 1
            let intermediateResults = index % 5 == 0
            let multipleChoice = index % 3 == 0
            
            entityManager.performAndWaitSave {
                // Setup ballot
                let baseBallot = entityManager.entityCreator
                    .ballotEntity(id: BytesUtility.generateRandomBytes(length: ThreemaProtocol.ballotIDLength)!)
                baseBallot.createDate = Date()
                baseBallot.creatorID = isIncoming ? "ECHOECHO" : myIdentityStoreMock.identity
                baseBallot.conversation = conversation
                baseBallot.title = ballotTitles[index % ballotTitles.count]

                if isClosed {
                    baseBallot.close()
                }
                
                var ballotChoices = [BallotChoiceEntity]()
                
                // Setup choices
                for choiceNo in 0..<Int.random(in: choicesPerBallot) {
                    let ballotChoice = entityManager.entityCreator.ballotChoiceEntity(
                        ballotEntity: baseBallot,
                        id: NSNumber(value: choiceNo)
                    )
                    ballotChoice.createDate = Date()
                    ballotChoice.name = ballotOptions[choiceNo % ballotOptions.count]
                    ballotChoice.orderPosition = NSNumber(value: choiceNo)
                    
                    ballotChoices.append(ballotChoice)
                }
                
                // Setup votes
                // We don't post vote messages here to avoid cluttering up the chat
                let ballotManager = BallotManager(entityManager: entityManager)
                for participant in 0..<Int.random(in: totalVotes) {
                    let maxChoices = multipleChoice ? 1 : Int.random(in: choicesPerBallot)
                    for _ in 0..<maxChoices {
                        ballotManager.updateChoice(
                            ballotChoices[Int.random(in: 0..<ballotChoices.count)],
                            with: NSNumber(booleanLiteral: true),
                            for: participants[participant % participants.count]
                        )
                    }
                }
                
                // Get sender
                let sender = entityManager.entityFetcher
                    .contactEntity(
                        for: isIncoming ? participants
                            .filter { $0 != myIdentityStoreMock.identity
                            }[Int.random(in: 0..<(participants.count - 1))] :
                            myIdentityStoreMock.identity
                    )
                
                // Always create open ballot message
                let ballotOpenMessage = entityManager.entityCreator.ballotMessageEntity(in: conversation)
                ballotOpenMessage.isOwn = NSNumber(value: !isIncoming)
                ballotOpenMessage.ballot = baseBallot
                ballotOpenMessage.sender = sender
                ballotOpenMessage.ballotState = NSNumber(value: BallotState.open.rawValue)
                
                // Only sometimes create ballot close message
                if isClosed {
                    let ballotCloseMessage = entityManager.entityCreator.ballotMessageEntity(in: conversation)
                    ballotCloseMessage.isOwn = NSNumber(value: !isIncoming)
                    ballotCloseMessage.ballot = baseBallot
                    ballotCloseMessage.sender = sender
                    ballotCloseMessage.ballotState = NSNumber(value: BallotState.closed.rawValue)
                }
            }
        }
    }
    
    // MARK: File message helper
    
    private func add(
        senderItem: URLSenderItem,
        caption: String? = nil,
        showCaptions: Bool = true,
        to group: Group,
        _ times: Int,
        timesAndAlternateEvery alternateEveryXMessage: Int,
        entityManager: EntityManager
    ) throws {
        let testBundle = Bundle(for: DBLoadTests.self)

        let testCaptionsURL = try XCTUnwrap(testBundle.url(forResource: "test_texts", withExtension: "json"))
        let captions = try JSONDecoder().decode([String].self, from: Data(contentsOf: testCaptionsURL))
        
        _ = createContacts(for: ["ECHOECHO"])
        let senderContact = try XCTUnwrap(entityManager.entityFetcher.contactEntity(for: "ECHOECHO"))
        
        for index in 0..<times {
            if showCaptions {
                // Add a caption if given or to every eighth message
                if let caption {
                    senderItem.caption = caption
                }
                else if index % 8 == 0 {
                    senderItem.caption = captions[(index / 8) % captions.count]
                }
                else {
                    senderItem.caption = nil
                }
            }
            
            var fileMessageCreationError: Error?
            
            entityManager.performAndWaitSave {
                do {
                    let fileMessageEntity = try self.createFileMessageEntity(
                        for: senderItem,
                        in: group.conversation,
                        with: entityManager
                    )
                    
                    if index % (alternateEveryXMessage * 2) >= alternateEveryXMessage {
                        // incoming message
                        fileMessageEntity.isOwn = false
                        fileMessageEntity.sender = senderContact
                    }
                    else {
                        // outgoing message
                        fileMessageEntity.isOwn = true
                    }
                }
                catch {
                    fileMessageCreationError = error
                }
            }
            
            if let fileMessageCreationError {
                XCTFail(fileMessageCreationError.localizedDescription)
            }
        }
    }
    
    // MARK: Location messages

    func testGroupWithLocationMessages() async throws {
        let numberOfMessagesToAdd = 100
        let alternateEveryXMessage = 5
        
        let entityManager = BusinessInjector.ui.entityManager
        
        // Create group
        let group = try await createGroup(named: "Location Messages", with: [], entityManager: entityManager)

        // Fetch contact
        _ = _ = createContacts(for: ["ECHOECHO"])
        let senderContact = try XCTUnwrap(entityManager.entityFetcher.contactEntity(for: "ECHOECHO"))
        
        // Load and add locations
        
        let testBundle = Bundle(for: DBLoadTests.self)
        let testLocationsURL = try XCTUnwrap(testBundle.url(forResource: "test_locations", withExtension: "json"))
        let locations = try JSONDecoder().decode([Location].self, from: Data(contentsOf: testLocationsURL))
    
        for index in 0..<numberOfMessagesToAdd {
            let location = locations[index % locations.count]
            
            entityManager.performAndWaitSave {
                let locationMessage = entityManager.entityCreator.locationMessageEntity(
                    latitude: location.latitude,
                    longitude: location.longitude,
                    accuracy: location.accuracy,
                    poiName: location.name,
                    poiAddress: location.address,
                    in: group.conversation,
                    setLastUpdate: true
                )
                
                if index % (alternateEveryXMessage * 2) >= alternateEveryXMessage {
                    // incoming message
                    locationMessage.isOwn = false
                    locationMessage.sender = senderContact
                }
                else {
                    // outgoing message
                    locationMessage.isOwn = true
                }
            }
        }
    }
    
    /// Used to parse  `test_locations.json`
    private struct Location: Codable {
        let name: String?
        let address: String?
        let latitude: Double
        let longitude: Double
        let accuracy: Double
    }
    
    // MARK: System messages
    
    func testGroupWithSystemMessages() async throws {
        let memberIDsToAddAndRemove = [
            "79PNJP93",
            "86C8TPSV",
            "89A6R535",
            "8K4PHKZ8",
            "938BYJZ2",
            "9DA769XW",
            "9UMM8RUX",
            "AMNR4PJH",
            "ANH9VK8J",
            "BR84ASE5",
            "BV9P56XX",
            "CHD7UH2B",
            "CPPDA7HM",
            "CT2AP9XA",
            "D2CJFSC2",
            "DSWMK3UZ",
            "EE365MVK",
        ]
        
        _ = createContacts(for: memberIDsToAddAndRemove)

        let entityManager = BusinessInjector.ui.entityManager
        
        // Create group
        let group = try await createGroup(named: "System Messages", with: [], entityManager: entityManager)
        let groupManager: GroupManagerProtocol = GroupManager(
            entityManager: entityManager,
            taskManager: TaskManagerMock()
        )

        // Add system messages by doing different group updates
        
        try await groupManager.setName(group: group, name: "System Messages 1")

        _ = try await groupManager.createOrUpdate(
            for: group.groupIdentity,
            members: Set(memberIDsToAddAndRemove),
            systemMessageDate: Date()
        )

        _ = try await groupManager.createOrUpdate(
            for: group.groupIdentity,
            members: [],
            systemMessageDate: Date()
        )
    }
    
    // MARK: Example group
    
    /// An example "Hikers" group
    ///
    /// Generates a group chat with named members and a set of messages. This is useful for demos.
    func testExampleGroup() async throws {
        let groupMemberIDsAndNames = [
            "4PBBKVUS": "Emily Yeung",
            "86C8TPSV": "Peter Schreiner",
            "9UMM8RUX": "Robert Diaz",
            "78HFFYMF": "Lisa Goldman",
            "4TWXB3EP": "Hanna Schmidt",
        ]

        // Ensure all contacts exist
        _ = createContacts(for: Array(groupMemberIDsAndNames.keys))

        let entityManager = BusinessInjector.ui.entityManager

        // Load all contacts and assign the names
        let members = try groupMemberIDsAndNames.map { id, name -> ContactEntity in
            let contact = try XCTUnwrap(entityManager.entityFetcher.contactEntity(for: id))
            
            let names = name.components(separatedBy: .whitespaces)
            entityManager.performAndWaitSave {
                contact.setFirstName(to: names.first, sortOrderFirstName: true)
                contact.setLastName(to: names.last, sortOrderFirstName: true)
            }
            
            return contact
        }
        
        let group = try await createGroup(
            named: "Hikers",
            with: Array(groupMemberIDsAndNames.keys),
            entityManager: entityManager
        )
        
        // Add messages
        
        addGroupTextMessage("Hello", sender: members[0], in: group, entityManager: entityManager)
        addGroupTextMessage(
            "Who's up for a hike next weekend?",
            sender: members[0],
            in: group,
            entityManager: entityManager
        )
        
        addGroupTextMessage("I'm in!", sender: members[1], in: group, entityManager: entityManager)
        addGroupTextMessage("Let's do it. ��", sender: members[2], in: group, entityManager: entityManager)
        
        let testBundle = Bundle(for: DBLoadTests.self)
        let testImageURL = try XCTUnwrap(testBundle.url(forResource: "Bild-1-0", withExtension: "jpg"))
        let imageSenderItem = try XCTUnwrap(ImageURLSenderItemCreator().senderItem(from: testImageURL))
        try add(
            senderItem: imageSenderItem,
            caption: "I can't, but last weekend was amazing.",
            to: group,
            1,
            timesAndAlternateEvery: 1,
            entityManager: entityManager
        )
        
        addGroupTextMessage(
            "I need to check how my schedule looks like. When do you plan to go? Only one day or both days?",
            sender: members[3],
            in: group,
            entityManager: entityManager
        )
        
        let testFileURL = try XCTUnwrap(testBundle.url(forResource: "Hike Churfirsten", withExtension: "gpx"))
        let fileSenderItem = try XCTUnwrap(URLSenderItem(
            url: testFileURL,
            type: UTType.pdf.identifier,
            renderType: 0,
            sendAsFile: false
        ))
        fileSenderItem.caption = "I would suggest this one day hike. What do you think?"
        
        var fileMessageCreationError: Error?
        
        entityManager.performAndWaitSave {
            do {
                let fileMessageEntity = try self.createFileMessageEntity(
                    for: fileSenderItem,
                    in: group.conversation,
                    with: entityManager
                )
                
                fileMessageEntity.isOwn = false
                fileMessageEntity.sender = members[0]
            }
            catch {
                fileMessageCreationError = error
            }
        }
        
        if let fileMessageCreationError {
            XCTFail(fileMessageCreationError.localizedDescription)
        }
    }
    
    // TODO: Helper
    
    private func createGroup(
        named: String,
        with members: [String],
        entityManager: EntityManager
    ) async throws -> Group {
        let groupManager: GroupManagerProtocol = GroupManager(
            entityManager: entityManager,
            taskManager: TaskManagerMock()
        )

        let groupIdentity = try GroupIdentity(
            id: XCTUnwrap(BytesUtility.generateRandomBytes(length: ThreemaProtocol.groupIDLength)),
            creator: ThreemaIdentity(XCTUnwrap(MyIdentityStore.shared().identity))
        )

        // Creation
        
        let (group, _) = try await groupManager.createOrUpdate(
            for: groupIdentity,
            members: Set(members),
            systemMessageDate: Date()
        )

        // Set name
        try await groupManager.setName(group: group, name: named)

        return group
    }
    
    // Workaround to add contacts as `addContacts(for:entityManager:)` doesn't seem to work
    private func createContacts(for ids: [String]) -> [String] {
        var createdContacts = [String]()
        var contactStoreExpectations = [XCTestExpectation]()
        for id in ids {
            print("Checking \(id)")
            let contactStoreExpectation = expectation(description: "Add contact to contact store")
            ContactStore.shared().addContact(
                with: id,
                verificationLevel: Int32(ContactEntity.VerificationLevel.unverified.rawValue)
            ) { _, _ in
                createdContacts.append(id)
                contactStoreExpectation.fulfill()
            } onError: { error in
                print("Failed to create contact from \(id) \(error.localizedDescription)")
                contactStoreExpectation.fulfill()
            }
            contactStoreExpectations.append(contactStoreExpectation)
        }
        wait(for: contactStoreExpectations, timeout: 30)
        
        return createdContacts
    }
    
    // the function below is a copy from entityManager.conversation,
    // but it allows creating a (potentially) duplicate conversation for contactEntity.identity
    private func getOrCreateDuplicateOneToOneConversation(
        forContact contactEntity: ContactEntity,
        entityManager: EntityManager
    ) -> ConversationEntity? {
        let conversation = entityManager.entityFetcher.conversationEntity(for: contactEntity.identity)
        
        if conversation == nil {
            let conversation = entityManager.entityCreator.conversationEntity()
            conversation.contact = contactEntity
            
            if contactEntity.showOtherThreemaTypeIcon {
                // Add work info as first message
                let systemMessage = entityManager.entityCreator.systemMessageEntity(
                    for: .contactOtherAppInfo,
                    in: conversation
                )
                systemMessage.remoteSentDate = Date.now
            }
            
            print("Created 1:1 conversation for \(contactEntity.identity)")
            return conversation
        }
        else {
            print("Found exising 1:1 conversation for \(contactEntity.identity)")
        }
        
        // Check if the contact still needs to be hidden
        if contactEntity.isHidden {
            contactEntity.isHidden = false
            
            let mediatorSyncableContacts = MediatorSyncableContacts()
            mediatorSyncableContacts.updateAcquaintanceLevel(
                identity: contactEntity.identity,
                value: NSNumber(integerLiteral: ContactAcquaintanceLevel.direct.rawValue)
            )
            mediatorSyncableContacts.syncAsync()
        }
        
        return conversation!
    }

    /// create a text message in the one to one conversation with the provided contact
    ///
    /// isOwn: true if I sent the message, false of the provided contact sent the message
    private func addOneToOneTextMessage(
        _ text: String,
        quoteID: Data? = nil,
        isOwn: Bool,
        contact: ContactEntity,
        entityManager: EntityManager
    ) {
        entityManager.performAndWaitSave {
            let conversation = self.getOrCreateDuplicateOneToOneConversation(
                forContact: contact,
                entityManager: entityManager
            )
            
            let unwrappedConversation = try! XCTUnwrap(conversation)

            let message = entityManager.entityCreator.textMessageEntity(
                text: text,
                in: unwrappedConversation,
                setLastUpdate: true
            )
            
            message.sent = true
            message.remoteSentDate = Date.now
            message.delivered = true
            message.deliveryDate = Date()
            
            message.quotedMessageID = quoteID
            
            if isOwn {
                message.isOwn = true
            }
            else {
                message.isOwn = false
            }
        }
    }

    @discardableResult private func addGroupTextMessage(
        _ text: String,
        quoteID: Data? = nil,
        sender: ContactEntity? = nil,
        in group: Group,
        entityManager: EntityManager
    ) -> TextMessageEntity {
        entityManager.performAndWaitSave {
            let message = entityManager.entityCreator.textMessageEntity(
                text: text,
                in: group.conversation,
                setLastUpdate: true
            )

            message.sent = true
            message.remoteSentDate = Date.now
            message.delivered = true
            message.deliveryDate = Date()
            
            message.quotedMessageID = quoteID
            
            if let sender {
                message.isOwn = false
                message.sender = sender
            }
            else {
                message.isOwn = true
            }
            
            return message
        }
    }
    
    // MARK: - Migration test
    
    func testAddTextMessagesForMigration() throws {
        let numberOfMessages = 200_000
        
        let testBundle = Bundle(for: DBLoadTests.self)
        let textsPath = try XCTUnwrap(testBundle.url(forResource: "test_texts", withExtension: "json"))
        let texts = try JSONDecoder().decode([String].self, from: Data(contentsOf: textsPath))
        
        var newConversation: ConversationEntity?
        
        _ = createContacts(for: ["ECHOECHO"])
        
        let entityManager = BusinessInjector.ui.entityManager
        entityManager.performAndWaitSave {
            if let contact = entityManager.entityFetcher.contactEntity(for: "ECHOECHO") {
                newConversation = entityManager.conversation(forContact: contact, createIfNotExisting: true)
            }
        }
        
        let conversation = try XCTUnwrap(newConversation)
        
        entityManager.performAndWaitSave {
            for index in 0..<(numberOfMessages / 2) {
                let calendar = Calendar.current
                let date = calendar.date(byAdding: .hour, value: -(index * 8), to: Date())
                let message = entityManager.entityCreator.textMessageEntity(
                    text: "\(index) - \(texts[index % texts.count])",
                    in: conversation,
                    setLastUpdate: true
                )
                
                message.isOwn = false
                message.sender = conversation.contact

                message.sent = true
                message.remoteSentDate = Date.now
                message.delivered = true
                message.deliveryDate = Date()
                message.read = true
                message.readDate = Date()
                
                print("Batch 1: \(index)/\(numberOfMessages / 2)")
            }
        }
        
        entityManager.performAndWaitSave {
            for index in 0..<(numberOfMessages / 2) {
                let calendar = Calendar.current
                let date = calendar.date(byAdding: .hour, value: -(index * 8), to: Date())!
                let message = entityManager.entityCreator.textMessageEntity(
                    text: "\(index) - \(texts[index % texts.count])",
                    in: conversation,
                    setLastUpdate: true
                )
                message.isOwn = true

                message.date = date
                message.sent = true
                message.remoteSentDate = calendar.date(byAdding: .hour, value: +1, to: date)!
                message.delivered = true
                message.deliveryDate = calendar.date(byAdding: .hour, value: +2, to: date)!
                
                print("Batch 2: \(index)/\(numberOfMessages / 2)")
            }
        }
    }

    private func createFileMessageEntity(
        for item: URLSenderItem,
        in conversationEntity: ConversationEntity,
        with entityManager: EntityManager
    ) throws -> FileMessageEntity {
        try entityManager.entityCreator.createFileMessageEntity(
            data: item.getData(),
            mimeType: item.getMimeType(),
            caption: item.caption,
            fileName: item.getName(),
            type: .file,
            duration: item.getDuration(),
            height: Int(item.getHeight()),
            width: Int(item.getWidth()),
            thumbnailData: item.getThumbnail().pngData(),
            thumbnailSize: item.getThumbnail().size,
            encryptionKey: BytesUtility.generateBlobEncryptionKey(),
            origin: NSNumber(integerLiteral: 1),
            in: conversationEntity
        )
    }
}

Messung
CHZG
849710093

¤ Dauer der Verarbeitung: 0.26 Sekunden  (vorverarbeitet am  2026-11-04) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.