CrmContactDaoTest.swift 209 lignes · 7510 octets
import XCTest
@testable import Card2vcf

final class CrmContactDaoTest: XCTestCase {
    private var db: Card2vcfDatabase!
    private var dao: (any CrmContactDao)!

    override func setUp() {
        super.setUp()
        db = Card2vcfDatabase(inMemory: true)
        dao = db.crmContactDao
    }

    override func tearDown() {
        dao = nil
        db = nil
        super.tearDown()
    }

    private func makeContact(
        fullName: String? = nil,
        phones: [String] = [],
        emails: [String] = [],
        notes: String? = nil,
        createdAt: Int64 = 0,
        serverId: String? = nil
    ) -> CrmContactEntity {
        var e = CrmContactEntity()
        e.fullName = fullName
        e.phones = phones
        e.emails = emails
        e.notes = notes
        e.createdAt = createdAt
        e.updatedAt = createdAt
        e.phonesText = phones.joined(separator: " ")
        e.emailsText = emails.joined(separator: " ")
        e.serverId = serverId
        return e
    }

    // MARK: - Round trip

    func testInsertGetByIdRoundTrip() async throws {
        var contact = makeContact(
            fullName: "Jean Dupont",
            phones: ["+33612345678", "0145678901"],
            emails: ["jean@exemple.fr"],
            notes: "rencontré au salon",
            createdAt: 1000
        )
        contact.company = "ACME"
        contact.tags = ["vip", "prospect"]
        let id = try await dao.insert(contact)
        XCTAssertGreaterThan(id, 0)

        var expected = contact
        expected.id = id
        let fetched = try await dao.getById(id)
        XCTAssertEqual(expected, fetched)

        // INSERT OR REPLACE with an existing id replaces, not duplicates.
        expected.company = "ACME Corp"
        let replacedId = try await dao.insert(expected)
        XCTAssertEqual(id, replacedId)
        let all = try await dao.getAll()
        XCTAssertEqual(1, all.count)
        XCTAssertEqual("ACME Corp", all[0].company)
    }

    func testUpdateAndFetchAllOrdering() async throws {
        let id1 = try await dao.insert(makeContact(fullName: "Ancien", createdAt: 100))
        let id3 = try await dao.insert(makeContact(fullName: "Récent", createdAt: 300))
        let id2 = try await dao.insert(makeContact(fullName: "Moyen", createdAt: 200))

        let all = try await dao.fetchAll()
        XCTAssertEqual([id3, id2, id1], all.map(\.id)) // createdAt DESC

        var updated = try await dao.getById(id2)!
        updated.fullName = "Milieu"
        updated.updatedAt = 250
        try await dao.update(updated)
        let fetched = try await dao.getById(id2)
        XCTAssertEqual("Milieu", fetched?.fullName)
        XCTAssertEqual(250, fetched?.updatedAt)
        let count = try await dao.fetchAll().count
        XCTAssertEqual(3, count)
    }

    // MARK: - serverId lookups

    func testServerIdLookups() async throws {
        let id = try await dao.insert(makeContact(fullName: "Ada", createdAt: 10, serverId: "srv-1"))
        _ = try await dao.insert(makeContact(fullName: "Grace", createdAt: 20))

        let byServer = try await dao.getByServerId("srv-1")
        XCTAssertEqual(id, byServer?.id)
        let missing = try await dao.getByServerId("srv-nope")
        XCTAssertNil(missing)

        try await dao.deleteByServerId("srv-1")
        let afterDelete = try await dao.getByServerId("srv-1")
        XCTAssertNil(afterDelete)
        let remaining = try await dao.getAll()
        XCTAssertEqual(1, remaining.count)
        XCTAssertEqual("Grace", remaining[0].fullName)
    }

    // MARK: - FTS search

