"""Tests for the Blender downloader module.""" import io import tarfile import tempfile import unittest import zipfile from pathlib import Path from unittest.mock import patch from blender_downloader import ( extract_tar_archive, extract_zip_archive, get_archive_extension, get_blender_executable, get_download_url, get_extracted_dir_name, ) class TestGetDownloadUrl(unittest.TestCase): def test_builds_correct_url(self) -> None: with patch("blender_downloader.get_platform_suffix", return_value="linux-x64"): with patch( "blender_downloader.get_archive_extension", return_value=".tar.xz" ): url = get_download_url("5.0.1") self.assertEqual( url, "https://download.blender.org/release/Blender5.0/blender-5.0.1-linux-x64.tar.xz", ) def test_major_minor_extraction(self) -> None: with patch("blender_downloader.get_platform_suffix", return_value="linux-x64"): with patch( "blender_downloader.get_archive_extension", return_value=".tar.xz" ): url = get_download_url("4.3.2") self.assertEqual( url, "https://download.blender.org/release/Blender4.3/blender-4.3.2-linux-x64.tar.xz", ) class TestGetExtractedDirName(unittest.TestCase): def test_dir_name_uses_platform_suffix(self) -> None: with patch( "blender_downloader.get_platform_suffix", return_value="windows-x64" ): name = get_extracted_dir_name("5.0.1") self.assertEqual(name, "blender-5.0.1-windows-x64") class TestGetArchiveExtension(unittest.TestCase): def test_windows_extension(self) -> None: with patch("blender_downloader.platform.system", return_value="Windows"): self.assertEqual(get_archive_extension(), ".zip") def test_non_windows_extension(self) -> None: with patch("blender_downloader.platform.system", return_value="Linux"): self.assertEqual(get_archive_extension(), ".tar.xz") class TestGetBlenderExecutable(unittest.TestCase): def _touch_executable(self, path: Path) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text("") def test_cache_match_is_exact_for_major_minor(self) -> None: with tempfile.TemporaryDirectory() as td: downloads = Path(td) target_dir = downloads / "blender-5.1.2-linux-x64" self._touch_executable(target_dir / "blender") # Must not match when requesting 5.1 wrong_dir = downloads / "blender-5.10.1-linux-x64" self._touch_executable(wrong_dir / "blender") with patch("blender_downloader.DOWNLOADS_DIR", downloads): with patch( "blender_downloader.get_platform_suffix", return_value="linux-x64" ): with patch( "blender_downloader.platform.system", return_value="Linux" ): with patch("blender_downloader.find_latest_patch") as latest: executable = get_blender_executable("5.1") self.assertEqual(executable, target_dir / "blender") latest.assert_not_called() def test_uses_windows_executable_name(self) -> None: with tempfile.TemporaryDirectory() as td: downloads = Path(td) target_dir = downloads / "blender-5.0.3-windows-x64" self._touch_executable(target_dir / "blender.exe") with patch("blender_downloader.DOWNLOADS_DIR", downloads): with patch( "blender_downloader.get_platform_suffix", return_value="windows-x64" ): with patch( "blender_downloader.platform.system", return_value="Windows" ): executable = get_blender_executable("5.0") self.assertEqual(executable, target_dir / "blender.exe") def test_uses_macos_bundle_executable_path(self) -> None: with tempfile.TemporaryDirectory() as td: downloads = Path(td) target_dir = downloads / "blender-5.1.1-macos-arm64" self._touch_executable( target_dir / "Blender.app" / "Contents" / "MacOS" / "Blender" ) with patch("blender_downloader.DOWNLOADS_DIR", downloads): with patch( "blender_downloader.get_platform_suffix", return_value="macos-arm64" ): with patch( "blender_downloader.platform.system", return_value="Darwin" ): executable = get_blender_executable("5.1") self.assertEqual( executable, target_dir / "Blender.app" / "Contents" / "MacOS" / "Blender", ) class TestSafeExtraction(unittest.TestCase): def test_tar_extraction_blocks_path_traversal(self) -> None: with tempfile.TemporaryDirectory() as td: temp_dir = Path(td) archive = temp_dir / "bad.tar.xz" out_dir = temp_dir / "extract" out_dir.mkdir() info = tarfile.TarInfo("../evil.txt") data = b"malicious" info.size = len(data) with tarfile.open(archive, "w:xz") as tar: tar.addfile(info, io.BytesIO(data)) with self.assertRaises(RuntimeError): extract_tar_archive(archive, out_dir) self.assertFalse((temp_dir / "evil.txt").exists()) def test_zip_extraction_blocks_path_traversal(self) -> None: with tempfile.TemporaryDirectory() as td: temp_dir = Path(td) archive = temp_dir / "bad.zip" out_dir = temp_dir / "extract" out_dir.mkdir() with zipfile.ZipFile(archive, "w") as zf: zf.writestr("../evil.txt", "malicious") with self.assertRaises(RuntimeError): extract_zip_archive(archive, out_dir) self.assertFalse((temp_dir / "evil.txt").exists()) if __name__ == "__main__": unittest.main()