InteractionMigrationTest.swift 112 lignes · 4483 octets
import XCTest
import SQLite3
@testable import Card2vcf

/// Preuve de rougeur (lot 2b, migration v5→v6) :
/// Sans les trois `ALTER TABLE interactions ADD COLUMN …` dans `migrateIfNeeded`, l'accès
/// aux nouvelles colonnes via `SqliteInteractionDao` lève une `DatabaseError` du type :
///
///   testMigrationV5ToV6_colonnesPresentes — threw error "DatabaseError: SELECT … FROM
///   interactions: table interactions has no column named audioPath"
///
final class InteractionMigrationTest: XCTestCase {

    private var dbPath: String!

    override func setUp() {
        super.setUp()
        dbPath = FileManager.default.temporaryDirectory
            .appendingPathComponent("migration_v5v6_\(UUID().uuidString).sqlite")
            .path
    }

    override func tearDown() {
        try? FileManager.default.removeItem(atPath: dbPath)
        super.tearDown()
    }

    /// Construit une base SQLite au format v5 (table interactions sans les nouvelles colonnes).
    private func buildV5Database(interactions: [(serverId: String, contactServerId: String, sujet: String, type: String)] = []) {
        var rawHandle: OpaquePointer?
        guard sqlite3_open(dbPath, &rawHandle) == SQLITE_OK, let rawHandle else { return }
        defer { sqlite3_close_v2(rawHandle) }

        var sql = """
            CREATE TABLE interactions (
                localId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
                serverId TEXT,
                contactServerId TEXT NOT NULL,
                type TEXT NOT NULL,
                sujet TEXT NOT NULL,
                description TEXT NOT NULL,
                creePar TEXT NOT NULL,
                createdAt INTEGER NOT NULL,
                updatedAt INTEGER
            );
            """
        for row in interactions {
            sql += """
                INSERT INTO interactions
                    (serverId, contactServerId, type, sujet, description, creePar, createdAt)
                VALUES ('\(row.serverId)', '\(row.contactServerId)', '\(row.type)', '\(row.sujet)', '', 'alice', 1000);
                """
        }
        sql += "PRAGMA user_version = 5;"
        sqlite3_exec(rawHandle, sql, nil, nil, nil)
    }

    func testMigrationV5ToV6_colonnesPresentes() async throws {
        buildV5Database(interactions: [("srv-i1", "c1", "Appel", "note")])

        // L'ouverture déclenche la migration v5→v6.
        let db = SqliteDatabase(filePath: dbPath)
        let dao = SqliteInteractionDao(db: db)

        // Données v5 préservées
        let interactions = try await dao.listAll()
        XCTAssertEqual(1, interactions.count, "L'interaction v5 doit être préservée après migration")
        XCTAssertEqual("srv-i1", interactions.first?.serverId)

        // Nouvelles colonnes présentes (valeur nil pour les lignes migrées)
        XCTAssertNil(interactions.first?.audioPath, "audioPath nul pour une interaction migrée depuis v5")
        XCTAssertNil(interactions.first?.transcriptionStatut)
        XCTAssertNil(interactions.first?.transcriptionErreur)
    }

    func testMigrationV5ToV6_preserveMultipleInteractions() async throws {
        buildV5Database(interactions: [
            ("i-alpha", "c-1", "Réunion", "note"),
            ("i-beta", "c-1", "Message vocal", "note_vocale"),
        ])

        let db = SqliteDatabase(filePath: dbPath)
        let dao = SqliteInteractionDao(db: db)
        let interactions = try await dao.listAll()

        XCTAssertEqual(2, interactions.count)
        XCTAssertTrue(interactions.contains { $0.serverId == "i-alpha" && $0.sujet == "Réunion" })
        XCTAssertTrue(interactions.contains { $0.serverId == "i-beta" && $0.type == "note_vocale" })
    }

    func testMigrationV5ToV6_insertionAvecNouvellesColonnes() async throws {
        buildV5Database()

        let db = SqliteDatabase(filePath: dbPath)
        let dao = SqliteInteractionDao(db: db)

        var entity = InteractionEntity()
        entity.contactServerId = "c1"
        entity.type = "note_vocale"
        entity.sujet = "Nouveau"
        entity.audioPath = "/tmp/note.wav"
        entity.transcriptionStatut = "en_attente"
        entity.createdAt = 2000

        let newId = try await dao.upsert(entity)
        let fetched = try await dao.getByLocalId(newId)

        XCTAssertEqual("/tmp/note.wav", fetched?.audioPath)
        XCTAssertEqual("en_attente", fetched?.transcriptionStatut)
        XCTAssertNil(fetched?.transcriptionErreur)
    }
}