    func testSearchIdsByNamePhoneAndNotes() async throws {
        let jean = try await dao.insert(
            makeContact(
                fullName: "Jean Dupont",
                phones: ["0612345678", "0145678901"],
                emails: ["jean@exemple.fr"],
                notes: "rencontré au salon photo",
                createdAt: 100
            )
        )
        let marie = try await dao.insert(
            makeContact(
                fullName: "Marie Curie",
                emails: ["marie@laboratoire.fr"],
                notes: "physique nucleaire",
                createdAt: 200
            )
        )

        // Name fragment, prefix matching (repository appends `*` to each token).
        let byName = try await dao.searchIds("dup*")
        XCTAssertEqual([jean], byName)
        // Case-insensitive.
        let byNameUpper = try await dao.searchIds("DUPONT*")
        XCTAssertEqual([jean], byNameUpper)
        // Multi-token = implicit AND.
        let byBothTokens = try await dao.searchIds("jean* dupont*")
        XCTAssertEqual([jean], byBothTokens)
        let noCrossMatch = try await dao.searchIds("jean* curie*")
        XCTAssertEqual([], noCrossMatch)

        // Phone fragment via phonesText.
        let byPhone = try await dao.searchIds("0612*")
        XCTAssertEqual([jean], byPhone)
        let byOtherPhone = try await dao.searchIds("0145678901*")
        XCTAssertEqual([jean], byOtherPhone)

        // Note fragment.
        let byNote = try await dao.searchIds("salon*")
        XCTAssertEqual([jean], byNote)
        let byNote2 = try await dao.searchIds("nucleaire*")
        XCTAssertEqual([marie], byNote2)

        // Email fragment via emailsText.
        let byEmail = try await dao.searchIds("laboratoire*")
        XCTAssertEqual([marie], byEmail)

        let none = try await dao.searchIds("introuvable*")
        XCTAssertEqual([], none)
    }

    func testSearchIndexFollowsUpdatesAndDeletes() async throws {
        let id = try await dao.insert(makeContact(fullName: "Jean Dupont", createdAt: 100))

        var renamed = try await dao.getById(id)!
        renamed.fullName = "Albert Einstein"
        try await dao.update(renamed)
        let oldName = try await dao.searchIds("dupont*")
        XCTAssertEqual([], oldName)
        let newName = try await dao.searchIds("einstein*")
        XCTAssertEqual([id], newName)

        // REPLACE by id must reindex too.
        renamed.fullName = "Isaac Newton"
        _ = try await dao.insert(renamed)
        let replacedOld = try await dao.searchIds("einstein*")
        XCTAssertEqual([], replacedOld)
        let replacedNew = try await dao.searchIds("newton*")
        XCTAssertEqual([id], replacedNew)

        try await dao.deleteById(id)
        let afterDelete = try await dao.searchIds("newton*")
        XCTAssertEqual([], afterDelete)
    }

    // MARK: - Duplicate exclusions

    func testExclusionsInsertIgnoreAndDelete() async throws {
        let first = try await dao.insertExclusion(DuplicateExclusionEntity(idA: 1, idB: 2))
        XCTAssertGreaterThan(first, 0)
        // Same pair again: OnConflictStrategy.IGNORE returns -1.
        let ignored = try await dao.insertExclusion(DuplicateExclusionEntity(idA: 1, idB: 2))
        XCTAssertEqual(-1, ignored)
        let second = try await dao.insertExclusion(DuplicateExclusionEntity(idA: 2, idB: 3))
        XCTAssertGreaterThan(second, 0)

        let exclusions = try await dao.getExclusions()
        XCTAssertEqual(2, exclusions.count)

        try await dao.deleteExclusionsFor(1)
        let remaining = try await dao.getExclusions()
        XCTAssertEqual(1, remaining.count)
        XCTAssertEqual(2, remaining[0].idA)
        XCTAssertEqual(3, remaining[0].idB)

        try await dao.deleteExclusionsFor(3)
        let empty = try await dao.getExclusions()
        XCTAssertEqual(0, empty.count)
    }
}