test_download.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests fuer das Download-Modul.
  4. """
  5. import pytest
  6. from pathlib import Path
  7. from unittest.mock import MagicMock, AsyncMock, patch
  8. from trixy_core.utils.download import (
  9. DownloadProgress,
  10. download_file,
  11. download_from_huggingface,
  12. download_from_github,
  13. download_and_extract,
  14. format_size,
  15. )
  16. from trixy_core.events.event_data.download import (
  17. BeforeDownload,
  18. DownloadCompleted,
  19. DownloadFailed,
  20. )
  21. class TestFormatSize:
  22. """Tests fuer die format_size Funktion."""
  23. def test_bytes(self):
  24. """Testet Byte-Formatierung."""
  25. assert format_size(500) == "500.0 B"
  26. def test_kilobytes(self):
  27. """Testet KB-Formatierung."""
  28. assert format_size(1024) == "1.0 KB"
  29. assert format_size(2048) == "2.0 KB"
  30. def test_megabytes(self):
  31. """Testet MB-Formatierung."""
  32. assert format_size(1024 * 1024) == "1.0 MB"
  33. assert format_size(5 * 1024 * 1024) == "5.0 MB"
  34. def test_gigabytes(self):
  35. """Testet GB-Formatierung."""
  36. assert format_size(1024 * 1024 * 1024) == "1.0 GB"
  37. def test_terabytes(self):
  38. """Testet TB-Formatierung."""
  39. assert format_size(1024 * 1024 * 1024 * 1024) == "1.0 TB"
  40. class TestDownloadProgress:
  41. """Tests fuer die DownloadProgress Klasse."""
  42. def test_initial_state(self):
  43. """Testet den initialen Zustand."""
  44. progress = DownloadProgress(total_size=1000)
  45. assert progress.downloaded == 0
  46. assert progress.total_size == 1000
  47. assert progress.percent == 0.0
  48. def test_update(self):
  49. """Testet das Update."""
  50. progress = DownloadProgress(total_size=1000)
  51. progress.update(500)
  52. assert progress.downloaded == 500
  53. assert progress.percent == 50.0
  54. def test_zero_total_size(self):
  55. """Testet mit total_size=0."""
  56. progress = DownloadProgress(total_size=0)
  57. progress.update(100)
  58. assert progress.percent == 0.0
  59. class TestDownloadFile:
  60. """Tests fuer die download_file Funktion."""
  61. @pytest.mark.asyncio
  62. async def test_creates_parent_directories(self, tmp_path):
  63. """Testet, dass Parent-Verzeichnisse erstellt werden."""
  64. dest_path = tmp_path / "subdir" / "file.txt"
  65. # Mock aiohttp
  66. with patch("trixy_core.utils.download.download_file") as mock_download:
  67. mock_download.return_value = True
  68. # Verzeichnis sollte erstellt werden
  69. dest_path.parent.mkdir(parents=True, exist_ok=True)
  70. assert dest_path.parent.exists()
  71. @pytest.mark.asyncio
  72. async def test_invalid_url_returns_false(self, tmp_path):
  73. """Testet, dass ungueltige URLs False zurueckgeben."""
  74. dest_path = tmp_path / "file.txt"
  75. result = await download_file(
  76. url="https://invalid.url.that.does.not.exist/file.txt",
  77. dest_path=dest_path,
  78. timeout=5,
  79. )
  80. assert result is False
  81. class TestDownloadFromGithub:
  82. """Tests fuer download_from_github."""
  83. def test_url_construction(self):
  84. """Testet, dass die GitHub URL korrekt konstruiert wird."""
  85. expected_url = "https://github.com/owner/repo/releases/download/v1.0.0/file.zip"
  86. assert "github.com" in expected_url
  87. assert "releases/download" in expected_url
  88. class TestDownloadEvents:
  89. """Tests fuer Download-Events mit trigger()."""
  90. @pytest.fixture
  91. def mock_event_manager(self):
  92. """Erstellt einen Mock-EventManager."""
  93. em = MagicMock()
  94. em.trigger = AsyncMock()
  95. return em
  96. @pytest.mark.asyncio
  97. async def test_before_download_event(self, tmp_path, mock_event_manager):
  98. """Testet, dass before_download Event getriggert wird."""
  99. dest_path = tmp_path / "file.txt"
  100. # trigger() gibt das Event-Objekt zurueck (nicht-gecancelled)
  101. mock_event_manager.trigger.return_value = BeforeDownload(
  102. download_id="test-download",
  103. url="https://example.com/nonexistent.txt",
  104. filename="file.txt",
  105. dest_path=str(dest_path),
  106. )
  107. await download_file(
  108. url="https://example.com/nonexistent.txt",
  109. dest_path=dest_path,
  110. event_manager=mock_event_manager,
  111. download_id="test-download",
  112. timeout=5,
  113. )
  114. # Pruefen, dass trigger() aufgerufen wurde
  115. calls = mock_event_manager.trigger.call_args_list
  116. event_names = [call[0][0] for call in calls]
  117. assert "before_download" in event_names
  118. # Da der Download fehlschlaegt, sollte auch download_failed getriggert werden
  119. assert "download_failed" in event_names
  120. @pytest.mark.asyncio
  121. async def test_before_download_cancel(self, tmp_path, mock_event_manager):
  122. """Testet, dass ein gecancelltes BeforeDownload den Download verhindert."""
  123. dest_path = tmp_path / "file.txt"
  124. # trigger() gibt ein gecancelltes Event zurueck
  125. cancelled_event = BeforeDownload(
  126. download_id="test-download",
  127. url="https://example.com/file.txt",
  128. filename="file.txt",
  129. dest_path=str(dest_path),
  130. )
  131. cancelled_event.cancel()
  132. mock_event_manager.trigger.return_value = cancelled_event
  133. result = await download_file(
  134. url="https://example.com/file.txt",
  135. dest_path=dest_path,
  136. event_manager=mock_event_manager,
  137. download_id="test-download",
  138. )
  139. assert result is False
  140. # Nur before_download sollte getriggert worden sein — kein Download-Versuch
  141. calls = mock_event_manager.trigger.call_args_list
  142. assert len(calls) == 1
  143. assert calls[0][0][0] == "before_download"
  144. @pytest.mark.asyncio
  145. async def test_before_download_redirect(self, tmp_path, mock_event_manager):
  146. """Testet, dass ein Plugin die URL aendern kann."""
  147. dest_path = tmp_path / "file.txt"
  148. new_dest = tmp_path / "redirected.txt"
  149. # trigger() gibt Event mit geaenderter URL/Pfad zurueck
  150. redirected_event = BeforeDownload(
  151. download_id="test-download",
  152. url="https://mirror.local/file.txt",
  153. filename="redirected.txt",
  154. dest_path=str(new_dest),
  155. )
  156. mock_event_manager.trigger.return_value = redirected_event
  157. await download_file(
  158. url="https://example.com/file.txt",
  159. dest_path=dest_path,
  160. event_manager=mock_event_manager,
  161. download_id="test-download",
  162. )
  163. # Pruefen, dass download_failed mit der neuen URL getriggert wurde
  164. calls = mock_event_manager.trigger.call_args_list
  165. failed_calls = [c for c in calls if c[0][0] == "download_failed"]
  166. if failed_calls:
  167. failed_event = failed_calls[0][0][1]
  168. assert isinstance(failed_event, DownloadFailed)
  169. assert failed_event.url == "https://mirror.local/file.txt"
  170. class TestDownloadAndExtract:
  171. """Tests fuer download_and_extract."""
  172. @pytest.mark.asyncio
  173. async def test_unsupported_format_fails(self, tmp_path):
  174. """Testet, dass nicht unterstuetzte Formate fehlschlagen."""
  175. result = await download_and_extract(
  176. url="https://example.com/file.rar",
  177. dest_dir=tmp_path,
  178. )
  179. assert result is False