RelanceTranscriptionTest.swift 137 lignes · 5738 octets
import XCTest
@testable import Card2vcf

/// Tests TDD pour la relance de transcription (Lot 3).
///
/// Ces tests vérifient que `SyncEngine.relancerTranscriptionInteraction` et
/// `relancerTranscriptionNoteProjet` appellent bien l'API, mettent à jour
/// le statut local en "en_attente" et effacent l'erreur précédente.
final class RelanceTranscriptionTest: XCTestCase {

    private var db: InMemoryCrmDatabase!
    private var api: FakeAilianceApi!
    private var engine: SyncEngine!

    override func setUp() {
        super.setUp()
        db = InMemoryCrmDatabase()
        api = FakeAilianceApi()
        engine = SyncEngine(api: api, db: db)
    }

    // MARK: - Relance interaction — succès

    func testRelancerInteraction_appellePApi() async throws {
        let localId = try await insererInteractionEchec(serverId: "i-rel-1", contactId: "c1")

        let erreur = try await engine.relancerTranscriptionInteraction(localId: localId)

        XCTAssertNil(erreur, "Pas d'erreur attendue en cas de succès")
        XCTAssertEqual(1, api.relancerInteractionCalls.count)
        XCTAssertEqual("c1", api.relancerInteractionCalls.first?.0)
        XCTAssertEqual("i-rel-1", api.relancerInteractionCalls.first?.1)
    }

    func testRelancerInteraction_repasseStatutEnAttente() async throws {
        let localId = try await insererInteractionEchec(serverId: "i-rel-2", contactId: "c1")

        _ = try await engine.relancerTranscriptionInteraction(localId: localId)

        let updated = try await db.interactionDao.getByLocalId(localId)
        XCTAssertEqual("en_attente", updated?.transcriptionStatut,
                       "Statut doit repasser à en_attente après relance")
    }

    func testRelancerInteraction_effaceErreur() async throws {
        let localId = try await insererInteractionEchec(serverId: "i-rel-3", contactId: "c1")

        _ = try await engine.relancerTranscriptionInteraction(localId: localId)

        let updated = try await db.interactionDao.getByLocalId(localId)
        XCTAssertNil(updated?.transcriptionErreur, "Motif d'erreur effacé après relance")
    }

    // MARK: - Relance interaction — échec réseau

    func testRelancerInteraction_echecReseau_retourneMsgClair() async throws {
        let localId = try await insererInteractionEchec(serverId: "i-rel-4", contactId: "c1")
        api.relancerInteractionResult = .err(code: -1, message: "serveur injoignable")

        let erreur = try await engine.relancerTranscriptionInteraction(localId: localId)

        XCTAssertNotNil(erreur, "Message d'erreur attendu")
        XCTAssertTrue(erreur!.contains("serveur injoignable"),
                      "Message d'erreur clair : \(erreur!)")
    }

    func testRelancerInteraction_echecReseau_neModifiePasStatut() async throws {
        let localId = try await insererInteractionEchec(serverId: "i-rel-5", contactId: "c1")
        api.relancerInteractionResult = .err(code: -1, message: "réseau")

        _ = try await engine.relancerTranscriptionInteraction(localId: localId)

        let kept = try await db.interactionDao.getByLocalId(localId)
        XCTAssertEqual("echec", kept?.transcriptionStatut,
                       "Statut préservé quand la relance échoue")
    }

    // MARK: - Relance note projet — succès

    func testRelancerNoteProjet_appellePApi() async throws {
        let localId = try await insererNoteEchec(serverId: "n-rel-1", projetId: "proj-1")

        let erreur = try await engine.relancerTranscriptionNoteProjet(localId: localId)

        XCTAssertNil(erreur)
        XCTAssertEqual(1, api.relancerNoteProjetCalls.count)
        XCTAssertEqual("proj-1", api.relancerNoteProjetCalls.first?.0)
        XCTAssertEqual("n-rel-1", api.relancerNoteProjetCalls.first?.1)
    }

    func testRelancerNoteProjet_repasseStatutEnAttente() async throws {
        let localId = try await insererNoteEchec(serverId: "n-rel-2", projetId: "proj-1")

        _ = try await engine.relancerTranscriptionNoteProjet(localId: localId)

        let updated = try await db.noteProjetDao.listAll().first { $0.localId == localId }
        XCTAssertEqual("en_attente", updated?.transcriptionStatut)
        XCTAssertNil(updated?.transcriptionErreur)
    }

    func testRelancerNoteProjet_echecReseau_retourneMsgClair() async throws {
        let localId = try await insererNoteEchec(serverId: "n-rel-3", projetId: "proj-1")
        api.relancerNoteProjetResult = .err(code: -1, message: "serveur injoignable")

        let erreur = try await engine.relancerTranscriptionNoteProjet(localId: localId)

        XCTAssertNotNil(erreur)
        XCTAssertTrue(erreur!.contains("serveur injoignable"))
    }

    // MARK: - Helpers

    @discardableResult
    private func insererInteractionEchec(serverId: String, contactId: String) async throws -> Int64 {
        var entity = InteractionEntity()
        entity.serverId = serverId
        entity.contactServerId = contactId
        entity.type = "note_vocale"
        entity.sujet = "Note échouée"
        entity.transcriptionStatut = "echec"
        entity.transcriptionErreur = "Erreur Whisper : audio trop court"
        entity.createdAt = 1_000
        return try await db.interactionDao.upsert(entity)
    }

    @discardableResult
    private func insererNoteEchec(serverId: String, projetId: String) async throws -> Int64 {
        var entity = NoteProjetEntity()
        entity.serverId = serverId
        entity.projetServerId = projetId
        entity.titre = "Note échouée"
        entity.transcriptionStatut = "echec"
        entity.transcriptionErreur = "Erreur Whisper : audio trop court"
        entity.createdAt = 1_000
        return try await db.noteProjetDao.upsert(entity)
    }
}