|
10 | 10 | from flickr_api.flickrerrors import FlickrAPIError, FlickrError |
11 | 11 |
|
12 | 12 | from flickr_download.flick_download import ( |
| 13 | + _download_file, |
| 14 | + _get_extension_from_url, |
13 | 15 | _get_metadata_db, |
| 16 | + _get_url_from_extras, |
14 | 17 | _load_defaults, |
15 | 18 | do_download_photo, |
16 | 19 | download_list, |
17 | 20 | find_user, |
18 | 21 | ) |
19 | 22 |
|
20 | 23 |
|
| 24 | +class TestGetUrlFromExtras: |
| 25 | + """Tests for _get_url_from_extras function.""" |
| 26 | + |
| 27 | + def test_get_url_with_specific_size_label(self) -> None: |
| 28 | + """Returns URL for specific size label.""" |
| 29 | + mock_photo = Mock() |
| 30 | + mock_photo.get = Mock( |
| 31 | + side_effect=lambda k: {"url_l": "https://example.com/large.jpg"}.get(k) |
| 32 | + ) |
| 33 | + |
| 34 | + result = _get_url_from_extras(mock_photo, "Large") |
| 35 | + assert result == "https://example.com/large.jpg" |
| 36 | + |
| 37 | + def test_get_url_largest_available(self) -> None: |
| 38 | + """Returns largest available URL when no size specified.""" |
| 39 | + mock_photo = Mock() |
| 40 | + # Only medium size available |
| 41 | + mock_photo.get = Mock( |
| 42 | + side_effect=lambda k: {"url_m": "https://example.com/medium.jpg"}.get(k) |
| 43 | + ) |
| 44 | + |
| 45 | + result = _get_url_from_extras(mock_photo, None) |
| 46 | + assert result == "https://example.com/medium.jpg" |
| 47 | + |
| 48 | + def test_get_url_prefers_original(self) -> None: |
| 49 | + """Prefers original URL when available.""" |
| 50 | + mock_photo = Mock() |
| 51 | + mock_photo.get = Mock( |
| 52 | + side_effect=lambda k: { |
| 53 | + "url_o": "https://example.com/original.jpg", |
| 54 | + "url_l": "https://example.com/large.jpg", |
| 55 | + }.get(k) |
| 56 | + ) |
| 57 | + |
| 58 | + result = _get_url_from_extras(mock_photo, None) |
| 59 | + assert result == "https://example.com/original.jpg" |
| 60 | + |
| 61 | + def test_returns_none_when_no_urls(self) -> None: |
| 62 | + """Returns None when no URLs in extras.""" |
| 63 | + mock_photo = Mock() |
| 64 | + mock_photo.get = Mock(return_value=None) |
| 65 | + |
| 66 | + result = _get_url_from_extras(mock_photo, None) |
| 67 | + assert result is None |
| 68 | + |
| 69 | + def test_returns_none_for_unknown_size_label(self) -> None: |
| 70 | + """Returns None for unknown size label.""" |
| 71 | + mock_photo = Mock() |
| 72 | + mock_photo.get = Mock(return_value=None) |
| 73 | + |
| 74 | + result = _get_url_from_extras(mock_photo, "UnknownSize") |
| 75 | + assert result is None |
| 76 | + |
| 77 | + |
| 78 | +class TestGetExtensionFromUrl: |
| 79 | + """Tests for _get_extension_from_url function.""" |
| 80 | + |
| 81 | + def test_extracts_jpg_extension(self) -> None: |
| 82 | + """Extracts .jpg extension from URL.""" |
| 83 | + url = "https://farm1.staticflickr.com/123/456_abc_o.jpg" |
| 84 | + assert _get_extension_from_url(url) == ".jpg" |
| 85 | + |
| 86 | + def test_extracts_png_extension(self) -> None: |
| 87 | + """Extracts .png extension from URL.""" |
| 88 | + url = "https://farm1.staticflickr.com/123/456_abc_o.png" |
| 89 | + assert _get_extension_from_url(url) == ".png" |
| 90 | + |
| 91 | + def test_handles_query_string(self) -> None: |
| 92 | + """Ignores query string when extracting extension.""" |
| 93 | + url = "https://farm1.staticflickr.com/123/456_abc_o.jpg?size=large" |
| 94 | + assert _get_extension_from_url(url) == ".jpg" |
| 95 | + |
| 96 | + def test_defaults_to_jpg_when_no_extension(self) -> None: |
| 97 | + """Defaults to .jpg when URL has no extension.""" |
| 98 | + url = "https://farm1.staticflickr.com/123/456_abc_o" |
| 99 | + assert _get_extension_from_url(url) == ".jpg" |
| 100 | + |
| 101 | + |
| 102 | +class TestDownloadFile: |
| 103 | + """Tests for _download_file function.""" |
| 104 | + |
| 105 | + @patch("flickr_download.flick_download.requests.get") |
| 106 | + def test_downloads_file_successfully(self, mock_get: Mock) -> None: |
| 107 | + """Downloads file from URL to local path.""" |
| 108 | + mock_response = Mock() |
| 109 | + mock_response.iter_content = Mock(return_value=[b"test content"]) |
| 110 | + mock_response.raise_for_status = Mock() |
| 111 | + mock_get.return_value = mock_response |
| 112 | + |
| 113 | + with tempfile.NamedTemporaryFile(delete=False) as f: |
| 114 | + fname = f.name |
| 115 | + |
| 116 | + try: |
| 117 | + _download_file("https://example.com/photo.jpg", fname) |
| 118 | + |
| 119 | + mock_get.assert_called_once_with( |
| 120 | + "https://example.com/photo.jpg", stream=True, timeout=60 |
| 121 | + ) |
| 122 | + with open(fname, "rb") as f: |
| 123 | + assert f.read() == b"test content" |
| 124 | + finally: |
| 125 | + Path(fname).unlink(missing_ok=True) |
| 126 | + |
| 127 | + @patch("flickr_download.flick_download.requests.get") |
| 128 | + def test_raises_on_http_error(self, mock_get: Mock) -> None: |
| 129 | + """Raises exception on HTTP error.""" |
| 130 | + import requests |
| 131 | + |
| 132 | + mock_response = Mock() |
| 133 | + mock_response.raise_for_status = Mock(side_effect=requests.HTTPError("404 Not Found")) |
| 134 | + mock_get.return_value = mock_response |
| 135 | + |
| 136 | + with tempfile.NamedTemporaryFile(delete=False) as f: |
| 137 | + fname = f.name |
| 138 | + |
| 139 | + try: |
| 140 | + try: |
| 141 | + _download_file("https://example.com/notfound.jpg", fname) |
| 142 | + assert False, "Expected HTTPError" |
| 143 | + except requests.HTTPError: |
| 144 | + pass # Expected |
| 145 | + finally: |
| 146 | + Path(fname).unlink(missing_ok=True) |
| 147 | + |
| 148 | + |
21 | 149 | class TestFindUser: |
22 | 150 | """Tests for find_user function.""" |
23 | 151 |
|
@@ -342,6 +470,50 @@ def mock_get_filename(pset: object, photo: object, suffix: Optional[str]) -> str |
342 | 470 | # Should not call save with skip_download=True |
343 | 471 | mock_photo.save.assert_not_called() |
344 | 472 |
|
| 473 | + @patch("flickr_download.flick_download._download_file") |
| 474 | + @patch("flickr_download.flick_download.set_file_time") |
| 475 | + def test_download_uses_prefetched_url( |
| 476 | + self, mock_set_file_time: Mock, mock_download_file: Mock |
| 477 | + ) -> None: |
| 478 | + """do_download_photo uses pre-fetched URL when available.""" |
| 479 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 480 | + mock_photo = Mock() |
| 481 | + mock_photo.id = "123" |
| 482 | + mock_photo.title = "Test Photo" |
| 483 | + mock_photo.__getitem__ = Mock( |
| 484 | + side_effect=lambda k: { |
| 485 | + "loaded": True, |
| 486 | + "taken": "2020-01-01 12:00:00", |
| 487 | + }.get(k) |
| 488 | + ) |
| 489 | + # Return prefetched URL |
| 490 | + mock_photo.get = Mock( |
| 491 | + side_effect=lambda k: {"url_o": "https://example.com/original.jpg"}.get(k) |
| 492 | + ) |
| 493 | + mock_photo.save = Mock() |
| 494 | + |
| 495 | + mock_pset = Mock() |
| 496 | + mock_pset.title = "Test Set" |
| 497 | + |
| 498 | + def mock_get_filename(pset: object, photo: object, suffix: Optional[str]) -> str: |
| 499 | + return "Test Photo" |
| 500 | + |
| 501 | + do_download_photo( |
| 502 | + tmpdir, |
| 503 | + mock_pset, |
| 504 | + mock_photo, |
| 505 | + None, |
| 506 | + "", |
| 507 | + mock_get_filename, |
| 508 | + ) |
| 509 | + |
| 510 | + # Should use _download_file instead of photo.save |
| 511 | + mock_download_file.assert_called_once() |
| 512 | + mock_photo.save.assert_not_called() |
| 513 | + # Verify correct URL was used |
| 514 | + call_args = mock_download_file.call_args[0] |
| 515 | + assert call_args[0] == "https://example.com/original.jpg" |
| 516 | + |
345 | 517 |
|
346 | 518 | class TestDownloadList: |
347 | 519 | """Tests for download_list function.""" |
|
0 commit comments