PlanificationMigrationTest.swift 128 lignes · 5177 octets
import XCTest
import SQLite3
@testable import Card2vcf

/// Vérifie la migration additive v6 → v7 (miroir de Room `MIGRATION_7_8`) :
/// - les colonnes de planification apparaissent sur `taches` et `projets` ;
/// - les lignes existantes sont **préservées**, avec les défauts attendus.
///
/// Sans ce filet, une montée de version viderait le carnet de projets de
/// l'utilisateur au lieu d'ajouter trois colonnes.
final class PlanificationMigrationTest: XCTestCase {

    private var dbPath: String!

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

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

    /// Schéma v6 réduit aux deux tables que la migration touche.
    private static let v6Sql = """
        CREATE TABLE projets (
            localId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
            serverId TEXT,
            nom TEXT NOT NULL,
            description TEXT NOT NULL,
            workflowServerId TEXT NOT NULL,
            membresJson TEXT NOT NULL,
            creePar TEXT NOT NULL,
            createdAt INTEGER NOT NULL,
            updatedAt INTEGER NOT NULL
        );
        CREATE TABLE taches (
            localId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
            serverId TEXT,
            projetLocalId INTEGER,
            projetServerId TEXT NOT NULL,
            titre TEXT NOT NULL,
            description TEXT NOT NULL,
            colonneId TEXT NOT NULL,
            ordre INTEGER NOT NULL,
            assigneA TEXT,
            auteur TEXT NOT NULL,
            createdAt INTEGER NOT NULL,
            updatedAt INTEGER NOT NULL
        );
        INSERT INTO projets
            (serverId, nom, description, workflowServerId, membresJson, creePar, createdAt, updatedAt)
        VALUES ('srv-p1', 'Salon', '', 'wf-1', '[]', 'alice', 1000, 1000);
        INSERT INTO taches
            (serverId, projetLocalId, projetServerId, titre, description, colonneId, ordre,
             assigneA, auteur, createdAt, updatedAt)
        VALUES ('srv-t1', 1, 'srv-p1', 'Câblage', '', 'explorer', 0, 'alice', 'alice', 1000, 1000);
        PRAGMA user_version = 6;
        """

    private func creerBaseV6() throws {
        var rawHandle: OpaquePointer?
        guard sqlite3_open(dbPath, &rawHandle) == SQLITE_OK, let rawHandle else {
            XCTFail("Cannot open test database at \(dbPath!)")
            return
        }
        defer { sqlite3_close_v2(rawHandle) }
        var errMsg: UnsafeMutablePointer<CChar>?
        guard sqlite3_exec(rawHandle, Self.v6Sql, nil, nil, &errMsg) == SQLITE_OK else {
            let msg = errMsg.map { String(cString: $0) } ?? "unknown"
            sqlite3_free(errMsg)
            XCTFail("v6 setup failed: \(msg)")
            return
        }
    }

    func testMigrationV6ToV7PreserveLesDonneesEtAjouteLesColonnes() async throws {
        try creerBaseV6()

        // Ouvrir avec SqliteDatabase déclenche la migration v6 → v7.
        let db = SqliteDatabase(filePath: dbPath)
        let projets = try await SqliteProjetDao(db: db).listAll()
        let taches = try await SqliteTacheDao(db: db).listAll()

        let projet = try XCTUnwrap(projets.first, "le projet doit survivre à la migration")
        XCTAssertEqual("Salon", projet.nom)
        XCTAssertFalse(projet.planification, "défaut : planification éteinte")
        XCTAssertNil(projet.echeance)
        XCTAssertEqual(7, projet.joursOuvres, "défaut : tous les jours")

        let tache = try XCTUnwrap(taches.first, "la tâche doit survivre à la migration")
        XCTAssertEqual("Câblage", tache.titre)
        XCTAssertEqual("alice", tache.assigneA)
        XCTAssertNil(tache.debut)
        XCTAssertNil(tache.dureeJours)
        XCTAssertNil(tache.etiquette)
        XCTAssertNil(tache.parentId)
        XCTAssertEqual("[]", tache.dependDeJson)
        XCTAssertEqual("[]", tache.sousTachesJson)
    }

    func testLesChampsDePlanificationFontUnAllerRetourEnBase() async throws {
        try creerBaseV6()
        let db = SqliteDatabase(filePath: dbPath)
        let tacheDao = SqliteTacheDao(db: db)

        // `XCTUnwrap` prend une autoclosure non asynchrone : l'appel `await`
        // doit être évalué avant.
        let existante = try await tacheDao.getByServerId("srv-t1")
        var tache = try XCTUnwrap(existante)
        tache.debut = "2026-09-21"
        tache.dureeJours = 3
        tache.etiquette = "contrat"
        tache.parentId = "srv-t0"
        tache.dependDeJson = #"["srv-t0"]"#
        tache.sousTachesJson = #"[{"id":"st-1","titre":"Première étape","fait":true}]"#
        _ = try await tacheDao.upsert(tache)

        let rechargee = try await tacheDao.getByServerId("srv-t1")
        let relue = try XCTUnwrap(rechargee)
        XCTAssertEqual(tache, relue)
        XCTAssertEqual("21/09 → 23/09", ApercuTache.periode(relue, joursOuvres: 5))
        XCTAssertEqual(1, ApercuTache.avancement(relue)?.total)
    }
}