ContratSyncFixturesTest.swift 111 lignes · 5281 octets
import XCTest
@testable import Card2vcf

/// Décode les fixtures de contrat de synchronisation avec les vrais DTO Swift.
///
/// Si un champ serveur est renommé, ce test devient rouge immédiatement.
/// Preuve de rougeur (vérifiée) : en renommant `transcription_erreur` → `transcription_error`
/// dans `interaction_note_vocale.json`, l'assertion suivante échoue :
///   XCTAssertEqual failed: ("Optional("Modèle ASR indisponible")") is not equal to ("nil")
final class ContratSyncFixturesTest: XCTestCase {

    private func fixture(_ nom: String) throws -> Data {
        let bundle = Bundle(for: ContratSyncFixturesTest.self)
        guard let url = bundle.url(forResource: nom, withExtension: nil) else {
            XCTFail("Fixture introuvable dans le bundle de test : \(nom)")
            throw NSError(domain: "fixture", code: 0)
        }
        return try Data(contentsOf: url)
    }

    // ---- sync_pull.json ----

    func testSyncPull_decodeSyncPullResponse() throws {
        let data = try fixture("sync_pull.json")
        let pull = try SyncJson.decoder().decode(SyncPullResponse.self, from: data)

        XCTAssertFalse(pull.contacts.isEmpty, "contacts doit contenir au moins un élément")
        XCTAssertFalse(pull.interactions.isEmpty, "interactions doit contenir au moins un élément")
        XCTAssertFalse(pull.notes.isEmpty, "notes doit contenir au moins un élément")
    }

    func testSyncPull_contactChampsCritiques() throws {
        let data = try fixture("sync_pull.json")
        let pull = try SyncJson.decoder().decode(SyncPullResponse.self, from: data)

        let contact = try XCTUnwrap(pull.contacts.first)
        XCTAssertFalse(contact.id.isEmpty, "id contact ne doit pas être vide")
        XCTAssertFalse(contact.creeLe.isEmpty, "creeLe contact ne doit pas être vide")
    }

    /// Contrat de planification : si le serveur renomme un de ces champs, le
    /// téléphone cesserait silencieusement d'afficher dates, étiquettes ou
    /// sous-tâches. Ce test rend la rupture immédiate.
    ///
    /// Preuve de rougeur (vérifiée) : en renommant `duree_jours` → `duree` dans
    /// `sync_pull.json`, ce test **et** `testSyncPull_periodeCalculeeCommeLeServeur`
    /// échouent —
    ///   XCTAssertEqual failed: ("Optional(3)") is not equal to ("nil")
    func testSyncPull_planificationDesTaches() throws {
        let data = try fixture("sync_pull.json")
        let pull = try SyncJson.decoder().decode(SyncPullResponse.self, from: data)

        let projet = try XCTUnwrap(pull.projets.first)
        XCTAssertTrue(projet.planification, "planification doit être transportée")
        XCTAssertEqual(5, projet.joursOuvres)
        XCTAssertEqual("2026-09-30", projet.echeance)

        let tache = try XCTUnwrap(pull.taches.first)
        XCTAssertEqual("2026-09-21", tache.debut)
        XCTAssertEqual(3, tache.dureeJours)
        XCTAssertEqual("contrat", tache.etiquette)
        XCTAssertEqual(2, tache.sousTaches.count)
        let premiere = try XCTUnwrap(tache.sousTaches.first)
        XCTAssertTrue(premiere.fait, "l'état coché doit survivre au transport")
    }

    /// La fin affichée sur le téléphone doit être celle du Gantt web.
    func testSyncPull_periodeCalculeeCommeLeServeur() throws {
        let data = try fixture("sync_pull.json")
        let pull = try SyncJson.decoder().decode(SyncPullResponse.self, from: data)
        let projet = try XCTUnwrap(pull.projets.first)
        let tache = try XCTUnwrap(pull.taches.first)

        // Lundi 2026-09-21 + 3 jours ouvrés en rythme 5 : mercredi 23.
        let debutIso = try XCTUnwrap(tache.debut)
        let debut = try XCTUnwrap(DateCivile(iso: debutIso))
        let duree = try XCTUnwrap(tache.dureeJours)
        let periode = CalendrierOuvre(joursOuvres: projet.joursOuvres)
            .periodeAffichee(debut, duree)
        XCTAssertEqual("21/09 → 23/09", periode)
    }

    // ---- interaction_note_vocale.json ----

    func testInteraction_decodeInteractionDto() throws {
        let data = try fixture("interaction_note_vocale.json")
        let dto = try SyncJson.decoder().decode(InteractionDto.self, from: data)

        XCTAssertEqual("note_vocale", dto.typeInteraction)
        XCTAssertEqual("vocale_contrat_1.wav", dto.pieceJointe)
        XCTAssertEqual("echec", dto.transcription)
        // Champ critique : si transcription_erreur est renommé côté serveur,
        // cette assertion devient rouge :
        //   XCTAssertEqual failed: ("Optional("Modèle ASR indisponible")") is not equal to ("nil")
        XCTAssertEqual("Modèle ASR indisponible", dto.transcriptionErreur)
    }

    // ---- note_projet.json ----

    func testNoteProjet_decodeNoteProjetDto() throws {
        let data = try fixture("note_projet.json")
        let dto = try SyncJson.decoder().decode(NoteProjetDto.self, from: data)

        XCTAssertEqual("projet-contrat-1", dto.projetId)
        XCTAssertEqual("note_contrat_1.wav", dto.audio)
        XCTAssertEqual("terminee", dto.transcription)
        // transcription_erreur null pour une transcription réussie
        XCTAssertNil(dto.transcriptionErreur,
                     "transcriptionErreur doit être nil pour une transcription réussie")
    }
}