AudioNoteRecorderTest.swift 138 lignes · 5618 octets
import XCTest
@testable import Card2vcf

/// Preuve de rougeur (exigence du plan `docs/plans/rattrapage-ios-et-tests.md`, Lot 2a) :
/// En commentant la garde `guard !enregistrementEnCours else { throw ... }` dans
/// `AudioNoteRecorder.demarrer()`, le test testDeuxDemarragesConsecutifsEchoue échoue avec :
///
///   testDeuxDemarragesConsecutifsEchoue — XCTAssertThrowsError failed: did not throw an error
///
/// Ce test détecte donc réellement l'absence de protection contre le double-démarrage.
final class AudioNoteRecorderTest: XCTestCase {
    private var moteur: FakeMoteurAudioCapture!
    private var recorder: AudioNoteRecorder!
    private var tempDir: URL!

    override func setUp() {
        super.setUp()
        tempDir = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString)
        try! FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
        moteur = FakeMoteurAudioCapture()
        recorder = AudioNoteRecorder(moteur: moteur, dureeMaxSecondes: 60)
    }

    override func tearDown() {
        super.tearDown()
        recorder.arreter()
        try? FileManager.default.removeItem(at: tempDir)
    }

    // MARK: - Démarrage / arrêt

    func testDeuxDemarragesConsecutifsEchoue() throws {
        let url1 = tempDir.appendingPathComponent("test1.wav")
        let url2 = tempDir.appendingPathComponent("test2.wav")
        try recorder.demarrer(fichierCible: url1)
        XCTAssertThrowsError(try recorder.demarrer(fichierCible: url2)) { error in
            XCTAssertEqual(error as? AudioNoteRecorderErreur, .enregistrementDejaEnCours)
        }
    }

    func testArreterSansDebutRetourneNil() {
        let resultat = recorder.arreter()
        XCTAssertNil(resultat, "arreter() sans demarrer() doit retourner nil")
    }

    func testEstEnCoursApresDepart() throws {
        let url = tempDir.appendingPathComponent("test.wav")
        XCTAssertFalse(recorder.estEnCours)
        try recorder.demarrer(fichierCible: url)
        XCTAssertTrue(recorder.estEnCours)
        recorder.arreter()
        XCTAssertFalse(recorder.estEnCours)
    }

    func testResultatContientChemin() throws {
        let url = tempDir.appendingPathComponent("note.wav")
        try recorder.demarrer(fichierCible: url)
        let resultat = recorder.arreter()
        XCTAssertEqual(url.path, resultat?.chemin)
    }

    func testDureeMsecPositive() throws {
        let url = tempDir.appendingPathComponent("duree.wav")
        try recorder.demarrer(fichierCible: url)
        let resultat = recorder.arreter()
        XCTAssertGreaterThanOrEqual(resultat?.dureeMsec ?? -1, 0)
    }

    // MARK: - Écriture WAV

    func testFichierWAVCreeSurDemarrer() throws {
        let url = tempDir.appendingPathComponent("sortie.wav")
        try recorder.demarrer(fichierCible: url)
        recorder.arreter()

        XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))
        let data = try Data(contentsOf: url)
        let riff = String(bytes: data[0..<4], encoding: .ascii) ?? ""
        XCTAssertEqual("RIFF", riff, "Le fichier produit doit être un WAV valide")
    }

    func testEchantillonsEcritsVersWAV() throws {
        let url = tempDir.appendingPathComponent("echantillons.wav")
        try recorder.demarrer(fichierCible: url)
        moteur.simulerEchantillons([100, 200, 300])
        recorder.arreter()

        let data = try Data(contentsOf: url)
        // 44 octets d'en-tête + 3 échantillons × 2 octets = 50
        XCTAssertEqual(50, data.count, "3 échantillons Int16 → 6 octets de données + 44 d'en-tête")
    }

    // MARK: - Transcripteur

    func testEchantillonsTransmisAuTranscripteur() throws {
        let url = tempDir.appendingPathComponent("transcription.wav")
        let transcripteur = FakeTranscripteurLocal()
        try recorder.demarrer(fichierCible: url, transcripteur: transcripteur)
        moteur.simulerEchantillons([10, 20, 30])
        moteur.simulerEchantillons([-1, -2])
        recorder.arreter()

        XCTAssertEqual(2, transcripteur.echantillonsRecus.count,
                       "Deux appels simulerEchantillons → deux appels accepterEchantillons")
        XCTAssertEqual([10, 20, 30], transcripteur.echantillonsRecus[0])
        XCTAssertEqual([-1, -2], transcripteur.echantillonsRecus[1])
    }

    func testSansTranscripteurNePasPlanter() throws {
        let url = tempDir.appendingPathComponent("sans_transcripteur.wav")
        try recorder.demarrer(fichierCible: url, transcripteur: nil)
        moteur.simulerEchantillons([1, 2, 3])
        let resultat = recorder.arreter()
        XCTAssertNotNil(resultat)
    }

    // MARK: - Arrêt automatique

    func testArretAutoApresDelai() throws {
        let recorder = AudioNoteRecorder(moteur: moteur, dureeMaxSecondes: 0.05)
        let url = tempDir.appendingPathComponent("auto.wav")
        try recorder.demarrer(fichierCible: url)
        XCTAssertTrue(recorder.estEnCours)
        RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.25))
        XCTAssertFalse(recorder.estEnCours,
                       "L'enregistrement doit s'arrêter automatiquement après dureeMaxSecondes")
    }

    // MARK: - Erreur moteur

    func testErreurMoteurPropagee() throws {
        moteur.prochainErreur = NSError(domain: "test", code: 42)
        let url = tempDir.appendingPathComponent("erreur.wav")
        XCTAssertThrowsError(try recorder.demarrer(fichierCible: url))
        XCTAssertFalse(recorder.estEnCours, "estEnCours doit rester false si demarrer() échoue")
    }
}