from __future__ import annotations from pathlib import Path import shutil import subprocess import sys import unittest import uuid from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) from narration_import import NarrationImportError, import_wav, remove_narration # noqa: E402 class NarrationImportTests(unittest.TestCase): def setUp(self) -> None: cache_root = Path(__file__).resolve().parents[1] / ".cache" cache_root.mkdir(exist_ok=True) self.directory = cache_root / f"narration-import-test-{uuid.uuid4().hex}" self.directory.mkdir() self.source = self.directory / "source.wav" self.source.write_bytes(b"disposable wav data") def tearDown(self) -> None: shutil.rmtree(self.directory) def test_success_uses_fixed_preset_and_replaces_only_after_temp_output(self) -> None: destination = self.directory / "narration.mp3" destination.write_bytes(b"old narration") def successful_ffmpeg(command: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]: Path(command[-1]).write_bytes(b"converted mp3") return subprocess.CompletedProcess(command, 0, "", "") with mock.patch("narration_import.subprocess.run", side_effect=successful_ffmpeg) as run: result = import_wav(self.directory, self.source, ffmpeg="ffmpeg") self.assertEqual(result, destination) self.assertEqual(destination.read_bytes(), b"converted mp3") command = run.call_args.args[0] self.assertEqual( command[1:-1], ["-y", "-i", str(self.source), "-vn", "-ar", "44100", "-ac", "1", "-c:a", "libmp3lame", "-b:a", "96k"], ) self.assertFalse(any(self.directory.glob(".narration-*.mp3"))) def test_failed_conversion_preserves_existing_narration(self) -> None: destination = self.directory / "narration.mp3" destination.write_bytes(b"old narration") failed = subprocess.CompletedProcess(["ffmpeg"], 1, "", "bad WAV") with mock.patch("narration_import.subprocess.run", return_value=failed): with self.assertRaisesRegex(NarrationImportError, "bad WAV"): import_wav(self.directory, self.source, ffmpeg="ffmpeg") self.assertEqual(destination.read_bytes(), b"old narration") self.assertFalse(any(self.directory.glob(".narration-*.mp3"))) def test_missing_ffmpeg_and_removal_are_safe(self) -> None: with mock.patch("narration_import.shutil.which", return_value=None): with self.assertRaisesRegex(NarrationImportError, "ffmpeg is required"): import_wav(self.directory, self.source) unrelated = self.directory / "other-attachment.txt" unrelated.write_text("keep", encoding="utf-8") narration = self.directory / "narration.mp3" narration.write_bytes(b"narration") self.assertTrue(remove_narration(self.directory)) self.assertFalse(narration.exists()) self.assertEqual(unrelated.read_text(encoding="utf-8"), "keep") def test_repository_fallback_is_used_when_ffmpeg_is_not_on_path(self) -> None: fallback = self.directory / "ffmpeg.exe" fallback.write_bytes(b"placeholder executable") def successful_ffmpeg(command: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]: Path(command[-1]).write_bytes(b"converted mp3") return subprocess.CompletedProcess(command, 0, "", "") with mock.patch("narration_import.shutil.which", return_value=None), mock.patch( "narration_import.subprocess.run", side_effect=successful_ffmpeg ) as run: import_wav(self.directory, self.source, fallback_ffmpeg=fallback) self.assertEqual(run.call_args.args[0][0], str(fallback)) if __name__ == "__main__": unittest.main()