| | @@ -0,0 +1,538 @@ |
| 1 | +"""Tests for all source connectors: import, instantiation, authenticate, list_videos."""
|
| 2 | +
|
| 3 | +import os
|
| 4 | +from unittest.mock import MagicMock, patch
|
| 5 | +
|
| 6 | +import pytest
|
| 7 | +
|
| 8 | +from video_proes.base import BaseSource, SourceFile
|
| 9 | +
|
| 10 | +# ---------------------------------------------------------------------------
|
| 11 | +# SourceFile model
|
| 12 | +# ---------------------------------------------------------------------------
|
| 13 | +
|
| 14 | +
|
| 15 | +def test_source_file_creation():
|
| 16 | + sf = SourceFile(name="test.mp4", id="abc123")
|
| 17 | + assert sf.name == "test.mp4"
|
| 18 | + assert sf.id == "abc123"
|
| 19 | + assert sf.size_bytes is None
|
| 20 | + assert sf.mime_type is None
|
| 21 | +
|
| 22 | +
|
| 23 | +def test_source_file_with_all_fields():
|
| 24 | + sf = SourceFile(
|
| 25 | + name="video.mp4",
|
| 26 | + id="v1",
|
| 27 | + size_bytes=1024,
|
| 28 | + mime_type="video/mp4",
|
| 29 | + modified_at="2025-01-01",
|
| 30 | + path="folder/video.mp4",
|
| 31 | + )
|
| 32 | + assert sf.size_bytes == 1024
|
| 33 | + assert sf.path == "folder/video.mp4"
|
| 34 | +
|
| 35 | +
|
| 36 | +# ---------------------------------------------------------------------------
|
| 37 | +# YouTubeSource
|
| 38 | +# ---------------------------------------------------------------------------
|
| 39 | +
|
| 40 | +
|
| 41 | +class TestYouTubeSource:
|
| 42 | + def test_import(self):
|
| 43 | + from video_processor.sources.youtube_source import YouTubeSource
|
| 44 | +
|
| 45 | + assert YouTubeSource is not None
|
| 46 | +
|
| 47 | + def test_constructor(self):
|
| 48 | + from video_processor.sources.youtube_source import YouTubeSource
|
| 49 | +
|
| 50 | + src = YouTubeSource(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
| 51 | + assert src.video_id == "dQw4w9WgXcQ"
|
| 52 | + assert src.audio_only is False
|
| 53 | +
|
| 54 | + def test_constructor_audio_only(self):
|
| 55 | + from video_processor.sources.youtube_source import YouTubeSource
|
| 56 | +
|
| 57 | + src = YouTubeSource(url="https://youtu.be/dQw4w9WgXcQ", audio_only=True)
|
| 58 | + assert src.audio_only is True
|
| 59 | +
|
| 60 | + def test_constructor_shorts_url(self):
|
| 61 | + from video_processor.sources.youtube_source import YouTubeSource
|
| 62 | +
|
| 63 | + src = YouTubeSource(url="https://youtube.com/shorts/dQw4w9WgXcQ")
|
| 64 | + assert src.video_id == "dQw4w9WgXcQ"
|
| 65 | +
|
| 66 | + def test_constructor_invalid_url(self):
|
| 67 | + from video_processor.sources.youtube_source import YouTubeSource
|
| 68 | +
|
| 69 | + with pytest.raises(ValueError, match="Could not extract"):
|
| 70 | + YouTubeSource(url="https://example.com/not-youtube")
|
| 71 | +
|
| 72 | + @patch.dict(os.environ, {}, clear=False)
|
| 73 | + def test_authenticate_no_ytdlp(self):
|
| 74 | + from video_processor.sources.youtube_source import YouTubeSource
|
| 75 | +
|
| 76 | + src = YouTubeSource(url="https://youtube.com/watch?v=dQw4w9WgXcQ")
|
| 77 | + with patch.dict("sys.modules", {"yt_dlp": None}):
|
| 78 | + # yt_dlp import will fail
|
| 79 | + result = src.authenticate()
|
| 80 | + # Result depends on whether yt_dlp is installed; just check it returns bool
|
| 81 | + assert isinstance(result, bool)
|
| 82 | +
|
| 83 | + def test_list_videos(self):
|
| 84 | + from video_processor.sources.youtube_source import YouTubeSource
|
| 85 | +
|
| 86 | + mock_ydl = MagicMock()
|
| 87 | + mock_ydl.__enter__ = MagicMock(return_value=mock_ydl)
|
| 88 | + mock_ydl.__exit__ = MagicMock(return_value=False)
|
| 89 | + mock_ydl.extract_info.return_value = {
|
| 90 | + "title": "Test Video",
|
| 91 | + "filesize": 1000,
|
| 92 | + }
|
| 93 | + mock_ydl_cls = MagicMock(return_value=mock_ydl)
|
| 94 | + mock_module = MagicMock()
|
| 95 | + mock_module.YoutubeDL = mock_ydl_cls
|
| 96 | +
|
| 97 | + with patch.dict("sys.modules", {"yt_dlp": mock_module}):
|
| 98 | + src = YouTubeSource(url="https://youtube.com/watch?v=dQw4w9WgXcQ")
|
| 99 | + files = src.list_videos()
|
| 100 | + assert isinstance(files, list)
|
| 101 | + assert len(files) == 1
|
| 102 | + assert files[0].name == "Test Video"
|
| 103 | +
|
| 104 | +
|
| 105 | +# ---------------------------------------------------------------------------
|
| 106 | +# WebSource
|
| 107 | +# ---------------------------------------------------------------------------
|
| 108 | +
|
| 109 | +
|
| 110 | +class TestWebSource:
|
| 111 | + def test_import(self):
|
| 112 | + from video_processor.sources.web_source import WebSource
|
| 113 | +
|
| 114 | + assert WebSource is not None
|
| 115 | +
|
| 116 | + def test_constructor(self):
|
| 117 | + from video_processor.sources.web_source import WebSource
|
| 118 | +
|
| 119 | + src = WebSource(url="https://example.com/page")
|
| 120 | + assert src.url == "https://example.com/page"
|
| 121 | +
|
| 122 | + def test_authenticate(self):
|
| 123 | + from video_processor.sources.web_source import WebSource
|
| 124 | +
|
| 125 | + src = WebSource(url="https://example.com")
|
| 126 | + assert src.authenticate() is True
|
| 127 | +
|
| 128 | + def test_list_videos(self):
|
| 129 | + from video_processor.sources.web_source import WebSource
|
| 130 | +
|
| 131 | + src = WebSource(url="https://example.com/article")
|
| 132 | + files = src.list_videos()
|
| 133 | + assert isinstance(files, list)
|
| 134 | + assert len(files) == 1
|
| 135 | + assert files[0].mime_type == "text/html"
|
| 136 | +
|
| 137 | +
|
| 138 | +# ---------------------------------------------------------------------------
|
| 139 | +# GitHubSource
|
| 140 | +# ---------------------------------------------------------------------------
|
| 141 | +
|
| 142 | +
|
| 143 | +class TestGitHubSource:
|
| 144 | + def test_import(self):
|
| 145 | + from video_processor.sources.github_source import GitHubSource
|
| 146 | +
|
| 147 | + assert GitHubSource is not None
|
| 148 | +
|
| 149 | + def test_constructor(self):
|
| 150 | + from video_processor.sources.github_source import GitHubSource
|
| 151 | +
|
| 152 | + src = GitHubSource(repo="owner/repo")
|
| 153 | + assert src.repo == "owner/repo"
|
| 154 | + assert src.include_issues is True
|
| 155 | + assert src.include_prs is True
|
| 156 | +
|
| 157 | + @patch.dict(os.environ, {"GITHUB_TOKEN": "ghp_test123"})
|
| 158 | + def test_authenticate_with_env_token(self):
|
| 159 | + from video_processor.sources.github_source import GitHubSource
|
| 160 | +
|
| 161 | + src = GitHubSource(repo="owner/repo")
|
| 162 | + result = src.authenticate()
|
| 163 | + assert result is True
|
| 164 | + assert src._token == "ghp_test123"
|
| 165 | +
|
| 166 | + @patch("requests.get")
|
| 167 | + @patch.dict(os.environ, {"GITHUB_TOKEN": "ghp_test123"})
|
| 168 | + def test_list_videos(self, mock_get):
|
| 169 | + from video_processor.sources.github_source import GitHubSource
|
| 170 | +
|
| 171 | + # Mock responses for readme, issues, and PRs
|
| 172 | + readme_resp = MagicMock()
|
| 173 | + readme_resp.ok = True
|
| 174 | +
|
| 175 | + issues_resp = MagicMock()
|
| 176 | + issues_resp.ok = True
|
| 177 | + issues_resp.json.return_value = [
|
| 178 | + {"number": 1, "title": "Bug report", "id": 1},
|
| 179 | + {"number": 2, "title": "Feature request", "id": 2, "pull_request": {}},
|
| 180 | + ]
|
| 181 | +
|
| 182 | + prs_resp = MagicMock()
|
| 183 | + prs_resp.ok = True
|
| 184 | + prs_resp.json.return_value = [
|
| 185 | + {"number": 3, "title": "Fix bug"},
|
| 186 | + ]
|
| 187 | +
|
| 188 | + mock_get.side_effect = [readme_resp, issues_resp, prs_resp]
|
| 189 | +
|
| 190 | + src = GitHubSource(repo="owner/repo")
|
| 191 | + src.authenticate()
|
| 192 | + files = src.list_videos()
|
| 193 | + assert isinstance(files, list)
|
| 194 | + # README + 1 issue (one filtered as PR) + 1 PR = 3
|
| 195 | + assert len(files) == 3
|
| 196 | +
|
| 197 | +
|
| 198 | +# ---------------------------------------------------------------------------
|
| 199 | +# RedditSource
|
| 200 | +# ---------------------------------------------------------------------------
|
| 201 | +
|
| 202 | +
|
| 203 | +class TestRedditSource:
|
| 204 | + def test_import(self):
|
| 205 | + from video_processor.sources.reddit_source import RedditSource
|
| 206 | +
|
| 207 | + assert RedditSource is not None
|
| 208 | +
|
| 209 | + def test_constructor(self):
|
| 210 | + from video_processor.sources.reddit_source import RedditSource
|
| 211 | +
|
| 212 | + src = RedditSource(url="https://reddit.com/r/python/comments/abc123/test/")
|
| 213 | + assert src.url == "https://reddit.com/r/python/comments/abc123/test"
|
| 214 | +
|
| 215 | + def test_authenticate(self):
|
| 216 | + from video_processor.sources.reddit_source import RedditSource
|
| 217 | +
|
| 218 | + src = RedditSource(url="https://reddit.com/r/test")
|
| 219 | + assert src.authenticate() is True
|
| 220 | +
|
| 221 | + def test_list_videos(self):
|
| 222 | + from video_processor.sources.reddit_source import RedditSource
|
| 223 | +
|
| 224 | + src = RedditSource(url="https://reddit.com/r/python/comments/abc/post")
|
| 225 | + files = src.list_videos()
|
| 226 | + assert isinstance(files, list)
|
| 227 | + assert len(files) == 1
|
| 228 | + assert files[0].mime_type == "text/plain"
|
| 229 | +
|
| 230 | +
|
| 231 | +# ---------------------------------------------------------------------------
|
| 232 | +# HackerNewsSource
|
| 233 | +# ---------------------------------------------------------------------------
|
| 234 | +
|
| 235 | +
|
| 236 | +class TestHackerNewsSource:
|
| 237 | + def test_import(self):
|
| 238 | + from video_processor.sources.hackernews_source import HackerNewsSource
|
| 239 | +
|
| 240 | + assert HackerNewsSource is not None
|
| 241 | +
|
| 242 | + def test_constructor(self):
|
| 243 | + from video_processor.sources.hackernews_source import HackerNewsSource
|
| 244 | +
|
| 245 | + src = HackerNewsSource(item_id=12345678)
|
| 246 | + assert src.item_id == 12345678
|
| 247 | + assert src.max_comments == 200
|
| 248 | +
|
| 249 | + def test_authenticate(self):
|
| 250 | + from video_processor.sources.hackernews_source import HackerNewsSource
|
| 251 | +
|
| 252 | + src = HackerNewsSource(item_id=12345678)
|
| 253 | + assert src.authenticate() is True
|
| 254 | +
|
| 255 | + def test_list_videos(self):
|
| 256 | + from video_processor.sources.hackernews_source import HackerNewsSource
|
| 257 | +
|
| 258 | + src = HackerNewsSource(item_id=99999)
|
| 259 | + files = src.list_videos()
|
| 260 | + assert isinstance(files, list)
|
| 261 | + assert len(files) == 1
|
| 262 | + assert files[0].id == "99999"
|
| 263 | +
|
| 264 | +
|
| 265 | +# ---------------------------------------------------------------------------
|
| 266 | +# RSSSource
|
| 267 | +# ---------------------------------------------------------------------------
|
| 268 | +
|
| 269 | +
|
| 270 | +class TestRSSSource:
|
| 271 | + def test_import(self):
|
| 272 | + from video_processor.sources.rss_source import RSSSource
|
| 273 | +
|
| 274 | + assert RSSSource is not None
|
| 275 | +
|
| 276 | + def test_constructor(self):
|
| 277 | + from video_processor.sources.rss_source import RSSSource
|
| 278 | +
|
| 279 | + src = RSSSource(url="https://example.com/feed.xml", max_entries=20)
|
| 280 | + assert src.url == "https://example.com/feed.xml"
|
| 281 | + assert src.max_entries == 20
|
| 282 | +
|
| 283 | + def test_authenticate(self):
|
| 284 | + from video_processor.sources.rss_source import RSSSource
|
| 285 | +
|
| 286 | + src = RSSSource(url="https://example.com/feed.xml")
|
| 287 | + assert src.authenticate() is True
|
| 288 | +
|
| 289 | + @patch("requests.get")
|
| 290 | + def test_list_videos(self, mock_get):
|
| 291 | + from video_processor.sources.rss_source import RSSSource
|
| 292 | +
|
| 293 | + rss_xml = """<?xml version="1.0"?>
|
| 294 | + <rss version="2.0">
|
| 295 | + <channel>
|
| 296 | + <item>
|
| 297 | + <title>Entry 1</title>
|
| 298 | + <link>https://example.com/1</link>
|
| 299 | + <description>First entry</description>
|
| 300 | + <pubDate>Mon, 01 Jan 2025 00:00:00 GMT</pubDate>
|
| 301 | + </item>
|
| 302 | + </channel>
|
| 303 | + </rss>"""
|
| 304 | + mock_resp = MagicMock()
|
| 305 | + mock_resp.text = rss_xml
|
| 306 | + mock_resp.raise_for_status = MagicMock()
|
| 307 | + mock_get.return_value = mock_resp
|
| 308 | +
|
| 309 | + src = RSSSource(url="https://example.com/feed.xml")
|
| 310 | + files = src.list_videos()
|
| 311 | + assert isinstance(files, list)
|
| 312 | + assert len(files) >= 1
|
| 313 | +
|
| 314 | +
|
| 315 | +# ---------------------------------------------------------------------------
|
| 316 | +# PodcastSource
|
| 317 | +# ---------------------------------------------------------------------------
|
| 318 | +
|
| 319 | +
|
| 320 | +class TestPodcastSource:
|
| 321 | + def test_import(self):
|
| 322 | + from video_processor.sources.podcast_source import PodcastSource
|
| 323 | +
|
| 324 | + assert PodcastSource is not None
|
| 325 | +
|
| 326 | + def test_constructor(self):
|
| 327 | + from video_processor.sources.podcast_source import PodcastSource
|
| 328 | +
|
| 329 | + src = PodcastSource(feed_url="https://example.com/podcast.xml", max_episodes=5)
|
| 330 | + assert src.feed_url == "https://example.com/podcast.xml"
|
| 331 | + assert src.max_episodes == 5
|
| 332 | +
|
| 333 | + def test_authenticate(self):
|
| 334 | + from video_processor.sources.podcast_source import PodcastSource
|
| 335 | +
|
| 336 | + src = PodcastSource(feed_url="https://example.com/podcast.xml")
|
| 337 | + assert src.authenticate() is True
|
| 338 | +
|
| 339 | + @patch("requests.get")
|
| 340 | + def test_list_videos(self, mock_get):
|
| 341 | + from video_processor.sources.podcast_source import PodcastSource
|
| 342 | +
|
| 343 | + podcast_xml = """<?xml version="1.0"?>
|
| 344 | + <rss version="2.0">
|
| 345 | + <channel>
|
| 346 | + <item>
|
| 347 | + <title>Episode 1</title>
|
| 348 | + <enclosure url="https://example.com/ep1.mp3" type="audio/mpeg" />
|
| 349 | + <pubDate>Mon, 01 Jan 2025 00:00:00 GMT</pubDate>
|
| 350 | + </item>
|
| 351 | + </channel>
|
| 352 | + </rss>"""
|
| 353 | + mock_resp = MagicMock()
|
| 354 | + mock_resp.text = podcast_xml
|
| 355 | + mock_resp.raise_for_status = MagicMock()
|
| 356 | + mock_get.return_value = mock_resp
|
| 357 | +
|
| 358 | + src = PodcastSource(feed_url="https://example.com/podcast.xml")
|
| 359 | + files = src.list_videos()
|
| 360 | + assert isinstance(files, list)
|
| 361 | + assert len(files) == 1
|
| 362 | + assert files[0].mime_type == "audio/mpeg"
|
| 363 | +
|
| 364 | +
|
| 365 | +# ---------------------------------------------------------------------------
|
| 366 | +# TwitterSource
|
| 367 | +# ---------------------------------------------------------------------------
|
| 368 | +
|
| 369 | +
|
| 370 | +class TestTwitterSource:
|
| 371 | + def test_import(self):
|
| 372 | + from video_processor.sources.twitter_source import TwitterSource
|
| 373 | +
|
| 374 | + assert TwitterSource is not None
|
| 375 | +
|
| 376 | + def test_constructor(self):
|
| 377 | + from video_processor.sources.twitter_source import TwitterSource
|
| 378 | +
|
| 379 | + src = TwitterSource(url="https://twitter.com/user/status/123456")
|
| 380 | + assert src.url == "https://twitter.com/user/status/123456"
|
| 381 | +
|
| 382 | + @patch.dict(os.environ, {"TWITTER_BEARER_TOKEN": "test_token"})
|
| 383 | + def test_authenticate_with_bearer_token(self):
|
| 384 | + from video_processor.sources.twitter_source import TwitterSource
|
| 385 | +
|
| 386 | + src = TwitterSource(url="https://twitter.com/user/status/123456")
|
| 387 | + assert src.authenticate() is True
|
| 388 | +
|
| 389 | + @patch.dict(os.environ, {}, clear=True)
|
| 390 | + def test_authenticate_no_token_no_gallery_dl(self):
|
| 391 | + from video_processor.sources.twitter_source import TwitterSource
|
| 392 | +
|
| 393 | + src = TwitterSource(url="https://twitter.com/user/status/123456")
|
| 394 | + with patch.dict("sys.modules", {"gallery_dl": None}):
|
| 395 | + result = src.authenticate()
|
| 396 | + assert isinstance(result, bool)
|
| 397 | +
|
| 398 | + def test_list_videos(self):
|
| 399 | + from video_processor.sources.twitter_source import TwitterSource
|
| 400 | +
|
| 401 | + src = TwitterSource(url="https://twitter.com/user/status/123456")
|
| 402 | + files = src.list_videos()
|
| 403 | + assert isinstance(files, list)
|
| 404 | + assert len(files) == 1
|
| 405 | +
|
| 406 | +
|
| 407 | +# ---------------------------------------------------------------------------
|
| 408 | +# ArxivSource
|
| 409 | +# ---------------------------------------------------------------------------
|
| 410 | +
|
| 411 | +
|
| 412 | +class TestArxivSource:
|
| 413 | + def test_import(self):
|
| 414 | + from video_processor.sources.arxiv_source import ArxivSource
|
| 415 | +
|
| 416 | + assert ArxivSource is not None
|
| 417 | +
|
| 418 | + def test_constructor(self):
|
| 419 | + from video_processor.sources.arxiv_source import ArxivSource
|
| 420 | +
|
| 421 | + src = ArxivSource(url_or_id="2301.07041")
|
| 422 | + assert src.arxiv_id == "2301.07041"
|
| 423 | +
|
| 424 | + def test_constructor_from_url(self):
|
| 425 | + from video_processor.sources.arxiv_source import ArxivSource
|
| 426 | +
|
| 427 | + src = ArxivSource(url_or_id="https://arxiv.org/abs/2301.07041v2")
|
| 428 | + assert src.arxiv_id == "2301.07041v2"
|
| 429 | +
|
| 430 | + def test_constructor_invalid(self):
|
| 431 | + from video_processor.sources.arxiv_source import ArxivSource
|
| 432 | +
|
| 433 | + with pytest.raises(ValueError, match="Could not extract"):
|
| 434 | + ArxivSource(url_or_id="not-an-arxiv-id")
|
| 435 | +
|
| 436 | + def test_authenticate(self):
|
| 437 | + from video_processor.sources.arxiv_source import ArxivSource
|
| 438 | +
|
| 439 | + src = ArxivSource(url_or_id="2301.07041")
|
| 440 | + assert src.authenticate() is True
|
| 441 | +
|
| 442 | + @patch("requests.get")
|
| 443 | + def test_list_videos(self, mock_get):
|
| 444 | + from video_processor.sources.arxiv_source import ArxivSource
|
| 445 | +
|
| 446 | + atom_xml = """<?xml version="1.0"?>
|
| 447 | + <feed xmlns="http://www.w3.org/2005/Atom"
|
| 448 | + xmlns:arxiv="http://arxiv.org/schemas/atom">
|
| 449 | + <entry>
|
| 450 | + <title>Test Paper</title>
|
| 451 | + <summary>Abstract text here.</summary>
|
| 452 | + <author><name>Author One</name></author>
|
| 453 | + <published>2023-01-15T00:00:00Z</published>
|
| 454 | + </entry>
|
| 455 | + </feed>"""
|
| 456 | + mock_resp = MagicMock()
|
| 457 | + mock_resp.text = atom_xml
|
| 458 | + mock_resp.raise_for_status = MagicMock()
|
| 459 | + mock_get.return_value = mock_resp
|
| 460 | +
|
| 461 | + src = ArxivSource(url_or_id="2301.07041")
|
| 462 | + files = src.list_videos()
|
| 463 | + assert isinstance(files, list)
|
| 464 | + assert len(files) == 2 # metadata + pdf
|
| 465 | +
|
| 466 | +
|
| 467 | +# ---------------------------------------------------------------------------
|
| 468 | +# S3Source
|
| 469 | +# ---------------------------------------------------------------------------
|
| 470 | +
|
| 471 | +
|
| 472 | +class TestS3Source:
|
| 473 | + def test_import(self):
|
| 474 | + from video_processor.sources.s3_source import S3Source
|
| 475 | +
|
| 476 | + assert S3Source is not None
|
| 477 | +
|
| 478 | + def test_constructor(self):
|
| 479 | + from video_processor.sources.s3_source import S3Source
|
| 480 | +
|
| 481 | + src = S3Source(bucket="my-bucket", prefix="videos/", region="us-east-1")
|
| 482 | + assert src.bucket == "my-bucket"
|
| 483 | + assert src.prefix == "videos/"
|
| 484 | + assert src.region == "us-east-1"
|
| 485 | +
|
| 486 | + def test_authenticate_success(self):
|
| 487 | + from video_processor.sources.s3_source import S3Source
|
| 488 | +
|
| 489 | + mock_client = MagicMock()
|
| 490 | + mock_client.head_bucket.return_value = {}
|
| 491 | + mock_boto3 = MagicMock()
|
| 492 | + mock_boto3.client.return_value = mock_client
|
| 493 | +
|
| 494 | + with patch.dict("sys.modules", {"boto3": mock_boto3}):
|
| 495 | + src = S3Source(bucket="my-bucket")
|
| 496 | + assert src.authenticate() is True
|
| 497 | +
|
| 498 | + def test_authenticate_failure(self):
|
| 499 | + from video_processor.sources.s3_source import S3Source
|
| 500 | +
|
| 501 | + mock_client = MagicMock()
|
| 502 | + mock_client.head_bucket.side_effect = Exception("Access Denied")
|
| 503 | + mock_boto3 = MagicMock()
|
| 504 | + mock_boto3.client.return_value = mock_client
|
| 505 | +
|
| 506 | + with patch.dict("sys.modules", {"boto3": mock_boto3}):
|
| 507 | + src = S3Source(bucket="bad-bucket")
|
| 508 | + assert src.authenticate() is False
|
| 509 | +
|
| 510 | + def test_list_videos(self):
|
| 511 | + from video_processor.sources.s3_source import S3Source
|
| 512 | +
|
| 513 | + mock_client = MagicMock()
|
| 514 | + mock_client.head_bucket.return_value = {}
|
| 515 | + paginator = MagicMock()
|
| 516 | + mock_client.get_paginator.return_value = paginator
|
| 517 | + paginator.paginate.return_value = [
|
| 518 | + {
|
| 519 | + "Contents": [
|
| 520 | + {"Key": "videos/clip.mp4", "Size": 5000},
|
| 521 | + {"Key": "videos/notes.txt", "Size": 100},
|
| 522 | + {"Key": "videos/movie.mkv", "Size": 90000},
|
| 523 | + ]
|
| 524 | + }
|
| 525 | + ]
|
| 526 | + mock_boto3 = MagicMock()
|
| 527 | + mock_boto3.client.return_value = mock_client
|
| 528 | +
|
| 529 | + with patch.dict("sys.modules", {"boto3": mock_boto3}):
|
| 530 | + src = S3Source(bucket="my-bucket")
|
| 531 | + src.authenticate()
|
| 532 | + files = src.list_videos()
|
| 533 | + assert isinstance(files, list)
|
| 534 | + # Only .mp4 and .mkv are video extensions
|
| 535 | + assert len(files) == 2
|
| 536 | + names = [f.name for f in files]
|
| 537 | + assert "clip.mp4" in names
|
| 538 | + a |